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"; } }
问题分析与解决方案
登出请求被拦截
现有Security配置中,/logout未加入放行路径列表,导致登出请求被身份校验拦截,无法触发完整登出流程。需在permitAll()的路径中添加/logout,同时调整授权规则顺序(先放行特定路径,再拦截其余请求)。会话清理不彻底
目前仅配置了删除Cookie和清除认证信息,需新增invalidateHttpSession(true)显式销毁HttpSession,确保本地会话完全失效。请求头拼接错误
products方法中Authorization头存在拼接错误:"Bearer"+"jwtTokenValue"会将字符串字面量作为token,而非变量值,且Bearer后缺少空格,导致调用产品服务时认证失败出现401错误,需修正为"Bearer " + jwtTokenValue。登出确认窗口问题
手动访问/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
相关产品推荐
相关产品推荐

