SpringSecurity CSRF传入正确相同的token无法登陆
前因
当我根据https://spring.io/guides/tutorials/spring-boot-oauth2 教程去实现一个oauth2demo时,点击logout始终无法成功登出,报错403,但是我检查request-header中x-xsrf-token和cookie中的XSRF-TOKEN的值相同。 https://stackoverflow.com/questions/74447118/csrf-protection-not-working-with-spring-security-6最后在这个回答中得到了解决办法。
简单总结:在Spring Security 5.8及更高版本中,默认使用XorCsrfTokenRequestAttributeHandler
匹配token,这就需要前端传入的token不能是raw token,具体解决可以参考这个文档:https://docs.spring.io/spring-security/reference/servlet/exploits/csrf.html#csrf-integration-javascript
调试过程
如何找到该问题的源头:在application.yml
中增加:
logging:
level:
org.springframework: trace
debug时点击logout按钮,会发现控制台报错: o.s.security.web.csrf.CsrfFilter: Invalid CSRF token found for http://localhost:8080/logout
。所以我就开始一步步在CsrfFilter
里进行调试。
根据报错信息,可以很直接的找到关键的方法为
public final class CsrfFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
.....
CsrfToken csrfToken = deferredCsrfToken.get();
String actualToken = this.requestHandler.resolveCsrfTokenValue(request, csrfToken);
if (!equalsConstantTime(csrfToken.getToken(), actualToken)) {
boolean missingToken = deferredCsrfToken.isGenerated();
this.logger
.debug(LogMessage.of(() -> "Invalid CSRF token found for " + UrlUtils.buildFullRequestUrl(request)));
AccessDeniedException exception = (!missingToken) ? new InvalidCsrfTokenException(csrfToken, actualToken)
: new MissingCsrfTokenException(actualToken);
this.accessDeniedHandler.handle(request, response, exception);
return;
}
filterChain.doFilter(request, response);
}
-
首先之所以会打印
Invalid CSRF token found for xxx
,是因为满足了if (!equalsConstantTime(csrfToken.getToken(), actualToken))
的条件,随后检查发现csrfToken.getToken()
的值正常,为Cookie里的XSRF-TOKEN。 异常的是actualToken
为空。 -
所以进入上一行
String actualToken = this.requestHandler.resolveCsrfTokenValue(request, csrfToken);
的resolveCsrfTokenValue
方法中检查。这里注意的是,在默认配置下,这里的requesthandler
为XorCsrfTokenRequestAttributeHandler
,所以需要进入到这个类查看该方法的实现。@Override public String resolveCsrfTokenValue(HttpServletRequest request, CsrfToken csrfToken) { String actualToken = super.resolveCsrfTokenValue(request, csrfToken); return getTokenValue(actualToken, csrfToken.getToken()); }
第一行
super.resolveCsrfTokenValue(request, csrfToken);
调用的是父类方法,即获取request header中X-XSRF-TOKEN
的值,此处正常。第二行getTokenValue(actualToken, csrfToken.getToken())
是将Cookie提取的token值和request header的值进行比较。进入该方法进行调试可发现actualToken
为空的原因为满足了以下条件:private static String getTokenValue(String actualToken, String token) { ...... if (actualBytes.length != tokenSize * 2) { return null; } .... }
解释
无论是看我开头提到总结的或是观察CsrfFilter
代码,会发现默认使用 XorCsrfTokenRequestAttributeHandler
,比较时会先进行处理(问了ai,该handler期望header传入的token的格式应该为Base64(随机字节+(TOKEN ⊕ 随机字节))
,所以才会有两倍长度比较的条件。而我们根据例子传入的普通原token。所以不能成功)。
解决
具体解决可以参考这个文档:https://docs.spring.io/spring-security/reference/servlet/exploits/csrf.html#csrf-integration-javascript,简单来说header传入的token使用CsrfTokenRequestAttributeHandler
处理即可。
额外提一下,解决方法中.csrfTokenRequestHandler(new SpaCsrfTokenRequestHandler())
这里注册自定义handler实际上是替换掉了默认的XorCsrfTokenRequestAttributeHandler
,具体实现是在CsrfConfigurer.configure(H http)
。
题外话
写这类的内容时,实在是很难把控内容的精细程度。一方面是写给我自己回顾,一方面也是希望能帮助到碰到相同问题的人。如果写的过细,以我自己的心性来说,我肯定是没耐心读的。如果写的太粗糙,那么又无法解决问题。所以我是以我实际如何发现,调试解决这个问题的流程进行记录的,而不是对相关代码从头到尾进行讲解。
这也让我想到另一个很常见的问题,即很多人提问如何提升自己时,说到相关优秀框架的源码读不进去。这段相同的CsrfFilter
代码,如果我没碰到这个问题,而是直接开始阅读,我肯定没耐心去调试,去搞懂很多细节。可能还是得多去尝试直接写一些demo,通过解决问题来学习。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek-R1本地部署如何选择适合你的版本?看这里
· 开源的 DeepSeek-R1「GitHub 热点速览」
· 传国玉玺易主,ai.com竟然跳转到国产AI
· 揭秘 Sdcb Chats 如何解析 DeepSeek-R1 思维链
· 自己如何在本地电脑从零搭建DeepSeek!手把手教学,快来看看! (建议收藏)