Spring Security | RestAPIでCSRFトークンを利用する

2022.08.16

SpringSecuritは、CSRFトークンをフォームに対してhidden項目としてCsrfRequestDataValueProcessorが自動的に埋め込んでいます。関連記事:https://volkruss.com/?p=3125

これはこれでいいのですが、RESTAPIでCSRFトークンを利用するにはどうすればいいのか。ということでやってみます。

CSRFトークンを利用する

https://www.baeldung.com/csrf-stateless-rest-api

上記のサイトに答えが書いてありますが、Securityの設定時にcsrfTokenRepositoryを利用するだけです。

以下のような設定クラスを作成して試してみます。

@Configuration
public class SecurityConfig {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
        http.authorizeRequests(auth -> {
            auth.antMatchers("/**").permitAll();
        });
        return http.build();
    }
}

PostmanからGET送信をするとクッキーにCSRFトークンが設定されているのがわかります

画像

この値をHeadersに付与してPOST送信をしてみます。SpringはX-XSRF-TOKENヘッダで受け取るようになっています。

画像

正しくPOSTメソッドが利用できています。

次にこのトークンを削除してPOST送信を行います

画像

しっかり403エラーになっていることがわかります。

CORSの設定

jsからの利用をしてみるためcorsの設定をしておきます。今回は3000番ポートからリクエストを送信します

@Configuration
public class SecurityConfig {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
        http.authorizeRequests(auth -> {
            auth.antMatchers("/**").permitAll();
        });
        // 追加
        http.cors().configurationSource(corsConfigurationSource());
        return http.build();
    }
    // CORSの設定
    @Bean
    public CorsConfigurationSource corsConfigurationSource(){
        CorsConfiguration cors = new CorsConfiguration();
        cors.setAllowedOrigins(Arrays.asList("http://localhost:3000"));
        cors.setAllowedMethods(Arrays.asList("GET","POST"));
        cors.setAllowedHeaders(Arrays.asList("*"));
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**",cors);
        return source;
    }
}

configurationSourceを設定してあげればOKです

JavaScriptから利用する

ボタンを二つ用意してGETとPOSTを行えるようにします。

画像

  • GETボタン
    • CookieにCSRFトークンを受け取る
  • POSTボタン
    • CSRFトークンを利用する

htmlとかは適当に書いてます

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <button id="get">GET</button>
    <button id="post">POST</button>
    <hr>
    <script>
        const getBtn = document.getElementById("get");
        getBtn.addEventListener('click',doget);
    
        const postBtn = document.getElementById("post");
        postBtn.addEventListener('click',dopost);
    
        function doget(){
            fetch('http://localhost:8080/get',
            {
                method:'GET',
                credentials: 'include' // これを付ければcookieを受け取れます
            })
            .then(res => res.text())
            .then(str => console.log(str))
        }
    
        function dopost(){
            const csrfToken = document.cookie.replace(/(?:(?:^|.*;\s*)XSRF-TOKEN\s*\=\s*([^;]*).*$)|^.*$/, '$1');
            console.log(csrfToken);
            fetch('http://localhost:8080/post',{
                method:'POST',
                credentials: 'include', // ここでも必要
                headers: {
                    'X-XSRF-TOKEN' : csrfToken // cookieから取得したcsrfトークンを設定する
                },
            })
            .then(res => res.text())
            .then(str => console.log(str))
        }
    
    </script>
</body>
</html>
  • csrfTokenの値は参考サイトの通りにcookieから取得しています
  • postmanでも試したようにcsrfトークンは”X-XSRF-TOKEN”というkey名で送信します
  • credentials: ‘include’がないとcookieを受け取れないし、ヘッダーに付与もできません
    • https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials

GETボタンを押してもCORSエラーになってしまいます

画像

Access to fetch at &#39;http://localhost:8080/get&#39; from origin &#39;http://localhost:3000&#39; has been blocked by CORS policy: The value of the &#39;Access-Control-Allow-Credentials&#39; header in the response is &#39;&#39; which must be &#39;true&#39; when the request&#39;s credentials mode is &#39;include&#39;.

Spring側でもCORSの設定でsetAllowCredentialsが必要になります。

// CORSの設定
@Bean
public CorsConfigurationSource corsConfigurationSource(){
    CorsConfiguration cors = new CorsConfiguration();
    cors.setAllowedOrigins(Arrays.asList("http://localhost:3000"));
    cors.setAllowedMethods(Arrays.asList("GET","POST"));
    cors.setAllowedHeaders(Arrays.asList("*"));
    // cookieなどの情報を許可
    cors.setAllowCredentials(true);
    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**",cors);
    return source;
}

これでOKです。GETとPOSTが問題なく動いてることが確認できます。

画像

  • https://spring.pleiades.io/spring-framework/docs/current/javadoc-api/org/springframework/web/cors/CorsConfiguration.html#setAllowCredentials-java.lang.Boolean-

cookieの値が取得できて、無事に疎通もできていることが確認できました。これでRestAPIでもcorsを無効化せずに利用できます。

参考

https://www.baeldung.com/csrf-stateless-rest-api

https://spring.pleiades.io/spring-security/reference/6.0/servlet/integrations/cors.html