Core篇——初探IdentityServer4(OpenID Connect客户端验证)
目录
1、Oauth2协议授权码模式介绍
2、IdentityServer4的OpenID Connect客户端验证简单实现
Oauth2协议授权码模式介绍
- 授权码模式是Oauth2协议中最严格的认证模式,它的组成以及运行流程是这样
1、用户访问客户端,客户端将用户导向认证服务器
2、用户在认证服务器输入用户名密码选择授权,认证服务器认证成功后,跳转至一个指定好的"跳转Url",同时携带一个认证码。
3、用户携带认证码请求指定好的"跳转Url"再次请求认证服务器(这一步后台完成,对用户不可见),此时,由认证服务器返回一个Token
4、客户端携带token请求用户资源
- OpenId Connect运行流程为
1、用户访问客户端,客户端将用户导向认证服务器
2、用户在认证服务器输入用户名密码认证授权
3、认证服务器返回token和资源信息
IdentityServer4的OpenID Connect客户端验证简单实现
Server部分
- 添加一个Mvc项目,配置Config.cs文件
-
1 public class Config 2 { 3 //定义要保护的资源(webapi) 4 public static IEnumerable<ApiResource> GetApiResources() 5 { 6 return new List<ApiResource> 7 { 8 new ApiResource("api1", "My API") 9 }; 10 } 11 //定义可以访问该API的客户端 12 public static IEnumerable<Client> GetClients() 13 { 14 return new List<Client> 15 { 16 new Client 17 { 18 ClientId = "mvc", 19 // no interactive user, use the clientid/secret for authentication 20 AllowedGrantTypes = GrantTypes.Implicit, //简化模式 21 // secret for authentication 22 ClientSecrets = 23 { 24 new Secret("secret".Sha256()) 25 }, 26 RequireConsent =true, //用户选择同意认证授权 27 RedirectUris={ "http://localhost:5001/signin-oidc" }, //指定允许的URI返回令牌或授权码(我们的客户端地址) 28 PostLogoutRedirectUris={ "http://localhost:5001/signout-callback-oidc" },//注销后重定向地址 参考https://identityserver4.readthedocs.io/en/release/reference/client.html 29 LogoUri="https://ss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=3298365745,618961144&fm=27&gp=0.jpg", 30 // scopes that client has access to 31 AllowedScopes = { //客户端允许访问个人信息资源的范围 32 IdentityServerConstants.StandardScopes.Profile, 33 IdentityServerConstants.StandardScopes.OpenId, 34 IdentityServerConstants.StandardScopes.Email, 35 IdentityServerConstants.StandardScopes.Address, 36 IdentityServerConstants.StandardScopes.Phone 37 } 38 } 39 }; 40 } 41 public static List<TestUser> GeTestUsers() 42 { 43 return new List<TestUser> 44 { 45 new TestUser 46 { 47 SubjectId = "1", 48 Username = "alice", 49 Password = "password" 50 }, 51 new TestUser 52 { 53 SubjectId = "2", 54 Username = "bob", 55 Password = "password" 56 } 57 }; 58 } 59 //openid connect 60 public static IEnumerable<IdentityResource> GetIdentityResources() 61 { 62 return new List<IdentityResource> 63 { 64 new IdentityResources.OpenId(), 65 new IdentityResources.Profile(), 66 new IdentityResources.Email() 67 }; 68 } 69 }
- 添加几个ViewModel 用来接收解析跳转URL后的参数
-
1 public class InputConsentViewModel 2 { 3 public string Button { get; set; } 4 public IEnumerable<string> ScopesConsented { get; set; } 5 6 public bool RemeberConsent { get; set; } 7 public string ReturnUrl { get; set; } 8 } 9 //解析跳转url后得到的应用权限等信息 10 public class ConsentViewModel:InputConsentViewModel 11 { 12 public string ClientId { get; set; } 13 public string ClientName { get; set; } 14 public string ClientUrl { get; set; } 15 public string ClientLogoUrl { get; set; } 16 public IEnumerable<ScopeViewModel> IdentityScopes { get; set; } 17 public IEnumerable<ScopeViewModel> ResourceScopes { get; set; } 18 } 19 //接收Scope 20 public class ScopeViewModel 21 { 22 public string Name { get; set; } 23 public string DisplayName { get; set; } 24 public string Description { get; set; } 25 public bool Emphasize { get; set; } 26 public bool Required { get; set; } 27 public bool Checked { get; set; } 28 } 29 public class ProcessConsentResult 30 { 31 public string RedirectUrl { get; set; } 32 public bool IsRedirectUrl => RedirectUrl != null; 33 public string ValidationError { get; set; } 34 public ConsentViewModel ViewModel { get; set; } 35 }
- 配置StartUp,将IdentityServer加入到DI容器,这里有个ConsentService,用来处理解析跳转URL的数据,这个Service在下面实现。
1 public void ConfigureServices(IServiceCollection services) 2 { 3 services.AddIdentityServer() 4 .AddDeveloperSigningCredential() //添加登录证书 5 .AddInMemoryIdentityResources(Config.GetIdentityResources()) //添加IdentityResources 6 .AddInMemoryApiResources(Config.GetApiResources()) 7 .AddInMemoryClients(Config.GetClients()) 8 .AddTestUsers(Config.GeTestUsers()); 9 services.AddScoped<ConsentService>(); 10 services.AddMvc(); 11 } 12 public void Configure(IApplicationBuilder app, IHostingEnvironment env) 13 { 14 if (env.IsDevelopment()) 15 { 16 app.UseDeveloperExceptionPage(); 17 } 18 else 19 { 20 app.UseExceptionHandler("/Home/Error"); 21 } 22 app.UseStaticFiles(); 23 app.UseIdentityServer();//引用IdentityServer中间件 24 app.UseMvc(routes => 25 { 26 routes.MapRoute( 27 name: "default", 28 template: "{controller=Home}/{action=Index}/{id?}"); 29 }); 30 }
- 添加一个ConsentService,用来根据Store拿到Resource
-
1 public class ConsentService 2 { 3 private readonly IClientStore _clientStore; 4 private readonly IResourceStore _resourceStore; 5 private readonly IIdentityServerInteractionService _identityServerInteractionService; 6 7 8 public ConsentService(IClientStore clientStore, 9 IResourceStore resourceStore, 10 IIdentityServerInteractionService identityServerInteractionService) 11 { 12 _clientStore = clientStore; 13 _resourceStore = resourceStore; 14 _identityServerInteractionService = identityServerInteractionService; 15 } 16 17 public async Task<ConsentViewModel> BuildConsentViewModel(string returnUrl) 18 { 19 //根据return url 拿到ClientId 等信息 20 var request = await _identityServerInteractionService.GetAuthorizationContextAsync(returnUrl); 21 if (returnUrl == null) 22 return null; 23 var client = await _clientStore.FindEnabledClientByIdAsync(request.ClientId); 24 var resources = await _resourceStore.FindEnabledResourcesByScopeAsync(request.ScopesRequested);//根据请求的scope 拿到resources 25 26 27 return CreateConsentViewModel(request, client, resources); 28 } 29 30 private ConsentViewModel CreateConsentViewModel(AuthorizationRequest request, Client client, Resources resources) 31 { 32 var vm = new ConsentViewModel(); 33 vm.ClientName = client.ClientName; 34 vm.ClientLoggoUrl = client.LogoUri; 35 vm.ClientUrl = client.ClientUri; 36 vm.RemeberConsent = client.AllowRememberConsent; 37 38 vm.IdentityScopes = resources.IdentityResources.Select(i => CreateScopeViewModel(i)); 39 //api resource 40 vm.ResourceScopes = resources.ApiResources.SelectMany(i => i.Scopes).Select(x => CreateScopeViewModel(scope: x)); 41 return vm; 42 } 43 //identity 1个scopes 44 private ScopeViewModel CreateScopeViewModel(IdentityResource identityResource) 45 { 46 return new ScopeViewModel 47 { 48 Name = identityResource.Name, 49 DisplayName = identityResource.DisplayName, 50 Description = identityResource.Description, 51 Required = identityResource.Required, 52 Checked = identityResource.Required, 53 Emphasize = identityResource.Emphasize 54 }; 55 } 56 //apiresource 57 private ScopeViewModel CreateScopeViewModel(Scope scope) 58 { 59 return new ScopeViewModel 60 { 61 Name = scope.Name, 62 DisplayName = scope.DisplayName, 63 Description = scope.Description, 64 Required = scope.Required, 65 Checked = scope.Required, 66 Emphasize = scope.Emphasize 67 }; 68 } 69 }
- 添加一个ConsentController,用来显示授权登录页面,以及相应的跳转登录逻辑。
1 public class ConsentController : Controller 2 { 3 private readonly ConsentService _consentService; 4 public ConsentController(ConsentService consentService) 5 { 6 _consentService = consentService; 7 } 8 9 public async Task<IActionResult> Index(string returnUrl) 10 { 11 //调用consentService的BuildConsentViewModelAsync方法,将跳转Url作为参数传入,解析得到一个ConsentViewModel 12 var model =await _consentService.BuildConsentViewModelAsync(returnUrl); 13 if (model == null) 14 return null; 15 return View(model); 16 } 17 [HttpPost] 18 public async Task<IActionResult> Index(InputConsentViewModel viewModel) 19 { 20 //用户选择确认按钮的时候,根据选择按钮确认/取消,以及勾选权限 21 var result = await _consentService.PorcessConsent(viewModel); 22 if (result.IsRedirectUrl) 23 { 24 return Redirect(result.RedirectUrl); 25 } 26 if (!string.IsNullOrEmpty(result.ValidationError)) 27 { 28 ModelState.AddModelError("", result.ValidationError); 29 } 30 return View(result.ViewModel); 31 } 32 }
- 配置服务端的登录Controller
-
1 public class AccountController : Controller 2 { 3 private readonly TestUserStore _user; //放入DI容器中的TestUser(GeTestUsers方法),通过这个对象获取 4 public AccountController(TestUserStore user) 5 { 6 _user = user; 7 } public IActionResult Login(string returnUrl = null) 8 { 9 ViewData["ReturnUrl"] = returnUrl; 10 return View(); 11 } 12 13 [HttpPost] 14 public async Task<IActionResult> Login(LoginViewModel loginViewModel,string returnUrl) 15 { 16 //用户登录 17 if (ModelState.IsValid) 18 { 19 ViewData["ReturnUrl"] = returnUrl; 20 var user = _user.FindByUsername(loginViewModel.Email); 21 if (user == null) 22 { 23 ModelState.AddModelError(nameof(loginViewModel.Email), "Email not exists"); 24 } 25 else 26 { 27 var result = _user.ValidateCredentials(loginViewModel.Email, loginViewModel.Password); 28 if(result) 29 { 30 var props = new AuthenticationProperties() 31 { 32 IsPersistent = true, 33 ExpiresUtc = DateTimeOffset.UtcNow.AddMinutes(30) 34 }; 35 await Microsoft.AspNetCore.Http.AuthenticationManagerExtensions.SignInAsync( //Id4扩展方法和HttpContext扩展方法重名,这里强制使用命名空间方法 36 this.HttpContext, 37 user.SubjectId, 38 user.Username, 39 props); 40 return RedirectToLoacl(returnUrl); 41 } 42 else 43 { 44 ModelState.AddModelError(nameof(loginViewModel.Email), "Wrong password"); 45 } 46 } 47 } 48 49 return View(); 50 }
- 接下来给Consent控制器的Index添加视图
-
1 @using mvcCookieAuthSample.ViewModels 2 @model ConsentViewModel 3 <h2>ConsentPage</h2> 4 @*consent*@ 5 <div class="row page-header"> 6 <div class="col-sm-10"> 7 @if (!string.IsNullOrWhiteSpace(Model.ClientLogoUrl)) 8 { 9 <div> 10 <img src="@Model.ClientLogoUrl" style="50px;height:50px" /> 11 </div> 12 } 13 <h1>@Model.ClientName</h1> 14 <p>希望使用你的账户</p> 15 </div> 16 </div> 17 @*客户端*@ 18 <div class="row"> 19 <div class="col-sm-8"> 20 <div asp-validation-summary="All" class="danger"></div> 21 <form asp-action="Index" method="post"> 22 <input type="hidden" asp-for="ReturnUrl"/> 23 @if (Model.IdentityScopes.Any()) 24 { 25 <div class="panel"> 26 <div class="panel-heading"> 27 <span class="glyphicon glyphicon-user"></span> 28 用户信息 29 </div> 30 <ul class="list-group"> 31 @foreach (var scope in Model.IdentityScopes) 32 { 33 @Html.Partial("_ScopeListitem.cshtml", scope); 34 } 35 </ul> 36 </div> 37 } 38 @if (Model.ResourceScopes.Any()) 39 { 40 <ul class="list-group"> 41 @foreach (var scope in Model.ResourceScopes) 42 { 43 @Html.Partial("_ScopeListitem.cshtml", scope); 44 }</ul> 45 } 46 <div> 47 <label> 48 <input type="checkbox" asp-for="RemeberConsent"/> 49 <strong>记住我的选择</strong> 50 </label> 51 </div> 52 <div> 53 <button name="button" value="yes" class="btn btn-primary" autofocus>同意</button> 54 <button name="button" value="no">取消</button> 55 @if (!string.IsNullOrEmpty(Model.ClientUrl)) 56 { 57 <a href="@Model.ClientUrl" class="pull-right btn btn-default"> 58 <span class="glyphicon glyphicon-info-sign" ></span> 59 <strong>@Model.ClientUrl</strong> 60 </a> 61 } 62 </div> 63 </form> 64 </div> 65 </div> 66 //这里用到了一个分部视图用来显示用户允许授权的身份资源和api资源 67 @using mvcCookieAuthSample.ViewModels 68 @model ScopeViewModel; 69 <li> 70 <label> 71 <input type="checkbox" 72 name="ScopesConsented" 73 id="scopes_@Model.Name" 74 value="@Model.Name" 75 checked=@Model.Checked 76 disabled=@Model.Required/> 77 @if (Model.Required) 78 { 79 <input type="hidden" name="ScopesConsented" value="@Model.Name" /> 80 } 81 <strong>@Model.Name</strong> 82 @if (Model.Emphasize) 83 { 84 <span class="glyphicon glyphicon-exclamation-sign"></span> 85 } 86 </label> 87 @if(string.IsNullOrEmpty(Model.Description)) 88 { 89 <div> 90 <label for="scopes_@Model.Name">@Model.Description</label> 91 </div> 92 } 93 </li>
- 添加客户端,依旧添加一个mvc项目,配置startup,Home/Index action打上Authorize标签。
-
1 public void ConfigureServices(IServiceCollection services) 2 { 3 services.AddAuthentication(options => { 4 options.DefaultScheme = "Cookies"; 5 options.DefaultChallengeScheme = "oidc";//openidconnectservice 6 }) 7 .AddCookie("Cookies") 8 .AddOpenIdConnect("oidc",options=> { 9 options.SignInScheme = "Cookies"; 10 options.Authority = "http://localhost:5000"; //设置认证服务器 11 options.RequireHttpsMetadata = false; 12 options.ClientId = "mvc"; //openidconfig的配置信息 13 options.ClientSecret = "secret"; 14 options.SaveTokens = true; 15 }); 16 services.AddMvc(); 17 } 18 public void Configure(IApplicationBuilder app, IHostingEnvironment env) 19 { 20 if (env.IsDevelopment()) 21 { 22 app.UseDeveloperExceptionPage(); 23 app.UseBrowserLink(); 24 } 25 else 26 { 27 app.UseExceptionHandler("/Home/Error"); 28 } 29 app.UseStaticFiles(); 30 app.UseAuthentication(); 31 app.UseMvc(routes => 32 { 33 routes.MapRoute( 34 name: "default", 35 template: "{controller=Home}/{action=Index}/{id?}"); 36 }); 37 }
设置服务端端口5000,运行服务器端;设置客户端端口5001,运行客户端。我们可以看到,localhost:5001会跳转至认证服务器
然后看下Url=》
使用config配置的testuser登录系统,选择允许授权的身份权限。登录成功后看到我们的Claims
总结
- 最后来总结一下
用户访问客户端(5001端口程序),客户端将用户导向认证服务器(5000程序),用户选择允许授权的身份资源和api资源后台解析(这两个资源分别由Resources提供,resources 由IResourceStore解析returnurl后的Scopes提供),最后由ProfileService返回数条Claim。(查看ConsentService的各个方法)