Edited at

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

More than 3 years have passed since last update.

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


環境

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渡せばいいけど、そうじゃない場合はどうするのがベターなんだろうかと調べると、以下のエントリがありました。

https://spring.io/guides/tutorials/spring-security-and-angular-js/

これは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となります。


サンプル

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

https://github.com/nenokido2000/spring-boot-csrf-sample


参考ページ

以下を参考にさせていただきました。

ありがとうございました。

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

https://spring.io/guides/tutorials/spring-security-and-angular-js/