首页 文章

如何使用JWT令牌刷新用户的会话

提问于
浏览
1

我是Angular的新手,我正在尝试实现一种机制,只要它们处于活动状态,就能让活跃用户保持登录状态 .

我有一个令牌 endpoints ,向用户发出JWT令牌

{  
      "access_token": "base64encodedandsignedstring",
      "token_type": "bearer",
      "expires_in": 299,
      "refresh_token": "f87ae3bee04b4ca39af6f22a198274df",
      "as:client_id": "mysite",
      "userName": "me@email.com",
      ".issued": "Wed, 19 Apr 2017 20:15:58 GMT",
      ".expires": "Wed, 19 Apr 2017 20:20:58 GMT"
}

另一个调用,它接受refresh_token并使用它来生成一个新的访问令牌 . 从Api的角度来看,这应该使我能够传递refresh_token并生成一个具有新过期日期的新JWT .

我不是100%肯定如何连接Angular端来支持这个,我的登录功能:

var _login = function (LoginData) {

    var data = "grant_type=password&username=" + LoginData.UserName + "&password=" + LoginData.Password + "&client_id=4TierWeb";

    var deferred = $q.defer();

    $http.post(serviceBase + 'authToken', data, { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }).then(function (response) {

        localStorageService.set('authorizationData', { token: response.data.access_token, userName: LoginData.userName, refreshToken: response.data.refresh_token, useRefreshTokens: true });

        _authentication.isAuth = true;
        _authentication.userName = LoginData.UserName;

        deferred.resolve(response);

    }, function (err, status) {
        _logOut();
        deferred.reject(err);
    });

    return deferred.promise;

};

我的刷新功能:

var _refreshToken = function () {
    var deferred = $q.defer();

    var authData = localStorageService.get('authorizationData');

    if (authData) {

        if (authData.useRefreshTokens) {

            var data = "grant_type=refresh_token&refresh_token=" + authData.refreshToken + "&client_id=4TierWeb";

            localStorageService.remove('authorizationData');

            $http.post(serviceBase + 'authToken', data, { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }).then(function (response) {

                localStorageService.set('authorizationData', { token: response.data.access_token, userName: response.data.userName, refreshToken: response.data.refresh_token, useRefreshTokens: true });
               // response.headers.Authorization = 'Bearer ' + response.token;
                deferred.resolve(response);

            }, function (err, status) {
                _logOut();
                deferred.reject(err);
            });
        }
    }

    return deferred.promise;
};

而我的拦截器:

app.factory('authInterceptorService', ['$q', '$location', 'localStorageService', function ($q, $location, localStorageService) {
    var authInterceptorServiceFactory = {
        request: function (config) {

            config.headers = config.headers || {};

            var authData = localStorageService.get('authorizationData');
            if (authData) {
                    config.headers.Authorization = 'Bearer ' + authData.token;
                    }
            return config;
        },
        responseError: function (error) {
            if (error.status === 401) {
                        $location.path('/login');
                    }
            return $q.reject(error);
        }
    };
    return authInterceptorServiceFactory;
}]);

我的拦截器在没有如上所述的刷新机制的情况下工作得很好,但是当我添加刷新机制时:

authService.RefreshToken();
   config.headers.Authorization = 'Bearer ' + authData.token;

我能够拉下一个新的JWT,但下一行似乎不再正常工作,我在登陆页面上得到401并且有效载荷中没有持票人令牌,我在这里缺少什么?

更新的拦截器:

app.factory('authInterceptorService',['$q', '$location', 'localStorageService', '$injector', function($q, $location, localStorageService, $injector) {
        return {
            request: function(config) {

                config.headers = config.headers || {};

                var authData = localStorageService.get('authorizationData');
                if (authData) {
                    config.headers.Authorization = 'Bearer ' + authData.token;
                }
                return config;
            },
            responseError: function(rejection) {
                //var promise = $q.reject(rejection);

                if (rejection.status === 401) {
                    var authService = $injector.get('authService');
                    // refresh the token
                    authService.refreshToken().then(function() {
                        // retry the request
                        var $http = $injector.get('$http');
                        return $http(rejection.config);
                    });
                }
                return $q.reject(rejection);
            }
        };
    }
]);

3 回答

  • 0

    您需要等待refresh_token请求完成获取新的访问令牌,然后使用响应发出新请求 .

    喜欢: authService.refreshToken().then(doRequest())

    让我们假设您在 authService 中有2个函数:

    function getAccessToken() { ...get access token like in login()... } - 返回Promise

    function refreshToken() { ...existing logic... } - 返回Promise

    我们假设您将使用jwt_decode(jwt)来解码JWT令牌 .

    我认为您可以通过两种方式实施:

    1st way: 获取令牌并立即订阅以便在过期时刷新

    function getAccessToken() {
      ...
      return $http(...)
        .then(function(response) {
           // ...correct credentials logic...
    
           if(authService.refreshTimeout) {
             $window.clearTimeout(authService.refreshTimeout);
           }
    
           // decode JWT token
           const access_token_jwt_data = jwt_decode(response.data.access_token);
    
           // myOffset is an offset you choose so you can refresh the token before expiry
           const expirationDate = new Date(access_token_jwt_data * 1000 - myOffset);
    
           // refresh the token when expired
           authService.refreshTimeout = $window.setTimeout(function() {
             authService.refreshToken();
           });
    
           return response.data;
        })
        .catch(function(error) {
          // ...invalid credentials logic...
          return $q.reject(error);
        });
    }
    

    NOTE: 您可以使用 window 而不是 $window . 我认为你当时并不需要新的摘要周期 . $ http请求成功完成时,将启动新的摘要 .

    NOTE: 这意味着您在重新加载页面时也需要注意这种情况 . 从而重新启用刷新超时 . 因此,您可以重用 getAccessToken() 中的逻辑来订阅到期日期,但这次您从 localStorage 获取令牌 . 这意味着您可以将此逻辑重构为一个名为 function subscribeToTokenExpiry(accessToken) 的新函数 . 因此,如果localStorage中存在访问令牌,则可以在 authService 构造函数中调用此函数 .

    2nd way: 从服务器收到错误代码后刷新HTTP拦截器中的令牌 .

    如果拦截器收到与令牌到期情况匹配的错误,则可以刷新令牌 . 这很大程度上取决于您的后端实现,因此您可能会收到HTTP 401或400或其他任何内容以及一些自定义错误消息或代码 . 所以你需要检查你的后端 . 还要检查它们在返回HTTP状态和错误代码时是否一致 . 一些实现细节可能会随着时间而改变,框架开发人员可能会建议用户不要依赖于特定的实现,因为它仅供内部使用 . 在这种情况下,您只能保留HTTP状态并省略代码,因为您将来有更好的机会获得相同的代码 . 但问问你的后端或那些创建框架的人 .

    NOTE: 关于Spring OAuth2后端实现,请在本答案的最后找到详细信息 .

    回到你的代码,拦截器应如下所示:

    app.factory('authInterceptorService',
        ['$q', '$location', 'localStorageService', 'authService', '$injector',
        function ($q, $location, localStorageService, authService, $injector) {
        var authInterceptorServiceFactory = {
            request: function (config) {
    
                config.headers = config.headers || {};
    
                var authData = localStorageService.get('authorizationData');
                if (authData) {
                    config.headers.Authorization = 'Bearer ' + authData.token;
                }
                return config;
            },
            responseError: function (response) {
                let promise = $q.reject(response);
    
                if (response.status === 401
                    && response.data 
                    && response.data.error === 'invalid_token') {
    
                    // refresh the token
                    promise = authService.refreshToken().then(function () {
                        // retry the request
                        const $http = $injector.get('$http');
                        return $http(response.config);
                    });
                }
    
                return promise.catch(function () {
                    $location.path('/login');
                    return $q.reject(response);
                });
            }
        };
        return authInterceptorServiceFactory;
    }]);
    

    Spring Security OAuth2 back-end related:

    我为那些对Spring Authorization Server实现感到好奇的人添加了这一部分,因为Spring是Java世界中非常流行的框架 .

    1) Expiry date

    关于到期日,这表示在 seconds . 在JWT解码字符串后,您将在access_token和refresh_token中找到"exp"键 .

    这是在几秒钟内,因为您添加使用DefaultAccessTokenConverterJwtAccessTokenConverter

    if (token.getExpiration() != null) {
      response.put(EXP, token.getExpiration().getTime() / 1000);
    }
    

    在配置授权服务器时添加JwtAccessTokenConverter

    @Configuration
    @EnableAuthorizationServer
    public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
      @Override
      public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        // ...
        endpoints.accessTokenConverter(jwtAccessTokenConverter)
        // ...
      }
    }
    

    2) Access token expired response

    您可能需要处理 HTTP 400HTTP 401 状态中的一个或两个并依赖 { "error": "invalid_token" } . 但这很大程度上取决于如何使用Spring实现后端 .

    请参阅下面的解释:

    关于资源服务器配置(我们向其发送请求以获取我们想要的资源的配置),流程如下:

    DefaultTokenServicesResourceServerTokenServices 实现 . 有两种可能的实现,一种是 DefaultTokenServices 和其他是RemoteTokenServices .

    如果我们使用 DefaultTokenServices ,那么将在资源服务器上检查令牌 . 这意味着资源服务器知道对令牌进行签名以检查令牌有效性的密钥 . 这种方法意味着将密钥分发给需要此类行为的所有方 .

    如果我们使用 RemoteTokenServices ,那么将针对由CheckTokenEndpoint处理的 /oauth/check_token endpoints 检查令牌 .

    到期时 CheckTokenEndpoint 将创建一个带有HTTP 400的InvalidTokenException,它将由OAuth2ExceptionJackson2Serializer转换为带有数据 { "error": "invalid_token", "error_description": "Token has expired" }HTTP 400 .

    另一方面, DefaultTokenServices 也将创建一个 InvalidTokenException 异常,但是有其他消息而不覆盖HTTP状态,因此最终是HTTP 401 . 所以这将成为 HTTP 401 与数据 { "error": "invalid_token", "error_description": "Access token expired: myTokenValue" } .

    再次发生这个, HTTP 400HTTP 401 ,因为在两种情况下都会抛出 InvalidTokenException ,而不会覆盖 getHttpErrorCode() ,而 getHttpErrorCode() ,但是 CheckTokenEndpoint 会覆盖 400 .

    Note: 我添加了Github Issue以检查此行为(400 vs 401)是否正确 .

  • 0

    我曾几次使用this interceptor没有任何问题 . 您可以将其设置为静默刷新令牌,并且只有在刷新失败时才会抛出错误(并导航到登录屏幕) . 希望这可以帮助

  • 0

    在Angular应用程序中使用刷新令牌是否安全?我不确定...... OIDC隐式流程(用于SPA或移动应用程序的流程),没有涉及刷新令牌 .

相关问题