又又又出问题了:
出安全漏洞了!!!!
稳定复现。
项目的身份认证是基于SpringSecurity的。难道是SpringSecurity出bug了?我不相信有人会特意花时间花精力去配置一个让druid页面未授权也可访问的规则,这不是多余嘛。现在哪个云数据库上没有慢SQL之类的监听,完全不需要使用druid自带的这个能力。
去项目确认下,的确没有关于“/druid/api.html”或“/druid/**”的规则。
难道是SpringSecurity出bug了?
查一查。
SpringSecurity比较庞大,从哪查起呢?断点打哪呢?
再补充点SpringSecurity的知识,方便理解:
安全框架有两个重要的概念,即认证(Authentication)和授权(Authorization)。 认证即确认主体可以访问当前系统的过程; 授权即确定主体通过认证后,检查在当前系统下所拥有的功能权限的过程。 这里的主体既可以是登录系统的用户,也可以是接入的设备或其它系统。 Spring Security会根据请求的URI的路径来确定该请求的过滤器链(Filter)以及最终的具体Servlet控制器(Controller)。 JackieTang,公众号:生活点亮技术Spring Security OAuth2是如何校验token的
从上图可以看到,Spring Security以一个单Filter(FilterChainProxy)存在于整个过滤链路中。这个FilterChainProxy代理着众多的Spring Security Filter。
再站在Tomcat(Web容器)的视角,来观察下一个请求从开始到结果都与哪些组件有交互,哪些先,哪些后。
容量评估实践:一个Tomcat最多能同时处理多少个HTTP请求?
通过上面的信息是不是可以有这样一个概念:
在基于SpringBoot的项目中,一个完整的网页请求,一定要与Filter组件进行交互!!!
SpringSecurity默认有12个Filter,
最终授权决策是在最后一个FilterSecurityInterceptor中。
SecurityMetadataSource
ConfigAttribute
),如hasRole('ADMIN')
、permitAll()
等。FilterSecurityInterceptor
会通过SecurityMetadataSource
获取请求对应的授权规则。DefaultFilterInvocationSecurityMetadataSource
是其常见实现类,它会将配置的路径和对应的授权规则进行映射存储,当请求到来时,根据请求路径匹配并返回相应的授权属性。FilterSecurityInterceptor
SecurityMetadataSource
),然后结合用户的认证信息,调用AccessDecisionManager
进行授权决策,判断用户是否有权限访问请求的资源。FilterSecurityInterceptor
会对其进行授权检查,决定是否允许该请求继续访问目标资源。那就在FilterSecurityInterceptor类的这行代码中打断点
InterceptorStatusToken token = super.beforeInvocation(fi);
org.springframework.security.web.access.intercept.FilterSecurityInterceptor#invoke
来看下Debug结果:
/druid/api.html命中了/**/*.html这条规则,获取了免认证授权。
logger.debug("Authorization successful");了
“/**/*.html”这条规则是哪来的,没看到呢?
去项目找了下,果然还有一个。一个项目配置了两个
原因找到了:
是因为配置了“/**/*.html”这一系统授权规则,然后这个http://127.0.0.1:8080/druid/api.html不需要授权就可以正常访问。
解决办法就很简单了:这条规则删除掉。重启服务,这个页面就访问时就需要认证信息了。
另外,可以把Druid的这个特性关掉。
spring.datasource.druid.stat-view-servlet.enabled=false
现在的服务一般都是高可用的,至少有两个服务。但/druid/api.html页面是没有提供分布式Session管理的,如果是使用Token还能用,如果是使用Session完全用不了。为什么用不了?想一想,可以留言交流。
小结
遇到这类问题怎么办?先查规则配置文件,如果没有找到答案。
可以这样这样来排查:
@Order
注解来调整配置类的加载顺序,@Order
的值越小,优先级越高。HttpSecurity
进行配置外,还可能通过 AuthorizeConfigProvider
进行配置。不同方式配置的规则可能会相互覆盖。AuthorizeConfigProvider
,检查每个 AuthorizeConfigProvider
的 configure
方法,确认规则的设置是否正确。application.properties
或 application.yml
)和 Java 代码进行 Spring Security 配置,可能会出现配置冲突的情况。antMatchers
、requestMatchers
等方法,查看所有对路径进行配置的地方,确认是否存在冲突。SecurityMetadataSource
的配置结果。application.properties
或 application.yml
)中启用 Spring Security 的 DEBUG 日志,以便查看配置加载和规则应用的详细过程。
logging.level.org.springframework.security=DEBUG
通过查看日志,可以了解到配置是如何被加载和应用的,从而发现问题所在。FilterSecurityInterceptor
类的 beforeInvocation
方法中设置断点,查看 SecurityMetadataSource
是如何获取授权配置属性的。在断点处,可以查看 obtainSecurityMetadataSource().getAttributes(object)
方法返回的结果,以及 SecurityMetadataSource
内部的存储结构,进一步分析配置错误的原因。通过以上步骤,基本可以定位到 SecurityMetadataSource
中授权规则配置异常的原因,并进行相应的修复
1、在排查原因的过程中发现用到@EnableWebSecurity,但SpringSecurity也可以正常工作,这是什么原因呢?
因为引入了组件spring-boot-starter-security 。
spring-boot-starter-security
引入 SecurityAutoConfiguration
,通过 @Import
触发 WebSecurityEnablerConfiguration
。@EnableWebSecurity
的隐式加载WebSecurityEnablerConfiguration
通过 @EnableWebSecurity
导入 WebSecurityConfiguration
。refresh()
阶段,@Configuration
类被解析并创建 Bean。Spring Boot自动配置通过SecurityAutoConfiguration隐式启用安全框架,无需显式注解,但显式添加可定制配置。
2、SpringSecurity各个组件是如何交互的
在 Spring Security 中,各组件通过精心设计的流程和交互机制协同工作,形成一个完整的安全防护体系。以下是它们的协作流程,以一个典型的 HTTP 请求为例进行说明:
FilterChainProxy
所有请求首先进入 Spring Security 的核心过滤器 FilterChainProxy
,它是安全过滤器链的入口点。SecurityFilterChain
FilterChainProxy
根据请求的 URL、HTTP 方法等信息,匹配到对应的 SecurityFilterChain
。例如:
http
.authorizeRequests()
.antMatchers("/api/**").authenticated()// 匹配 /api/ 开头的请求
.anyRequest().permitAll();// 其他请求允许访问
SecurityContextPersistenceFilter
SecurityContext
(包含用户认证信息)。SecurityContext
。UsernamePasswordAuthenticationFilter
拦截 /login
请求,提取用户名和密码,创建 UsernamePasswordAuthenticationToken
。OAuth2LoginAuthenticationFilter
处理 OAuth2 回调,获取访问令牌并完成用户认证。JwtAuthenticationToken
。AuthenticationManager
验证凭据
认证过滤器将 Authentication
对象(如 UsernamePasswordAuthenticationToken
)传递给 AuthenticationManager。
Authentication authentication = authenticationManager.authenticate(token);
AuthenticationProvider
执行验证
AuthenticationManager
(通常是 ProviderManager
)委托 AuthenticationProvider
验证凭据:DaoAuthenticationProvider
从 UserDetailsService
加载用户信息,比对密码。OAuth2UserService
处理 OAuth2 用户信息加载。AuthenticationManager
返回一个已认证的 Authentication
对象,过滤器将其存储到 SecurityContextHolder 中:
SecurityContextHolder.getContext().setAuthentication(authentication);FilterSecurityInterceptor
获取授权规则
当请求到达需要授权的资源时,FilterSecurityInterceptor
从 SecurityMetadataSource
获取该请求的授权配置(如 hasRole('ADMIN')
)。AccessDecisionManager
执行投票
FilterSecurityInterceptor
将 Authentication
、请求对象和授权配置传递给 AccessDecisionManager
,后者组织 AccessDecisionVoter
进行投票:
accessDecisionManager.decide(authentication, request, configAttributes);
AccessDecisionVoter
投票
不同的投票者根据授权配置进行投票:RoleVoter
检查用户是否拥有指定角色。WebExpressionVoter
计算表达式(如 #oauth2.throwOnError(authenticated)
)。AccessDeniedException
,由 ExceptionTranslationFilter
处理。ExceptionTranslationFilter
捕获异常AuthenticationException
)重定向到登录页(表单登录)或返回 401(REST API)。AccessDeniedException
)返回 403 错误或重定向到拒绝访问页面。SecurityContextPersistenceFilter
将 SecurityContext
存储到 Session 中(如果启用了 Session),并清理线程上下文。阶段 | 核心组件 | 协作逻辑 |
---|---|---|
请求分发 | FilterChainProxy、SecurityFilterChain | 根据请求匹配安全规则链 |
认证 | AuthenticationFilter、AuthenticationManager、AuthenticationProvider | 提取凭据 → 验证 → 存储认证信息 |
授权 | FilterSecurityInterceptor、SecurityMetadataSource、AccessDecisionManager、AccessDecisionVoter | 获取规则 → 投票决策 → 允许 / 拒绝访问 |
异常处理 | ExceptionTranslationFilter | 将安全异常转换为 HTTP 响应(如 401、403) |
会话管理 | SecurityContextPersistenceFilter | 存储 / 恢复 SecurityContext |
这种分层、模块化的设计使得 Spring Security 既强大又灵活,能够适应各种复杂的安全需求。
最后以一个大图结束:
但凡问题,皆有原因。
只是有些原因,你之前没有想到而已。