使用Identity Server 4建立Authorization Server (3)

solenovex發表於2017-11-06

預備知識: http://www.cnblogs.com/cgzl/p/7746496.html

第一部分: http://www.cnblogs.com/cgzl/p/7780559.html

第二部分: http://www.cnblogs.com/cgzl/p/7788636.html

上一部分簡單的弄了個web api 並通過Client_Credentials和ResourceOwnerPassword兩種方式獲取token然後進行api請求.

這次講一下Authentication 身份驗證 (而Authorization是授權, 注意區分), 使用的是OpenIdConnect.

這次我們使用的是一個MVC客戶端. 

建立MVC客戶端專案

在同一個解決方案建立一個名字叫MvcClient的asp.net core mvc 專案:

不要配置Authentication(身份驗證), 應該是沒有驗證.

修改執行方式為控制檯, 埠改為5002, 也就是修改launchSettings.json, 把IISExpress相關的去掉:

{
  "profiles": {
    "MvcClient": {
      "commandName": "Project",
      "launchBrowser": true,
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      },
      "applicationUrl": "http://localhost:5002/"
    }
  }
}

在HomeController的About方法上面新增Authorize屬性:

        [Authorize]
        public IActionResult About()
        {
            ViewData["Message"] = "Your application description page.";

            return View();
        }

然後設定解決方案的啟動專案為MvcClient和Authorization Server, 解決方案右鍵屬性:

然後執行專案, 在http://localhost:5002 裡點選About選單:

就會出現以下異常 (500):

我們現在要做的就是, 使用者點選About之後, 頁面重定向到Authorization Server, 使用者填寫完資訊之後登陸到Authorization Server之後再重定向回到該網站(MvcClient).

這也意味著使用者是在Authorization Server使用使用者名稱和密碼, 而MvcClient不儲存使用者的使用者名稱和密碼.

下面就開始配置

新增OpenId Connect Authentication

在Startup的ConfigureServices裡面新增:

public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();

            JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
            services.AddAuthentication(options =>
            {
                options.DefaultScheme = "Cookies";
                options.DefaultChallengeScheme = "oidc";
            })
            .AddCookie("Cookies")
            .AddOpenIdConnect("oidc", options =>
            {
                options.SignInScheme = "Cookies";

                options.Authority = "http://localhost:5000";
                options.RequireHttpsMetadata = false;

                options.ClientId = "mvc_implicit";
                options.SaveTokens = true;
            });
        }

JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); 這句話是指, 我們關閉了JWT的Claim 型別對映, 以便允許well-known claims.

這樣做, 就保證它不會修改任何從Authorization Server返回的Claims.

AddAuthentication()方法是像DI註冊了該服務.

這裡我們使用Cookie作為驗證使用者的首選方式: DefaultScheme = "Cookies".

而把DefaultChanllangeScheme設為"oidc"是因為, 當使用者需要登陸的時候, 將使用的是OpenId Connect Scheme.

然後的AddCookie, 是表示新增了可以處理Cookie的處理器(handler).

最後AddOpenIdConnect是讓上面的handler來執行OpenId Connect 協議.

其中的Authority是指信任的Identity Server ( Authorization Server).

ClientId是Client的識別標誌. 目前Authorization Server還沒有配置這個Client, 一會我們再弄.

Client名字也暗示了我們要使用的是implicit flow, 這個flow主要應用於客戶端應用程式, 這裡的客戶端應用程式主要是指javascript應用程式. implicit flow是很簡單的重定向flow, 它允許我們重定向到authorization server, 然後帶著id token重定向回來, 這個 id token就是openid connect 用來識別使用者是否已經登陸了. 同時也可以獲得access token. 很明顯, 我們不希望access token出現在那個重定向中. 這個一會再說.

一旦OpenId Connect協議完成, SignInScheme使用Cookie Handler來發布Cookie (中介軟體告訴我們已經重定向回到MvcClient了, 這時候有token了, 使用Cookie handler來處理).

SaveTokens為true表示要把從Authorization Server的Reponse中返回的token們持久化在cookie中.

注意正式生產環境要使用https, 這裡就不用了.

接下來在Startup的Configure方法配置中介軟體, 以確保每次請求都執行authentication:

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
                app.UseBrowserLink();
            }
            else
            {
                app.UseExceptionHandler("/Home/Error");
            }

            app.UseAuthentication();

            app.UseStaticFiles();
            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}");
            });
        }

注意在管道配置的位置一定要在useMVC之前.

在Authorization Server新增Client

在Authorization Server的InMemoryConfiguration裡面新增Client:

public static IEnumerable<Client> Clients()
        {
            return new[]
            {
                new Client
                {
                    ClientId = "socialnetwork",
                    ClientSecrets = new [] { new Secret("secret".Sha256()) },
                    AllowedGrantTypes = GrantTypes.ResourceOwnerPasswordAndClientCredentials,
                    AllowedScopes = new [] { "socialnetwork" }
                },
                new Client
                {
                    ClientId = "mvc_implicit",
                    ClientName = "MVC Client",
                    AllowedGrantTypes = GrantTypes.Implicit,
                    RedirectUris = { "http://localhost:5002/signin-oidc" },
                    PostLogoutRedirectUris = { "http://localhost:5002/signout-callback-oidc" },
                    AllowedScopes = new List<string>
                    {
                        IdentityServerConstants.StandardScopes.OpenId,
                        IdentityServerConstants.StandardScopes.Profile,
               "socialnetwork" } }
}; }

ClientId要和MvcClient裡面指定的名稱一致.

OAuth是使用Scopes來劃分Api的, 而OpenId Connect則使用Scopes來限制資訊, 例如使用offline access時的Profile資訊, 還有使用者的其他細節資訊. 

這裡GrantType要改為Implicit. 使用Implicit flow時, 首先會重定向到Authorization Server, 然後登陸, 然後Identity Server需要知道是否可以重定向回到網站, 如果不指定重定向返回的地址的話, 我們的Session有可能就會被劫持. 

RedirectUris就是登陸成功之後重定向的網址, 這個網址(http://localhost:5002/signin-oidc)在MvcClient裡, openid connect中介軟體使用這個地址就會知道如何處理從authorization server返回的response. 這個地址將會在openid connect 中介軟體設定合適的cookies, 以確保配置的正確性.

而PostLogoutRedirectUris是登出之後重定向的網址. 有可能發生的情況是, 你登出網站的時候, 會重定向到Authorization Server, 並允許從Authorization Server也進行登出動作.

最後還需要指定OpenId Connect使用的Scopes, 之前我們指定的socialnetwork是一個ApiResource. 而這裡我們需要新增的是讓我們能使用OpenId Connect的SCopes, 這裡就要使用Identity Resources. Identity Server帶了幾個常量可以用來指定OpenId Connect預包裝的Scopes. 上面的AllowedScopes設定的就是我們要用的scopes, 他們包括 openid Connect和使用者的profile, 同時也包括我們之前寫的api resource: "socialnetwork". 要注意區分, 這裡有Api resources, 還有openId connect scopes(用來限定client可以訪問哪些資訊), 而為了使用這些openid connect scopes, 我們需要設定這些identity resoruces, 這和設定ApiResources差不多:

        public static IEnumerable<IdentityResource> GetIdentityResources()
        {
            return new List<IdentityResource>
            {
                new IdentityResources.OpenId(),
                new IdentityResources.Profile(),
            };
        }

然後我們需要配置Authorization Server來允許使用這些Identity Resources, Statup的:

public void ConfigureServices(IServiceCollection services)
        {
            services.AddIdentityServer()
                //.AddDeveloperSigningCredential()
                .AddSigningCredential(new X509Certificate2(@"D:\Projects\test\socialnetwork.pfx", "Bx@steel"))
                .AddInMemoryIdentityResources(InMemoryConfiguration.GetIdentityResources())
                .AddTestUsers(InMemoryConfiguration.Users().ToList())
                .AddInMemoryClients(InMemoryConfiguration.Clients())
                .AddInMemoryApiResources(InMemoryConfiguration.ApiResources());

            services.AddMvc();
        }

設定完了, 執行一下, 點選About選單就會重定向到Authorization Server:

注意看URL, 我們確實是在Authorization Server.

然後輸入使用者名稱密碼(TestUser的), 會看見一個請求允許的畫面:

可以看到網站請求了Profile資訊和User Identity. 

這個時候看上面選單處, 可以發現使用者已經成功登陸了Authorization Server:

所以這就允許我們做SSO(Single Sign-On) 單點登入了. 這時候其他使用這個Authorization Server的Client應用, 由於使用者已經登陸到Authorization Server了, 只需要請求使用者的許可來訪問使用者的資料就行了.

然後點選同意 Yes Allow, 就會重定向返回MvcClient網站的About頁面:

在View中顯示Claims

 開啟MvcClient的About.cshtml:

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

顯示所有使用者的Claims. Claims就是從Authorization Server返回的Payload裡面的資料.

執行進入About頁面:

嗯當前使用者有這些資訊....

想要從MvcClient呼叫WebApi

我們現在想從MvcClient呼叫WebApi的api/Values節點, 這就需要使用從Authorization Server返回的token. 但是由於我們使用的是implicit flow, 而使用implicit flow, 一切資料都是被髮送到Client的. 這就是說, 為了讓MvcClient知道使用者已經成功登陸了, Authorization Server將會告訴Client(Chrome瀏覽器)重定向回到MvcClient網站, 並附帶著資料. 這意味著token和其他安全資訊將會在瀏覽器裡面被傳遞. 也就是說從Authorization Server傳送access token的時候, 如果有人監聽的話就會看見這些資料, 使用ssl能有效阻止監聽到資料. 當然肯定有辦法解決這個問題, 例如使用其他flow. 但是有時候還是必須要使用implicit flow 獲取到access token. 我們需要做的就是告訴Authorization Server可以使用implicit flow來獲取token.

首先我們把token顯示出來:

@using Microsoft.AspNetCore.Authentication
<div>
    <strong>id_token</strong>
    <span>@await ViewContext.HttpContext.GetTokenAsync("id_token")</span>
</div>
<div>
    <strong>access_token</strong>
    <span>@await ViewContext.HttpContext.GetTokenAsync("access_token")</span>
</div>
<dl>
    @foreach (var claim in User.Claims)
    {
        <dt>@claim.Type</dt>
        <dd>@claim.Value</dd>
    }
</dl>

id_token是openid connect指定的, 你需要從authorization server獲得它, 用來驗證你的身份, 知道你已經登陸了. id_token不是你用來訪問api的.

access_token是用來訪問api的.

執行一下:

可以看到id_token有了, 而access_token沒有, 這是因為我們還沒有告訴Authorization Server在使用implicit flow時可以允許返回Access token.

修改Authorization Server的Client來允許返回Access Token

new Client
                {
                    ClientId = "mvc_implicit",
                    ClientName = "MVC Client",
                    AllowedGrantTypes = GrantTypes.Implicit,
                    RedirectUris = { "http://localhost:5002/signin-oidc" },
                    PostLogoutRedirectUris = { "http://localhost:5002/signout-callback-oidc" },
                    AllowedScopes = new List<string>
                    {
                        IdentityServerConstants.StandardScopes.OpenId,
                        IdentityServerConstants.StandardScopes.Profile,
                        "socialnetwork"
                    },
                    AllowAccessTokensViaBrowser = true
                }

在某種情況下還是不建議這麼做.

然後在執行一下:

還是沒有access token. 這是因為我們需要重新登陸來獲取access token. 我們首先需要登出.

實現Logout 登出

在MvcClient的homeController新增方法:

        public async Task Logout()
        {
            await HttpContext.SignOutAsync("Cookies");
            await HttpContext.SignOutAsync("oidc");
        }

這裡需要確保同時登出本地應用(MvcClient)的Cookies和OpenId Connect(去Identity Server清除單點登入的Session).

執行, 在瀏覽器輸入地址: http://localhost:5002/Home/Logout

然後就會跳轉到Identity Server的Logout了的頁面:

這寫到, 點選here可以返回到mvcclient.

點選here, 回到mvcclient, 然後點選About, 重新登陸. 同意, 重定向回來:

還是沒有access token.....

看看authorization server的控制檯:

有個地方寫到返回型別是id_token. 這表示我們要進行的是Authentication.

而我們想要的是既做Authentication又做Authorization. 也就是說我們既要id_token還要token本身.

這麼做, 在MvcClient的CongifureServices:

public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();

            JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
            services.AddAuthentication(options =>
            {
                options.DefaultScheme = "Cookies";
                options.DefaultChallengeScheme = "oidc";
            })
            .AddCookie("Cookies")
            .AddOpenIdConnect("oidc", options =>
            {
                options.SignInScheme = "Cookies";

                options.Authority = "http://localhost:5000";
                options.RequireHttpsMetadata = false;

                options.ClientId = "mvc_implicit";
                options.ResponseType = "id_token token";
                options.SaveTokens = true;
            });
        }

然後重新執行, 退出, 重登入:

這次終於看到access_token了....

現在就可以使用access_token訪問api了.

先寫到這. 明後天繼續.

相關文章