Spring Security WebFlux - 带有身份验证的主体

Posted

技术标签:

【中文标题】Spring Security WebFlux - 带有身份验证的主体【英文标题】:Spring Security WebFlux - body with Authentication 【发布时间】:2018-10-05 13:02:14 【问题描述】:

我想实现简单的 Spring Security WebFlux 应用程序。 我想使用像

这样的 JSON 消息

   'username': 'admin', 
   'password': 'adminPassword'
 

在正文(对 /signin 的 POST 请求)中登录我的应用程序。

我做了什么?

我创建了这个配置

@Configuration
@EnableWebFluxSecurity
@EnableReactiveMethodSecurity(proxyTargetClass = true)
public class WebFluxSecurityConfig 

    @Autowired
    private ReactiveUserDetailsService userDetailsService;

    @Autowired
    private ObjectMapper mapper;

    @Bean
    public PasswordEncoder passwordEncoder() 
        return new BCryptPasswordEncoder(11);
    

    @Bean
    public ServerSecurityContextRepository securityContextRepository() 
        WebSessionServerSecurityContextRepository securityContextRepository =
                new WebSessionServerSecurityContextRepository();

        securityContextRepository.setSpringSecurityContextAttrName("securityContext");

        return securityContextRepository;
    

    @Bean
    public ReactiveAuthenticationManager authenticationManager() 
        UserDetailsRepositoryReactiveAuthenticationManager authenticationManager =
                new UserDetailsRepositoryReactiveAuthenticationManager(userDetailsService);

        authenticationManager.setPasswordEncoder(passwordEncoder());

        return authenticationManager;
    

    @Bean
    public AuthenticationWebFilter authenticationWebFilter() 
        AuthenticationWebFilter filter = new AuthenticationWebFilter(authenticationManager());

        filter.setSecurityContextRepository(securityContextRepository());
        filter.setAuthenticationConverter(jsonBodyAuthenticationConverter());
        filter.setRequiresAuthenticationMatcher(
                ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, "/signin")
        );

        return filter;
    



    @Bean
    public Function<ServerWebExchange, Mono<Authentication>> jsonBodyAuthenticationConverter() 
        return exchange -> 
            return exchange.getRequest().getBody()
                    .cache()
                    .next()
                    .flatMap(body -> 
                        byte[] bodyBytes = new byte[body.capacity()];
                        body.read(bodyBytes);
                        String bodyString = new String(bodyBytes);
                        body.readPosition(0);
                        body.writePosition(0);
                        body.write(bodyBytes);

                        try 
                            UserController.SignInForm signInForm = mapper.readValue(bodyString, UserController.SignInForm.class);

                            return Mono.just(
                                    new UsernamePasswordAuthenticationToken(
                                            signInForm.getUsername(),
                                            signInForm.getPassword()
                                    )
                            );
                         catch (IOException e) 
                            return Mono.error(new LangDopeException("Error while parsing credentials"));
                        
                    );
        ;
    

    @Bean
    public SecurityWebFilterChain securityWebFiltersOrder(ServerHttpSecurity httpSecurity,
                                                          ReactiveAuthenticationManager authenticationManager) 
        return httpSecurity
                .csrf().disable()
                .httpBasic().disable()
                .logout().disable()
                .formLogin().disable()
                .securityContextRepository(securityContextRepository())
                .authenticationManager(authenticationManager)
                .authorizeExchange()
                    .anyExchange().permitAll()
                .and()
                .addFilterAt(authenticationWebFilter(), SecurityWebFiltersOrder.AUTHENTICATION)
                .build();
    


但我使用 jsonBodyAuthenticationConverter() 并读取传入请求的正文。正文只能读取一次,所以我有一个错误

java.lang.IllegalStateException: Only one connection receive subscriber allowed.

实际上它可以工作,但有例外(会话设置在 cookie 中)。如何重制它而不出现此错误?

现在我只创建了类似的东西:

@PostMapping("/signin")
public Mono<Void> signIn(@RequestBody SignInForm signInForm, ServerWebExchange webExchange) 
    return Mono.just(signInForm)
               .flatMap(form -> 
                    UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
                            form.getUsername(),
                            form.getPassword()
                    );

                    return authenticationManager
                            .authenticate(token)
                            .doOnError(err -> 
                                System.out.println(err.getMessage());
                            )
                            .flatMap(authentication -> 
                                SecurityContextImpl securityContext = new SecurityContextImpl(authentication);

                                return securityContextRepository
                                        .save(webExchange, securityContext)
                                        .subscriberContext(ReactiveSecurityContextHolder.withSecurityContext(Mono.just(securityContext)));
                            );
                );
    

并从配置中删除AuthenticationWebFilter

【问题讨论】:

我做了一个实现webflux安全+jwt的功能示例项目,希望对github.com/eriknyk/webflux-jwt-security-demo有帮助 【参考方案1】:

你快到了。以下转换器对我有用:

public class LoginJsonAuthConverter implements Function<ServerWebExchange, Mono<Authentication>> 

    private final ObjectMapper mapper;

    @Override
    public Mono<Authentication> apply(ServerWebExchange exchange) 
        return exchange.getRequest().getBody()
                .next()
                .flatMap(buffer -> 
                    try 
                        SignInRequest request = mapper.readValue(buffer.asInputStream(), SignInRequest.class);
                        return Mono.just(request);
                     catch (IOException e) 
                        log.debug("Can't read login request from JSON");
                        return Mono.error(e);
                    
                )
                .map(request -> new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword()));
    

此外,您不需要登录控制器; spring-security 将在过滤器中为您检查每个请求。以下是我使用 ServerAuthenticationEntryPoint 配置 spring-security 的方法:

@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http,
                                                        ReactiveAuthenticationManager authManager) 
    return http
            .csrf().disable()
            .authorizeExchange()
            .pathMatchers("/api/**").authenticated()
            .pathMatchers("/**", "/login", "/logout").permitAll()
            .and().exceptionHandling().authenticationEntryPoint(restAuthEntryPoint)
            .and().addFilterAt(authenticationWebFilter(authManager), SecurityWebFiltersOrder.AUTHENTICATION)
            .logout()
            .and().build();

希望这会有所帮助。

【讨论】:

【参考方案2】:

最后我配置了 WebFlux 安全性(注意注销处理,注销没有任何标准的 5.0.4.RELEASE 即用型配置,无论如何您必须禁用默认注销配置,因为默认注销规范会创建新的SecurityContextRepository 默认情况下不允许您设置存储库)。

更新: 仅当您在 SecurityContextRepository 中为 Web 会话设置自定义 SpringSecurityContextAttributeName 时,默认注销配置才起作用。

@Configuration
@EnableWebFluxSecurity
@EnableReactiveMethodSecurity(proxyTargetClass = true)
public class WebFluxSecurityConfig 

    @Autowired
    private ReactiveUserDetailsService userDetailsService;

    @Autowired
    private ObjectMapper mapper;

    @Bean
    public PasswordEncoder passwordEncoder() 
        return new BCryptPasswordEncoder(11);
    

    @Bean
    public ServerSecurityContextRepository securityContextRepository() 
        WebSessionServerSecurityContextRepository securityContextRepository =
                new WebSessionServerSecurityContextRepository();

        securityContextRepository.setSpringSecurityContextAttrName("langdope-security-context");

        return securityContextRepository;
    

    @Bean
    public ReactiveAuthenticationManager authenticationManager() 
        UserDetailsRepositoryReactiveAuthenticationManager authenticationManager =
                new UserDetailsRepositoryReactiveAuthenticationManager(userDetailsService);

        authenticationManager.setPasswordEncoder(passwordEncoder());

        return authenticationManager;
    

    @Bean
    public SecurityWebFilterChain securityWebFiltersOrder(ServerHttpSecurity httpSecurity) 
        return httpSecurity
                .csrf().disable()
                .httpBasic().disable()
                .formLogin().disable()
                .logout().disable()
                .securityContextRepository(securityContextRepository())
                .authorizeExchange()
                    .anyExchange().permitAll() // Currently
                .and()
                .addFilterAt(authenticationWebFilter(), SecurityWebFiltersOrder.AUTHENTICATION)
                .addFilterAt(logoutWebFilter(), SecurityWebFiltersOrder.LOGOUT)
                .build();
    

    private AuthenticationWebFilter authenticationWebFilter() 
        AuthenticationWebFilter filter = new AuthenticationWebFilter(authenticationManager());

        filter.setSecurityContextRepository(securityContextRepository());
        filter.setAuthenticationConverter(jsonBodyAuthenticationConverter());
        filter.setAuthenticationSuccessHandler(new RedirectServerAuthenticationSuccessHandler("/home"));
        filter.setAuthenticationFailureHandler(
                new ServerAuthenticationEntryPointFailureHandler(
                        new RedirectServerAuthenticationEntryPoint("/authentication-failure")
                )
        );
        filter.setRequiresAuthenticationMatcher(
                ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, "/signin")
        );

        return filter;
    

    private LogoutWebFilter logoutWebFilter() 
        LogoutWebFilter logoutWebFilter = new LogoutWebFilter();

        SecurityContextServerLogoutHandler logoutHandler = new SecurityContextServerLogoutHandler();
        logoutHandler.setSecurityContextRepository(securityContextRepository());

        RedirectServerLogoutSuccessHandler logoutSuccessHandler = new RedirectServerLogoutSuccessHandler();
        logoutSuccessHandler.setLogoutSuccessUrl(URI.create("/"));

        logoutWebFilter.setLogoutHandler(logoutHandler);
        logoutWebFilter.setLogoutSuccessHandler(logoutSuccessHandler);
        logoutWebFilter.setRequiresLogoutMatcher(
                ServerWebExchangeMatchers.pathMatchers(HttpMethod.GET, "/logout")
        );

        return logoutWebFilter;
    

    private Function<ServerWebExchange, Mono<Authentication>> jsonBodyAuthenticationConverter() 
        return exchange -> exchange
                .getRequest()
                .getBody()
                .next()
                .flatMap(body -> 
                    try 
                        UserController.SignInForm signInForm =
                                mapper.readValue(body.asInputStream(), UserController.SignInForm.class);

                        return Mono.just(
                                new UsernamePasswordAuthenticationToken(
                                        signInForm.getUsername(),
                                        signInForm.getPassword()
                                )
                        );
                     catch (IOException e) 
                        return Mono.error(new LangDopeException("Error while parsing credentials"));
                    
                );
    


【讨论】:

以上是关于Spring Security WebFlux - 带有身份验证的主体的主要内容,如果未能解决你的问题,请参考以下文章

WebFlux Spring Security配置

Spring Webflux Security 中的角色层次结构

将 spring-security 与 spring-webflux 一起使用时禁用 WebSession 创建

在 Spring WebFlux 中使用 Spring Security 实现身份验证的资源是啥

Spring WebFlux + Security - 我们有“记住我”功能吗?

如何在 Spring WebFlux Security(Reactive Spring Security)配置中将多个用户角色添加到单个 pathMatcher/Route?