理解跨域问题,前后端分离中,在springboot后端解决跨域问题

一:什么是跨域问题

源于JavaScript的同源策略。即只有 协议+主机名+端口号全部相同,才允许相互访问。如果其中有一个不同,正常情况下浏览器就会把收到的报文丢弃,然后报一个cors policy的错误。

二:出现情况(前后端分离开发vue+springboot)

我在本地用nginx服务器挂了一个端口127.0.0.1:10086 用来提供 静态页面;
而静态页面中需要用ajax请求127.0.0.1:8080的后端接口获取数据。
  • 前端请求
 axios.post("http://127.0.0.1:8080/test/hello").then(function(response){
		console.log(response);
	},function(error){
        console.log(error);
    })
  • 后端响应
@PostMapping("/test/hello")
public  AjaxResponse testhello(){
    log.info("hello");
    return "hello";
}

  • 当前端页面按钮触发ajax请求后,会有cors policy的报错。

三:报错原因

  1. 首先明确,这个页面发起的ajax请求是被我的后端请求给接受到了的,进入了Controller里面处理了逻辑业务;而且最后服务器是把Response响应给返回了!
  2. 那么为什么还会报错呢?!原因在于cors policy同域策略,通俗地讲就是浏览器在接受到这个Response的时候才反应过来,这个Response如果浏览器他接收的话他就是违规的,所以浏览器选择遵守规定放弃了这个Response体,并弹出cors policy错误警告。

四:解决跨域问题(springboot后端解决)

  1. 要解决这个问题也很简单,只需要告诉浏览器“这个报文你接受吧,我同意你不遵守cors规则”就行了。这样浏览器就不会扔掉数据报错了。

  2. 如何告诉浏览器呢?!这时候我们需要在Response报文中加一个响应头Header信息,也就是Access-Control-Allow-Origin:【被允许跨域访问的源】;

理解跨域问题,前后端分离中,在springboot后端解决跨域问题_第1张图片

  1. 在springboot有几种处理的方法,但要知道所有方法的最后其实都是给Response加上了一个Header信息,告诉浏览器不要扔掉信息,都是这个原理。
  • 方法一: 用httpServletResponse封装好的类直接给返回头加上这个信息(此方法用于理解…)。

    @PostMapping("/hello")
        public  AjaxResponse testhello(HttpServletResponse response){
            response.setHeader("Access-Control-Allow-Origin","http://127.0.0.1:10086");
            log.info("hello");
            return AjaxResponse.success("hello");
    }
    
  • 方法二:实现WebMvcConfigurer接口,然后重写addCorsMappings(CorsRegistry registry)方法。

    @Configuration
    public class WebMvcConfig implements WebMvcConfigurer {
       @Override
        public void addCorsMappings(CorsRegistry registry) {
            String mapping = "/**"; // 所有请求,也可配置成特定请求,如/api/**
            String origins = "http://localhost:10086"; // * 表示所有来源,也可以配置成特定的来源才允许跨域,如http://www.xxxx.com
            String methods = "*"; // 所有方法,GET、POST、PUT等
            Boolean allowCredentials = true; // 表示是否允许携带cookie  //解决Session问题
            long maxAge =30 * 1000; //表示探测请求通过后,保持认证的时间。 //这个探测请求是针对复杂请求设计的,最后面说明
            registry
                .addMapping(mapping)
                .allowedOrigins(origins)
                .allowedMethods(methods)
    //          .allowCredentials(true)
    //          .maxAge(maxAge)
            ;
        }  
    }
    

    实际上只是针对跨域问题,只需要配置好mapping origins 和methods三个参数就好了,配置好了以后再次用前端向后端发起跨域请求,就会发现不再报错了。

    这个方法二有个弊端,就是如果你还配置了拦截器,那么就会产生冲突。最后面记录我的填坑过程。

  • 方法三:

    实现Filter接口重写doFilter方法,过滤器中添加头部

@WebFilter(filterName = "MyFilter",urlPatterns = "/*")
public class CorsFilterConfig implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
        HttpServletResponse httpServletResponse = (HttpServletResponse) servletResponse;
        //指定允许其他域名访问
        httpServletResponse.setHeader("Access-Control-Allow-Origin", "*");
		
		//前端携带cookie时,也就是写了 {withCredentials: true},后端不能用通配符,
        //得用"Access-Control-Allow-Origin",httpServletRequest.getHeader("origin")
        
            //响应头设置
        httpServletResponse.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");

        //允许携带cookie
        httpServletResponse.setHeader("Access-Control-Allow-Credentials", "true");

        //响应类型
        httpServletResponse.setHeader("Access-Control-Allow-Methods", "POST, GET, PUT, OPTIONS, DELETE");

        //option验证后时间
        httpServletResponse.setHeader("Access-Control-Max-Age", "3600");

        filterChain.doFilter(servletRequest, servletResponse);
    }

    @Override
    public void destroy() {

    }
}

五:解决Session维护问题:

5.1用方法二解决:

  1. 假设,当我们用方法二重写addCorsMappings(CorsRegistry registry)后,能够跨域请求了。

    会出现一个问题。那就是ajax的请求是不会自动携带cookie的,意味着后端无法凭借前端cookie记录的JSessionId来获取后端存储的Session

    通俗讲就是每一次ajax请求后端都会视作为不同的用户,每次都会给这个ajax请求的Response消息头里设置一个新的“Set-Cookie:JSESSIONID=44AA8F3EFF388BD7ADE0551BC33FADB”。

  2. 如何解决?很简单,让前端页面发起ajax请求的时候让它带上所有cookie不就好了!同时我们返回的Response里面需要加上一个头信息"Access-Control-Allow-Credentials:true"。

理解跨域问题,前后端分离中,在springboot后端解决跨域问题_第2张图片

  • 具体步骤如下

  • 第一步让前端的ajax部分加上withCredentials: true这个参数,保证前端会带上cookie。

    比如我这里用的axios就这样写

   axios.post("http://127.0.0.1:8080/test/hello", {withCredentials: true}).then(function(response){
   		console.log(response);
   	},function(error){
           console.log(error);
       })

或者用Jquery封装的ajax这样加

   $.ajax({
               url: "http://localhost:8080/orders",
               type: "GET",
               xhrFields: {
                   withCredentials: true
               },
               success: function (data) {
                   render(data);
               }
    })
  • 第二步,在后端addCorsMappings(CorsRegistry registry)方法里添加 allowCredentials(true)。

    (注意,前端授权了cookie发送,也就是配置了withCredentials: true,那么后端allowedOrigins(origins)这个参数origins就不能写 * 匹配所有,而且allowCredentials(allowCredentials)必须是true。)

    我理解意思就是前端把cookie全带过来了,后端得指定要求需要的一个域里面的cookie,毕竟不能全要。

     @Configuration
     public class WebMvcConfig implements WebMvcConfigurer {
        @Override
         public void addCorsMappings(CorsRegistry registry) {
             String mapping = "/**"; 
             String origins = "http://localhost:10086"; //写成String origins = "*"; 错误,这时候需要指定明确的域
             String methods = "*"; 
             Boolean allowCredentials = true; // 表示是否允许携带cookie  //解决Session问题
             registry
                 .addMapping(mapping)
                 .allowedOrigins(origins)
                 .allowedMethods(methods)
                 .allowCredentials(allowCredentials)
             ;
         }  
     }

这样你会发现session就被同步了,每次Ajax的请求的Response都不会被重新赋值新的JSessionId了。用Session记录登陆信息也可以实现了。

5.2用方法三解决:

同方法二介绍的一样

  1. 用Filter进行实现跨域并维护session,前端同样要在发送请求的时候添加{withCredentials: true}参数,
  2. 后端的filter中也要在response中添加头部信息 httpServletResponse.setHeader(“Access-Control-Allow-Credentials”, “true”)。和。

六:所谓的addCorsMappings与拦截器冲突问题

  • 如果用第二种方法解决跨域和session维护问题,而且你后端还要配值拦截器,那么就有可能出现问题,就是很多网友说的冲突问题。
  • 总结记录如下:

6.1 首先要了解option请求。

  • options请求又叫做预检请求(我选用json格式导致请求变成了需要先发送预检请求的复杂请求),在真正的请求发送出去之前,浏览器会先发送一个options请求向服务询问此接口是否允许我访问。也就是说,你的数据请求实际上浏览器发送了两个请求。第一次是preflight,也就是options请求,用于请求验证, 第二次才是我真正需要发送的Post请求。

  • 预检请求的头信息参数含义。

    在还没有配置拦截器的情况下用方法二或者方法三解决跨域问题和session维护问题后,预检请求有下列正常参数。

  • 预检请求发出Request头部信息中有两个参数:

    Access-Control-Request-Headers: content-type  //告知服务器,实际请求携带自定义请求首部字段Content-Type
    Access-Control-Request-Method: POST  //告知服务器,实际请求将使用 POST 方法
    
  • 预检请求返回的Response头信息中的重要参数:

    Access-Control-Allow-Origin: http://localhost:10086  //表明服务器允许跨域,允许的域是http://localhost:10086
    Access-Control-Allow-Methods: POST, GET, OPTIONS  // 表明服务器允许客户端使用 POST,GET 和 OPTIONS 方法发起请求
    Access-Control-Allow-Headers: Content-Type  // 表明服务器允许请求中携带字段Content-Type
    Access-Control-Max-Age: 1800  //这个预检请求认证通过的有效期为1800秒,在有效时间内,浏览器无须为同一请求再次发起预检请求,请注意,浏览器自身维护了一个最大有效时间
    
  • 在没有配置拦截器的情况下,一切都正常有序。

6.2 配值拦截器之后的冲突。

  • 配置了拦截器就意味着必然会拦截到第一次的option请求。因为option请求是不会携带参数信息例如cookie,token那些个,所以这个时候你的逻辑没有对option进行特殊处理的话,就有极大可能不放行此次option请求。就会出现两种异常情况。
  • 第一种异常
  1. 用方法二的addCorsMappings方法解决跨域和session;然后配置了拦截器阻拦了option请求(阻拦的原因包括抛出异常或者拦截器return了false)。这个时候你会发现第二次的请求根本不允许发送。这次是浏览器根本不允许发出去,不同于最开始讨论的等到服务器返回了浏览器才扔掉。

  2. 检查option请求返回的Response体,发现添加的允许跨域的几个头部信息没有被添加上。
    理解跨域问题,前后端分离中,在springboot后端解决跨域问题_第3张图片

  3. 原因:重写addCorsMappings()来给Response添加Access-Control-Allow-Origin头部信息的操作也是在拦截器的某一步操作里做的,这就导致Option请求在被拦截后由于不被放行所以连同导致后面给添加Access-Control-Allow-Origin等头部信息的操作也没能执行。所以第二次的正真请求Request里面也不会被允许发送。 这就是很多网友遇到的冲突问题。

  4. 解决方法A:直接换第三种的重写filter方法。filter优先于拦截器,这样添加跨域的头部和拦截option就不会搅和在一起了。就算我的option请求被拦截不被放行,也不影响我添加允许跨域和维护session的标志,因为filter优先于其他拦截器,无论后面操作如何都先把重要的头部信息给我添加上了再说。

  5. 解决方法B:无论怎么样拦截器都放行option请求,

if(request.getMethod().equals("OPTIONS")){
   return true;
}
  • 第二种异常
  1. 用方法三重写filter解决上述所有问题,基本没啥异常了,但是这只是针对拦截器里面没有抛出异常的情况。如果我在拦截器里手动抛出了异常阻拦了option请求,而不是正常的return了false阻拦,那么第二次的正真请求同样也不被允许发送出去。

  2. 前端报错如下,没有正确的status。
    在这里插入图片描述

  3. 解决方法A:取消手动抛出异常。。

  4. 解决方法B:无论怎么样拦截器都放行option请求。

if(request.getMethod().equals("OPTIONS")){
   return true;
}

七:总结:

  • springboot后端处理跨域问题有两个主要方法:
  1. 重写addCorsMappings(CorsRegistry registry)
  2. 实现Filter接口重写doFilter方法
  • 如果还需要用拦截器来实现一些业务逻辑,比如我在拦截器里面用session判断是否登陆,而且还要手动抛出异常的话。那么最好用实现Filter接口重写doFilter()的方法,并且针对option请求进行放行操作。原因已经在上面记录了。
  • 全剧终,希望对你我有帮助。

你可能感兴趣的:(JavaWeb)