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);

}
  1. 首先之所以会打印Invalid CSRF token found for xxx ,是因为满足了if (!equalsConstantTime(csrfToken.getToken(), actualToken))的条件,随后检查发现csrfToken.getToken()的值正常,为Cookie里的XSRF-TOKEN。 异常的是actualToken 为空。

  2. 所以进入上一行String actualToken = this.requestHandler.resolveCsrfTokenValue(request, csrfToken);resolveCsrfTokenValue 方法中检查。这里注意的是,在默认配置下,这里的requesthandlerXorCsrfTokenRequestAttributeHandler ,所以需要进入到这个类查看该方法的实现。

    	@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,通过解决问题来学习。

posted @   allworldg  阅读(21)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek-R1本地部署如何选择适合你的版本?看这里
· 开源的 DeepSeek-R1「GitHub 热点速览」
· 传国玉玺易主,ai.com竟然跳转到国产AI
· 揭秘 Sdcb Chats 如何解析 DeepSeek-R1 思维链
· 自己如何在本地电脑从零搭建DeepSeek!手把手教学,快来看看! (建议收藏)
点击右上角即可分享
微信分享提示