入門教程: MVC 認(rèn)證和WebAPI

術(shù)語http://www.tuicool.com/articles/2mQjIr


本文翻譯自IdentityServer教程茁计,如感覺有不好理解的地方料皇,請參考原文


</br>

本教程將引導(dǎo)你建立一個基礎(chǔ)版的IdentityServer星压。從簡單化角度践剂,本教程將合并IdentityServerClient到同一個Web程序--這不是真實(shí)使用場景,但是可以讓你快速了解IdentityServer的核心概念租幕。

請從此處here獲取完整代碼.

第一節(jié) - MVC 認(rèn)證和授權(quán)

第一節(jié)我們將創(chuàng)建一個簡單的MVC程序舷手,并通過IdentityServer添加認(rèn)證。然后仔細(xì)了解聲明(claims),聲明轉(zhuǎn)換授權(quán)劲绪。

創(chuàng)建Web應(yīng)用程序

在Visual Studio 2015中創(chuàng)建一個標(biāo)準(zhǔn)的MVC應(yīng)用并且設(shè)置認(rèn)證方式為“無認(rèn)證"男窟。


創(chuàng)建MVC應(yīng)用

通過屬性框把項(xiàng)目切換到SSL:


設(shè)置 ssl

重要
不要忘記在項(xiàng)目屬性框中更新啟動URL(https://localhost:44387/)。

添加 IdentityServer

IdentityServer 基于 OWIN/Katana 并且通過 Nuget 分發(fā). 用下面的命令在程序包管理器控制臺添加對應(yīng)的包到剛剛創(chuàng)建的的WEB應(yīng)用中:

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

配置IdentityServer - Clients

添加一個Clients類贾富,使IdentityServer知道支持的Client(客戶)的基本信息:

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:44387/"
                },
                
                AllowAccessToAllScopes = true
            }
        };
    }
}

注意 當(dāng)前客戶可以訪問所有的范圍(Scope) (通過AllowAccessToAllScopes設(shè)置).在生產(chǎn)環(huán)境需要對它限制.后面有更詳細(xì)解釋歉眷。

配置 IdentityServer - Users

下面我們會加一些用戶到IdentityServer --這里我們直接硬編碼一些用戶,生產(chǎn)環(huán)境應(yīng)該從其他數(shù)據(jù)源中獲取颤枪。IdentityServer提供對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

IdentityServer通過startup類來配置。在Startup類中畏纲,我們提供了客戶扇住,用戶,范圍盗胀,簽名證書和其它配置信息艘蹋。生產(chǎn)環(huán)境應(yīng)該從Windows certificates store或者類似的源加載證書。簡單起見我們直接把證書加到項(xiàng)目中票灰,你可以從這里直接下載.添加到工程中女阀,并且設(shè)置為始終復(fù)制.

關(guān)于如何從Azure WebSites裝載證書宅荤,請看這里.

public class Startup
{
    public void Configuration(IAppBuilder app)
    {
        app.Map("/identity", idsrvApp =>
            {
                idsrvApp.UseIdentityServer(new IdentityServerOptions
                {
                    SiteName = "Embedded IdentityServer",
                    SigningCertificate = LoadCertificate(),

                    Factory = new IdentityServerServiceFactory()
                                .UseInMemoryUsers(Users.Get())
                                .UseInMemoryClients(Clients.Get())
                                .UseInMemoryScopes(StandardScopes.All)
                });
            });
    }

    X509Certificate2 LoadCertificate()
    {
        return new X509Certificate2(
            string.Format(@"{0}\bin\idsrv3test.pfx", AppDomain.CurrentDomain.BaseDirectory), "idsrv3test");
    }
}

我們已經(jīng)添加好全功能的IdentityServer, 可以通過發(fā)現(xiàn)端點(diǎn)(discovery endpoint)了解配置情況
https://localhost:44387/identity/.well-known/openid-configuration

disco

RAMMFAR

最后,不要忘記在web.config中添加RAMMFAR支持浸策,否則有些內(nèi)嵌的資源無法在IIS中正常加載:

<system.webServer>
  <modules runAllManagedModulesForAllRequests="true" />
</system.webServer>

添加和配置OpenID Connect 認(rèn)證中間件

支持OIDC認(rèn)證需要另外兩個nuget 程序包:

install-package Microsoft.Owin.Security.Cookies
install-package Microsoft.Owin.Security.OpenIdConnect

在startup.cs中使用缺省值配置cookie中間件

app.UseCookieAuthentication(new CookieAuthenticationOptions
    {
        AuthenticationType = "Cookies"
    });

同樣在startup.cs中配置OpenID Connect中間件冯键,指向我們內(nèi)嵌的IdentityServer并使用我們前面配置的客戶信息:

app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
    {
        Authority = "https://localhost:44387/identity",
        ClientId = "mvc",
        RedirectUri = "https://localhost:44387/",
        ResponseType = "id_token",

        SignInAsAuthenticationType = "Cookies"
    });

添加被保護(hù)的資源和現(xiàn)實(shí)聲明

使用IdentityServer是為了保護(hù)一些資源(頁面,API)的訪問, 本教程中庸汗,我們通過全局授權(quán)過濾器惫确,簡單保護(hù)Home控制器的About頁面,并且顯示哪一個聲明(用戶)在訪問蚯舱。

[Authorize]
public ActionResult About()
{
    return View((User as ClaimsPrincipal).Claims);
}

對應(yīng)的View(About.cshtml)修改如下:

@model IEnumerable<System.Security.Claims.Claim>

<dl>
    @foreach (var claim in Model)
    {
        <dt>@claim.Type</dt>
        <dd>@claim.Value</dd>
    }
</dl>

認(rèn)證和聲明

經(jīng)過上面的設(shè)置后雕薪,在例子程序的主頁上單擊關(guān)于鏈接將激活認(rèn)證機(jī)制,例子程序?qū)@示一個登陸界面--用前面硬編碼的用戶(bob)和密碼(secret)登陸后-- 會發(fā)回一個token到主程序晓淀,OpenID connect中間件驗(yàn)證token,提取聲明信息盏档,然后把聲明信息傳給cookie中間件設(shè)置認(rèn)證cookie凶掰。如下圖用戶現(xiàn)在登陸啦。

譯者注此處需要使用https://localhost:44837/來訪問蜈亩,不能使用http懦窘,否則會反復(fù)認(rèn)證。

login
login
claims
claims

添加角色聲明和范圍

接下來稚配,我們將添加角色聲明來進(jìn)行授權(quán)畅涂。
現(xiàn)在我們離開OIDC的標(biāo)準(zhǔn)范圍--讓我們定義一個包含角色聲明的角色范圍并且添加到標(biāo)準(zhǔn)范圍中。

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中使用新定義的Scope:

Factory = new IdentityServerServiceFactory()
    .UseInMemoryUsers(Users.Get())
    .UseInMemoryClients(Clients.Get())
    .UseInMemoryScopes(Scopes.Get()),

然后我們添加一些角色聲明給硬編碼的Bob:

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")
                }
            }
        };
    }
}

修改中間件配置請求角色信息

OIDC中間件默認(rèn)只要求兩個Scopes:openidprofile -- 這就是為什么IdentityServer包括主題(subject)和名字聲明〉来ǎ現(xiàn)在我們加上roles范圍:

app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
    {
        Authority = "https://localhost:44319/identity",
                    
        ClientId = "mvc",
        Scope = "openid profile roles",
        RedirectUri = "https://localhost:44319/",
        ResponseType = "id_token",

        SignInAsAuthenticationType = "Cookies"
    });

修改編譯成功后午衰,訪問關(guān)于頁面,這是就能看到角色聲明啦冒萄。

角色聲明

Claims transformation

仔細(xì)檢查關(guān)于頁面上的聲明信息臊岸,有兩點(diǎn)引起我們的注意:

  • 有些聲明帶有很長的類型名
  • 很多的聲明信息我們并不需要.

長類型名是由微軟的JWT handler試圖把聲明類型映射到.Net的ClaimTypes類型上。我們可以通過下面的代碼關(guān)閉這個功能(在Startup類里面)尊流。關(guān)閉這個功能后帅戒,對于跨域訪問會有些問題,--例子中不會有問題崖技,但是大部分oauth2服務(wù)會跨域的--,所以我們要調(diào)整反跨站點(diǎn)請求偽造

AntiForgeryConfig.UniqueClaimTypeIdentifier = Constants.ClaimTypes.Subject;
JwtSecurityTokenHandler.InboundClaimTypeMap = new Dictionary<string, string>();

修改后逻住,聲明看起來簡潔多了:

簡潔的聲明

長長的聲明名稱沒了,但是還有很多底層的協(xié)議用的聲明迎献,我們并不需要瞎访。把原始的聲明轉(zhuǎn)換成程序需要的聲明叫做聲明轉(zhuǎn)換。在這個過程中忿晕,我們拿到傳入的全部聲明装诡,選擇那些聲明需要以及從數(shù)據(jù)源中獲取更多的聲明信息以便程序使用银受。

OIDC中間件有一個通知機(jī)制讓我們做聲明轉(zhuǎn)換,轉(zhuǎn)換后的聲明會保存到cookie中。

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 = 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);
                    
                    return Task.FromResult(0);    
                }
        }
    });

最終的聲明信息看起來簡單多了:

transformed claims
transformed claims

授權(quán)

好了鸦采,我們現(xiàn)在認(rèn)證了用戶宾巍,也有了用戶的一些信息,現(xiàn)在我們要加上一些簡單的授權(quán)規(guī)則了渔伯。
MVC有一個內(nèi)置的特性[Authorize]顶霞,用于標(biāo)識需要認(rèn)證的頁面(webapi),通過這個特性,我們也可以標(biāo)識一些角色需求锣吼。
我們不建議使用這個特性选浑,它把業(yè)務(wù)邏輯和權(quán)限策略混在一起啦。分離業(yè)務(wù)邏輯和權(quán)限策略可以讓代碼更清晰玄叠,有更好的可測試性古徒。(關(guān)于這一點(diǎn),請閱讀 參考文件).

資源授權(quán)

添加下面的程序包獲得新的授權(quán)架構(gòu)和特性:

install-package Thinktecture.IdentityModel.Owin.ResourceAuthorization.Mvc

然后我們在HomeContact操作上添加特性读恃,標(biāo)記這個操作會讀取(read) 聯(lián)系人詳情(contactDetaisl)資源:

[ResourceAuthorize("Read", "ContactDetails")]
public ActionResult Contact()
{
    ViewBag.Message = "Your contact page.";

    return View();
}

注意這個特性不是標(biāo)記允許讀聯(lián)系人信息 --- 具體的授權(quán)管理(操作隧膘、資源和誰能夠操作這些資源)從應(yīng)用中分離到AuthorizationManager啦。

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();
        }
    }
}

最后我們把授權(quán)管理放到Startup的OWIN的管道中去:

app.UseResourceAuthorization(new AuthorizationManager());

調(diào)試下這個例子寺惫,單步跟隨代碼疹吃,了解一下整個認(rèn)證授權(quán)過程。

角色授權(quán)

注意西雀,如果你選擇角色授權(quán)[Authorize(Roles = "Foo,Bar")], 當(dāng)前用戶被認(rèn)證但未授權(quán)角色的時候萨驶,有可能會進(jìn)入一種重定向死循環(huán)。因?yàn)?code>Authroize特性發(fā)現(xiàn)訪問用戶被認(rèn)證但未授權(quán)艇肴, 會讓操作返回401未授權(quán)腔呜,401未授權(quán)會重定向到認(rèn)證服務(wù)器(IdentityServer), 而認(rèn)證服務(wù)器又會認(rèn)證這個用戶并重定向回去再悼。噢哦育谬,重定向死循環(huán)開始啦。帮哈。膛檀。。
這個問題可以通過重載Authrize特性的 HandleUnauthorizedRequest方法來解決娘侍。代碼如下:

// 定制授權(quán)特性 Customized authorization attribute:
public class AuthAttribute : AuthorizeAttribute
{
    protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
    {
        if (filterContext.HttpContext.User.Identity.IsAuthenticated)
        {
            // 403 我們知道你是誰咖刃,但是你無權(quán)訪問
            filterContext.Result = new HttpStatusCodeResult(System.Net.HttpStatusCode.Forbidden);
        }
        else
        {
            // 401 你是誰?請登陸后再試
            filterContext.Result = new HttpUnauthorizedResult();
        }
    }
}

// Usage:
[Auth(Roles = "Geek")]
public ActionResult About()
{
    // ...
}

更多的授權(quán)和拒絕訪問場景

讓我們在Home控制器上加一個新的操作憾筏,進(jìn)一步探索授權(quán)過程:

[ResourceAuthorize("Write", "ContactDetails")]
public ActionResult UpdateContact()
{
    ViewBag.Message = "Update your contact details!";

    return View();
}

當(dāng)你訪問 /home/updatecontact URL 你會看到禁止頁面嚎杨。

iis forbidden
iis forbidden

實(shí)際上會有不同的響應(yīng),如果你已經(jīng)登陸氧腰,那么你會看到上述的禁止頁面枫浙,否則刨肃,你會被重定向到登陸頁面。這是認(rèn)證授權(quán)設(shè)計好的流程(進(jìn)一步學(xué)習(xí)請看這里).

可以通過檢查403狀態(tài)碼來處理禁止情況---我們提供了一個現(xiàn)成的filter箩帚,代碼如下:

[ResourceAuthorize("Write", "ContactDetails")]
[HandleForbidden]
public ActionResult UpdateContact()
{
    ViewBag.Message = "Update your contact details!";

    return View();
}

HandleForbidden filter(可以作用在全局)會把未授權(quán)的訪問(403)重定向到一個特殊的視圖----默認(rèn)會得到一個禁止視圖真友。
譯者注需要在view的shared目錄下創(chuàng)建一個Forbidden.cshtml視圖,否則會報告404錯誤紧帕。

forbidden
forbidden

更好的辦法是使用authroization manager進(jìn)行精細(xì)控制:

[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();
}

增加登出功能

在操作中調(diào)用katana認(rèn)證的Signout方法就可以登出盔然,非常簡單明了。

public ActionResult Logout()
{
    Request.GetOwinContext().Authentication.SignOut();
    return Redirect("/");
}

Signout方法調(diào)用 IdentityServerendsession 的方法是嗜,這個方法會清除認(rèn)證cookie和結(jié)束當(dāng)前session愈案。

simple logout
simple logout

一般來說,登出時應(yīng)該關(guān)閉瀏覽器鹅搪,清除所有的會話數(shù)據(jù)站绪。有一些應(yīng)用則希望用戶登出的時候,以一個匿名用戶繼續(xù)留在網(wǎng)站丽柿。通過簡單幾步就可以達(dá)到這個目的崇众,首先注冊一個有效的URL作為登出完成后的地址。這個在MVC應(yīng)用的Client中定義: (注意新的 PostLogoutRedirectUris 設(shè)置):

new Client 
{
    Enabled = true,
    ClientName = "MVC Client",
    ClientId = "mvc",
    Flow = Flows.Implicit,

    RedirectUris = new List<string>
    {
        "https://localhost:44387/"
    },
    PostLogoutRedirectUris = new List<string>
    {
        "https://localhost:44387/"
    }
}

接下來航厚,客戶端需要把登陸時得到的token 發(fā)送給登出方法,以便我們重定向到正確的URL上(不是垃圾郵件地址或者釣魚地址)锰蓬。之前的代碼我們丟棄了這個token幔睬,現(xiàn)在我們要改變聲明轉(zhuǎn)換邏輯來保存它。

這個需要通過在SecurityTokenValidated通知里面增加一行代碼來實(shí)現(xiàn)芹扭。

// keep the id_token for logout
nid.AddClaim(new Claim("id_token", n.ProtocolMessage.IdToken));

最后一步麻顶,我們通過OIDC中間件的通知機(jī)制,登出時把id_token發(fā)送到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);
    }

經(jīng)過上面的改變以后辅肾,IdentityServer將顯示一個鏈接給用戶,通過這個鏈接可以回到最初的Web應(yīng)用:

logout with redirect
logout with redirect

TipIdentityServerOptions上面轮锥,有一個 AuthenticationOptions 對象. 這個對象有一個屬性EnablePostSignOutAutoRedirect. 和你想的一樣矫钓, 把他設(shè)置為true,登出時會自動重定向舍杜,不需要用戶點(diǎn)擊鏈接新娜。

增加Google賬號認(rèn)證

現(xiàn)在我們要提供第三方認(rèn)證功能,首先添加一個katana認(rèn)證中間件給IdentityServer--這里我們使用Google既绩。

在Google登記我們的IdentityServer

我們必須在Google的開發(fā)者控制面板中登記我們的IdentityServer概龄,按照下面的流程一步步做就好。

打開新的瀏覽器饲握,轉(zhuǎn)到下面的鏈接:

https://console.developers.google.com

創(chuàng)建一個新項(xiàng)目

googlecreateproject
googlecreateproject

啟用Google+ API

googleapis
googleapis

同意界面填寫郵件地址和產(chǎn)品名稱

googleconfigureconsent
googleconfigureconsent

創(chuàng)建應(yīng)用程序

googlecreateclient
googlecreateclient

單擊創(chuàng)建用戶ID后私杜,你會得到一個客戶id(Client id)和客戶密鑰(client secret).保存好他們蚕键,在配置google認(rèn)證中間件的時候我們需要用到他們。

增加Google認(rèn)證中間件

通過Nuget程序包管理器控制臺添加Google中間件:

install-package Microsoft.Owin.Security.Google

配置中間件

Startup中添加下述方法:
注意:需要用剛才的客戶Id和密鑰替換代碼中的...

private void ConfigureIdentityProviders(IAppBuilder app, string signInAsType)
{
    app.UseGoogleAuthentication(new GoogleOAuth2AuthenticationOptions
        {
            AuthenticationType = "Google",
            Caption = "Sign-in with Google",
            SignInAsAuthenticationType = signInAsType,

            ClientId = "...",
            ClientSecret = "..."
        });
}

然后我們把IdentityServer的認(rèn)證選項(xiàng)指向這個方法
Next we point our IdentityServer options class to this method:

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
    }
});

代碼修改完成衰粹,下一次用戶登錄時锣光,你會看到右邊有一個"Sign-in with google"的選項(xiàng):

googlesignin
googlesignin

注意使用Google登陸沒有角色聲明信息,因?yàn)間oogle本身沒有角色概念寄猩。 在接受第三方認(rèn)證時嫉晶,需要考慮第三方給的聲明信息可能不全。

第二節(jié)- WebAPI支持

這一節(jié)田篇,我們將增加一個Web API項(xiàng)目到解決方案中去替废。這個API由IdentityServer保護(hù)。我們的MVC應(yīng)用會使用可信子系統(tǒng)代理認(rèn)證方法來調(diào)用這個API泊柬。

添加 Web API 項(xiàng)目

最簡單的增加API項(xiàng)目的方式是添加一個空Web項(xiàng)目

增加一個空Web項(xiàng)目

通過Nuget增加WebAPI和Katana的支持:

install-package Microsoft.Owin.Host.SystemWeb
install-package Microsoft.Aspnet.WebApi.Owin

添加一個測試控制器

下面這個控制器會返回所有的聲明信息給調(diào)用者--我們可以通過這個方法得到token所包含的信息椎镣。

[Route("identity")]
[Authorize]
public class IdentityController : ApiController
{
    public IHttpActionResult Get()
    {
        var user = User as ClaimsPrincipal;
        var claims = from c in user.Claims
                        select new
                        {
                            type = c.Type,
                            value = c.Value
                        };

        return Json(claims);
    }
}

在Startup中連接Web API 和 Security

在所有基于katana的應(yīng)用,配置都發(fā)生在Startup中兽赁。

public class Startup
{
    public void Configuration(IAppBuilder app)
    {
        // web api configuration
        var config = new HttpConfiguration();
        config.MapHttpAttributeRoutes();

        app.UseWebApi(config);
    }
}

我們希望用IdentityServer來保護(hù)我們的API---需要實(shí)現(xiàn)兩件事:

  • 只接受來自IdentityServer的令牌
  • 只接受給API的令牌 - 為了實(shí)現(xiàn)這一點(diǎn)状答,我們給API接口一個名字sampleApi(也叫作用域)

To accomplish that, we add a Nuget packages:
為了達(dá)到這個目標(biāo),我們需要安裝一個Nuget包:

install-package IdentityServer3.AccessTokenValidation

..并在Startup中使用他們:

public class Startup
{
    public void Configuration(IAppBuilder app)
    {
        app.UseIdentityServerBearerTokenAuthentication(new IdentityServerBearerTokenAuthenticationOptions
        {
            Authority = "https://localhost:44319/identity",
            RequiredScopes = new[] { "sampleApi" }
        });
        
        // web api configuration
        var config = new HttpConfiguration();
        config.MapHttpAttributeRoutes();

        app.UseWebApi(config);
    }
}

注意
IdentityServer發(fā)送標(biāo)準(zhǔn)的JWT(JSON Web Tokens),你也可以用無格式的katana JWT中間件來驗(yàn)證他們刀崖。上面安裝的中間件自動用IdentityServer的自動發(fā)現(xiàn)文檔(metadata)來配置自己惊科,用起來比較方便。

在IdentityServer中注冊API

接下來亮钦,我們需要注冊這個API--通過擴(kuò)展作用域來實(shí)現(xiàn)馆截,這次我們增加一個資源作用域:

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")
                }
            },
            new Scope
            {
                Enabled = true,
                DisplayName = "Sample API",
                Name = "sampleApi",
                Description = "Access to a sample API",
                Type = ScopeType.Resource
            }
        };

        scopes.AddRange(StandardScopes.All);

        return scopes;
    }
}

注冊web api客戶端

下面我們要調(diào)用這個API,你可以使用客戶端證書(作為一個服務(wù)賬號)蜂莉,或者使用用戶身份蜡娶。

我們首先使用客戶端證書
第一步,我們注冊為MVC 應(yīng)用一個新的客戶映穗,因?yàn)榘踩矫娴脑蚪颜牛琁dentityServer 只允許每個客戶一個flow。
而我們當(dāng)前的MVC客戶端已經(jīng)使用隱式flow蚁滋,所以我們需要為服務(wù)到服務(wù)的通信創(chuàng)建一個新的客戶宿接。

public static class Clients
{
    public static IEnumerable<Client> Get()
    {
        return new[]
        {
            new Client 
            {
                ClientName = "MVC Client",
                ClientId = "mvc",
                Flow = Flows.Implicit,

                RedirectUris = new List<string>
                {
                    "https://localhost:44319/"
                },
                PostLogoutRedirectUris = new List<string>
                {
                    "https://localhost:44319/"
                },
                AllowedScopes = new List<string>
                {
                    "openid",
                    "profile",
                    "roles",
                    "sampleApi"
                }
            },
            new Client
            {
                ClientName = "MVC Client (service communication)",   
                ClientId = "mvc_service",
                Flow = Flows.ClientCredentials,

                ClientSecrets = new List<Secret>
                {
                    new Secret("secret".Sha256())
                },
                AllowedScopes = new List<string>
                {
                    "sampleApi"
                }
            }
        };
    }
}

備注 上面的代碼片段通過AllowdScopes設(shè)置,限制了不同的客戶端可以訪問的作用域辕录。

調(diào)用API

調(diào)用這個API由兩部分組成:

  • 使用客戶證書從IdentityServer獲得訪問令牌澄阳。
  • 使用訪問令牌調(diào)用API

下面的nuget包可以簡化OAuth2的交互,把它安裝到MVC項(xiàng)目下:(注意不是webapi項(xiàng)目)

install-package IdentityModel

在MVC的Controllers目錄下增加一個新的類 CallApiController. 下面的代碼片段使用服務(wù)端客戶憑據(jù)獲得sampleApi的訪問令牌踏拜。

private async Task<TokenResponse> GetTokenAsync()
{
    var client = new TokenClient(
        "https://localhost:44319/identity/connect/token",
        "mvc_service",
        "secret");

    return await client.RequestClientCredentialsAsync("sampleApi");
}

下面的代碼片段使用訪問令牌調(diào)用web Api獲得identity信息:

private async Task<string> CallApi(string token)
{
    var client = new HttpClient();
    client.SetBearerToken(token);

    var json = await client.GetStringAsync("https://localhost:44321/identity");
    return JArray.Parse(json).ToString();
}

加上一個視圖和對應(yīng)的控制方法碎赢,調(diào)用上述輔助方法,一個新的顯示聲明的操作就okay啦速梗。代碼如下:

public class CallApiController : Controller
{
    // GET: CallApi/ClientCredentials
    public async Task<ActionResult> ClientCredentials()
    {
        var response = await GetTokenAsync();
        var result = await CallApi(response.AccessToken);

        ViewBag.Json = result;
        return View("ShowApiResult");
    }

    // helpers omitted
}

創(chuàng)建一個ShowApiResult.cshtml 文件, 簡單的顯示結(jié)果的視圖:

<h2>Result</h2>

<pre>@ViewBag.Json</pre>

訪問這個URL肮塞,結(jié)果如下:


callapiclientcreds
callapiclientcreds

換句話說襟齿,API知道調(diào)用者的信息:

  • 發(fā)布者信息,聽眾和過期時間(通過令牌驗(yàn)證中間件)
  • 令牌在那個作用域里面有效(通過作用域驗(yàn)證中間件)
  • 客戶端ID

令牌包含的所有聲明信息會保存到ClaimsPrincipal枕赵,可以通過.User屬性查看猜欺,使用。

使用登錄用戶的權(quán)限信息

現(xiàn)在我們使用登錄者的權(quán)限信息調(diào)用WebAPI拷窜。在OpenID 連接中間件作用域上面配置上sampleAPI, 同時在期望響應(yīng)類型上加上 token开皿,要求認(rèn)證服務(wù)器返回訪問令牌。

Scope = "openid profile roles sampleApi",
ResponseType = "id_token token"

為了優(yōu)化效率篮昧,IdentityServer發(fā)現(xiàn)請求包括訪問token后赋荆,會把聲明從標(biāo)識令牌中移除,這樣可以減小標(biāo)識令牌的大小懊昨。有了訪問令牌后窄潭,聲明信息可以從用戶信息接口獲取。
從用戶信息結(jié)構(gòu)獲取聲明很簡單酵颁,UserInfoClient類簡化了操作嫉你。另外,我們把訪問令牌放到cookie里面躏惋,要訪問API的時候幽污,我們從cookie中獲取,而不用每次都去認(rèn)證服務(wù)器認(rèn)證簿姨。

譯者注 :標(biāo)識令牌在每次調(diào)用webapi或者請求頁面時都要從客戶端發(fā)到服務(wù)器端距误,太大會影響通訊效率。

SecurityTokenValidated = async n =>
    {
        var nid = new ClaimsIdentity(
            n.AuthenticationTicket.Identity.AuthenticationType,
            Constants.ClaimTypes.GivenName,
            Constants.ClaimTypes.Role);

        // get userinfo data
        var userInfoClient = new UserInfoClient(
            new Uri(n.Options.Authority + "/connect/userinfo"),
            n.ProtocolMessage.AccessToken);

        var userInfo = await userInfoClient.GetAsync();
        userInfo.Claims.ToList().ForEach(ui => nid.AddClaim(new Claim(ui.Item1, ui.Item2)));

        // keep the id_token for logout
        nid.AddClaim(new Claim("id_token", n.ProtocolMessage.IdToken));

        // add access token for sample API
        nid.AddClaim(new Claim("access_token", n.ProtocolMessage.AccessToken));

        // keep track of access token expiration
        nid.AddClaim(new Claim("expires_at", DateTimeOffset.Now.AddSeconds(int.Parse(n.ProtocolMessage.ExpiresIn)).ToString()));

        // add some other app specific claim
        nid.AddClaim(new Claim("app_specific", "some data"));

        n.AuthenticationTicket = new AuthenticationTicket(
            nid,
            n.AuthenticationTicket.Properties);
    }

練習(xí):請重新配置IdentityServer款熬,設(shè)置作用域聲明的AlwaysIncludeInIdToken強(qiáng)制包括一些聲明在標(biāo)識令牌中,無論IdentityServer是否優(yōu)化令牌訪問攘乒。

調(diào)用API

我們現(xiàn)在把訪問令牌保存到了cookie中贤牛,我們可以從聲明對象(claims principal)中取出令牌,并用這個令牌調(diào)用服務(wù)则酝。

// GET: CallApi/UserCredentials
public async Task<ActionResult> UserCredentials()
{
    var user = User as ClaimsPrincipal;
    var token = user.FindFirst("access_token").Value;
    var result = await CallApi(token);

    ViewBag.Json = result;
    return View("ShowApiResult");
}

登陸后殉簸,轉(zhuǎn)到UserCredentials頁面,你會看到sub信息,說明你現(xiàn)在是使用用戶的權(quán)限在訪問API沽讹。
譯者注:sub是用戶的唯一標(biāo)識般卑, 之前使用特定客戶端的權(quán)限的時候,是沒有這個標(biāo)識的爽雄。

userdelegation
userdelegation

現(xiàn)在可以增加一個role的作用域聲明到sampleApi作用域中蝠检。--用戶角色將會包括在訪問令牌中。

new Scope
{
    Enabled = true,
    DisplayName = "Sample API",
    Name = "sampleApi",
    Description = "Access to a sample API",
    Type = ScopeType.Resource,

    Claims = new List<ScopeClaim>
    {
        new ScopeClaim("role")
    }
}
delegationroles
delegationroles
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末挚瘟,一起剝皮案震驚了整個濱河市叹谁,隨后出現(xiàn)的幾起案子饲梭,更是在濱河造成了極大的恐慌,老刑警劉巖焰檩,帶你破解...
    沈念sama閱讀 211,042評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件憔涉,死亡現(xiàn)場離奇詭異,居然都是意外死亡析苫,警方通過查閱死者的電腦和手機(jī)兜叨,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,996評論 2 384
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來衩侥,“玉大人国旷,你說我怎么就攤上這事《倨梗” “怎么了议街?”我有些...
    開封第一講書人閱讀 156,674評論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長璧榄。 經(jīng)常有香客問我特漩,道長,這世上最難降的妖魔是什么骨杂? 我笑而不...
    開封第一講書人閱讀 56,340評論 1 283
  • 正文 為了忘掉前任涂身,我火速辦了婚禮,結(jié)果婚禮上搓蚪,老公的妹妹穿的比我還像新娘蛤售。我一直安慰自己,他們只是感情好妒潭,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,404評論 5 384
  • 文/花漫 我一把揭開白布悴能。 她就那樣靜靜地躺著,像睡著了一般雳灾。 火紅的嫁衣襯著肌膚如雪漠酿。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,749評論 1 289
  • 那天谎亩,我揣著相機(jī)與錄音炒嘲,去河邊找鬼。 笑死匈庭,一個胖子當(dāng)著我的面吹牛夫凸,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播阱持,決...
    沈念sama閱讀 38,902評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼夭拌,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起啼止,我...
    開封第一講書人閱讀 37,662評論 0 266
  • 序言:老撾萬榮一對情侶失蹤蚊夫,失蹤者是張志新(化名)和其女友劉穎智袭,沒想到半個月后哀澈,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體与帆,經(jīng)...
    沈念sama閱讀 44,110評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,451評論 2 325
  • 正文 我和宋清朗相戀三年巩那,在試婚紗的時候發(fā)現(xiàn)自己被綠了吏夯。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,577評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡即横,死狀恐怖噪生,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情东囚,我是刑警寧澤跺嗽,帶...
    沈念sama閱讀 34,258評論 4 328
  • 正文 年R本政府宣布,位于F島的核電站页藻,受9級特大地震影響桨嫁,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜份帐,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,848評論 3 312
  • 文/蒙蒙 一璃吧、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧废境,春花似錦畜挨、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,726評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至驮宴,卻和暖如春逮刨,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背幻赚。 一陣腳步聲響...
    開封第一講書人閱讀 31,952評論 1 264
  • 我被黑心中介騙來泰國打工禀忆, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留臊旭,地道東北人落恼。 一個月前我還...
    沈念sama閱讀 46,271評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像离熏,于是被迫代替她去往敵國和親佳谦。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,452評論 2 348

推薦閱讀更多精彩內(nèi)容