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 - 带有身份验证的主体的主要内容,如果未能解决你的问题,请参考以下文章
Spring Webflux Security 中的角色层次结构
将 spring-security 与 spring-webflux 一起使用时禁用 WebSession 创建
在 Spring WebFlux 中使用 Spring Security 实现身份验证的资源是啥
Spring WebFlux + Security - 我们有“记住我”功能吗?
如何在 Spring WebFlux Security(Reactive Spring Security)配置中将多个用户角色添加到单个 pathMatcher/Route?