Spring 引导安全无法禁用 CSRF 保护

Spring Boot security can not disable CSRF protection

我的 Spring Boot REST API 受 Keycloak 实例保护。由于CSRF保护只允许GET和POST,我想禁用它。但是,我的方法似乎不起作用,因为 REST API 将 return HTTP 状态 403 对于任何来源不同于 http://localhost:8080 的请求。以下是我如何配置我的安全性:

package de.longnguyen.security;

import de.longnguyen.controller.KeycloakController;
import org.keycloak.adapters.springboot.KeycloakSpringBootConfigResolver;
import org.keycloak.adapters.springsecurity.KeycloakSecurityComponents;
import org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationProvider;
import org.keycloak.adapters.springsecurity.config.KeycloakWebSecurityConfigurerAdapter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
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.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.authority.mapping.SimpleAuthorityMapper;
import org.springframework.security.core.session.SessionRegistryImpl;
import org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
import org.springframework.security.web.session.SessionManagementFilter;


@Configuration
@EnableWebSecurity
@ComponentScan(basePackageClasses = KeycloakSecurityComponents.class)
public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
    private static final String[] ALLOWED = new String[]{"/", "/static/**", "/v2/api-docs", "/swagger*/**", "/webjars/**"};

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) {
        KeycloakAuthenticationProvider keycloakAuthenticationProvider = keycloakAuthenticationProvider();
        keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper());
        auth.authenticationProvider(keycloakAuthenticationProvider);
    }

    @Bean
    public KeycloakSpringBootConfigResolver KeycloakConfigResolver() {
        return new KeycloakSpringBootConfigResolver();
    }

    @Bean
    @Override
    protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
        return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl());
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        super.configure(web);
        web.ignoring()
                .antMatchers(ALLOWED);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        super.configure(http);
        http
                .cors()
                .and()
                .authorizeRequests()
                .antMatchers(ALLOWED).permitAll()
                .anyRequest()
                .authenticated()
                .and()
                .addFilterBefore(new CustomCorsFilter(), SessionManagementFilter.class)
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .csrf().disable();
    }
}

Origin 为 http://localhost:8080 的请求将有效:

但是,来源为 http://localhost:3000 的完全相同的请求将不起作用:

编辑:

这是我的 CustomCorsFilter 的样子:

package de.longnguyen.security;

import lombok.extern.slf4j.Slf4j;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;

@Slf4j
public class CustomCorsFilter implements Filter {

    public void destroy() {
    }

    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        if (req instanceof HttpServletRequest && res instanceof HttpServletResponse) {
            HttpServletRequest request = (HttpServletRequest) req;
            HttpServletResponse response = (HttpServletResponse) res;
            response.setHeader("Access-Control-Allow-Origin", request.getHeader("Origin"));
            response.setHeader("Vary", "Origin");
            response.setHeader("Access-Control-Max-Age", "3600");
            response.setHeader("Access-Control-Allow-Credentials", "true");
            response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE, PUT");
            response.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, X-CSRF-TOKEN");
        }
        chain.doFilter(req, res);
    }

    public void init(FilterConfig filterConfig) {
    }
}

编辑编辑:

我的日志是这样的:

2020-03-26 23:08:45.758 DEBUG 27582 --- [nio-8080-exec-2] o.s.s.w.u.matcher.AntPathRequestMatcher  : Checking match of request : '/api/v1/keycloak/delete'; against '/'
2020-03-26 23:08:45.758 DEBUG 27582 --- [nio-8080-exec-2] o.s.s.w.u.matcher.AntPathRequestMatcher  : Checking match of request : '/api/v1/keycloak/delete'; against '/static/**'
2020-03-26 23:08:45.758 DEBUG 27582 --- [nio-8080-exec-2] o.s.s.w.u.matcher.AntPathRequestMatcher  : Checking match of request : '/api/v1/keycloak/delete'; against '/v2/api-docs'
2020-03-26 23:08:45.758 DEBUG 27582 --- [nio-8080-exec-2] o.s.s.w.u.matcher.AntPathRequestMatcher  : Checking match of request : '/api/v1/keycloak/delete'; against '/swagger*/**'
2020-03-26 23:08:45.758 DEBUG 27582 --- [nio-8080-exec-2] o.s.s.w.u.matcher.AntPathRequestMatcher  : Checking match of request : '/api/v1/keycloak/delete'; against '/webjars/**'
2020-03-26 23:08:45.758 DEBUG 27582 --- [nio-8080-exec-2] o.s.security.web.FilterChainProxy        : /api/v1/keycloak/delete at position 1 of 17 in additional filter chain; firing Filter: 'WebAsyncManagerIntegrationFilter'
2020-03-26 23:08:45.758 DEBUG 27582 --- [nio-8080-exec-2] o.s.security.web.FilterChainProxy        : /api/v1/keycloak/delete at position 2 of 17 in additional filter chain; firing Filter: 'SecurityContextPersistenceFilter'
2020-03-26 23:08:45.758 DEBUG 27582 --- [nio-8080-exec-2] o.s.security.web.FilterChainProxy        : /api/v1/keycloak/delete at position 3 of 17 in additional filter chain; firing Filter: 'HeaderWriterFilter'
2020-03-26 23:08:45.758 DEBUG 27582 --- [nio-8080-exec-2] o.s.security.web.FilterChainProxy        : /api/v1/keycloak/delete at position 4 of 17 in additional filter chain; firing Filter: 'CorsFilter'
2020-03-26 23:08:45.759 DEBUG 27582 --- [nio-8080-exec-2] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to public org.springframework.http.ResponseEntity<java.lang.String> de.longnguyen.controller.KeycloakController.delete()
2020-03-26 23:08:45.760 DEBUG 27582 --- [nio-8080-exec-2] o.s.web.cors.DefaultCorsProcessor        : Reject: HTTP 'DELETE' is not allowed
2020-03-26 23:08:45.761 DEBUG 27582 --- [nio-8080-exec-2] o.s.s.w.header.writers.HstsHeaderWriter  : Not injecting HSTS header since it did not match the requestMatcher org.springframework.security.web.header.writers.HstsHeaderWriter$SecureRequestMatcher@3fffd0ea
2020-03-26 23:08:45.761 DEBUG 27582 --- [nio-8080-exec-2] s.s.w.c.SecurityContextPersistenceFilter : SecurityContextHolder now cleared, as request processing completed

您可以尝试将您的方法更改为以下内容。

@Override
    public void configure(HttpSecurity http) throws Exception {
        http
            .csrf()
            .disable()
            .headers()
            .frameOptions()
            .disable()
        .and()
...
}

而不是添加 .addFilterBefore(new CustomCorsFilter(), 你可以在你的配置中创建一个 Bean

@Bean
CorsConfigurationSource corsConfigurationSource() {
    CorsConfiguration configuration = new CorsConfiguration();
    configuration.setAllowedOrigins(Collections.singletonList("*"));
    configuration.setAllowedMethods(Arrays.asList("GET", "POST", "OPTIONS", "DELETE", "PUT", "PATCH"));
    configuration.setAllowedHeaders(Arrays.asList("X-Requested-With", "Origin", "Content-Type", "Accept", "Authorization"));
    configuration.setAllowCredentials(true);
    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**", configuration);
    return source;
}

SessionManagementFilter 可能也是如此。

@LongNguyen 这是不正确的。每当您从浏览器发送跨域请求时,浏览器都会向服务器发送预检请求以读取 Access-Conrol-Allow-Origin header。如果服务器未设置响应 header,则浏览器不会发送您的实际请求。人们使用的技巧是让服务器单独运行并在客户端实现 CORS 中间件。就像 angular 应用程序中的 HTTPInterceptor 添加 Access-Control-Allow-Origin= * 响应 header 并欺骗浏览器相信 header 来自服务器。