如何使用 JWT 令牌刷新用户会话
How can I refresh a user's session using JWT token
我是 Angular 的新手,我正在尝试实施一种机制,让活跃用户在活跃时保持登录状态。
我有一个向用户颁发 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);
}
};
}
]);
您需要等待 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 令牌。
我认为您可以通过两种方式实施:
第一种方式:获取令牌并立即订阅,以便在过期时刷新
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);
});
}
注意: 您可以使用 window
而不是 $window
。我不认为你在那一刻真的需要一个新的摘要周期。无论 $http 请求是否成功完成,都会启动一个新的摘要。
注意: 这意味着您在重新加载页面时也需要注意大小写。因此重新启用刷新超时。因此,您可以重用 getAccessToken()
中的逻辑来订阅到期日期,但这次您从 localStorage
中获取令牌。这意味着您可以将此逻辑重构为一个名为 function subscribeToTokenExpiry(accessToken)
之类的新函数。因此,如果您的 localStorage 中有访问令牌,您可以在 authService
构造函数中调用此函数。
第二种方式:在从服务器收到错误代码后刷新 HTTP 拦截器中的令牌。
如果您的拦截器收到与令牌过期情况相匹配的错误,您可以刷新您的令牌。这在很大程度上取决于您的后端实现,因此您可能会收到 HTTP 401 或 400 或其他任何内容以及一些自定义错误消息或代码。所以你需要检查你的后端。还要检查它们在返回 HTTP 状态和错误代码时是否一致。一些实现细节可能会随着时间而改变,框架开发人员可能会建议用户不要依赖该特定实现,因为它仅供内部使用。在那种情况下,您可以只保留 HTTP 状态并省略代码,因为您将来有更好的机会拥有相同的代码。但是问问你的后端或者创建框架的那些。
注意: 关于 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 安全 OAuth2 后端相关:
我添加此部分是为了那些对 Spring 授权服务器实施感到好奇的人,因为 Spring 是 Java 世界中非常流行的框架。
1) 有效期
关于到期日期,以秒表示。在 JWT 解码字符串后,您会在 access_token 和 refresh_token 中找到 "exp" 键。
这是以秒为单位的,因为您添加了 JwtAccessTokenConverter
which uses DefaultAccessTokenConverter
:
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) 访问令牌过期响应
您可能需要处理 HTTP 400
和 HTTP 401
状态中的一种或两种,并依赖于 { "error": "invalid_token" }
。但这在很大程度上取决于后端是如何使用 Spring 实现的。
请参阅以下说明:
关于资源服务器配置(我们向其发送请求以获取我们想要的资源的服务器),流程如下:
OAuth2AuthenticationProcessingFilter
从请求中获取访问令牌的 servlet 过滤器
OAuth2AuthenticationManager
解析令牌字符串
DefaultTokenServices
获取访问令牌对象。
OAuth2AuthenticationProcessingFilter
try catch 会将异常委托给 OAuth2AuthenticationEntryPoint
,它会为异常创建响应。
DefaultTokenServices
是一个 ResourceServerTokenServices
实现。
这样的实现有两种可能,一种是这个DefaultTokenServices
,另一种是RemoteTokenServices
.
如果我们使用DefaultTokenServices
,那么将在资源服务器上检查令牌。这意味着资源服务器知道签署令牌的密钥,以便检查令牌的有效性。这种方法意味着将密钥分发给所有需要这种行为的各方。
如果我们使用 RemoteTokenServices
,那么令牌将根据 CheckTokenEndpoint
处理的 /oauth/check_token
端点进行检查。
到期时 CheckTokenEndpoint
将在 HTTP 400
中创建一个 InvalidTokenException
with HTTP 400, that will converted by OAuth2ExceptionJackson2Serializer
数据 { "error": "invalid_token", "error_description": "Token has expired" }
.
另一方面,DefaultTokenServices
也会创建一个 InvalidTokenException
异常,但带有其他消息并且不会覆盖 HTTP 状态,因此最终是 HTTP 401。所以这将变成 HTTP 401
数据 { "error": "invalid_token", "error_description": "Access token expired: myTokenValue" }
.
再次发生这种情况,HTTP 400
或 HTTP 401
,因为在这两种情况下都会抛出 InvalidTokenException
DefaultTokenServices
抛出而不会覆盖 getHttpErrorCode()
即 [=58] =] 但 CheckTokenEndpoint
用 400
覆盖它。
注意:我添加了一个 Github Issue 以检查此行为(400 与 401)是否正确。
我曾多次使用 this interceptor,没有任何问题。
您可以将其设置为静默刷新令牌,并且仅在刷新失败时抛出错误(并导航至登录屏幕)。希望这有帮助
在 Angular 应用程序中使用刷新令牌是否安全?我不知道...
OIDC 隐式流(用于 SPA 或移动应用程序的流),不涉及刷新令牌。
我是 Angular 的新手,我正在尝试实施一种机制,让活跃用户在活跃时保持登录状态。
我有一个向用户颁发 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);
}
};
}
]);
您需要等待 refresh_token 请求完成获取新的访问令牌,然后使用响应发出新请求。
赞:authService.refreshToken().then(doRequest())
假设您在 authService
:
function getAccessToken() { ...get access token like in login()... }
- 返回 Promise
function refreshToken() { ...existing logic... }
- 返回 Promise
假设您将使用 jwt_decode(jwt)
解码 JWT 令牌。
我认为您可以通过两种方式实施:
第一种方式:获取令牌并立即订阅,以便在过期时刷新
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);
});
}
注意: 您可以使用 window
而不是 $window
。我不认为你在那一刻真的需要一个新的摘要周期。无论 $http 请求是否成功完成,都会启动一个新的摘要。
注意: 这意味着您在重新加载页面时也需要注意大小写。因此重新启用刷新超时。因此,您可以重用 getAccessToken()
中的逻辑来订阅到期日期,但这次您从 localStorage
中获取令牌。这意味着您可以将此逻辑重构为一个名为 function subscribeToTokenExpiry(accessToken)
之类的新函数。因此,如果您的 localStorage 中有访问令牌,您可以在 authService
构造函数中调用此函数。
第二种方式:在从服务器收到错误代码后刷新 HTTP 拦截器中的令牌。
如果您的拦截器收到与令牌过期情况相匹配的错误,您可以刷新您的令牌。这在很大程度上取决于您的后端实现,因此您可能会收到 HTTP 401 或 400 或其他任何内容以及一些自定义错误消息或代码。所以你需要检查你的后端。还要检查它们在返回 HTTP 状态和错误代码时是否一致。一些实现细节可能会随着时间而改变,框架开发人员可能会建议用户不要依赖该特定实现,因为它仅供内部使用。在那种情况下,您可以只保留 HTTP 状态并省略代码,因为您将来有更好的机会拥有相同的代码。但是问问你的后端或者创建框架的那些。
注意: 关于 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 安全 OAuth2 后端相关:
我添加此部分是为了那些对 Spring 授权服务器实施感到好奇的人,因为 Spring 是 Java 世界中非常流行的框架。
1) 有效期
关于到期日期,以秒表示。在 JWT 解码字符串后,您会在 access_token 和 refresh_token 中找到 "exp" 键。
这是以秒为单位的,因为您添加了 JwtAccessTokenConverter
which uses DefaultAccessTokenConverter
:
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) 访问令牌过期响应
您可能需要处理 HTTP 400
和 HTTP 401
状态中的一种或两种,并依赖于 { "error": "invalid_token" }
。但这在很大程度上取决于后端是如何使用 Spring 实现的。
请参阅以下说明:
关于资源服务器配置(我们向其发送请求以获取我们想要的资源的服务器),流程如下:
OAuth2AuthenticationProcessingFilter
从请求中获取访问令牌的 servlet 过滤器OAuth2AuthenticationManager
解析令牌字符串DefaultTokenServices
获取访问令牌对象。OAuth2AuthenticationProcessingFilter
try catch 会将异常委托给OAuth2AuthenticationEntryPoint
,它会为异常创建响应。
DefaultTokenServices
是一个 ResourceServerTokenServices
实现。
这样的实现有两种可能,一种是这个DefaultTokenServices
,另一种是RemoteTokenServices
.
如果我们使用DefaultTokenServices
,那么将在资源服务器上检查令牌。这意味着资源服务器知道签署令牌的密钥,以便检查令牌的有效性。这种方法意味着将密钥分发给所有需要这种行为的各方。
如果我们使用 RemoteTokenServices
,那么令牌将根据 CheckTokenEndpoint
处理的 /oauth/check_token
端点进行检查。
到期时 CheckTokenEndpoint
将在 HTTP 400
中创建一个 InvalidTokenException
with HTTP 400, that will converted by OAuth2ExceptionJackson2Serializer
数据 { "error": "invalid_token", "error_description": "Token has expired" }
.
另一方面,DefaultTokenServices
也会创建一个 InvalidTokenException
异常,但带有其他消息并且不会覆盖 HTTP 状态,因此最终是 HTTP 401。所以这将变成 HTTP 401
数据 { "error": "invalid_token", "error_description": "Access token expired: myTokenValue" }
.
再次发生这种情况,HTTP 400
或 HTTP 401
,因为在这两种情况下都会抛出 InvalidTokenException
DefaultTokenServices
抛出而不会覆盖 getHttpErrorCode()
即 [=58] =] 但 CheckTokenEndpoint
用 400
覆盖它。
注意:我添加了一个 Github Issue 以检查此行为(400 与 401)是否正确。
我曾多次使用 this interceptor,没有任何问题。 您可以将其设置为静默刷新令牌,并且仅在刷新失败时抛出错误(并导航至登录屏幕)。希望这有帮助
在 Angular 应用程序中使用刷新令牌是否安全?我不知道... OIDC 隐式流(用于 SPA 或移动应用程序的流),不涉及刷新令牌。