こんにちは。フリューのジョンです。
最近はVue.jsがあればテンプレートエンジンなんていらないと思いながらThymeleafを触っています。
さて、今回はSpring Securityを使ったAjax通信についてハマったのでそれの解消方法について書かせていただきます。
具体的には、Spring Securityの機能の一つである、クロスサイトスクリプティング防止機能でハマりました。
クロスサイトスクリプティングの説明については、今回省きます。他サイトを参照してください。
概要
環境は以下のとおりです。
spring-boot | 1.5.3.RELEASE |
spring-boot-starter-security | 1.5.3.RELEASE |
Thymeleaf | 3.0.6.RELEASE |
結論から言えば、Spring Securityを導入すると、単純なPOSTアクセスをすることができなくなります。
理由としては、認可した場所からのリクエストであることを検証するためにトークンが必要になるためです。その為、これを解消するには以下の方法があります。
上手くいかない例
まずうまくいかない、例を見てみましょう。
<body> <form action="/test" method="post"> <input name="body" value="100"> <button>post</button> </form> </body>
@Controller @RequestMapping("/") public class TestController { @GetMapping public String get() { return "test"; } @PostMapping("test") public String post(@RequestBody String body) { return "test"; } }
この状態でボタンを叩くと以下のような画面になります。
Formにトークンを付ける
これはSpring Security+Thymeleafを導入すると簡単に対応できます。
具体的にはFormのaction属性をThymeleafのものにします。
<body> <form th:action="@{/test}" method="post"> <input name="body" value="100"> <button>post</button> </form> </body>
Formタグを見てみると自動でinput[type=’hidden’]が追加されているのがわかると思います。
Ajaxにする場合、このinput[name=’_csrf’]の値をJavaScriptで取得すれば良いのです。しかし、この方法はFormタグがなければ成り立ちません。
FormがないAjaxのpost通信をするためには、以下の方法があります。
Cookieにトークンをつけて、リクエストヘッダにトークンをつけて送信する
Spring Securityでは、Cookieにトークンをつけることができます。そのためには、CookieCsrfTokenRepositoryを使用します。
@Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() ...... .and().csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()); } }
withHttpOnlyFalseとしているのは、HTTPでもCookieにトークンをつけてもらうためです(デフォルトはHTTPSだけにしかCookieは付いてきません)。リクエストを見てみると、以下のようにXSRF-TOKENという名前のCookieがついているのがわかります。
このCookieの値をJavaScriptで読み込み、リクエストヘッダにX-XSRF-TOKENをつけます。
<body> <div id="app"> <input v-model="body"/> <button v-on:click="submit">post</button> </div> <script> new Vue({ el: '#app', data() { return { body: 100 } }, methods: { submit: function() { const xsrf = $.cookie('XSRF-TOKEN'); $.ajax({ type: 'post', url: '/test', data: { body: this.body }, headers: { 'X-XSRF-TOKEN': xsrf } }).done(() => console.log('success')).fail(() => console.log('fail')); } } }) </script> </body>
これによりAjax通信が可能になります。しかし、いちいちCookieをとってリクエストヘッダに渡すのは面倒ですね。
Axiosというライブラリではそれを解消してくれます。内部でcookie()を見て、リクエストヘッダに追加してくれているのです。
submit: function() { axios.post('/test', { body: this.body }).then(() => console.log('success')).catch(() => console.log('fail')); }
非常に便利ですね。
Cookieの名前、ヘッダの名前は、デフォルトの値ですので、変更可能です。もちろんAxiosの方も変更が可能です。
AxiosのFormData通信
ちょっと話が変わりますが、Spring SecurityでのAjaxのlogin機能を実装しようとした時、すこし、ハマりましたので追記します。
Axiosで通信するとデフォルトはJsonでの通信になります。しかし、loginには、FormDataでパラメータを渡さなければいけません。
実装方法としては、以下のようにAxiosのpostメソッドの第3パラメータに変換メソッドを追加してあげます。
axios.post('/login', params, { transformRequest: [function(data) { const formData = new FormData(); formData.append('username', data.username); formData.append('password', data.password); return formData; }] })
まとめ
Spring Securityを使ったajax通信は少しだけ工夫が必要です。
しかし、その設定は本当に少しだけです。
ここではSpring Securityの機能自体について触れませんでしたが、Spring Securityは本当に素晴らしいので、ぜひ使ってみてください。
そして、Ajax通信だけのために、jQueryを使うのは止めましょう。Axiosを使ってみましょう。(個人の感想です)
最後に
弊社は、Springを実践利用してバリバリコード書きたいエンジニア募集中です!