四、用JSON作前后端分离的交互
在Spring Reactive Security中,Security过滤器是通过类ServerHttpSecurity
配置的,用户认证过滤器是AuthenticationWebFilter
,相当于SpringSecurity中的UsernamePasswordAuthenticationFilter
。
在AuthenticationWebFilter
中,用户名和密码的解析是通过 ServerAuthenticationConverter
实现的。但是在ServerHttpSecurity的内部类表单配置类FormLoginSpec
没有ServerAuthenticationConverter的配置属性:
private final RedirectServerAuthenticationSuccessHandler defaultSuccessHandler = new RedirectServerAuthenticationSuccessHandler(
"/");
private RedirectServerAuthenticationEntryPoint defaultEntryPoint;
private ReactiveAuthenticationManager authenticationManager;
private ServerSecurityContextRepository securityContextRepository;
private ServerAuthenticationEntryPoint authenticationEntryPoint;
private boolean isEntryPointExplicit;
private ServerWebExchangeMatcher requiresAuthenticationMatcher;
private ServerAuthenticationFailureHandler authenticationFailureHandler;
private ServerAuthenticationSuccessHandler authenticationSuccessHandler = this.defaultSuccessHandler;
private FormLoginSpec() {
}
同时在ServerHttpSecurity
没有替换过滤器的方法,只有添加过滤器的方法。所以要想解析JSON参数,必须要自定义AuthenticationWebFilter。
修改配置类MyReactiveSecurityConfig:
@Configuration
@Slf4j
public class MyReactiveSecurityConfig {
@Autowired
private ReactiveUserDetailsService reactiveUserDetailsService;
@Autowired(required = false)
private ReactiveUserDetailsPasswordService userDetailsPasswordService;
private final static String LOGIN_PAGE="/doLogin";
// @Bean
// public ReactiveUserDetailsService reactiveUserDetailsService() {
// UserDetails user = User.withUsername("user")
// .password("12345")
// .roles("USER")
// .build();
// return new MapReactiveUserDetailsService(user);
// }
// @Bean
// public PasswordEncoder passwordEncoder() {
// return NoOpPasswordEncoder.getInstance();
// }
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http
.authorizeExchange(exchanges -> exchanges
.pathMatchers(LOGIN_PAGE).permitAll()
.anyExchange().authenticated()
)
.httpBasic().disable()
.formLogin().disable()
.csrf().disable();
http.addFilterAt(authenticationManager(), SecurityWebFiltersOrder.FORM_LOGIN);
return http.build();
}
@Bean
public ReactiveAuthenticationManager userDetailsRepositoryReactiveAuthenticationManager() {
UserDetailsRepositoryReactiveAuthenticationManager manager = new UserDetailsRepositoryReactiveAuthenticationManager(this.reactiveUserDetailsService);
manager.setPasswordEncoder(passwordEncoder());
manager.setUserDetailsPasswordService(this.userDetailsPasswordService);
return manager;
}
@Bean
public AuthenticationWebFilter authenticationManager() {
AuthenticationWebFilter authenticationFilter = new AuthenticationWebFilter(userDetailsRepositoryReactiveAuthenticationManager());
authenticationFilter.setRequiresAuthenticationMatcher(ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, LOGIN_PAGE));
authenticationFilter.setAuthenticationFailureHandler((webFilterExchange, exception) -> {
log.info("认证异常",exception);
ServerWebExchange exchange = webFilterExchange.getExchange();
exchange.getResponse().getHeaders().add("Content-type", MediaType.APPLICATION_JSON_UTF8_VALUE);
Flux<DataBuffer> dataBufferFlux = DataBufferUtils.read(new ByteArrayResource("认证失败".getBytes(StandardCharsets.UTF_8)), exchange.getResponse().bufferFactory(), 1024 * 8);
return exchange.getResponse().writeAndFlushWith(t -> {
t.onSubscribe(new Subscription() {
@Override
public void request(long l) {
}
@Override
public void cancel() {
}
});
t.onNext(dataBufferFlux);
t.onComplete();
});
});
authenticationFilter.setAuthenticationConverter(new JsonServerAuthenticationConverter());
authenticationFilter.setAuthenticationSuccessHandler((webFilterExchange, authentication) -> {
log.info("认证成功:{}",authentication);
ServerWebExchange exchange = webFilterExchange.getExchange();
exchange.getResponse().getHeaders().add("Content-type", MediaType.APPLICATION_JSON_UTF8_VALUE);
Flux<DataBuffer> dataBufferFlux = DataBufferUtils.read(new ByteArrayResource("认证成功".getBytes(StandardCharsets.UTF_8)), exchange.getResponse().bufferFactory(), 1024 * 8);
return exchange.getResponse().writeAndFlushWith(t -> {
t.onSubscribe(new Subscription() {
@Override
public void request(long l) {
}
@Override
public void cancel() {
}
});
t.onNext(dataBufferFlux);
t.onComplete();
});
});
authenticationFilter.setSecurityContextRepository(new WebSessionServerSecurityContextRepository());
return authenticationFilter;
}
}
配置SecurityWebFilterChain时将httpBasic,formLogin,csrf全部禁止了。禁用csrf是为了演示方便。禁用httpBasic,formLogin这两个是为了只能用JSON格式参数才能登陆。同时要自定义AuthenticationWebFilter。增加的ReactiveAuthenticationManager
是为了配置AuthenticationWebFilter的authenticationManagerResolver属性。同时重定义JsonServerAuthenticationConverter
解析JSON参数。setAuthenticationFailureHandler
处理认证失败的场景。setAuthenticationSuccessHandler
处理认证成功的场景。都是返回JSON数据。
@Slf4j
public class JsonServerAuthenticationConverter extends ServerFormLoginAuthenticationConverter {
private ObjectMapper objectMapper = new ObjectMapper();
private String usernameParameter = "username";
private String passwordParameter = "password";
public JsonServerAuthenticationConverter() {
}
public JsonServerAuthenticationConverter(String usernameParameter,String passwordParameter) {
this.usernameParameter = usernameParameter;
this.passwordParameter = passwordParameter;
}
public Mono<Authentication> apply(ServerWebExchange exchange) {
Flux<DataBuffer> body = exchange.getRequest().getBody();
return body.map(t -> {
String s = t.toString(StandardCharsets.UTF_8);
log.info("参数:{}",s);
Map<String , Object> map = null;
try {
map = objectMapper.readValue(s, Map.class);
} catch (JsonProcessingException e) {
log.info("解析JSON参数异常",e);
map = null;
}
if (map != null && !map.isEmpty()) {
String username = (String) map.get(usernameParameter);
String password = (String) map.get(passwordParameter);
Authentication unauthenticated = UsernamePasswordAuthenticationToken.unauthenticated(username, password);
return unauthenticated;
}
return null;
}).next();
}
}
从请求中获取JSON数据,解析成Map,根据用户名参数和密码参数构造Authentication。最后的next
是为了将Flux转成Mono。因为返回值是Mono类型。ServerFormLoginAuthenticationConverter是org.springframework.security.web.server.ServerFormLoginAuthenticationConverter
。本来要实现接口org.springframework.security.web.server.authentication.ServerAuthenticationConverter
,偷下懒,就继承了org.springframework.security.web.server.ServerFormLoginAuthenticationConverter
。
Authentication unauthenticated = UsernamePasswordAuthenticationToken.unauthenticated(username, password);
这个是必须的。不然在我的IDEA上一直提示类型不同。
用Postman演示:
在访问其他接口:
在演示认证失败:
还可以试下用浏览器是否可以登录。