1200字范文,内容丰富有趣,写作的好帮手!
1200字范文 > springsecurity sessionregistry session共享_要学就学透彻!Spring Security 中 CSRF 防御源码解析...

springsecurity sessionregistry session共享_要学就学透彻!Spring Security 中 CSRF 防御源码解析...

时间:2023-11-12 11:27:53

相关推荐

springsecurity sessionregistry session共享_要学就学透彻!Spring Security 中 CSRF 防御源码解析...

今日干货

刚刚发表查看:66666回复:666

公众号后台回复 ssm,免费获取松哥纯手敲的 SSM 框架学习干货。

上篇文章松哥和大家聊了什么是 CSRF 攻击,以及 CSRF 攻击要如何防御。主要和大家聊了 Spring Security 中处理该问题的几种办法。

今天松哥来和大家简单的看一下 Spring Security 中,CSRF 防御源码。

本文是本系列第 19 篇,阅读本系列前面文章有助于更好的理解本文:

挖一个大坑,Spring Security 开搞!松哥手把手带你入门 Spring Security,别再问密码怎么解密了手把手教你定制 Spring Security 中的表单登录Spring Security 做前后端分离,咱就别做页面跳转了!统统 JSON 交互Spring Security 中的授权操作原来这么简单Spring Security 如何将用户数据存入数据库?Spring Security+Spring Data Jpa 强强联手,安全管理只有更简单!Spring Boot + Spring Security 实现自动登录功能Spring Boot 自动登录,安全风险要怎么控制?在微服务项目中,Spring Security 比 Shiro 强在哪?SpringSecurity 自定义认证逻辑的两种方式(高级玩法)Spring Security 中如何快速查看登录用户 IP 地址等信息?Spring Security 自动踢掉前一个登录用户,一个配置搞定!Spring Boot + Vue 前后端分离项目,如何踢掉已登录用户?Spring Security 自带防火墙!你都不知道自己的系统有多安全!什么是会话固定攻击?Spring Boot 中要如何防御会话固定攻击?集群化部署,Spring Security 要如何处理 session 共享?松哥手把手教你在 SpringBoot 中防御 CSRF 攻击!so easy!

本文主要从两个方面来和大家讲解:

返回给前端的_csrf参数是如何生成的。前端传来的_csrf参数是如何校验的。

1.随机字符串生成

我们先来看一下 Spring Security 中的 csrf 参数是如何生成的。

首先,Spring Security 中提供了一个保存 csrf 参数的规范,就是 CsrfToken:

publicinterfaceCsrfTokenextendsSerializable{

StringgetHeaderName();

StringgetParameterName();

StringgetToken();

}

这里三个方法都好理解,前两个是获取_csrf参数的 key,第三个是获取_csrf参数的 value。

CsrfToken 有两个实现类,如下:

默认情况下使用的是 DefaultCsrfToken,我们来稍微看下 DefaultCsrfToken:

publicfinalclassDefaultCsrfTokenimplementsCsrfToken{

privatefinalStringtoken;

privatefinalStringparameterName;

privatefinalStringheaderName;

publicDefaultCsrfToken(StringheaderName,StringparameterName,Stringtoken){

this.headerName=headerName;

this.parameterName=parameterName;

this.token=token;

}

publicStringgetHeaderName(){

returnthis.headerName;

}

publicStringgetParameterName(){

returnthis.parameterName;

}

publicStringgetToken(){

returnthis.token;

}

}

这段实现很简单,几乎没有添加额外的方法,就是接口方法的实现。

CsrfToken 相当于就是_csrf参数的载体。那么参数是如何生成和保存的呢?这涉及到另外一个类:

publicinterfaceCsrfTokenRepository{

CsrfTokengenerateToken(HttpServletRequestrequest);

voidsaveToken(CsrfTokentoken,HttpServletRequestrequest,

HttpServletResponseresponse);

CsrfTokenloadToken(HttpServletRequestrequest);

}

这里三个方法:

generateToken 方法就是 CsrfToken 的生成过程。saveToken 方法就是保存 CsrfToken。loadToken 则是如何加载 CsrfToken。

CsrfTokenRepository 有四个实现类,在上篇文章中,我们用到了其中两个:HttpSessionCsrfTokenRepository 和 CookieCsrfTokenRepository,其中 HttpSessionCsrfTokenRepository 是默认的方案。

我们先来看下 HttpSessionCsrfTokenRepository 的实现:

publicfinalclassHttpSessionCsrfTokenRepositoryimplementsCsrfTokenRepository{

privatestaticfinalStringDEFAULT_CSRF_PARAMETER_NAME="_csrf";

privatestaticfinalStringDEFAULT_CSRF_HEADER_NAME="X-CSRF-TOKEN";

privatestaticfinalStringDEFAULT_CSRF_TOKEN_ATTR_NAME=HttpSessionCsrfTokenRepository.class

.getName().concat(".CSRF_TOKEN");

privateStringparameterName=DEFAULT_CSRF_PARAMETER_NAME;

privateStringheaderName=DEFAULT_CSRF_HEADER_NAME;

privateStringsessionAttributeName=DEFAULT_CSRF_TOKEN_ATTR_NAME;

publicvoidsaveToken(CsrfTokentoken,HttpServletRequestrequest,

HttpServletResponseresponse){

if(token==null){

HttpSessionsession=request.getSession(false);

if(session!=null){

session.removeAttribute(this.sessionAttributeName);

}

}

else{

HttpSessionsession=request.getSession();

session.setAttribute(this.sessionAttributeName,token);

}

}

publicCsrfTokenloadToken(HttpServletRequestrequest){

HttpSessionsession=request.getSession(false);

if(session==null){

returnnull;

}

return(CsrfToken)session.getAttribute(this.sessionAttributeName);

}

publicCsrfTokengenerateToken(HttpServletRequestrequest){

returnnewDefaultCsrfToken(this.headerName,this.parameterName,

createNewToken());

}

privateStringcreateNewToken(){

returnUUID.randomUUID().toString();

}

}

这段源码其实也很好理解:

saveToken 方法将 CsrfToken 保存在 HttpSession 中,将来再从 HttpSession 中取出和前端传来的参数做笔记。loadToken 方法当然就是从 HttpSession 中读取 CsrfToken 出来。generateToken 是生成 CsrfToken 的过程,可以看到,生成的默认载体就是 DefaultCsrfToken,而 CsrfToken 的值则通过 createNewToken 方法生成,是一个 UUID 字符串。在构造 DefaultCsrfToken 是还有两个参数 headerName 和 parameterName,这两个参数是前端保存参数的 key。

这是默认的方案,适用于前后端不分的开发,具体用法可以参考上篇文章。

如果想在前后端分离开发中使用,那就需要 CsrfTokenRepository 的另一个实现类 CookieCsrfTokenRepository ,代码如下:

publicfinalclassCookieCsrfTokenRepositoryimplementsCsrfTokenRepository{

staticfinalStringDEFAULT_CSRF_COOKIE_NAME="XSRF-TOKEN";

staticfinalStringDEFAULT_CSRF_PARAMETER_NAME="_csrf";

staticfinalStringDEFAULT_CSRF_HEADER_NAME="X-XSRF-TOKEN";

privateStringparameterName=DEFAULT_CSRF_PARAMETER_NAME;

privateStringheaderName=DEFAULT_CSRF_HEADER_NAME;

privateStringcookieName=DEFAULT_CSRF_COOKIE_NAME;

privatebooleancookieHttpOnly=true;

privateStringcookiePath;

privateStringcookieDomain;

publicCookieCsrfTokenRepository(){

}

@Override

publicCsrfTokengenerateToken(HttpServletRequestrequest){

returnnewDefaultCsrfToken(this.headerName,this.parameterName,

createNewToken());

}

@Override

publicvoidsaveToken(CsrfTokentoken,HttpServletRequestrequest,

HttpServletResponseresponse){

StringtokenValue=token==null?"":token.getToken();

Cookiecookie=newCookie(this.cookieName,tokenValue);

cookie.setSecure(request.isSecure());

if(this.cookiePath!=null&&!this.cookiePath.isEmpty()){

cookie.setPath(this.cookiePath);

}else{

cookie.setPath(this.getRequestContext(request));

}

if(token==null){

cookie.setMaxAge(0);

}

else{

cookie.setMaxAge(-1);

}

cookie.setHttpOnly(cookieHttpOnly);

if(this.cookieDomain!=null&&!this.cookieDomain.isEmpty()){

cookie.setDomain(this.cookieDomain);

}

response.addCookie(cookie);

}

@Override

publicCsrfTokenloadToken(HttpServletRequestrequest){

Cookiecookie=WebUtils.getCookie(request,this.cookieName);

if(cookie==null){

returnnull;

}

Stringtoken=cookie.getValue();

if(!StringUtils.hasLength(token)){

returnnull;

}

returnnewDefaultCsrfToken(this.headerName,this.parameterName,token);

}

publicstaticCookieCsrfTokenRepositorywithHttpOnlyFalse(){

CookieCsrfTokenRepositoryresult=newCookieCsrfTokenRepository();

result.setCookieHttpOnly(false);

returnresult;

}

privateStringcreateNewToken(){

returnUUID.randomUUID().toString();

}

}

和 HttpSessionCsrfTokenRepository 相比,这里_csrf数据保存的时候,都保存到 cookie 中去了,当然读取的时候,也是从 cookie 中读取,其他地方则和 HttpSessionCsrfTokenRepository 是一样的。

OK,这就是我们整个_csrf参数生成的过程。

总结一下,就是生成一个 CsrfToken,这个 Token,本质上就是一个 UUID 字符串,然后将这个 Token 保存到 HttpSession 中,或者保存到 Cookie 中,待请求到来时,从 HttpSession 或者 Cookie 中取出来做校验。

2.参数校验

那接下来就是校验了。

校验主要是通过 CsrfFilter 过滤器来进行,我们来看下核心的 doFilterInternal 方法:

protectedvoiddoFilterInternal(HttpServletRequestrequest,

HttpServletResponseresponse,FilterChainfilterChain)throwsServletException,IOException{

request.setAttribute(HttpServletResponse.class.getName(),response);

CsrfTokencsrfToken=this.tokenRepository.loadToken(request);

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

if(!this.requireCsrfProtectionMatcher.matches(request)){

filterChain.doFilter(request,response);

return;

}

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

}

这个方法我来稍微解释下:

首先调用 tokenRepository.loadToken 方法读取 CsrfToken 出来,这个 tokenRepository 就是你配置的 CsrfTokenRepository 实例,CsrfToken 存在 HttpSession 中,这里就从 HttpSession 中读取,CsrfToken 存在 Cookie 中,这里就从 Cookie 中读取。如果调用 tokenRepository.loadToken 方法没有加载到 CsrfToken,那说明这个请求可能是第一次发起,则调用 tokenRepository.generateToken 方法生成 CsrfToken ,并调用 tokenRepository.saveToken 方法保存 CsrfToken。大家注意,这里还调用 request.setAttribute 方法存了一些值进去,这就是默认情况下,我们通过 jsp 或者 thymeleaf 标签渲染_csrf的数据来源。requireCsrfProtectionMatcher.matches 方法则使用用来判断哪些请求方法需要做校验,默认情况下,"GET", "HEAD", "TRACE", "OPTIONS" 方法是不需要校验的。接下来获取请求中传递来的 CSRF 参数,先从请求头中获取,获取不到再从请求参数中获取。获取到请求传来的 csrf 参数之后,再和一开始加载到的 csrfToken 做比较,如果不同的话,就抛出异常。

如此之后,就完成了整个校验工作了。

3.LazyCsrfTokenRepository

前面我们说了 CsrfTokenRepository 有四个实现类,除了我们介绍的两个之外,还有一个 LazyCsrfTokenRepository,这里松哥也和大家做一个简单介绍。

在前面的 CsrfFilter 中大家发现,对于常见的 GET 请求实际上是不需要 CSRF 攻击校验的,但是,每当 GET 请求到来时,下面这段代码都会执行:

if(missingToken){

csrfToken=this.tokenRepository.generateToken(request);

this.tokenRepository.saveToken(csrfToken,request,response);

}

生成 CsrfToken 并保存,但实际上却没什么用,因为 GET 请求不需要 CSRF 攻击校验。

所以,Spring Security 官方又推出了 LazyCsrfTokenRepository。

LazyCsrfTokenRepository 实际上不能算是一个真正的 CsrfTokenRepository,它是一个代理,可以用来增强 HttpSessionCsrfTokenRepository 或者 CookieCsrfTokenRepository 的功能:

publicfinalclassLazyCsrfTokenRepositoryimplementsCsrfTokenRepository{

@Override

publicCsrfTokengenerateToken(HttpServletRequestrequest){

returnwrap(request,this.delegate.generateToken(request));

}

@Override

publicvoidsaveToken(CsrfTokentoken,HttpServletRequestrequest,

HttpServletResponseresponse){

if(token==null){

this.delegate.saveToken(token,request,response);

}

}

@Override

publicCsrfTokenloadToken(HttpServletRequestrequest){

returnthis.delegate.loadToken(request);

}

privateCsrfTokenwrap(HttpServletRequestrequest,CsrfTokentoken){

HttpServletResponseresponse=getResponse(request);

returnnewSaveOnAccessCsrfToken(this.delegate,request,response,token);

}

privatestaticfinalclassSaveOnAccessCsrfTokenimplementsCsrfToken{

privatetransientCsrfTokenRepositorytokenRepository;

privatetransientHttpServletRequestrequest;

privatetransientHttpServletResponseresponse;

privatefinalCsrfTokendelegate;

SaveOnAccessCsrfToken(CsrfTokenRepositorytokenRepository,

HttpServletRequestrequest,HttpServletResponseresponse,

CsrfTokendelegate){

this.tokenRepository=tokenRepository;

this.request=request;

this.response=response;

this.delegate=delegate;

}

@Override

publicStringgetToken(){

saveTokenIfNecessary();

returnthis.delegate.getToken();

}

privatevoidsaveTokenIfNecessary(){

if(this.tokenRepository==null){

return;

}

synchronized(this){

if(this.tokenRepository!=null){

this.tokenRepository.saveToken(this.delegate,this.request,

this.response);

this.tokenRepository=null;

this.request=null;

this.response=null;

}

}

}

}

}

这里,我说三点:

generateToken 方法,该方法用来生成 CsrfToken,默认 CsrfToken 的载体是 DefaultCsrfToken,现在换成了 SaveOnAccessCsrfToken。SaveOnAccessCsrfToken 和 DefaultCsrfToken 并没有太大区别,主要是 getToken 方法有区别,在 SaveOnAccessCsrfToken 中,当开发者调用 getToken 想要去获取 csrfToken 时,才会去对 csrfToken 做保存操作(调用 HttpSessionCsrfTokenRepository 或者 CookieCsrfTokenRepository 的 saveToken 方法)。LazyCsrfTokenRepository 自己的 saveToken 则做了修改,相当于放弃了 saveToken 的功能,调用该方法并不会做保存操作。

使用了 LazyCsrfTokenRepository 之后,只有在使用 csrfToken 时才会去存储它,这样就可以节省存储空间了。

LazyCsrfTokenRepository 的配置方式也很简单,在我们使用 Spring Security 时,如果对 csrf 不做任何配置,默认其实就是 LazyCsrfTokenRepository+HttpSessionCsrfTokenRepository 组合。

当然我们也可以自己配置,如下:

@Override

protectedvoidconfigure(HttpSecurityhttp)throwsException{

http.authorizeRequests().anyRequest().authenticated()

.and()

.formLogin()

.loginPage("/login.html")

.successHandler((req,resp,authentication)->{

resp.getWriter().write("success");

})

.permitAll()

.and()

.csrf().csrfTokenRepository(newLazyCsrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()));

}

4.小结

今天主要和小伙伴聊了一下 Spring Security 中 csrf 防御的原理。

整体来说,就是两个思路:

生成 csrfToken 保存在 HttpSession 或者 Cookie 中。请求到来时,从请求中提取出来 csrfToken,和保存的 csrfToken 做比较,进而判断出当前请求是否合法。

好啦,不知道小伙伴们有没有 GET 到呢?如果觉得有收获,记得点个在看鼓励下松哥哦~

今日干货

刚刚发表查看:13500回复:135

公众号后台回复 SpringBoot,免费获取 274 页SpringBoot修炼手册。

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。