IdentityServer4系列 | 簡化模式

1、前言

從上一篇關於資源密碼憑證模式中,經過使用client_id和client_secret以及用戶名密碼經過應用Client(客戶端)直接獲取,從而請求獲取受保護的資源,可是這種方式存在client可能存了用戶密碼這不安全性問題,因此須要作到client是高可信的應用。所以,咱們能夠考慮經過其餘方式來解決這個問題。javascript

咱們經過Oauth2.0的簡化受權模式瞭解到,可使用這種方式來解決這個問題,讓用戶本身在IdentityServer服務器進行登陸驗證,客戶端不須要知道用戶的密碼,從而實現用戶密碼的安全性。html

因此在這一篇中,咱們將經過多種受權模式中的簡化受權模式進行說明,主要針對介紹IdentityServer保護API的資源,簡化受權訪問API資源。前端

2、初識

有些 Web 應用是純前端應用,沒有後端,必須將令牌儲存在前端。RFC 6749 就規定了這種方式,容許直接向前端頒發令牌。這種方式沒有受權碼這個中間步驟,因此稱爲(受權碼)"簡化"(implicit)。java

 簡化模式(implicit grant type)不經過第三方應用程序的服務器,直接在瀏覽器中向認證服務器申請令牌,跳過了"受權碼"這個步驟(受權碼模式後續會說明)。全部步驟在瀏覽器中完成,令牌對訪問者是可見的,且客戶端不須要認證。git

這種方式把令牌直接傳給前端,是很不安全的。所以,只能用於一些安全要求不高的場景,而且令牌的有效期必須很是短,一般就是會話期間(session)有效,瀏覽器關掉,令牌就失效了。github

2.1 適用範圍

這種模式的使用場景是基於瀏覽器的應用數據庫

這種模式基於安全性考慮,建議把token時效設置短一些, 不支持refresh tokenc#

2.2 受權流程:

+----------+
     | Resource |
     |  Owner   |
     |          |
     +----------+
          ^
          |
         (B)
     +----|-----+          Client Identifier     +---------------+
     |         -+----(A)-- & Redirection URI --->|               |
     |  User-   |                                | Authorization |
     |  Agent  -|----(B)-- User authenticates -->|     Server    |
     |          |                                |               |
     |          |<---(C)--- Redirection URI ----<|               |
     |          |          with Access Token     +---------------+
     |          |            in Fragment
     |          |                                +---------------+
     |          |----(D)--- Redirection URI ---->|   Web-Hosted  |
     |          |          without Fragment      |     Client    |
     |          |                                |    Resource   |
     |     (F)  |<---(E)------- Script ---------<|               |
     |          |                                +---------------+
     +-|--------+
       |    |
      (A)  (G) Access Token
       |    |
       ^    v
     +---------+
     |         |
     |  Client |
     |         |
     +---------+

簡化受權流程描述後端

(A)客戶端攜帶客戶端標識以及重定向URI到受權服務器;api

(B)用戶確認是否要受權給客戶端;

(C)受權服務器獲得許可後,跳轉到指定的重定向地址,並將令牌也包含在了裏面;

(D)客戶端不攜帶上次獲取到的包含令牌的片斷,去請求資源服務器;

(E)資源服務器會向瀏覽器返回一個腳本;

(F)瀏覽器會根據上一步返回的腳本,去提取在C步驟中獲取到的令牌;

(G)瀏覽器將令牌推送給客戶端。

2.2.1 過程詳解


訪問令牌請求
參數 是否必須 含義
response_type 必需 表示受權類型,此處的值固定爲"token"
client_id 必需 客戶端ID
redirect_uri 可選 表示重定向的URI
scope 可選 表示受權範圍。
state 可選 表示隨機字符串

(1)資源服務器生成受權URL並將用戶重定向到受權服務器

​ (用戶的操做:用戶訪問https://resourcesServer/index.html跳轉到登陸地址,選擇受權服務器方式登陸)

在受權開始以前,它首先生成state參數(隨機字符串)。client端將須要存儲這個(cookie,會話或其餘方式),以便在下一步中使用。

第一步,A 網站提供一個連接,要求用戶跳轉到 B 網站,受權用戶數據給 A 網站使用。

https://oauth2Server/oauth2/default/v1/authorize?
response_type=token
&client_id=${clientId}
&redirect_uri=https://resourcesServer/implicit.html
&scope=受權範圍
&state=隨機字符串

生成的受權URL如上所述(如上),請求這個地址後重定向訪問受權服務器,其中 response_type參數爲token,表示直接返回令牌。

(2)驗證受權服務器登錄狀態

(用戶的操做:若是未登錄用帳號 User,密碼12345登錄https://oauth2Server/login,若是已登錄受權服務器不須要此步驟)

若是未登錄帳號,自動跳轉到受權服務器登錄地址,登錄受權服務器之後用戶被重定向client端

https://resourcesServer/implicit.html

如已提早登錄受權服務器或受權服務器登錄會話還存在自動重定向到client端

https://resourcesServer/implicit.html

(3)驗證狀態參數

(用戶的操做:無需操做)

用戶被重定向回客戶機,URL中如今有一個片斷包含訪問令牌以及一些其餘信息。

用戶跳轉到 B 網站,登陸後贊成給予 A 網站受權。這時,B 網站就會跳回redirect_uri參數指定的跳轉網址,而且把令牌做爲 URL 參數,傳給 A 網站。

https://resourcesServer/authorization-code.html

\#access_token=&token_type=Bearer&expires_in=3600&scope=photo&state=隨機字符串

其中,token參數就是令牌,A網站所以直接在前端拿到令牌。

注意,令牌的位置是 URL 錨點(fragment),而不是查詢字符串(querystring),這是由於 OAuth 2.0 容許跳轉網址是 HTTP 協議,所以存在"中間人攻擊"的風險,而瀏覽器跳轉時,錨點不會發到服務器,就減小了泄漏令牌的風險。

用戶使用這個令牌訪問資源服務器,當令牌失效時使用刷新令牌去換取新的令牌

3、實踐

在示例實踐中,咱們將建立一個受權訪問服務,定義一個MVC客戶端,MVC客戶端經過IdentityServer上請求訪問令牌,並使用它來訪問API。

3.1 搭建 Authorization Server 服務

搭建認證受權服務

3.1.1 安裝Nuget包

IdentityServer4 程序包

3.1.2 配置內容

創建配置內容文件Config.cs

public static class Config
    {
        public static IEnumerable<IdentityResource> IdentityResources =>
            new IdentityResource[]
            {
                new IdentityResources.OpenId(),
                new IdentityResources.Profile(),
            };


        public static IEnumerable<ApiScope> ApiScopes =>
            new ApiScope[]
            {
                new ApiScope("Implicit_scope1")
            };

        public static IEnumerable<ApiResource> ApiResources =>
            new ApiResource[]
            {

                new ApiResource("api1","api1")
                {
                    Scopes={ "Implicit_scope1" },
                    ApiSecrets={new Secret("apipwd".Sha256())}  //api密鑰
                }
            };

        public static IEnumerable<Client> Clients =>
            new Client[]
            {
                 new Client
                {
                    ClientId = "Implicit_client",
                    ClientName = "Implicit Auth",
                    AllowedGrantTypes = GrantTypes.Implicit,
                    RedirectUris ={
                    "http://localhost:5002/signin-oidc",  //跳轉登陸到的客戶端的地址
                    },
                    PostLogoutRedirectUris ={
                        "http://localhost:5002/signout-callback-oidc",//跳轉登出到的客戶端的地址
                    },      
                    AllowedScopes = {
                           IdentityServerConstants.StandardScopes.OpenId,
                            IdentityServerConstants.StandardScopes.Profile,
                         "Implicit_scope1"
                     },
                      // 是否須要贊成受權 (默認是false)
                      RequireConsent=true
                 }, 
            };
    }

RedirectUris : 登陸成功回調處理的客戶端地址,處理回調返回的數據,能夠有多個。

PostLogoutRedirectUris :跳轉登出到的客戶端的地址。

這兩個都是配置的客戶端的地址,且是identityserver4組件裏面封裝好的地址,做用分別是登陸,註銷的回調

由於是簡化受權的方式,因此咱們經過代碼的方式來建立幾個測試用戶。

新建測試用戶文件TestUsers.cs

public class TestUsers
    {
        public static List<TestUser> Users
        {
            get
            {
                var address = new
                {
                    street_address = "One Hacker Way",
                    locality = "Heidelberg",
                    postal_code = 69118,
                    country = "Germany"
                };

                return new List<TestUser>
                {
                    new TestUser
                    {
                        SubjectId = "1",
                        Username = "i3yuan",
                        Password = "123456",
                        Claims =
                        {
                            new Claim(JwtClaimTypes.Name, "i3yuan Smith"),
                            new Claim(JwtClaimTypes.GivenName, "i3yuan"),
                            new Claim(JwtClaimTypes.FamilyName, "Smith"),
                            new Claim(JwtClaimTypes.Email, "i3yuan@email.com"),
                            new Claim(JwtClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean),
                            new Claim(JwtClaimTypes.WebSite, "http://i3yuan.top"),
                            new Claim(JwtClaimTypes.Address, JsonSerializer.Serialize(address), IdentityServerConstants.ClaimValueTypes.Json)
                        }
                    }
                };
            }
        }
    }

返回一個TestUser的集合。

經過以上添加好配置和測試用戶後,咱們須要將用戶註冊到IdentityServer4服務中,接下來繼續介紹。

3.1.3 註冊服務

在startup.cs中ConfigureServices方法添加以下代碼:

public void ConfigureServices(IServiceCollection services)
        {
            var builder = services.AddIdentityServer()
               .AddTestUsers(TestUsers.Users); //添加測試用戶

            // in-memory, code config
            builder.AddInMemoryIdentityResources(Config.IdentityResources);
            builder.AddInMemoryApiScopes(Config.ApiScopes);
            builder.AddInMemoryApiResources(Config.ApiResources);
            builder.AddInMemoryClients(Config.Clients);

            // not recommended for production - you need to store your key material somewhere secure
            builder.AddDeveloperSigningCredential();
        }

3.1.4 配置管道

在startup.cs中Configure方法添加以下代碼:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseStaticFiles();
           

            app.UseRouting();
            app.UseCookiePolicy();
            app.UseAuthentication();
            app.UseAuthorization();
            app.UseIdentityServer();
            app.UseEndpoints(endpoints =>
            {
                endpoints.MapDefaultControllerRoute();
            }); 
        }

以上內容是快速搭建簡易IdentityServer項目服務的方式。

這搭建 Authorization Server 服務跟上一篇資源密碼憑證模式有何不一樣之處呢?

  1. 在Config中配置客戶端(client)中定義了一個AllowedGrantTypes的屬性,這個屬性決定了Client能夠被哪一種模式被訪問,GrantTypes.Implicit簡化受權。因此在本文中咱們須要添加一個Client用於支持簡化受權(implicit)。
  2. 簡化受權不經過第三方應用程序的服務器,直接在瀏覽器中向認證服務器申請令牌,全部步驟在瀏覽器中完成,因此須要配置對應的回調地址和登出地址。這也是不一樣於以前的資源全部者憑證模式

3.2 搭建MVC 客戶端

實現對客戶端認證受權訪問資源

3.2.1 快速搭建一個MVC項目

3.2.2 安裝Nuget包

IdentityServer4.AccessTokenValidation 包

3.2.3 註冊服務

要將對 OpenID Connect 身份認證的支持添加到MVC應用程序中。

在startup.cs中ConfigureServices方法添加以下代碼:

public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllersWithViews();
        services.AddAuthorization();

        services.AddAuthentication(options =>
            {
                options.DefaultScheme = "Cookies";
                options.DefaultChallengeScheme = "oidc";
            })
               .AddCookie("Cookies")
              .AddOpenIdConnect("oidc", options =>
              {
                  options.Authority = "http://localhost:5001";
                  options.RequireHttpsMetadata = false;
                  options.ClientId = "Implicit_client";
                  options.SaveTokens = true;
                  options.GetClaimsFromUserInfoEndpoint = true;
              });
    }
  1. AddAuthentication注入添加認證受權,當須要用戶登陸時,使用 cookie 來本地登陸用戶(經過「Cookies」做爲DefaultScheme),並將 DefaultChallengeScheme 設置爲「oidc」,

  2. 使用 AddCookie 添加能夠處理 cookie 的處理程序。

  3. 由於簡化模式的實現是就是 OpenID Connect,因此在AddOpenIdConnect用於配置執行 OpenID Connect 協議的處理程序。Authority代表以前搭建的 IdentityServer 受權服務地址。而後咱們經過ClientId。識別這個客戶端。 SaveTokens用於在 cookie 中保留來自IdentityServer 的令牌。

3.2.4 配置管道

而後要確保認證服務執行對每一個請求的驗證,加入UseAuthenticationUseAuthorizationConfigure中,在startup.cs中Configure方法添加以下代碼:

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

            app.UseRouting();

            app.UseCookiePolicy();
            app.UseAuthentication();
            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllerRoute(
                    name: "default",
                    pattern: "{controller=Home}/{action=Index}/{id?}");
            });
        }

UseAuthentication將身份驗證中間件添加到管道中;

UseAuthorization 將啓動受權中間件添加到管道中,以便在每次調用主機時執行身份驗證受權功能。

3.2.5 添加受權

在HomeController控制器並添加[Authorize]特性到其中一個方法。在進行請求的時候,需進行認證受權經過後,才能進行訪問。

[Authorize]
        public IActionResult Privacy()
        {
            ViewData["Message"] = "Secure page.";
            return View();
        }

還要修改主視圖以顯示用戶的Claim以及cookie屬性。

@using Microsoft.AspNetCore.Authentication

<h2>Claims</h2>

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

<h2>Properties</h2>

<dl>
    @foreach (var prop in (await Context.AuthenticateAsync()).Properties.Items)
    {
        <dt>@prop.Key</dt>
        <dd>@prop.Value</dd>
    }
</dl>

訪問 Privacy 頁面,跳轉到認證服務地址,進行帳號密碼登陸,Logout 用於用戶的註銷操做。

3.3 效果

3.3.1 項目測試

4、問題

4.1 SameSite策略

在Chrome瀏覽器中,進行認證受權的時候,用戶登陸以後,沒法跳轉到原網頁,仍是停留在登陸頁中,能夠看控制檯就發現上圖的效果。

最後查找資料發現,是Google將於2020年2月份發佈Chrome 80版本。本次發佈將推動Google的「漸進改良Cookie」策略,打造一個更爲安全和保障用戶隱私的網絡環境。因此本次更新可能致使瀏覽器沒法向服務端發送Cookie。若是你有多個不一樣域名的應用,部分用戶頗有可能出現會話時常被打斷的狀況,還有部分用戶可能沒法正常登出系統。

因此咱們須要解決這個問題:

方法一:將域名升級爲 HTTPS

方法二:使用代碼修改 SameSite 設置

新增 SameSiteCookiesServiceCollectionExtensions 類 (能夠下載源碼查看)

private const SameSiteMode Unspecified = (SameSiteMode)(-1);
 
改成
 
private const SameSiteMode Unspecified = SameSiteMode.Lax;

若是沒有域名或內網環境,可使用該方法,在 Startup 添加引用。

public IServiceProvider ConfigureServices(IServiceCollection services)
{
    ...
    services.ConfigureNonBreakingSameSiteCookies();
    ...

參考資料 Chrome80調整SameSite策略對IdentityServer4的影響以及處理方案

5、總結

  1. 本篇主要闡述以簡化受權,編寫一個MVC客戶端,並經過客戶端以瀏覽器的形式請求IdentityServer上請求獲取訪問令牌,從而訪問資源。
  2. 簡化模式解決了客戶端模式用戶身份驗證和受權的問題,也解決了上一篇中資源全部者密碼憑證受權面臨的用戶密碼暴露的問題,是基於瀏覽器的應用。但因爲token攜帶在url中,安全性方面不能保證,建議把token時效設置短一些
  3. 在後續會對在安全性方面作得更好的模式進行說明,數據庫持久化問題,以及如何應用在API資源服務器中和配置在客戶端中,會進一步說明。
  4. 若是有不對的或不理解的地方,但願你們能夠多多指正,提出問題,一塊兒討論,不斷學習,共同進步。
  5. 項目地址

6、附加

OpenID Connect資料

Implicit Grant資料

samesite問題解決

相關文章
相關標籤/搜索