Spring Security怎么实现的跨域?

前言

如果你的项目使用了服务网关, 比如Spring Gateway 听我的, 跨域问题在服务网关解决, 如果你的项目没有使用服务网关再考虑在Spring Security解决跨域问题好嘛? (当然我这句话需要按照你的项目实际情况考虑)

至于为什么我要这么说, 我就问问你, Spring Security是不是过滤器? 那Spring Gateway也是不是过滤器? 那谁的过滤器先执行? 谁的过滤器后执行? 如果 Spring Security添加了cors跨域, 但是被Spring Gateway修改了怎么办? 反之又如何?

你必须考虑, 如果Spring Security或者Spring Gateway出现了异常是否会绕过你想象中必须执行的但实际并未执行的过滤器

记住过滤器很好用, 但顺序和执行情况需要严重关切

主要讲了什么?

本篇将介绍跨域问题解决方案, 其中有 Spring 跨域 和 Spring Security跨域问题

基础

什么是跨域?

浏览器从一个域名的网页去请求另一个域名的资源时,域名、端口、协议任一不同,都是跨域域名:

主域名不同 www.baidu.com/index.html -->www.sina.com/test.js 

子域名不同 www.666.baidu.com/index.html -->www.555.baidu.com/test.js 

域名和域名ipwww.baidu.com/index.html -->http://180.149.132.47/test.js

端口: www.baidu.com:8080/index.html–…www.baidu.com:8081/test.js

协议: www.baidu.com:8080/index.html–…www.baidu.com:8080/test.js

备注: 1、端口和协议的不同,只能通过后台来解决 

2、localhost和127.0.0.1虽然都指向本机,但也属于跨域

跨域问题的解决方案都有哪些?

1、客户端浏览器解除跨域限制(理论上可以但是不现实)

2、发送JSONP请求替代XHR请求(并不能适用所有的请求方式,不推荐)

3、修改服务器端(包括HTTP服务器和应用服务器)(推荐

JSONP

JSONP(JSON with Padding)是JSON的一种补充使用方式,不是官方协议

JSONP 利用了前端标签的属性src可以访问第三方网站请求并执行的漏洞, 访问第三方网站, 并返回第三方网站的内容

所以站在用户网站来说, 你需要提供:

  • 一个带着 src 属性的标签, 比如 <script> <img> 等, 里面填入跨域服务端的请求地址

  • 一个带着参数的回调函数, 该参数用户接收跨域服务端的数据

  • functioncallback(data) { console.log(data) alert(data) } 复制代码

  • 想办法告知跨域服务端, 你的回调函数名(一般放在 src 末尾的 callback 属性上, 比如: http://xxx.com?callback=回调函数名)

  • ?callback=callback // 将回调函数的名字传递给跨域服务端复制代码

跨域服务端呢?

  • 需要构建一个controller或者说构建一个请求, 该请求对应着用户网站的 src 地址

  • @GetMapping("jsonp")publicvoidjsonp(@RequestParam("callback") String callback, @RequestParam("hello") String hello, HttpServletResponse response)throws IOException { System.out.println("前端给的数据 hello: " + hello); Stringdata="我是第三方服务给出的数据"; response.setContentType("text/javascript;charset=UTF-8"); response.getWriter().write(callback + "('" + data + "')"); } 复制代码

  • 请求需要带上callback字符串

  • String callback, 复制代码

  • 在该请求的最后返回一个String, 该String返回用户网站的回调函数名(回调函数名(服务端的数据))

  • response.setContentType("text/javascript;charset=UTF-8"); response.getWriter().write(callback + "('" + data + "')"); // 这里返回了一个正在回调函数调用的代码, 并且传递了参数 data复制代码

对了如果你得application.yml下有Spring Security的依赖, 可以添加下代码

server:port:8080spring:security:user:name:zhazhapassword:"{noop}123456"复制代码
跨域服务端返回给前端 <script> 标签的字符串将被转化为 callback 函数执行

我们还可以使用 jquery 的方式调用

<!DOCTYPE html><htmllang="en"><head><metacharset="UTF-8"><title>Title</title><scriptsrc="https://code.jquery.com/jquery-3.1.1.min.js"></script></head><body><h1>跨域案例</h1><script>functioncallback(data) {
      console.log(data)
      alert(data)
   }
   $.ajax({
      url: "http://localhost:8080/jqJsonp",
      type: "get",
      dataType: "jsonp",
      data: {hello: "hello"},
      jsonp: "callback",
      success: function (data) {
         alert(data)
      },
      error: function () {
         alert("Wrong!")
      }
   });
</script></body></html>复制代码
@GetMapping("jqJsonp")@ResponseBodypublic String jqJsonp(@RequestParam("callback") String callback, @RequestParam("hello") String hello) {
   System.out.println("前端给的数据 hello: " + hello);
   Stringdata="我是第三方服务给出的数据";
   return callback + "('" + data + "')";
}
复制代码
总结

jSONP的优点:

  1. 原理简单

  1. 方便快捷, 随时可以搞

JSONP的缺点:

  1. 只支持GET请求

  1. 服务端需要修改代码

  1. 发送的不是 XHR 请求,无法使用 XHR 对象(但这也是为什么可以解决跨域问题的根本)

这项技术在3-4年前可能还有人用, 现在基本上没什么人用, 因为有更好的选择, 但是思路很好

其他方法就不介绍了, 思路差不错, 比如 PostMessage 等方案, 还有一部分借助iframe的, 可能被 Spring Security 拦截, 不好用

CORS跨域资源共享(推荐)

CORS(Cross-Origin Resource Sharing)该技术由 W3C 为浏览器提供了一种跨域资源共享方案

跨域还有什么思路?

可以告诉浏览器, 在两个域名之间开辟一个共享资源空间(或者管道), 这样两个域名就可以进行跨域调用了, 这是一种思路

但是这个共享资源空间肯定是有限制的
共享资源空间, 可以是 header 可以是 cookie, 又或者是 localStore 只要浏览器能够读取到的位置

那么CORS又是怎么实现的呢?

CORS怎么实现的?

CORS新增了一组HTTP请求头字段,通过这些字段,服务器告诉浏览器,哪些网站通过浏览器有权限访问哪些资源。

简单请求(可以不看)

假如站点 https://foo.example 的网页应用想要访问 https://bar.other 的资源。

foo.example 的网页中可能包含类似于下面的 JavaScript 代码:

const xhr = newXMLHttpRequest();
const url = 'https://bar.other/resources/public-data/';

xhr.open('GET', url);
xhr.onreadystatechange = someHandler;
xhr.send();
复制代码

此操作实行了客户端和服务器之间的简单交换,使用 CORS 首部字段来处理权限:

以下是浏览器发送给服务器的请求报文:

GET /resources/public-data/ HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Connection: keep-alive
Origin: https://foo.example
复制代码

请求首部字段 Origin 表明该请求来源于 http://foo.example

让我们来看看服务器如何响应:

HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 00:23:53 GMT
Server: Apache/2
Access-Control-Allow-Origin: *
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Transfer-Encoding: chunked
Content-Type: application/xml

[…XML Data…]
复制代码

本例中,服务端返回的 Access-Control-Allow-Origin标头的 Access-Control-Allow-Origin: * 值表明,该资源可以被任意外源访问。

Access-Control-Allow-Origin: *
复制代码

使用Origin和 Access-Control-Allow-Origin 就能完成最简单的访问控制。如果 https://bar.other使用Origin和 Access-Control-Allow-Origin 就能完成最简单的访问控制。如果 https://bar.other 的资源持有者想限制他的资源只能通过 https://foo.example 来访问(也就是说,非 https://foo.example 域无法通过跨源访问访问到该资源),他可以这样做:

Access-Control-Allow-Origin: https://foo.example
复制代码
备注: 当响应的是 附带身份凭证的请求时,服务端 必须明确 Access-Control-Allow-Origin 的值,而不能使用通配符“*”。
复杂请求(可以不看)
了解有预检请求便可

与简单请求不同,“需预检的请求"要求必须首先使用OPTIONS方法发起一个预检请求到服务器,以获知服务器是否允许该实际请求。"预检请求"的使用,可以避免跨域请求对服务器的用户数据产生未预期的影响。如下是一个需要执行预检请求的HTTP请求:

const xhr = newXMLHttpRequest();
xhr.open('POST', 'https://bar.other/resources/post-here/');
xhr.setRequestHeader('X-PINGOTHER', 'pingpong');
xhr.setRequestHeader('Content-Type', 'application/xml');
xhr.onreadystatechange = handler;
xhr.send('<person><name>Arun</name></person>');
复制代码

上面的代码使用POST请求发送一个XML 请求体,该请求包含了一个非标准的HTTP X-PINGOTHER请求首部。这样的请求首部并不是HTTP/1.1的一部分,但通常对于web应用很有用处。另外,该请求的Content-Type为application/xml,且使用了自定义的请求首部,所以该请求需要首先发起“预检请求”。

备注: 如下所述,实际的 POST 请求不会携带 Access-Control-Request-* 首部,它们仅用于 OPTIONS 请求。

下面是服务端和客户端完整的信息交互。首次交互是预检请求/响应

OPTIONS /doc HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Connection: keep-alive
Origin: https://foo.example
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER, Content-Type

HTTP/1.1 204 No Content
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2
Access-Control-Allow-Origin: https://foo.example
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
Access-Control-Max-Age: 86400
Vary: Accept-Encoding, Origin
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
复制代码

Copy to Clipboard

从上面的报文中,我们看到,第 1 - 10 行使用 OPTIONS 方法发送了预检请求,浏览器根据上面的JavaScript代码片断所使用的请求参数来决定是否需要发送,这样服务器就可以回应是否可以接受用实际的请求参数来发送请求。OPTIONS 是 HTTP/1.1 协议中定义的方法,用于从服务器获取更多信息,是安全的方法。该方法不会对服务器资源产生影响。注意 OPTIONS 预检请求中同时携带了下面两个首部字段:

Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER, Content-Type
复制代码

Copy to Clipboard

首部字段 Access-Control-Request-Method 告知服务器,实际请求将使用 POST 方法。首部字段 Access-Control-Request-Headers 告知服务器,实际请求将携带两个自定义请求首部字段:X-PINGOTHER 与 Content-Type。服务器据此决定,该实际请求是否被允许。

第 12 - 21 行为预检请求的响应,表明服务器将接受后续的实际请求方法(POST)和请求头(X-PINGOTHER)。重点看第 15 - 18 行:

Access-Control-Allow-Origin: https://foo.example
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
Access-Control-Max-Age: 86400
复制代码

服务器的响应携带了 Access-Control-Allow-Origin: https://foo.example,从而限制请求的源域。同时,携带的 Access-Control-Allow-Methods 表明服务器允许客户端使用 POST 和 GET 方法发起请求(与 Allow 响应首部类似,但该标头具有严格的访问控制)。

首部字段 Access-Control-Allow-Headers 表明服务器允许请求中携带字段 X-PINGOTHER 与 Content-Type。与 Access-Control-Allow-Methods 一样,Access-Control-Allow-Headers 的值为逗号分割的列表。

最后,首部字段 Access-Control-Max-Age 给定了该预检请求可供缓存的时间长短,单位为秒,默认值是 5 秒。在有效时间内,浏览器无须为同一请求再次发起预检请求。以上例子中,该响应的有效时间为 86400 秒,也就是 24 小时。请注意,浏览器自身维护了一个最大有效时间,如果该首部字段的值超过了最大有效时间,将不会生效。

预检请求完成之后,发送实际请求:

POST /doc HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Connection: keep-alive
X-PINGOTHER: pingpong
Content-Type: text/xml; charset=UTF-8
Referer: https://foo.example/examples/preflightInvocation.html
Content-Length: 55
Origin: https://foo.example
Pragma: no-cache
Cache-Control: no-cache

<person><name>Arun</name></person>

HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:40 GMT
Server: Apache/2
Access-Control-Allow-Origin: https://foo.example
Vary: Accept-Encoding, Origin
Content-Encoding: gzip
Content-Length: 235
Keep-Alive: timeout=2, max=99
Connection: Keep-Alive
Content-Type: text/plain

[Some XML payload]
复制代码

实战

Spring的处理方式

注意, 如果你在项目中引入了 Spring Security, 那么下面几种 Spring 方式可能会报错, 也可能会失效

spring有三种方式处理跨域问题

@CrossOrigin
// 允许 http://localhost:8080 网址的请求@CrossOrigin(origins = "http://localhost:8080")@PostMapping("post")public String post() {
return"hello, post";
}
复制代码

@CrossOrigin注解各属性含义如下:

  • allowCredentials: 浏览器是否应当发送凭证信息,如Cookie。

  • allowedHeaders: 请求被允许的请求头字段,表示所有字段。

  • exposedHeaders: 哪些响应头可以作为响应的一部分暴露出来。注意,这里只可以一一列举,通配符在这里是无效的。maxAge: 预检请求的有效期,有效期内不必再次发送预检请求,默认是1800秒。

  • methods: 允许的请求方法,表示允许所有方法。

  • origins: 允许的域,表示允许所有域。

源码分析

  • @CrossOrigin注解在AbstractHandlerMethodMapping 的内部类MappingRegistry 的register方法中完成解析的,@CrossOrigin注解中的内容会被解析成一个配置对象CorsConfiguration。

  • 将@CrossOrigin所标记的请求方法对象HandlerMethod和 CorsConfiguration一一对应存入一个名为corsLookup 的 Map集合中。

  • 当请求到达 DispatcherServlet#fdoDispatch 方法之后,调用AbstractHandlerMapping#getHandler方法获取执行链 HandlerExecutionChain时,会从corsLookup集合中获取到CorsConfiguration对象。

  • 根据获取到的CorsConfiguration对象构建一个CorsInterceptor拦截器。

  • 在CorsInterceptor拦截器中触发对DefaultCorsProcessor#processRequest 的调用,跨域请求的校验工作将在该方法中完成。

全局跨域方式
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@ConfigurationpublicclassGlobalCorsConfigimplementsWebMvcConfigurer {
   
   /**
    * 配置全局跨域解决方案
    *
    * @param registry
    */@OverridepublicvoidaddCorsMappings(CorsRegistry registry) {
      registry.addMapping("/**")
            .allowedMethods("*")
            .allowedOrigins("*")
            .allowedHeaders("*")
            .allowCredentials(false)
            .exposedHeaders("")
            .maxAge(3600);
   }
}
复制代码
通过注册CorsFilter的方式
@Beanpublic FilterRegistrationBean<CorsFilter> corsFilter() {
  UrlBasedCorsConfigurationSourcesource=newUrlBasedCorsConfigurationSource();
  CorsConfigurationconfig=newCorsConfiguration();
  config.setAllowCredentials(true);
  config.addAllowedOrigin("*");
  config.addAllowedHeader("*");
  config.addAllowedMethod("*");
  source.registerCorsConfiguration("/**", config);
  FilterRegistrationBean<CorsFilter> bean = newFilterRegistrationBean<>(newCorsFilter(source));
  bean.setOrder(-1); // 这行后面会解释, 为什么?return bean;
}
复制代码

Spring Security处理方案

如果我们配置了 Spring Security的话, 上面的 Spring 解决跨域问题的方式可能失效, 有的还可以使用, 为什么呢?

通过@CrossOrigin注解或者重写addCorsMappings方法配置跨域,统统失效了, 通过CorsFilter 配置的跨域,有没有失效则要看过滤器的优先级,如果过滤器优先级高于Spring Security过滤器,即先于Spring Security过滤器执行,则CorsFilter 所配置的跨域处理依然有效;如果过滤器优先级低于Spring Security过滤器,则CorsFilter 所配置的跨域处理就会失效。这就是为什么我们配置优先级为 -1

意思说, Spring Security你可以看作一个拦截器, 如果 Spring Security优先级高于我们自己配置的cors跨域解决方案(在CorsIntercepter中校验), 那么就会以某些理由拦截下来, 而使得我们配置的跨域失效, 那是为什么呢?

我们知道, 在复杂请求的情况下, 会发送一个预检请求, 但他没有携带任何的认证信息, 直接就会被 Spring Security拦截, 而等到复杂请求发送过来之后, 该请求没有预检请求的信息, 所以也是导致跨域请求失效

如果使用了CorsFilter配置跨域,只要过滤器的优先级高于Spring Security过滤器,即在Spring Security过滤器之前执行了跨域请求校验,那么就不会有问题。如果 CorsFilter的优先级低于Spring Security过滤器,则预检请求一样需要先经过Spring Security过滤器,由于没有携带认证信息,在经过Spring Security过滤器时就会被拦截下来。

那么怎么解决呢?

  • 可以放行 OPTIONS 请求, 但是不安全

  • 可以继续使用前面的 通过注册CorsFilter的方式 这种方式, 只要优先级比Spring Security高就行, 不过那也太不专业了

Spring Security的解决方案
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

@ConfigurationpublicclassSecurityConfigextendsWebSecurityConfigurerAdapter {
   
   @Overrideprotectedvoidconfigure(HttpSecurity http)throws Exception {
      http
            .authorizeRequests()
            .anyRequest().authenticated()
            .and()
            .formLogin()
            .defaultSuccessUrl("/")
            .permitAll()
            .and()
            .cors().configurationSource(configurationSource())
            .and().csrf().disable();
   }
   
   private UrlBasedCorsConfigurationSource configurationSource() {
      finalUrlBasedCorsConfigurationSourcesource=newUrlBasedCorsConfigurationSource();
      finalCorsConfigurationconfig=newCorsConfiguration();
      config.setAllowCredentials(true); // 允许cookies跨域
      config.addAllowedOrigin("*");// #允许向该服务器提交请求的URI,*表示全部允许,在SpringMVC中,如果设成*,会自动转成当前请求头中的Origin
      config.addAllowedHeader("*");// #允许访问的头信息,*表示全部
      config.setMaxAge(18000L);// 预检请求的缓存时间(秒),即在这个时间段里,对于相同的跨域请求不会再预检了
      config.addAllowedMethod("*");
      source.registerCorsConfiguration("/**", config);
      return source;
   }
}
复制代码

源码就不分析了, 说白了就是借助上面的方法帮忙 new CorsFilter 过滤器和填充内容

踩坑

我这里呢,给出一些建议,如果你的项目使用的spring gateway,也就是网关的存在,那么你最好跨域就不要在spring security中使用的。直接在spring gateway里面用就可以了。这样会少走很多坑。

因为spring security说白了是过滤器,而spring gateway也是过滤器,这两个过滤器有的时候会串台。

特别是在发现异常的时候。有的时候会绕过双方的拦截器过程直接跳转到异常处理环节,这样就会出现问题。

这里一定要着重关注这一点。他们之间的顺序很重要,出现异常也要防护。

作者:bangiao

原文链接:https://juejin.cn/post/7187306054198231101

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值