本教程将搭建一个最小可以运行的IdentityServer。为简单起见,咱们将identityserver和客户端放在同一Web应用程序-这可能不会是一个很现实的状况下,但可让你不太复杂的开始。ios
完整的源代码能够在这里找到。git
在第一部分中咱们将建立一个简单的MVC应用程序并添加认证经过identityserver它。而后,咱们将有一个更仔细的看claims,claims的变化和受权.github
在Visual Studio 2013中,建立一个标准的MVC应用程序和设置认证,“没有认证”。web
你能够在属性窗口启用SSLapi
注意:不要忘记更新你的项目属性中的url浏览器
IdentityServer基于OWIN/Katana做为NuGet包。要将其添加到新建立的应用程序上,安装如下2个包:安全
install-package Microsoft.Owin.Host.Systemweb
install-package Thinktecture.IdentityServer3
IdentityServer须要一些关于客户端信息,这能够简单地提供使用客户端对象:cookie
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用户-这里经过提供一个简单的C#类完成,固然你能够从任何数据存储加载用户。咱们提供了ASP.NET Identity 和MembershipReboot支持检索用户信息。session
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") } } }; } }
配置启动类。在这里,咱们提供有关客户信息的用户,范围,签名证书和其余一些配置选项。生产要从Windows证书存储区或其余固定源负载签名证书。这里咱们简单地添加到项目文件(你能够下载一个测试证书的地方。它添加该项目并将其属性【复制到输出目录】更改成始终复制。mvc
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>
增长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" });
一个受保护的资源:
[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>
点击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看起像这样:
经过配置能够控制哪些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如今看起来像这样:
如今,咱们有身份验证和一些声明,咱们能够开始添加简单的受权规则。
MVC有一个内置的属性称为[Authorize]身份验证的用户,您还可使用此属性来诠释角色成员资格要求。咱们不建议这种方法,由于这一般会致使代码,混合的关注,如业务/控制器逻辑和受权政策。咱们建议将受权逻辑从控制器中分离,从而致使更清洁的代码和更好的可测性(在 here 阅读更多)。
要添加新的受权基础设施和新的属性,咱们添加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());
运行示例,并经过代码来熟悉验证的流程。
经过重写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,登出后将自动重定向到客户端。
下一步咱们要启用第三方身份验证。这是经过添加额外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 类型。