本教程將搭建一個最小能夠運行的IdentityServer。為簡單起見,我們將identityserver和客戶端放在同一Web應用程序-這可能不會是一個很現實的情況下,但可以讓你不太復雜的開始。
完整的源代碼可以在這里找到。
Part 1 - MVC MVC認證與授權
在第一部分中我們將創建一個簡單的MVC應用程序並添加認證通過identityserver它。然后,我們將有一個更仔細的看claims,claims的變化和授權.
創建一個 web application
在Visual Studio 2013中,創建一個標准的MVC應用程序和設置認證,“沒有認證”。
你可以在屬性窗口啟用SSL
注意:不要忘記更新你的項目屬性中的url
添加 IdentityServer 引用
IdentityServer基於OWIN/Katana作為NuGet包。要將其添加到新創建的應用程序上,安裝以下2個包:
install-package Microsoft.Owin.Host.Systemweb
install-package Thinktecture.IdentityServer3
IdentityServer配置——客戶端
IdentityServer需要一些關於客戶端信息,這可以簡單地提供使用客戶端對象:
public static class Clients { public static IEnumerable<Client> Get() { return new[] { new Client { Enabled = true, ClientName = "MVC Client", ClientId = "mvc", Flow = Flows.Implicit, RedirectUris = new List<string> { "https://localhost:44319/" } } }; } }
IdentityServer配置——用戶
下一步我們將添加一些IdentityServer用戶-這里通過提供一個簡單的C#類完成,當然你可以從任何數據存儲加載用戶。我們提供了ASP.NET Identity 和MembershipReboot支持檢索用戶信息。
public static class Users { public static List<InMemoryUser> Get() { return new List<InMemoryUser> { new InMemoryUser { Username = "bob", Password = "secret", Subject = "1", Claims = new[] { new Claim(Constants.ClaimTypes.GivenName, "Bob"), new Claim(Constants.ClaimTypes.FamilyName, "Smith") } } }; } }
添加 Startup.cs
配置啟動類。在這里,我們提供有關客戶信息的用戶,范圍,簽名證書和其他一些配置選項。生產要從Windows證書存儲區或其他固定源負載簽名證書。這里我們簡單地添加到項目文件(你可以下載一個測試證書的地方。它添加該項目並將其屬性【復制到輸出目錄】更改為始終復制。
public class Startup { public void Configuration(IAppBuilder app) { app.Map("/identity", idsrvApp => { idsrvApp.UseIdentityServer(new IdentityServerOptions { SiteName = "Embedded IdentityServer", SigningCertificate = LoadCertificate(), Factory = InMemoryFactory.Create( users : Users.Get(), clients: Clients.Get(), scopes : StandardScopes.All) }); }); } X509Certificate2 LoadCertificate() { return new X509Certificate2( string.Format(@"{0}\bin\identityServer\idsrv3test.pfx", AppDomain.CurrentDomain.BaseDirectory), "idsrv3test"); } }
在瀏覽器中輸入以下地址以檢查配置https://localhost:44319/identity/.well-known/openid-configuration
注意:
最后一件事,在配置文件中添加下面的代碼,否則我們的一些嵌入式資產將不能正確使用IIS加載
<system.webServer> <modules runAllManagedModulesForAllRequests="true" /> </system.webServer>
添加和配置OpenID Connect 中間件
增加OIDC 認證的MVC應用程序中,我們需要添加兩包:
install-package Microsoft.Owin.Security.Cookies
install-package Microsoft.Owin.Security.OpenIdConnect
在startup.cs中配置默認認證類型為cookie
app.UseCookieAuthentication(new CookieAuthenticationOptions { AuthenticationType = "Cookies" });
使用嵌入的OpenID Connect Server
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions { Authority = "https://localhost:44319/identity", ClientId = "mvc", RedirectUri = "https://localhost:44319/", ResponseType = "id_token", SignInAsAuthenticationType = "Cookies" });
添加一個受保護的資源和Claims
一個受保護的資源:
[Authorize] public ActionResult About() { return View((User as ClaimsPrincipal).Claims); }
相應的視圖看起來像這樣:
@model IEnumerable<System.Security.Claims.Claim> <dl> @foreach (var claim in Model) { <dt>@claim.Type</dt> <dd>@claim.Value</dd> } </dl>
Authentication and claims
點擊About鏈接將觸發認證。identityserver將顯示登錄頁面
登錄成功后可以看到登錄信息:
增加Role Claim 和 Scope
在下一步中,我們要向我們的用戶添加一些角色聲明,我們將在以后使用它來進行授權。
現在我們有了OIDC 標准scope-定義一個角色的scope包括claims,和一些標准屬性:
public static class Scopes { public static IEnumerable<Scope> Get() { var scopes = new List<Scope> { new Scope { Enabled = true, Name = "roles", Type = ScopeType.Identity, Claims = new List<ScopeClaim> { new ScopeClaim("role") } } }; scopes.AddRange(StandardScopes.All); return scopes; } }
改變在Startup.cs的factory類使用定義的scope
Factory = new IdentityServerServiceFactory() .UseInMemoryUsers(Users.Get()) .UseInMemoryClients(Clients.Get()) .UseInMemoryScopes(Scopes.Get()),
下一步我們為bob添加幾個Claim
public static class Users { public static IEnumerable<InMemoryUser> Get() { return new[] { new InMemoryUser { Username = "bob", Password = "secret", Subject = "1", Claims = new[] { new Claim(Constants.ClaimTypes.GivenName, "Bob"), new Claim(Constants.ClaimTypes.FamilyName, "Smith"), new Claim(Constants.ClaimTypes.Role, "Geek"), new Claim(Constants.ClaimTypes.Role, "Foo") } } }; } }
改變中間件配置:
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions { Authority = "https://localhost:44319/identity", ClientId = "mvc", Scope = "openid profile roles", RedirectUri = "https://localhost:44319/", ResponseType = "id_token", SignInAsAuthenticationType = "Cookies" });
成功驗證后,您現在應該看到用戶Claim集合中的角色Claim
Claims 轉換
默認情況下那些Claims看起像這樣:
通過配置可以控制哪些claim需要被記錄:
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions { Authority = "https://localhost:44319/identity", ClientId = "mvc", Scope = "openid profile roles", RedirectUri = "https://localhost:44319/", ResponseType = "id_token", SignInAsAuthenticationType = "Cookies", UseTokenLifetime = false, Notifications = new OpenIdConnectAuthenticationNotifications { SecurityTokenValidated = async n => { var id = n.AuthenticationTicket.Identity; // we want to keep first name, last name, subject and roles var givenName = id.FindFirst(Constants.ClaimTypes.GivenName); var familyName = id.FindFirst(Constants.ClaimTypes.FamilyName); var sub = id.FindFirst(Constants.ClaimTypes.Subject); var roles = id.FindAll(Constants.ClaimTypes.Role); // create new identity and set name and role claim type var nid = new ClaimsIdentity( id.AuthenticationType, Constants.ClaimTypes.GivenName, Constants.ClaimTypes.Role); nid.AddClaim(givenName); nid.AddClaim(familyName); nid.AddClaim(sub); nid.AddClaims(roles); // add some other app specific claim nid.AddClaim(new Claim("app_specific", "some data")); n.AuthenticationTicket = new AuthenticationTicket( nid, n.AuthenticationTicket.Properties); } } });
在添加上述代碼后,我們的Claims現在看起來像這樣:
Authorization
現在,我們有身份驗證和一些聲明,我們可以開始添加簡單的授權規則。
MVC有一個內置的屬性稱為[Authorize]身份驗證的用戶,您還可以使用此屬性來詮釋角色成員資格要求。我們不建議這種方法,因為這通常會導致代碼,混合的關注,如業務/控制器邏輯和授權政策。我們建議將授權邏輯從控制器中分離,從而導致更清潔的代碼和更好的可測性(在 here 閱讀更多)。
Resource Authorization
要添加新的授權基礎設施和新的屬性,我們添加NuGet包:
install-package Thinktecture.IdentityModel.Owin.ResourceAuthorization.Mvc
[ResourceAuthorize("Read", "ContactDetails")] public ActionResult Contact() { ViewBag.Message = "Your contact page."; return View(); }
請注意,屬性是不表達權限,我們單獨的邏輯去控制權限:
public class AuthorizationManager : ResourceAuthorizationManager { public override Task<bool> CheckAccessAsync(ResourceAuthorizationContext context) { switch (context.Resource.First().Value) { case "ContactDetails": return AuthorizeContactDetails(context); default: return Nok(); } } private Task<bool> AuthorizeContactDetails(ResourceAuthorizationContext context) { switch (context.Action.First().Value) { case "Read": return Eval(context.Principal.HasClaim("role", "Geek")); case "Write": return Eval(context.Principal.HasClaim("role", "Operator")); default: return Nok(); } } }
最后在Startup.cs中添加配置:
app.UseResourceAuthorization(new AuthorizationManager());
運行示例,並通過代碼來熟悉驗證的流程。
Role Authorization
通過重寫AuthorizeAttribute控制返回的結果
// Customized authorization attribute: public class AuthAttribute : AuthorizeAttribute { protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext) { if (filterContext.HttpContext.User.Identity.IsAuthenticated) { // 403 we know who you are, but you haven't been granted access filterContext.Result = new HttpStatusCodeResult(System.Net.HttpStatusCode.Forbidden); } else { // 401 who are you? go login and then try again filterContext.Result = new HttpUnauthorizedResult(); } } } // Usage: [Auth(Roles = "Geek")] public ActionResult About() { // ... }
其他的授權和處理訪問被拒絕的情況
通過在HomeController中添加一個新的Action來進行更多的授權:
[ResourceAuthorize("Write", "ContactDetails")] public ActionResult UpdateContact() { ViewBag.Message = "Update your contact details!"; return View(); }
當你試圖訪問這個地址的時候,你會看到一個被禁止的錯誤頁面。
事實上,如果用戶已經通過認證,你會看到不同的響應。如果不是MVC將重定向到登錄頁面,如果通過驗證,您會看到禁止響應。這是由設計(閱讀更多 here)。
你可以通過檢查403個狀態碼來處理這個被禁止的情況,我們提供了一個這樣的過濾框:
[ResourceAuthorize("Write", "ContactDetails")] [HandleForbidden] public ActionResult UpdateContact() { ViewBag.Message = "Update your contact details!"; return View(); }
添加HandleForbidden 后,看起是這樣:
你也可以使用授權管理命令檢查權限,這樣更靈活:
[HandleForbidden] public ActionResult UpdateContact() { if (!HttpContext.CheckAccess("Write", "ContactDetails", "some more data")) { // either 401 or 403 based on authentication state return this.AccessDenied(); } ViewBag.Message = "Update your contact details!"; return View(); }
添加注銷功能
添加注銷功能很簡單,直接創建一個Action並且調用 Request.GetOwinContext().Authentication.SignOut()方法即可。
public ActionResult Logout() { Request.GetOwinContext().Authentication.SignOut(); return Redirect("/"); }
這個方法會通知identityserver endsession 節點,它將清除身份驗證Cookie並終止您的會話:
通常,現在最安全的事情是簡單地關閉瀏覽器窗口,以清除所有的會話數據。
有時候我們需要注銷后以匿名的方式保持訪問網站,這需要一些步驟,首先你需要登記注銷手續后,返回一個有效的URL是完整的。這是在Client中定義的(注意PostLogoutRedirectUris的
設置):
new Client { Enabled = true, ClientName = "MVC Client", ClientId = "mvc", Flow = Flows.Implicit, RedirectUris = new List<string> { "https://localhost:44319/" }, PostLogoutRedirectUris = new List<string> { "https://localhost:44319/" } }
下一步,客戶已經證明身份注銷端點來確保我們重定向到正確的URL(而不是一些垃圾郵件/釣魚頁面)。這是通過發送在身份驗證過程中接收的客戶端發送的初始標識令牌的。到目前為止,我們已經注銷了這個令牌,現在是時候改變claims 轉換邏輯來保存它。這是通過添加這行代碼來完成我們的securitytokenvalidated通知:
// keep the id_token for logout nid.AddClaim(new Claim("id_token", n.ProtocolMessage.IdToken));
最后一步,我們將附加一個id_token用於和identityserver通信。這是通過使用中間件來做的:
RedirectToIdentityProvider = n => { if (n.ProtocolMessage.RequestType == OpenIdConnectRequestType.LogoutRequest) { var idTokenHint = n.OwinContext.Authentication.User.FindFirst("id_token"); if (idTokenHint != null) { n.ProtocolMessage.IdTokenHint = idTokenHint.Value; } } return Task.FromResult(0); }
做好這些事情后,identityserver注銷頁面會給用戶一個鏈接返回到調用應用程序:
提示:在IdentityServerOptions
配置項中有個AuthenticationOptions配置項,你可以將他賦值為EnablePostSignOutAutoRedirect,登出后將自動重定向到客戶端。
添加Google賬號登陸
下一步我們要啟用第三方身份驗證。這是通過添加額外Owin認證中間件identityserver -在我們的例子將使用谷歌。
首先需要在Google開發者控制台https://console.developers.google.com創建一個項目:
下一步啟用Google+ API
下一步配置與電子郵件地址和產品名稱
下一步創建一個應用程序
在創建客戶端應用程序后,會得到一個 client id 和 client secret。把這兩個值配置到Owin中間件中
private void ConfigureIdentityProviders(IAppBuilder app, string signInAsType) { app.UseGoogleAuthentication(new GoogleOAuth2AuthenticationOptions { AuthenticationType = "Google", Caption = "Sign-in with Google", SignInAsAuthenticationType = signInAsType, ClientId = "...", ClientSecret = "..." }); }
下一步配置IdentityServer授權選項,使用上面的身份提供程序:
idsrvApp.UseIdentityServer(new IdentityServerOptions { SiteName = "Embedded IdentityServer", SigningCertificate = LoadCertificate(), Factory = new IdentityServerServiceFactory() .UseInMemoryUsers(Users.Get()) .UseInMemoryClients(Clients.Get()) .UseInMemoryScopes(Scopes.Get()), AuthenticationOptions = new IdentityServer3.Core.Configuration.AuthenticationOptions { IdentityProviders = ConfigureIdentityProviders } });
完成后在登錄界面會有一個Google登錄的按鈕:
注意:在使用谷歌賬號登陸后,角色role
claim 丟失了。這是有道理的,因為谷歌沒有角色的概念,不是所有的身份提供程序將提供相同的claim 類型。