本节是上节的补充,主要讲解如何使用 Refresh Token 刷新 Access Token。
设置并启用过期时间
打开 Idp 项目,修改 MVC Client 的 AccessTokenLifetime 为 60s:
// MVC client, authorization codenew Client{...// 设为 True 即支持 Refresh TokenAllowOfflineAccess = true, // offline_accessAccessTokenLifetime = 60, // 60 secondsAllowedScopes ={...}},
通过 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 Tokenvar 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 和 Propertiesvar currentAuthenticateResult =await HttpContext.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme);// 更新 Cookie 里面的 TokencurrentAuthenticateResult.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 中的代码。
