在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/

bannerAds