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.注入認證中間件,同時啟動認證

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]

[Authorize] public class HomeController : Controller { //... }
4.About試圖展示用戶信息與Token

@{ 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)

public static IEnumerable<ApiResource> GetApiResources() { return new List<ApiResource> { new ApiResource("api","My Api") }; }
4.注冊IdentityResource,即授權后客戶端可訪問的用戶信息(PS:IdentityResource對應的是OpenId Connect中的Scope)

public static IEnumerable<IdentityResource> GetIdentityResources() => new List<IdentityResource> { new IdentityResources.OpenId(), new IdentityResources.Profile(), new IdentityResources.Email() };
5.注冊客戶端,即可被授權的Client

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進行測試

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

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

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

public class LoginViewModel { public string UserName { get; set; } [DataType(DataType.Password)] public string Password { get; set; } }
10.添加登錄View,Login.cshtml

@{ 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

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

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