IdentityServer4能解决什么问题
假设我们开发了一套【微博程序】,主要拥有两个功能:【登录验证】、【数据获取】
随后我们又开发了【简书程序】、【知乎程序】,它们的主要功能也是:【登录验证】、【数据获取】
这时候我们就会想一个问题,每个应用程序的【数据获取】可能各不相同。但是【登录验证】能否做成单点登录?于是有了如下的结构
注意:由于【微博程序】、【简书程序】、【知乎程序】,都是我们自己开发的程序,所以我们可以将所有登录都汇总到【微博登录中心】,用户信息在每个程序之间的传输(哪些用户信息可以在程序间共享,哪些用户信息不能在程序间共享),我们是方便控制的。
用户还会使用很多第三方程序。对于用户而言,肯定不想每用到一个第三方程序都要重新去维护自己的个人信息。有没有一种方式既可以实现用户信息在多个第三方程序间共享(用户就不用每使用一个第三方系统都需要维护个人信息),同时又能保证个人信息安全?
注意:【微博登录中心】与【第三方程序】所使用的是【双向箭头】,说明【第三方程序】从【微博登录中心】中获取到了【用户信息】。而如何保证【用户信息】的安全,就是用【IdentityServer4】来实现的。
IdentityServer4如何保证用户信息安全
从上文最后一个场景中,我们了解到允许【第三方程序】访问【用户信息】可以为用户提供很多便利。但是提供便利的同时如何能保证【信息安全】又是一个不得不解决的问题。要保证用户信息的安全,至少要满足以下3点。
(A) 用户同意【第三方程序】访问自己的【用户信息】,或者说用户必须告诉【登录中心】:我同意当前这个【第三方程序】访问我的【用户信息】
(B)【第三方程序】必须是在【登录中心】登记过,即【登录中心】认证【第三方程序】的身份是否真实可靠
(C)【登录中心】将【用户信息】划分为【可共享信息】与【不可共享信息】,【登录中心】授权【第三方程序】访问【可共享信息】
当然这里只是抛砖引玉,数据传输间的加密协议、【第三方程序】访问【登录中心】的次数限制。。。都没有列出来。总之最好有一套成熟(公认)的协议来保证【数据共享】与【信息安全】,所以这里就引出了:OpenID Connect,IentityServer4就是依靠OpenID Connect来保证用户信息的共享与安全。
何为OpenID Connect
OpenID的定义
OpenID 是一个以用户为中心的数字身份识别框架,它具有开放、分散性。OpenID 的创建基于这样一个概念:我们可以通过 URI (又叫 URL 或网站地址)来认证一个网站的唯一身份,同理,我们也可以通过这种方式来作为用户的身份认证
总结为:OpenId用于身份认证(Authentication)
OpenID Connect的定义
OpenID Connect 1.0 是基于OAuth 2.0协议之上的简单身份层,它允许客户端根据授权服务器的认证结果最终确认终端用户的身份,以及获取基本的用户信息;它支持包括Web、移动、JavaScript在内的所有客户端类型去请求和接收终端用户信息和身份认证会话信息;它是可扩展的协议,允许你使用某些可选功能,如身份数据加密、OpenID提供商发现、会话管理等。
总结为:OpenID Connect = OIDC = OpenID(Authentication)+ OAuth2.0(Authorization)
由于OpenID Connect的授权是基于OAuth2.0协议的,所以下面需要着重介绍一下OAuth2.0
OAuth2.0定义
OAuth 2.0是行业标准的授权协议。 OAuth 2.0取代了2006年创建的原始OAuth协议所做的工作。OAuth 2.0专注于客户端开发人员的简单性,同时为Web应用程序,桌面应用程序,移动电话和客厅设备提供特定的【授权流程】。
上面这段话摘自OAuth2.0官网,总结:为各种端(web应用、桌面应用、移动设备。。。)提供【授权流程】。
OAuth2.0授权流程图
(A)用户打开客户端以后,客户端要求用户给予授权。
(B)用户同意给予客户端授权。
(C)客户端使用上一步获得的授权,向认证服务器申请令牌。
(D)认证服务器对客户端进行认证以后,确认无误,同意发放令牌。
(E)客户端使用令牌,向资源服务器申请获取资源。
(F)资源服务器确认令牌无误,同意向客户端开放资源。
流程图与解释均摘自OAuth2.0 RFC 6749,流程总结为:用户授权-》申请令牌-》获取资源
OAuth2.0四种授权模式
客户端必须得到用户的授权(authorization grant),才能获得令牌(access token)。OAuth 2.0定义了四种授权方式。
(1)授权码模式(authorization code):功能最完整、流程最严密的授权模式。它的特点就是通过客户端的后台服务器,与"服务提供商"的认证服务器进行互动。(用户授权->客户端请求授权码(Authorization Code)->客户端获取授权码->客户端请求令牌(Access Token)->客户端获取令牌->客户端请求资源)
(2)简化模式(implicit):不通过第三方应用程序的服务器,直接在浏览器中向认证服务器申请令牌,跳过了"授权码"这个步骤,因此得名。所有步骤在浏览器中完成,令牌对访问者是可见的,且客户端不需要认证。(用户授权->客户端请求令牌(Access Token)->客户端获取令牌->客户端请求资源)
(3)密码模式(resource owner password credentials):用户向客户端提供自己的用户名和密码。客户端使用这些信息,向"服务商提供商"索要授权。(用户将用户名&密码提供给客户端->客户端请求令牌(Access Token)->客户端获取令牌->客户端请求资源)
(4)客户端模式(client credentials):客户端以自己的名义,而不是以用户的名义,向"服务提供商"进行认证。(客户端请求令牌(Access Token)->客户端获取令牌->客户端请求资源)
每种模式的流程图、关键字,在这里就不做过多的赘述,大家可以参阅阮一峰的文章:理解OAuth2.0
OpenID Connect(OIDC)流程概述
OAuth2提供了Access Token来解决授权第三方客户端访问受保护资源的问题;OIDC在这个基础上提供了ID Token来解决第三方客户端标识用户身份认证的问题。OIDC的核心在于在OAuth2的授权流程中,一并提供用户的身份认证信息(ID Token)给到第三方客户端,ID Token使用JWT格式来包装,得益于JWT(JSON Web Token)的自包含性,紧凑性以及防篡改机制,使得ID Token可以安全的传递给第三方客户端程序并且容易被验证。此外还提供了UserInfo的接口,用户获取用户的更完整的信息
总结为:OIDC新增了一个【ID Token】,【ID Token】主要用来给【第三方平台】标识(认证)用户(通过sub与subid),同时也可以将【用户信息】存储其中。
OpenID Connect(OIDC)认证授权流程图
名词介绍
(A)AuthN:认证
(B)AuthZ:授权
(C)EU:End User:用户
(D)RP:Relying Party :用来代指OAuth2中的受信任的客户端,身份认证和授权信息的消费方
(E)OP:OpenID Provider,服务端(比如OAuth2中的授权服务),用来为客户端提供用户的身份认证信息
(F)ID Token:JWT格式的数据,包含用户身份认证的信息(还可包含用户其它信息)
(G)Access Token:授权码,服务点授权成功后,返回给客户端。用于请求用户接口信息
(H)UserInfo Endpoint:用户信息接口(受OAuth2保护),当客户端使用Access Token访问时,返回用户的信息,此接口必须使用HTTPS
流程图
(A)RP(客户端)向OpenID提供商(OP)发送请求
(B)OP对用户进行身份验证并获得授权
(C)OP以Id Token、Access Token响应
(D)RP可以使用Access Token向UserInfo端点发送请求
(E)UserInfo端点返回有关用户的信息
OpenID Connect(OIDC)三种授权模式
授权码模式:客户端请求用户信息->用户授权->授权服务返回授权码(Authorization Code)->客户端获取授权码(Authorization Code)->客户端请求令牌(Access Token & Id Token)->客户端获取令牌->客户端请求资源
(A)RP发送一个认证请求给OP,请求中必须包含Client ID
(B)OP验证用户信息,同时获取用户同意/授权
(C)OP将授权码(Code)返回给RP
(D)RP向Token Endpoint申请Id Token与Access Token,请求中必须包含Code
(E)RP向UserInfo Endpoint申请用户信息(Claims),请求中必须包含Access Token
简化模式(implicit):客户端请求用户信息->用户授权->授权服务返回令牌(Id Token & Access Token)
(A)RP发送一个认证请求给OP(附带client_id)
(B)OP验证用户信息,同时获取用户同意/授权
(C)OP将Id Token与Access Token返回给客户端,Id Token中可以包含用户信息(Claims)
混合模式:客户端请求用户信息->用户授权->授权服务返回授权码(Authorization Code)与一些其它参数->客户端获取授权码(Authorization Code)->客户端请求令牌(Access Token & Id Token)->客户端获取令牌->客户端请求资源
(A)RP发送一个认证请求给OP,请求中必须包含Client ID
(B)OP验证用户信息,同时获取用户同意/授权
(C)OP将授权码(Code)返回给RP,可根据相应类型返回附加参数(官网给出的例子中并没有给出附加参数的例子)
(D)RP向Token Endpoint申请Id Token与Access Token,请求中必须包含Code
(E)RP向UserInfo Endpoint申请用户信息(Claims),请求中必须包含Access Token
IdentityServer4在ASP.NET Core中的运用
回到文章开头的假设,我们拥有【用户信息】,第三方程序希望通过用户授权来访问【用户信息】。这里我准备用【简化模式(implicit)】来演示ASP.NET Core中运用IdentityServer4来实现单点登录功能。至于为何选择【简化模式】,因为它的实现最简单,从简单的入手,方便快速了解框架实现套路。
One:客户端搭建
1.新建一个ASP.NET Core MVC项目
2.注入认证中间件,同时启动认证
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
public void ConfigureServices(IServiceCollection services) { services.AddMvc(); //清空默认绑定的用户信息 JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); //添加认证服务 services.AddAuthentication(options => { options.DefaultScheme = "Cookies"; //默认使用Cookies方案进行认证 options.DefaultChallengeScheme = "oidc"; //默认认证失败时启用oidc方案 }) .AddCookie("Cookies") //添加Cookies认证方案 //添加oidc方案 .AddOpenIdConnect("oidc", options => { options.SignInScheme = "Cookies"; //身份验证成功后使用Cookies方案来保存信息 options.Authority = "http://localhost:16584"; //授权服务地址 options.RequireHttpsMetadata = false; options.ClientId = "mvc_implicit"; options.SaveTokens = true; }); } public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } //启用认证 app.UseAuthentication(); app.UseStaticFiles(); app.UseMvc(routes => { routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); }); }
3.资源添加授权保护,即:HomeController中添加[Authorize]
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
[Authorize] public class HomeController : Controller { //... }
4.About试图展示用户信息与Token
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
@{ ViewData["Title"] = "About"; } <h2>@ViewData["Title"]</h2> <h3>@ViewData["Message"]</h3> @using Microsoft.AspNetCore.Authentication <p> <dl> @foreach (var claim in User.Claims) { <dt>@claim.Type</dt> <dd>@claim.Value</dd> } <dt>Access Token</dt> <dd>@await ViewContext.HttpContext.GetTokenAsync("access_token")</dd> <dt>Refresh Token</dt> <dd>@await ViewContext.HttpContext.GetTokenAsync("refresh_token")</dd> <dt>Id Token</dt> <dd>@await ViewContext.HttpContext.GetTokenAsync("id_token")</dd> </dl> </p>
Two:授权服务搭建
1.新建一个ASP.NET Core MVC项目
2.引入nuget包,IdentityServer4 v2.2.0
3.注册ApiResource,即授权后可访问的Api(PS:ApiResource对应的是OAuth2.0中的Scope)
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
public static IEnumerable<ApiResource> GetApiResources() { return new List<ApiResource> { new ApiResource("api","My Api") }; }
4.注册IdentityResource,即授权后客户端可访问的用户信息(PS:IdentityResource对应的是OpenId Connect中的Scope)
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
public static IEnumerable<IdentityResource> GetIdentityResources() => new List<IdentityResource> { new IdentityResources.OpenId(), new IdentityResources.Profile(), new IdentityResources.Email() };
5.注册客户端,即可被授权的Client
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
public static IEnumerable<Client> GetClients() => new List<Client> { new Client { ClientId = "mvc_implicit", ClientName = "MVC Client", AllowedGrantTypes = GrantTypes.Implicit, //简化模式 RequireConsent = false, //Consent是授权页面,这里我们不进行授权 RedirectUris = { "http://localhost:1798/signin-oidc" }, PostLogoutRedirectUris = { "http://localhost:1798/signout-callback-oidc" }, //授权后可以访问的用户信息(OpenId Connect Scope)与Api(OAuth2.0 Scope) AllowedScopes = new List<string> { IdentityServerConstants.StandardScopes.OpenId, IdentityServerConstants.StandardScopes.Profile, IdentityServerConstants.StandardScopes.Email, "api" } } };
RedirectUris:客户端oidc自带的地址(功能),用于处理登录成功后处理授权服务返回的response,同时保存配置
PostLogoutRedirectUris:客户端oidc自带的地址(功能),退出登录后跳转到授权服务,将授权服务也推出登录
6.注册用户,这里就用IdentityServer4提供的TestUser进行测试
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
public static List<TestUser> GetTestUsers() => new List<TestUser> { new TestUser() { SubjectId="1", Username="test", Password="123456" } };
7.在.NET Core中注入IdentityServer4,同时把上面的(3)(4)(5)(6)注入到.NET Core中,同时启动IdentityServer4
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
public void ConfigureServices(IServiceCollection services) { //注入IdentityServer4 services.AddIdentityServer(c => { //登陆地址 c.UserInteraction.LoginUrl = "/account/login"; }) .AddDeveloperSigningCredential() //下面是注入资源信息 .AddInMemoryApiResources(Config.GetApiResources()) .AddInMemoryClients(Config.GetClients()) .AddInMemoryIdentityResources(Config.GetIdentityResources()) .AddTestUsers(Config.GetTestUsers()); services.AddMvc(); } public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } //启动IdentityServer4 app.UseIdentityServer(); app.UseStaticFiles(); app.UseMvc(routes => { routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); }); }
8.添加登录Controller,AccountController
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
public class AccountController : Controller { private readonly TestUserStore _users; public AccountController(TestUserStore users) { _users = users; } public IActionResult Index() { return View("Login"); } public IActionResult Login(string returnUrl = null) { ViewData["ReturnUrl"] = returnUrl; return View(); } [HttpPost] public async Task<IActionResult> Login(LoginViewModel model, string returnUrl = null) { if (ModelState.IsValid) { ViewData["ReturnUrl"] = returnUrl; var user = _users.FindByUsername(model.UserName); if (user == null) { ModelState.AddModelError(nameof(model.UserName), "Username not exists"); } else { if (user.Password.Equals(model.Password)) { //(保存)认证信息字典 AuthenticationProperties props = new AuthenticationProperties { IsPersistent = true, //认证信息是否跨域有效 ExpiresUtc = DateTimeOffset.UtcNow.Add(TimeSpan.FromMinutes(30)) //凭据有效时间 }; await Microsoft.AspNetCore.Http.AuthenticationManagerExtensions.SignInAsync( HttpContext, user.SubjectId, user.Username, props); return RedirectToLoacl(returnUrl); } ModelState.AddModelError(nameof(model.Password), "Password Error"); } } return View(model); } public async Task<IActionResult> Logout() { await HttpContext.SignOutAsync(); return RedirectToAction("Index", "Home"); } private IActionResult RedirectToLoacl(string returnUrl) { if (Url.IsLocalUrl(returnUrl)) { return Redirect(returnUrl); } return RedirectToAction(nameof(HomeController.Index), "Home"); } }
9.添加登录ViewModel,LoginViewModel
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
public class LoginViewModel { public string UserName { get; set; } [DataType(DataType.Password)] public string Password { get; set; } }
10.添加登录View,Login.cshtml
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
@{ ViewData["Title"] = "Login"; } @model LoginViewModel; <div class="row"> <div class="col-md-4"> <section> <form method="post" asp-controller="Account" asp-action="Login" asp-route-returnUrl="@ViewData["ReturnUrl"]"> <h4>Use a local account to log in.</h4> <hr /> <div class="form-group"> <label asp-for="UserName"></label> <input asp-for="UserName" class="form-control" /> <span asp-validation-for="UserName" class="text-danger"></span> </div> <div class="form-group"> <label asp-for="Password"></label> <input asp-for="Password" type="password" class="form-control" /> <span asp-validation-for="Password" class="text-danger"></span> </div> <div class="form-group"> <button type="submit" class="btn btn-default">Log in</button> </div> </form> </section> </div> </div> @section Scripts { @await Html.PartialAsync("_ValidationScriptsPartial") }
PS:上面贴出的代码中,大家需要注意的就是地址配置,我配置的都是我本机的地址,而且我并没有配置固定地址,各位可以配置成固定地址。
Three:显示效果
1.先运行服务端
2.再运行客户端,会直接跳转到服务端的Login页面。PS:URL后面跟着client_Id,登录成功后的跳转页面等
3.登录成功后跳转回客户端
4.点击About,查看返回的用户信息
5.遗留问题
1.简化模式默认是不返回Access Token,如果需要返回,需要做如下配置
a.授权服务,客户端注册时,开启返回Access Token
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
public static IEnumerable<Client> GetClients() => new List<Client> { new Client { ClientId = "mvc_implicit", ClientName = "MVC Client", AllowedGrantTypes = GrantTypes.Implicit, //简化模式 RequireConsent = false, //Consent是授权页面,这里我们不进行授权 RedirectUris = { "http://localhost:1798/signin-oidc" }, PostLogoutRedirectUris = { "http://localhost:1798/signout-callback-oidc" }, //授权后可以访问的用户信息(OpenId Connect Scope)与Api(OAuth2.0 Scope) AllowedScopes = new List<string> { IdentityServerConstants.StandardScopes.OpenId, IdentityServerConstants.StandardScopes.Profile, IdentityServerConstants.StandardScopes.Email, "api" }, //允许返回Access Token AllowAccessTokensViaBrowser = true } };
b.客户端请求类型(response_type),需要包含Access Token
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
public void ConfigureServices(IServiceCollection services) { services.AddMvc(); //清空默认绑定的用户信息 JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); //添加认证服务 services.AddAuthentication(options => { options.DefaultScheme = "Cookies"; //默认使用Cookies方案进行认证 options.DefaultChallengeScheme = "oidc"; //默认认证失败时启用oidc方案 }) .AddCookie("Cookies") //添加Cookies认证方案 //添加oidc方案 .AddOpenIdConnect("oidc", options => { options.SignInScheme = "Cookies"; //身份验证成功后使用Cookies方案来保存信息 options.Authority = "http://localhost:16584"; //授权服务地址 options.RequireHttpsMetadata = false; options.ClientId = "mvc_implicit"; options.ResponseType = "id_token token"; //默认只返回id_token 这里添加上token(Access Token) options.SaveTokens = true; }); }
2.我们并没有得到用户邮箱、profile等
如果要解决这个问题,需要添加ProfileService,请参考IdentityServer4
参考博文与实例代码下载
http://www.cnblogs.com/cgzl/p/7793241.html