簡體   English   中英

Spring + Angular 2 + Oauth2 + CORS 的 CSRF 問題

[英]CSRF issue with Spring + Angular 2 + Oauth2 + CORS


我正在開發基於 Spring 4.3 和 Angular (TypeScript) 4.3 的客戶端 - 服務器應用程序,在 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);            
    }
}     

受 Oauth2 保護,如 spring 文檔中所述。 顯然,我修改了上述內容以適合我的應用程序。 一切正常:我能夠通過 refresh_token 和 access_token 使用 Oauth2 保護/my-api/test Oauth2 沒有問題。


2. CORS 配置:
由於服務器相對於客戶端位於單獨的域上(服務器: 10.0.0.143:8080 : 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配置:

如果我現在像這樣在 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 設置在響應 cookie 中(通過使用 Chrome 檢查請求可見,響應頭下的 Set-Cookie)。

接下來應該發生的是,angular 在執行第一個 POST 請求時,應該讀取從 Spring 收到的 cookie 並生成一個名為X-XSRF-TOKEN的請求 Header,其值等於 cookie 的值。

如果我檢查失敗的 POST 請求的 Header,我會看到沒有 X-XSRF-TOKEN,就像 angular 沒有按照它的 CSRF 規范做它應該做的一樣,請看圖片:

失敗的 CSRF 請求,Chrome 檢查器

查看 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);
  }
}


如果我自己添加 X-XSRF-TOKEN 標頭怎么辦?
由於 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 檢索失敗,因為即使 Spring 在響應中返回 XSRF-TOKEN cookie,它也不會在瀏覽器中設置,因為它與客戶端位於不同的域中(服務器域: 10.0.0.143:8080 ,客戶端域: localhost:4200 )

如果服務器也在 localhost 上運行,即使在不同的端口上(即服務器域: localhost:8080 ,客戶端域: localhost:4200 ),響應中從 spring 服務器設置的 cookie 在瀏覽器中正確設置,因此可以使用this.getCookie('XSRF-TOKEN')方法通過 angular 檢索

看看我的意思是觀察下圖中兩個不同調用的結果:

localhost 和跨域 POST 請求,chrome 檢查

如果我是對的,這與域localhost:4200無法通過 javascript 讀取域10.0.0.143:8080的 cookie 的事實是一致的。 請注意,選項withCredentials = true允許 cookie 從服務器流向客戶端,但只是透明的,這意味着它們不能通過 javascript 修改。 只有服務器可以讀取和寫入自己域的 cookie。 甚至客戶端也可以,但前提是它與服務器在同一個域中運行(我說得對嗎?)。 如果服務器和客戶端都在同一個域上運行,即使在不同的端口上,手動添加頭也是有效的(但在生產服務器和客戶端位於不同的域中,所以這不是解決方案)。


所以問題是

目前的選項是:

  1. 如果我正確理解了該機制,如果客戶端和服務器位於不同的域上,Spring 和 Angular 標准 CSRF 令牌交換機制將無法工作,因為 (1) angular 實現不支持它和 (2) javascript 無法訪問 XSRF-TOKEN cookie因為后者在服務器的域上。 如果是這種情況,我可以在沒有 CSRF 的情況下僅依賴無狀態 oauth2 refresh_token 和 access_token 安全性嗎? 從安全的角度來看可以嗎?

  2. 或者,另一方面,我遺漏了一些東西,還有另一個我沒有看到的原因(這就是我問你的原因,親愛的開發人員)實際上 CSRF 和 CORS 應該可以工作,所以這是我的代碼錯誤或錯誤缺少一些東西。

鑒於這種情況,你能告訴我你會怎么做嗎? 我的代碼中是否有一些錯誤導致跨域場景中的 CSRF 不起作用? 如果您需要更多信息來詢問我的問題,請告訴我。

抱歉有點長,但我認為最好解釋一下完整的解決方案,以便讓您了解我面臨的問題。 而且,此外,我編寫和解釋的代碼可能對某些人有用。

最好的問候, 吉安卡羅

廣告 2. 您的代碼完全有效並且您正確設置了所有內容。 Spring 中的 CSRF 保護設計為前端與后端在同一域中。 由於 Angular 無法訪問 CSRF 數據,因此顯然無法在修改請求中設置它。 如果沒有在常規標頭(不是 cookie)的服務器過濾器中設置它們,就無法訪​​問它們。

廣告 1. JWT 令牌的安全性足夠好,因為大公司成功使用了它們。 但是,請記住,令牌本身應使用 RSA 密鑰(而不是更簡單的 MAC 密鑰)進行簽名,並且所有通信都必須通過安全連接 (https/ssl)。 使用刷新令牌總是會略微降低安全性。 業務應用程序通常會忽略它們。 一般受眾應用程序必須安全地存儲它們,但仍可以選擇在濫用時放棄它們的有效性。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM