OAuth2 使用刷新令牌(使用 Spring Security OAuth 遺留棧)

Spring Security
Remote
1
12:08 AM · Nov 30 ,2025

1. 概述

在本文中,我們將為 OAuth 2 安全的應用程序添加“記住我”功能,通過利用 OAuth 2 刷新令牌。

本文是我們在使用 OAuth 2 安全 Spring REST API 的系列文章的延續,該 API 通過 AngularJS 客户端訪問。要設置授權服務器、資源服務器和前端客户端,您可以參考介紹性文章。

注意:本文使用 Spring OAuth 遺留項目

2. OAuth 2 訪問令牌和刷新令牌

首先,我們快速回顧一下 OAuth 2 令牌以及它們的使用方法。

在首次使用 密碼 grant 類型進行身份驗證時,用户需要發送有效的用户名和密碼,以及客户端 ID 和密鑰。如果身份驗證請求成功,服務器會返回一個類似於以下格式的響應:

{
    "access_token": "2e17505e-1c34-4ea6-a901-40e49ba786fa",
    "token_type": "bearer",
    "refresh_token": "e5f19364-862d-4212-ad14-9d6275ab1a62",
    "expires_in": 59,
    "scope": "read write",
}

我們可以看到服務器響應包含訪問令牌以及刷新令牌。訪問令牌將用於後續需要身份驗證的 API 調用,而 刷新令牌的目的在於獲取一個新的有效訪問令牌,或直接撤銷之前的令牌

為了使用 刷新令牌 grant 類型獲取新的訪問令牌,用户不再需要輸入憑據,只需客户端 ID、密鑰和當然是刷新令牌。

使用兩種類型的令牌的目標是提高用户安全性。通常,訪問令牌具有較短的有效期限,以便如果攻擊者獲取訪問令牌,他們只有有限的時間來使用它。另一方面,如果刷新令牌被泄露,這毫無用處,因為客户端 ID 和密鑰也需要。

刷新令牌的另一個好處是允許撤銷訪問令牌,即使用户表現出異常行為,例如從新的 IP 地址登錄,也不需要重新發送令牌。

3. 使用刷新令牌的記住我功能

用户通常希望保留會話,因為他們不需要每次訪問應用程序時都輸入憑據。

由於訪問令牌的有效時間較短,我們可以使用刷新令牌生成新的訪問令牌,從而避免每次訪問令牌過期時要求用户輸入憑據。

在下一部分,我們將討論兩種實現此功能的途徑:

  • 首先,通過攔截任何返回 401 狀態碼的用户請求,這意味着訪問令牌無效。當這種情況發生時,如果用户已選中“記住我”選項,我們將自動使用 refresh_token grant 類型發出請求以獲取新的訪問令牌,然後重新執行初始請求。
  • 其次,我們可以主動刷新訪問令牌——在訪問令牌即將過期前幾秒發送請求以刷新令牌。

第二種選項具有優勢,即用户的請求不會被延遲。

4. 存儲刷新令牌

在關於刷新令牌的文章中,我們添加了一個 CustomPostZuulFilter,該過濾器攔截向 OAuth 服務器發送的請求,提取在身份驗證過程中返回的刷新令牌,並將其存儲在服務器端 cookie 中:

@Component
public class CustomPostZuulFilter extends ZuulFilter {

    @Override
    public Object run() {
        //...
        Cookie cookie = new Cookie("refreshToken", refreshToken);
        cookie.setHttpOnly(true);
        cookie.setPath(ctx.getRequest().getContextPath() + "/oauth/token");
        cookie.setMaxAge(2592000); // 30 days
        ctx.getResponse().addCookie(cookie);
        //...
    }
}

接下來,我們在登錄表單上添加一個複選框,該複選框與 loginData.remember 變量綁定:

<input type="checkbox"  ng-model="loginData.remember" id="remember"/>
<label for="remember">Remeber me</label>

我們的登錄表單現在將顯示一個額外的複選框:

remember

loginData 對象將包含在身份驗證請求中,因此它將包含 remember 參數。在身份驗證請求發送之前,我們將根據參數設置一個名為 remember 的 cookie:

function obtainAccessToken(params){
    if (params.username != null){
        if (params.remember != null){
            $cookies.put("remember","yes");
        }
        else {
            $cookies.remove("remember");
        }
    }
    //...
}

因此,我們將檢查此 cookie 以確定我們是否應該刷新訪問令牌,具體取決於用户是否希望被記住:

5. 通過攔截 401 響應刷新令牌

為了攔截返回 401 響應的請求,我們修改我們的 AngularJS 應用程序,添加一個攔截器,其中包含一個 responseError 函數:

app.factory('rememberMeInterceptor', ['$q', '$injector', '$httpParamSerializer', 
  function($q, $injector, $httpParamSerializer) {  
    var interceptor = {
        responseError: function(response) {
            if (response.status == 401){
                
                // refresh access token

                // make the backend call again and chain the request
                return deferred.promise.then(function() {
                    return $http(response.config);
                });
            }
            return $q.reject(response);
        }
    };
    return interceptor;
}]);

我們的函數檢查狀態是否為 401 – 這意味着 Access Token 無效,如果確實,則嘗試使用 Refresh Token 以獲取新的有效 Access Token。

如果此操作成功,則該函數將繼續重試導致 401 錯誤的原請求。這確保了用户體驗的無縫性。

讓我們更詳細地瞭解刷新 Access Token 的過程。首先,我們需要初始化必要的變量:

var $http = $injector.get('$http');
var $cookies = $injector.get('$cookies');
var deferred = $q.defer();

var refreshData = {grant_type:"refresh_token"};
                
var req = {
    method: 'POST',
    url: "oauth/token",
    headers: {"Content-type": "application/x-www-form-urlencoded; charset=utf-8"},
    data: $httpParamSerializer(refreshData)
}

你可以看到 req 變量,我們將使用它向 /oauth/token 端點發送一個帶有參數 grant_type=refresh_token 的 POST 請求。

接下來,我們將使用我們已注入的 $http 模塊來發送請求。如果請求成功,我們將設置一個新的 Authentication 標題,其中包含新的 Access Token 值,以及一個新的 access_token cookie 值。如果請求失敗,這可能會發生,如果 Refresh Token 最終也會過期,則用户將被重定向到登錄頁面:

$http(req).then(
    function(data){
        $http.defaults.headers.common.Authorization= 'Bearer ' + data.data.access_token;
        var expireDate = new Date (new Date().getTime() + (1000 * data.data.expires_in));
        $cookies.put("access_token", data.data.access_token, {'expires': expireDate});
        window.location.href="index";
    },function(){
        console.log("error");
        $cookies.remove("access_token");
        window.location.href = "login";
    }
);

Refresh Token 通過我們在上一篇文章中實現的 CustomPreZuulFilter 添加到請求中:

@Component
public class CustomPreZuulFilter extends ZuulFilter {

    @Override
    public Object run() {
        //...
        String refreshToken = extractRefreshToken(req);
        if (refreshToken != null) {
            Map<String, String[]> param = new HashMap<String, String[]>();
            param.put("refresh_token", new String[] { refreshToken });
            param.put("grant_type", new String[] { "refresh_token" });

            ctx.setRequest(new CustomHttpServletRequest(req, param));
        }
        //...
    }
}

除了定義攔截器,還需要將其註冊到 $httpProvider 中:

app.config(['$httpProvider', function($httpProvider) {  
    $httpProvider.interceptors.push('rememberMeInterceptor');
}]);

6. 主動刷新令牌

實施“記住我”功能的一種方式是,在當前令牌過期之前請求新的訪問令牌。

收到訪問令牌時,JSON 響應包含一個 expires_in 值,該值指定令牌的有效時間(以秒為單位)。

將此值存儲在每個身份驗證的 Cookie 中:

$cookies.put("validity", data.data.expires_in);

然後,使用 AngularJS $timeout 服務,在令牌過期前 10 秒內安排刷新請求:

if ($cookies.get("remember") == "yes"){
    var validity = $cookies.get("validity");
    if (validity >10) validity -= 10;
    $timeout( function(){ $scope.refreshAccessToken(); }, validity * 1000);
}

7. 結論

在本教程中,我們探討了兩種使用 OAuth2 應用程序和 AngularJS 前端實現“記住我”功能的實現方式。

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.