Spring Security WebFlux - тело с аутентификацией - PullRequest
0 голосов
/ 25 апреля 2018

Я хочу реализовать простое приложение Spring Security WebFlux.
Я хочу использовать JSON-сообщение типа

{
   'username': 'admin', 
   'password': 'adminPassword'
} 

в теле (запрос POST к / signin) для входа в мое приложение.

Что я сделал?

Я создал эту конфигурацию

@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.

На самом деле это работает, но за исключением (сеанс устанавливается в куки). Как я могу переделать это без этой ошибки?

Теперь я создал только что-то вроде:

@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 из конфига.

Ответы [ 2 ]

0 голосов
/ 26 апреля 2018

Наконец, я настраиваю безопасность WebFlux, поэтому (обратите внимание на обработку выхода из системы, выход из системы не имеет стандартной готовой конфигурации для 5.0.4.RELEASE, вы все равно должны отключить конфигурацию выхода из системы по умолчанию, потому что спецификация выхода из системы по умолчанию создает новуюSecurityContextRepository по умолчанию и не позволяет вам устанавливать свой репозиторий).

ОБНОВЛЕНИЕ: Конфигурация выхода из системы по умолчанию не работает только в том случае, если вы задаете настраиваемое SpringSecurityContextAttributeName в SecurityContextRepository для веб-сеанса.

@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"));
                    }
                });
    }

}
0 голосов
/ 25 апреля 2018

Вы почти у цели. У меня работал следующий конвертер:

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 будет проверять каждый запрос для вас в фильтре. Вот как я настроил Spring-Security с ServerAuthenticationEntryPoint:

@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();
}

Надеюсь, это поможет.

...