Spring Security 如何防御跨站请求伪造(CSRF)

  1. CSRF 是什么
  2. 模拟 CSRF 攻击
  3. CSRF 防御思路
  4. 如何使用 Spring Security 预防 CSRF
  5. Spring Security 实现 CSRF 的原理

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 方法进行防护。

1)假设用户打开了招商银行网上银行网站,并且登录。

2)登录成功后,网上银行会返回 Cookie 给前端,浏览器将 Cookie 保存下来。

3)用户在没有登出网上银行的情况下,在浏览器里边打开了一个新的选项卡,然后又去访问了一个危险网站。

4)这个危险网站上有一个超链接,超链接的地址指向了招商银行网上银行。
5)用户点击了这个超链接,由于这个超链接会自动携带上浏览器中保存的 Cookie,所以用户不知不觉中就访问了网上银行,进而可能给自己造成了损失。

注意,黑客网站根本不知道你的 Cookie 里边存的啥,他也不需要知道,因为 CSRF 攻击是浏览器自动携带上 Cookie 中的数据的

模拟 CSRF 攻击

新建 被攻击项目,代号 bank,开发转账接口:

http://localhost:8080/transferAccounts

新建 危险网站,代号 danger,新建一张钓鱼网页:

<body>
  <form action="http://localhost:8080/transferAccounts" method="post">
    <input type="hidden" value="BKB880000111" name="cardNumber">
    <input type="hidden" value="10000" name="money">
    <input type="submit" value="点击查看美女图片">
  </form>
</body>

好奇心驱使下的你点击了美女图片…

这时,钓鱼网页就会给 S880000111 这个银行卡账号转账 10000 块!

CSRF 防御思路

  • 通过 referer、token 或者 验证码 来检测用户提交
  • 尽量不要在页面的链接中暴露用户隐私信息
  • 对于用户修改删除等操作最好都使用 post 操作
  • 避免全站通用的 cookie,严格设置 cookie 的域

如何使用 Spring Security 预防 CSRF

Spring Security 中默认就提供了 csrf 防御,我这里主要说下基于前后端分离的项目如何实现~

添加配置,将 XSRF-TOKEN 存储到 Cookie 中返回给前端

http.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());

这个时候前端的 Cookie 中会多一项:

前端在提交表单时要带上 _csrf 参数(以下示例是伪代码):

_csrf = cookie.get('XSRF-TOKEN');

do ajax post, url = 'login', params = {
  cardNumber: 'S880000111',
  money: '10000',
  _csrf:_csrf
})

如果你不喜欢 Spring Security 的 CSRF,关闭方式:

// 关闭跨站脚本攻击
http.csrf().disable();

Spring Security 实现 CSRF 的原理

这是 CsrfToken 接口

public interface CsrfToken extends Serializable {

    String getHeaderName();

    String getParameterName();

    String getToken();

}

通过 CookieCsrfTokenRepository 来生成和管理 XSRF-TOKEN

public final class CookieCsrfTokenRepository implements CsrfTokenRepository {

    ...

    @Override
    public CsrfToken generateToken(HttpServletRequest request) {
        return new DefaultCsrfToken(this.headerName, this.parameterName, createNewToken());
    }

    @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((this.secure != null) ? this.secure : request.isSecure());
        cookie.setPath(StringUtils.hasLength(this.cookiePath) ? this.cookiePath : this.getRequestContext(request));
        cookie.setMaxAge((token != null) ? -1 : 0);
        cookie.setHttpOnly(this.cookieHttpOnly);
        if (StringUtils.hasLength(this.cookieDomain)) {
          cookie.setDomain(this.cookieDomain);
        }
        response.addCookie(cookie);
    }

    @Override
    public CsrfToken loadToken(HttpServletRequest request) {
        Cookie cookie = WebUtils.getCookie(request, this.cookieName);
        if (cookie == null) {
          return null;
        }
        String token = cookie.getValue();
        if (!StringUtils.hasLength(token)) {
          return null;
        }
        return new DefaultCsrfToken(this.headerName, this.parameterName, token);
    }

  ...

}

通过 CsrfFilter 实现 CSRF-TOKEN 对比

public final class CsrfFilter extends OncePerRequestFilter {

    ...

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
        throws ServletException, IOException {
      request.setAttribute(HttpServletResponse.class.getName(), response);
      // 从数据库中获取保存的 csrfToken
      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);
        return;
      }
      // 从请求中提取 csrfToken
      String actualToken = request.getHeader(csrfToken.getHeaderName());
      if (actualToken == null) {
        // 可以放在请求头,也可以放在请求体中
        actualToken = request.getParameter(csrfToken.getParameterName());
      }
      // 从请求中提取 csrfToken,和保存的 csrfToken 做比较,进而判断当前请求是否合法。
      if (!csrfToken.getToken().equals(actualToken)) {
        this.logger.debug(
            LogMessage.of(() -> "Invalid CSRF token found for " + UrlUtils.buildFullRequestUrl(request)));
        AccessDeniedException exception = (!missingToken) ? new InvalidCsrfTokenException(csrfToken, actualToken)
            : new MissingCsrfTokenException(actualToken);
        this.accessDeniedHandler.handle(request, response, exception);
        return;
      }
      filterChain.doFilter(request, response);
    }

  ...

}

转载请注明来源。 欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。 可以在下面评论区评论,也可以邮件至 sharlot2050@foxmail.com。

文章标题:Spring Security 如何防御跨站请求伪造(CSRF)

字数:1.3k

本文作者:夏来风

发布时间:2021-06-08, 23:00:00

原始链接:http://www.demo1024.com/blog/spring-security-csrf/

版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。