SpringSecurity之CSRF
前言
前一篇讲解了关于用户注销以及自动登录(记住我)等功能。今天我们来看一下关于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练习生》