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


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

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

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

第三部分: http://www.cnblogs.com/cgzl/p/7793241.html

上一篇講了使用OpenId Connect進行Authentication.

下面講

Hybrid Flow和Offline Access

目前我們解決方案里面有三個項目 Authorization Server, Web api和Mvc Client. 在現實世界中, 他們可能都在不同的地方.

現在讓我們從MvcClient使用從Authorization Server獲取的token來訪問web api. 並且確保這個token不過期.

現在我們的mvcClient使用的是implicit flow, 也就是說, token 被發送到client. 這種情況下 token的生命可能很短, 但是我們可以重定向到authorization server 重新獲取新的token.

例如, 在SPA(Single Page Application)中, implicit flow基本上就是除了resource owner password flow 以外唯一合適的flow, 但是我們的網站可能會在client(SPA client/或者指用戶)沒使用網站的時候訪問api, 為了這樣做, 不但要保證token不過期, 我們還需要使用別的flow. 我們要介紹一下authorization code flow. 它和implicit flow 很像, 不同的是, 在重定向回到網站的時候獲取的不是access token, 而是從authorization server獲取了一個code, 使用它網站可以交換一個secret, 使用這個secret可以獲取access token和refresh tokens.

Hybrid Flow, 是兩種的混合, 首先identity token通過瀏覽器傳過來了, 然后客戶端可以在進行任何工作之前對其驗證, 如果驗證成功, 客戶端就會再打開一個通道向Authorization Server請求獲取access token.

首先在Authorization server的InMemoryConfiguration添加一個Client:

new Client
                {
                    ClientId = "mvc_code",
                    ClientName = "MVC Code Client",
                    AllowedGrantTypes = GrantTypes.HybridAndClientCredentials,
                    ClientSecrets =
                    {
                        new Secret("secret".Sha256()) },
                    RedirectUris = { "http://localhost:5002/signin-oidc" },
                    PostLogoutRedirectUris = { "http://localhost:5002/signout-callback-oidc" },
                    AllowedScopes = new List<string>
                    {
                        IdentityServerConstants.StandardScopes.OpenId,
                        IdentityServerConstants.StandardScopes.Profile,
                        IdentityServerConstants.StandardScopes.Email,
                        "socialnetwork"
                    },
                    AllowOfflineAccess = true,
                    AllowAccessTokensViaBrowser = true
                }

 

首先肯定要修改一下ClientId.

GrantType要改成Hybrid或者HybrdAndClientCredentials, 如果只使用Code Flow的話不行, 因為我們的網站使用Authorization Server來進行Authentication, 我們想獲取Access token以便被授權來訪問api. 所以這里用HybridFlow.

還需要添加一個新的Email scope, 因為我想改變api來允許我基於email來創建用戶的數據, 因為authorization server 和 web api是分開的, 所以用戶的數據庫也是分開的. Api使用用戶名(email)來查詢數據庫中的數據.

AllowOfflineAccess. 我們還需要獲取Refresh Token, 這就要求我們的網站必須可以"離線"工作, 這里離線是指用戶和網站之間斷開了, 並不是指網站離線了.

這就是說網站可以使用token來和api進行交互, 而不需要用戶登陸到網站上. 

修改MvcClient的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_code";
                options.ClientSecret = "secret";
                options.ResponseType = "id_token code";
                options.Scope.Add("socialnetwork");
                options.Scope.Add("offline_access");
                options.SaveTokens = true;
                options.GetClaimsFromUserInfoEndpoint = true;
            });
        }

首先改ClientId和Authorization server一致. 這樣用戶訪問的時候和implicit差不多, 只不過重定向回來的時候, 獲取了一個code, 使用這個code可以換取secret然后獲取access token.

所以需要在網站(MvcClient)上指定Client Secret. 這個不要泄露出去.

還需要改變reponse type, 不需要再獲取access token了, 而是code, 這意味着使用的是Authorization Code flow.

還需要指定請求訪問的scopes: 包括 socialnetwork api和離線訪問

最后還可以告訴它從UserInfo節點獲取用戶的Claims.

運行

點擊About, 重定向到Authorization Server:

同時在Authorization Server的控制台可以看見如下信息:

這里可以看到請求訪問的scope, response_type. 還告訴我們respose mode是from_post, 這就是說, 在這登陸后重定向回到網站是使用的form post方式.

然后登陸:

這里可以看到請求訪問的范圍, 包括個人信息和Application Access.

點擊Yes, Allow:

重定向回到了網站. 這里看起來好像和以前一樣. 但是如果看一下Authorization Server的控制台:

就會看到一個request. 中間件發起了一個請求使用Authorization Code和ClientId和secret來換取了Access token.

當Authorization驗證上述信息后, 它就會創建一個token.

打印Refresh Token

修改MvcClient的About.cshtml:

@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>
<div>
    <strong>refresh_token</strong>
    <span>@await ViewContext.HttpContext.GetTokenAsync("refresh_token")</span>
</div>
<dl>
    @foreach (var claim in User.Claims)
    {
        <dt>@claim.Type</dt>
        <dd>@claim.Value</dd>
    }
</dl>

刷新頁面:

看到了refresh token.

這些token包含了什么時候過期的信息.

如果access token過期了, 就無法訪問api了. 所以需要確保access token不過期. 這就需要使用refresh token了.

復制一下refresh token, 然后使用postman:

使用這個refresh token可以獲取到新的access token和refresh_token, 當這個access_token過期的時候, 可以使用refresh_token再獲取一個access_token和refresh_token......

而如果使用同一個refresh token兩次, 就會得到下面的結果:

看看Authorization Server的控制台, 顯示是一個invalid refresh token:

所以說, refresh token是一次性的.

獲取自定義Claims

web api 要求request請求提供access token, 以證明請求的用戶是已經授權的. 現在我們准備從Access token里面提取一些自定義的Claims, 例如Email.

看看Authorization Server的Client配置:

Client的AllowedScopes已經包括了Email. 但是還沒有配置Authorization Server允許這個Scope. 所以需要修改GetIdentityResources()(我自己的代碼可能改名成IdentityResources()了):

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

然后需要為TestUser添加一個自定義的Claims;

public static IEnumerable<TestUser> Users()
        {
            return new[]
            {
                new TestUser
                {
                    SubjectId = "1",
                    Username = "mail@qq.com",
                    Password = "password",
                    Claims = new [] { new Claim("email", "mail@qq.com") }
                }
            };
        }

然后需要對MvcClient進行設置, 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_code";
                options.ClientSecret = "secret";
                options.ResponseType = "id_token code";
                options.Scope.Add("socialnetwork");
                options.Scope.Add("offline_access");
                options.Scope.Add("email");
                options.SaveTokens = true;
                options.GetClaimsFromUserInfoEndpoint = true;
            });
        }

添加email scope. 所以MvcClient就會也請求這個scope.

運行:

這時在同意(consent)頁面就會出現email address一欄.

同意之后, 可以看到email已經獲取到了.

使用Access Token調用Web Api

首先在web api項目建立一個IdentityController:

namespace WebApi.Controllers
{
    [Route("api/[controller]")]
    public class IdentityController: Controller
    {
        [Authorize]
        [HttpGet]
        public IActionResult Get()
        {
            var username = User.Claims.First(x => x.Type == "email").Value;
            return Ok(username);
            //return new JsonResult(from c in User.Claims select new { c.Type, c.Value});
        }

    }
}

我們想要通過自定義的claim: email的值.

然后回到mvcClient的HomeController, 添加一個方法:

        [Authorize]
        public async Task<IActionResult> GetIdentity()
        {
            var token = await HttpContext.GetTokenAsync("access_token");
            using (var client = new HttpClient())
            {
                client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
                var content = await client.GetStringAsync("http://localhost:5001/api/identity");
                // var json = JArray.Parse(content).ToString();
                return Ok(new { value = content });
            }
        }

這里首先通過HttpContext獲得access token, 然后在請求的Authorization Header加上Bearer Token.

讓我們運行一下, 並在MvcClient和Web Api里面都設好斷點,

登錄后在瀏覽器輸入 http://localhost:5002/Home/GetIdentity 以執行GetIdenttiy方法, 然后進入Web Api看看斷點調試情況:

由於我們已經授權了, 所以可以看到User的一些claims, 而其中沒有email這個claim. 再運行就報錯了.

這是怎么回事? 我們回到About頁面, 復制一下access_token, 去jwt.io分析一下:

確實沒有email的值, 所以提取不出來.

所以我們需要把email添加到access token的數據里面, 這就需要告訴Authorization Server的Api Resource里面要包括User的Scope, 因為這是Identity Scope, 我們想要把它添加到access token里:

修改Authorization Server的InMemoryConfiguration的ApiResources():

public static IEnumerable<ApiResource> ApiResources()
        {
            return new[]
            {
                new ApiResource("socialnetwork", "社交網絡")
                { UserClaims = new [] { "email" } }
            };
        }

這對這個Api Resouce設置它的屬性UserClaims, 里面寫上email.

然后再運行一下程序, 這里需要重新登陸, 首先分析一下token:

有email了. 

然后執行GetIdentity(), 在web api斷點調試, 可以看到UserClaims已經包含了email:

上面這些如果您不會的話, 需要整理總結一下.

用戶使用Authorization Server去登錄網站(MvcClient), 也就是說用戶從網站跳轉到第三方的系統完成了身份的驗證, 然后被授權可以訪問web api了(這里講的是用戶通過mvcClient訪問api). 當訪問web api的時候, 首先和authorization server溝通確認access token的正確性, 然后就可以成功的訪問api了.

刷新Access Token

根據配置不同, token的有效期可能差別很大, 如果token過期了, 那么發送請求之后就會返回401 UnAuthorized.

當然如果token過期了, 你可以讓用戶重定向到Authorization Server重新登陸,再回來操作, 不過這樣太不友好, 太繁瑣了.

既然我們有refresh token了, 那不如向authorization server請求一個新的access token和refresh token. 然后再把這些更新到cookie里面. 所以下次再調用api的時候使用的是新的token.

在MvcClient的HomeController添加RefreshTokens()方法:

首先需要安裝IdentityModel, 它是OpenIdConnect, OAuth2.0的客戶端庫:

        [Authorize]
        public async Task RefreshTokensAsync()
        {
            var authorizationServerInfo = await DiscoveryClient.GetAsync("http://localhost:5000/");
            var client = new TokenClient(authorizationServerInfo.TokenEndpoint, "mvc_code", "secret");
            var refreshToken = await HttpContext.GetTokenAsync("refresh_token");
            var response = await client.RequestRefreshTokenAsync(refreshToken);
            var identityToken = await HttpContext.GetTokenAsync("identity_token");
            var expiresAt = DateTime.UtcNow + TimeSpan.FromSeconds(response.ExpiresIn);
            var tokens = new[]
            {
                new AuthenticationToken
                {
                    Name = OpenIdConnectParameterNames.IdToken,
                    Value = identityToken
                },
                new AuthenticationToken
                {
                    Name = OpenIdConnectParameterNames.AccessToken,
                    Value = response.AccessToken
                },
                new AuthenticationToken
                {
                    Name = OpenIdConnectParameterNames.RefreshToken,
                    Value = response.RefreshToken
                },
                new AuthenticationToken
                {
                    Name = "expires_at",
                    Value = expiresAt.ToString("o", CultureInfo.InvariantCulture)
                }
            };
            var authenticationInfo = await HttpContext.AuthenticateAsync("Cookies");
            authenticationInfo.Properties.StoreTokens(tokens);
            await HttpContext.SignInAsync("Cookies", authenticationInfo.Principal, authenticationInfo.Properties);
        }

首先使用一個叫做discovery client的東西來獲取Authorization Server的信息. Authorization Server里面有一個discovery節點(endpoint), 可以通過這個地址查看: /.well-known/openid-configuration. 從這里可以獲得很多信息, 例如: authorization節點, token節點, 發布者, key, scopes等等.

然后使用TokenClient, 參數有token節點, clientId和secret. 然后可以使用這個client和refreshtoken來請求新的access token等. 

找到refresh token后, 使用client獲取新的tokens, 返回結果是tokenresponse. 你可以設斷點查看一下token reponse里面都有什么東西, 這里就不弄了, 里面包括identitytoken, accesstoken, refreshtoken等等.

然后需要找到原來的identity token, 因為它相當於是cookie中存儲的主鍵...

然后設置一下過期時間.

然后將老的identity token和新獲取到的其它tokens以及過期時間, 組成一個集合.

然后使用這些tokens來重新登陸用戶. 不過首先要獲取當前用戶的authentication信息, 使用HttpContext.AuthenticateAsync("Cookies"), 參數是AuthenticationScheme. 然后修改屬性, 存儲新的tokens.

最后就是重登錄, 把當前用戶信息的Principal和Properties傳進去. 這就會更新客戶端的Cookies, 用戶也就保持登陸並且刷新了tokens.

先簡單調用一下這個方法:

[Authorize]
        public async Task<IActionResult> GetIdentity()
        {
            await RefreshTokensAsync();
            var token = await HttpContext.GetTokenAsync("access_token");
            using (var client = new HttpClient())
            {
                client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
                var content = await client.GetStringAsync("http://localhost:5001/api/identity");
                //var json = JArray.Parse(content).ToString();
                return Ok(new { value = content });
            }
        }

正式生產環境中可不要這么做, 正式環境中應該在401之后, 調用這個方法, 如果再失敗, 再返回錯誤.

運行一下:

發現獲取的access token是空的, 一定是哪出現了問題, 看一下 authorization server的控制台:

說refresh token不正確(應該是內存數據和cookie數據不匹配). 那就重新登陸.

看斷點, 有token了:

並且和About頁面顯示的不一樣, 說明刷新token了.

也可以看一下authorization server的控制台:

說明成功請求了token.

今天先到這里.


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM