Spring Security WebFlux - тело с аутентификацией
Я хочу реализовать простое приложение 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
из конфига.
3 ответа
Вы почти там. У меня работал следующий конвертер:
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();
}
Надеюсь это поможет.
Наконец, я настраиваю безопасность 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"));
}
});
}
}
Вы можете просто сохранить контекст безопасности в репозитории и при необходимости поместить его в контекст:
@Autowired
private ServerSecurityContextRepository securityContextRepository;
@PostMapping("/signin")
public Mono<Void> signIn(@RequestBody SignInForm signInForm, ServerWebExchange webExchange) {
// TODO check credentials
SecurityContext securityContext = new SecurityContextImpl(new UsernamePasswordAuthenticationToken(user, null))
return securityContextRepository
.save(webExchange, securityContext)
.contextWrite(ReactiveSecurityContextHolder.withSecurityContext(Mono.just(securityContext)));
}
Строка с contextWrite требуется только в случае использования SecurityContext в текущем запросе. В последующих запросах AuthorizationFilter все равно установит контекст.