Spring security 交叉域

Spring security 交叉域,spring-security,cross-domain,csrf,Spring Security,Cross Domain,Csrf,我的REST API后端当前使用基于cookie的CSRF保护 基本过程是,后端设置一个可由客户端应用程序读取的cookie,然后在后续的HXR请求(我的CORS设置允许)中,一个自定义头随cookie一起传递,服务器检查两个值是否匹配 本质上,它都是通过SpringSecurity中一行非常简单的Java代码实现的 .csrf().csrfTokenRepository(new CookieCsrfTokenRepository()) 当UI从同一个域提供服务时,这非常有效,因为客户端中的J

我的REST API后端当前使用基于cookie的CSRF保护

基本过程是,后端设置一个可由客户端应用程序读取的cookie,然后在后续的HXR请求(我的CORS设置允许)中,一个自定义头随cookie一起传递,服务器检查两个值是否匹配

本质上,它都是通过SpringSecurity中一行非常简单的Java代码实现的

.csrf().csrfTokenRepository(new CookieCsrfTokenRepository())
当UI从同一个域提供服务时,这非常有效,因为客户端中的JS可以轻松访问(非http专用)cookie以读取值并发送自定义头

当我希望我的客户端应用程序部署到不同的域时,我的挑战就来了,例如

API: api.x.com
UI: ui.y.com
我解决这个问题的想法是

  • 不只是在cookie中发送回令牌,它还可以在自定义响应头中与cookie一起发送回令牌
  • 然后,客户端读取自定义头和本地sore(可以使用本地存储,也可以在客户端动态创建cookie,但这次是在UI域上,以便以后可以读取)
  • 当客户端在自定义请求头中发出XHR请求时,该值随后将被客户端使用,并且在步骤1中设置的cookie也将随之使用
  • 服务器检查这两个值(cookie和请求头)是否已设置,以及它们是否完全匹配
  • 这是一种众所周知/可接受的方法吗?任何人都可以从安全角度识别这种方法的任何明显缺陷

    显然,API服务器需要允许UI域的CORS+允许凭据,并在CORS策略中公开自定义响应头

    编辑 我将尝试使用我编写的自定义存储库在Spring Security中实现这一点:

    import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
    import org.springframework.security.web.csrf.CsrfToken;
    import org.springframework.security.web.csrf.CsrfTokenRepository;
    
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    
    /**
     * This class is essentially a wrapper for a cookie based CSRF protection scheme.
     * <p>
     * The issue with the pure cookie based mechanism is that if you deploy the UI on a different domain to the API then the client is not able to read the cookie value when a new CSRF token is generated (even if the cookie is not HTTP only).
     * <p>
     * This mechanism essentially does the same thing, but also provides a response header so that the client can read this value and the use some local mechanism to store the token (session storage, local storage, local user agent DB, construct a new cookie on the UI domain etc).
     */
    public class CrossDomainHeaderAndCookieCsrfTokenRepository implements CsrfTokenRepository {
    
        public static final String XSRF_HEADER_NAME = "X-XSRF-TOKEN";
        private static final String XSRF_TOKEN_COOKIE_NAME = "XSRF-TOKEN";
        private static final String CSRF_QUERY_PARAM_NAME = "_csrf";
    
        private final CookieCsrfTokenRepository delegate = new CookieCsrfTokenRepository();
    
        public CrossDomainHeaderAndCookieCsrfTokenRepository() {
            delegate.setCookieHttpOnly(true);
            delegate.setHeaderName(XSRF_HEADER_NAME);
            delegate.setCookieName(XSRF_TOKEN_COOKIE_NAME);
            delegate.setParameterName(CSRF_QUERY_PARAM_NAME);
        }
    
        @Override
        public CsrfToken generateToken(final HttpServletRequest request) {
            return delegate.generateToken(request);
        }
    
        @Override
        public void saveToken(final CsrfToken token, final HttpServletRequest request, final HttpServletResponse response) {
            delegate.saveToken(token, request, response);
            response.setHeader(token.getHeaderName(), token.getToken());
        }
    
        @Override
        public CsrfToken loadToken(final HttpServletRequest request) {
            return delegate.loadToken(request);
        }
    }
    
    import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
    导入org.springframework.security.web.csrf.CsrfToken;
    导入org.springframework.security.web.csrf.CsrfTokenRepository;
    导入javax.servlet.http.HttpServletRequest;
    导入javax.servlet.http.HttpServletResponse;
    /**
    *此类本质上是基于cookie的CSRF保护方案的包装器。
    *
    *纯基于cookie的机制存在的问题是,如果将UI部署在不同的域上到API,则在生成新的CSRF令牌时,客户端无法读取cookie值(即使cookie不只是HTTP)。
    *
    *该机制基本上做了相同的事情,但也提供了一个响应头,以便客户端可以读取该值,并使用一些本地机制来存储令牌(会话存储、本地存储、本地用户代理数据库、在UI域上构造新cookie等)。
    */
    公共类CrossDomainHeader和DookiecsRFTokenRepository实现CsrfTokenRepository{
    公共静态最终字符串XSRF_HEADER_NAME=“X-XSRF-TOKEN”;
    私有静态最终字符串XSRF_TOKEN_COOKIE_NAME=“XSRF-TOKEN”;
    私有静态最终字符串CSRF_QUERY_PARAM_NAME=“_CSRF”;
    私有最终CookieCsrfTokenRepository委托=新CookieCsrfTokenRepository();
    public CrossDomainHeader和OkiecSrftokenRepository(){
    delegate.setCookieHttpOnly(true);
    delegate.setHeaderName(XSRF_头_名称);
    setCookieName(XSRF_令牌_COOKIE_名称);
    delegate.setParameterName(CSRF_QUERY_PARAM_NAME);
    }
    @凌驾
    公共CsrfToken generateToken(最终HttpServletRequest请求){
    返回委托。generateToken(请求);
    }
    @凌驾
    public void saveToken(最终CsrfToken令牌、最终HttpServletRequest请求、最终HttpServletResponse响应){
    saveToken(令牌、请求、响应);
    setHeader(token.getHeaderName(),token.getToken());
    }
    @凌驾
    公共CsrfToken loadToken(最终HttpServletRequest请求){
    返回delegate.loadToken(请求);
    }
    }
    
    我认为您可以为CsrfTokenRepository提供另一个实现,以支持CSRF令牌的不同域模式

    您可以通过对代码进行以下更改来克隆原始实现:

    ....
    
    private String domain;
    private Pattern domainPattern;
    
    ....
    
    public void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response) {
    
        ....
    
        String domain = getDomain(request);
        if (domain != null) {
            cookie.setDomain(domain);
        }
    
        response.addCookie(cookie);
    }
    
    .....    
    
    public void setDomainPattern(String domainPattern) {
        if (this.domain != null) {
            throw new IllegalStateException("Cannot set both domainName and domainNamePattern");
        }
        this.domainPattern = Pattern.compile(domainPattern, Pattern.CASE_INSENSITIVE);
    }
    
    public void setDomain(String domain) {
        if (this.domainPattern != null) {
            throw new IllegalStateException("Cannot set both domainName and domainNamePattern");
        }
        this.domain = domain;
    }
    
    private String getDomain(HttpServletRequest request) {
        if (this.domain != null) {
            return this.domain;
        }
        if (this.domainPattern != null) {
            Matcher matcher = this.domainPattern.matcher(request.getServerName());
            if (matcher.matches()) {
                return matcher.group(1);
            }
        }
        return null;
    }
    
    然后,提供新的实现

    .csrf().csrfTokenRepository(new CustomCookieCsrfTokenRepository())
    

    我已经成功地在生产中使用了一个类似于我的描述编辑中的类大约1年了。课程为:

    /**
     * This class is essentially a wrapper for a cookie based CSRF protection scheme.
     * The issue with the pure cookie based mechanism is that if you deploy the UI on a different domain to the API then
     * the client is not able to read the cookie value when a new CSRF token is generated (even if the cookie is not HTTP only).
     * This mechanism does the same thing, but also provides a response header so that the client can read this value and the use
     * some local mechanism to store the token (local storage, local user agent DB, construct a new cookie on the UI domain etc).
     *
     * @see <a href="https://stackoverflow.com/questions/45424496/csrf-cross-domain">https://stackoverflow.com/questions/45424496/csrf-cross-domain</a>
     */
    public class CrossDomainCsrfTokenRepository implements CsrfTokenRepository {
    
        public static final String XSRF_HEADER_NAME = "X-XSRF-TOKEN";
        public static final String XSRF_TOKEN_COOKIE_NAME = "XSRF-TOKEN";
        private static final String CSRF_QUERY_PARAM_NAME = "_csrf";
    
        private final CookieCsrfTokenRepository delegate = new CookieCsrfTokenRepository();
    
        public CrossDomainCsrfTokenRepository() {
            delegate.setCookieHttpOnly(true);
            delegate.setHeaderName(XSRF_HEADER_NAME);
            delegate.setCookieName(XSRF_TOKEN_COOKIE_NAME);
            delegate.setParameterName(CSRF_QUERY_PARAM_NAME);
        }
    
        @Override
        public CsrfToken generateToken(final HttpServletRequest request) {
            return delegate.generateToken(request);
        }
    
        @Override
        public void saveToken(final CsrfToken token, final HttpServletRequest request, final HttpServletResponse response) {
            delegate.saveToken(token, request, response);
            response.setHeader(XSRF_HEADER_NAME, nullSafeTokenValue(token));
        }
    
        @Override
        public CsrfToken loadToken(final HttpServletRequest request) {
            return delegate.loadToken(request);
        }
    
        private String nullSafeTokenValue(final CsrfToken token) {
            return ofNullable(token)
                .map(CsrfToken::getToken)
                .orElse("");
        }
    }
    
    请注意,我还为本文中显示的
    WebSecurityConfig
    类启用了CORS属性源bean,以白名单相关的XSRF头:

    @Bean
        public UrlBasedCorsConfigurationSource corsConfigurationSource() {
            final CorsConfiguration configuration = new CorsConfiguration();
            configuration.setAllowedOrigins(properties.getAllowedOrigins());
            configuration.setAllowedMethods(allHttpMethods());
            configuration.setAllowedHeaders(asList(CrossDomainCsrfTokenRepository.XSRF_HEADER_NAME, CONTENT_TYPE));
            configuration.setExposedHeaders(asList(LOCATION, CrossDomainCsrfTokenRepository.XSRF_HEADER_NAME));
            configuration.setAllowCredentials(true);
            configuration.setMaxAge(HOURS.toSeconds(properties.getMaxAgeInHours()));
            final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
            source.registerCorsConfiguration("/**", configuration);
            return source;
        }
    

    我在这行
    response.setHeader(token.getHeaderName(),token.getToken())中得到了一个NPE
    但我刚刚添加了一个空验证,这使它能够工作。有一个PR请求来处理这个@EranMedan,我查看了你上面发布的PR。我似乎无法确定需要更改哪些依赖项才能获得此修复。我现在正在使用spring boot starter安全保护。你知道我需要在依赖项列表中更改什么吗?@alphathesis我认为它还不在maven central中,例如,唯一的方法是从源代码克隆和构建,然后进行mvn安装,然后作为依赖项包含,但在meantimep.s中复制修改后的类可能更容易。我会小心使用自定义头+本地保存的想法,这听起来像是一个可能的解决方案,但这并没有列在OWASP CSRF缓解列表中(这并不意味着它一定有缺陷),但我会测试它,并在宣布它为安全之前,与一些笔测试仪/appsec专家一起检查它。我至少要添加源标题过滤器(只需忽略任何没有来源或引用的POST请求,并在所有应用程序的XHR请求中添加自定义标题,确保您具有良好的CORS标题、CSP、HST等。感谢您的评论,基本上我认为这就是我最终要做的。我发布了我最终使用的代码。感谢您提供的示例。
    @Bean
        public UrlBasedCorsConfigurationSource corsConfigurationSource() {
            final CorsConfiguration configuration = new CorsConfiguration();
            configuration.setAllowedOrigins(properties.getAllowedOrigins());
            configuration.setAllowedMethods(allHttpMethods());
            configuration.setAllowedHeaders(asList(CrossDomainCsrfTokenRepository.XSRF_HEADER_NAME, CONTENT_TYPE));
            configuration.setExposedHeaders(asList(LOCATION, CrossDomainCsrfTokenRepository.XSRF_HEADER_NAME));
            configuration.setAllowCredentials(true);
            configuration.setMaxAge(HOURS.toSeconds(properties.getMaxAgeInHours()));
            final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
            source.registerCorsConfiguration("/**", configuration);
            return source;
        }