前言
前一篇讲解了关于用户注销以及自动登录(记住我)等功能。今天我们来看一下关于CSRF的使用及避免。
什么是CSRF
跨站请求伪造(英语:Cross-site request forgery),也被称为 one-click attack 或者 session riding,通常缩写为 CSRF 或者 XSRF, 是一种挟制用户在当前已 登录的 Web 应用程序上执行非本意的操作的攻击方法。跟跨网站脚本(XSS)相比,XSS利用的是用户对指定网站的信任,CSRF利用的是网站对用户网页浏览器的信任。
跨站请求攻击,简单地说,是攻击者通过一些技术手段欺骗用户的浏览器去访问一个 自己曾经认证过的网站并运行一些操作(如发消息,发邮件,甚至财产操作如购买商品和转账)。由于浏览器曾经认证过,所以被访问的网站会认为是真正的用户操作而去运行。 这利用了 web 中用户身份验证的一个漏洞:简单的身份验证只能保证请求发自某个用户的浏览器,却不能保证请求本身是用户自愿发出的。 从 Spring Security 4.0 开始,默认情况下会启用 CSRF 保护,以防止CSRF攻击应用程序,SpringSecurity CSRF 会针对 PATCH,POST,PUT 和 DELETE 方法进行防护。
注意,这里面不包括GET、HEAD、TRACE、OPTIONS请求,GET、HEAD、TRACE、OPTIONS请求还是会存在这种问题的。
Spring Security实现CSRF的原理
1. 生成 csrfToken,然后保存到Cookie或者HttpSession中去。SaveOnAccessCsrfToken 类有个接口 CsrfTokenRepository
接口实现类:HttpSessionCsrfTokenRepository,CookieCsrfTokenRepository
2. 请求到达时,从请求当中提取到csrfToken,然后将其与保存的csrfToken进行比较,进而判断出当前请求是否合法的。主要通过CsrfFilter过滤器来完成。
// // Source code recreated from a .class file by IntelliJ IDEA // (powered by Fernflower decompiler) // package org.springframework.security.web.csrf; import java.io.IOException; import java.security.MessageDigest; import java.util.Arrays; import java.util.HashSet; import java.util.function.Supplier; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.core.log.LogMessage; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.crypto.codec.Utf8; 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 { public static final RequestMatcher DEFAULT_CSRF_MATCHER = new CsrfFilter.DefaultRequiresCsrfMatcher(); private static final String SHOULD_NOT_FILTER = "SHOULD_NOT_FILTER" + CsrfFilter.class.getName(); private final Log logger = LogFactory.getLog(this.getClass()); private final CsrfTokenRepository tokenRepository; private RequestMatcher requireCsrfProtectionMatcher; private AccessDeniedHandler accessDeniedHandler; public CsrfFilter(CsrfTokenRepository csrfTokenRepository) { this.requireCsrfProtectionMatcher = DEFAULT_CSRF_MATCHER; this.accessDeniedHandler = new AccessDeniedHandlerImpl(); Assert.notNull(csrfTokenRepository, "csrfTokenRepository cannot be null"); this.tokenRepository = csrfTokenRepository; } protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException { return Boolean.TRUE.equals(request.getAttribute(SHOULD_NOT_FILTER)); } 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); if (!this.requireCsrfProtectionMatcher.matches(request)) { if (this.logger.isTraceEnabled()) { this.logger.trace("Did not protect against CSRF since request did not match " + this.requireCsrfProtectionMatcher); } filterChain.doFilter(request, response); } else { String actualToken = request.getHeader(csrfToken.getHeaderName()); if (actualToken == null) { actualToken = request.getParameter(csrfToken.getParameterName()); } if (!equalsConstantTime(csrfToken.getToken(), actualToken)) { this.logger.debug(LogMessage.of(() -> { return "Invalid CSRF token found for " + UrlUtils.buildFullRequestUrl(request); })); AccessDeniedException exception = !missingToken ? new InvalidCsrfTokenException(csrfToken, actualToken) : new MissingCsrfTokenException(actualToken); this.accessDeniedHandler.handle(request, response, (AccessDeniedException)exception); } else { filterChain.doFilter(request, response); } } } public static void skipRequest(HttpServletRequest request) { request.setAttribute(SHOULD_NOT_FILTER, Boolean.TRUE); } public void setRequireCsrfProtectionMatcher(RequestMatcher requireCsrfProtectionMatcher) { Assert.notNull(requireCsrfProtectionMatcher, "requireCsrfProtectionMatcher cannot be null"); this.requireCsrfProtectionMatcher = requireCsrfProtectionMatcher; } public void setAccessDeniedHandler(AccessDeniedHandler accessDeniedHandler) { Assert.notNull(accessDeniedHandler, "accessDeniedHandler cannot be null"); this.accessDeniedHandler = accessDeniedHandler; } private static boolean equalsConstantTime(String expected, String actual) { if (expected == actual) { return true; } else if (expected != null && actual != null) { byte[] expectedBytes = Utf8.encode(expected); byte[] actualBytes = Utf8.encode(actual); return MessageDigest.isEqual(expectedBytes, actualBytes); } else { return false; } } private static final class DefaultRequiresCsrfMatcher implements RequestMatcher { private final HashSet<String> allowedMethods; private DefaultRequiresCsrfMatcher() { this.allowedMethods = new HashSet(Arrays.asList("GET", "HEAD", "TRACE", "OPTIONS")); } public boolean matches(HttpServletRequest request) { return !this.allowedMethods.contains(request.getMethod()); } public String toString() { return "CsrfNotRequired " + this.allowedMethods; } } }
好了,今天关于SpringSecurity的CSRF攻击防范就讲到这里。
欢迎大家点击下方卡片,关注《coder练习生》