1. 概述
本教程将演示如何为OAuth Spring Security应用添加登出功能。我们将探讨两种实现方式:首先基于Keycloak的OAuth应用实现登出(参考使用OAuth2创建REST API),然后通过Zuul代理实现登出。
本文使用Spring Security 5的OAuth栈。若需使用Spring Security OAuth遗留栈,请参考旧版文章:OAuth安全应用中的登出(遗留栈版)。
2. 前端应用实现登出
由于访问令牌由授权服务器管理,必须在该层级使其失效。具体步骤因授权服务器而异。以Keycloak为例,根据其文档,可直接通过浏览器重定向到以下URL实现登出:
http://auth-server/auth/realms/{realm-name}/protocol/openid-connect/logout?redirect_uri=encodedRedirectUri
除传递重定向URI外,还需向Keycloak的登出接口提交id_token_hint。该参数需携带编码后的id_token值。
参考保存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(['/']);
}
关键点:若要在授权服务器响应中获取ID令牌,需在scope参数中包含openid。
现在看登出流程的具体实现。修改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;
}
除重定向外,还需清除从授权服务器获取的访问令牌和ID令牌。上述代码先删除令牌,再将浏览器重定向到Keycloak的登出接口。
注意:重定向URI设为http://localhost:8089/
(应用统一入口),登出后将返回该页面。当前会话对应的访问令牌、ID令牌和刷新令牌的删除操作由授权服务器完成(本例中浏览器应用未保存刷新令牌)。
3. 通过Zuul代理实现登出
在处理刷新令牌一文中,我们已搭建了基于Zuul代理和自定义过滤器的令牌刷新机制。现在为其添加登出功能。
本次将使用Keycloak的另一组API:通过POST请求调用logout接口实现非浏览器方式的会话登出,而非前述的URL重定向。
3.1 定义登出路由
首先在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路由
⚠️ 重要:子路由auth/refresh/revoke
必须定义在主路由auth/refresh
之前,否则Zuul会始终匹配主路由。
使用子路由而非独立路由的原因是:需要访问路径受限的refreshToken cookie(该cookie路径设为/auth/refresh
及其子路径,详见令牌提取)。
3.2 POST调用授权服务器的*/logout*
增强CustomPreZuulFilter实现,拦截/auth/refresh/revoke
请求并添加必要参数:
@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");
}
//...
}
}
此处提取refreshToken cookie并组装为表单参数(与刷新令牌请求类似,但无需grant_type)。
3.3 移除刷新令牌
通过重定向方式登出时,关联的刷新令牌会被授权服务器自动失效。但本方案中,客户端的httpOnly cookie仍会保留。由于无法通过JavaScript删除,需在服务端处理。
修改CustomPostZuulFilter,拦截/auth/refresh/revoke
请求时移除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 移除Angular客户端的访问令牌
除刷新令牌外,还需清除客户端的access_token cookie。在Angular控制器中添加方法:
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')
);
}
该方法将绑定到登出按钮:
<a class="btn btn-default pull-right"(click)="logout()" href="#">Logout</a>
4. 总结
本教程深入探讨了OAuth安全应用的用户登出实现及令牌失效机制。完整示例代码可在GitHub获取。