Spring Boot2.x-13前后端分离的跨域问题解决方法之Nginx

版权声明:【show me the code ,change the world】 https://blog.csdn.net/yangshangwei/article/details/87746706

概述

随着前后端分离这种开发模式的普及,前台和后台分开部署,可能部署在一台主机上不同的端口下,也有可能部署在多个主机上,前后台通过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

刚才概述中
在这里插入图片描述

再细化点

  1. 用户向服务器发送请求,比如登录操作发送用户名和密码

  2. 服务器验证通过后,通过HttpServletRequest#getSession()#setAttribute等方法保存相关数据

  3. 服务器向用户返回一个 session_id,浏览器set-cookie Cookie 即Cookie = session_id

  4. 用户随后的每一次请求,都会通过 Cookie,将 session_id 传回服务器。

  5. 服务器收到 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 Value
	   proxy_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 header
			   if ($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丢失的问题

猜你喜欢

转载自blog.csdn.net/yangshangwei/article/details/87746706