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

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

@Override
 protected void configure(HttpSecurity http) throws Exception {
     http.authorizeRequests().antMatchers("/user/**").hasAnyRole("USER","ADMIN")
             .antMatchers("/books/**").hasAnyRole("ADMIN")
             .antMatchers("/").permitAll()
             .and()
             .formLogin()
             .loginPage("/login.html")
             .permitAll()
             .and()
             //启用
             .cors();
 }

创建CorsConfigurationSource bean

@Bean
public CorsConfigurationSource corsConfigurationSource(){
    CorsConfiguration configuration = new CorsConfiguration();
    //允许google站点跨域
    configuration.setAllowedOrigins(Arrays.asList("www.google.com"));
    //允许跨域的防范
    configuration.setAllowedMethods(Arrays.asList("GET","POST"));
    //是否允许携带凭证
    configuration.setAllowCredentials(true);
    UrlBasedCorsConfigurationSource urlBasedCorsConfigurationSource = new UrlBasedCorsConfigurationSource();
    //对所有的url开放
    urlBasedCorsConfigurationSource.registerCorsConfiguration("/**",configuration);
    return urlBasedCorsConfigurationSource;
}

以上是Spring Security实现CORS实现,是不是相当简单。

Spring Security支持Cors实现细节

我们看到在http配置中,我们添加了一个cors()方法进行处理。我们可以看看详细的细节。一起来看看。

public CorsConfigurer<HttpSecurity> cors() throws Exception {
        return (CorsConfigurer)this.getOrApply(new CorsConfigurer());
    }

由上面可以看出,会去获取一个CorsConfigurer类去进行处理。
我们来进入CorsConfigurer看看。
CorsConfigurer.class:

public class CorsConfigurer<H extends HttpSecurityBuilder<H>> extends AbstractHttpConfigurer<CorsConfigurer<H>, H> {
    public CorsConfigurer<H> configurationSource(CorsConfigurationSource configurationSource) {
        this.configurationSource = configurationSource;
        return this;
    }
    public void configure(H http) {
        ApplicationContext context = (ApplicationContext)http.getSharedObject(ApplicationContext.class);
        //获取corsFilter过滤器
        CorsFilter corsFilter = this.getCorsFilter(context);
        //请配置corsFilter bean或corsConfigurationSourcebean
        Assert.state(corsFilter != null, () -> {
            return "Please configure either a corsFilter bean or a corsConfigurationSourcebean.";
        });
        http.addFilter(corsFilter);
    }
    //获取CorsFilter的处理逻辑
    private CorsFilter getCorsFilter(ApplicationContext context) {
    	//configurationSource不为空
        if (this.configurationSource != null) {
      		//创建CorsFilter
            return new CorsFilter(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);
                    return new CorsFilter(configurationSource);
                } else {
                    boolean mvcPresent = ClassUtils.isPresent("org.springframework.web.servlet.handler.HandlerMappingIntrospector", context.getClassLoader());
                    return mvcPresent ? CorsConfigurer.MvcCorsFilter.getMvcCorsFilter(context) : null;
                }
            }
        }
    }

    static class MvcCorsFilter {
        private static final String HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME = "mvcHandlerMappingIntrospector";

        MvcCorsFilter() {
        }

        private static CorsFilter getMvcCorsFilter(ApplicationContext context) {
            if (!context.containsBean("mvcHandlerMappingIntrospector")) {
                throw new NoSuchBeanDefinitionException("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);
                return new CorsFilter(mappingIntrospector);
            }
        }
    }
}

上面可以看出是通过过滤器进行处理,之后将过滤器添加到Spring Security的过滤器链路中。
CorsFilter.class:

private final CorsConfigurationSource configSource;
	//DefaultCorsProcessor处理cors
    private CorsProcessor processor = new DefaultCorsProcessor();
    public CorsFilter(CorsConfigurationSource configSource) {
        Assert.notNull(configSource, "CorsConfigurationSource must not be null");
        this.configSource = configSource;
    }
    public void setCorsProcessor(CorsProcessor processor) {
        Assert.notNull(processor, "CorsProcessor must not be null");
        this.processor = processor;
    }

从上面可以看出,是通过DefaultCorsProcessor进行处理
DefaultCorsProcessor.class:

public boolean processRequest(@Nullable CorsConfiguration config, HttpServletRequest request, HttpServletResponse response) throws IOException {
        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)) {
            return true;
        } else if (response.getHeader("Access-Control-Allow-Origin") != null) {
            logger.trace("Skip: response already contains \"Access-Control-Allow-Origin\"");
            return true;
        } else {
            boolean preFlightRequest = CorsUtils.isPreFlightRequest(request);
            if (config == null) {
                if (preFlightRequest) {
                    this.rejectRequest(new ServletServerHttpResponse(response));
                    return false;
                } else {
                    return true;
                }
            } else {
                return this.handleInternal(new ServletServerHttpRequest(request), new ServletServerHttpResponse(response), config, preFlightRequest);
            }
        }
    }
	//拒绝请求
    protected void rejectRequest(ServerHttpResponse response) throws IOException {
        response.setStatusCode(HttpStatus.FORBIDDEN);
        response.getBody().write("Invalid CORS request".getBytes(StandardCharsets.UTF_8));
        response.flush();
    }
	//处理
    protected boolean handleInternal(ServerHttpRequest request, ServerHttpResponse response, CorsConfiguration config, boolean preFlightRequest) throws IOException {
    	//获取Origin
        String requestOrigin = request.getHeaders().getOrigin();
        //获取allowOrigin
        String allowOrigin = this.checkOrigin(config, requestOrigin);
        HttpHeaders responseHeaders = response.getHeaders();
        if (allowOrigin == null) {
            logger.debug("Reject: '" + requestOrigin + "' origin is not allowed");
            this.rejectRequest(response);
            return false;
        } 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);
                return false;
            } 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);
                    return false;
                } 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();
                    return true;
                }
            }
        }
    }

跨域请求伪造CSRF

CSRF的全称是(Cross Site Request Forgery) , 可译为跨域请求伪造,也被称为 one-click attack 或者 session riding,通常缩写为 CSRF 或者 XSRF。是一种挟制用户在当前已登录的Web应用程序上执行非本意的操作的攻击方法。跟跨网站脚本(XSS)相比,XSS 利用的是用户对指定网站的信任,CSRF 利用的是网站对用户网页浏览器的信任。
CSRF攻击过程:
在这里插入图片描述
CSRF漏洞产生的原因主要是对用户请求缺少更安全的验证机制。防范CSRF漏洞的主要思路就是加强后台对用户及用户请求的验证,而不能仅限于cookie的识别。

跨域请求伪造CSRF防御措施

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

  1. 使用相关工具进行检查是否存在CSRF漏洞,如CSRFTester等
  2. HTTP Referer
    HTTP Referer是由浏览器添加的一个请求头字段, 用于标识请求来源,通常用在一些统计相关的场景,浏览器端无法轻易篡改该值。 当校验到请求来自其他站点时,可以认为是CSRF攻击,从而拒绝该服务。当然,这种方式简单便捷,但并非完全可靠。因为部分浏览器可以篡改 HTTP Referer,如果用户在浏览器中设置了不被跟踪, 那么HTTP Referer字段就不会自动添加, 当合法用户访问时, 系统会认为是CSRF攻击,从而拒绝访问。
  3. CsrfToken认证
    CSRF是利用用户的登录态进行攻击的,而用户的登录态记录在cookie中。其实攻击者并不知道用户的cookie存放了哪些数据,于是想方设法让用户自身发起请求,这样浏览器便会自行将cookie传送到服务器完成身份校验。
    CsrfToken 的防范思路是添加一些并不存放于 cookie 的验证值,并在每个请求中都进行校验,便可以阻止CSRF攻击。
    具体做法是在用户登录时,由系统发放一个CsrfToken值, 用户携带该CsrfToken值与用户名、密码等参数完成登录。系统记录该会话的 CsrfToken 值,之后在用户的任何请求中,都必须带上该CsrfToken值, 并由系统进行校验。
    这种方法需要与前端配合,包括存储CsrfToken值,以及在任何请求中( 包括表单和Ajax) 携带CsrfToken值。安全性相较于HTTP Referer提高很多, 但也存在一定的弊端。 例如,在现有的系统中进行改造时, 前端的工作量会非常大, 几乎要对所有请求进行处理。 如果都是XMLHttpRequest,则可以统一添加CsrfToken值; 但如果存在大量的表单和a标签,就会变得非常烦琐。因此建议在系统开发之初考虑如何防御CSRF攻击。

Spring Security中进行防御CSRF攻击

CSRF 攻击完全是基于浏览器进行的,如果我们的系统前端并非在浏览器中运作,就应当关闭CSRF。
Spring Security通过注册一个CsrfFilter来专门处理CSRF攻击。
在相应的配置类中添加如下代码:

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().antMatchers("/user/**").hasAnyRole("USER","ADMIN")
                .antMatchers("/books/**").hasAnyRole("ADMIN")
                .antMatchers("/").permitAll()
                .and()
                .formLogin()
                .loginPage("/login.html")
                .permitAll()
                .and()
                //启用
                .cors()
                .and()
                //开启csrf
                .csrf();
    }
}

csrf()方法:

public CsrfConfigurer<HttpSecurity> csrf() throws Exception {
        ApplicationContext context = this.getContext();
        return (CsrfConfigurer)this.getOrApply(new CsrfConfigurer(context));
    }

从上面可以看出,是通过CsrfConfigurer类进行处理
CsrfConfigurer.class:

public void configure(H http) {
        CsrfFilter filter = new CsrfFilter(this.csrfTokenRepository);
        RequestMatcher requireCsrfProtectionMatcher = this.getRequireCsrfProtectionMatcher();
        if (requireCsrfProtectionMatcher != null) {
            filter.setRequireCsrfProtectionMatcher(requireCsrfProtectionMatcher);
        }

        AccessDeniedHandler accessDeniedHandler = this.createAccessDeniedHandler(http);
        if (accessDeniedHandler != null) {
            filter.setAccessDeniedHandler(accessDeniedHandler);
        }

        LogoutConfigurer<H> logoutConfigurer = (LogoutConfigurer)http.getConfigurer(LogoutConfigurer.class);
        if (logoutConfigurer != null) {
            logoutConfigurer.addLogoutHandler(new CsrfLogoutHandler(this.csrfTokenRepository));
        }

        SessionManagementConfigurer<H> sessionConfigurer = (SessionManagementConfigurer)http.getConfigurer(SessionManagementConfigurer.class);
        if (sessionConfigurer != null) {
            sessionConfigurer.addSessionAuthenticationStrategy(this.getSessionAuthenticationStrategy());
        }

        filter = (CsrfFilter)this.postProcess(filter);
        http.addFilter(filter);
    }

通过上面的类可以了解到,配置使用CsrfFilter过滤器进行处理,并且使用默认的csrfTokenRepository,这个属性值为LazyCsrfTokenRepository

private CsrfTokenRepository csrfTokenRepository = new LazyCsrfTokenRepository(new HttpSessionCsrfTokenRepository());

LazyCsrfTokenRepository.class类:

public final class LazyCsrfTokenRepository implements CsrfTokenRepository {
	//响应属性
    private static final String HTTP_RESPONSE_ATTR = HttpServletResponse.class.getName();
    //代理类
    private final CsrfTokenRepository delegate;
	//构造器
    public LazyCsrfTokenRepository(CsrfTokenRepository delegate) {
        Assert.notNull(delegate, "delegate cannot be null");
        this.delegate = delegate;
    }
	//生产csrfToken
    public CsrfToken generateToken(HttpServletRequest request) {
        return this.wrap(request, this.delegate.generateToken(request));
    }
	//保存csrfToken
    public void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response) {
        if (token == null) {
            this.delegate.saveToken(token, request, response);
        }
    }
	//加载csrfToken
    public CsrfToken loadToken(HttpServletRequest request) {
        return this.delegate.loadToken(request);
    }
	//包含csrfToken
    private CsrfToken wrap(HttpServletRequest request, CsrfToken token) {
        HttpServletResponse response = this.getResponse(request);
        return new LazyCsrfTokenRepository.SaveOnAccessCsrfToken(this.delegate, request, response, token);
    }
    
    private static final class SaveOnAccessCsrfToken implements CsrfToken {
        private transient CsrfTokenRepository tokenRepository;
        private transient HttpServletRequest request;
        private transient HttpServletResponse response;
        private final CsrfToken delegate;

        SaveOnAccessCsrfToken(CsrfTokenRepository tokenRepository, HttpServletRequest request, HttpServletResponse response, CsrfToken delegate) {
            this.tokenRepository = tokenRepository;
            this.request = request;
            this.response = response;
            this.delegate = delegate;
        }

        public String getHeaderName() {
            return this.delegate.getHeaderName();
        }

        public String getParameterName() {
            return this.delegate.getParameterName();
        }

        public String getToken() {
            this.saveTokenIfNecessary();
            return this.delegate.getToken();
        }

        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            } else if (obj != null && this.getClass() == obj.getClass()) {
                LazyCsrfTokenRepository.SaveOnAccessCsrfToken other = (LazyCsrfTokenRepository.SaveOnAccessCsrfToken)obj;
                if (this.delegate == null) {
                    if (other.delegate != null) {
                        return false;
                    }
                } else if (!this.delegate.equals(other.delegate)) {
                    return false;
                }

                return true;
            } else {
                return false;
            }
        }

        public int hashCode() {
            int prime = true;
            int result = 1;
            int result = 31 * result + (this.delegate == null ? 0 : this.delegate.hashCode());
            return result;
        }

        public String toString() {
            return "SaveOnAccessCsrfToken [delegate=" + this.delegate + "]";
        }

        private void saveTokenIfNecessary() {
            if (this.tokenRepository != null) {
                synchronized(this) {
                    if (this.tokenRepository != null) {
                        this.tokenRepository.saveToken(this.delegate, this.request, this.response);
                        this.tokenRepository = null;
                        this.request = null;
                        this.response = null;
                    }

                }
            }
        }
    }
}

SaveOnAccessCsrfToken implements CsrfToken
Spring security中CsrfToken是一个用于描述Token值, 以及验证时应当获取哪个请求参数或请求头字段的接口。

public interface CsrfToken extends Serializable {
	//获取header
    String getHeaderName();
    //获取参数名
    String getParameterName();
    //获取CsrfToken
    String getToken();
}

CsrfTokenRepository是实现CsrfToken生成,保存以及加载操作

public interface CsrfTokenRepository {
	//生成
	CsrfToken generateToken(HttpServletRequest request);
	//保存
    void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response);
	//加载
    CsrfToken loadToken(HttpServletRequest request);
}

在默认情况下,Spring Security使用的是HttpSessionCsrfTokenRepository.
HttpSessionCsrfTokenRepository.class类:

public final class HttpSessionCsrfTokenRepository implements CsrfTokenRepository {
	//默认的CSRF参数
    private static final String DEFAULT_CSRF_PARAMETER_NAME = "_csrf";
    //默认的CSRF名
    private static final String DEFAULT_CSRF_HEADER_NAME = "X-CSRF-TOKEN";
    //默认的CSRFToken属性名
    private static final String DEFAULT_CSRF_TOKEN_ATTR_NAME = HttpSessionCsrfTokenRepository.class.getName().concat(".CSRF_TOKEN");
    private String parameterName = "_csrf";
    private String headerName = "X-CSRF-TOKEN";
    private String sessionAttributeName;
    public HttpSessionCsrfTokenRepository() {
        this.sessionAttributeName = DEFAULT_CSRF_TOKEN_ATTR_NAME;
    }
    public void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response) {
        HttpSession session;
        if (token == null) {
            session = request.getSession(false);
            if (session != null) {
                session.removeAttribute(this.sessionAttributeName);
            }
        } else {
            session = request.getSession();
            //将csrfToken与session关联
            session.setAttribute(this.sessionAttributeName, token);
        }
    }
	//加载CsrfToken
    public CsrfToken loadToken(HttpServletRequest request) {
        HttpSession session = request.getSession(false);
        //从session中获取
        return session == null ? null : (CsrfToken)session.getAttribute(this.sessionAttributeName);
    }
	//生成CsrfToken
    public CsrfToken generateToken(HttpServletRequest request) {
    	//返回默认的DefaultCsrfToken实例
        return new DefaultCsrfToken(this.headerName, this.parameterName, this.createNewToken());
    }
	//设置参数名
    public void setParameterName(String parameterName) {
        Assert.hasLength(parameterName, "parameterName cannot be null or empty");
        this.parameterName = parameterName;
    }
	//设置header
    public void setHeaderName(String headerName) {
        Assert.hasLength(headerName, "headerName cannot be null or empty");
        this.headerName = headerName;
    }
    //设置session属性名
    public void setSessionAttributeName(String sessionAttributeName) {
        Assert.hasLength(sessionAttributeName, "sessionAttributename cannot be null or empty");
        this.sessionAttributeName = sessionAttributeName;
    }
	//创建新的token
    private String createNewToken() {
        return UUID.randomUUID().toString();
    }
}

HttpSessionCsrfTokenRepositoryCsrfToken值存储在HttpSession中,并指定前端把CsrfToken值放在名为_csrf的请求参数或名为X-CSRF-TOKEN的请求头字段里(可以调用相应的设置方法来重新设定)。校验时,通过对比HttpSession内存储的CsrfToken值与前端携带的CsrfToken值是否一致,便能断定本次请求是否为CSRF攻击。
CsrfTokenRepository接口的另外一个实现是基于cookie的。
CookieCsrfTokenRepository.class类:

public final class CookieCsrfTokenRepository implements CsrfTokenRepository {
    static final String DEFAULT_CSRF_COOKIE_NAME = "XSRF-TOKEN";
    static final String DEFAULT_CSRF_PARAMETER_NAME = "_csrf";
    static final String DEFAULT_CSRF_HEADER_NAME = "X-XSRF-TOKEN";
    private String parameterName = "_csrf";
    private String headerName = "X-XSRF-TOKEN";
    private String cookieName = "XSRF-TOKEN";
    private boolean cookieHttpOnly = true;
    private String cookiePath;
    private String cookieDomain;
    private Boolean secure;
    private int cookieMaxAge = -1;

    public CookieCsrfTokenRepository() {
    }

    public CsrfToken generateToken(HttpServletRequest request) {
        return new DefaultCsrfToken(this.headerName, this.parameterName, this.createNewToken());
    }
	//保存CsrfToken
    public void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response) {
        String tokenValue = token != null ? token.getToken() : "";
        //将CsrfToken与cookie关联 以及设置cookie的参数
        Cookie cookie = new Cookie(this.cookieName, tokenValue);
        cookie.setSecure(this.secure != null ? this.secure : request.isSecure());
        cookie.setPath(StringUtils.hasLength(this.cookiePath) ? this.cookiePath : this.getRequestContext(request));
        cookie.setMaxAge(token != null ? this.cookieMaxAge : 0);
        cookie.setHttpOnly(this.cookieHttpOnly);
        if (StringUtils.hasLength(this.cookieDomain)) {
            cookie.setDomain(this.cookieDomain);
        }
        response.addCookie(cookie);
    }
	//加载CsrfToken
    public CsrfToken loadToken(HttpServletRequest request) {
        Cookie cookie = WebUtils.getCookie(request, this.cookieName);
        if (cookie == null) {
            return null;
        } else {
            String token = cookie.getValue();
            return !StringUtils.hasLength(token) ? null : new DefaultCsrfToken(this.headerName, this.parameterName, token);
        }
    }

    public void setParameterName(String parameterName) {
        Assert.notNull(parameterName, "parameterName cannot be null");
        this.parameterName = parameterName;
    }

    public void setHeaderName(String headerName) {
        Assert.notNull(headerName, "headerName cannot be null");
        this.headerName = headerName;
    }

    public void setCookieName(String cookieName) {
        Assert.notNull(cookieName, "cookieName cannot be null");
        this.cookieName = cookieName;
    }

    public void setCookieHttpOnly(boolean cookieHttpOnly) {
        this.cookieHttpOnly = cookieHttpOnly;
    }

    private String getRequestContext(HttpServletRequest request) {
        String contextPath = request.getContextPath();
        return contextPath.length() > 0 ? contextPath : "/";
    }

    public static CookieCsrfTokenRepository withHttpOnlyFalse() {
        CookieCsrfTokenRepository result = new CookieCsrfTokenRepository();
        result.setCookieHttpOnly(false);
        return result;
    }

    private String createNewToken() {
        return UUID.randomUUID().toString();
    }

    public void setCookiePath(String path) {
        this.cookiePath = path;
    }

    public String getCookiePath() {
        return this.cookiePath;
    }

    public void setCookieDomain(String cookieDomain) {
        this.cookieDomain = cookieDomain;
    }

    public void setSecure(Boolean secure) {
        this.secure = secure;
    }

    public void setCookieMaxAge(int cookieMaxAge) {
        Assert.isTrue(cookieMaxAge != 0, "cookieMaxAge cannot be zero");
        this.cookieMaxAge = cookieMaxAge;
    }
}

cookie只有在同域的情况下才能被读取, 所以杜绝了第三方站点跨域获取CsrfToken值的可能。CSRF攻击本身是不知道cookie内容的,只是利用了当请求自动携带cookie时可以通过身份验证的漏洞。但服务器对CsrfToken值的校验并非取自cookie,而是需要前端手动将CsrfToken值作为参数携带在请求里,所以cookie内的CsrfToken值并没有被校验的作用, 仅仅作为一个存储容器使用.
CsrfToken相关的管理已经了解了,我们实现的时候是通过CsrfFilter处理的。
CsrfFilter.class类:

public final class CsrfFilter extends OncePerRequestFilter {
    public static final RequestMatcher DEFAULT_CSRF_MATCHER = new CsrfFilter.DefaultRequiresCsrfMatcher();
    private static final String SHOULD_NOT_FILTER = "SHOULD_NOT_FILTER" + CsrfFilter.class.getName();
    private final Log logger = LogFactory.getLog(this.getClass());
    private final CsrfTokenRepository tokenRepository;
    private RequestMatcher requireCsrfProtectionMatcher;
    private AccessDeniedHandler accessDeniedHandler;

    public CsrfFilter(CsrfTokenRepository csrfTokenRepository) {
        this.requireCsrfProtectionMatcher = DEFAULT_CSRF_MATCHER;
        this.accessDeniedHandler = new AccessDeniedHandlerImpl();
        Assert.notNull(csrfTokenRepository, "csrfTokenRepository cannot be null");
        this.tokenRepository = csrfTokenRepository;
    }

    protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
        return Boolean.TRUE.equals(request.getAttribute(SHOULD_NOT_FILTER));
    }

    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        request.setAttribute(HttpServletResponse.class.getName(), response);
        //加载csrfToken
        CsrfToken csrfToken = this.tokenRepository.loadToken(request);
        boolean missingToken = csrfToken == null;
        if (missingToken) {
        	//如果不存在就生成
            csrfToken = this.tokenRepository.generateToken(request);
            //保存csrfToken
            this.tokenRepository.saveToken(csrfToken, request, response);
        }
		
        request.setAttribute(CsrfToken.class.getName(), csrfToken);
        request.setAttribute(csrfToken.getParameterName(), csrfToken);
        if (!this.requireCsrfProtectionMatcher.matches(request)) {
            if (this.logger.isTraceEnabled()) {
                this.logger.trace("Did not protect against CSRF since request did not match " + this.requireCsrfProtectionMatcher);
            }
            filterChain.doFilter(request, response);
        } else {
            String actualToken = request.getHeader(csrfToken.getHeaderName());
            if (actualToken == null) {
                actualToken = request.getParameter(csrfToken.getParameterName());
            }
            if (!equalsConstantTime(csrfToken.getToken(), actualToken)) {
                this.logger.debug(LogMessage.of(() -> {
                    return "Invalid CSRF token found for " + UrlUtils.buildFullRequestUrl(request);
                }));
                AccessDeniedException exception = !missingToken ? new InvalidCsrfTokenException(csrfToken, actualToken) : new MissingCsrfTokenException(actualToken);
                this.accessDeniedHandler.handle(request, response, (AccessDeniedException)exception);
            } else {
                filterChain.doFilter(request, response);
            }
        }
    }

    public static void skipRequest(HttpServletRequest request) {
        request.setAttribute(SHOULD_NOT_FILTER, Boolean.TRUE);
    }

    public void setRequireCsrfProtectionMatcher(RequestMatcher requireCsrfProtectionMatcher) {
        Assert.notNull(requireCsrfProtectionMatcher, "requireCsrfProtectionMatcher cannot be null");
        this.requireCsrfProtectionMatcher = requireCsrfProtectionMatcher;
    }

    public void setAccessDeniedHandler(AccessDeniedHandler accessDeniedHandler) {
        Assert.notNull(accessDeniedHandler, "accessDeniedHandler cannot be null");
        this.accessDeniedHandler = accessDeniedHandler;
    }

    private static boolean equalsConstantTime(String expected, String actual) {
        if (expected == actual) {
            return true;
        } else if (expected != null && actual != null) {
            byte[] expectedBytes = Utf8.encode(expected);
            byte[] actualBytes = Utf8.encode(actual);
            return MessageDigest.isEqual(expectedBytes, actualBytes);
        } else {
            return false;
        }
    }

    private static final class DefaultRequiresCsrfMatcher implements RequestMatcher {
    	//允许的方法
        private final HashSet<String> allowedMethods;
        private DefaultRequiresCsrfMatcher() {
       	//默认的允许方法
            this.allowedMethods = new HashSet(Arrays.asList("GET", "HEAD", "TRACE", "OPTIONS"));
        }
        public boolean matches(HttpServletRequest request) {
            //方法的校验
            return !this.allowedMethods.contains(request.getMethod());
        }
        public String toString() {
            return "CsrfNotRequired " + this.allowedMethods;
        }
    }
}

CsrfFilter的处理流程很清晰,当一个请求到达时,首先会调用csrfTokenRepository的loadToken方法加载该会话的CsrfToken值。如果加载不到,则证明请求是首次发起的,应该生成并保存一个新的CsrfToken值。如果可以加载到CsrfToken值, 那么先排除部分不需要验证CSRF攻击的请求方法(默认忽略了GET、 HEAD、 TRACE和OPTIONS) 。
Spring Security还提供了一个LazyCsrfTokenRepository,用来延时保存CsrfToken值(允许创建,但只有真正使用时才会被保存)


Spring Security配置跨域有多种方法。一种常见的方法是使用@CrossOrigin注解或重写addCorsMappings方法来配置跨域,但是当项目中引入了Spring Security依赖后,这种配置方式可能会失效。 为了解决这个问题,可以使用Spring Security提供的更专业的跨域方案。首先,需要创建一个继承自WebSecurityConfigurerAdapter的配置类,并重写configure方法。在configure方法中,可以通过调用HttpSecurity对象的cors方法来启用跨域配置。 在cors方法中,可以通过CorsConfigurationSource对象的configurationSource方法来配置具体的跨域设置。可以使用CorsConfiguration对象来设置允许的请求头、请求方法和请求来源。此外,还可以设置预检请求的缓存时间。最后,需要将CorsConfiguration对象注册到UrlBasedCorsConfigurationSource对象中。 下面是一个示例的配置类的代码: ```java @Configuration public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .mvcMatchers("/hello1").permitAll() .anyRequest().authenticated() .and() .formLogin() .and() .cors() // 跨域配置 .configurationSource(configurationSource()); } CorsConfigurationSource configurationSource() { CorsConfiguration corsConfiguration = new CorsConfiguration(); corsConfiguration.setAllowedHeaders(Collections.singletonList("*")); corsConfiguration.setAllowedMethods(Collections.singletonList("*")); corsConfiguration.setAllowedOrigins(Collections.singletonList("*")); corsConfiguration.setMaxAge(3600L); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", corsConfiguration); return source; } } ``` 通过以上配置Spring Security会自动应用跨域配置,并且保持其他安全配置不受影响。这种方式可以确保跨域配置Spring Security过滤器之前生效,避免了跨域配置失效的问题。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [Spring Security(七) ——跨域配置](https://blog.csdn.net/tongkongyu/article/details/125982927)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值