SpringSecurity的防Csrf攻击实现代码解析
CSRF(Cross-siterequestforgery)跨站请求伪造,也被称为OneClickAttack或者SessionRiding,通常缩写为CSRF或XSRF,是一种对网站的恶意利用。尽管听起来像跨站脚本(XSS),但它与XSS非常不同,XSS利用站点内的信任用户,而CSRF则通过伪装成受信任用户的请求来利用受信任的网站。与XSS攻击相比,CSRF攻击往往不大流行(因此对其进行防范的资源也相当稀少)和难以防范,所以被认为比XSS更具危险性。
CSRF是一种依赖web浏览器的、被混淆过的代理人攻击(deputyattack)。
如何防御
使用POST请求时,确实避免了如img、script、iframe等标签自动发起GET请求的问题,但这并不能杜绝CSRF攻击的发生。一些恶意网站会通过表单的形式构造攻击请求
publicfinalclassCsrfFilterextendsOncePerRequestFilter{
publicstaticfinalRequestMatcherDEFAULT_CSRF_MATCHER=new
CsrfFilter.DefaultRequiresCsrfMatcher();
privatefinalLoglogger=LogFactory.getLog(this.getClass());
privatefinalCsrfTokenRepositorytokenRepository;
privateRequestMatcherrequireCsrfProtectionMatcher;
privateAccessDeniedHandleraccessDeniedHandler;
publicCsrfFilter(CsrfTokenRepositorycsrfTokenRepository){
this.requireCsrfProtectionMatcher=DEFAULT_CSRF_MATCHER;
this.accessDeniedHandler=newAccessDeniedHandlerImpl();
Assert.notNull(csrfTokenRepository,"csrfTokenRepositorycannotbenull");
this.tokenRepository=csrfTokenRepository;
}
//通过这里可以看出SpringSecurity的csrf机制把请求方式分成两类来处理
protectedvoiddoFilterInternal(HttpServletRequestrequest,HttpServletResponseresponse,
FilterChainfilterChain)throwsServletException,IOException{
request.setAttribute(HttpServletResponse.class.getName(),response);
CsrfTokencsrfToken=this.tokenRepository.loadToken(request);
booleanmissingToken=csrfToken==null;
if(missingToken){
csrfToken=this.tokenRepository.generateToken(request);
this.tokenRepository.saveToken(csrfToken,request,response);
}
request.setAttribute(CsrfToken.class.getName(),csrfToken);
request.setAttribute(csrfToken.getParameterName(),csrfToken);
//第一类:"GET","HEAD","TRACE","OPTIONS"四类请求可以直接通过
if(!this.requireCsrfProtectionMatcher.matches(request)){
filterChain.doFilter(request,response);
}else{
//第二类:除去上面四类,包括POST都要被验证携带token才能通过
StringactualToken=request.getHeader(csrfToken.getHeaderName());
if(actualToken==null){
actualToken=request.getParameter(csrfToken.getParameterName());
}
if(!csrfToken.getToken().equals(actualToken)){
if(this.logger.isDebugEnabled()){
this.logger.debug("InvalidCSRFtokenfoundfor"+
UrlUtils.buildFullRequestUrl(request));
}
if(missingToken){
this.accessDeniedHandler.handle(request,response,new
MissingCsrfTokenException(actualToken));
}else{
this.accessDeniedHandler.handle(request,response,new
InvalidCsrfTokenException(csrfToken,actualToken));
}
}else{
filterChain.doFilter(request,response);
}
}
}
publicvoidsetRequireCsrfProtectionMatcher(RequestMatcherrequireCsrfProtectionMatcher){
Assert.notNull(requireCsrfProtectionMatcher,"requireCsrfProtectionMatchercannotbe
null");
this.requireCsrfProtectionMatcher=requireCsrfProtectionMatcher;
}
publicvoidsetAccessDeniedHandler(AccessDeniedHandleraccessDeniedHandler){
Assert.notNull(accessDeniedHandler,"accessDeniedHandlercannotbenull");
this.accessDeniedHandler=accessDeniedHandler;
}
privatestaticfinalclassDefaultRequiresCsrfMatcherimplementsRequestMatcher{
privatefinalHashSetallowedMethods;
privateDefaultRequiresCsrfMatcher(){
this.allowedMethods=newHashSet(Arrays.asList("GET","HEAD","TRACE","OPTIONS"));
}
publicbooleanmatches(HttpServletRequestrequest){
return!this.allowedMethods.contains(request.getMethod());
}
}
}
禁用Csrf
@EnableWebSecurity
publicclassWebSecurityConfigextends
WebSecurityConfigurerAdapter{
@Override
protectedvoidconfigure(HttpSecurityhttp)throwsException{
http
//关闭打开的csrf保护
.csrf().disable();
}
}
用户登录时,系统发放一个CsrfToken值,用户携带该CsrfToken值与用户名、密码等参数完成登录。系统记录该会话的CsrfToken值,之后在用户的任何请求中,都必须带上该CsrfToken值,并由系统进行校验。
这种方法需要与前端配合,包括存储CsrfToken值,以及在任何请求中(包括表单和Ajax)携带CsrfToken值。安全性相较于HTTPReferer提高很多,如果都是XMLHttpRequest,则可以统一添加CsrfToken值;但如果存在大量的表单和a标签,就会变得非常烦琐。
SpringSecurity中使用CsrfToken
SpringSecurity通过注册一个CsrfFilter来专门处理CSRF攻击,在SpringSecurity中,CsrfToken是一个用于描述Token值,以及验证时应当获取哪个请求参数或请求头字段的接口
publicinterfaceCsrfTokenextendsSerializable{
StringgetHeaderName();
StringgetParameterName();
StringgetToken();
}
//CsrfTokenRepository则定义了如何生成、保存以及加载CsrfToken。
publicinterfaceCsrfTokenRepository{
CsrfTokengenerateToken(HttpServletRequestrequest);
voidsaveToken(CsrfTokentoken,HttpServletRequestrequest,
HttpServletResponseresponse);
CsrfTokenloadToken(HttpServletRequestrequest);
}
HttpSessionCsrfTokenRepository
在默认情况下,SpringSecurity加载的是一个HttpSessionCsrfTokenRepository
HttpSessionCsrfTokenRepository将CsrfToken值存储在HttpSession中,并指定前端把CsrfToken值放在名为“_csrf”的请求参数或名为“X-CSRF-TOKEN”的请求头字段里(可以调用相应的设置方法来重新设定)。校验时,通过对比HttpSession内存储的CsrfToken值与前端携带的CsrfToken值是否一致,便能断定本次请求是否为CSRF攻击。
这种方式在某些单页应用中局限性比较大,灵活性不足。
CookieCsrfTokenRepository
SpringSecurity还提供了另一种方式,即CookieCsrfTokenRepository
CookieCsrfTokenRepository是一种更加灵活可行的方案,它将CsrfToken值存储在用户的cookie内。减少了服务器HttpSession存储的内存消耗,并且当用cookie存储CsrfToken值时,前端可以用JavaScript读取(需要设置该cookie的httpOnly属性为false),而不需要服务器注入参数,在使用方式上更加灵活。
存储在cookie中是不可以被CSRF利用的,cookie只有在同域的情况下才能被读取,所以杜绝了第三方站点跨域获取CsrfToken值的可能。CSRF攻击本身是不知道cookie内容的,只是利用了当请求自动携带cookie时可以通过身份验证的漏洞。但服务器对CsrfToken值的校验并非取自cookie,而是需要前端手动将CsrfToken值作为参数携带在请求里
下面是csrfFilter的过滤过程
@Override
protectedvoiddoFilterInternal(HttpServletRequestrequest,
HttpServletResponseresponse,FilterChainfilterChain)
throwsServletException,IOException{
request.setAttribute(HttpServletResponse.class.getName(),response);
//获取到cookie中的csrfToken(CookieTokenRepository)或者从session中获取(HttpSessionCsrfTokenRepository)
CsrfTokencsrfToken=this.tokenRepository.loadToken(request);
finalbooleanmissingToken=csrfToken==null;
//加载不到,则证明请求是首次发起的,应该生成并保存一个新的CsrfToken值
if(missingToken){
csrfToken=this.tokenRepository.generateToken(request);
this.tokenRepository.saveToken(csrfToken,request,response);
}
request.setAttribute(CsrfToken.class.getName(),csrfToken);
request.setAttribute(csrfToken.getParameterName(),csrfToken);
//排除部分不需要验证CSRF攻击的请求方法(默认忽略了GET、HEAD、TRACE和OPTIONS)
if(!this.requireCsrfProtectionMatcher.matches(request)){
filterChain.doFilter(request,response);
return;
}
//实际的token从header或者parameter中获取
StringactualToken=request.getHeader(csrfToken.getHeaderName());
if(actualToken==null){
actualToken=request.getParameter(csrfToken.getParameterName());
}
if(!csrfToken.getToken().equals(actualToken)){
if(this.logger.isDebugEnabled()){
this.logger.debug("InvalidCSRFtokenfoundfor"
+UrlUtils.buildFullRequestUrl(request));
}
if(missingToken){
this.accessDeniedHandler.handle(request,response,
newMissingCsrfTokenException(actualToken));
}
else{
this.accessDeniedHandler.handle(request,response,
newInvalidCsrfTokenException(csrfToken,actualToken));
}
return;
}
filterChain.doFilter(request,response);
}
用户想要坚持CSRFToken在cookie中。默认情况下CookieCsrfTokenRepository将编写一个名为XSRF-TOKEN的cookie和从头部命名X-XSRF-TOKEN中读取或HTTP参数_csrf。
//代码如下: .and().csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
我们在日常使用中,可以采用header或者param的方式添加csrf_token,下面示范从cookie中获取token
Signintocontinue