07 为 MVC 客户端刷新 Token.mp4

本节是上节的补充,主要讲解如何使用 Refresh Token 刷新 Access Token。

设置并启用过期时间

打开 Idp 项目,修改 MVC Client 的 AccessTokenLifetime 为 60s:

  1. // MVC client, authorization code
  2. new Client
  3. {
  4. ...
  5. // 设为 True 即支持 Refresh Token
  6. AllowOfflineAccess = true, // offline_access
  7. AccessTokenLifetime = 60, // 60 seconds
  8. AllowedScopes =
  9. {
  10. ...
  11. }
  12. },

通过 jwt.io 能够看到过期时间设置在 Token 里面了。

image.png

结果发现 exp 都过了还能从 Api1 获得资源。

实际上是 Api1 中没有及时的验证 Token(默认为 300s 验证一次)。

修改 Api1,Token 验证间隔为 1 分钟,且 Token 必须包含过期时间:

  1. services.AddAuthentication("Bearer")
  2. .AddJwtBearer("Bearer", options =>
  3. {
  4. options.Authority = "http://localhost:5000";
  5. options.RequireHttpsMetadata = false;
  6. options.Audience = "api1";
  7. options.TokenValidationParameters.ClockSkew = TimeSpan.FromMinutes(1);
  8. options.TokenValidationParameters.RequireExpirationTime = true;
  9. });

效果:过期后报错 Exception: Unauthorized

image.png

Refresh Token

参考 OpenID Connect 协议 构造 RefreshTokenRequest:

image.png

在 MVC Client 的 HomeController 中添加刷新 Token 的方法:

  1. private async Task<string> RenewTokenAsync()
  2. {
  3. var client = new HttpClient();
  4. var disco = await client.GetDiscoveryDocumentAsync("http://localhost:5000/");
  5. if (disco.IsError) throw new Exception(disco.Error);
  6. var refreshToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.RefreshToken);
  7. // Refresh Access Token
  8. var tokenResponse = await client.RequestRefreshTokenAsync(new RefreshTokenRequest
  9. {
  10. Address = disco.TokenEndpoint,
  11. ClientId = "mvc client",
  12. ClientSecret = "mvc secret",
  13. Scope = "api1 openid profile email phone address",
  14. GrantType = OpenIdConnectGrantTypes.RefreshToken,
  15. RefreshToken = refreshToken
  16. });
  17. if (tokenResponse.IsError) throw new Exception(tokenResponse.Error);
  18. var expiresAt = DateTime.UtcNow + TimeSpan.FromSeconds(tokenResponse.ExpiresIn);
  19. var tokens = new[]
  20. {
  21. new AuthenticationToken
  22. {
  23. Name = OpenIdConnectParameterNames.IdToken,
  24. Value = tokenResponse.IdentityToken
  25. },
  26. new AuthenticationToken
  27. {
  28. Name = OpenIdConnectParameterNames.AccessToken,
  29. Value = tokenResponse.AccessToken
  30. },
  31. new AuthenticationToken
  32. {
  33. Name = OpenIdConnectParameterNames.RefreshToken,
  34. Value = tokenResponse.RefreshToken
  35. },
  36. new AuthenticationToken
  37. {
  38. Name = "expires_at",
  39. Value = expiresAt.ToString("o", CultureInfo.InvariantCulture)
  40. }
  41. };
  42. // 获取身份认证的结果,包含当前的 Principal 和 Properties
  43. var currentAuthenticateResult =
  44. await HttpContext.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme);
  45. // 更新 Cookie 里面的 Token
  46. currentAuthenticateResult.Properties.StoreTokens(tokens);
  47. // 登录
  48. await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme,
  49. currentAuthenticateResult.Principal, currentAuthenticateResult.Properties);
  50. return tokenResponse.AccessToken;
  51. }

ToString(“o”):image.png

在 Index Action 中刷新 Token:

  1. public async Task<IActionResult> Index()
  2. {
  3. var client = new HttpClient();
  4. var disco = await client.GetDiscoveryDocumentAsync("http://localhost:5000/");
  5. if (disco.IsError) throw new Exception(disco.Error);
  6. var accessToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.AccessToken);
  7. client.SetBearerToken(accessToken);
  8. var response = await client.GetAsync("http://localhost:5001/identity");
  9. if (!response.IsSuccessStatusCode)
  10. {
  11. if (response.StatusCode == HttpStatusCode.Unauthorized)
  12. {
  13. // 这样写仅为了方便演示
  14. await RenewTokenAsync();
  15. return RedirectToAction();
  16. }
  17. throw new Exception(response.ReasonPhrase);
  18. }
  19. var content = await response.Content.ReadAsStringAsync();
  20. return View("Index", content);
  21. }

注:IdentityServer4.Samples 项目没有了,推荐参考官方文档 Switching to Hybrid Flow and adding API Access back 中的代码。