在Spring Boot中使用Spring Security的CSRF防护措施
我之前玩过一下,但因为有些东西已经忘记了,所以我打算复习并总结一下。
环境
Spring Boot 1.3.3.RELEASE 的中文翻译可以是:春季引导 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.WebSecurityConfigurerAdapter 的配置类,并附加 @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中编写表单,那么…
<!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防护令牌将自动嵌入为“_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检查的方法的表单嵌入令牌的配置。
参考资料:http://docs.spring.io/spring-security/site/docs/current/reference/html/csrf.html。
在使用这个令牌时,将通过org.springframework.security.web.csrf.CsrfFilter进行检查。
如果不使用图像处理的情况下
如果有类似SPA的处理功能,需要对API进行POST/PUT等请求,那么这些请求也将成为CSRF检查的对象。
如果是页面跳转,可以通过隐藏字段传递令牌,但是在其他情况下应该如何更好地处理呢?我搜索到以下条目。
这里使用的是Angular,但是我发现以下内容与特定的框架无关。
-
- Spring Securityはリクエストヘッダ「X-CSRF-TOKEN」がある場合には、その値をCSRFチェックのtokenとして使用するよ。
-
- クライアントサイドへのtokenの受け渡しは、Cookieを使うのがいいんじゃない。
-
- tokenをCookieに設定するのは、Filter作ってそこで実装するよ。
- 作ったFilterは、Spring SecurityのFilter Chainに追加してね。
那么,我将尝试实施这些措施。
创建一个将token设置到Cookie中的过滤器。
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)中,假设令牌已设置,我会获取该令牌并与上次获取的令牌进行比较,如果有差异,则重新设置Cookie。
将创建的过滤器添加到Spring 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方法进行添加。
春季安全是
org.springframework.security.web.FilterChainProxy的VirtualFilterChain
在这之后的位置添加似乎是可以的,但是要确定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的值有效,则检查通过。
样品
我在下面放置了已经制作好的样本源代码。
请参考页面
我已参考了您所提供的内容,非常感谢。
http://docs.spring.io/spring-security/site/docs/current/reference/html/csrf.html
https://spring.io/guides/tutorials/spring-security-and-angular-js/
http://docs.spring.io/spring-security/site/docs/current/reference/html/csrf.html
https://spring.io/guides/tutorials/spring-security-and-angular-js/