1. 从403错误认识AccessDeniedException
遇到SpringSecurity的403错误就像在机场被安检拦下——明明带了登机牌(Token),却被拒绝进入候机厅。最近在调试OAuth2登录接口时就踩到这个坑:前端带着有效Token请求/oauth/token接口,明明配置了permitAll,却收到"AccessDeniedException: Access is denied"的红色警告。
这个问题背后藏着SpringSecurity的三重安检机制:
- 配置层:HttpSecurity的antMatchers配置像机场的准入名单
- 决策层:AbstractAccessDecisionManager如同安检主管,协调多个安检员(Voter)投票
- 执行层:WebExpressionVoter等具体实现才是真正检查登机牌的安检员
我曾在项目中配置了这样的白名单:
http.authorizeRequests()
.antMatchers("/oauth/**").permitAll()
.anyRequest().authenticated();
理论上所有访问/oauth/**路径的请求都应该放行,但实际带着Token访问时却触发403。这就像VIP通道写着"所有人可进",但实际却检查邀请函——矛盾的规则导致了系统行为异常。
2. 决策投票器的暗箱操作
2.1 AffirmativeBased的民主投票
当请求触发安全拦截时,AbstractAccessDecisionManager的子类AffirmativeBased会组织投票。它的工作流程就像议会表决:
- 遍历所有Voter实现(议员)
- 收集每个Voter的投票结果(1通过/0弃权/-1拒绝)
- 只要有一个-1就立即抛出AccessDeniedException
关键源码片段如下:
public void decide(Authentication authentication, Object object,
Collection<ConfigAttribute> configAttributes) {
int deny = 0;
for (AccessDecisionVoter voter : getDecisionVoters()) {
int result = voter.vote(authentication, object, configAttributes);
if (result == -1) {
throw new AccessDeniedException("Access is denied");
}
}
}
2.2 WebExpressionVoter的严格执法
众多Voter中最容易"误判"的就是WebExpressionVoter。它就像恪守教条的安检员,会严格执行以下逻辑:
- 检查请求是否匹配配置的表达式(如permitAll)
- 如果Authentication不是匿名用户(即携带Token),则忽略permitAll
- 强制要求进行完整的权限校验
这解释了为什么匿名访问正常而带Token访问失败。就像机场的快速通道,对普通旅客开放,但VIP旅客反而需要额外核验。
3. 权限链的完整工作流程
3.1 从请求到异常的完整路径
一个携带Token的请求会经历这样的安检流程:
- 请求拦截:FilterSecurityInterceptor捕获请求,类似机场入口的闸机
- 身份识别:从SecurityContextHolder获取Authentication对象,如同扫描登机牌
- 权限提取:读取配置的ConfigAttribute,类似查询航班权限列表
- 投票决策:多个Voter进行投票,像不同部门的安检程序
- 异常抛出:任一Voter返回-1立即触发AccessDeniedException
3.2 典型冲突场景分析
在OAuth2密码模式中常见的死锁场景:
- 前端首次登录不带Token → 正常通过
- 登录后存储Token → 后续所有请求自动携带
- 用户点击"刷新"再次触发登录接口 → 带Token请求被拒
解决方案是修改前端拦截器逻辑:
service.interceptors.request.use(config => {
// 排除登录接口的Token携带
if (!config.url.includes('/oauth/token')) {
config.headers['Authorization'] = 'Bearer '+getToken()
}
return config
})
4. 实战调试技巧与解决方案
4.1 日志诊断三板斧
- 开启DEBUG日志:
logging.level.org.springframework.security=DEBUG
- 关键检查点:
- FilterSecurityInterceptor的"Secure object"日志
- AffirmativeBased的"Voter returned"记录
- Authentication对象的toString()输出
- 重点关注字段:
- Granted Authorities:查看实际权限列表
- Authenticated:认证状态是否为true
- Details:请求来源IP等辅助信息
4.2 配置优化方案
对于需要完全放行的接口,推荐双重配置:
http.authorizeRequests()
.antMatchers("/oauth/token")
.permitAll()
.access("permitAll()")
.anyRequest().authenticated();
这种写法同时满足:
- 配置层的antMatchers白名单
- 表达式层的permitAll强制声明
- 确保WebExpressionVoter不会误判
5. 深度源码追踪指南
想要真正理解权限控制,建议按这个顺序阅读源码:
- 起点:FilterSecurityInterceptor.doFilter()
- 核心流程:
- AbstractSecurityInterceptor.beforeInvocation()
- AffirmativeBased.decide()
- 关键实现:
- WebExpressionVoter.vote()
- SecurityExpressionRoot.hasAuthority()
重点观察Authentication对象在不同阶段的转换:
// 匿名用户状态
AnonymousAuthenticationToken@1234:
Principal: anonymousUser;
Authorities: ROLE_ANONYMOUS
// 认证后状态
UsernamePasswordAuthenticationToken@5678:
Principal: admin;
Authorities: [ROLE_ADMIN]
6. 避坑经验分享
在微服务架构中,我遇到过这些典型问题:
-
网关层与资源层的权限冲突
- 现象:网关放行但资源服务拒绝
- 解决:统一权限配置中心
-
自定义Voter的排序问题
- 现象:投票结果不符合预期
- 技巧:实现Ordered接口控制顺序
-
前后端分离的特殊场景
- 跨域预检请求被拦截
- 需要特殊处理OPTIONS方法:
.antMatchers(HttpMethod.OPTIONS).permitAll()
记住一个原则:SpringSecurity的权限控制就像洋葱,每层都可能成为障碍。调试时要逐层剥离,用日志和断点结合的方式,才能准确定位问题所在。

1620

被折叠的 条评论
为什么被折叠?



