天天看點

使用 Sa-Token 的全局過濾器解決跨域問題(三種方式全版)

作者:馬士兵老師
使用 Sa-Token 的全局過濾器解決跨域問題(三種方式全版)

在 web 開發中,跨域絕對是比較折磨新同學的一個問題,本文将講解三種常見的跨域情形,并講解如何使用 Sa-Token 架構解決跨域問題。

什麼情況下會發生跨域

簡單了解,就是你在 A 域名下的頁面,去調用 B 域名的接口,浏覽器感覺你這次調用可能是不安全的請求行為,于是它需要用 cors 安全政策來确認一下這個請求是由使用者真實的意願發出的,而不是被 csrf 僞造請求攻擊偷偷發送的。(這麼說隻是為了友善大家了解,不是特别嚴謹,實際上同域名下部分情形也會出現跨域問題)

請仔細了解上面這段話,因為它說明了兩點:

  • 跨域不是後端接口對前端浏覽器的限制,而是前端浏覽器對使用者的限制。
  • 跨域不是在保護後端接口免受攻擊,而是浏覽器在保護使用者,避免使用者發送自己不想發送的請求。

請一定要記住上面跨域的本質,明白了症狀和原因,我們才能對症下藥。

一般情況下,我們會碰到三種跨域場景:

  • 1、本地頁面調用測試伺服器,隻在項目開發階段會有跨域問題。(比較簡單)
  • 2、使用 header 頭送出 token,産生的跨域問題。(比較常見+通用,推薦使用)
  • 3、使用第三方 Cookie 送出 token,産生的跨域問題。(最古老的方案,目前新版浏覽器對此方案限制越來越嚴格,非必要不選擇此方案,如果對此方案不是很熟悉就貿然使用也容易出現安全問題)

跨域情形一:隻在項目開發階段會有跨域問題

有些公司項目的開發方式為:

  • 在項目開發時:使用本地頁面調用測試伺服器接口。(域名不同,存在跨域問題)
  • 在項目部署時:将後端接口和前端頁面部署在同一域名下。(域名一緻,不存在跨域問題)

這種情況下比較好解決,在代碼層面我們無需任何更改,隻在前端用戶端做出一定的更改就行了。比如說:在前端配置一個代理伺服器,或者修改一下 Chrome 用戶端使其去除跨域限制。

具體的方案有很多,大家可參考這篇部落格:手把手教你解決web前端跨域問題

上面是說的普通前後端分離開發,而在APP、小程式 開發中,其天然就是個沒有跨域限制的用戶端,我們什麼都不用做就能解決跨域問題。

跨域情形二:使用 header 頭送出 token,産生的跨域問題(比較常見+通用,推薦使用)

當你使用 header 頭送出 token 時,會産生跨域問題。此方案比較常見+通用,推薦使用。

jquery 代碼示例:

js複制代碼	$.ajax({
		url: "/user/getInfo",
		type: "post", 
		data: {},
		dataType: 'json',
		headers: {
			"X-Requested-With": "XMLHttpRequest",
			// 重點處:請求的 header 頭裡塞入自定義參數
			"satoken": localStorage.getItem("satoken")
		},
		success: function(res){
			console.log(res);
		},
		error: function(xhr, type, errorThrown){
			return alert("異常:" + JSON.stringify(xhr));
		}
	});
           

Axios 代碼示例:

js複制代碼    axios({
        url: "/user/getInfo",
        method: 'post',
        data: {},
        headers: {
            "Content-Type": "application/x-www-form-urlencoded",
			// 重點處:請求的 header 頭裡塞入自定義參數
            "satoken": localStorage.getItem("satoken")
        }
    }).
    then(function (response) { // 成功時執行
        const res = response.data;
		console.log(res);
    }).
    catch(function (error) {
        return alert("異常:" + JSON.stringify(error));
    })
           

此時在後端,我們應該添加以下響應頭:

java複制代碼/**
 * [Sa-Token 權限認證] 配置類 
 *
 * @author click33
 */
@Configuration
public class SaTokenConfigure implements WebMvcConfigurer {

	/**
     * 注冊 [Sa-Token 全局過濾器] 
     */
    @Bean
    public SaServletFilter getSaServletFilter() {
        return new SaServletFilter()
        		
        		// 指定 [攔截路由] 與 [放行路由]
        		.addInclude("/**").addExclude("/favicon.ico")
        		
        		// 認證函數: 每次請求執行 
        		.setAuth(obj -> {
					SaManager.getLog().debug("----- 請求path={}  送出token={}", SaHolder.getRequest().getRequestPath(), StpUtil.getTokenValue());
        			// ...
        		})
        		
        		// 異常處理函數:每次認證函數發生異常時執行此函數 
        		.setError(e -> {
        			return SaResult.error(e.getMessage());
        		})
        		
        		// 前置函數:在每次認證函數之前執行
        		.setBeforeAuth(obj -> {
					SaHolder.getResponse()

        			// ---------- 設定跨域響應頭 ----------
        			// 允許指定域通路跨域資源
        			.setHeader("Access-Control-Allow-Origin", "*")
        			// 允許所有請求方式
        			.setHeader("Access-Control-Allow-Methods", "*")
        			// 允許的header參數
        			.setHeader("Access-Control-Allow-Headers", "*")
        			// 有效時間
        			.setHeader("Access-Control-Max-Age", "3600")
        			;
        			
        			// 如果是預檢請求,則立即傳回到前端 
        			SaRouter.match(SaHttpMethod.OPTIONS)
        				.free(r -> System.out.println("--------OPTIONS預檢請求,不做處理"))
        				.back();
        		})
        		;
    }

}
           

如果你的項目是 WebFlux 環境,隻需要把過濾器名稱從 SaServletFilter 更換為 SaReactorFilter 即可,其它保持不變。

跨域情形三:使用第三方 Cookie 送出 token,産生的跨域問題。

這是最古老的方案,目前新版浏覽器對此方案限制越來越嚴格,非必要不選擇此方案,如果對此方案不是很熟悉就貿然使用也容易出現安全問題。

jquery 代碼示例:

js複制代碼	$.ajax({
		url: "/user/getInfo",
		type: "post", 
		data: {},
		dataType: 'json',
		// 重點處:指定是跨域模式,需要送出第三方 Cookie 
		crossDomain: true,
		xhrFields:{
			withCredentials: true
		},
		headers: {
			"X-Requested-With": "XMLHttpRequest"
		},
		success: function(res){
			console.log(res);
		},
		error: function(xhr, type, errorThrown){
			return alert("異常:" + JSON.stringify(xhr));
		}
	});
           

Axios 代碼示例:

js複制代碼    axios({
        url: "/user/getInfo",
        method: 'post',
        data: {},
		// 重點處:開啟第三方 Cookie 
		withCredentials: true,
        headers: {
            "Content-Type": "application/x-www-form-urlencoded"
        }
    }).
    then(function (response) { // 成功時執行
        console.log(res);
    }).
    catch(function (error) {
        return alert("異常:" + JSON.stringify(error));
    })
           

此時在後端,我們應該添加以下響應頭:

java複制代碼/**
 * [Sa-Token 權限認證] 配置類 
 *
 * @author click33
 */
@Configuration
public class SaTokenConfigure implements WebMvcConfigurer {

	/**
     * 注冊 [Sa-Token 全局過濾器] 
     */
    @Bean
    public SaServletFilter getSaServletFilter() {
        return new SaServletFilter()
        		
        		// 指定 [攔截路由] 與 [放行路由]
        		.addInclude("/**").addExclude("/favicon.ico")
        		
        		// 認證函數: 每次請求執行 
        		.setAuth(obj -> {
					SaManager.getLog().debug("----- 請求path={}  送出token={}", SaHolder.getRequest().getRequestPath(), StpUtil.getTokenValue());
        			// ...
        		})
        		
        		// 異常處理函數:每次認證函數發生異常時執行此函數 
        		.setError(e -> {
        			return SaResult.error(e.getMessage());
        		})
        		
        		// 前置函數:在每次認證函數之前執行
        		.setBeforeAuth(obj -> {

					// 獲得用戶端domain
					SaRequest request = SaHolder.getRequest();
					String origin = request.getHeader("Origin");
					if (origin == null) {
						origin = request.getHeader("Referer");
					}

        			// ---------- 設定跨域響應頭 ----------
					SaHolder.getResponse()
					// 允許第三方 Cookie 
					.setHeader("Access-Control-Allow-Credentials", "true")
        			// 允許指定域通路跨域資源
        			.setHeader("Access-Control-Allow-Origin", origin)
        			// 允許所有請求方式
        			.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE")
					// 允許的header參數
					.setHeader("Access-Control-Allow-Headers", "x-requested-with,satoken")
        			// 有效時間
        			.setHeader("Access-Control-Max-Age", "3600")
					;
        			
        			// 如果是預檢請求,則立即傳回到前端 
        			SaRouter.match(SaHttpMethod.OPTIONS)
        				.free(r -> System.out.println("--------OPTIONS預檢請求,不做處理"))
        				.back();
        		})
        		;
    }

}
           

如果你的項目是 WebFlux 環境,隻需要把過濾器名稱從 SaServletFilter 更換為 SaReactorFilter 即可,其它保持不變。