配套源码:https://gitee.com/jardeng/IdentitySolution
本篇将创建使用[Code-授权码]授权模式的客户端,来对受保护的API资源进行访问。
1、接上一篇项目,因为之前创建IdentityServer认证服务器没有使用IdentityServer4提供的模板,在Code授权码模式就没有进行登录、授权的界面,所以重新创建一下IdentityServer项目。
重新使用IdentityServer4模板 - is4inmem创建项目。
将之前IdentityServer认证服务器Config.cs复制到新建的IdentityServer服务器即可,最后的IdentityServer认证服务器项目结构为:
然后在IdentityServer项目Config.cs中添加一个返回身份资源的方法
然后在IdentityServer项目Config.cs中添加一个客户端
注意:localhost:6001指的是我们将要创建的MVC客户端的项目地址,并非IdentityServer认证服务器的地址
/// 授权码模式(Code) /// 适用于保密客户端(Confidential Client),比如ASP.NET MVC等服务器端渲染的Web应用 new Client { ClientId = "mvc client", ClientName = "ASP.NET Core MVC Client", AllowedGrantTypes = GrantTypes.Code, ClientSecrets = { new Secret("mvc secret".Sha256()) }, RedirectUris = { "http://localhost:6001/signin-oidc" }, FrontChannelLogoutUri = "http://localhost:6001/signout-oidc", PostLogoutRedirectUris = { "http://localhost:6001/signout-callback-oidc" }, AlwaysIncludeUserClaimsInIdToken = true, AllowOfflineAccess = true, AllowedScopes = { "api1", IdentityServerConstants.StandardScopes.OpenId, IdentityServerConstants.StandardScopes.Profile, IdentityServerConstants.StandardScopes.Email, IdentityServerConstants.StandardScopes.Address, IdentityServerConstants.StandardScopes.Phone } }
其中,RedirectUris的signin-oidc / FrontChannelLogoutUri的signout-oidc / PostLogoutRedirectUris的signout-callback-oidc,都是固定的地址写法。
完整的Config.cs代码:
using IdentityModel; using IdentityServer4; using IdentityServer4.Models; using IdentityServer4.Test; using System.Collections.Generic; using System.Security.Claims; namespace IdentityServer { /// <summary> /// IdentityServer资源和客户端配置文件 /// </summary> public static class Config { /// <summary> /// 身份资源集合 /// </summary> public static IEnumerable<IdentityResource> Ids => new IdentityResource[] { new IdentityResources.OpenId(), new IdentityResources.Profile(), new IdentityResources.Email(), new IdentityResources.Address(), new IdentityResources.Phone() }; /// <summary> /// API资源集合 /// 如果您将在生产环境中使用此功能,那么给您的API取一个逻辑名称就很重要。 /// 开发人员将使用它通过身份服务器连接到您的api。 /// 它应该以简单的方式向开发人员和用户描述您的api。 /// </summary> public static IEnumerable<ApiResource> Apis => new List<ApiResource> { new ApiResource("api1", "My API") }; /// <summary> /// 客户端集合 /// </summary> public static IEnumerable<Client> Clients => new Client[] { /// 客户端模式(Client Credentials) /// 可以将ClientId和ClientSecret视为应用程序本身的登录名和密码。 /// 它将您的应用程序标识到身份服务器,以便它知道哪个应用程序正在尝试与其连接。 new Client { //客户端标识 ClientId = "client", //没有交互用户,使用clientid/secret进行身份验证,适用于和用户无关,机器与机器之间直接交互访问资源的场景。 AllowedGrantTypes = GrantTypes.ClientCredentials, //认证密钥 ClientSecrets = { new Secret("secret".Sha256()) }, //客户端有权访问的作用域 AllowedScopes = { "api1" } }, /// 资源所有者密码凭证(ResourceOwnerPassword) /// Resource Owner其实就是User,所以可以直译为用户名密码模式。 /// 密码模式相较于客户端凭证模式,多了一个参与者,就是User。 /// 通过User的用户名和密码向Identity Server申请访问令牌。 new Client { ClientId = "client1", AllowedGrantTypes = GrantTypes.ResourceOwnerPassword, ClientSecrets = { new Secret("secret".Sha256()) }, AllowedScopes = { "api1" } }, /// 授权码模式(Code) /// 适用于保密客户端(Confidential Client),比如ASP.NET MVC等服务器端渲染的Web应用 new Client { ClientId = "mvc client", ClientName = "ASP.NET Core MVC Client", AllowedGrantTypes = GrantTypes.Code, ClientSecrets = { new Secret("mvc secret".Sha256()) }, RedirectUris = { "http://localhost:6001/signin-oidc" }, FrontChannelLogoutUri = "http://localhost:6001/signout-oidc", PostLogoutRedirectUris = { "http://localhost:6001/signout-callback-oidc" }, AlwaysIncludeUserClaimsInIdToken = true, AllowOfflineAccess = true, AllowedScopes = { "api1", IdentityServerConstants.StandardScopes.OpenId, IdentityServerConstants.StandardScopes.Profile, IdentityServerConstants.StandardScopes.Email, IdentityServerConstants.StandardScopes.Address, IdentityServerConstants.StandardScopes.Phone } } }; /// <summary> /// 用户集合 /// </summary> public static List<TestUser> Users => new List<TestUser> { new TestUser{SubjectId = "818727", Username = "alice", Password = "alice", Claims = { new Claim(JwtClaimTypes.Name, "Alice Smith"), new Claim(JwtClaimTypes.GivenName, "Alice"), new Claim(JwtClaimTypes.FamilyName, "Smith"), new Claim(JwtClaimTypes.Email, "[email protected]"), new Claim(JwtClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean), new Claim(JwtClaimTypes.WebSite, "http://alice.com"), new Claim(JwtClaimTypes.Address, @"{ 'street_address': 'One Hacker Way', 'locality': 'Heidelberg', 'postal_code': 69118, 'country': 'Germany' }", IdentityServer4.IdentityServerConstants.ClaimValueTypes.Json) } }, new TestUser{SubjectId = "88421113", Username = "bob", Password = "bob", Claims = { new Claim(JwtClaimTypes.Name, "Bob Smith"), new Claim(JwtClaimTypes.GivenName, "Bob"), new Claim(JwtClaimTypes.FamilyName, "Smith"), new Claim(JwtClaimTypes.Email, "[email protected]"), new Claim(JwtClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean), new Claim(JwtClaimTypes.WebSite, "http://bob.com"), new Claim(JwtClaimTypes.Address, @"{ 'street_address': 'One Hacker Way', 'locality': 'Heidelberg', 'postal_code': 69118, 'country': 'Germany' }", IdentityServer4.IdentityServerConstants.ClaimValueTypes.Json), new Claim("location", "somewhere") } } }; } }
2、创建一个名为 CodeMvcApp 的ASP.NET Core MVC客户端应用。
选择Web 应用程序(模型视图控制器)模板
创建完成后的项目截图
3、添加nuget包:IdentityServer4、IdentityModel、System.IdentityModel.Tokens.Jwt
4、配置MVC客户端
> Config.cs的ConfigureServices方法:
public void ConfigureServices(IServiceCollection services) { services.AddControllersWithViews(); JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); services.AddAuthentication(options => { options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; 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); options.Scope.Add(OidcConstants.StandardScopes.OfflineAccess); }); }
> Config.cs的Configure方法:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } else { app.UseExceptionHandler("/Home/Error"); } app.UseStaticFiles(); app.UseRouting(); app.UseAuthentication(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllerRoute( name: "default", pattern: "{controller=Home}/{action=Index}/{id?}"); }); }
给HomeController控制器加上[Authorize]特性
IdentityServer认证服务器需要在开发环境才能出现首页,所以另外打开这个项目并启动。
再启动CodeMvcApp项目
我们看到MVC客户端默认跳转到了localhost:5000(IdentityServer认证服务器)的登录页(Account/Login),因为MVC客户端默认启动的是Home/Index,且Home控制器已被标记Authorize特性,需要登录才能访问
使用 alice / alice 进行登录,进入到了IdentityServer认证服务器的授权页面(consent),点击Yes, Allow
进入到了MVC客户端首页
我们打开IdentityServer认证服务器地址:http://localhost:5000
可以看到IdentityServer认证服务器显示了当前的登录用户,此时点击用户名可以显示出Logout登出按钮,点击登出即可完成注销登录
5、获取accecc_token并访问受保护API资源,修改HomeController的Index方法
public async Task<IActionResult> Index() { HttpClient client = new HttpClient(); DiscoveryDocumentResponse disco = await client.GetDiscoveryDocumentAsync("http://localhost:5000/"); if (disco.IsError) { throw new Exception(disco.Error); } string accessToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.AccessToken); client.SetBearerToken(accessToken); HttpResponseMessage response = await client.GetAsync("http://localhost:6000/WeatherForecast"); if (!response.IsSuccessStatusCode) { throw new Exception(response.ReasonPhrase); } string content = await response.Content.ReadAsStringAsync(); return View("Index", content); }
命名空间
using System; using System.Diagnostics; using System.Net.Http; using System.Threading.Tasks; using CodeMvcApp.Models; using IdentityModel.Client; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using Microsoft.IdentityModel.Protocols.OpenIdConnect;
修改Index.cshtml来显示访问API的结果
@{ ViewData["Title"] = "Home Page"; } @model string <div class="text-center"> <h1 class="display-4">Welcome</h1> <p>Learn about <a href="https://docs.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p> </div> <p>@Model</p>
重新启动MVC客户端,成功获取access_token,并使用access_tokem访问受保护的API资源
6、显示登录的用户,并实现登出
修改Views/Shared/_Layout.cshtml,增加当前登录用户名称和登出按钮的显示
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>@ViewData["Title"] - CodeMvcApp</title> <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" /> <link rel="stylesheet" href="~/css/site.css" /> </head> <body> <header> <nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3"> <div class="container"> <a class="navbar-brand" asp-area="" asp-controller="Home" asp-action="Index">CodeMvcApp</a> <button class="navbar-toggler" type="button" data-toggle="collapse" data-target=".navbar-collapse" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation"> <span class="navbar-toggler-icon"></span> </button> <div class="navbar-collapse collapse d-sm-inline-flex flex-sm-row-reverse"> <ul class="navbar-nav flex-grow-1" style="position: relative;"> <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" style="position: absolute; right: 0;"> <span>Welcome,@User.Claims.FirstOrDefault(x => x.Type.Equals("given_name")).Value</span> <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Logout" style="display: inline-block;">Logout</a> </li> } </ul> </div> </div> </nav> </header> <div class="container"> <main role="main" class="pb-3"> @RenderBody() </main> </div> <footer class="border-top footer text-muted"> <div class="container"> © 2020 - CodeMvcApp - <a asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a> </div> </footer> <script src="~/lib/jquery/dist/jquery.min.js"></script> <script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script> <script src="~/js/site.js" asp-append-version="true"></script> @RenderSection("Scripts", required: false) </body> </html>
修改HomeController,增加Logout方法
using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication.OpenIdConnect; public async Task Logout() { await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); await HttpContext.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme); }
重新运行项目,导航栏右侧就显示了当前用户名和登出按钮
点击Logout登出,跳转到了IdentityServer认证服务器的登出页面(Account/Logout),此时已经登出了,但是界面停在了IdentityServer的注销成功页面
点击“here”,可以跳转到MVC客户端,但是不是很友好
此时我们打开IdentityServer认证服务器地址:http://localhost:5000,看到IdentityServer认证服务器的用户已经显示被注销
然后来解决上面不友好的问题,修改IdentityServer服务器,打开Quickstart/Account/AccountOptions.cs,将AutomaticRedirectAfterSignOut设置为true,即登出后自动跳转
修改完成后重启IdentityServer认证服务器,再重启MVC客户端即可解决。
Over, Thanks!!!