06 Authorization Code Flow 实例
原文:https://www.yuque.com/yuejiangliu/dotnet/ac0xok
06 Authorization Code Flow 实例.mp4 (137.6 MB)
Adding User Authentication with OpenID Connect 中的解释。最后在 HomeController 上标注 [Authorize]
以保护 HomeController。
2、添加 MVC Client
打开 Idp 项目,添加 MVC Client:
// MVC client, authorization code new Client { ClientId = "mvc client", ClientName = "ASP.NET Core MVC Client", AllowedGrantTypes = GrantTypes.CodeAndClientCredentials, ClientSecrets = { new Secret("mvc secret".Sha256()) }, RedirectUris = { "http://localhost:5002/signin-oidc" }, FrontChannelLogoutUri = "http://localhost:5002/signout-oidc", PostLogoutRedirectUris = { "http://localhost:5002/signout-callback-oidc" }, // 总是在 Id Token 里面包含所有 User Claims 信息 AlwaysIncludeUserClaimsInIdToken = true, // 设为 True 即支持 Refresh Token AllowOfflineAccess = true, // offline_access AllowedScopes = { "api1", IdentityServerConstants.StandardScopes.OpenId, IdentityServerConstants.StandardScopes.Email, IdentityServerConstants.StandardScopes.Address, IdentityServerConstants.StandardScopes.Phone, IdentityServerConstants.StandardScopes.Profile } },
3、运行项目
依次启动 Idp、Api1 和 MVC Client 三个项目。
由于 MVC Client 的 HomeController 被保护了,所以启动项目后会自动跳转到授权服务器进行身份认证。
身份认证完毕后跳转到 grant 界面:
同意后回到 MVC Client 的 Home 界面:
4、在 MVC Client 中获取各个 Token
修改 HomeController 的 Privacy 方法,获取各个 Token:
public async Task<IActionResult> Privacy() { var accessToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.AccessToken); var idToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.IdToken); var refreshToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.RefreshToken); return View(); }
注:修改 MVC Client 后每次重新调试记得手动清除浏览器 Cookie。
通过 Preserve log 查看 Authorization Token:
5、通过 Fiddler 监视到两个往返
点下 Yes, Allow 的瞬间,可以通过 Fiddler 监视到两个往返。
1 - 身份认证请求(浏览器与授权服务器间通讯):
HTTP/1.1 302 Found Location: https://server.example.com/authorize? response _type=code &scope=openid%20profile&20email &client_id=s6BhdRkqt3 &state=af0ifjsldkj &redirect_uri=https$3A%2F%2Fclient.example.org%2Fcb
1 - 身份认证请求的响应:
HTTP/1.1 302 Found Location: https://client.example.org/cb? code=Splx10BeZQQYbYS6WxSbIA &state=af0ifjsldkj
2 - Token 请求(客户端服务器与授权服务器间通讯):
POST /token HTTP/1.1 Host: server.example.com Content-Iype: application/x-www-form-urlencoded Authorization: Basic czzCaGRSa3FOMzpniDFmQmFOM2JW grant_type=authorization_code &code=Splx10BeZQQYbYS6WxSbIA &redirect_uri=https%3A%2F%2Fclient.example.org%2Fcb
2 - Token 请求的响应:
HTTP/1.1 200 OK Content-Type: application/json Cache-Control: no-store Pragma: no-cache { "access_token": "S1AV32hkKG", "token_type": "Bearer", "refresh_token": "8xLOxBtZp8", "expires_in": 3600, "id_token": "eyJhbGci0iJSUzI1NiIsImtpZCI6IjF10WdkazcifQ.ewogImlzc yI6ICJodHRwOi8vc2VyimVyLmV4YW1wbGUuY29tIiwKICJzdWIi0iAiM)Q4Mjg5 NzYXMDAXIiwKICJh/Qi0iAiczZCaGRSa3FOMyIsCiAibm9uY2Ui0iAibi0wUz2 fV3pBMK1qIiwKICJLeHAi01AXMzExM)gxOTcwLAogImlhdCI6IDEzMTEyODA5Nz AKEQ.ggwehZ1EuVLuxNuuIJKX V8a OMXzROEHR9R6jgdqrOOF4daGU96Sr P6g Jp6IcmD3HP990bi1PR3-cwh3LO-p146waJ8IhehcwL7F09JdijmBqkvPeB2T9CU NgeGpe-gccMg4vfKjkMBFcGvnzZUN4 _KSP0aAp1tOJ1zZwgjxqGByKHiOtX7TpG QyHE51cMiKPXEEIQILVqOpC E2DzL7emopioaoZTF mO NOYzFC6g6EJbOEoRoS K5hoDalrcvRYLSrQAZZKf1yuVCyixEoV9GfNQC3_o3jzM2PAithfubEEBLuVWk4 XUVrWOLrL10nx7RkKUSNXNHg-rvMzgg" }
6、展示 Token
修改 HomeController 的 Privacy 方法,展示 Token:
public async Task<IActionResult> Privacy() { var accessToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.AccessToken); var idToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.IdToken); var refreshToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.RefreshToken); ViewData["accessToken"] = accessToken; ViewData["idToken"] = idToken; ViewData["refreshToken"] = refreshToken; return View(); }
Privacy 的 View:
@{ ViewData["Title"] = "Privacy Policy"; } <h1>@ViewData["Title"]h1> <h2>Access Token:h2> <p>@ViewData["accessToken"]p> <h2>Id Token:h2> <p>@ViewData["idToken"]p> <h2>Refresh Token:h2> <p>@ViewData["refreshToken"]p> <dl> @foreach (var claim in User.Claims) { <dt>@claim.Typedt> <dd>@claim.Valuedd> } dl>
7、使用 Access Token 访问 Api1 的资源
先修改 MVC Client HomeController 的 Index Action:
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) throw new Exception(response.ReasonPhrase); var content = await response.Content.ReadAsStringAsync(); return View("Index", content); }
再修改 Index View:
@model string @{ ViewData["Title"] = "Home Page"; } <div class="text-center"> <h1 class="display-4">Api1 Resource Respose:h1> <p>@Modelp> div>
效果:
8、单点登录
用户登录后,再打开授权服务器可以看到 alice 处于登录状态(即会话被 Idp 项目保持了)。
此时如果有其他 Web 应用请求授权,跳转到授权服务器后会发现 alice 已经登录了,它就无需登录直接跳转回去。整个过程就相当于是一个单点登录。
9、登出
登出时既要登出 MVC 客户端,又要登出 IdentityServer4 用户会话。
MVC Client _Layout 中添加 Logout 链接:
<ul class="navbar-nav flex-grow-1"> <li class="nav-item"> <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Homea> li> <li class="nav-item"> <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Privacy">Privacya> li> @if (User.Identity.IsAuthenticated) { <li class="nav-item"> <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Logout">Logouta> li> } ul>
Logout Action:
public async Task Logout() { // 登出当前网站的 Cookie await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); // 登出 IdentityServer4 的 Cookie await HttpContext.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme); }
通过设置 Idp 项目 Quickstart\Account\AccountOptions.cs 的 AutomaticRedirectAfterSignOut = true;
可以在登出后自动跳转回客户端。