Spring Security怎么实现的跨域?
前言
如果你的项目使用了服务网关, 比如Spring Gateway 听我的, 跨域问题在服务网关解决, 如果你的项目没有使用服务网关再考虑在Spring Security解决跨域问题好嘛? (当然我这句话需要按照你的项目实际情况考虑)
至于为什么我要这么说, 我就问问你, Spring Security是不是过滤器? 那Spring Gateway也是不是过滤器? 那谁的过滤器先执行? 谁的过滤器后执行? 如果 Spring Security添加了cors
跨域, 但是被Spring Gateway修改了怎么办? 反之又如何?
你必须考虑, 如果Spring Security或者Spring Gateway出现了异常是否会绕过你想象中必须执行的但实际并未执行的过滤器
记住过滤器很好用, 但顺序和执行情况需要严重关切
主要讲了什么?
基础
什么是跨域?
浏览器从一个域名的网页去请求另一个域名的资源时,域名、端口、协议任一不同,都是跨域 域名:
主域名不同 www.百度.com/index.html -->www.sina.com/test.js
子域名不同 www.666.百度.com/index.html -->www.555.百度.com/test.js
域名和域名ip
www.百度.com/index.html -->http://180.149.132.47/test.js
端口: www.百度.com:8080/index.html–… www.百度.com:8081/test.js
协议: www.百度.com:8080/index.html–… www.百度.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>
等, 里面填入跨域服务端的请求地址-
<script src="http://localhost:8080/jsonp?callback=callback"></script>
-
-
一个带着参数的回调函数, 该参数用户接收跨域服务端的数据
-
function callback(data) { console.log(data) alert(data) }
-
-
想办法告知跨域服务端, 你的回调函数名(一般放在
src
末尾的callback
属性上, 比如:http://xxx.com?callback=回调函数名
)-
?callback=callback // 将回调函数的名字传递给跨域服务端
-
跨域服务端呢?
-
需要构建一个
controller
或者说构建一个请求, 该请求对应着用户网站的src
地址-
@GetMapping("jsonp") public void jsonp(@RequestParam("callback") String callback, @RequestParam("hello") String hello, HttpServletResponse response) throws IOException { System.out.println("前端给的数据 hello: " hello); String data = "我是第三方服务给出的数据"; 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: 8080
spring:
security:
user:
name: zhazha
password: "{noop}123456"
跨域服务端返回给前端
<script>
标签的字符串将被转化为callback
函数执行
我们还可以使用 jquery
的方式调用
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="https://code.jquery.com/jquery-3.1.1.min.js"></script>
</head>
<body>
<h1>跨域案例</h1>
<script>
function callback(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")
@ResponseBody
public String jqJsonp(@RequestParam("callback") String callback, @RequestParam("hello") String hello) {
System.out.println("前端给的数据 hello: " hello);
String data = "我是第三方服务给出的数据";
return callback "('" data "')";
}
总结
jSONP
的优点:
- 原理简单
- 方便快捷, 随时可以搞
JSONP
的缺点:
- 只支持
GET
请求 - 服务端需要修改代码
- 发送的不是
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 = new XMLHttpRequest();
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
的资源持有者想限制他的资源只能通过 https://foo.example
来访问(也就是说,非 https://foo.example
域无法通过跨源访问访问到该资源),他可以这样做:
Access-Control-Allow-Origin: https://foo.example
备注: 当响应的是附带身份凭证的请求时,服务端必须明确
Access-Control-Allow-Origin
的值,而不能使用通配符“*
”。
复杂请求(可以不看)
了解有预检请求便可
与简单请求不同,“需预检的请求"要求必须首先使用OPTIONS方法发起一个预检请求到服务器,以获知服务器是否允许该实际请求。"预检请求"的使用,可以避免跨域请求对服务器的用户数据产生未预期的影响。 如下是一个需要执行预检请求的HTTP请求:
const xhr = new XMLHttpRequest();
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;
@Configuration
public class GlobalCorsConfig implements WebMvcConfigurer {
/**
* 配置全局跨域解决方案
*
* @param registry
*/
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedMethods("*")
.allowedOrigins("*")
.allowedHeaders("*")
.allowCredentials(false)
.exposedHeaders("")
.maxAge(3600);
}
}
通过注册CorsFilter
的方式
@Bean
public FilterRegistrationBean<CorsFilter> corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOrigin("*");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
source.registerCorsConfiguration("/**", config);
FilterRegistrationBean<CorsFilter> bean = new FilterRegistrationBean<>(new CorsFilter(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;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.defaultSuccessUrl("/")
.permitAll()
.and()
.cors().configurationSource(configurationSource())
.and().csrf().disable();
}
private UrlBasedCorsConfigurationSource configurationSource() {
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
final CorsConfiguration config = new CorsConfiguration();
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也是过滤器,这两个过滤器有的时候会串台。
特别是在发现异常的时候。有的时候会绕过双方的拦截器过程直接跳转到异常处理环节,这样就会出现问题。
这里一定要着重关注这一点。他们之间的顺序很重要,出现异常也要防护。
这篇好文章是转载于:学新通技术网
- 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
- 本站站名: 学新通技术网
- 本文地址: /boutique/detail/tanggfbhh
-
photoshop保存的图片太大微信发不了怎么办
PHP中文网 06-15 -
《学习通》视频自动暂停处理方法
HelloWorld317 07-05 -
word里面弄一个表格后上面的标题会跑到下面怎么办
PHP中文网 06-20 -
Android 11 保存文件到外部存储,并分享文件
Luke 10-12 -
photoshop扩展功能面板显示灰色怎么办
PHP中文网 06-14 -
微信公众号没有声音提示怎么办
PHP中文网 03-31 -
excel下划线不显示怎么办
PHP中文网 06-23 -
excel打印预览压线压字怎么办
PHP中文网 06-22 -
TikTok加速器哪个好免费的TK加速器推荐
TK小达人 10-01 -
怎样阻止微信小程序自动打开
PHP中文网 06-13