CSRF(Cross-site request forgery),也被称为:one click attack/session riding,中文名称:跨站请求伪造,通常缩写为:CSRF/XSRF
。
跨站请求攻击:简单地说,是攻击者通过一些技术手段欺骗用户的浏览器去访问一个自己曾经认证过的网站并运行一些操作(如发邮件,发消息,甚至财产操作如转账和购买商品)。 由于浏览器曾经认证过,所以被访问的网站会认为是真正的用户操作而去运行。 这利用了 web 中用户身份验证的一个漏洞:简单的身份验证只能保证请求发自某个用户的浏览器,却不能保证请求本身是用户自愿发出的。
图来自网络:
1)检查 HTTP Referer 字段 HTTP头中有一个 Referer字段,这个字段用以标明请求来源于哪个地址。在处理敏感数据请求时,通常来说,Referer字段应和请求的地址位于同一域名下。 以上文银行操作为例,Referer字段地址通常应该是转账按钮所在的网页地址,应该也位于www.examplebank.com之下。而如果是CSRF攻击传来的请求,Referer字段会是包含恶意网址的地址,不会位于www.examplebank.com之下,这时候服务器就能识别出恶意的访问。
这种办法简单易行,工作量低,仅需要在关键访问处增加一步校验。但这种办法也有其局限性,因其完全依赖浏览器发送正确的Referer字段。虽然http协议对此字段的内容有明确的规定,但并无法保证来访的浏览器的具体实现,亦无法保证浏览器没有安全漏洞影响到此字段。并且也存在攻击者攻击某些浏览器,篡改其 Referer字段的可能。
2)在请求地址中添加 token 并验证 由于 CSRF的本质在于攻击者欺骗用户去访问自己设置的地址,所以如果要求在访问敏感数据请求时,要求用户浏览器提供不保存在 cookie中,并且攻击者无法伪造的数据作为校验,那么攻击者就无法再运行 CSRF攻击。这种数据通常是窗体中的一个数据项。服务器将其生成并附加在窗体中,其内容是一个伪随机数。当客户端通过窗体提交请求时,这个伪随机数也一并提交上去以供校验。正常的访问时,客户端浏览器能够正确得到并传回这个伪随机数,而通过 CSRF传来的欺骗性攻击中,攻击者无从事先得知这个伪随机数的值,服务端就会因为校验 token的值为空或者错误,拒绝这个可疑请求。
二、Spring Security支持CSRF防御从 Spring Security 4.0 开始,默认情况下会启用 CSRF 保护,以防止 CSRF 攻击应用程序。 Spring Security CSRF 会针对除了 “GET”, “HEAD”, “TRACE”, "OPTIONS"四类的请求进行防御。
1、核心类 1.1 CsrfToken接口CsrfToken接口定义了获取消息头、请求参数、令牌等API。
public interface CsrfToken extends Serializable {
String getHeaderName();
String getParameterName();
String getToken();
}
默认的实现类为: DefaultCsrfToken
。
CsrfTokenRepository接口定义了生成、保存、加载 CsrfToken等API。
public interface CsrfTokenRepository {
CsrfToken generateToken(HttpServletRequest var1);
void saveToken(CsrfToken var1, HttpServletRequest var2, HttpServletResponse var3);
CsrfToken loadToken(HttpServletRequest var1);
}
默认的实现类为:HttpSessionCsrfTokenRepository
。
-
HttpSessionCsrfTokenRepository是一个基于 HttpSession保存 csrf token的存储实现。
-
CookieCsrfTokenRepository会将 csrf token以名 XSRF-TOKEN写入 Cookie,然后接收请求时从名为 X-XSRF-TOKEN的 header或者名为 _csrf的 http请求参数读取 csrf token。但是CookieCsrfTokenRepository写入的 Cookie默认具有 cookieHttpOnly属性,前端 js是不能操作它的,这就需要在 Spring Security的配置中将
cookieHttpOnly属性设为false
。 -
LazyCsrfTokenRepository,它是一个代理,可以用来增强 HttpSessionCsrfTokenRepository 或者 CookieCsrfTokenRepository 的功能
CsrfFilter过滤器用于处理跨站请求伪造。 HttpSession 或者 Cookie 中的 csrf token值与内存中保存的的是否一致,
- 如果一致框架则认为当然登录页面是安全的,
- 如果不一致,会报403forbidden错误。
注意:
- 如果使用 Spring Security 自带的默认登录页面时,可以看到默认登录页面是加了隐藏域。
- 如果使用自定义的登录页面时,我们需要手动添加这个隐藏域。
- 如果是前后端分离的项目,可以放在 Cookie 中。
Spring Security 默认是开启 CSRF 保护的。我们可以指定 CSRF使用哪个 CsrfTokenRepository实现类。在 Spring Security配置类中添加。
.csrf()
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) //
我们使用 CookieCsrfTokenRepository将 令牌放在 Cookie 中返回前端,前端先从 Cookie 中提取出 XSRF-TOKEN,放到 _csrf _csrf 参数中,通过一个 POST 请求执行登录操作。
启动项目,点击登录,我们就可以看到 Cookie中看到一个 XSRF-TOKEN。登录成功
3、源码分析HttpSessionCsrfTokenRepository方式自行查看。重点查看 CookieCsrfTokenRepository类。
3.1 CSRF 的原理- 生成 csrfToken 保存到 HttpSession 或者 Cookie 中
- 请求到来时,从请求中提取 csrfToken,和保存的 csrfToken 做比较,进而判断当前请求是否合法。主要通过 CsrfFilter 过滤器来完成。
查看 CookieCsrfTokenRepository类的 generateToken方法。XSRF-TOKEN的值默认是一个 uuid。
思考:它是什么时候被生产的呢?
3.3 csrf token校验请求会经过 CsrfFilter过滤器处理,查看 它的父类的 doFilter方法,会调用它子类 CsrfFilter的 doFilterInternal方法
。
//看出SpringSecurity的csrf机制把请求方式分成两类来处理
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
request.setAttribute(HttpServletResponse.class.getName(), response);
//获取令牌
CsrfToken csrfToken = this.tokenRepository.loadToken(request);
boolean missingToken = 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);
//1."GET", "HEAD", "TRACE", "OPTIONS"四类请求可以直接通过
if (!this.requireCsrfProtectionMatcher.matches(request)) {
filterChain.doFilter(request, response);
} else {
//2.除去上面四类,包括POST都要被验证携带token才能通过
//从Header或者Parameter中获取前端的token
String actualToken = request.getHeader(csrfToken.getHeaderName());
if (actualToken == null) {
actualToken = request.getParameter(csrfToken.getParameterName());
}
if (!csrfToken.getToken().equals(actualToken)) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Invalid CSRF token found for " +
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);
}
}
}
逻辑如下:
- 在tokenRepository查询后端保存的令牌。
- 如果是 GET等四类请求则不会使用 CSRF防御,直接放行
- 如果使用 CSRF防御,就从Header或者Parameter中获取前端的token,然后对前后端的令牌进行校验。如果校验不通过,抛出 InvalidCsrfTokenException异常,并调用访问拒绝处理器进行处理。如果通过就放行
1)在tokenRepository查询后端保存的令牌
2)调用requireCsrfProtectionMatcher.matches判断请求方法是否是GET,HEAD,TRACE,OPTIONS,匹配则不会使用CSRF防御。直接放行
3)使用 CSRF防御,令牌校验
参考文章:
- 什么是CSRF:https://blog.csdn.net/weixin_40482816/article/details/114301717
– 求知若饥,虚心若愚。