zoukankan      html  css  js  c++  java
  • 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登录”就可以完成登录啦!

    参考:

  • 相关阅读:
    iOS进阶二-KVC
    iOS进阶一OC对象的本质
    2019-01-19
    2019-01-12
    2019
    2018-12-23 随笔
    2018-12-18 随笔
    2018-12-10
    2018-12-01
    2018.11.23 随笔
  • 原文地址:https://www.cnblogs.com/xhy0826/p/12498097.html
Copyright © 2011-2022 走看看