Identity Server4 基础应用(三)Authorization Code(Part II)

  在上一篇文章记录了Authorization Code授权方式的基本流程和使用后,还遗留了几个问题,怎么利用Access Token去访问api资源,Refresh Token怎么刷新Access Token,接着试着实现一个退出当前的登录的操作,并试试如何使用第三方登录。

访问Api资源

  前文我们访问的是授权服务器上的用户认证信息,及得到的都是用户的Claims数据,如果我们想通过MVC客户端去访问我们在第一篇文章中建立的WebApi的资源的话,由于这个资源也是被授权服务器保护的,所以在访问时我们也需要Access Token的。
  接下来试着在上文的MVC程序中新建一个Action去请求WebApi的资源,我们建立一个名为CallApi的Action,其中利用一个现成扩展方法获取已经在取得授权时存下的Access Token,随后请求WebApi。

 1 public async Task<IActionResult> CallApi()
 2 {
 3     //我们利用拓展方法获取存下来的Access Token
 4     var accessToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.AccessToken);
 5 
 6     var client = new HttpClient();
 7     //携带上AccessToken
 8     client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
 9     //去请求受保护的api1上的资源
10     var content = await client.GetStringAsync("http://localhost:5001/identity");
11     ViewBag.Json = JArray.Parse(content).ToString();
12     return View("Json");
13 }

接着建立一个简单的View来显示获取到的数据

1 <pre>@ViewBag.Json</pre>

紧接着我们要确定在授权服务器上我们为名为mvc的这个Client的AllowedScopes中是否添加了api1,同样的在MVC程序中的ConfigureServices中是否也添加了api1这个scope。

随后我们进行调试,运行程序后,经过登录等认证操作后自动的存下了AccessToken,随后我们请求CallApi得到WebApi返回的数据。

Refresh Token

  在前文最后请求Access Token时,从Fiddler捕获到的授权服务器的Response中看到,授权服务器返回给了MVC客户端的Tokens包含Access Token和Id Token。但是缺少了我们在介绍Authorization Code授权流程时说到的Refresh Token。我们需要修改下设置,确保在授权服务器要请求的client中将AllowOfflineAccess属性设置为True,并且在MVC程序中,在oidc配置中为scope添加offline_access。

 1 new Client
 2 {
 3     ClientId = "mvc",
 4     ClientName = "MVC Client",
 5     AllowedGrantTypes = GrantTypes.Code,
 6     RequirePkce = false,
 7     ClientSecrets = { new Secret("mvc secret".Sha256()) },
 8     RedirectUris = { "http://localhost:5002/signin-oidc" },
 9     FrontChannelLogoutUri = "http://localhost:5002/signout-oidc",
10     PostLogoutRedirectUris = { "http://localhost:5002/signout-callback-oidc" },
11     AlwaysIncludeUserClaimsInIdToken = true,
12     AllowOfflineAccess = true,    //若要使用Refresh Token,必须将这个属性置成true
13     AccessTokenLifetime = 60,     //设置Access Token的过期时间
14     AllowedScopes =
15     {
16         "api1",
17         //因为我们要请求的资源包含用户信息,所以在scope中需要包括上
18         IdentityServer4.IdentityServerConstants.StandardScopes.OpenId,
19         IdentityServer4.IdentityServerConstants.StandardScopes.Profile,
20         IdentityServer4.IdentityServerConstants.StandardScopes.Email,
21         IdentityServer4.IdentityServerConstants.StandardScopes.Phone,
22     }
23 }
 1 .AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options =>
 2 {
 3     options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
 4     options.Authority = "http://localhost:5000";    //授权服务器地址
 5     options.RequireHttpsMetadata = false;           //暂时不用https
 6     options.ClientId = "mvc";
 7     options.ClientSecret = "mvc secret";
 8     options.ResponseType = "code";  //代表Authorization Code
 9     options.SaveTokens = true;      //表示把获取的Token存到Cookie中
10     options.Scope.Add("api1");  //这个scope中对应了WebApi(API1)上的资源
11 
12     //*************新增,用以获取Refresh Code:*******************
13     options.Scope.Add("offline_access");
14     //***********************************************************
15 });

  接着我们修改下MVC中“HomeController/Index”中的代码,在这里用前面用过的扩展方法去获取这几个Token,包括RefreshToken。并在相应的View中将这几个值显示出来。当我们登录后便可以在浏览器中看到这几个Token了。

 1 public async Task<IActionResult> Index()
 2 {
 3     var accessToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.AccessToken);
 4     var idToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.IdToken);
 5     var refreshToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.RefreshToken);
 6 
 7     ViewData["accessToken"] = accessToken;
 8     ViewData["idToken"] = idToken;
 9     ViewData["refreshToken"] = refreshToken;
10     return View();
11 }

  在能获取到Refresh Token后要怎么利用呢,客户端再获取到AccessToken后是有一个默认的有效时间的,时长是3600秒。在AccessToken的有效时间到期后便会失效,这个时候用户就需要重新授权去获取新的AccessToken来使用。在有了Refresh Token后,客户端在发现AccessToken失效时就可以使用Refresh Token向授权服务器发送请求,便可以获取到新的Access Token。
  接下来我们撸码做实验,先在授权服务器上将Access Token过期时间修改的短一点,我们在clientId为mvc的Client中修改其属性。

1 AllowOfflineAccess = true,    //若要使用Refresh Token,必须将这个属性置成true
2 AccessTokenLifetime = 60,     //设置Access Token的过期时间为1分钟

还需要修改下WebApi的ConfigureServices中的配置

 1 services.AddAuthentication("Bearer")
 2     .AddJwtBearer("Bearer", options =>
 3     {
 4         options.Authority = "http://localhost:5000";  //这里指定授权服务器的地址
 5         options.RequireHttpsMetadata = false;       //暂时先不用https
 6         options.Audience = "api1";                  //关联到授权服务器上的api资源,住进“api1”这个门牌号里
 7         //************************新增*************************
 8         options.TokenValidationParameters.ClockSkew = TimeSpan.FromMinutes(1);  //每隔多长时间检查一次Token的有效性
 9         options.TokenValidationParameters.RequireExpirationTime = true;         //要求Access Token必须包含一个超时时间
10         //*****************************************************
11     });

  现在我们试着清除一下Cookie,重新发起请求,继续请求CallApi这个Action,能够获取到WebApi返回的数据。接着等待一段时间后再次访问,会看到返回了401状态码显示未授权,这就表示WebApi再次去验证Access Token时发现已经过期失效了。

为了解决这个问题,我们需要在后台代码中利用Refresh Token去获取新的AccessToken。并修改之前CallApi中的代码,当检测到授权无效时就会获取新的Token并随后再次发起请求。具体代码如下。在IdentityModel中为我们提供了扩展方法RequestRefreshTokenAsync利用RefreshToken来获取新的有效Tokens,并且我们将过期失效的Tokens和新申请的Tokens都打印出来对比一下。

 1 /// <summary>
 2 /// 获取新的AccessToken
 3 /// </summary>
 4 /// <returns></returns>
 5 private async Task<Dictionary<string, string>> RefreshAccessTokensAsync()
 6 {
 7     var client = new HttpClient();
 8     var disco = await client.GetDiscoveryDocumentAsync("http://localhost:5000");//需要引入“IdentityModel”
 9     if (disco.IsError)
10     {
11         throw new Exception(disco.Error);
12     }
13     var refreshToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.RefreshToken);
14 
15     // 获取新的Tokens
16     var tokenResult = await client.RequestRefreshTokenAsync(new RefreshTokenRequest
17     {
18         Address = disco.TokenEndpoint,
19         ClientId = "mvc",
20         ClientSecret = "mvc secret",
21         Scope = "api1 openid profile email phone address",
22         GrantType = OpenIdConnectGrantTypes.RefreshToken,
23         RefreshToken = refreshToken
24     });
25     if (tokenResult.IsError)
26     {
27         throw new Exception(tokenResult.Error);
28     }
29     var newTokens = new Dictionary<string, string>
30     {
31         {OpenIdConnectParameterNames.AccessToken, tokenResult.AccessToken},
32         {OpenIdConnectParameterNames.IdToken, tokenResult.IdentityToken},
33         {OpenIdConnectParameterNames.RefreshToken, tokenResult.RefreshToken},
34     };
35     var expiresAt = DateTime.UtcNow + TimeSpan.FromSeconds(tokenResult.ExpiresIn);
36     // 获取身份认证结果,主要有两个属性:
37     //(1)Properties:包含用到的所有Tokens
38     //(2)Principal:包含用户的Claims
39     AuthenticateResult info = await HttpContext.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme);
40     info.Properties.UpdateTokenValue("refresh_token", newTokens["refresh_token"]);
41     info.Properties.UpdateTokenValue("access_token", newTokens["access_token"]);
42     info.Properties.UpdateTokenValue("id_token", newTokens["id_token"]);
43     info.Properties.UpdateTokenValue("expires_at", expiresAt.ToString("o", CultureInfo.InvariantCulture));
44 
45     // 再次登录
46     await HttpContext.SignInAsync("Cookies", info.Principal, info.Properties);
47     return newTokens;
48 }

修改Action中的代码

 1 [Route(nameof(CallApi))]
 2 public async Task<IActionResult> CallApi()
 3 {
 4     //我们利用拓展方法获取存下来的Access Token
 5     var accessToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.AccessToken);
 6     var client = new HttpClient();
 7     //携带上AccessToken
 8     client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
 9     //去请求受保护的api1上的资源
10     //var content = await client.GetStringAsync("http://localhost:5001/identity");
11     var response = await client.GetAsync("http://localhost:5001/identity");
12     if (!response.IsSuccessStatusCode)
13     {
14         ViewBag.Json = response.ReasonPhrase;
15         if (response.StatusCode == HttpStatusCode.Unauthorized) //如果时授权无效,刷新AccessToken
16         {
17             Dictionary<string, string> newAccessTokens;
18             if (response.StatusCode == HttpStatusCode.Unauthorized)
19             {
20                 Dictionary<string, string> UnValidTokens = new Dictionary<string, string>
21                 {
22                     {
23                         OpenIdConnectParameterNames.AccessToken,
24                         await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.AccessToken)
25                     },
26                     {
27                         OpenIdConnectParameterNames.IdToken,
28                         await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.IdToken)
29                     },
30                     {
31                         OpenIdConnectParameterNames.RefreshToken,
32                         await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.RefreshToken)
33                     }
34                 };
35                 newAccessTokens = await RefreshAccessTokensAsync(); //使用RefreshToken去获取新的AccessToken
36                 Debug.WriteLine(JsonConvert.SerializeObject(UnValidTokens));
37                 Debug.WriteLine(JsonConvert.SerializeObject(newAccessTokens));
38                 return RedirectToAction(); //再次请求这个Action
39             }
40         }
41     }
42     var content = await response.Content.ReadAsStringAsync();
43     ViewBag.Json = JArray.Parse(content).ToString();
44     return View("Json");
45 }

可以看到debug输出了无效的Tokens和新取得的Tokens。容易发现这两个Token中的具体值差异其实不大,我们可以在https://jwt.io/将Token进行解码,可以看到两此Token的授权有效时间是不一样的。

增加登出功能

添加Logout的Action,并在View中添加一个链接指向这个Action

1 public IActionResult Logout()
2 {
3     return SignOut(CookieAuthenticationDefaults.AuthenticationScheme
4         , OpenIdConnectDefaults.AuthenticationScheme);
5 }

来到QuickStart/Account/AccountOptions.cs,设置一下登出后跳转到登录界面。

1 public static bool AutomaticRedirectAfterSignOut = true;    //登出后可直接跳转到登录界面

当我们点击页面的Logout后,完成登出,紧接着跳转到登录界面

 

增加第三方登录

这里我们参照官网的例子,添加通过Google登录的功能

访问https://console.developers.google.com,创建一个Google+ API并启动它。

创建OAuth凭据

在创建凭据的时候我们将重定向的Url设置成“http://localhost:5000/signin-google”

完成之后我们就可以得到Id(Client Id)和密钥(Client Secret)

在ConfigureServices方法中继续追加下面的代码,将Id和密钥贴到相应的位置

1 services.AddAuthentication().AddGoogle("Google", options =>
2     {
3         options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
4         options.ClientId = "<insert here>";
5         options.ClientSecret = "<insert here>";
6     });

完成以上配置后和原来一样启动程序,在登陆界面选择“Google登录”就可以完成登录啦!

参考:

posted @ 2020-03-15 16:04  冬瓜山总教练  阅读(355)  评论(0编辑  收藏  举报