Spring + Angular 2 + Oauth2 + CORS 的 CSRF 问题

CSRF issue with Spring + Angular 2 + Oauth2 + CORS


我正在开发一个基于 Spring 4.3 和 Angular (TypeScript) 4.3 的 client-server 应用程序,在 CORS 场景中,在生产环境中,服务器和客户端位于不同的域中。客户端通过 http 请求请求 REST 服务器 API。


1。 REST 和 OAUTH 配置:
服务器公开 REST API:

@RestController
@RequestMapping("/my-api")
public class MyRestController{

@RequestMapping(value = "/test", method = RequestMethod.POST)   
    public ResponseEntity<Boolean> test()
    {                   
        return new ResponseEntity<Boolean>(true, HttpStatus.OK);            
    }
}     

如 spring 文档中所述,受 Oauth2 保护。显然我修改了上面的内容以适合我的应用程序。 一切正常:我可以通过 refresh_token 和 access_token 使用 Oauth2 保护 /my-api/test。 Oauth2 没有问题。


2。 CORS 配置:
由于服务器相对于客户端位于单独的域中(服务器:10.0.0.143:8080,客户端:localhost:4200,正如我现在正在开发的那样),我需要在服务器端启用 CORS:

@Configuration
@EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {

    ...

    @Autowired 
    SimpleCorsFilter simpleCorsFilter;   

    @Override
    public void configure(HttpSecurity http) throws Exception {

      http
        .cors().and()
        .addFilterAfter(simpleCorsFilter, CorsFilter.class)
        .csrf().disable()  // notice that now csrf is disabled

    ... (the rest of http security configuration follows)...

    }     

}

其中 SimpleCorsFilter 添加了我需要的 headers:

@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class SimpleCorsFilter extends OncePerRequestFilter  {

    public SimpleCorsFilter() {         
    }

    @Override
    public void destroy() {
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException {

        response.addHeader("Access-Control-Allow-Origin", "http://localhost:4200");
        response.addHeader("Access-Control-Allow-Methods", "GET,PUT,POST,DELETE,PATCH,OPTIONS");
        response.addHeader("Access-Control-Max-Age", "3600");
        response.addHeader("Access-Control-Allow-Credentials", "true");           
        response.addHeader("Access-Control-Allow-Headers", "MyCustomHeader, Authorization, X-XSRF-TOKEN");

        if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
            response.setStatus(HttpServletResponse.SC_OK);
        } else {
            chain.doFilter(request, response);
        }

    }
}

现在,如果我使用 angular2 发出 http get 或 post 请求,示例:

callPostMethod(tokenData) {

      const url = 'my-api.domain.com/my-api';
      const pars = new HttpParams();
      const body = null;
      let hds = new HttpHeaders()
          .append('Authorization', 'Bearer ' + tokenData.access_token)
          .append('Content-Type', 'application/x-www-form-urlencoded');

      return this.http.post <Installation> (url, body, {
              params : pars,
              headers: hds,
              withCredentials: true
          });
  }

一切正常。所以即使是 CORS 配置似乎也没问题。


3。 CSRF 配置:

如果我现在像这样在 Spring 中启用 CSRF 配置:

@Configuration
@EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {

    ...

    @Autowired 
    SimpleCorsFilter simpleCorsFilter;   

    @Override
    public void configure(HttpSecurity http) throws Exception {

      http
        .cors().and()
        .addFilterAfter(simpleCorsFilter, CorsFilter.class)
        .csrf().csrfTokenRepository(getCsrfTokenRepository()) // notice that csrf is now enabled

    ... (the rest of http security configuration follows)...

    }   

    @Bean
    @Autowired
    // used only because I want to setCookiePath to /, otherwise I can simply use
    // http.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
    public CsrfTokenRepository getCsrfTokenRepository() {
        CookieCsrfTokenRepository tokenRepository = CookieCsrfTokenRepository.withHttpOnlyFalse();
        //tokenRepository.setHeaderName("X-XSRF-TOKEN"); // has already this name --> comment
        tokenRepository.setCookiePath("/");
        return tokenRepository;
    }         
}

在第一个 POST 请求中,它给了我 403 错误:

"Invalid CSRF Token &#39;null&#39; was found on the request parameter &#39;_csrf&#39; or header &#39;X-XSRF-TOKEN&#39;."


为什么会这样(据我了解..)?
通过探索 CSRF 的工作机制,我注意到 Spring 正确地生成了一个名为 XSRF-TOKEN 的 cookie,它是在响应 cookie 中设置的(通过使用 Chrome、Set-Cookie 下响应 Headers).

接下来应该发生的是 angular,在执行第一个 POST 请求时,应该读取从 Spring 收到的 cookie 并生成一个请求 Header 调用 X-XSRF-TOKEN,其值等于cookie的值

如果我检查失败的POST请求的Header,我看到没有X-XSRF-TOKEN,就像angular没有做出它应该做的按照它的 CSRF 规范来做,见图:

Failed CSRF request, Chrome inspector

查看xsrf (/angular/angular/blob/4.3.5/packages/common/http/src/xsrf.ts)的angular实现,你可以看到在HttpXsrfInterceptor中,如果没有添加csrf headers目标URL以http开头(后面是angularxsrf.ts源码复制粘贴):

/**
 * `HttpInterceptor` which adds an XSRF token to eligible outgoing requests.
 */
@Injectable()
export class HttpXsrfInterceptor implements HttpInterceptor {
  constructor(
      private tokenService: HttpXsrfTokenExtractor,
      @Inject(XSRF_HEADER_NAME) private headerName: string) {}

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    const lcUrl = req.url.toLowerCase();
    // Skip both non-mutating requests and absolute URLs.
    // Non-mutating requests don't require a token, and absolute URLs require special handling
    // anyway as the cookie set
    // on our origin is not the same as the token expected by another origin.
    if (req.method === 'GET' || req.method === 'HEAD' || lcUrl.startsWith('http://') ||
        lcUrl.startsWith('https://')) {
      return next.handle(req);
    }
    const token = this.tokenService.getToken();

    // Be careful not to overwrite an existing header of the same name.
    if (token !== null && !req.headers.has(this.headerName)) {
      req = req.clone({headers: req.headers.set(this.headerName, token)});
    }
    return next.handle(req);
  }
}


如果我自己添加 X-XSRF-TOKEN HEADER 会怎样?
由于 angular 没有,我尝试通过如下修改请求 headers 将 header 附加到请求中:

const hds = new HttpHeaders()
          .append('Authorization', 'Bearer ' + tokenData.access_token)
          .append('Content-Type', 'application/x-www-form-urlencoded');

      if (this.getCookie('XSRF-TOKEN') !== undefined) {
          hds = hds.append('X-XSRF-TOKEN', this.getCookie('XSRF-TOKEN'));
      }

其中 this.getCookie('XSRF-TOKEN') 是一种使用 'angular2-cookie/services' 读取浏览器 cookie 的方法,但是 this.getCookie('XSRF-TOKEN') returns null.

为什么?据我了解,javascript cookie 检索失败,因为即使 XSRF-TOKEN cookie 在响应中由 Spring 返回,它也没有在浏览器中设置,因为它与客户端处于不同域(服务器域:10.0.0.143:8080,客户端域:localhost:4200

如果服务器 运行 也在本地主机上,即使在不同的端口上(即服务器域:localhost:8080,客户端域:localhost:4200), spring 服务器在响应中设置的 cookie 已在浏览器中正确设置,因此可以通过 angular 使用方法 this.getCookie('XSRF-TOKEN').

检索

看看我的意思,观察下图中两个不同调用的结果:

localhost and cross-domain POST requests, chrome inspection

如果我是正确的,这与域 localhost:4200 无法通过 javascript 域 10.0.0.143:8080 的 cookie 读取这一事实是一致的。请注意选项 withCredentials = true 允许 cookie 从服务器流向客户端,但只是透明的,这意味着它们不能通过 javascript 修改。只有服务器可以读写自己域的cookies。甚至客户端也可以,但前提是它 运行 与服务器位于同一域中(我说得对吗?)。 如果服务器和客户端 运行 在同一个域上,即使在不同的端口上,手动添加 header 也有效(但在生产服务器和客户端在不同的域上,所以这不是解决方案)。


问题是

目前的选项是:

  1. 如果我对机制的理解正确,Spring 和 Angular 如果客户端和服务器位于不同的域中,标准 CSRF 令牌交换机制将无法工作,因为 (1) angular 实现不支持它,并且 (2) javascript 无法访问 XSRF-TOKEN cookie,因为后者位于服务器的域中。如果是这种情况,我可以只依赖无状态 oauth2 refresh_token 和 access_token 安全性,而无需 CSRF 吗?从安全角度来看可以吗?

  2. 或者,也许,另一方面,我遗漏了一些东西,还有另一个我没有看到的原因(这就是我问你的原因,亲爱的开发人员)实际上 CSRF 和 CORS 应该工作,所以这是我的代码错误或遗漏了一些东西。

在这种情况下,你能告诉我你会怎么做吗?有没有电子我的代码中的 ror 使 cross-domain 场景中的 CSRF 不起作用? 如果您需要更多信息来问我的问题,请告诉我。

抱歉有点长,但我认为最好解释完整的解决方案,以便您理解我面临的问题。 此外,我编写和解释的代码可能对某些人有用。

此致, 詹卡洛

广告 2。您的代码完全有效,并且您正确设置了所有内容。 spring 中的 CSRF 保护设计为前端与后端位于同一域中。由于 Angular 无法访问 CSRF 数据,因此显然无法在修改请求中设置它。如果不在服务器过滤器中以常规 headers(不是 cookie)设置它们,就无法访​​问它们。

广告 1。 JWT 令牌的安全性足够好,因为大公司成功地使用了它们。但是,请记住,令牌本身应该使用 RSA 密钥(而不是更简单的 MAC 密钥)进行签名,并且所有通信 必须 通过安全连接(https/ssl)。使用刷新令牌总是会略微降低安全性。业务应用程序通常会省略它们。一般受众应用程序必须安全地存储它们,但可以选择在滥用情况下降低它们的有效性。