【问题标题】:Spring security. How to log out user (revoke oauth2 token)春季安全。如何注销用户(撤销 oauth2 令牌)
【发布时间】:2014-03-26 02:31:52
【问题描述】:

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

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

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

SecurityContextHolder.getContext().getAuthentication();

我在那里看到了我的老用户。

如何解决?

【问题讨论】:

    标签: java spring spring-security oauth-2.0


    【解决方案1】:

    这是我的实现(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
    

    【讨论】:

    • 如果您不想删除访问令牌(请记住,您可以随时更改查询!)您可能需要使用 DefaultServices(如果您正在使用它)
    • 可以直接使用ConsumerTokenServices的revoke函数
    • 我正在尝试在 github 示例应用程序中实施您的建议。你愿意看看吗?这是链接:stackoverflow.com/questions/36683434/…
    • @Ming,我在注销时使用ConsumerTokenServices 撤销访问令牌,但下一次登录尝试直接将我带到 OAuth 批准页面而不是完全身份验证。是否也需要从浏览器中删除令牌?
    • 可以直接用@RequestHeader(value="Authorization") String authHeader代替HttpServletRequest作为参数
    【解决方案2】:

    camposer 的响应可以使用 Spring OAuth 提供的 API 进行改进。实际上,不需要直接访问 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 注销过滤器的端点,因此会话无效,客户端必须再次提供凭据才能访问 /oauth/authorize 端点。

    【讨论】:

    • 我尝试了同样的方法,它返回 200 响应,但我仍然能够使用相同的令牌并访问数据。
    【解决方案3】:

    这取决于您使用的 oauth2 'grant type' 的类型。

    如果您在客户端应用程序中使用了 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<String, String> logout(@RequestParam(name = "access_token") String accessToken) {
            LOGGER.debug("Invalidating token {}", accessToken);
            consumerTokenServices.revokeToken(accessToken);
            Map<String, String> 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<String, String> params = new LinkedMultiValueMap<>();
                params.add("access_token", accessToken);
    
                HttpHeaders headers = new HttpHeaders();
                headers.add("Authorization", "bearer " + accessToken);
    
                HttpEntity<String> request = new HttpEntity(params, headers);
    
                HttpMessageConverter formHttpMessageConverter = new FormHttpMessageConverter();
                HttpMessageConverter stringHttpMessageConverternew = new StringHttpMessageConverter();
                restTemplate.setMessageConverters(Arrays.asList(new HttpMessageConverter[]{formHttpMessageConverter, stringHttpMessageConverternew}));
                try {
                    ResponseEntity<String> 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 令牌,则不能使其无效,因为该令牌不由授权服务器管理。

    【讨论】:

    • 在 JWT 的情况下,是的,你是对的。您是否知道一种配置身份验证服务器根本不创建会话的方法(它在“oauth dance”期间使用会话来存储 oauth 客户端凭据)并改用请求参数。
    【解决方案4】:

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

    如果您使用 JDBC 标记笔划,那么您只需将其从表中删除... 无论如何,您必须手动添加 /logout 端点,然后调用它:

    @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);
            }
    }
    

    【讨论】:

    • 我正在使用 JDBC 令牌存储,您能否指导我如何获取最后一行“tokenStore.removeAccessToken(oAuth2AccessToken);”的 tokenStore ?
    • @ChiragShah 我记得我无法通过这种方法达到我的目标,请跟踪当前错误以查看正确的实现:(无论您的令牌存储类型是什么)github.com/spring-guides/tut-spring-security-and-angular-js/…
    【解决方案5】:

    您可以通过编程方式退出:

    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);
    }
    

    【讨论】:

      【解决方案6】:

      &lt;http&gt;&lt;/http&gt; 标签中添加以下行。

      <logout invalidate-session="true" logout-url="/logout" delete-cookies="JSESSIONID" />
      

      这将删除 JSESSIONID 并使会话无效。注销按钮或标签的链接类似于:

      <a href="${pageContext.request.contextPath}/logout">Logout</a>
      

      编辑: 您想使 java 代码中的会话无效。我假设您必须在注销用户之前立即执行一些任务,然后使会话无效。如果这是用例,您应该使用自定义注销处理程序。访问this网站了解更多信息。

      【讨论】:

      • 你为什么用 Java 代码来做呢?有什么具体的用例吗?
      • 是的。具体用例。
      • 查看编辑。我想您必须在注销过程之前做一些事情。您可以编写注销处理程序来执行此类任务。
      【解决方案7】:

      这适用于 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<String, String> postParams = new LinkedMultiValueMap<String, String>();
          postParams.set( OAuth2Constants.REFRESH_TOKEN, accessToken.getRefreshToken().getValue() );
      
          HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<MultiValueMap<String, String>>(postParams, requestHeaders);
          RestTemplate restTemplate = new RestTemplate();
          try {
              ResponseEntity<String> 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 );
      }
      

      【讨论】:

      • 仅供参考:这是在 Keylcoak 2.4.0 FINAL 上测试的。
      【解决方案8】:

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

      @Controller
      public class RevokeTokenController {
      
          @Autowired
          private TokenStore tokenStore;
      
          @RequestMapping(value = "/revoke-token", method = RequestMethod.GET)
          public @ResponseBody ResponseEntity<HttpStatus> 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>(HttpStatus.NOT_FOUND);
                  }           
              }
      
              return new ResponseEntity<HttpStatus>(HttpStatus.OK);
          }
      }
      

      我这样做是因为如果您再次尝试使相同的访问令牌无效,则会引发空指针异常。

      【讨论】:

      • 我尝试了同样的方法,它返回 200 响应,但我仍然能够使用相同的令牌并访问数据。
      【解决方案9】:

      在 AuthServer

      @Override
      public void configure(AuthorizationServerEndpointsConfigurer endpoints)
          throws Exception {
        ...
        endpoints.addInterceptor(new HandlerInterceptorAdapter() {
          @Override
          public void postHandle(HttpServletRequest request,
              HttpServletResponse response, Object handler,
              ModelAndView modelAndView) throws Exception {
            if (modelAndView != null
                && modelAndView.getView() instanceof RedirectView) {
              RedirectView redirect = (RedirectView) modelAndView.getView();
              String url = redirect.getUrl();
              if (url.contains("code=") || url.contains("error=")) {
                HttpSession session = request.getSession(false);
                if (session != null) {
                  session.invalidate();
                }
              }
            }
          }
        });
      }
      

      在客户现场

      .and()
      .logout().logoutSuccessUrl("/").permitAll()
      .and().csrf()
      .ignoringAntMatchers("/login", "/logout")
      .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
      

      对我来说似乎是一个更好的解决方案。转介这个link

      【讨论】:

        【解决方案10】:

        用于带有 Spring Boot Rest Security 和 oauth2.0 的注销令牌 用户如下

        import org.springframework.security.oauth2.provider.token.ConsumerTokenServices;
        
        @RestController
        @RequestMapping("/v1/user/")
        public class UserController {
            @Autowired
            private ConsumerTokenServices consumerTokenServices;
        
            /**
             * Logout. This method is responsible for logout user from application based on
             * given accessToken.
             * 
             * @param accessToken the access token
             * @return the response entity
             */
            @GetMapping(value = "/oauth/logout")
            public ResponseEntity<Response> logout(@RequestParam(name = "access_token") String accessToken) {
                consumerTokenServices.revokeToken(accessToken);
                return new ResponseEntity<>(new Response(messageSource.getMessage("server.message.oauth.logout.successMessage",  null, LocaleContextHolder.getLocale())), HttpStatus.OK);
        
            }
        }
        

        【讨论】:

          【解决方案11】:

          您可以从数据库中删除访问令牌和刷新令牌以节省空间。

              @PostMapping("/oauth/logout")
          public ResponseEntity<String> revoke(HttpServletRequest request) {
              try {
                  String authorization = request.getHeader("Authorization");
                  if (authorization != null && authorization.contains("Bearer")) {
                      String tokenValue = authorization.replace("Bearer", "").trim();
          
                      OAuth2AccessToken accessToken = tokenStore.readAccessToken(tokenValue);
                      tokenStore.removeAccessToken(accessToken);
          
                      //OAuth2RefreshToken refreshToken = tokenStore.readRefreshToken(tokenValue);
                      OAuth2RefreshToken refreshToken = accessToken.getRefreshToken();
                      tokenStore.removeRefreshToken(refreshToken);
                  }
              } catch (Exception e) {
                  return ResponseEntity.badRequest().body("Invalid access token");
              }
          
              return ResponseEntity.ok().body("Access token invalidated successfully");
          }
          

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2015-10-02
            • 1970-01-01
            • 2011-11-11
            • 2016-06-09
            • 1970-01-01
            • 2018-09-21
            • 1970-01-01
            • 2021-02-09
            相关资源
            最近更新 更多