javascript
Spring Boot2.x-13前后端分离的跨域问题解决方法之Nginx
文章目錄
- 概述
- 瀏覽器同源策略
- 后臺搭建
- pom.xml
- interceptor 配置
- Controller
- 啟動測試
- 瀏覽器和session
- 后端工程發布到服務器上
- 問題復現
- 通過Nginx反向代理解決跨域問題
- 安裝Nginx
- 修改配置文件
- 修改前臺頁面訪問地址
- 原因分析
- 啟動Nginx 測試
- 小結
概述
隨著前后端分離這種開發模式的普及,前臺和后臺分開部署,可能部署在一臺主機上不同的端口下,也有可能部署在多個主機上,前后臺通過ajax或者axios等方式調用restful接口進行交互。由于瀏覽器的“同源策略”,協議、域名、端口號但凡有一個不同,勢必會產生跨域問題。
如果發生跨域的話,瀏覽器中每次請求的session都是一個新的,即sessionId肯定不相同。
我們知道 ,服務器可以為每個用戶瀏覽器創建一個session對象。默認情況下一個瀏覽器中獨占一個session.
http請求是無狀態的,那服務器是如何知道多次瀏覽器的請求是同一個會話呢?
事實上服務器創建session出來后,會將session的id,以cookie的形式回寫給客戶機,這樣,只要瀏覽器不關,再去訪問服務器時,都會帶著session的id號去,服務器發現客戶端瀏覽器攜帶session id過來了,就會使用內存中與之對應的session為之服務。 下文配合代碼和瀏覽器一起來看下。
瀏覽器同源策略
參考阮一峰老師的文章:瀏覽器同源政策及其規避方法
后臺搭建
為了簡單,我們使用Spring Boot 快速搭建個后臺服務,提供restful接口。 我這里加上了interceptor,其實驗證這個問題,沒必要加。 加上一方面是熟悉下攔截器的使用,二來也可以看下request中請求的URI
pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.1.3.RELEASE</version><relativePath /> <!-- lookup parent from repository --></parent><groupId>com.artisan</groupId><artifactId>CrossDomain</artifactId><version>0.0.1-SNAPSHOT</version><name>CrossDomainByNginxBackground</name><description>Artisan </description><properties><java.version>1.8</java.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-devtools</artifactId><optional>true</optional><scope>true</scope></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>interceptor 配置
不多說了,MyInterceptor.java 參考 Spring Boot2.x-12 Spring Boot2.1.2中Filter和Interceptor 的使用
按照工程中restful的設計,注意下 WebConfig中的攔截路徑即可。
Controller
package com.artisan.controller;import javax.servlet.http.HttpServletRequest;import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController;@RestController @RequestMapping("/artisan") public class ArtisanController {@GetMapping("/getValueFromSession")public String getSession(HttpServletRequest request) {// 獲取當前request的session,將屬性設置到session里request.getSession().setAttribute("artisan", "artisanTest");return "sessionId:" + request.getSession().getId() + ", artisans的屬性值:" + request.getSession().getAttribute("artisan");}@GetMapping("/checkCrossDomain")public String checkCrossDomain(HttpServletRequest request) {return "sessionId:" + request.getSession().getId() + ", artisans的屬性值:" + request.getSession().getAttribute("artisan");}}啟動測試
沒在application.yml中指定server.port ,使用了默認的8080端口,啟動項目,確保可以訪問
http://localhost:8080/artisan/getValueFromSession
不要關閉瀏覽器,繼續訪問
http://localhost:8080/artisan/checkCrossDomain
注意下這兩個sessionId是一樣的,說明是同一個session
瀏覽器和session
剛才概述中
再細化點
用戶向服務器發送請求,比如登錄操作發送用戶名和密碼
服務器驗證通過后,通過HttpServletRequest#getSession()#setAttribute等方法保存相關數據
服務器向用戶返回一個 session_id,瀏覽器set-cookie Cookie 即Cookie = session_id
用戶隨后的每一次請求,都會通過 Cookie,將 session_id 傳回服務器。
服務器收到 session_id,找到前期保存的數據,由此得知用戶的身份。
當然了單節點的情況下還好,如果是集群環境,或者是跨域的服務請求,那么久需要實現session 數據共享,使集群中的每臺服務器都能夠讀取 session。
總的來說【集群環境下】我目前所了解的有三種思路
-
session復制,比如Tomcat支持的Session復制. 優點:tomcat內置支持 缺點:如果集群過大,session 復制為all to all占用帶寬,效率不高
-
session 數據持久化,寫入redis或者數據庫等。優點架構清晰,缺點是工程量大。而且也需要考慮session數據的持久層的高可用,否則單點登錄就會失敗。
-
服務端不保存 session ,所有數據都保存在客戶端,比如 JWT (JSON WEB TOKEN)
我們清空瀏覽器的緩存(包括cookie)
結合上面建好的工程來演示下上面的描述。
重新訪問 http://localhost:8080/artisan/getValueFromSession
上面的截圖就是: 服務器創建session出來后,會將session的id,以cookie的形式回寫給客戶機
不要關閉瀏覽器,新開個窗口訪問
http://localhost:8080/artisan/checkCrossDomain
上面的截圖就是: 只要瀏覽器不關,再去訪問服務器時,都會帶著session的id號去,服務器發現客戶端瀏覽器攜帶session id過來了,就會使用內存中與之對應的session為之服務
后端工程發布到服務器上
把剛才的spring boot 服務端,達成了可執行的jar 【sts 工程右鍵-- Run As --Maven build , 輸入clean package (清除、打包)】 ,放到192.168.31.34服務器上 , 為了創造一個不同的ip地址。 順便我把端口號也通過啟動腳本設置成了9000
啟動腳本如下:
#!/bin/bash nohup java -jar CrossDomain-0.0.1-SNAPSHOT.jar --server.port=9000 > log.txt & tail -f log.txt問題復現
為了模擬【協議、域名、端口號但凡有一個不同,勢必會產生跨域問題 】,那就讓ip地址+端口號不同吧。
正好前幾天折騰axis , 搭建axis環境的時候,正好需要用tomcat去驗證下是否搭建成功(把axis拷貝到tomcat的webapps下),那順便借用下這里的index.html ,修改后的index.html如下
<!DOCTYPE html> <html lang="en"> <head><meta charset="UTF-8"/><title>Cross Domain Test</title> </head> <body> <h2>Artisan</h2> <button type="submit" id="btn">跨域請求</button> <p id="crossDomainRequest1"></p> <p id="crossDomainRequest2"></p> </body><script src="https://code.jquery.com/jquery-3.2.1.min.js"></script> <script>$("#btn").click(function(event){$.ajax({url: 'http://192.168.31.34:9000/artisan/getValueFromSession',type: "GET",success: function (data) {$("#crossDomainRequest1").html("跨域訪問成功->getValueFromSession方法返回:" + data);$.ajax({url: 'http://192.168.31.34:9000/artisan/checkCrossDomain',type: "GET",success: function (data) {$("#crossDomainRequest2").html("跨域訪問成功->checkCrossDomain方法返回:" + data);}});},error: function (data) {$("#crossDomainRequest1").html("發生跨域錯誤!!");}});}); </script></html>啟動tomcat ,訪問 http://localhost:8080/axis/index.html ,點擊按鈕,觀察開發者工具中的Network和Console
點擊 getValueFromSession 查看,
服務端其實是返回了,也從側面說明了跨域問題是瀏覽器的“同源策略”導致,和服務端不相干。
再繼續看下報錯
Access to XMLHttpRequest at ‘http://192.168.31.34:9000/artisan/getValueFromSession’ from origin ‘http://localhost:8080’ has been blocked by CORS policy: No ‘Access-Control-Allow-Origin’ header is present on the requested resource.
如上 發生了跨域問題。
通過Nginx反向代理解決跨域問題
原理: Nginx的反向代理“欺詐”瀏覽器,使得瀏覽器和服務器是同源訪問。
安裝Nginx
因為要測試跨域 ,為了方便,服務端放到了服務器上,使用Nginx部署的前臺我們就放到本地吧,所以使用了windows版本的Nginx 。
Nginx 下載地址: http://nginx.org/en/download.html
修改配置文件
worker_processes 1;events {worker_connections 1024; }http {include mime.types;default_type application/octet-stream;sendfile on;keepalive_timeout 65;#前端頁面服務器信息server {#啟動的端口和域名listen 8888; server_name localhost;#添加頭部信息,proxy_set_header用來重定義發往后端服務器的請求頭。#語法 proxy_set_header Field Valueproxy_set_header Cookie $http_cookie;proxy_set_header X-Forwarded-Host $host;proxy_set_header X-Forwarded-Server $host;proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;#代理地址 及映射的服務端的地址 # 最重要的配置 location /frontend/ { proxy_pass http://192.168.31.34:9000/; #使用代理地址時末尾加上斜杠"/" # 如下 proxy_set_header 和 add_header 不加經過驗證也是OK的。# 使用add_header指令來設置response headerif ($request_method = 'OPTIONS') {add_header 'Access-Control-Allow-Origin' '*';add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range,Token';add_header 'Access-Control-Max-Age' 1728000;add_header 'Content-Type' 'text/plain; charset=utf-8';add_header 'Content-Length' 0;return 204;}if ($request_method = 'POST') {add_header 'Access-Control-Allow-Origin' '*';add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range,Token';add_header 'Access-Control-Expose-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range,Token';}if ($request_method = 'GET') {add_header 'Access-Control-Allow-Origin' '*';add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range,Token';add_header 'Access-Control-Expose-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range,Token';} }#添加攔截路徑和根目錄location / {root html/artisan; # 根目錄index index.html index.htm; #首頁} } }最重要的是 proxy_pass配置
關于add_header ,比如 GET 增加了 add_header ,在瀏覽器中GET請求的方法可以在response header查看到相關信息
add_header ‘Access-Control-Expose-Headers’ 必須要加上你請求時所帶的header,比如我們經常用的Token
參考: https://enable-cors.org/server_nginx.html
下面的瀏覽器返回截圖,是沒有增加add_header的,故特意貼一張截圖如上,增加上也是OK的,更細粒度的控制,請知悉。
修改前臺頁面訪問地址
<!DOCTYPE html> <html lang="en"> <head><meta charset="UTF-8"/><title>Nginx Cross Domain Test</title> </head> <body> <h2>Artisan</h2><button type="submit" id="btn">跨域請求</button><p id="crossDomainRequest1"></p> <p id="crossDomainRequest2"></p> </body><script src="https://code.jquery.com/jquery-3.2.1.min.js"></script> <script>$("#btn").click(function(event){$.ajax({url: 'http://localhost:8888/frontend/artisan/getValueFromSession',type: "GET",success: function (data) {$("#crossDomainRequest1").html("跨域訪問成功->getValueFromSession方法返回:" + data);$.ajax({url: 'http://localhost:8888/frontend/artisan/checkCrossDomain',type: "GET",success: function (data) {$("#crossDomainRequest2").html("跨域訪問成功->checkCrossDomain方法返回:" + data);}});},error: function (data) {$("#crossDomainRequest1").html("發生跨域錯誤!!");}});}); </script></html>原因分析
先看index.html的存放位置
與 nginx的配置文件中如下配置保持一致
同時配置的啟動端口和域名,對應配置文件中的
所以通過訪問 http://localhost:8888/index.html 就找到了 html/artisan目錄下的index.html文件
再看下 index.html中修改的請求地址,由原先的直接請求后臺,改為請求Nginx,讓Nginx去轉發請求
localhost:8888上面說了,下面來看下這個frontend是個啥東西呢? 是自定義的,叫啥都行,只要能對應上就行。
意思是讓Nginx代理該請求
html中的兩個地址經過Nginx后,發生如下變化
請求URL:http://localhost:8888/frontend/artisan/getValueFromSession
代理后的URL:http://192.168.31.34:9000/artisan/getValueFromSession
請求URL:http://localhost:8888/frontend/artisan/checkCrossDomain
代理后的URL:http://192.168.31.34:9000/artisan/checkCrossDomain
代理后的地址也是192.168.31.34:9000端口了,和服務端 192.168.31.34:9000一致,也就不存在跨域問題了。
跨域操作實際上是由Nginx的proxy_pass進行完成.
這個可以從控制臺中得到確認
啟動Nginx 測試
雙擊nginx.exe 啟動Nginx , 訪問 http://localhost:8888/index.html
訪問正常,且是通過一個session , 跨域問題使用Nginx得到解決。
小結
-
通過Nginx去解決跨域問題本質上是間接跨域,因為使用反向代理欺騙瀏覽器,所以瀏覽器任務客戶端和服務端在相同的域名中,可以認為是同源訪問,所以session不會丟失。上面的實驗結論也證明了這一點
-
如果使用CORS實現了直接跨域,主要是在服務端通過給response設置header屬性,幫助服務器資源進行跨域授權。 因為發生跨域訪問,服務器會每次都創建新的Session,會導致session丟失,安全性和靈活性更高,但需要開發人員去解決跨域session丟失的問題。
總結
以上是生活随笔為你收集整理的Spring Boot2.x-13前后端分离的跨域问题解决方法之Nginx的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Shell-使用和wait让你的脚本并行
- 下一篇: 并发编程-08安全发布对象之发布与逸出