七、注销

一、使用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。

posted @ 2023-06-08 22:16  shigp1  阅读(40)  评论(0编辑  收藏  举报