#前段として、普通にCORSのWeb APIを作るところは、こちらのTutorialをご参照ください。この記事は、CORSのWeb APIを作ったあとに、OAuth2で認証・認可フローを追加したときの話です。よろしければ手前みそですが、こちらの記事が親記事になりますので、ご参照ください。
あれ、OAuth2のフローがブラウザから上手く動かない?
先日、Spring BootでOAuth2の機能を使って自社のWebアプリケーションをつくっていたところ、Chromeなどのブラウザから利用した際に、HTTP GETやPOSTを送信したつもりなのに、なぜかHTTP OPTIONSが送信され、エラーになる、という事態に遭遇しました。
これはpreflight request(プリフライトリクエスト)と呼ばれ、CORS(クロスオリジンリソースシェアリング)、つまり公開されているWeb APIに対して、その公開サーバとは異なるホスト名のWebサーバからUIを提供されたブラウザがWeb APIにアクセスする際には、HTTP GETやPOSTの前に、どんなHTTP Methodが利用できるか、ヘッダのオプションは何か、といったことをHTTP OPTIONSで確認するための動作だそうです。
CORSの既定動作として標準化されているようで、詳細はMDNの解説ページを参照いただければと思いますが、プリフライトリクエストを行なう条件に対して、OAuth2の場合
・Content-Typeが、application/jsonである
・認証トークンを、Authorization Bearer:(トークン)として、ヘッダに(Agentが自動で設定せず)自分で設定する
などなど、完全に引っかかってしまうので、OPTIONSが送信されてしまいます。
Springでどう対応すれば良い?
ところが、Spring Boot(Spring WebService/MVC/Security)では、何もしないデフォルトの状態では、OPTIONSに対して403 Forbiddenを返してしまいます。Spring Securityの機能を使って、OAuth2をサポートしたはずなのに!
振り返って、このCORSについてのMDNの解説ページを見ると、シンプルなクロスサイトリクエストにおいて許可されたメソッドは、GET・HEAD・POSTとありますね…OPTIONSはないぞ…と思って、Springの仕様書のCORSのページを見ると、おお、たしかに同じことが、27.3節に書いてあります。「By default all origins and GET, HEAD, and POST methods are allowed」。
答えは『Filter based CORS support』のようだ
このSpringの仕様書のCORSサポートのページには、CORSの様々な対応が書いてありますが、同じページの27.5節が、今回に関係するところです。
Spring SecurityでOAuth2対応をすると、Spring Securityはフィルタとして実装されているので、それよりも優先度の高いフィルタとして設定する必要がある、というのが話のようでした。
先ほどの仕様書のページだと、このフィルタに優先度の設定が無いので、実際は、最後の行にある「You need to ensure that CorsFilter is ordered before the other filters, see this blog post about how to configure Spring Boot accordingly.」に従って、リンク先のブログを見ます。
自分が少し修正したサンプルがこちらです。
@Configuration
public class MyCorsPreflightConfiguration {
@Bean
public FilterRegistrationBean corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOrigin(CorsConfiguration.ALL);
config.addAllowedHeader(CorsConfiguration.ALL);
config.addAllowedMethod(CorsConfiguration.ALL); // 細かく設定可
source.registerCorsConfiguration("/oauth/token", config); // OAuth EP
source.registerCorsConfiguration("/func/**", config); // 個別設定
FilterRegistrationBean bean = new FilterRegistrationBean(new CorsFilter(source));
bean.setOrder(Ordered.HIGHEST_PRECEDENCE);
return bean;
}
}
今回は、ヘッダやメソッドは全部OKにしていますが、実際に参考にして頂く際は、細かく指定して頂いたほうが良いと思います。
また、この設定が適用されるパス(例では/func/**)は、個々のAPIのエンドポイントに合わせてください。
おまけ
この対応をするのに、stackoverflowなど、いろいろなサイトを参考にさせて頂きましたが、過去には、Springの開発者宛にもかなりの議論があったようです。そして幾つかの回避策[1][2]を編み出したdeveloperの方々がいます。恐らくそれを反映した形で、最新のSpringの仕様書が更新されているようでしたので、この最終形をさくっと見たら良いという情報が(はまった自分も含め)役に立つのではないか、と今回ご紹介した次第です。
(終わり)