首页 文章

Spring Angular 2 Oauth2 CORS的CSRF问题

提问于
浏览
10

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

1. REST AND OAUTH CONFIGURATION:
服务器公开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);            
    }
}

受到Oauth2的保护,正如Spring文档中所解释的那样 . 显然我修改了上面的内容以适合我的应用程序 . 一切正常:我能够通过refresh_token和access_token保护 /my-api/test 与Oauth2 . Oauth2没问题 .

2. CORS CONFIGURATION:
由于服务器位于相对于客户端的单独域(服务器: 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添加我需要的标头:

@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 CONFIGURATION:

如果我现在在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;."

WHY DOES THIS HAPPEN (as far as I understood..) ?
通过探索CSRF如何工作的机制,我注意到Spring正确生成了一个名为 XSRF-TOKEN 的cookie,它在响应cookie中设置(通过使用Chrome检查请求,响应标头下的Set-Cookie可见) .

接下来应该发生的是,当执行第一个POST请求时,angular应该读取从Spring接收的cookie并生成一个名为 X-XSRF-TOKEN 的请求Header,其值等于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),您可以看到在HttpXsrfInterceptor中,如果目标URL以 http 开头,则不会添加csrf标头(接下来是angular xsrf.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);
  }
}

WHAT IF I ADD THE X-XSRF-TOKEN HEADER MYSELF?
由于angular没有,我试图通过修改请求标头来自己将标头附加到请求,如下所示:

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') 返回null .

为什么?据我所知, javascript cookie retrieval fails because, even if XSRF-TOKEN cookie is returned by Spring in the response, it is not set in the browser because it is in a different domain with respect to the client (server domain: 10.0.0.143:8080, client domain: localhost:4200) .

相反,服务器也运行在localhost,即使在不同的端口(即服务器域: localhost:8080 ,客户端域: localhost:4200 ), the cookie set from spring server in the response is correctly set in the browser and thus can be retrieved by 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进行修改 . 只有服务器可以读写自己域的cookie . 或者甚至客户端都可以,但前提是它与服务器在同一个域中运行(我是否正确?) . 如果服务器和客户端都在同一个域上运行,即使在不同的端口上运行,手动标头添加也会起作用(但是在 生产环境 服务器和客户端中,它们位于不同的域上,因此这不是解决方案) .

SO THE QUESTION IS

目前的选择是:

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

  • 或者,另一方面,我错过了一些东西,还有另一个原因,我没有看到(这就是为什么我问你,亲爱的开发人员),实际上CSRF和CORS应该工作,所以这是我的代码错误或缺少一些东西 .

鉴于这种情况,你能告诉我你会做什么?我的代码中是否存在一些错误,这使得跨域方案的CSRF不起作用?如果您需要其他信息来询问我的问题,请与我们联系 .

很抱歉有点长,但我认为最好先解释完整的解决方案,以便让您了解我所面临的问题 . 此外,我编写和解释的代码对某些人有用 .

最好的问候,吉安卡洛

1 回答

  • 2

    Ad.2 . 您的代码完全有效,您可以正确设置所有内容 . Spring 天的CSRF保护设计与前端在同一领域 . 由于Angular无法访问CSRF数据,因此显然无法在修改请求中设置它 . 如果没有在常规标头(不是cookie)中将它们设置在服务器过滤器中,则无法访问它们 .

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

相关问题