ちょっと前にいじってたんですが、諸々忘れかけているので復習を兼ねてまとめてみます。
環境
Spring Boot 1.3.3.RELEASE
設定
依存関係を追加
...中略
<dependencies>
...中略
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>
...中略
設定クラス追加
org.springframework.security.config.annotation.web.WebWebSecurityConfigurerAdapter
を継承した設定クラスを作成し、@EnableWebSecurityを付与しておきます。
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」として自動的に埋め込まれます。
<!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の作成
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に追加する。
設定クラスに記述追加します。
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/