You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何基于数据库字段动态配置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表达式,结合数据库状态和用户认证情况实现动态控制:

  1. 实现获取数据库配置的服务类(添加缓存避免频繁查库):
@Service
class SecurityConfigService {
    @Autowired
    private lateinit var yourRepository: YourRepository

    // 用@Cacheable缓存结果,可设置过期时间保证实时性
    @Cacheable(value = ["securityConfig"], key = "'isAccessRestricted'")
    fun isAccessRestricted(): Boolean {
        // 替换为实际查询数据库is_enable字段的逻辑
        return yourRepository.findSystemConfig().isEnable
    }
}
  1. 注册自定义表达式处理器:
@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
    }
}
  1. 修改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的权限决策流程:

  1. 实现自定义投票器:
@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
        }
    }
}
  1. 修改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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.17 09:52:33