Swagger-UI Fetch 不同 Origin 的 api-docs (CORS 問題) | Piece of DevOps

在程式碼的拼拼湊湊下,終於在 Spring 5 加入 SpringFox 的 Swagger -UI。原本以為一切就這樣快快樂樂的到一個段落,但事情往往不會這麼簡單......

因為 SpringFox 的 Swagger-UI 有兩種使用方式:
  1. 使用 URL:http(s)://<your-host>/swagger-ui.html。直接連上內崁的 Swagger-UI。
  2. 使用 URL:http(s)://<your-host>/v2/api-docs。取得 API 文件的 json 檔,再透過其它主機上的 Swagger-UI 顯示。
第一種方式相當直覺,API Server 在哪裡,文件就在哪裡。第二種則是將文件的 json 檔導向其它 Server 上的 Swagger-UI ,以顯示 API 文件內容。這兩種方式都沒有什麼太大的問題,有問題的地方在於:
  • 誰可以看到這份 API 文件?
  • 是否要將 API 文件顯示也要交由 API Server 處理?
基於以上兩點考量,服務的架構從原本的第一種方式簡單了結,變成需要符合以下條件的架構:
  • 將 API 文件顯示工作拆出 API Server(也就是第二種方式)。
  • 只有通過 JWT 認證的 Request 才能透過 /v2/api-docs 從 Server 取得 API 的文件。
只有敘述可能沒辦法快速瞭解整體關係,所以附上關係圖。如下:


需要實作部分看起來,只要讓 Swagger-UI 取得 /v2/api-docs 時,帶 JWT Token,然後將URL:/v2/api-docs 加入原有的 JWT Filter 中即可。到目前為止,感覺一切都還在掌控之中(Flag 立起來! 立起來!)。

果然,馬上遇到第一個問題:

要怎麼讓 Swagger-UI 取得 /v2/api-docs 時,帶 JWT Token?

翻了一下 Swagger-UI 的 Code 後,一陣濃濃的絕望。看起來是沒有現成的參數能讓我帶入 JWT Token。所以只好決定直接改 Source Code ,把 Token 硬塞進取得 /v2/api-docs 的 Request 中。

經過一陣翻找,終於找到 Swagger-UI 用來下載 API 文件的程式碼:download-url.js。只要在執行 Fetch 前,將 JWT Token 做好,並帶入 Header 之中(如下),即可將 JWT Token 傳給 API Server,通過 JWT 認證。

download-url.js

...
const actions = {
    download: (url)=> ({ errActions, specSelectors, specActions, getConfigs }) => {
      let { fetch } = fn
      const config = getConfigs()
      url = url || specSelectors.url()
      specActions.updateLoadingStatus("loading")
      errActions.clear({source: "fetch"})
      //
      // 在這裡把 JWT Token 做出來,以便在下方帶入(以下 token 只是任意給予的範例)
      //
      var token = "abcd"
      fetch({
        url,
        loadSpec: true,
        requestInterceptor: config.requestInterceptor || (a => a),
        responseInterceptor: config.responseInterceptor || (a => a),
        credentials: "same-origin",
        headers: {
          "Accept": "application/json,*/*",
          "Authorization": "Bearer " + token // 加入 JWT Token
        }
      }).then(next,next)
...

帶入 JWT Token 的部分,就到一個段落。接下來,將 /v2/api-docs 加入既有的 JWT Filter 的部分,也沒有遇到什麼問題,就不贅述。看起來一切就緒了,就來馬上測試一下吧!

是忘記 CORS,還是害怕想起?

就在連線的時候,一切都不對勁了。因為 Swagger-UI 說這樣母湯:

Failed to load API definition.

Fetch error
Failed to fetch http://127.0.0.1:8080/v2/api-docs

Fetch error
Possible cross-origin (CORS) issue? The URL origin (http://127.0.0.1:8080) does not match the page (http://localhost:3002). Check the server returns the correct 'Access-Control-Allow-*' headers.


看來是 CORS 找上門了(救命啊!)。為什麼會找上門呢?原來是因為我們已經進行
Cross Origin 取得資源的動作,而不自知。所以 Brower 接管此請求,除非我們好好跑完 CORS 的流程,不然我們什麼都拿不到。一陣懊惱後,爬了一些文章然後仔細看了錯誤訊息,找到一點端倪:

Check the server returns the correct 'Access-Control-Allow-*' headers.

因為在 Request 中加入了額外的 Header:Authorization,所以我們需要在 API Server 端完成 CORS 流程中的 Preflighted 階段。在這個階段,我們需要讓 API Server 回覆允許存取的 Origin、Header 等 Request 所需夾帶參數給 Browser,讓 Browser 判斷此 CORS Request 是否可以繼續執行下去。要注意的是,在這階段完成前,我們所夾帶的額外參數不會讓 API Server 收到(詳情可以看這裡)。

那麼接下來,我們就在 API Server 再加上 CORS 的 Filter ,來處理這件事吧!

API Server 的 CORS Filter 設定

以下為 CORS Filter 的設定方式。這邊要注意的是,CORS 會是兩個階段,第一個就是剛剛所提到的 Preflighted。在此階段,使用到的 Method 是 OPTIONS。也因為 CORS,會需要收到 Preflighted 階段的 HTTP Success Code(200) 後才會跑到第二階段,也就是實際的 API 內容。所以在以下設定中,當收 OPTIONS 時,會先將設定好的 Response 夾帶 HTTP Status Code 200 回傳給 Client 端,以讓第二階段可以照流程運作下去,也就是以下 Else 的情況。

另外,如果想要嚴謹一點處理 CORS,也可以在其中先判斷哪些 Request 所需要的允許內容(Access-Control-Allow-*)是我們許可的。如果不通過判斷,及回覆非 200 的 Status Code,Browser 就會終止此Cross Origin 請求。

@Component
public class CORSFilter implements Filter {
    private static Logger log = Logger.getLogger(CORSFilter.class.getName());

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {
        HttpServletResponse response = (HttpServletResponse) res;
        HttpServletRequest request = (HttpServletRequest) req;

        response.setHeader("Access-Control-Allow-Origin", "*");
        response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE");
        response.setHeader("Access-Control-Max-Age", "3600");
        response.setHeader("Access-Control-Allow-Credentials", "true");
        response.setHeader("Access-Control-Allow-Headers", "Content-Type, api_key, Authorization, Origin");
        response.setHeader("Content-Type", "application/json");

        if(request.getMethod().equals("OPTIONS")){ // 階段 1
            response.setStatus(200);
            response.flushBuffer();
        }else{                                     // 階段 2
            chain.doFilter(req, res);
        }
    }

    @Override
    public void init(FilterConfig filterConfig) {}

    @Override
    public void destroy() {}
}

收尾

這些設定結束後,基本上就可以成功串起 Swagger-UI 跟 Spring 囉。經過這次經驗讓我學到:
  1. 不要亂立 Flag。
  2. 血尿的開始,都是因為對所要處理的事務一知半解。
  3. 技術債不會不見,只是什麼時候出現。這次的 CORS 就是很好的例子。
此外,除了這種 Swagger-UI 帶 Auth 串接 Spring 的方式,或許還有更好的方法。此方式僅提供參考,希望對讀者有所幫助。

熱門文章