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的異常。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章