本节是上节的补充,主要讲解如何使用 Refresh Token 刷新 Access Token。
设置并启用过期时间
打开 Idp 项目,修改 MVC Client 的 AccessTokenLifetime 为 60s:
// MVC client, authorization code
new Client
{
...
// 设为 True 即支持 Refresh Token
AllowOfflineAccess = true, // offline_access
AccessTokenLifetime = 60, // 60 seconds
AllowedScopes =
{
...
}
},
通过 jwt.io 能够看到过期时间设置在 Token 里面了。
结果发现 exp 都过了还能从 Api1 获得资源。
实际上是 Api1 中没有及时的验证 Token(默认为 300s 验证一次)。
修改 Api1,Token 验证间隔为 1 分钟,且 Token 必须包含过期时间:
services.AddAuthentication("Bearer")
.AddJwtBearer("Bearer", options =>
{
options.Authority = "http://localhost:5000";
options.RequireHttpsMetadata = false;
options.Audience = "api1";
options.TokenValidationParameters.ClockSkew = TimeSpan.FromMinutes(1);
options.TokenValidationParameters.RequireExpirationTime = true;
});
效果:过期后报错 Exception: Unauthorized
Refresh Token
参考 OpenID Connect 协议 构造 RefreshTokenRequest:
在 MVC Client 的 HomeController 中添加刷新 Token 的方法:
private async Task<string> RenewTokenAsync()
{
var client = new HttpClient();
var disco = await client.GetDiscoveryDocumentAsync("http://localhost:5000/");
if (disco.IsError) throw new Exception(disco.Error);
var refreshToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.RefreshToken);
// Refresh Access Token
var tokenResponse = await client.RequestRefreshTokenAsync(new RefreshTokenRequest
{
Address = disco.TokenEndpoint,
ClientId = "mvc client",
ClientSecret = "mvc secret",
Scope = "api1 openid profile email phone address",
GrantType = OpenIdConnectGrantTypes.RefreshToken,
RefreshToken = refreshToken
});
if (tokenResponse.IsError) throw new Exception(tokenResponse.Error);
var expiresAt = DateTime.UtcNow + TimeSpan.FromSeconds(tokenResponse.ExpiresIn);
var tokens = new[]
{
new AuthenticationToken
{
Name = OpenIdConnectParameterNames.IdToken,
Value = tokenResponse.IdentityToken
},
new AuthenticationToken
{
Name = OpenIdConnectParameterNames.AccessToken,
Value = tokenResponse.AccessToken
},
new AuthenticationToken
{
Name = OpenIdConnectParameterNames.RefreshToken,
Value = tokenResponse.RefreshToken
},
new AuthenticationToken
{
Name = "expires_at",
Value = expiresAt.ToString("o", CultureInfo.InvariantCulture)
}
};
// 获取身份认证的结果,包含当前的 Principal 和 Properties
var currentAuthenticateResult =
await HttpContext.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme);
// 更新 Cookie 里面的 Token
currentAuthenticateResult.Properties.StoreTokens(tokens);
// 登录
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme,
currentAuthenticateResult.Principal, currentAuthenticateResult.Properties);
return tokenResponse.AccessToken;
}
ToString(“o”):
在 Index Action 中刷新 Token:
public async Task<IActionResult> Index()
{
var client = new HttpClient();
var disco = await client.GetDiscoveryDocumentAsync("http://localhost:5000/");
if (disco.IsError) throw new Exception(disco.Error);
var accessToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.AccessToken);
client.SetBearerToken(accessToken);
var response = await client.GetAsync("http://localhost:5001/identity");
if (!response.IsSuccessStatusCode)
{
if (response.StatusCode == HttpStatusCode.Unauthorized)
{
// 这样写仅为了方便演示
await RenewTokenAsync();
return RedirectToAction();
}
throw new Exception(response.ReasonPhrase);
}
var content = await response.Content.ReadAsStringAsync();
return View("Index", content);
}
注:IdentityServer4.Samples 项目没有了,推荐参考官方文档 Switching to Hybrid Flow and adding API Access back 中的代码。