Spring全家桶-Spring Security之跨域与CORS
Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC(控制反转),DI(依赖注入)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。
文章目录
前言
在项目的开发中,我们和前端联调的时候,经常会遇到跨域的问题,那跨域是什么?Spring Security
是怎么实现的呢?怎么去解决跨域的问题呢?
一、跨域是什么?
跨域是浏览器的同源策略,是浏览器限制脚本的跨域访问。在通常情况下请求是可以正常发起的,后端也进行相关的处理。只是在返回相应的数据的时候被浏览器拦截了,导致相应的内容不可使用。其中的一个场景就是CSRF攻击。
二、跨域产生的条件?
在进行跨域处理的时候,跨域的产生条件是什么呢?
不仅不同站点间的访问存在跨域问题, 同站点间的访问可能也会遇到跨域问题, 只要请求的URL与所在页面的URL首部不同即产生跨域。
请求的协议不同,如在http协议下访问https协议的资源时,会产生跨域问题
不同的域名访问会产生主机跨域
相同的域名,但是不同的端口会产生端口跨域
上面的三种情况都会产生跨域问题
三、如何解决跨域问题?
我们解决跨域问题有如下几种方式:
使用jsonp
使用nginx等网络代理进行转发请求
CROS处理等
四、JSONP处理及其原理
JSONP(JSON With Padding)
是一种非官方的解决方案。 由于浏览器允许一些带src属性的标签跨域, 例如, iframe、 script、 img等, 所以JSONP
利用script标签可以实现跨域.
加入我们有如下场景:我们访问user/list的时候,后台返回的json对象的数组如下:
{"code":"0000","message":"请求成功","success":true,"data":[{"username":"tony","password":"asdfacasdradxx"},{"username":"tony2","password":"asdfacasdradxx"}]}
以上是获取用户的列表接口返回的结果。但在跨域的情况下,浏览器的同源策略导致用户请求无法响应信息,此时通过script标签去加载响应的接口
<script src="user/list?callback=jsonp"></script>
这样便可以成功获取响应信息了, 只是得到的JSON数据无法直接在JavaScript中使用。 如果后端介入, 那么在返回浏览器之前应将响应信息包装成JSONP
的形式,如下:
jsonp({
"code":"0000",
"message":"请求成功",
"success":true,
"data":[
{
"username":"tony",
"password":"asdfacasdradxx"
},
{
"username":"tony2",
"password":"asdfacasdradxx"
}
]
})
之后我们就可以通过调用JSONP
的方法进行返回值的处理了.就像调用javascript
函数一样调用。?callback=jsonp
相当于请求之后调用回调方法jsonp,jsonp方法直接这样处理
window.jsonp=function(data){
console.log(data);}
其中的data就是后台返回的相关数据。注意:
JSONP只支持GET请求。
五、使用CORS解决跨域问题
CORS(Cross-Origin Resource Sharing)
的规范中有一组新增的HTTP首部字段,允许服务器声明其提供的资源允许哪些站点跨域使用。通常情况下,跨域请求即便在不被支持的情况下, 服务器也会接收并进行处理, 在CORS
的规范中则避免了这个问题。 浏览器首先会发起一个请求方法为OPTIONS
的预检请求, 用于确认服务器是否允许跨域, 只有在得到许可后才会发出实际请求。 此外, 预检请求还允许服务器通知浏览器跨域携带身份凭证。
CORS新增的HTTP首部字段由服务器控制, 下面我们来看看常用的几个首部字段:
- Access-Control-Allow-Origin:允许取值为
<origin>
或*
<origin>
:指被允许的站点,使用URL首部匹配原则*
:匹配所有站点, 表示允许来自所有域的请求。如果需要浏览器在发起请求时携带凭证信息, 则不允许设置为*
。如果设置了具体的站点信息, 则响应头中的Vary
字段还需要携带Origin
属性,因为服务器对不同的域会返回不同的内容。Vary
:Accept-Encoding,Origin
- Access-Control-Allow-Methods:字段仅在预检请求的响应中指定有效,用于表明服务器允许跨域的HTTP方法, 多个方法之间用逗号隔开.
- Access-Control-Allow-Headers:字段仅在预检请求的响应中指定有效, 用于表明服务器允许携带的首部字段。 多个首部字段之间用逗号隔开。
- Access-Control-Max-Age:字段用于指明本次预检请求的有效期, 单位为秒。 在有效期内, 预检请求不需要再次发起
- Access-Control-Allow-Credentials:
true|false
- 当Access-Control-Allow-Credentials字段取值为
true
时, 浏览器会在接下来的真实请求中携带用户凭证信息(cookie等),服务器也可以使用Set-Cookie向用户浏览器写入新的cookie。注意,使用AccessControl-Allow-Credentials时, Access-Control-Allow-Origin不应该设置为*
注意:
CORS不支持IE8以下版本的浏览器
- 当Access-Control-Allow-Credentials字段取值为
CORS控制场景
- 简单请求
在CORS中,并非所有的跨域访问都会触发预检请求。例如,不携带自定义请求头信息的GET
请求、HEAD
请求,以及Content-Type
为application/x-www-form-urlencoded
、multipart/form-data
或text/plain
的POST
请求, 这类请求被称为简单请求。
浏览器在发起请求时,会在请求头中自动添加一个Origin
属性, 值为当前页面的 URL 首部。当服务器返回响应时,若存在跨域访问控制属性,则浏览器会通过这些属性判断本次请求是否被允许。如果允许,则跨域成功 - 预检请求
预检请求它会发送一个OPTIONS
请求到目标站点,以查明该请求是否安全,防止请求对目标站点的数据造成破坏。若是请求以GET
、HEAD
、POST
以外的方法发起;或者使用POST
方法,但请求数据为application/x-www-form-urlencoded
、multipart/form-data
和text/plain
以外的数据类型;再或者,使用了自定义请求头,则都会被当成预检请求类型处理。 - 带凭证请求
带凭证的请求就是携带了用户cookie等信息的请求.
指定了withCredentials为true。浏览器在实际发出请求时, 将同时向服务器发送 cookie,并期待在服务器返回的响应信息中指明 Access-Control-AllowCredentials为true,否则浏览器会拦截,并抛出错误
Spring Security中CORS支持
在Spring Security
中提供cors
的支持也很简单,只需要调整响应的配置类,创建相应的bean即可。下面就一起来看看。
创建项目spring-security-cors
pom.xml
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency>
创建配置类WebSecurityConfig
@Overrideprotectedvoidconfigure(HttpSecurity http)throwsException{
http.authorizeRequests().antMatchers("/user/**").hasAnyRole("USER","ADMIN").antMatchers("/books/**").hasAnyRole("ADMIN").antMatchers("/").permitAll().and().formLogin().loginPage("/login.html").permitAll().and()//启用.cors();}
创建CorsConfigurationSource
bean
@BeanpublicCorsConfigurationSourcecorsConfigurationSource(){CorsConfiguration configuration=newCorsConfiguration();//允许google站点跨域
configuration.setAllowedOrigins(Arrays.asList("www.google.com"));//允许跨域的防范
configuration.setAllowedMethods(Arrays.asList("GET","POST"));//是否允许携带凭证
configuration.setAllowCredentials(true);UrlBasedCorsConfigurationSource urlBasedCorsConfigurationSource=newUrlBasedCorsConfigurationSource();//对所有的url开放
urlBasedCorsConfigurationSource.registerCorsConfiguration("/**",configuration);return urlBasedCorsConfigurationSource;}
以上是Spring Security
实现CORS
实现,是不是相当简单。
Spring Security支持Cors实现细节
我们看到在http配置中,我们添加了一个cors()
方法进行处理。我们可以看看详细的细节。一起来看看。
publicCorsConfigurer<HttpSecurity>cors()throwsException{return(CorsConfigurer)this.getOrApply(newCorsConfigurer());}
由上面可以看出,会去获取一个CorsConfigurer
类去进行处理。
我们来进入CorsConfigurer
看看。CorsConfigurer.class
:
publicclassCorsConfigurer<HextendsHttpSecurityBuilder<H>>extendsAbstractHttpConfigurer<CorsConfigurer<H>,H>{publicCorsConfigurer<H>configurationSource(CorsConfigurationSource configurationSource){this.configurationSource= configurationSource;returnthis;}publicvoidconfigure(H http){ApplicationContext context=(ApplicationContext)http.getSharedObject(ApplicationContext.class);//获取corsFilter过滤器CorsFilter corsFilter=this.getCorsFilter(context);//请配置corsFilter bean或corsConfigurationSourcebeanAssert.state(corsFilter!=null,()->{return"Please configure either a corsFilter bean or a corsConfigurationSourcebean.";});
http.addFilter(corsFilter);}//获取CorsFilter的处理逻辑privateCorsFiltergetCorsFilter(ApplicationContext context){//configurationSource不为空if(this.configurationSource!=null){//创建CorsFilterreturnnewCorsFilter(this.configurationSource);}else{boolean containsCorsFilter= context.containsBeanDefinition("corsFilter");//如果包含corsFilter的bean,就直接返回if(containsCorsFilter){return(CorsFilter)context.getBean("corsFilter",CorsFilter.class);}else{//否则就新建一个boolean containsCorsSource= context.containsBean("corsConfigurationSource");if(containsCorsSource){CorsConfigurationSource configurationSource=(CorsConfigurationSource)context.getBean("corsConfigurationSource",CorsConfigurationSource.class);returnnewCorsFilter(configurationSource);}else{boolean mvcPresent=ClassUtils.isPresent("org.springframework.web.servlet.handler.HandlerMappingIntrospector", context.getClassLoader());return mvcPresent?CorsConfigurer.MvcCorsFilter.getMvcCorsFilter(context):null;}}}}staticclassMvcCorsFilter{privatestaticfinalString HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME="mvcHandlerMappingIntrospector";MvcCorsFilter(){}privatestaticCorsFiltergetMvcCorsFilter(ApplicationContext context){if(!context.containsBean("mvcHandlerMappingIntrospector")){thrownewNoSuchBeanDefinitionException("mvcHandlerMappingIntrospector","A Bean named mvcHandlerMappingIntrospector of type "+HandlerMappingIntrospector.class.getName()+" is required to use MvcRequestMatcher. Please ensure Spring Security & Spring MVC are configured in a shared ApplicationContext.");}else{HandlerMappingIntrospector mappingIntrospector=(HandlerMappingIntrospector)context.getBean("mvcHandlerMappingIntrospector",HandlerMappingIntrospector.class);returnnewCorsFilter(mappingIntrospector);}}}}
上面可以看出是通过过滤器进行处理,之后将过滤器添加到Spring Security
的过滤器链路中。CorsFilter.class
:
privatefinalCorsConfigurationSource configSource;//DefaultCorsProcessor处理corsprivateCorsProcessor processor=newDefaultCorsProcessor();publicCorsFilter(CorsConfigurationSource configSource){Assert.notNull(configSource,"CorsConfigurationSource must not be null");this.configSource= configSource;}publicvoidsetCorsProcessor(CorsProcessor processor){Assert.notNull(processor,"CorsProcessor must not be null");this.processor= processor;}
从上面可以看出,是通过DefaultCorsProcessor
进行处理DefaultCorsProcessor.class
:
publicbooleanprocessRequest(@NullableCorsConfiguration config,HttpServletRequest request,HttpServletResponse response)throwsIOException{Collection<String> varyHeaders= response.getHeaders("Vary");if(!varyHeaders.contains("Origin")){
response.addHeader("Vary","Origin");}if(!varyHeaders.contains("Access-Control-Request-Method")){
response.addHeader("Vary","Access-Control-Request-Method");}if(!varyHeaders.contains("Access-Control-Request-Headers")){
response.addHeader("Vary","Access-Control-Request-Headers");}if(!CorsUtils.isCorsRequest(request)){returntrue;}elseif(response.getHeader("Access-Control-Allow-Origin")!=null){
logger.trace("Skip: response already contains \"Access-Control-Allow-Origin\"");returntrue;}else{boolean preFlightRequest=CorsUtils.isPreFlightRequest(request);if(config==null){if(preFlightRequest){this.rejectRequest(newServletServerHttpResponse(response));returnfalse;}else{returntrue;}}else{returnthis.handleInternal(newServletServerHttpRequest(request),newServletServerHttpResponse(response), config, preFlightRequest);}}}//拒绝请求protectedvoidrejectRequest(ServerHttpResponse response)throwsIOException{
response.setStatusCode(HttpStatus.FORBIDDEN);
response.getBody().write("Invalid CORS request".getBytes(StandardCharsets.UTF_8));
response.flush();}//处理protectedbooleanhandleInternal(ServerHttpRequest request,ServerHttpResponse response,CorsConfiguration config,boolean preFlightRequest)throwsIOException{//获取OriginString requestOrigin= request.getHeaders().getOrigin();//获取allowOriginString allowOrigin=this.checkOrigin(config, requestOrigin);HttpHeaders responseHeaders= response.getHeaders();if(allowOrigin==null){
logger.debug("Reject: '"+ requestOrigin+"' origin is not allowed");this.rejectRequest(response);returnfalse;}else{//校验请求的方法HttpMethod requestMethod=this.getMethodToUse(request, preFlightRequest);List<HttpMethod> allowMethods=this.checkMethods(config, requestMethod);if(allowMethods==null){
logger.debug("Reject: HTTP '"+ requestMethod+"' is not allowed");this.rejectRequest(response);returnfalse;}else{//请求中header列表List<String> requestHeaders=this.getHeadersToUse(request, preFlightRequest);//请求中允许的header列表List<String> allowHeaders=this.checkHeaders(config, requestHeaders);if(preFlightRequest&& allowHeaders==null){
logger.debug("Reject: headers '"+ requestHeaders+"' are not allowed");this.rejectRequest(response);returnfalse;}else{
responseHeaders.setAccessControlAllowOrigin(allowOrigin);if(preFlightRequest){
responseHeaders.setAccessControlAllowMethods(allowMethods);}if(preFlightRequest&&!allowHeaders.isEmpty()){
responseHeaders.setAccessControlAllowHeaders(allowHeaders);}if(!CollectionUtils.isEmpty(config.getExposedHeaders())){
responseHeaders.setAccessControlExposeHeaders(config.getExposedHeaders());}if(Boolean.TRUE.equals(config.getAllowCredentials())){
responseHeaders.setAccessControlAllowCredentials(true);}if(preFlightRequest&& config.getMaxAge()!=null){
responseHeaders.setAccessControlMaxAge(config.getMaxAge());}
response.flush();returntrue;}}}}
跨域请求伪造CSRF
CSRF
的全称是(Cross Site Request Forgery) , 可译为跨域请求伪造,也被称为 one-click attack 或者 session riding,通常缩写为 CSRF 或者 XSRF。是一种挟制用户在当前已登录的Web应用程序上执行非本意的操作的攻击方法。跟跨网站脚本(XSS)相比,XSS 利用的是用户对指定网站的信任,CSRF 利用的是网站对用户网页浏览器的信任。
CSRF攻击过程:
CSRF漏洞产生的原因主要是对用户请求缺少更安全的验证机制。防范CSRF漏洞的主要思路就是加强后台对用户及用户请求的验证,而不能仅限于cookie的识别。
跨域请求伪造CSRF防御措施
遇到CRSF我们该如何防御呢?
- 使用相关工具进行检查是否存在CSRF漏洞,如CSRFTester等
- HTTP Referer
HTTP Referer是由浏览器添加的一个请求头字段,