如何基于数据库字段动态配置Spring Security的未认证用户访问权限?
问题描述
我有一个使用Spring Security的Spring应用,部分REST接口当前允许未认证用户访问。希望实现:当数据库表中的is_enable字段值为true时,这些接口仅对已认证用户开放;当该字段值为false时,恢复为所有人可访问。
最初的想法是创建一个@Service类,用于检查数据库的is_enable字段值以及用户是否已认证,当is_enable为true且用户未认证时抛出异常。但觉得这并非最优方案,且手动在每个REST控制器方法中调用该检查方法的体验不佳。
请问是否可以将该检查逻辑集成到WebSecurityConfiguration中?我的WebSecurityConfiguration配置代码如下:
override fun configure(http: HttpSecurity) { http.httpBasic().disable() .formLogin().disable() .csrf().disable() .authorizeRequests { request -> request .requestMatchers( EndpointRequest.toAnyEndpoint(), AntPathRequestMatcher("/v1/chat", HttpMethod.POST.name), AntPathRequestMatcher("/v1/chat/**", HttpMethod.GET.name), ) .permitAll() .anyRequest().authenticated() } .oauth2ResourceServer() .jwt().decoder(jwtDecoder).jwtAuthenticationConverter(JwtAuthenticationVtbConverter()) }
解决方案
当然可以把这个逻辑集成到Spring Security的配置里,无需在每个控制器手动调用。以下是两种可行的实现方案:
方案一:自定义SpEL权限表达式
利用Spring Security支持的自定义SpEL表达式,结合数据库状态和用户认证情况实现动态控制:
- 实现获取数据库配置的服务类(添加缓存避免频繁查库):
@Service class SecurityConfigService { @Autowired private lateinit var yourRepository: YourRepository // 用@Cacheable缓存结果,可设置过期时间保证实时性 @Cacheable(value = ["securityConfig"], key = "'isAccessRestricted'") fun isAccessRestricted(): Boolean { // 替换为实际查询数据库is_enable字段的逻辑 return yourRepository.findSystemConfig().isEnable } }
- 注册自定义表达式处理器:
@Configuration class MethodSecurityConfig : GlobalMethodSecurityConfiguration() { @Autowired private lateinit var securityConfigService: SecurityConfigService override fun createExpressionHandler(): MethodSecurityExpressionHandler { val handler = DefaultMethodSecurityExpressionHandler() handler.setRootObjectPostProcessor { root -> val securityRoot = root as DefaultSecurityExpressionRoot // 注入自定义表达式方法 securityRoot.set("accessRestricted", securityConfigService.isAccessRestricted()) securityRoot } return handler } }
- 修改WebSecurity配置,用表达式替代
permitAll():
override fun configure(http: HttpSecurity) { http.httpBasic().disable() .formLogin().disable() .csrf().disable() .authorizeRequests { request -> request .requestMatchers( EndpointRequest.toAnyEndpoint(), AntPathRequestMatcher("/v1/chat", HttpMethod.POST.name), AntPathRequestMatcher("/v1/chat/**", HttpMethod.GET.name), ) .access("!accessRestricted() || isAuthenticated()") .anyRequest().authenticated() } .oauth2ResourceServer() .jwt().decoder(jwtDecoder).jwtAuthenticationConverter(JwtAuthenticationVtbConverter()) }
表达式逻辑:当is_enable为false(即!accessRestricted())时允许所有人访问;当is_enable为true时,仅允许已认证用户(isAuthenticated())访问。
方案二:自定义AccessDecisionVoter
如果需要更复杂的权限判断逻辑,可以自定义投票器参与Spring Security的权限决策流程:
- 实现自定义投票器:
@Component class DynamicAccessVoter : AccessDecisionVoter<FilterInvocation> { @Autowired private lateinit var securityConfigService: SecurityConfigService override fun supports(configAttribute: ConfigAttribute): Boolean { // 匹配自定义权限标识 return configAttribute.attribute == "DYNAMIC_ACCESS" } override fun supports(clazz: Class<*>): Boolean { return FilterInvocation::class.java.isAssignableFrom(clazz) } override fun vote(authentication: Authentication?, filterInvocation: FilterInvocation, configAttributes: MutableCollection<ConfigAttribute>): Int { val isRestricted = securityConfigService.isAccessRestricted() if (!isRestricted) { // 不限制,直接允许访问 return ACCESS_GRANTED } // 限制时检查用户是否已认证(排除匿名用户) return if (authentication != null && authentication.isAuthenticated && authentication.principal != "anonymousUser") { ACCESS_GRANTED } else { ACCESS_DENIED } } }
- 修改WebSecurity配置,添加自定义投票器并使用自定义权限标识:
@Autowired private lateinit var dynamicAccessVoter: DynamicAccessVoter override fun configure(http: HttpSecurity) { http.httpBasic().disable() .formLogin().disable() .csrf().disable() .authorizeRequests { request -> request .requestMatchers( EndpointRequest.toAnyEndpoint(), AntPathRequestMatcher("/v1/chat", HttpMethod.POST.name), AntPathRequestMatcher("/v1/chat/**", HttpMethod.GET.name), ) .hasAuthority("DYNAMIC_ACCESS") .anyRequest().authenticated() } .oauth2ResourceServer() .jwt().decoder(jwtDecoder).jwtAuthenticationConverter(JwtAuthenticationVtbConverter()) .and() .accessDecisionManager(UnanimousBased(listOf(dynamicAccessVoter))) }
关键注意点
- 缓存优化:必须给
isAccessRestricted()方法添加缓存,否则每次请求都查询数据库会严重降低系统性能。可以通过Spring Cache的@Cacheable实现,同时设置合理的过期时间平衡实时性和性能。 - 实时生效:如果需要
is_enable字段修改后立即生效,可以提供一个手动刷新缓存的接口,或者缩短缓存过期时间。
内容的提问来源于stack exchange,提问作者NeverSleeps
相关产品推荐
相关产品推荐

