春天的安全。如何注销用户(撤销oauth2令牌)

时间:2014-02-24 12:19:56

标签: java spring spring-security oauth-2.0

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

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

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

SecurityContextHolder.getContext().getAuthentication();

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

如何解决?

11 个答案:

答案 0 :(得分:33)

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

答案 1 :(得分:9)

使用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注销过滤器的端点添加了重定向,因此会话无效,客户端必须再次提供凭据才能访问/ oauth / authorize端点。

答案 2 :(得分:5)

这取决于oauth2'授权类型'你正在使用的。

如果您在客户端应用中使用了弹簧@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网络令牌,则无法使其失效,因为该令牌不受授权服务器管理。

答案 3 :(得分:2)

由您的令牌存储实施决定。

如果您使用 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);
        }
}

答案 4 :(得分:0)

<http></http>代码中添加以下行。

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

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

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

编辑: 您想要从java代码中使会话无效。我假设您必须在将用户注销之前完成一些任务,然后使会话无效。如果是用例,则应使用custom注销处理程序。访问this网站了解详情。

答案 5 :(得分:0)

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

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

答案 6 :(得分:0)

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

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

我这样做是因为如果您尝试再次使相同的访问令牌无效,则会抛出Null Pointer异常。

答案 7 :(得分:0)

在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

答案 8 :(得分:0)

用于具有Spring Boot Rest安全性和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);

    }
}

答案 9 :(得分:0)

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

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

答案 10 :(得分:-2)

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

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