06 Authorization Code Flow 实例.mp4
使用 Authorization Code Flow 保护 ASP.NET Core MVC 客户端(为其做用户的身份认证),并访问被保护资源。
简单说就是 MVC 做客户端,IdentityServer4 做身份认证和授权。
OAuth 2.0 vs OpenID Connect

OAuth 2.0 - Authorization Code Grant

流程按字母先后顺序执行。
OpenlD Connect - Authorization Code Flow

主要差别就是除了 Access Token,客户端还能从授权服务器获得 Id Token,进而通过它获得最终用户的相关信息。
- D 通过前端浏览器的重定向完成
- E 通过后端服务器间的通讯完成
Authorization Code
- 适用于机密客户端(Confidential Client)
- 服务器端的 Web 应用
- 对用户和客户端进行身份认证
客户端类型:

Authorization Code 实战
新建 MVC 项目

修改 launchSettings.json 把端口改为 5002:
{"profiles": {"MvcClient": {"commandName": "Project","launchBrowser": true,"applicationUrl": "http://localhost:5002","environmentVariables": {"ASPNETCORE_ENVIRONMENT": "Development"}}}}
安装 IdentityModel NuGet 包。
在 Startup 里面注册并启用身份认证中间件:
public void ConfigureServices(IServiceCollection services){...services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);// 关闭 JWT Claim 映射JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();// 注册身份认证中间件services.AddAuthentication(options =>{// CookieAuthenticationDefaults.AuthenticationScheme == "Cookies"options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;// OpenIdConnectDefaults.AuthenticationScheme == "OpenIdConnect"options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;}).AddCookie(CookieAuthenticationDefaults.AuthenticationScheme).AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options =>{options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;options.Authority = "http://localhost:5000";options.RequireHttpsMetadata = false;options.ClientId = "mvc client";options.ClientSecret = "mvc secret";options.SaveTokens = true;options.ResponseType = "code";options.Scope.Clear();options.Scope.Add("api1");options.Scope.Add(OidcConstants.StandardScopes.OpenId);options.Scope.Add(OidcConstants.StandardScopes.Profile);options.Scope.Add(OidcConstants.StandardScopes.Email);options.Scope.Add(OidcConstants.StandardScopes.Phone);options.Scope.Add(OidcConstants.StandardScopes.Address);// 必须添加 OfflineAccess 才能获取到 Refresh Tokenoptions.Scope.Add(OidcConstants.StandardScopes.OfflineAccess);});}public void Configure(IApplicationBuilder app, IHostingEnvironment env){if (env.IsDevelopment()){app.UseDeveloperExceptionPage();}else{app.UseExceptionHandler("/Home/Error");}// 要在添加 MVC 中间件之前添加 Authentication 到管道app.UseAuthentication();app.UseStaticFiles();...}
这些代码的具体作用可以参考官方文档 Adding User Authentication with OpenID Connect 中的解释。
最后在 HomeController 上标注 [Authorize] 以保护 HomeController。
添加 MVC Client
打开 Idp 项目,添加 MVC Client:
// MVC client, authorization codenew 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 TokenAllowOfflineAccess = true, // offline_accessAllowedScopes ={"api1",IdentityServerConstants.StandardScopes.OpenId,IdentityServerConstants.StandardScopes.Email,IdentityServerConstants.StandardScopes.Address,IdentityServerConstants.StandardScopes.Phone,IdentityServerConstants.StandardScopes.Profile}},
运行项目
依次启动 Idp、Api1 和 MVC Client 三个项目。
由于 MVC Client 的 HomeController 被保护了,所以启动项目后会自动跳转到授权服务器进行身份认证。

身份认证完毕后跳转到 grant 界面:

同意后回到 MVC Client 的 Home 界面:

在 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:

通过 Fiddler 监视到两个往返

点下 Yes, Allow 的瞬间,可以通过 Fiddler 监视到两个往返。

1 - 身份认证请求(浏览器与授权服务器间通讯):
HTTP/1.1 302 FoundLocation: 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 FoundLocation: https://client.example.org/cb?code=Splx10BeZQQYbYS6WxSbIA&state=af0ifjsldkj
2 - Token 请求(客户端服务器与授权服务器间通讯):
POST /token HTTP/1.1Host: server.example.comContent-Iype: application/x-www-form-urlencodedAuthorization: Basic czzCaGRSa3FOMzpniDFmQmFOM2JWgrant_type=authorization_code&code=Splx10BeZQQYbYS6WxSbIA&redirect_uri=https%3A%2F%2Fclient.example.org%2Fcb
2 - Token 请求的响应:
HTTP/1.1 200 OKContent-Type: application/jsonCache-Control: no-storePragma: no-cache{"access_token": "S1AV32hkKG","token_type": "Bearer","refresh_token": "8xLOxBtZp8","expires_in": 3600,"id_token": "eyJhbGci0iJSUzI1NiIsImtpZCI6IjF10WdkazcifQ.ewogImlzcyI6ICJodHRwOi8vc2VyimVyLmV4YW1wbGUuY29tIiwKICJzdWIi0iAiM)Q4Mjg5NzYXMDAXIiwKICJh/Qi0iAiczZCaGRSa3FOMyIsCiAibm9uY2Ui0iAibi0wUz2fV3pBMK1qIiwKICJLeHAi01AXMzExM)gxOTcwLAogImlhdCI6IDEzMTEyODA5NzAKEQ.ggwehZ1EuVLuxNuuIJKX V8a OMXzROEHR9R6jgdqrOOF4daGU96Sr P6gJp6IcmD3HP990bi1PR3-cwh3LO-p146waJ8IhehcwL7F09JdijmBqkvPeB2T9CUNgeGpe-gccMg4vfKjkMBFcGvnzZUN4 _KSP0aAp1tOJ1zZwgjxqGByKHiOtX7TpGQyHE51cMiKPXEEIQILVqOpC E2DzL7emopioaoZTF mO NOYzFC6g6EJbOEoRoSK5hoDalrcvRYLSrQAZZKf1yuVCyixEoV9GfNQC3_o3jzM2PAithfubEEBLuVWk4XUVrWOLrL10nx7RkKUSNXNHg-rvMzgg"}
展示 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.Type</dt><dd>@claim.Value</dd>}</dl>

使用 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>@Model</p></div>
效果:
单点登录
用户登录后,再打开授权服务器可以看到 alice 处于登录状态(即会话被 Idp 项目保持了)。
此时如果有其他 Web 应用请求授权,跳转到授权服务器后会发现 alice 已经登录了,它就无需登录直接跳转回去。整个过程就相当于是一个单点登录。

登出
登出时既要登出 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">Home</a></li><li class="nav-item"><a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a></li>@if (User.Identity.IsAuthenticated){<li class="nav-item"><a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Logout">Logout</a></li>}</ul>
Logout Action:
public async Task Logout(){// 登出当前网站的 Cookieawait HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);// 登出 IdentityServer4 的 Cookieawait HttpContext.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme);}
通过设置 Idp 项目 Quickstart\Account\AccountOptions.cs 的 AutomaticRedirectAfterSignOut = true; 可以在登出后自动跳转回客户端。
