spring安全。 如何注销用户(撤销oauth2令牌)

当我想要注销时,我调用此代码:

request.getSession().invalidate(); SecurityContextHolder.getContext().setAuthentication(null); 

但在它之后(在使用旧的oauth令牌的下一个请求中)我调用

SecurityContextHolder.getContext().getAuthentication();

我在那里看到我的老用户

怎么解决?

这是我的实现(Spring OAuth2):

 @Controller public class OAuthController { @Autowired private TokenStore tokenStore; @RequestMapping(value = "/oauth/revoke-token", method = RequestMethod.GET) @ResponseStatus(HttpStatus.OK) public void logout(HttpServletRequest request) { String authHeader = request.getHeader("Authorization"); if (authHeader != null) { String tokenValue = authHeader.replace("Bearer", "").trim(); OAuth2AccessToken accessToken = tokenStore.readAccessToken(tokenValue); tokenStore.removeAccessToken(accessToken); } } } 

用于检测:

 curl -X GET -H "Authorization: Bearer $TOKEN" http://localhost:8080/backend/oauth/revoke-token 

使用Spring OAuth提供的API可以改进camposer的响应。 实际上,没有必要直接访问HTTP头,但删除访问令牌的REST方法可以实现如下:

 @Autowired private AuthorizationServerTokenServices authorizationServerTokenServices; @Autowired private ConsumerTokenServices consumerTokenServices; @RequestMapping("/uaa/logout") public void logout(Principal principal, HttpServletRequest request, HttpServletResponse response) throws IOException { OAuth2Authentication oAuth2Authentication = (OAuth2Authentication) principal; OAuth2AccessToken accessToken = authorizationServerTokenServices.getAccessToken(oAuth2Authentication); consumerTokenServices.revokeToken(accessToken.getValue()); String redirectUrl = getLocalContextPathUrl(request)+"/logout?myRedirect="+getRefererUrl(request); log.debug("Redirect URL: {}",redirectUrl); response.sendRedirect(redirectUrl); return; } 

我还向Spring Security注销filter的端点添加了重定向,因此会话无效,客户端必须再次提供凭据才能访问/ oauth / authorize端点。

它取决于您的令牌存储实施。

如果您使用JDBC令牌笔划,那么您只需要将其从表中删除…无论如何,您必须手动添加/注销端点然后调用:

 @RequestMapping(value = "/logmeout", method = RequestMethod.GET) @ResponseBody public void logmeout(HttpServletRequest request) { String token = request.getHeader("bearer "); if (token != null && token.startsWith("authorization")) { OAuth2AccessToken oAuth2AccessToken = okenStore.readAccessToken(token.split(" ")[1]); if (oAuth2AccessToken != null) { tokenStore.removeAccessToken(oAuth2AccessToken); } } 

它取决于您正在使用的oauth2’授权类型’的类型。

如果您在客户端应用程序中使用了spring的@EnableOAuth2Sso ,则最常见的是“授权代码”。 在这种情况下,Spring安全性会将登录请求重定向到“授权服务器”,并使用从“授权服务器”接收的数据在客户端应用程序中创建会话。

您可以在客户端应用程序调用/logout端点轻松销毁会话,但客户端应用程序再次将用户发送到“授权服务器”并再次返回记录。

我建议创建一种机制来拦截客户端应用程序中的注销请求,并从此服务器代码中调用“授权服务器”以使令牌无效。

我们需要的第一个更改是使用Claudio Tasso提出的代码在授权服务器上创建一个端点,以使用户的access_token无效。

 @Controller @Slf4j public class InvalidateTokenController { @Autowired private ConsumerTokenServices consumerTokenServices; @RequestMapping(value="/invalidateToken", method= RequestMethod.POST) @ResponseBody public Map logout(@RequestParam(name = "access_token") String accessToken) { LOGGER.debug("Invalidating token {}", accessToken); consumerTokenServices.revokeToken(accessToken); Map ret = new HashMap<>(); ret.put("access_token", accessToken); return ret; } } 

然后在客户端应用程序中,创建一个LogoutHandler

 @Slf4j @Component @Qualifier("mySsoLogoutHandler") public class MySsoLogoutHandler implements LogoutHandler { @Value("${my.oauth.server.schema}://${my.oauth.server.host}:${my.oauth.server.port}/oauth2AuthorizationServer/invalidateToken") String logoutUrl; @Override public void logout(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) { LOGGER.debug("executing MySsoLogoutHandler.logout"); Object details = authentication.getDetails(); if (details.getClass().isAssignableFrom(OAuth2AuthenticationDetails.class)) { String accessToken = ((OAuth2AuthenticationDetails)details).getTokenValue(); LOGGER.debug("token: {}",accessToken); RestTemplate restTemplate = new RestTemplate(); MultiValueMap params = new LinkedMultiValueMap<>(); params.add("access_token", accessToken); HttpHeaders headers = new HttpHeaders(); headers.add("Authorization", "bearer " + accessToken); HttpEntity request = new HttpEntity(params, headers); HttpMessageConverter formHttpMessageConverter = new FormHttpMessageConverter(); HttpMessageConverter stringHttpMessageConverternew = new StringHttpMessageConverter(); restTemplate.setMessageConverters(Arrays.asList(new HttpMessageConverter[]{formHttpMessageConverter, stringHttpMessageConverternew})); try { ResponseEntity response = restTemplate.exchange(logoutUrl, HttpMethod.POST, request, String.class); } catch(HttpClientErrorException e) { LOGGER.error("HttpClientErrorException invalidating token with SSO authorization server. response.status code: {}, server URL: {}", e.getStatusCode(), logoutUrl); } } } } 

并在WebSecurityConfigurerAdapter注册:

 @Autowired MySsoLogoutHandler mySsoLogoutHandler; @Override public void configure(HttpSecurity http) throws Exception { // @formatter:off http .logout() .logoutSuccessUrl("/") // using this antmatcher allows /logout from GET without csrf as indicated in // https://docs.spring.io/spring-security/site/docs/current/reference/html/csrf.html#csrf-logout .logoutRequestMatcher(new AntPathRequestMatcher("/logout")) // this LogoutHandler invalidate user token from SSO .addLogoutHandler(mySsoLogoutHandler) .and() ... // @formatter:on } 

注意:如果您使用的是JWT Web令牌,则无法使其失效,因为令牌不受授权服务器管理。

标记中添加以下行。

  

这将删除JSESSIONID并使会话无效。 并且链接到注销按钮或标签将是这样的:

 Logout 

编辑:您想要从java代码中使会话无效。 我假设您必须在将用户注销之前完成一些任务,然后使会话无效。 如果这是用例,则应使用custom注销处理程序。 访问此站点以获取更多信息。

这适用于Keycloak机密客户端注销。 我不知道为什么在keycloak上的人们在java非Web客户端及其端点上没有更强大的文档,我想这就是具有开源库的野兽的本质。 我不得不花费一些时间在他们的代码中:

  //requires a Keycloak Client to be setup with Access Type of Confidential, then using the client secret public void executeLogout(String url){ HttpHeaders requestHeaders = new HttpHeaders(); //not required but recommended for all components as this will help w/t'shooting and logging requestHeaders.set( "User-Agent", "Keycloak Thick Client Test App Using Spring Security OAuth2 Framework"); //not required by undertow, but might be for tomcat, always set this header! requestHeaders.set( "Accept", "application/x-www-form-urlencoded" ); //the keycloak logout endpoint uses standard OAuth2 Basic Authentication that inclues the //Base64-encoded keycloak Client ID and keycloak Client Secret as the value for the Authorization header createBasicAuthHeaders(requestHeaders); //we need the keycloak refresh token in the body of the request, it can be had from the access token we got when we logged in: MultiValueMap postParams = new LinkedMultiValueMap(); postParams.set( OAuth2Constants.REFRESH_TOKEN, accessToken.getRefreshToken().getValue() ); HttpEntity> requestEntity = new HttpEntity>(postParams, requestHeaders); RestTemplate restTemplate = new RestTemplate(); try { ResponseEntity response = restTemplate.exchange(url, HttpMethod.POST, requestEntity, String.class); System.out.println(response.toString()); } catch (HttpClientErrorException e) { System.out.println("We should get a 204 No Content - did we?\n" + e.getMessage()); } } //has a hard-coded client ID and secret, adjust accordingly void createBasicAuthHeaders(HttpHeaders requestHeaders){ String auth = keycloakClientId + ":" + keycloakClientSecret; byte[] encodedAuth = Base64.encodeBase64( auth.getBytes(Charset.forName("US-ASCII")) ); String authHeaderValue = "Basic " + new String( encodedAuth ); requestHeaders.set( "Authorization", authHeaderValue ); } 

用户composer php提供的解决方案非常适合我。 我对代码做了一些小改动,如下所示,

 @Controller public class RevokeTokenController { @Autowired private TokenStore tokenStore; @RequestMapping(value = "/revoke-token", method = RequestMethod.GET) public @ResponseBody ResponseEntity logout(HttpServletRequest request) { String authHeader = request.getHeader("Authorization"); if (authHeader != null) { try { String tokenValue = authHeader.replace("Bearer", "").trim(); OAuth2AccessToken accessToken = tokenStore.readAccessToken(tokenValue); tokenStore.removeAccessToken(accessToken); } catch (Exception e) { return new ResponseEntity(HttpStatus.NOT_FOUND); } } return new ResponseEntity(HttpStatus.OK); } } 

我这样做是因为如果您尝试再次使相同的访问令牌无效,它会抛出空指针exception。

以编程方式,您可以这样注销:

 public void logout(HttpServletRequest request, HttpServletResponse response) { Authentication auth = SecurityContextHolder.getContext().getAuthentication(); if (auth != null){ new SecurityContextLogoutHandler().logout(request, response, auth); } SecurityContextHolder.getContext().setAuthentication(null); }