SpringMVC/boot-CSRF安全方案
阅读原文时间:2023年07月09日阅读:1

1. CSRF原理与防御方案概述

  1. 增删改的接口参数值都有规律可循,可以被人恶意构造增删改接口

  2. 将恶意构造的增删改接口发给对应特定用户,让特定用户点击

  3. 特定用户使用自己的认证信息对该接口发起了请求,可能被新增危险信息(比如管理员账号),修改敏感信息(比如退款金额),删除关键信息(比如删除差评)

  4. 参数不可猜解,发起请求时在参数中增加随机token参数

  5. token参数在后台与保存在cookie,session,tair中的token参数进行比对,若不匹配或者没有该参数,则校验不通过

  6. 黑客无法获取到特定用户的随机token值,所以杜绝CSRF的危害

2 ali修复方案

若为正常业务,提供数据/文件等增删改服务,则需要配置CSRF,请继续看下去

若应用不是Web应用,或者只是HSF服务或者给其他应用服务器调用的API接口服务(纯内网的纯Server to Server,不是通过Login获取登陆态的接口,而是通过AKSK加签名验证签名) ,则不需要配置CSRF,请提供相应加签验签代码给对应答疑或者安全工程师进行确认并关闭漏洞。

Step1. 配置扩展包POM依赖

注意:SpringMVC扩展安全包引入后会默认开启一系列开关,包括XSS开关,CSRF开关等,从而导致业务短暂出现异常(前端页面乱码,接口访问返回403状态码等),只需要继续按照文档操作下去,业务最终会恢复正常。

1. SpringMVC扩展包

请参考网上SpringMVC安全扩展引入文档

2 SpringBoot扩展包 (starter)

请参考网上SpringBoot安全扩展引入文档

Step2. 查看是否依赖成功

POM配置完成后,在IDEA的External Libraries中查找是否以下包存在

//SpringMVC仅检查这个包
com.alibaba.security:security-spring-webmvc

//SpringBoot仅检查这个包
com.alibaba.security:security-spring-boot-starter

Step1. 显式配置CSRF开关

虽然CSRF在安全包引入之后,会自动开启CSRF拦截,但是为了确保配置可读性以及后续问题排查方便,请在resources下的"application.properties" (若没有,请创建)文件中协商如下配置:

spring.security.csrf.enabled = true

注:开启了该开关之后,所有访问请求都会因为没有token带入被拦截导致访问不成功,需要继续配置下去,让业务恢复正常

Step2.token带给前端

配置好开关之后,安全包会生成一个随机字符串,我们称为CSRF_Token,该token会被默认存入cookie中。若使用VM,则可以通过VM调用相关接口获得。

这个Token需要在前端的每个增删改接口请求中作为参数带入给服务器用于校验安全性

不同前端技术方案有不同带入方式:

1 VM后端模板

a. VM后端模板有三种token带入请求的配置方式:

  • 在application.properties中统一配置

  • CSRF Token 自动生成的URL映射列表,多值使用逗号分隔(默认值为空)

  • 当前URL风格为ant风格,风格值由配置项 spring.security.csrf.url.style 决定

    spring.security.csrf.token.urls = /csrf_token/**

b. 在Controller类级别使用注解@CsrfTokenModel配置

    @Controller
    @RequestMapping("/csrf")
    @CsrfTokenModel
    public class CsrfController {
        @RequestMapping("/form")
       public String form() {
            return "csrf_form";
         }
     }

c.在Controller类级别使用注解配置

此种情况可以使得该controller下所有模板渲染。都可以通过宏获取token的参数名称和值

在方法级别使用注解@CsrfTokenModel配置

@Controller
@RequestMapping("/csrf")
public class CsrfController {
    @RequestMapping("/form")
    @CsrfTokenModel
    public String form() {
        return "csrf_form";
    }
}

如果需要CSRF Token校验的Controller或者方法过多时,当前框架还提供一种便利的方式, 即URL映射级别的自动生成方式,只需在application.properties文件中增加如下配置:

  • CSRF Token 自动生成的URL映射列表,多值使用逗号分隔(默认值为空)

  • 当前URL风格为正则表达式,风格值由配置项 spring.security.csrf.url.style 决定

    spring.security.csrf.token.urls = /csrf_token/**

  • CSRF Token 模型属性名称

    spring.security.csrf.token.model.attribute = csrfToken

后端配置好之后,在VM模板中,针对所有请求form表单,增加对应字段,确保每次请求都能带上

  • Velocity Template Code

     <form method="post" action="/form/submit">
     &nbsp; &nbsp; <input type="hidden" name="${csrfToken.parameterName}"      value="${csrfToken.token}">
     &nbsp; &nbsp; <input type="text" name="name"/>
     &nbsp; &nbsp; <br>
     &nbsp; &nbsp; <input type="submit" value="Submit"/>
     </form>
  • 渲染后的HTML

               
       

正常情况下如上图所示,渲染后,字段name为p_csrf, value为随机生成的值,同时会在cookie中放入对应字段。请注意,token的字段名一定是要从csrftoken这个obj中取出来的,不能在前端自定义,若要在后端更换字段名,请参考下面的『CSRF定制化功能』

2. ajax前后端分离

ajax发起请求的情况下,token无法直接渲染到页面上,通过下方途径解决该问题。

  • 在cookie中读取token,将其带入到ajax请求的参数中。然后传到后端(Cookie中token的key默认为XSRF-TOKEN)

  • 若cookie中的XSRF-TOKEN值无法被js读取,请检查该值httponly属性未true,若为true,请在"application.properties"中新增一个配置项,如下:

  • 如果是在参数中携带,默认Token名称是_csrf,如果是在header中携带,默认Token名称是X-XSRF-TOKEN

    spring.security.csrf.cookieHttpOnly = false

  • 设置完成之后,请清除浏览器缓存之后重新尝试获取XSRF-TOKEN值

3. 跨域下的token传输

在一般业务场景下,安全包会将token种到服务端对应的域名cookie下,可以被前端js调用和植入到header或者参数中。但是在跨域场景下,前端页面与后端服务端不是同一个域名,导致无法取到服务端域名下的cookie。

假设aaaa.com要跨域访问bbbb.com的接口,bbbb.com的接口做了csrf校验。此时按照如下步骤进行token交互:

开启CORS跨域头的业务解决方案如下:

1. bbbb.com新增一个接口,返回自身的csrf token

该接口实现示例如下:

    @RequestMapping(value = "/ajax", produces = MediaType.APPLICATION_JSON_VALUE)
&nbsp; &nbsp; @CrossOrigin(origins = "http://aaaaaa.com:7001", maxAge = 3600)
&nbsp; &nbsp; @ResponseBody
&nbsp; &nbsp; public CsrfToken getCsrfToken(HttpServletRequest request, HttpServletResponse response) {
&nbsp; &nbsp; &nbsp; &nbsp; CsrfToken csrfToken = csrfTokenRepository.loadToken(request);
&nbsp; &nbsp; &nbsp; &nbsp; if (csrfToken == null) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; csrfToken = csrfTokenRepository.generateToken(request);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; csrfTokenRepository.saveToken(csrfToken, request, response);
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; return csrfToken;

&nbsp; &nbsp; }
2. aaaa.com携带with-credentials的头部来获取该token
function callOtherDomain(){
 &nbsp; &nbsp; &nbsp; var xhr = new XMLHttpRequest();
&nbsp; &nbsp; &nbsp; &nbsp; if(xhr) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; xhr.open('GET', 'http://bbbbbb.com:7001/csrf/ajax', true);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; xhr.withCredentials = true;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; xhr.onload = function () {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; result.innerHTML = xhr.responseText;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; var json = JSON.parse(xhr.responseText);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; token_key = json.paramterName;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; token = json.token;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; };
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; xhr.send(null);
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; }
3. 将获取到的token存放在客户端上(比如localstorage,或者页面隐藏字段中)
4. aaaa.com 访问bbbb.com其他接口的时候,获取token作为接口参数/头部参数传递给bbbb.com,同时访问该接口时应该设置withcredentials = true:
function buttonClick(token_key, token){
&nbsp; &nbsp; &nbsp; &nbsp; var xhr = new XMLHttpRequest();
&nbsp; &nbsp; &nbsp; &nbsp; xhr.open('GET', 'http://bbbbbb.com:7001/csrf/cors/check');
&nbsp; &nbsp; &nbsp; &nbsp; xhr.withCredentials = true;
&nbsp; &nbsp; &nbsp; &nbsp; xhr.onload = function () {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; result.innerHTML = xhr.responseText;
&nbsp; &nbsp; &nbsp; &nbsp; };
&nbsp; &nbsp; &nbsp; &nbsp; xhr.onerror = function () {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; result.innerHTML = "Error!";
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; xhr.send(token_key + '=' + token + '&' + otherparams)
&nbsp; &nbsp; }

Step3. 后端进行Token校验

后端进行token校验,目前只提供全局url检查方式,在没有显式配置情况下默认对所有POST请求进行token检查,为了更好的对业务进行支持,建议在classpath下的application.properties进行显式配置,如下:

//根据业务需求进行配置是否拦截GET请求,安全要求POST请求必须拦截

spring.security.csrf.supportedMethods = POST,GET

//使用ant风格配置需要进行token检查的url(安全要求对所有增删改进行token校验)

spring.security.csrf.url.included = /**&nbsp;

//使用ant风格配置无需需要进行token检查的url,只能对查询接口进行excluded

spring.security.csrf.url.excluded = /csrf/nocheck

校验之后,若成功,则会顺利执行对应后台功能,若失败,则会返回403的状态码或者301跳转taobao.error的情况,如下:

status : 403
message : Invalid CSRF Token '' was found on the request parameter 'p_csrf' or header 'h_csrf'.
===&nbsp;
status: 301
location: err2.taobao.com