SpringSecurity5(12-Csrf防护)

打印 上一主题 下一主题

主题 1854|帖子 1854|积分 5572

工作原理

从 Spring Security 4.x 开始,默认启用 CSRF 保护,该默认配置将 CSRF Token 添加到名为 _csrf 的 HttpServletRequest 属性中。Spring Security 通过 CsrfFilter 实现 CSRF 防护,如果 CSRF Token 不存在或值不正确,则拒绝该哀求并将相应的状态设置为 403
SpringSecurity 的 Csrf 机制把哀求方式分为两类来处理

  • GET、HEAD、TRACE、OPTIONS 这四类哀求可以直接通过
  • 除去上面,包括 POST 都要被验证携带 token 才能通过
为了保护 MVC 应用,Spring 会在每个生成的视图中添加一个 CSRF Token,该 Token 必须在每次修改状态的 HTTP 哀求(PATCH、POST、PUT 和 DELETE)中提交给服务器,这可以保护应用免受 CSRF 攻击,因为攻击者无法从自己的页面获取此 Token。
用户登录时,体系发放一个 CsrfToken 值,用户携带该 CsrfToken 值与用户名、密码等参数完成登录,体系记录该会话的 CsrfToken 值,之后在用户的任何哀求中,都必须带上该 CsrfToken 值,并由体系进行校验。这种方法必要与前端配置,包括存储 CsrfToken 值,以及在任何哀求中(表单和 ajax)携带 CsrfToken 值,如果都是 XMLHttpRequest,则可以统一添加 CsrfToken 值,但如果存在大量的表单和 a 标签,就会变得非常烦琐
_csrf 属性包罗以下信息:

  • token:CSRF Token 值
  • parameterName:HTML 表单参数的名称,此中必须包罗 Token 值
  • headerName:HTTP Header 的名称,此中必须包罗 Token 值
HTML 表单
如果视图利用 HTML 表单,可以利用 parameterName 和 token 值添加隐藏 input
  1. [/code][b]JSON 哀求[/b]
  2. 如果视图利用 JSON,则必要利用 headerName 和 token 值添加 HTTP 哀求头信息。
  3. [list=1]
  4. [*]在 meta 标签中包罗 Token 值和 Header 名称
  5. [/list][code]
复制代码

  • 用 JQuery 获取 meta 标签值
  1. var token = $("meta[name='_csrf']").attr("content");
  2. var header = $("meta[name='_csrf_header']").attr("content");
复制代码

  • 利用这些值来设置 XHR Header
  1. $(document).ajaxSend(function(e, xhr, options) {
  2.     xhr.setRequestHeader(header, token);
  3. });
复制代码
CsrfFilter
  1. @Override
  2. protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
  3.     request.setAttribute(HttpServletResponse.class.getName(), response);
  4.     // 从 CsrfTokenRepository 中获取当前用户的 CsrfToken
  5.     CsrfToken csrfToken = this.tokenRepository.loadToken(request);
  6.     boolean missingToken = (csrfToken == null);
  7.     // 如果找不到 CsrfToken 就生成一个并保存到 CsrfTokenRepository 中
  8.     if (missingToken) {
  9.         csrfToken = this.tokenRepository.generateToken(request);
  10.         this.tokenRepository.saveToken(csrfToken, request, response);
  11.     }
  12.     // 在请求中添加 CsrfToken
  13.     request.setAttribute(CsrfToken.class.getName(), csrfToken);
  14.     request.setAttribute(csrfToken.getParameterName(), csrfToken);
  15.     // 如果是 "GET", "HEAD", "TRACE", "OPTIONS" 这些方法,直接放行
  16.     if (!this.requireCsrfProtectionMatcher.matches(request)) {
  17.         if (this.logger.isTraceEnabled()) {
  18.             this.logger.trace("Did not protect against CSRF since request did not match "
  19.                     + this.requireCsrfProtectionMatcher);
  20.         }
  21.         filterChain.doFilter(request, response);
  22.         return;
  23.     }
  24.     // 从用户请求头中获取 CsrfToken
  25.     String actualToken = request.getHeader(csrfToken.getHeaderName());
  26.     if (actualToken == null) {
  27.         // 头信息中拿不到,再从 param 中获取一次
  28.         actualToken = request.getParameter(csrfToken.getParameterName());
  29.     }
  30.     // 如果请求所携带的 CsrfToken 与从 Repository 中获取的不同,则阻止访问
  31.     if (!equalsConstantTime(csrfToken.getToken(), actualToken)) {
  32.         this.logger.debug(
  33.                 LogMessage.of(() -> "Invalid CSRF token found for " + UrlUtils.buildFullRequestUrl(request)));
  34.         AccessDeniedException exception = (!missingToken) ? new InvalidCsrfTokenException(csrfToken, actualToken): new MissingCsrfTokenException(actualToken);
  35.         this.accessDeniedHandler.handle(request, response, exception);
  36.         return;
  37.     }
  38.     // 正常情况下继续执行过滤器链的后续流程
  39.     filterChain.doFilter(request, response);
  40. }
复制代码
CsrfToken
  1. public interface CsrfToken extends Serializable {
  2.     // 获取请求头名称
  3.     String getHeaderName();
  4.     // 获取应该包含 Token 的参数名称
  5.     String getParameterName();
  6.     // 获取具体的 Token 值
  7.     String getToken();
  8. }
复制代码
CsrfTokenRepository
  1. public interface CsrfTokenRepository {
  2.     // 生成新的 token
  3.     CsrfToken generateToken(HttpServletRequest request);
  4.     // 保存 token,如果 token 传入 null 等同于删除
  5.     void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response);
  6.     // 从目标地点获取 token
  7.     CsrfToken loadToken(HttpServletRequest request);
  8. }
复制代码
CookieCsrfTokenRepository

它将 CsrfToken 值存储在用户的 cookie 内,减少了服务器 HttpSession 存储的内存消耗,而且当用 cookie 存储 CsrfToken 值时,前端可以用 JS 读取(必要设置该 cookie 的 httpOnly 属性为 false),而不必要服务器注入参数。默认环境下 CookieCsrfTokenRepository 将编写一个名为 XSRF-TOKEN 的 cookie 和从头部定名 X-XSRF-TOKEN 或 HTTP 参数 _csrf 中读取
存储在 cookie 中是不可以被 Csrf 利用的,cookie 只有在同域的环境下才能被读取,以是杜绝了第三方站点跨域读取 CsrfToken 值的大概。CSRF 攻击本身是不知道 cookie 内容的,只是利用了当哀求自动携带 cookie 时可以通过身份验证的漏洞,但服务器对 CsrfToken 值的校验并非取自 cookie,而是必要前端手动将 CsrfToken 值作为参数携带在哀求里
  1. @Override
  2. public void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response) {
  3.     // 判断参数 token 是否为空
  4.     String tokenValue = (token != null) ? token.getToken() : "";
  5.     // 根据 token,创建 Cookies
  6.     Cookie cookie = new Cookie(this.cookieName, tokenValue);
  7.     cookie.setSecure((this.secure != null) ? this.secure : request.isSecure());
  8.     cookie.setPath(StringUtils.hasLength(this.cookiePath) ? this.cookiePath : this.getRequestContext(request));
  9.     cookie.setMaxAge((token != null) ? this.cookieMaxAge : 0);
  10.     cookie.setHttpOnly(this.cookieHttpOnly);
  11.     if (StringUtils.hasLength(this.cookieDomain)) {
  12.         cookie.setDomain(this.cookieDomain);
  13.     }
  14.     // 最终返回给浏览器
  15.     response.addCookie(cookie);
  16. }
  17. @Override
  18. public CsrfToken loadToken(HttpServletRequest request) {
  19.     // 获取请求 Cookies
  20.     Cookie cookie = WebUtils.getCookie(request, this.cookieName);
  21.     if (cookie == null) {
  22.         return null;
  23.     }
  24.     // 获取 Cookeis 中的 Token
  25.     String token = cookie.getValue();
  26.     if (!StringUtils.hasLength(token)) {
  27.         return null;
  28.     }
  29.     // 获取到以后,创建 Token 对象
  30.     return new DefaultCsrfToken(this.headerName, this.parameterName, token);
  31. }
复制代码
HttpSessionCsrfTokenRepository

在默认环境下,SpringSecurity 加载的是一个 HttpSessionCsrfTokenRepository,HttpSessionCsrfTokenRepository 将 CsrfToken 值存储在 HttpSession 中,并指定前端把 CsrfToken 值放在 "_csrf " 的哀求参数或名为 " X-CSRF-TOKEN " 的哀求头字段里。校验时,通过对比 HttpSession 内存储的 CsrfToken 值与前端携带的 CsrfToken 值是否一致,便能断定本次哀求是否为 CSRF 攻击
  1. @Override
  2. public void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response) {
  3.     // 如果传入 token 为空,则删除当前会话的 Session
  4.     if (token == null) {
  5.         HttpSession session = request.getSession(false);
  6.         if (session != null) {
  7.             session.removeAttribute(this.sessionAttributeName);
  8.         }
  9.     }
  10.     else {
  11.         // 否则将 token 存入当前会话
  12.         HttpSession session = request.getSession();
  13.         session.setAttribute(this.sessionAttributeName, token);
  14.     }
  15. }
  16. @Override
  17. public CsrfToken loadToken(HttpServletRequest request) {
  18.     HttpSession session = request.getSession(false);
  19.     if (session == null) {
  20.         return null;
  21.     }
  22.     // 获取会话中的 Token 对象
  23.     return (CsrfToken) session.getAttribute(this.sessionAttributeName);
  24. }
复制代码
利用案例

如果无状态 API 利用基于 Token 的身份验证(如 JWT),就不必要 CSRF 保护。反之,如果利用 Session Cookie 进行身份验证,就必要启用 CSRF 保护。无状态 API 无法像 MVC 配置那样添加 CSRF Token,因为它不会生成任何 HTML 视图。
Session Cookie

后端配置
  1. @Configuration
  2. public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
  3.    
  4.     @Override
  5.     protected void configure(HttpSecurity http) throws Exception {
  6.         http.csrf()
  7.           .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
  8.     }
  9. }
复制代码
在这种环境下,可以利用 CookieCsrfTokenRepository 在 Cookie 中发送 CSRF Token,此配置将为前端设置一个名为 XSRF-TOKEN 的 Cookie。由于将 HTTP-only 标志设置为 false,因此前端能利用 JavaScript 获取此 Cookie。
前端配置

通过 JavaScript 从 document.cookie 列表中搜刮 XSRF-TOKEN Cookie 值。
由于该列表以字符串形式存储,因此可以利用此 regex (正则)进行检索:
  1. const csrfToken = document.cookie.replace(/(?:(?:^|.*;\s*)XSRF-TOKEN\s*\=\s*([^;]*).*$)|^.*$/, '$1');
复制代码
然后,必须向每个修改 API 状态的 REST 哀求发送 Token(POST、PUT、DELETE 和 PATCH),Spring 会通过 X-XSRF-TOKEN Header 来吸收它,只需利用 JavaScript Fetch API 设置即可:
  1. fetch(url, {
  2.   method: 'POST',
  3.   body: /* 发送给服务器的请求体 */,
  4.   headers: { 'X-XSRF-TOKEN': csrfToken },
  5. })
复制代码
无状态 API

JWT 配置
  1. @Configuration
  2. public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
  3.    
  4.     @Override
  5.     protected void configure(HttpSecurity http) throws Exception {
  6.         http.csrf().disable();
  7.     }
  8. }
复制代码


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

篮之新喜

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表