IdentityServer4系列 | 简化模式
一、前言
從上一篇關于資源密碼憑證模式中,通過使用client_id和client_secret以及用戶名密碼通過應用Client(客戶端)直接獲取,從而請求獲取受保護的資源,但是這種方式存在client可能存了用戶密碼這不安全性問題,所以需要做到client是高可信的應用。因此,我們可以考慮通過其他方式來解決這個問題。
我們通過Oauth2.0的「簡化授權」模式了解到,可以使用這種方式來解決這個問題,讓用戶自己在IdentityServer服務器進行登錄驗證,客戶端不需要知道用戶的密碼,從而實現用戶密碼的安全性。
所以在這一篇中,我們將通過多種授權模式中的「簡化授權」模式進行說明,主要針對介紹「IdentityServer」保護API的資源,「簡化授權」訪問API資源。
二、初識
?有些 Web 應用是純前端應用,沒有后端,必須將令牌儲存在前端。RFC 6749 就規定了這種方式,允許直接向前端頒發令牌。這種方式沒有授權碼這個中間步驟,所以稱為(授權碼)"簡化"(implicit)。
?「簡化模式」(implicit grant type)「不通過第三方應用程序的服務器」,直接在瀏覽器中向認證服務器申請令牌,跳過了"授權碼"這個步驟(授權碼模式后續會說明)。所有步驟在瀏覽器中完成,令牌對訪問者是可見的,且客戶端不需要認證。
?這種方式把令牌直接傳給前端,是很不安全的。因此,只能用于一些安全要求不高的場景,并且令牌的有效期必須非常短,通常就是會話期間(session)有效,瀏覽器關掉,令牌就失效了。
?2.1 適用范圍
這種模式的使用場景是基于瀏覽器的應用
?這種模式基于安全性考慮,建議把token時效設置短一些, 不支持refresh token
?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到授權服務器;
(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 協議,因此存在"中間人攻擊"的風險,而瀏覽器跳轉時,錨點不會發到服務器,就減少了泄漏令牌的風險。
?用戶使用這個令牌訪問資源服務器,當令牌失效時使用刷新令牌去換取新的令牌
三、實踐
?在示例實踐中,我們將創建一個授權訪問服務,定義一個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?configbuilder.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?securebuilder.AddDeveloperSigningCredential();}3.1.4 配置管道
在startup.cs中Configure方法添加如下代碼:
????????public?void?Configure(IApplicationBuilder?app,?IWebHostEnvironment?env){if?(env.IsDevelopment()){app.UseDeveloperExceptionPage();}app.UseRouting();app.UseIdentityServer();app.UseEndpoints(endpoints?=>{endpoints.MapGet("/",?async?context?=>{await?context.Response.WriteAsync("Hello?World!");});});}以上內容是快速搭建簡易IdentityServer項目服務的方式。
「這搭建 Authorization Server 服務跟上一篇資源密碼憑證模式有何不同之處呢?」
?在Config中配置客戶端(client)中定義了一個AllowedGrantTypes的屬性,這個屬性決定了Client可以被哪種模式被訪問,「GrantTypes.Implicit」為「簡化授權」。所以在本文中我們需要添加一個Client用于支持簡化授權(「implicit」)。
「簡化授權不通過第三方應用程序的服務器」,直接在瀏覽器中向認證服務器申請令牌,所有步驟在瀏覽器中完成,所以需要配置對應的回調地址和登出地址。這也是不同于之前的「資源所有者憑證模式」。
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;});} ?AddAuthentication注入添加認證授權,當需要用戶登錄時,使用 cookie 來本地登錄用戶(通過“Cookies”作為DefaultScheme),并將 DefaultChallengeScheme 設置為“oidc”,
使用 AddCookie 添加可以處理 cookie 的處理程序。
因為「簡化模式」的實現是就是 OpenID Connect,所以在AddOpenIdConnect用于配置執行 OpenID Connect 協議的處理程序。Authority表明之前搭建的 IdentityServer 授權服務地址。然后我們通過ClientId。識別這個客戶端。SaveTokens用于在 cookie 中保留來自IdentityServer 的令牌。
3.2.4 配置管道
然后要確保認證服務執行對每個請求的驗證,加入UseAuthentication和UseAuthorization到Configure中,在startup.cs中Configure方法添加如下代碼:
????????public?void?Configure(IApplicationBuilder?app,?IWebHostEnvironment?env){if?(env.IsDevelopment()){app.UseDeveloperExceptionPage();}????app.UseRouting();app.UseCookiePolicy();app.UseAuthentication();app.UseAuthorization();app.UseEndpoints(endpoints?=>{endpoints.MapDefaultControllerRoute();});} ?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.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的影響以及處理方案
?五、總結
本篇主要闡述以「簡化授權」,編寫一個MVC客戶端,并通過客戶端以瀏覽器的形式請求「IdentityServer」上請求獲取訪問令牌,從而訪問資源。
「簡化模式」解決了客戶端模式用戶身份驗證和授權的問題,也解決了上一篇中「資源所有者密碼憑證授權」面臨的用戶密碼暴露的問題,是基于瀏覽器的應用。但由于token攜帶在url中,安全性方面不能保證,建議把token時效設置短一些
在后續會對在安全性方面做得更好的模式進行說明,數據庫持久化問題,以及如何應用在API資源服務器中和配置在客戶端中,會進一步說明。
如果有不對的或不理解的地方,希望大家可以多多指正,提出問題,一起討論,不斷學習,共同進步。
項目地址
https://github.com/i3yuan/Yuan.IdentityServer4.Demo/tree/main/DiffAuthMode/ImplicitMVC
六、附加
「OpenID Connect」資料
「Implicit Grant資料」
「samesite問題解決」
創作挑戰賽新人創作獎勵來咯,堅持創作打卡瓜分現金大獎總結
以上是生活随笔為你收集整理的IdentityServer4系列 | 简化模式的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 如何在 C# 中使用 AutoMappe
- 下一篇: 学习搭建 Consul 服务发现与服务网