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

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

概述


随着前后端分离这种开发模式的普及,前台和后台分开部署,可能部署在一台主机上不同的端口下,也有可能部署在多个主机上,前后台通过ajax或者axios等方式调用restful接口进行交互。由于浏览器的“同源策略”,协议、域名、端口号但凡有一个不同,势必会产生跨域问题。


如果发生跨域的话,浏览器中每次请求的session都是一个新的,即sessionId肯定不相同。


我们知道 ,服务器可以为每个用户浏览器创建一个session对象。默认情况下一个浏览器中独占一个session.


http请求是无状态的,那服务器是如何知道多次浏览器的请求是同一个会话呢?


事实上服务器创建session出来后,会将session的id,以cookie的形式回写给客户机,这样,只要浏览器不关,再去访问服务器时,都会带着session的id号去,服务器发现客户端浏览器携带session id过来了,就会使用内存中与之对应的session为之服务。 下文配合代码和浏览器一起来看下。


浏览器同源策略


参考阮一峰老师的文章:浏览器同源政策及其规避方法


后台搭建

为了简单,我们使用Spring Boot 快速搭建个后台服务,提供restful接口。 我这里加上了interceptor,其实验证这个问题,没必要加。 加上一方面是熟悉下拦截器的使用,二来也可以看下request中请求的URI


20190220011209993.png


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中的拦截路径即可。


20190220011755873.png


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


20190220012328430.png

不要关闭浏览器,继续访问

http://localhost:8080/artisan/checkCrossDomain

20190220012509241.png

注意下这两个sessionId是一样的,说明是同一个session


浏览器和session

刚才概述中

20190220012750674.png

再细化点


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


服务器验证通过后,通过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)



20190220013216180.png


结合上面建好的工程来演示下上面的描述。

重新访问 http://localhost:8080/artisan/getValueFromSession


20190220013345519.png



上面的截图就是: 服务器创建session出来后,会将session的id,以cookie的形式回写给客户机

不要关闭浏览器,新开个窗口访问

http://localhost:8080/artisan/checkCrossDomain

20190220013649711.png


上面的截图就是: 只要浏览器不关,再去访问服务器时,都会带着session的id号去,服务器发现客户端浏览器携带session id过来了,就会使用内存中与之对应的session为之服务


后端工程发布到服务器上


20190220014520962.png


把刚才的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


20190220020511353.png



点击 getValueFromSession 查看,

20190220015528647.png

服务端其实是返回了,也从侧面说明了跨域问题是浏览器的“同源策略”导致,和服务端不相干。


再继续看下报错


20190220015815982.png

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



20190220020749500.png


修改配置文件

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


20190220142850997.png


下面的浏览器返回截图,是没有增加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的存放位置


20190220022401197.png


与 nginx的配置文件中如下配置保持一致


20190220022435704.png


同时配置的启动端口和域名,对应配置文件中的


20190220022638544.png


所以通过访问 http://localhost:8888/index.html 就找到了 html/artisan目录下的index.html文件


再看下 index.html中修改的请求地址,由原先的直接请求后台,改为请求Nginx,让Nginx去转发请求

20190220022744985.png



localhost:8888上面说了,下面来看下这个frontend是个啥东西呢? 是自定义的,叫啥都行,只要能对应上就行。


20190220022850465.png


意思是让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进行完成.


这个可以从控制台中得到确认

20190220023730519.png


启动Nginx 测试


双击nginx.exe 启动Nginx , 访问 http://localhost:8888/index.html


20190220022051216.png


访问正常,且是通过一个session , 跨域问题使用Nginx得到解决。


小结


通过Nginx去解决跨域问题本质上是间接跨域,因为使用反向代理欺骗浏览器,所以浏览器任务客户端和服务端在相同的域名中,可以认为是同源访问,所以session不会丢失。上面的实验结论也证明了这一点


如果使用CORS实现了直接跨域,主要是在服务端通过给response设置header属性,帮助服务器资源进行跨域授权。 因为发生跨域访问,服务器会每次都创建新的Session,会导致session丢失,安全性和灵活性更高,但需要开发人员去解决跨域session丢失的问题。

相关实践学习
基于函数计算快速搭建Hexo博客系统
本场景介绍如何使用阿里云函数计算服务命令行工具快速搭建一个Hexo博客。
相关文章
|
13天前
|
人工智能 JSON 前端开发
Spring Boot解决跨域问题方法汇总
Spring Boot解决跨域问题方法汇总
1天搞定SpringBoot+Vue全栈开发 (9)JWT跨域认证
1天搞定SpringBoot+Vue全栈开发 (9)JWT跨域认证
|
5天前
|
Java 关系型数据库 Docker
docker打包部署spring boot应用(mysql+jar+Nginx)
docker打包部署spring boot应用(mysql+jar+Nginx)
|
8天前
|
JSON 安全 前端开发
跨域详解及Spring Boot 3中的跨域解决方案
本文介绍了Web开发中的跨域问题,包括概念、原因、影响以及在Spring Boot 3中的解决方案。跨域是由浏览器的同源策略限制引起的,阻碍了不同源之间的数据传输。解决方法包括CORS、JSONP和代理服务器。在Spring Boot 3中,可以通过配置CorsFilter来允许跨域请求,实现前后端分离项目的正常运行。
61 3
 跨域详解及Spring Boot 3中的跨域解决方案
|
13天前
|
Java Spring
快速解决Spring Boot跨域困扰:使用CORS实现无缝跨域支持
这是一个简单的配置示例,用于在Spring Boot应用程序中实现CORS支持。根据你的项目需求,你可能需要更详细的配置来限制允许的来源、方法和标头。
29 3
|
13天前
|
前端开发 JavaScript Java
SpringBoot解决跨域访问的问题
本文介绍了跨域访问的概念及其解决方案。同源策略规定浏览器限制不符合协议、Host和端口的请求,导致跨域访问被禁止。为解决此问题,文中提出了三种策略:1) 前端利用HTML标签的特性(如script、iframe)和JSONP、postMessage规避同源策略;2) 通过代理,如nginx或nodejs中间件,使得所有请求看似来自同一源;3) CORS(跨域资源共享),通过设置HTTP响应头允许特定跨域请求。在SpringBoot中,实现CORS有四种方式,包括使用CorsFilter、重写WebMvcConfigurer、CrossOrigin注解以及直接设置响应头。
|
13天前
|
前端开发 Java 应用服务中间件
Springboot解决跨域问题方案总结(包括Nginx,Gateway网关等)
Springboot解决跨域问题方案总结(包括Nginx,Gateway网关等)
|
13天前
|
消息中间件 Java 关系型数据库
JAVA云HIS医院管理系统源码、基于Angular+Nginx+ Java+Spring,SpringBoot+ MySQL + MyCat
JAVA云HIS医院管理系统 常规模版包括门诊管理、住院管理、药房管理、药库管理、院长查询、电子处方、物资管理、媒体管理等,为医院管理提供更有力的保障。 HIS系统以财务信息、病人信息和物资信息为主线,通过对信息的收集、存储、传递、统计、分析、综合查询、报表输出和信息共享,及时为医院领导及各部门管理人员提供全面、准确的各种数据。
43 1
|
13天前
|
Java 应用服务中间件 Maven
SpringBoot 项目瘦身指南
SpringBoot 项目瘦身指南
65 0
|
13天前
|
缓存 安全 Java
Spring Boot 面试题及答案整理,最新面试题
Spring Boot 面试题及答案整理,最新面试题
144 0