入門教程:.NET開源OpenID Connect 和OAuth解決方案IdentityServer v3 MVC認證與受權(四)

本教程將搭建一個最小可以運行的IdentityServer。爲簡單起見,咱們將identityserver和客戶端放在同一Web應用程序-這可能不會是一個很現實的狀況下,但可讓你不太複雜的開始。ios

完整的源代碼能夠在這裏找到。git

Part 1 - MVC MVC認證與受權

在第一部分中咱們將建立一個簡單的MVC應用程序並添加認證經過identityserver它。而後,咱們將有一個更仔細的看claims,claims的變化和受權.github

建立一個 web application

在Visual Studio 2013中,建立一個標準的MVC應用程序和設置認證,「沒有認證」。web

你能夠在屬性窗口啓用SSLapi

 

注意:不要忘記更新你的項目屬性中的url瀏覽器

添加 IdentityServer 引用

IdentityServer基於OWIN/Katana做爲NuGet包。要將其添加到新建立的應用程序上,安裝如下2個包:安全

install-package Microsoft.Owin.Host.Systemweb
install-package Thinktecture.IdentityServer3

IdentityServer配置——客戶端

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配置——用戶

下一步咱們將添加一些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")
                }
            }
        };
    }
}

添加 Startup.cs

配置啓動類。在這裏,咱們提供有關客戶信息的用戶,範圍,簽名證書和其餘一些配置選項。生產要從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>

 

添加和配置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 類型。

相關文章
相關標籤/搜索