未使用Webflux生成的CSRF令牌

时间:2018-11-04 18:31:35

标签: java spring spring-boot spring-security spring-webflux

我有一个受Spring Security保护的Webflux应用程序,其中默认情况下启用了CSRF保护。但是,我无法将CSRF令牌保存在会话中。

经过一些调查,我发现它可能来自WebSessionServerCsrfTokenRepository.class。在此类中,有一种generateToken方法应根据生成的CSRF令牌创建Mono:

public Mono<CsrfToken> generateToken(ServerWebExchange exchange) {
        return Mono.fromCallable(() -> {
            return this.createCsrfToken();
        });
    }

private CsrfToken createCsrfToken() {
        return new DefaultCsrfToken(this.headerName, this.parameterName, this.createNewToken());
    }

    private String createNewToken() {
        return UUID.randomUUID().toString();
    }

但是,即使generateToken调用了CsrfWebFilter方法,也从未调用过createCsrfToken方法,并且我也从未获得过将CSRF令牌保存在会话中的信息。我的断点从不进入createCsrfToken方法,这可能意味着它从未被订阅。

我正在Netty上使用Spring Boot 2.1.0.RELEASE和Spring Security 5.1.1.RELEASE运行。

我在一个包含以下依赖项的空示例应用程序上重现了该问题:

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-oauth2-client</artifactId>
        </dependency>

我是否缺少某些东西,或者Spring Security是否有问题?

更新

通过进一步的调查,我认为问题来自Spring Security CsrfWebFilter.class中的这种方法:

private Mono<Void> continueFilterChain(ServerWebExchange exchange, WebFilterChain chain) {
        return Mono.defer(() -> {
            Mono<CsrfToken> csrfToken = this.csrfToken(exchange);
            exchange.getAttributes().put(CsrfToken.class.getName(), csrfToken);
            return chain.filter(exchange);
        });
    }

在这里,csrfToken Mono从未订阅。当我以这种方式重写过滤器时,我设法在会话中添加了令牌:

private Mono<Void> continueFilterChain(ServerWebExchange exchange, WebFilterChain chain) {
        return Mono.defer(() -> {
            return this.csrfToken(exchange)
                    .map(csrfToken -> exchange.getAttributes().put(CsrfToken.class.getName(), csrfToken))
                    .then(chain.filter(exchange));
        });
    }

但是,_csrf参数从未添加到我的Thymeleaf模型中,因此以下测试无效:

<form name="test-csrf" action="/test" method="post">
            <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
            <button type="submit">Escape!</button>
        </form>

2 个答案:

答案 0 :(得分:0)

万一有人遇到这个问题,我和Spring团队的人进行了讨论。

实际上,它不打算直接订阅csrfToken Mono,而仅在需要时订阅它。开发人员有责任在应用程序中触发订阅,并且有两种方式可以完成订阅。

方法1:显式订阅。

通过 @ModelAttribute 在某些 @ControllerAdvice 或抽象控制器类中提供订阅:

@ModelAttribute(CsrfRequestDataValueProcessor.DEFAULT_CSRF_ATTR_NAME)
    public Mono<CsrfToken> getCsrfToken(final ServerWebExchange exchange) {

        return exchange.getAttributeOrDefault(CsrfToken.class.getName(), Mono.empty());
    }

方法2:使用Thymeleaf自动处理CSRF。

确保您的POM中具有以下依赖项,以将Thymeleaf与Spring Security结合使用:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

<dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>

这将自动将CSRF令牌添加到模型中,并通过隐藏的输入形式传递它(仍然需要将其添加到POST Ajax请求的标头中)。

有关更多信息,这是我在春季打开的问题:https://github.com/spring-projects/spring-security/issues/6046

答案 1 :(得分:0)

当我检查 attributesexchange 属性时,我在那里没有看到 CsrfToken,所以我无法订阅它。在浏览器会话中检查 cookie 时,我确实看到了 XSRF-TOKEN

我最终在我们的项目中使用它作为过滤器:

@Component
@Slf4j
class CsrfHeaderFilter implements WebFilter {
   
    @Override
    Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
        def xsrfToken = exchange.getRequest().getCookies().getFirst("XSRF-TOKEN").value
        exchange = exchange.mutate().request({
            it.header("X-XSRF-TOKEN", xsrfToken)
        }).build()
        log.debug(xsrfToken)
        chain.filter(exchange)
    }
}

然后修改我的安全配置,将这个过滤器放在 CsrfWebFilter 之前:

@EnableWebFluxSecurity
@EnableReactiveMethodSecurity
class WebSecurityConfiguration {

    @Autowired
    CsrfHeaderFilter csrfHeaderFilter

    @Bean
    SecurityWebFilterChain SecurityWebFilterChain(ServerHttpSecurity http) {
        http
                .addFilterBefore(csrfHeaderFilter, SecurityWebFiltersOrder.CSRF)
                .httpBasic().disable()
                .formLogin().disable()
                .oauth2Login().and()
                .csrf({
                    it.csrfTokenRepository(new CookieServerCsrfTokenRepository())
                })
                .authorizeExchange()
                .pathMatchers("/actuator/health").permitAll()
                .pathMatchers("/**").authenticated()
                .and().build()
    }
}

这对我们有用,因为 CsrfWebFilter 期望找到令牌的地方之一是在请求标头 X-XSRF-TOKEN 中。