一、Token 的生命周期
Identity token
用于登录客户端;通常来说,如果用户在程序中还是活跃的,那么应该保持登录状态Access token
的过期由 IDP Policy 控制,默认一小时有效
二、设置 Token 时限示例
- 修改 IDP 的
Config::GetClients()
, 为 new client 添加如下代码
//以下 token 的时间单位都是秒
IdentityTokenLifetime = 5 * 60, // 默认就是五分钟,所以这里也可以注释掉
AuthorizationCodeLifetime = 5 * 60, // 默认五分钟
AccessTokenLifetime = 2 * 60, // 默认 一小时。因为 IDP 有自己的刷新机制(5分钟间隔),所以这个 token 实际过期的时间也许比两分钟长
运行后,进入主页,等待两分钟或更久,再刷新页面,会提示没有访问权限
三、获取长期有效的 token
使用
Refresh token
来获取新的 Access token
可以避免token过期提示以及让用户重新输入登录条件客户端发送给 IDP 一个 post 请求,需要把 clientid 和 secret key 作为 authorization header 添加到 http 请求头中,同时添加
Refresh token
作为请求 body。 IDP 收到请求验证通过后,会返回新的 identity token 和 access token(以及可选的 refresh token)支持 refresh token 需要 IDP 启用
offline_access
scope示例:
- 修改 IDP 的 Config::GetClients(), 为 new client 添加如下代码
AllowOfflineAccess = true, // 启用 'offline_access' scope
AbsoluteRefreshTokenLifetime = 30 * 24 * 60 * 60, // refresh token 的绝对过期时间,默认30天
RefreshTokenExpiration = TokenExpiration.Sliding, // refresh token 一般不需要设置绝对的过期时间,设置成 sliding 模式就好
SlidingRefreshTokenLifetime = 5 * 60,// sliding 模式下,当请求新的 refresh token时,他的过期时间会被重置为这里设置的值(但不会超过 AbsoluteRefreshTokenLifetime 的设置)
UpdateAccessTokenClaimsOnRefresh = true, // refresh token 请求是否更新 access token 里面携带的 user claim 信息;设为 true, 即使 token 没有过期,也会更新 accesstoken 的 claim 值
+修改 Client::Startup
类的 ConfigureServices
,添加 scope
options.Scope.Add("offline_access");
- 修改
ImageClient
类为如下
using Microsoft.AspNetCore.Http;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using IdentityModel.Client;
using Microsoft.AspNetCore.Authentication;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
namespace ImageGallery.Client.Services
{
public class ImageGalleryHttpClient : IImageGalleryHttpClient
{
private readonly IHttpContextAccessor _httpContextAccessor;
private HttpClient _httpClient = new HttpClient();
public ImageGalleryHttpClient(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
public async Task<HttpClient> GetClient()
{
var accessToken = string.Empty;
// get the current HttpContext to access the tokens
var currentContext = _httpContextAccessor.HttpContext;
var expires_at = await currentContext.GetTokenAsync("expires_at");
if (string.IsNullOrWhiteSpace(expires_at)
|| DateTime.Parse(expires_at).AddSeconds(-60).ToUniversalTime() < DateTime.UtcNow)
{
// token 过期时间未设置,或者当前距离过期时间小于 60s
accessToken = await RenewTokens();
}
else
{
// get access token from currentContext
accessToken = await currentContext.GetTokenAsync(OpenIdConnectParameterNames.AccessToken);
}
if (!string.IsNullOrWhiteSpace(accessToken))
{
// set as Bearer token
_httpClient.SetBearerToken(accessToken);
}
_httpClient.BaseAddress = new Uri("https://localhost:44363/");
_httpClient.DefaultRequestHeaders.Accept.Clear();
_httpClient.DefaultRequestHeaders.Accept.Add(
new MediaTypeWithQualityHeaderValue("application/json"));
return _httpClient;
}
private async Task<string> RenewTokens()
{
// get the current HttpContext to access the tokens
var currentContext = _httpContextAccessor.HttpContext;
// get metadata
var discoveryClient = new DiscoveryClient("https://localhost:44319/");
var metaDataResponse = await discoveryClient.GetAsync(); // 结果包含 token endpoint 的 uri
// create a new token client to get new tokens
var tokenClient = new TokenClient(metaDataResponse.TokenEndpoint, "junguoguoimagegalleryclient", "junguoguosecret");
// get the saved refresh token
var currentRefreshToken = await currentContext.GetTokenAsync(OpenIdConnectParameterNames.RefreshToken);
// refresh the tokens
var tokenResult = await tokenClient.RequestRefreshTokenAsync(currentRefreshToken);
if (!tokenResult.IsError)
{
// update the tokens & expiration value
// 将新返回的 accesstoken, idtoken, refreshtoken 存下来
var updatedTokens = new List<AuthenticationToken>();
updatedTokens.Add(new AuthenticationToken()
{
Name = OpenIdConnectParameterNames.IdToken,
Value = tokenResult.IdentityToken
});
updatedTokens.Add(new AuthenticationToken()
{
Name = OpenIdConnectParameterNames.AccessToken,
Value = tokenResult.AccessToken
});
updatedTokens.Add(new AuthenticationToken()
{
Name = OpenIdConnectParameterNames.RefreshToken,
Value = tokenResult.RefreshToken
});
// 存储更新的 access token 过期时间
var expiresAt = DateTime.UtcNow + TimeSpan.FromSeconds(tokenResult.ExpiresIn);
updatedTokens.Add(new AuthenticationToken()
{
Name = "expires_at",
Value = expiresAt.ToString("o",CultureInfo.InvariantCulture) // 格式类似:"2018-08-26T02:42:05.8636746Z"
});
// get autenticate result, containing the current principal & properties
// 获取 cookie 并更新
var currentAuthenticationResult = await currentContext.AuthenticateAsync("Cookies");
// store the updated tokens
currentAuthenticationResult.Properties.StoreTokens(updatedTokens);
// sign in
await currentContext.SignInAsync("Cookies", currentAuthenticationResult.Principal,
currentAuthenticationResult.Properties);
// return the new access token
return tokenResult.AccessToken;
}
else
{
//throw new Exception("Problem encountered while refreshing tokens", tokenResult.Exception);
return ""; // 不抛异常而是直接返回空值,会提示 AccessDenied 页面
}
}
}
}
如此,运行网站,等待超时时间后,会重新获取 token 而不是提示无权访问
github commit 点击这里查看