Logout in an OAuth Secured Application – 在OAuth安全的应用程序中注销

最后修改: 2017年 4月 24日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

In this quick tutorial, we’re going to show how we can add logout functionality to an OAuth Spring Security application.

在这个快速教程中,我们将展示如何在OAuth Spring Security应用程序中添加注销功能

We’ll see a couple of ways to do this. First, we’ll see how to logout our Keycloak user from the OAuth application as described in Creating a REST API with OAuth2, and then, using the Zuul proxy we saw earlier.

我们将看到有几种方法可以做到这一点。首先,我们将看到如何从OAuth应用程序中注销我们的Keycloak用户,如使用OAuth2创建REST API中所述,然后,使用我们之前看到的Zuul代理

We’ll use the OAuth stack in Spring Security 5. If you want to use the Spring Security OAuth legacy stack, have a look at this previous article: Logout in an OAuth Secured Application (using the legacy stack).

我们将在Spring Security 5中使用OAuth栈。如果你想使用Spring Security的OAuth传统栈,请看之前的这篇文章。在OAuth安全的应用程序中注销(使用传统栈)

2. Logout Using Front-End Application

2.使用前端应用程序注销

As the Access Tokens are managed by the Authorization Server, they will need to be invalidated at this level. The exact steps to do this will be slightly different depending on the Authorization Server you’re using.

由于访问令牌是由授权服务器管理的,它们将需要在这一层次上失效。这样做的具体步骤将根据你所使用的授权服务器而略有不同。

In our example, as per the Keycloak documentation, for logging out directly from a browser application, we can redirect the browser to http://auth-server/auth/realms/{realm-name}/protocol/openid-connect/logout?redirect_uri=encodedRedirectUri.

在我们的例子中,根据Keycloak 文档,对于直接从浏览器应用程序注销,我们可以将浏览器重定向到http://auth-server/auth/realms/{realm-name}/protocol/openid-connect/logout? redirect_uri=encodedRedirectUri

Along with sending the redirect URI, we also need to pass an id_token_hint to Keycloak’s Logout endpoint. This should carry the encoded id_token value.

在发送重定向URI的同时,我们还需要向Keycloak的id_token_hint传入Logout端点。这应该携带编码的id_token值。

Let’s recall how we’d saved the access_token, we’ll similarly save the id_token as well:

让我们回顾一下我们是如何保存access_token的,我们也将同样保存id_token

saveToken(token) {
  var expireDate = new Date().getTime() + (1000 * token.expires_in);
  Cookie.set("access_token", token.access_token, expireDate);
  Cookie.set("id_token", token.id_token, expireDate);
  this._router.navigate(['/']);
}

Importantly, in order to obtain the ID Token in the Authorization Server’s response payload, we should include openid in the scope parameter.

重要的是,为了在授权服务器的响应有效载荷中获得ID 令牌,我们应该在scope 参数中包含openid

Now let’s see the logging out process in action.

现在让我们看看注销过程的实际情况。

We’ll modify our function logout in App Service:

我们将在App Service中修改我们的函数logout

logout() {
  let token = Cookie.get('id_token');
  Cookie.delete('access_token');
  Cookie.delete('id_token');
  let logoutURL = "http://localhost:8083/auth/realms/baeldung/protocol/openid-connect/logout?
    id_token_hint=" + token + "&post_logout_redirect_uri=" + this.redirectUri;

  window.location.href = logoutURL;
}

Apart from the redirection, we also need to discard the Access and ID Tokens that we’d obtained from the Authorization Server.

除了重定向,我们还需要丢弃我们从授权服务器获得的访问和ID令牌

Hence, in the above code, first we deleted the tokens, and then redirected the browser to Keycloak’s logout API.

因此,在上述代码中,首先我们删除了令牌,然后将浏览器重定向到Keycloak的logout API。

Notably, we passed in the redirect URI as http://localhost:8089/ – the one we’re using throughout the application – so we’ll end up on the landing page after logging out.

值得注意的是,我们传入的重定向URI是http://localhost:8089/ –我们在整个应用程序中使用的URI–所以我们在注销后会在登陆页面上结束。

The deletion of Access, ID and Refresh Tokens corresponding to the current session is performed at the Authorization Server’s end. Our browser application had not saved the Refresh Token at all in this case.

与当前会话对应的访问、ID和刷新令牌的删除是在授权服务器端进行的。在这种情况下,我们的浏览器应用程序根本没有保存刷新令牌。

3. Logout Using Zuul Proxy

3.使用Zuul代理注销

In a previous article on Handling the Refresh Token, we have set up our application to be able to refresh the Access Token, using a Refresh Token. This implementation makes use of a Zuul proxy with custom filters.

在之前关于处理刷新令牌的文章中,我们已经将我们的应用程序设置为能够使用刷新令牌来刷新访问令牌。该实施方案使用了带有自定义过滤器的Zuul代理。

Here we’ll see how to add the logout functionality to the above.

在这里,我们将看到如何在上面添加注销功能。

This time around, we’ll utilize another Keycloak API to log out a user. We’ll be invoking POST on the logout endpoint to log out a session via a non-browser invocation, instead of the URL redirect we used in the previous section.

这一次,我们将利用另一个Keycloak API来注销一个用户。我们将在logout端点上调用POST,通过非浏览器调用注销会话,而不是我们在上一节使用的URL重定向。

3.1. Define Route for Logout

3.1.定义注销的路线

To start with, let’s add another route to the proxy in our application.yml:

首先,让我们在我们的application.yml中向代理添加另一条路由。

zuul:
  routes:
    //...
    auth/refresh/revoke:
      path: /auth/refresh/revoke/**
      sensitiveHeaders:
      url: http://localhost:8083/auth/realms/baeldung/protocol/openid-connect/logout
    
    //auth/refresh route

In fact, we added a sub-route to the already existing auth/refresh. It’s important that we add the sub-route before the main route, otherwise, Zuul will always map the URL of the main route.

事实上,我们在已经存在的auth/refresh上添加了一个子路由。重要的是,我们要在主路由之前添加子路由,否则,Zuul会一直映射主路由的URL

We added a sub route instead of a main one in order to have access to the HTTP-only refreshToken cookie, which was set to have a very limited path as /auth/refresh (and its sub-paths). We’ll see why we need the cookie in the next section.

我们添加了一个子路径而不是主路径,以便能够访问HTTP专用的refreshToken cookie,它被设置为/auth/refresh(及其子路径)的路径非常有限。我们将在下一节中看到为什么我们需要这个cookie。

3.2. POST to Authorization Server’s /logout

3.2.POST到授权服务器的/logout

Now let’s enhance the CustomPreZuulFilter implementation to intercept the /auth/refresh/revoke URL and add the necessary information to be passed on to the Authorization Server.

现在让我们加强CustomPreZuulFilter的实现,以拦截/auth/refresh/revoke URL,并添加必要的信息以传递给授权服务器。

The form parameters required for logout are similar to those of the Refresh Token request, except there is no grant_type:

注销所需的表单参数与Refresh Token请求的参数相似,只是没有grant_type

@Component 
public class CustomPostZuulFilter extends ZuulFilter { 
    //... 
    @Override 
    public Object run() { 
        //...
        if (requestURI.contains("auth/refresh/revoke")) {
            String cookieValue = extractCookie(req, "refreshToken");
            String formParams = String.format("client_id=%s&client_secret=%s&refresh_token=%s", 
              CLIENT_ID, CLIENT_SECRET, cookieValue);
            bytes = formParams.getBytes("UTF-8");
        }
        //...
    }
}

Here, we simply extracted the refreshToken cookie and sent in the required formParams.

在这里,我们只是提取了refreshToken cookie,并发送了所需的formParams.

3.3. Remove the Refresh Token

3.3.移除刷新令牌

When revoking the Access Token using the logout redirection as we saw earlier, the Refresh Token associated with it is also invalidated by the Authorization Server.

当使用logout重定向撤销访问令牌时,正如我们之前看到的那样,与之相关的刷新令牌也被授权服务器废止。

However, in this case, the httpOnly cookie will remain set on the Client. Given that we can’t remove it via JavaScript, we need to remove it from the server-side.

然而,在这种情况下,httpOnly cookie将保持在客户端上设置。鉴于我们不能通过JavaScript删除它,我们需要从服务器端删除它。

For that, let’s add to the CustomPostZuulFilter implementation that intercepts the /auth/refresh/revoke URL so that it will remove the refreshToken cookie when encountering this URL:

为此,让我们在CustomPostZuulFilter实现中添加拦截/auth/refresh/revoke URL的功能,以便在遇到这个URL时删除refreshToken Cookie

@Component
public class CustomPostZuulFilter extends ZuulFilter {
    //...
    @Override
    public Object run() {
        //...
        String requestMethod = ctx.getRequest().getMethod();
        if (requestURI.contains("auth/refresh/revoke")) {
            Cookie cookie = new Cookie("refreshToken", "");
            cookie.setMaxAge(0);
            ctx.getResponse().addCookie(cookie);
        }
        //...
    }
}

3.4. Remove the Access Token from the Angular Client

3.4.从Angular客户端删除访问令牌

Besides revoking the Refresh Token, the access_token cookie will also need to be removed from the client-side.

除了撤销刷新令牌,还需要从客户端删除access_token cookie。

Let’s add a method to our Angular controller that clears the access_token cookie and calls the /auth/refresh/revoke POST mapping:

让我们给Angular控制器添加一个方法,清除access_token cookie并调用/auth/refresh/revoke POST映射。

logout() {
  let headers = new HttpHeaders({
    'Content-type': 'application/x-www-form-urlencoded; charset=utf-8'});
  
  this._http.post('auth/refresh/revoke', {}, { headers: headers })
    .subscribe(
      data => {
        Cookie.delete('access_token');
        window.location.href = 'http://localhost:8089/';
        },
      err => alert('Could not logout')
    );
}

This function will be called when clicking on the Logout button:

当点击注销按钮时,这个函数将被调用。

<a class="btn btn-default pull-right"(click)="logout()" href="#">Logout</a>

4. Conclusion

4.总结

In this quick, but in-depth tutorial, we’ve shown how we can logout a user from an OAuth secured application and invalidate the tokens of that user.

在这个快速但深入的教程中,我们展示了如何从OAuth安全应用程序中注销用户并使该用户的令牌失效。

The full source code of the examples can be found over on GitHub.

示例的完整源代码可以在GitHub上找到over