CSRF(Cross-site request forgery),中文名称:跨站请求伪造,也被称为:one click attack/session riding,缩写为:CSRF/XSRF。
你这可以这么理解CSRF攻击:攻击者盗用了你的身份,以你的名义发送恶意请求。CSRF能够做的事情包括:以你名义发送邮件,发消息,盗取你的账号,甚至于购买商品,虚拟货币转账…造成的问题包括:个人隐私泄露以及财产安全。
CSRF这种攻击方式在2000年已经被国外的安全人员提出,但在国内,直到06年才开始被关注,08年,国内外的多个大型社区和交互网站分别爆出CSRF漏洞,如:NYTimes.com(纽约时报)、Metafilter(一个大型的BLOG网站),YouTube和百度HI…而现在,互联网上的许多站点仍对此毫无防备,以至于安全业界称CSRF为“沉睡的巨人”。
CSRF的原理:先登录受信任网站A,并在本地生成Cookie,然后在不登出A的情况下,访问危险网站B。
看到这里,你也许会说:“如果我不满足以上两个条件中的一个,我就不会受到CSRF的攻击”。是的,确实如此,但你不能保证以下情况不会发生:
1.你不能保证你登录了一个网站后,不再打开一个tab页面并访问另外的网站。
2.你不能保证你关闭浏览器了后,你本地的Cookie立刻过期,你上次的会话已经结束。(事实上,关闭浏览器不能结束一个会话,但大多数人都会错误的认为关闭浏览器就等于退出登录/结束会话了…)
Spring Security Web使用CsrfFilter解决Cross-Site Request Forgery (CSRF)攻击,使用的模式是Synchronizer token pattern (STP)。
STP模式本意是每个请求都生成一个不同的,随机的,不可预测的token用于CSRF保护。这种严格的模式CSRF保护能力很强。但是每请求必验给服务端增加了额外的负担,另外它也要求浏览器必须保持正确的事件顺序,从而会带来一些可用性上的问题(比如用户打开了多个Tab的情况)。所以Spring Security中把这种限制放宽到了每个session使用一个csrf token,并且仅针对会对服务器进行状态更新的HTTP动作:PATCH, POST, PUT,DELETE等。
package org.springframework.security.web.csrf;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashSet;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.access.AccessDeniedHandlerImpl;
import org.springframework.security.web.util.UrlUtils;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
import org.springframework.web.filter.OncePerRequestFilter;
public final class CsrfFilter extends OncePerRequestFilter {
/**
* The default RequestMatcher that indicates if CSRF protection is required or
* not. The default is to ignore GET, HEAD, TRACE, OPTIONS and process all other
* requests.
* 用于检测哪些请求需要csrf保护,这里的缺省配置是:GET, HEAD, TRACE, OPTIONS这种只读的
* HTTP动词都被忽略不做csrf保护,而其他PATCH, POST, PUT,DELETE等会修改服务器状态的HTTP
* 动词会受到当前Filter的csrf保护。
*/
public static final RequestMatcher DEFAULT_CSRF_MATCHER = new DefaultRequiresCsrfMatcher();
private final Log logger = LogFactory.getLog(getClass());
private final CsrfTokenRepository tokenRepository;
private RequestMatcher requireCsrfProtectionMatcher = DEFAULT_CSRF_MATCHER;
// 用于CSRF保护验证逻辑失败进行处理
private AccessDeniedHandler accessDeniedHandler = new AccessDeniedHandlerImpl();
// 构造函数,使用指定的csrf token存储库构造一个CsrfFilter实例
// 缺省情况下,使用Spring Security 的 Springboot web 应用,选择使用的
// csrfTokenRepository是一个做了惰性封装的HttpSessionCsrfTokenRepository实例。
// 也就是说相应的 csrf token保存在http session中。
public CsrfFilter(CsrfTokenRepository csrfTokenRepository) {
Assert.notNull(csrfTokenRepository, "csrfTokenRepository cannot be null");
this.tokenRepository = csrfTokenRepository;
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
request.setAttribute(HttpServletResponse.class.getName(), response);
// 从csrf token存储库中获取针对当前请求的csrf token。
CsrfToken csrfToken = this.tokenRepository.loadToken(request);
// 记录针对当前请求是否不存在csrf token
final boolean missingToken = csrfToken == null;
if (missingToken) {
// 如果存储库中尚不存在针对当前请求的csrf token,生成一个,把它关联到
// 当前请求保存到csrf token存储库中
csrfToken = this.tokenRepository.generateToken(request);
this.tokenRepository.saveToken(csrfToken, request, response);
}
// 将从存储库中获取得到的或者新建并保存到存储库的csrf token保存为请求的两个属性
request.setAttribute(CsrfToken.class.getName(), csrfToken);
request.setAttribute(csrfToken.getParameterName(), csrfToken);
if (!this.requireCsrfProtectionMatcher.matches(request)) {
// 检测当前请求是否需要csrf保护,如果不需要,放行继续执行filter chain的其他逻辑
filterChain.doFilter(request, response);
return;
}
// 尝试从请求头部或者参数中获取浏览器端传递过来的实际的csrf token。
// 缺省情况下,从头部取出时使用header name: X-CSRF-TOKEN
// 从请求中获取参数时使用的参数名称是 : _csrf
String actualToken = request.getHeader(csrfToken.getHeaderName());
if (actualToken == null) {
actualToken = request.getParameter(csrfToken.getParameterName());
}
if (!csrfToken.getToken().equals(actualToken)) {
// csrf token存储库中取出的token和浏览器端传递过来的token不相等的情况有两种:
// 1. 针对该请求在存储库中并不存在csrf token
// 2. 针对该请求在存储库中的csrf token和请求参数实际携带的不一致
if (this.logger.isDebugEnabled()) {
this.logger.debug("Invalid CSRF token found for "
+ UrlUtils.buildFullRequestUrl(request));
}
if (missingToken) {
// 1. 针对该请求在存储库中并不存在csrf token , 处理方案:
// 抛出异常 MissingCsrfTokenException
this.accessDeniedHandler.handle(request, response,
new MissingCsrfTokenException(actualToken));
}
else {
// 2. 针对该请求在存储库中的csrf token和请求参数实际携带的不一致,处理方案:
// 抛出异常 InvalidCsrfTokenException
this.accessDeniedHandler.handle(request, response,
new InvalidCsrfTokenException(csrfToken, actualToken));
}
return;
}
// 当前请求需要经该Filter的csrf验证逻辑并且通过了csrf验证,放行,继续执行filter chain
// 其他部分逻辑
filterChain.doFilter(request, response);
}
/**
* Specifies a RequestMatcher that is used to determine if CSRF protection
* should be applied. If the RequestMatcher returns true for a given request,
* then CSRF protection is applied.
*
* 指定一个RequestMatcher用来检测一个请求是否需要应用csrf保护验证逻辑。
*
* The default is to apply CSRF protection for any HTTP method other than GET, HEAD,
* TRACE, OPTIONS.
* 缺省行为是针对GET, HEAD,TRACE, OPTIONS这种只读性的HTTP请求不做csrf保护验证,验证其他
* 那些会更新服务器状态的HTTP请求,比如PATCH, POST, PUT,DELETE等。
*
*
* @param requireCsrfProtectionMatcher the RequestMatcher used to determine if
* CSRF protection should be applied.
*/
public void setRequireCsrfProtectionMatcher(
RequestMatcher requireCsrfProtectionMatcher) {
Assert.notNull(requireCsrfProtectionMatcher,
"requireCsrfProtectionMatcher cannot be null");
this.requireCsrfProtectionMatcher = requireCsrfProtectionMatcher;
}
/**
* Specifies a AccessDeniedHandler that should be used when CSRF protection
* fails.
* 指定一个AccessDeniedHandler用于CSRF保护验证逻辑失败进行处理。
*
* The default is to use AccessDeniedHandlerImpl with no arguments.
* 缺省行为是使用一个不但参数的AccessDeniedHandlerImpl实例。
*
* @param accessDeniedHandler the AccessDeniedHandler to use
*/
public void setAccessDeniedHandler(AccessDeniedHandler accessDeniedHandler) {
Assert.notNull(accessDeniedHandler, "accessDeniedHandler cannot be null");
this.accessDeniedHandler = accessDeniedHandler;
}
// 用于检测哪些HTTP请求需要应用csrf保护的RequestMatcher,
// 缺省行为是针对GET, HEAD,TRACE, OPTIONS这种只读性的HTTP请求不做csrf保护,
// 其他那些会更新服务器状态的HTTP请求,比如PATCH, POST, PUT,DELETE等需要csrf保护。
private static final class DefaultRequiresCsrfMatcher implements RequestMatcher {
private final HashSet<String> allowedMethods = new HashSet<>(
Arrays.asList("GET", "HEAD", "TRACE", "OPTIONS"));
@Override
public boolean matches(HttpServletRequest request) {
return !this.allowedMethods.contains(request.getMethod());
}
}
}