Spring全家桶-Spring Security之跨域与CORS与防护

2022年6月4日13:25:43

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以下版本的浏览器

CORS控制场景

  • 简单请求
    在CORS中,并非所有的跨域访问都会触发预检请求。例如,不携带自定义请求头信息的GET请求、HEAD请求,以及Content-Typeapplication/x-www-form-urlencodedmultipart/form-datatext/plainPOST请求, 这类请求被称为简单请求。
    浏览器在发起请求时,会在请求头中自动添加一个Origin属性, 值为当前页面的 URL 首部。当服务器返回响应时,若存在跨域访问控制属性,则浏览器会通过这些属性判断本次请求是否被允许。如果允许,则跨域成功
  • 预检请求
    预检请求它会发送一个OPTIONS请求到目标站点,以查明该请求是否安全,防止请求对目标站点的数据造成破坏。若是请求以GETHEADPOST以外的方法发起;或者使用POST方法,但请求数据为application/x-www-form-urlencodedmultipart/form-datatext/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攻击过程:
Spring全家桶-Spring Security之跨域与CORS与防护
CSRF漏洞产生的原因主要是对用户请求缺少更安全的验证机制。防范CSRF漏洞的主要思路就是加强后台对用户及用户请求的验证,而不能仅限于cookie的识别。

跨域请求伪造CSRF防御措施

遇到CRSF我们该如何防御呢?

  1. 使用相关工具进行检查是否存在CSRF漏洞,如CSRFTester等
  2. HTTP Referer
    HTTP Referer是由浏览器添加的一个请求头字段,
  • 作者:Tony-devj
  • 原文链接:https://blog.csdn.net/qian1314520hu/article/details/125009874
    更新时间:2022年6月4日13:25:43 ,共 10990 字。