七、注销
一、使用logout
修改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()
.pathMatchers("/verify_code").permitAll()
.anyExchange().authenticated()
)
.formLogin()
.loginPage(LOGIN_PAGE)
.and()
// .loginPage(LOGIN_PAGE)
// .and()
.csrf().disable()
.logout();
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(new RedirectServerAuthenticationFailureHandler(LOGIN_PAGE + "?error"));
authenticationFilter.setAuthenticationConverter(new KaptchServerAuthenticationConverter());
authenticationFilter.setAuthenticationSuccessHandler(new RedirectServerAuthenticationSuccessHandler("/"));
authenticationFilter.setSecurityContextRepository(new WebSessionServerSecurityContextRepository());
return authenticationFilter;
}
}
增加了注销配置。
用postman模拟登录后。在访问注销接口:
注销成功后,重定向到登录页面。
还可以增加logoutSuccessHandler注销成功后返回json格式数据:
@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http
.authorizeExchange(exchanges -> exchanges
.pathMatchers(LOGIN_PAGE).permitAll()
.pathMatchers("/verify_code").permitAll()
.anyExchange().authenticated()
)
.formLogin()
.loginPage(LOGIN_PAGE)
.and()
// .loginPage(LOGIN_PAGE)
// .and()
.csrf().disable()
.logout()
.logoutSuccessHandler(((webFilterExchange, 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();
});
}));
http.addFilterAt(authenticationManager(), SecurityWebFiltersOrder.FORM_LOGIN);
return http.build();
}
访问注销接口:
二、源码分析
注销操作由LogoutWebFilter过滤器处理。
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
return this.requiresLogout.matches(exchange).filter((result) -> result.isMatch())
.switchIfEmpty(chain.filter(exchange).then(Mono.empty())).map((result) -> exchange)
.flatMap(this::flatMapAuthentication).flatMap((authentication) -> {
WebFilterExchange webFilterExchange = new WebFilterExchange(exchange, chain);
return logout(webFilterExchange, authentication);
});
}
requiresLogout
匹配以POST方法访问的/logout
路径请求。调用flatMapAuthentication从ServerWebExchange获取Authentication。如果获取不到则设置匿名用户anonymousAuthenticationToken
:
private Mono<Authentication> flatMapAuthentication(ServerWebExchange exchange) {
return exchange.getPrincipal().cast(Authentication.class).defaultIfEmpty(this.anonymousAuthenticationToken);
}
private AnonymousAuthenticationToken anonymousAuthenticationToken = new AnonymousAuthenticationToken("key",
"anonymous", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS"));
最后调用logout处理注销操作。
private Mono<Void> logout(WebFilterExchange webFilterExchange, Authentication authentication) {
logger.debug(LogMessage.format("Logging out user '%s' and transferring to logout destination", authentication));
return this.logoutHandler.logout(webFilterExchange, authentication)
.then(this.logoutSuccessHandler.onLogoutSuccess(webFilterExchange, authentication))
.subscriberContext(ReactiveSecurityContextHolder.clearContext());
}
logoutHandler默认是SecurityContextServerLogoutHandler,SecurityContextServerLogoutHandler会将SecurityContext从WebSession中移除。logoutSuccessHandler默认重定向到/login?logout
。
这里有个问题,重新配置登录页面了,但是LogoutWebFilter没有修改注销后重定向的url,还是/login?logout
,为什么注销后还能重定向到自定义的登录页?
因为有ExceptionTranslationWebFilter。重新配置登录页后,/login?logout
没有开放访问权限,重定向到/login?logout
会发生AccessDeniedException异常,同时Authentication被LogoutWebFilter清楚了,所以由ExceptionTranslationWebFilter的authenticationEntryPoint处理,在配置登录页面时,重设置了authenticationEntryPoint的重定向url。最终会重定向到自定义的登录页。
修改:
@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http
.authorizeExchange(exchanges -> exchanges
.pathMatchers(LOGIN_PAGE).permitAll()
.pathMatchers("/verify_code").permitAll()
.pathMatchers("/login").permitAll()
.anyExchange().authenticated()
)
.formLogin()
.loginPage(LOGIN_PAGE)
.and()
// .loginPage(LOGIN_PAGE)
// .and()
.csrf().disable()
.logout()
;
http.addFilterAt(authenticationManager(), SecurityWebFiltersOrder.FORM_LOGIN);
return http.build();
}
重新访问注销,发现出现404。