知識庫 / Spring / Spring Security RSS 訂閱

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

Spring Security
HongKong
9
02:26 PM · Dec 06 ,2025

1. 概述

本文將為使用 OAuth 2 認證的應用程序添加“記住我”功能,通過利用 OAuth 2 刷新令牌。

本文是我們在使用 OAuth 2 保護 Spring REST API 系列中的延續,該 API 通過 AngularJS 客户端訪問。要設置授權服務器、資源服務器和前端客户端,請參考入門文章。

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

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

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

在首次使用 密碼 授權類型進行身份驗證時,用户需要發送有效的用户名和密碼,以及客户端 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調用,而刷新令牌的目的在於獲取一個新的有效訪問令牌,或者直接撤銷之前的令牌。

為了使用refresh_token grant type 獲取新的訪問令牌,用户不再需要輸入憑據,只需提供客户端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>

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

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 端點發送 POST 請求,並使用 grant_type=refresh_token 參數。

接下來,我們使用我們已注入的 $http 模塊發送請求。如果請求成功,我們將設置一個新的 Authentication 標頭,包含新的訪問令牌值,以及一個新的 access_token Cookie 值。如果請求失敗,這可能會發生在刷新令牌最終過期時,則用户將被重定向到登錄頁面:

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

刷新令牌通過我們在上一篇文章中實現的 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</em/> 中:

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

6. 主動刷新令牌

另一種實現“記住我”功能的做法是在當前令牌過期之前請求新的訪問令牌。

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

為每個身份驗證過程保存該值:

$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.