【1】什么是CSRF
跨站请求伪造(英语:Cross-site request forgery),也被称为 one-click attack 或者 session riding,通常缩写为 CSRF 或者 XSRF, 是一种挟制用户在当前已登录的Web应用程序上执行非本意的操作的攻击方法。跟跨网站脚本(XSS)相比,XSS利用的是用户对指定网站的信任,CSRF 利用的是网站对用户网页浏览器的信任。
跨站请求攻击,简单地说,是攻击者通过一些技术手段欺骗用户的浏览器去访问一个自己曾经认证过的网站并运行一些操作(如发邮件,发消息,甚至财产操作如转账和购买商品)。由于浏览器曾经认证过,所以被访问的网站会认为是真正的用户操作而去运行。这利用了web中用户身份验证的一个漏洞:简单的身份验证只能保证请求发自某个用户的浏览器,却不能保证请求本身是用户自愿发出的。
从Spring Security 4.0开始,默认情况下会启用CSRF保护,以防止CSRF攻击应用程序,Spring Security CSRF会针对PATCH,POST,PUT和DELETE
方法进行防护。
如下所示,默认开启CSRF保护,当发起post请求时会被拦截:
【2】核心原理
如下所示,在CsrfFilter中会根据请求尝试获取csrfToken。如果不存在,则生成并存储到session和request中。
这里tokenRepository默认是LazyCsrfTokenRepository
包装了HttpSessionCsrfTokenRepository
:
默认使用对其请求方式进行判断,如果不是"GET", "HEAD", "TRACE", "OPTIONS"
直接放行。
if (!this.requireCsrfProtectionMatcher.matches(request)) {
filterChain.doFilter(request, response);
return;
}
然后根据上面得到的csrfToken与请求携带的token进行对比:
//从请求头获取
String actualToken = request.getHeader(csrfToken.getHeaderName());
if (actualToken == null) {
//从请求参数获取
actualToken = request.getParameter(csrfToken.getParameterName());
}
//如果token不相等
if (!csrfToken.getToken().equals(actualToken)) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Invalid CSRF token found for "
+ UrlUtils.buildFullRequestUrl(request));
}
//缺失token
if (missingToken) {
this.accessDeniedHandler.handle(request, response,
new MissingCsrfTokenException(actualToken));
}
else {
// token不匹配
this.accessDeniedHandler.handle(request, response,
new InvalidCsrfTokenException(csrfToken, actualToken));
}
return;
}
【3】应用实践
这里以thymeleaf模板引擎为例说明使用。
① 引入pom文件
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!--对Thymeleaf添加Spring Security标签支持-->
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
② 页面携带CSRF信息
<form method="post" action="update_token">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
用户名: <input type="text" name="username" /><br />
密 码: <input type="password" name="password" /><br />
<button type="submit">修改</button>
</form>
SecurityConfig默认就开启csrf保护功能,无需额外配置。
③ 使用CookieCsrfTokenRepository
其本质是将token持久化在cookie名字为XSRF-TOKEN上,然后从请求头X-XSRF-TOKEN
读取进行对比。
如下所示将token包装为cookie对象添加到response上:
@Override
public void saveToken(CsrfToken token, HttpServletRequest request,
HttpServletResponse response) {
String tokenValue = token == null ? "" : token.getToken();
Cookie cookie = new Cookie(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);
}
SecurityConfig使用如下代码来指定csrfTokenRepository:
and().csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());