SpringSecurityとPOSTで嵌った話
SpringSecurity+SpringBootでRestAPIサーバーを作っていたのですが、
ブラウザからPOSTを投げてみると、GETでは認証OKな状態でも403エラーになってしまう現象にぶつかりました。
結構原因究明に苦労したので、備忘も兼ねて書いておきます。
そもそもなぜ弾かれたか?
SpringSecurityは厳重なCSRF対策を行っており、有効化されている場合にPOSTやPUTと言ったリクエストメソッドに対してトークンを要求するため。
トークンを何らかの方法で取得した上で、HttpRequestのHeaderに詰めてやらないと問答無用に403エラーで弾いてきます。
トークンの取得方法
デフォルトではサーバーサイドにリクエストスコープで保持されるだけなのでクライアントサイドからはログインしていてもトークンは見えません。
手動でクライアントサイドに渡して管理しておくか、設定を切り替えてCookieに入るようにする必要があります。
具体的にはWebSecurityConfigで以下のような形にします。
@Override
public void configure(HttpSecurity http) throws Exception {
http.oauth2Login().and().authorizeRequests().antMatchers("/**")
.authenticated()
.and() //ここからが問題の部分
.csrf()
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
}
こうすることで、CookieにXSRF-TOKENという名前でトークンが入ってくるようになります。
トークンを入れてPOSTを投げる
取得したトークンはX-XSRF-TOKEN
という名前でヘッダに入れます。
Cookieに入っていた時のキー名とは微妙に異なるので注意です。
実際のコードは大体以下のような形になります。
なお、Cookieからの値の取り出しにjs-cookieを使っています。
import Cookie from 'js-cookie';
//中略
const xsrf = Cookie.get('XSRF-TOKEN');
const resp = await fetch(url),{
method: 'POST',
credentials: 'same-origin',
headers: {
"Content-Type": "application/json; charset=utf-8",
'X-XSRF-TOKEN': xsrf
},
body:JSON.stringify(param)
});
ちなみに、axiosなどライブラリによってはここを自動でやってくれるものがあるそうです。
余談
逆に、外部からWebHookなどのPOSTリクエストを認証無しで受けたい場合は、ignoringの方を使うとうまく行きます。
別途アクセス制御はかけないと危険ですが。
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/css/**", "/image/**", "/js/**").antMatchers(HttpMethod.POST, "/api/outer/**");
}
まとめ
- SpringSecurityを使っている場合POSTにはtokenが必要
- WebSecurityConfigで設定しないとtokenはCookieに乗ってくれない
- テスト用にRestClientから手動で叩く場合でも同じ手法でOK