You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

OAuth2/OIDC应用登出失效求助:无需重认证即可访问资源

登出功能失效排查与解决

问题现象

  • 点击登出按钮后,确认操作仍保持登录状态,无需重新向身份提供商认证即可直接访问受保护资源页面
  • 登出确认窗口未自动弹出;手动访问/logout时会出现登出确认窗口,但操作完成后依旧无需重新登录就能访问资源
  • 浏览器控制台出现Failed to load resource: the server responded with a status of 401 ()错误

相关代码

客户模块Security配置

@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true)
public class SecurityConfig  {
   
    private final ClientRegistrationRepository clientRegistrationRepository;
   
    public  SecurityConfig(ClientRegistrationRepository clientRegistrationRepository){
        this.clientRegistrationRepository=clientRegistrationRepository;
    }

    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.csrf(Customizer.withDefaults())//默认配置
                .authorizeHttpRequests(ar-> ar.anyRequest().authenticated())
                .headers(h->h.frameOptions(fo->fo.disable()))
                .csrf(crsf->crsf.ignoringRequestMatchers("/h2-console/**"))
                .authorizeHttpRequests(ar->ar.requestMatchers("/","/webjars/**","/h2-console/**",  "/oauth2Login/**").permitAll())
                .oauth2Login(al->al.loginPage("/oauth2Login").defaultSuccessUrl("/"))
                .logout((logout)->logout
                        .logoutSuccessHandler(oidcLogoutSuccessHandler())
                        .logoutSuccessUrl("/").permitAll()
                        .clearAuthentication(true)
                        .deleteCookies("JSESSIONID"))
                        .exceptionHandling(eh->eh.accessDeniedPage("/notAuthorized"));
        return http.build();

    }

    private OidcClientInitiatedLogoutSuccessHandler oidcLogoutSuccessHandler() {
        final OidcClientInitiatedLogoutSuccessHandler oidcClientInitiatedLogoutSuccessHandler=
                new OidcClientInitiatedLogoutSuccessHandler(this.clientRegistrationRepository);
        oidcClientInitiatedLogoutSuccessHandler.setPostLogoutRedirectUri("{baseUrl}?logoutsuccess=true");
        return oidcClientInitiatedLogoutSuccessHandler;
    }
    @Bean
    public GrantedAuthoritiesMapper userAuthoritiesMapper() {
        return (authorities -> {
            final Set<GrantedAuthority> mappedAuthorities=new HashSet<>();
            authorities.forEach((authority)->{
                if (authority instanceof OidcUserAuthority oidcAuth){
                    mappedAuthorities.addAll(mappedAuthorities(oidcAuth.getIdToken().getClaims()));
                    System.out.println(oidcAuth.getAttributes());
                } else if (authority instanceof OAuth2UserAuthority oauth2Auth) {
                    mappedAuthorities.addAll(mappedAuthorities(oauth2Auth.getAttributes()));
                }
            });
        return mappedAuthorities;

    });
    }

    private List<SimpleGrantedAuthority> mappedAuthorities(final Map<String, Object> attributes) {
        final Map<String, Object> realmAccess= (Map<String, Object>) attributes.getOrDefault("realm_access", Collections.emptyMap());
        final Collection<String> roles=((Collection<String>)realmAccess.getOrDefault("roles", Collections.emptyList()));
        return roles.stream()
                .map(SimpleGrantedAuthority::new)
                .toList();
    }
}

控制器代码

@Controller   
public class CustomerController {
    /**
     * 访问界面
     */
    private  CustomerRepository customerRepository;
    private ClientRegistrationRepository clientRegistrationRepository;
    public CustomerController(CustomerRepository customerRepository, ClientRegistrationRepository registrationRepository) {
        this.customerRepository = customerRepository;
        this.clientRegistrationRepository = clientRegistrationRepository;
    }


    @GetMapping("/customers")
    @PreAuthorize("hasAuthority('ADMIN')")
    public String customers(Model model){  
        List<Customer> customersList = customerRepository.findAll();
        model.addAttribute("customers", customersList);
        return "customers";//返回模型属性名=CustomerList
    }

    @GetMapping("/products")
    
    public String products(Model model){
        SecurityContext context= SecurityContextHolder.getContext();
        Authentication authentication = context.getAuthentication();
        OAuth2AuthenticationToken oAuth2AuthenticationToken = (OAuth2AuthenticationToken) authentication;
        DefaultOidcUser oidcUser= (DefaultOidcUser) oAuth2AuthenticationToken.getPrincipal();
        String jwtTokenValue=oidcUser.getIdToken().getTokenValue();
        RestClient restClient= RestClient.create("http://localhost:8085");
        List<Product> products=restClient.get()
                .uri("/products")
                .headers(httpHeaders ->httpHeaders.set(HttpHeaders.AUTHORIZATION,"Bearer"+"jwtTokenValue"))
                .retrieve()
                .body(new ParameterizedTypeReference<>() {});
        model.addAttribute("products", products);
        return "products";
    }


    
    @GetMapping("/auth")
    @ResponseBody
    public Authentication auth(Authentication authentication){
        return authentication;
    }
  
    @GetMapping("/")
    public String index(){
        return "index";
    }

    @GetMapping("/notAuthorized")
    public String notAuthorized(){
        return "notAuthorized";
    }

    @GetMapping("/oauth2Login")
    public String oauth2Login(Model model){
        String authorizationRequestBaseUri="oauth2/authorization";
        Map<String, String> oauth2AuthenticationUrls=new HashMap();
        Iterable<ClientRegistration> clientRegistrations=(Iterable<ClientRegistration>)clientRegistrationRepository;
        clientRegistrations.forEach(clientRegistration->{
            oauth2AuthenticationUrls.put(clientRegistration.getClientName(),
                    authorizationRequestBaseUri+"/"+ clientRegistration.getRegistrationId());
        });
        model.addAttribute("urls",oauth2AuthenticationUrls);
        return "oauth2Login";
    }
}

问题分析与解决方案

  1. 登出请求被拦截
    现有Security配置中,/logout未加入放行路径列表,导致登出请求被身份校验拦截,无法触发完整登出流程。需在permitAll()的路径中添加/logout,同时调整授权规则顺序(先放行特定路径,再拦截其余请求)。

  2. 会话清理不彻底
    目前仅配置了删除Cookie和清除认证信息,需新增invalidateHttpSession(true)显式销毁HttpSession,确保本地会话完全失效。

  3. 请求头拼接错误
    products方法中Authorization头存在拼接错误:"Bearer"+"jwtTokenValue"会将字符串字面量作为token,而非变量值,且Bearer后缺少空格,导致调用产品服务时认证失败出现401错误,需修正为"Bearer " + jwtTokenValue。

  4. 登出确认窗口问题
    手动访问/logout出现确认窗口是Spring Security默认行为,可通过配置logoutRequestMatcher(new AntPathRequestMatcher("/logout", "POST")),并将前端登出按钮改为POST请求,避免GET请求的确认弹窗。

修改后的关键代码

SecurityFilterChain配置

public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http.csrf(Customizer.withDefaults())
            .headers(h->h.frameOptions(fo->fo.disable()))
            .csrf(crsf->crsf.ignoringRequestMatchers("/h2-console/**"))
            .authorizeHttpRequests(ar->ar
                    .requestMatchers("/","/webjars/**","/h2-console/**",  "/oauth2Login/**", "/logout").permitAll()
                    .anyRequest().authenticated())
            .oauth2Login(al->al.loginPage("/oauth2Login").defaultSuccessUrl("/"))
            .logout((logout)->logout
                    .logoutSuccessHandler(oidcLogoutSuccessHandler())
                    .logoutSuccessUrl("/").permitAll()
                    .clearAuthentication(true)
                    .invalidateHttpSession(true)
                    .deleteCookies("JSESSIONID"))
            .exceptionHandling(eh->eh.accessDeniedPage("/notAuthorized"));
    return http.build();
}

products方法Authorization头

.headers(httpHeaders ->httpHeaders.set(HttpHeaders.AUTHORIZATION,"Bearer " + jwtTokenValue))

内容的提问来源于stack exchange,提问作者Christophe

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.26 02:07:34