84
101

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Spring BootでSpring SecurityのCSRF対策を使う

Last updated at Posted at 2016-06-30

ちょっと前にいじってたんですが、諸々忘れかけているので復習を兼ねてまとめてみます。

環境

Spring Boot 1.3.3.RELEASE

設定

依存関係を追加

pom.xml
...中略
  <dependencies>
...中略
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
  </dependencies>

...中略

設定クラス追加

org.springframework.security.config.annotation.web.WebWebSecurityConfigurerAdapter
を継承した設定クラスを作成し、@EnableWebSecurityを付与しておきます。

Security設定クラス例
package jp.gr.java_conf.nenokido2000.sample;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    /*
     * { @inheritDoc }
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth)
            throws Exception {
        auth.inMemoryAuthentication().withUser("user").password("user")
                .roles("USER");
    }

    /*
     * { @inheritDoc }
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().anyRequest().authenticated();

        http.formLogin().loginPage("/login").usernameParameter("username")
                .passwordParameter("password")
                .loginProcessingUrl("/login/auth").failureUrl("/login/error")
                .permitAll();

        http.logout().logoutUrl("/logout").permitAll()
                .logoutSuccessUrl("/login")
                .deleteCookies("JSESSIONID").invalidateHttpSession(true);

    }

サンプルなので、inMemoryAuthenticationを用いた簡易的な認証と、ログイン/ログアウトの設定、全リクエストにログイン済の認可を適用するだけにしてあります。

##CSRF対策の実装

Thymeleafを用いた画面遷移ベースの処理の場合

前段の設定がされたアプリケーションで、こんな感じでThymeleafにform書いておけば

フォームの例
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
  <body>
    <div id="container">
      <form id="loginForm" method="post" th:action="@{/login/auth}">
        <label for="username">username</label>
        <input id="username" name="username" type="text" />
        <label for="password">password</label>
        <input id="password" name="password" type="password" />
        <button type="submit">login</button>
      </form>
    </div>
  </body>
</html>

CSRF対策用のtokenが「_csrf」として自動的に埋め込まれます。

フォームの例_HTML変換後
<!DOCTYPE HTML>
<html>
  <body>
    <div id="container">
      <form id="loginForm" method="post" action="/login/auth">
        <label for="username">username</label>
        <input id="username" name="username" type="text" />
        <label for="password">password</label>
        <input id="password" name="password" type="password" />
        <button type="submit">login</button>
      <input type="hidden" name="_csrf" value="5e5f0472-c675-43ad-be0a-d673a0325db4" /></form>
    </div>
  </body>
</html>

Thymeleaf + @EnableWebSecurityを用いた設定を行うと、CSRFのチェックが必要なmethodを用いたformに自動的にtokenが埋め込まれる設定になるそうです。

参考:http://docs.spring.io/spring-security/site/docs/current/reference/html/csrf.html

このtokenを用いて、org.springframework.security.web.csrf.CsrfFilterでチェックが行われます。

画面を用いない処理の場合

例えばSPAっぽい作りの処理があって、APIに対してPOST/PUTなどのリクエストを行う、といった場合も、それらのリクエストはCSRFチェックの対象となります。
画面遷移ならhiddenでtoken渡せばいいけど、そうじゃない場合はどうするのがベターなんだろうかと調べると、以下のエントリがありました。

これはAngularを使ってますが、特にFWに限定されたことではない部分として、以下の内容を読み取りました。

  • Spring Securityはリクエストヘッダ「X-CSRF-TOKEN」がある場合には、その値をCSRFチェックのtokenとして使用するよ。
  • クライアントサイドへのtokenの受け渡しは、Cookieを使うのがいいんじゃない。
  • tokenをCookieに設定するのは、Filter作ってそこで実装するよ。
  • 作ったFilterは、Spring SecurityのFilter Chainに追加してね。

では、これらを実装してみます。

tokenをCookieに設定するfilterの作成

Filterの実装サンプル
package jp.gr.java_conf.nenokido2000.sample.filter;

import java.io.IOException;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.WebUtils;

/**
 * <pre>
 * API通信に対してCSRFトークンをCookieで設定するためのFilter
 * </pre>
 * 
 * @author naoki.enokido
 *
 */
public class CsrfCookieFilter extends OncePerRequestFilter {

    /** CookieにCSRFを設定する際の名称 */
    private static final String CSRF_COOKIE_NAME = "_ctkn";

    /** CookieにCSRFを設定する際の有効範囲 */
    private static final String CSRF_COOKIE_PATH = "/";

    /*
     * { @inheritDoc }
     */
    @Override
    protected void doFilterInternal(HttpServletRequest request,
            HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {

        final CsrfToken csrf = (CsrfToken) request.getAttribute(CsrfToken.class
                .getName());

        if (csrf != null) {
            final String token = csrf.getToken();
            Cookie cookie = WebUtils.getCookie(request, CSRF_COOKIE_NAME);
            if (cookie == null || token != null
                    && !token.equals(cookie.getValue())) {
                cookie = new Cookie(CSRF_COOKIE_NAME, token);
                cookie.setPath(CSRF_COOKIE_PATH);
                response.addCookie(cookie);
            }
        }

        filterChain.doFilter(request, response);
    }
}

参考にしたエントリにあったコードを参考に作成しています。
前段の処理(org.springframework.security.web.csrf.CsrfFilter)でtokenが設定されている前提で、それを取得して前回取得していたtokenと差異があれば再度Cookieを設定し直す、という処理になります。

作成したFilterをSpring SecurityのFilter Chainに追加する。

設定クラスに記述追加します。

Security設定クラス例
package jp.gr.java_conf.nenokido2000.sample;

import jp.gr.java_conf.nenokido2000.sample.filter.CsrfCookieFilter;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.csrf.CsrfFilter;

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

...中略

    /*
     * { @inheritDoc }
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {

...中略
        
        http.addFilterAfter(new CsrfCookieFilter(), CsrfFilter.class);
    }
}

org.springframework.security.config.annotation.web.builders.HttpSecurity#addFilterAfter
を呼び出して追加します。

Spring Securityは

org.springframework.security.web.FilterChainProxy$VirtualFilterChain

を用いて独自のFilter Chainを持っており、この中に追加することになります。
追加する位置としてはtokenが設定された後であれば大丈夫そうですが、設定された直後、即ち

org.springframework.security.web.csrf.CsrfFilter

の直後に追加するのが望ましいようです。

クライアントサイド

超雑ですが、React + superagent + cookieでサンプル書いてみました。

クライアントサイド例
import React from 'react';
import request from 'superagent';
import cookie from 'cookie';

export default class App extends React.Component {

    constructor(props) {
        super(props);
    }

    handleGet() {
        request
            .get('/api')
            .end((err, res) => {
                if (res.ok) {
                    alert(res.body.message);
                } else if (res.forbidden) {
                    alert('forbidden');
                } else {
                    alert(`error:${res.status}`);
                }
            });
    }

    handlePut() {
        const cookies = cookie.parse(document.cookie);
        const csrf = cookies._ctkn;
        if (csrf) {
            request
                .put('/api')
                .set('X-CSRF-TOKEN', csrf)
                .end((err, res) => {
                    if (res.ok) {
                        alert(res.body.message);
                    } else if (res.forbidden) {
                        alert('forbidden');
                    } else {
                        alert(`error:${res.status}`);
                    }
                });
        } else {
            alert('エラー:不正な遷移を検知したため処理を続行できません');
        }
    }

    render() {
        return (
            <div>
                <h2>Spring Boot CSRF Sample</h2>
                <button type="button" onClick={this.handleGet.bind(this)} >GET</button>
                <button type="button" onClick={this.handlePut.bind(this)} >PUT</button>
            </div>
        );
    }
}

GETの場合は特に何もしてないですが、PUTの場合にはCookieに付与されたtokenを「X-CSRF-TOKEN」ヘッダとして送信しています。
これにより、サーバ側でCSRFのチェックが行われ、tokenの値が妥当であればチェックOKとなります。

##サンプル
作成したサンプルソース群を以下に置いてあります。

参考ページ

以下を参考にさせていただきました。
ありがとうございました。

http://docs.spring.io/spring-security/site/docs/current/reference/html/csrf.html
https://spring.io/guides/tutorials/spring-security-and-angular-js/

84
101
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
84
101

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?