AJAX以及跨域情况下POST请求出现CSRF问题的解决方案

CSRF是什么?

跨站点请求伪造(Cross-Site Request Forgery,CSRF)是一种常见的网络攻击手段。由于http协议本身无状态,客户端与服务端在基于http协议进行数据交互时,常利用cookie进行服务端和客户端的之间交互的状态和数据的记录,因此cookie里面可能会放置服务端生成的session id(会话ID)和用来识别客户端访问服务端过 中的客户端的身份标记,那么恶意站点就有可能通过正常站点中存在的cookie信息来伪造用户的请求,会给服务器代理很大的危险。CSRF的示意图如下:

为克服CSRF的危险,Spring Security提供了Token的机制来方式CSRF攻击。在Spring Boot项目中引入Spring Security的maven依赖即可以默认开启Spring Security的配置,此时会默认开启CSRF验证。此时具有如下的一些特性:

  • CSRF验证不针对GET类型请求,仅针对POST请求。
  • Spring Security开启后会自动默认生成CSRF参数(包括TOKEN值),当POST请求时需要请求携带对应的TOKEN值用于验证。如果请求中不存在验证参数或者验证参数和服务端保存的不一致,则认为是异常的请求则会出现403异常返回值。

CSRF的参数不建议存放在cookie中(因为会被恶意请求得到),当前后端不分离是可以存放到DOM中进行保存,Spring在返回页面时自动填充CSRF参数。但对于前后端分离的情况(跨域环境),下面的解决方案中还是将CSRF参数放到cookie中进行保存,并通过开启withCredentials设置允许cookie的跨站点发送。

AJAX中解决CSRF问题

系统和环境描述:

  • Spring Boot开启Security(会自动开启CSRF机制)
  • JSP页面(Spring Boot项目中的JSP,无跨域情况)
  • AJAX POST请求
  • 请求出现403异常

简单的禁用CSRF

在继承了WebSecurityConfigurerAdapter的Spring管理类的configure方法中加入http.csrf().disable()代码即可以禁用Spring Security开启的CSRF机制。可以解决上述的POST请求403问题。

@Configuration
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
    protected void configure(HttpSecurity http) throws Exception {
        // 根据角色设置访问权限
        setRoleAuthorize(http);
        // 禁用CSRF
        disableCsrf(http);
    }

    private void disableCsrf(HttpSecurity http) throws Exception {
        http.csrf().disable();
    }

设置AJAX请求

如果需要使用安全认证,则可以在不禁用CSRF的情况下,在JSP页面进行如下的代码设置。首先在JSP页面的<head>标签中加入如下的<meta>标签,加入后在Spring相应该页面后会自动填充其中的csrf字段的值

    <meta name="_csrf" content="${_csrf.token}"/>
    <meta name="_csrf_header" content="${_csrf.headerName}"/>

然后在AJAX请求前,加入如下的代码设置。这种代码的设置也是Spring Boot文档中推荐使用的:

     $(document).ready(function () {
            // 设置请求头
            var token = $("meta[name='_csrf']").attr("content");
            var header = $("meta[name='_csrf_header']").attr("content");

            $(document).ajaxSend(function (e, xhr, options) {
                xhr.setRequestHeader(header, token);
            });

            // 自己的POST请求方法
            $("#submit").click(function () {
                var userId = $("#userId").val();
                var productId = $("#productId").val();
                var quantity = $("#quantity").val();

                var params = {
                    userId: userId,
                    productId: productId,
                    quantity: quantity
                };

                $.post("./start", params, function(result) {
                    alert(result.msgInfo);
                });
            });
        })
    </script>

Form表单提交时设置隐藏字段

如果是在Form表单POST提交时出现CSRF的问题,则可以在表单中加入下面的隐藏字段一并提交,即可以避免POST请求出现CSRF的问题:

    <form action="/signOut" method="POST">
        # 表单其他内容
        # ...
        # 表单隐藏字段
        <input type="hidden" id="${_csrf.parameterName}" name="${_csrf.parameterName}" value="${_csrf.token}"/>
    </form>

此外还有如下两种方式,暂时未进行测试。

1. 直接设置POST请求header:

var headers = {};
headers['X-CSRF-TOKEN'] = "[[${_csrf.token}]]";
$.ajax({
    url: url,
    type: "POST",
    headers: headers,
    dataType: "json",
    success: function(result) {
    }
});

2. 直接作为POST请求参数进行设置:

$.ajax({
    url: url,
    data: {
        "[[${_csrf.parameterName}]]": "[[${_csrf.token}]]"
        },
    type: "POST",
    dataType: "json",
    success: function(result) {
    }
});

跨域情况下使用Axios时解决CSRF问题

该情况下使用前后端分离的程序设计思想,跨域环境如下:

  • 服务端:Spring Boot项目(开启Spring Security)
  • 前端:Ice(封装React)
  • 请求工具:Axios GET/POST(GET请求不存在跨域问题)
  • 发送Post请求时同样出现403异常

解决跨域情况下CSRF问题

对于前后端分离设置的跨域情况下的CSRF问题解决,由于前端页面之间的转换不在经过Spring服务端,因此Spring生成的CSRF参数无法直接放置到页面内作为请求传入。

因此下面使用cookie存放Spring生成的CSRF参数,并实现跨域环境下的cookie请求的方式解决该问题。具体如下:

1. Axios POST请求中设置withCredentials: true(该值默认为false),开启允许跨域发送cookie验证。

axios({
      method: 'post',
      url: 'http://localhost:8080/react-user/create',
      headers: {
        'Accept': 'application/json',
        'Content-Type': 'application/json'
      },
      data: {
        userName: username,
        sex: sex,
        note: note
      },
      withCredentials: true, // 开启跨域cookie验证(同时需要Spring Boot服务端也开启对应的设置)
      // xsrfCookieName: 'XSRF-TOKEN', // 这里实际上是默认的设置,所以可以不设置
    }).then(function (response) {
      console.log(response);
    }).catch(function (error) {
      console.log(error);
    });

同时需要注意,在axios中已经默认设置了如下的字段用于XSRF。因此对于请求头中可以不设置XSRF-TOKEN来发送请求。

  // `xsrfCookieName` is the name of the cookie to use as a value for xsrf token
  xsrfCookieName: 'XSRF-TOKEN', // default

  // `xsrfHeaderName` is the name of the http header that carries the xsrf token value
  xsrfHeaderName: 'X-XSRF-TOKEN', // default

 2. Spring Boot服务端设置Access-Control-Allow-Credentials: true响应头,与上述前端设置配合来开启跨域环境下cookie的正确发送。在IoC中加入如下的Bean:

    @Bean
    public WebMvcConfigurer webMvcConfigurer() {
        return new WebMvcConfigurer() {
            @Override
            public void addCorsMappings(CorsRegistry registry) {
                // spring boot跨域设置
                registry.addMapping("/**")
                        //设置允许跨域请求的域名
                        .allowedOrigins("*")
                        //是否允许证书 不再默认开启(在跨域情况下使用cookie时开启,需要axios开启该对应的选项)
                        .allowCredentials(true)
                        //设置允许的方法
                        .allowedMethods("*")
                        //跨域允许时间
                        .maxAge(3600);
            }
        };
    }

此时如果只在前端中设置了withCredentials: true,服务端没有开启allowCredentials(true),则请求时会出现如下的异常:

The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'.

注:并且如果服务端没有上述设置,则跨域环境下前端无法获取到cookie中的值,输出为空。加上以后才能够获取到cookie中的值。

3. 配置Spring Security使用cookie来存储XSRF_TOKEN的值。完成上述配置后,服务端和客户端就可以在跨域环境下来发送cookie完成CSRF验证了,但还需要在Spring Security中将需要验证的XSRF_TOKEN的值放入到cookie中,才能供客户端获取并发送。因此需要继承了WebSecurityConfigurerAdapter的Spring管理类的configure方法中加入如下代码:

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

注意:同时完成以上3点配置,才可以在跨域环境下正常的使用POST请求,并开启CSRF验证。否则还是会出现请求403的异常。

发布了322 篇原创文章 · 获赞 64 · 访问量 4万+

猜你喜欢

转载自blog.csdn.net/yitian_z/article/details/104622752