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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/21987589/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-13 11:51:56  来源:igfitidea点击:

Spring security. How to log out user (revoke oauth2 token)

javaspringspring-securityoauth-2.0

提问by gstackoverflow

When I want to get logout I invoke this code:

当我想注销时,我调用以下代码:

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

But after it (in next request using old oauth token) I invoke

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

SecurityContextHolder.getContext().getAuthentication();

SecurityContextHolder.getContext().getAuthentication();

and I see my old user there.

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

How to fix it?

如何解决?

回答by Sunil Singh Bora

Programmatically you can log out this way:

以编程方式,您可以通过以下方式注销:

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

回答by Jeevan Patil

Add following line in <http></http>tag.

<http></http>标签中添加以下行。

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

This will delete JSESSIONID and invalidate session. And link to logout button or label would be something like:

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

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

EDIT: You want to invalidate session from java code. I assume you have to do some task right before logging the user out, and then invalidate session. If this is the use case, you should use custome logout handlers. Visit thissite for more information.

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

回答by camposer

Here's my implementation (Spring OAuth2):

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

For testing:

用于检测:

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

回答by Pasha GR

Its up to your Token Store Implementation.

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

If you use JDBCtoken stroke then you just need to remove it from table... Anyway you must add /logout endpoint manually then call this :

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

回答by Claudio Tasso

The response by camposercan be improved using the API provided by Spring OAuth. In fact, it's not necessary to access directly to the HTTP headers, but the REST method which removes the access token can be implemented as follows:

可以使用 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;
}

I also added a redirect to the endpoint of Spring Security logout filter, so the session is invalidated and the client must provide credentials again in order to access to the /oauth/authorize endpoint.

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

回答by rpr

It depends on type of oauth2 'grant type' that you're using.

这取决于您使用的 oauth2“授权类型”的类型。

The most common if your have used spring's @EnableOAuth2Ssoin your client app is 'Authorization Code'. In this case Spring security redirects login request to the 'Authorization Server' and creates a session in your client app with the data received from 'Authorization Server'.

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

You can easy destroy your session at the client app calling /logoutendpoint, but then client app sends user again to 'authorization server' and returns logged again.

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

I propose to create a mechanism to intercept logout request at client app and from this server code, call "authorization server" to invalidate the token.

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

The first change that we need is create one endpoint at the authorization server, using the code proposed by Claudio Tasso, to invalidate the user's access_token.

我们需要的第一个更改是在授权服务器上创建一个端点,使用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;
    }
}

Then at the client app, create a LogoutHandler:

然后在客户端应用程序中,创建一个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);
            }
        }


    }
}

And register it at WebSecurityConfigurerAdapter:

并在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
}

One note: If you're using JWT web tokens, you can't invalidate it, because the token is not managed by the authorization server.

一个注意事项:如果您使用的是 JWT Web 令牌,则不能使其无效,因为该令牌不受授权服务器管理。

回答by heregear

This works for Keycloak Confidential Client logout. I have no idea why the folks over at keycloak don't have more robust docs on java non-web clients and their endpoints in general, I guess that's the nature of the beast with open source libs. I had to spend a bit of time in their code:

这适用于 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 );
}

回答by Nikhil

Solution provided by user composerperfectly worked for me. I made some minor changes to the code as follows,

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

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

I did this because If you try to invalidate same access token again, it throws Null Pointer exception.

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

回答by Ram

At AuthServer

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

At client site

在客户现场

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

Seems a better solutions for me. referred this link

对我来说似乎是更好的解决方案。提到这个链接

回答by dheeraj kumar

for logout token with spring boot rest security and oauth2.0 user as follow

对于带有 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);

    }
}