1. 概述
本快速教程將演示如何為 OAuth Spring Security 應用程序添加註銷功能。
我們將看到幾種實現方法。首先,我們將從 OAuth 應用程序中註銷 Keycloak 用户,正如在“使用 OAuth2 創建 REST API”中所述;然後,使用我們之前看到的 Zuul 代理。
我們將使用 Spring Security 5 中的 OAuth 棧。 如果您想使用 Spring Security OAuth 遺留棧,請參閲“在 OAuth 安全應用程序中註銷”(使用遺留棧)這篇以前的文章。
2. 使用前端應用程序註銷
由於訪問令牌由授權服務器管理,因此需要在該級別中失效它們。 具體步驟將根據您使用的授權服務器而略有不同。
在我們的示例中,根據 Keycloak 文檔,從瀏覽器應用程序直接註銷時,我們可以將瀏覽器重定向到 http://auth-server/auth/realms/{realm-name}/protocol/openid-connect/logout?redirect_uri=encodedRedirectUri。
除了發送重定向 URI 之外,我們還需要將 id_token_hint 傳遞給 Keycloak 的 註銷端點。 這應該攜帶編碼後的 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;
}除了重定向之外,我們還需要丟棄來自授權服務器獲得的 Access 和 ID Tokens。
因此,在上述代碼中,我們首先刪除了這些 tokens,然後將瀏覽器重定向到 Keycloak 的 logout API。
值得注意的是,我們傳遞了重定向 URI 為 http://localhost:8089/ – 我們在整個應用程序中使用的 URI – 這樣在註銷後,我們就會回到登陸頁面。
當前會話中 Access、ID 和 Refresh Tokens 的刪除在授權服務器端執行。在本例中,我們的瀏覽器應用程序根本沒有保存 Refresh Token。
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 route事實上,我們為現有的 auth/refresh 添加了一個子路由。 重要的是在添加子路由之前添加該子路由,否則 Zuul 會始終將 URL 映射到主路由。
為了訪問 HTTP-only refreshToken Cookie,我們添加了一個子路由,而不是主路由。該 Cookie 的路徑設置為非常有限的路徑 /auth/refresh (及其子路徑)。 我們將在下一部分中看到為什麼我們需要該 Cookie。
3.2. 向授權服務器發送 POST 請求:/logout
現在,讓我們增強 <em>CustomPreZuulFilter</em> 的實現,使其攔截 <em>/auth/refresh/revoke</em> URL 並將必要的信息傳遞給授權服務器。
註銷所需的表單參數與刷新令牌請求類似,但缺少 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");
}
//...
}
}在這裏,我們僅提取了 refreshToken 餅乾,併發送了所需的 formParams
3.3. 取消刷新令牌
當使用先前所述的 <em>logout</em> 重定向方式撤銷 Access Token 時,相關的 Refresh Token 也會被 Authorization Server 無效化。
但是,在這種情況下,客户端仍然會設置 <em>httpOnly</em> Cookie。由於我們無法通過 JavaScript 刪除它,因此需要在服務器端刪除它。
為此,我們將在 <em>CustomPostZuulFilter</em> 的實現中添加攔截器,以便它在遇到 <em>/auth/refresh/revoke</em> 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. 從 Angular 客户端移除訪問令牌
除了撤銷刷新令牌之外,還需要從客户端移除 access_token 餅乾。
讓我們為我們的 Angular 控制器添加一個方法,清除 access_token 餅乾並調用 /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')
);
}此函數將在點擊“登出”按鈕時被調用:
<a class="btn btn-default pull-right"(click)="logout()" href="#">Logout</a>4. 結論
在本快速但深入的教程中,我們演示瞭如何從一個使用 OAuth 認證的應用中註銷用户,並失效該用户的令牌。