上一篇文章介紹了OAuth2.0以及如何使用.Net來實現基於OAuth的身份驗證,本文是對上一篇文章的補充,主要是介紹OAuth與Jwt以及OpenID Connect之間的關係與區別。 本文主要內容有: ● Jwt簡介 ● .Net的Jwt實現 ● OAuth與Jwt ● .Net中使用Jwt ...
上一篇文章介紹了OAuth2.0以及如何使用.Net來實現基於OAuth的身份驗證,本文是對上一篇文章的補充,主要是介紹OAuth與Jwt以及OpenID Connect之間的關係與區別。
本文主要內容有:
● Jwt簡介
● .Net的Jwt實現
● OAuth與Jwt
● .Net中使用Jwt Bearer Token實現OAuth身份驗證
● OAuth與OpenID Connect
註:本章內容源碼下載:https://files.cnblogs.com/files/selimsong/OAuth2Demo_jwt.zip
Jwt簡介
Jwt(Json Web Token)它是一種基於Json用於安全的信息傳輸標準,Jwt具有以下幾個特點:
● 緊湊:Jwt由於是為Web準備的,所以就需要讓數據儘可能小,能夠在Url、Post參數或者Http Header中攜帶Jwt,同時由於數據小,所以也增加了數據傳輸的速度。
● 自包含:在Jwt的playload部分包含了所有應該包含的信息,特別是在Jwt用於身份驗證時playload中包含了用戶必要的身份信息(註:不應該包含敏感信息),這樣在進行身份驗證時就無需去資料庫中查詢用戶信息。
● 可信:Jwt是帶有數字簽名的,可以知道Jwt在傳輸過程中是否被篡改,保證數據是完整的,可用的簽名演算法有RS256(RSA+SHA-256)、HS256(HMAC+SHA-256)等。
Jwt有兩個用途,其一是用於數據交互,因為Jwt是被簽名的,可以保證數據的完整性。另外就是用來攜帶用戶信息進行身份驗證。
Jwt包含三個部分:
● Header:包含了簽名演算法以及令牌類型(預設為JWT)。如:
註:alg以及typ均是縮寫,其目的就是為了減小jwt的大小。
● Playload:包含Jwt所攜帶的信息內容,Playload中包含了3種類型的Claim(聲明)定義,分別是標準的,如iss(issuer,Jwt的發行者)、sub(subject,Jwt所代表的用戶)、aud(audience,Jwt的接收者)、exp(expiration time,Jwt的過期時間),還有一些是公共約定的如: http://www.iana.org/assignments/jwt/jwt.xhtml,另外就是私有自定義的,這些用來存放具體的信息。
Playload的結構如下:
● Signature:包含了Header以及Playload的base64Url編碼後的簽名結果,其計算過程如下:
最終三個部分均使用Base64Url的方式進行編碼後使用符號“.”進行分隔,以下是一個完整Jwt的例子:
註:Jwt中的數據是透明的,既任何人拿到數據都能Base64Url反編碼的形式看到內容,簽名僅僅是保證內容不被纂改,所以不能在Jwt中包含敏感數據。以上例子均來自https://jwt.io/introduction/
.Net的Jwt實現
Jwt是一個標準,在https://jwt.io/上可以看到很多不同語言對Jwt的實現,而.Net的其中一個實現是System.IdentityModel.Tokens.Jwt組件,該組件是由微軟實現的,它有兩個重要的類型分別是:
註:從名稱(IdentityModel)都可以看出,微軟的這個實現主要是用於身份驗證的,如果使用Jwt的目的不是身份驗證可以選擇其它的組件或自定義實現。
● JwtSecurityToken:這個類型是Jwt的一個封裝,它除了包含Jwt的三個要素(Header、Playload、Signature)外,還拓展了一些如Subject、Iusser、Audiences、有效期、簽名演算法、簽名密鑰等重要屬性。
下圖是JwtSecurityToken的部分定義:
● JwtSecurityTokenHandler:該對象用來對Jwt進行操作,如Jwt的創建、驗證( 包含發佈者、接收者、簽名等驗證)、Jwt的序列化與反序列化(字元串形式與對象形式之間的轉換)
下圖是JwtSecurityTokenHandler的部分定義:
OAuth與Jwt
OAuth與Jwt前者是一個授權協議後者是一個信息安全傳輸標準,看起來它們之間並沒有什麼關係,但其實OAuth的Access Token有一種實現方式就是Jwt。
為什麼要使用Jwt來作為OAuth的Access Token?首先來看一下上一篇文章中生成的Access Token:
它是一個加密後的字元串,該字元串包含了用戶的相關信息,但是該字元串只能夠被使用Microsoft.Owin.Security.OAuth組件的應用程式解密(不包括參照源碼的實現),並且還要保證加解密的密鑰是相同的。但是OAuth很多時候是用於一些分散式的場景中,甚至還會使用不同語言來編寫不同的應用、服務。這樣的話上面這種Token的實現方式就無法滿足需求。
所以需要使用Jwt Bearer Token來解決不同應用中的Token識別問題。
.Net中使用Jwt Bearer Token實現OAuth身份驗證
在上一篇文章中提到了Microsoft.Owin.Security.OAuth組件中Access Token的生成實際上是對一個AuthenticationTicket對象序列化並加密後的字元串,而Access Token的驗證則是對加密後的字元串解密並反序列化獲得AuthenticationTicket對象的過程。
而對於Access Token來說無論是Microsoft.Owin.Security.OAuth組件的實現方式還是Jwt,甚至是自定義格式,它的核心都在於如何將用戶信息包含到一個字元串令牌中,並且能夠通過這個字元串令牌還原出正確的用戶信息。對於這一個過程在.Net的Owin身份驗證解決方案中將其抽象為一個ISecureDataFormat<TData>介面,其中身份驗證的泛型TData類型為AuthenticationTicket。下圖是ISecureDataFormat介面的定義,它的兩個方法就是用於進行字元串加密令牌與用戶信息對象之間的轉換,可參考《ASP.NET沒有魔法——ASP.NET Identity的加密與解密》
上一篇文章中也給出了Microsoft.Owin.Security.OAuth組件中,預設對Access Token加解密對象是TicketDataFormat,該對象實際上就是一個實現了ISecureDataFormat介面的類型,用於通過數據保護器來完成數據對象的序列化與加解密的工作,可參考《ASP.NET沒有魔法——ASP.NET Identity的加密與解密》:
可以這樣理解要在.Net中實現基於Jwt Bearer Token的OAuth身份驗證,僅需要在Microsoft.Owin.Security.OAuth組件的基礎上自定義一個ISecureDataFormat<AuthenticationTicket>類型即可。
Jwt主要屬性的說明
實現之前再次對Jwt的一些重要屬性進行說明:
● Issuer:發佈者,Jwt裡面包含並且會進行驗證的信息,Token的發佈者,該發佈者實際上就是身份驗證伺服器本身。
● Audience:觀眾,發佈者生成一個Token是根據觀眾來生成的,因為整個驗證體系是以發佈者為中心的分散式的包含多種應用的,為了保證數據安全一個Token只應該針對其中一個應用有效,所以在驗證Jwt時還要對Audience進行驗證。
● Subject:主題,在身份驗證中一般用於保存用戶信息,如用戶名。
它們三的關係如下圖:
User代表的就是Subject,在OAuth中有Client的概念,OAuth的Client就相當於Audience。之前已經實現了Client的管理,現在為每一個Client添加一個用來數字簽名的密鑰,該密鑰是一個32位byte數組的Base64編碼字元串。另外這裡是使用HMAC演算法來完成對Token的摘要計算。
實現一個基於Jwt的ISecureDataFormat<AuthenticationTicket>
下麵就開始介紹如何來實現這個ISecureDataFormat:
1. 通過Nuget安裝Microsoft.Owin.Security.Jwt組件:
註:微軟實現了一個用於解析Jwt Bearer Token的組件,但是該組件只實現了Unprotect方法,使用這個組件開發可以減少一些工作量。
2. 瞭解Microsoft.Owin.Security.Jwt中JwtFormat類型:
Microsoft.Owin.Security.Jwt中實現了一個JwtFormat的對象,該對象正好實現了需要的ISecureDataFormat介面:
但是從源碼中得知該對象沒有實現Protect方法:
而它的UnProtect方法的實現主要工作如下:
● 對發佈者以及Token的簽名、過期時間等進行驗證(註:驗證操作是由System.IdentityModel.Tokens.Jwt組件中的JwtSecurityTokenHandler類型提供的)。
● 驗證成功後獲取Token中包含的用戶信息。
3. 實現Jwt的Protect方法:
完整代碼:
1 public class MyJwtFormat : ISecureDataFormat<AuthenticationTicket> 2 { 3 //用於從AuthenticationTicket中獲取Audience信息 4 private const string AudiencePropertyKey = "aud"; 5 6 private readonly string _issuer = string.Empty; 7 //Jwt的發佈者和用於數字簽名的密鑰 8 public MyJwtFormat(string issuer) 9 { 10 _issuer = issuer; 11 } 12 13 public string Protect(AuthenticationTicket data) 14 { 15 if (data == null) 16 { 17 throw new ArgumentNullException("data"); 18 } 19 //獲取Audience名稱及其信息 20 string audienceId = data.Properties.Dictionary.ContainsKey(AudiencePropertyKey) ? 21 data.Properties.Dictionary[AudiencePropertyKey] : null; 22 if (string.IsNullOrWhiteSpace(audienceId)) throw new InvalidOperationException("AuthenticationTicket.Properties does not include audience"); 23 var audience = ClientRepository.Clients.Where(c => c.Id == audienceId).FirstOrDefault(); 24 if (audience == null) throw new InvalidOperationException("Audience invalid."); 25 //根據密鑰創建用於數字簽名的SigningCredentials,該對象在JwtSecurityToken中使用 26 var keyByteArray = TextEncodings.Base64Url.Decode(audience.Secret); 27 var signingKey = new InMemorySymmetricSecurityKey(keyByteArray); 28 var signingCredentials = new SigningCredentials(signingKey, 29 SecurityAlgorithms.HmacSha256Signature, SecurityAlgorithms.Sha256Digest); 30 //獲取發佈時間和過期時間 31 var issued = data.Properties.IssuedUtc; 32 var expires = data.Properties.ExpiresUtc; 33 //創建JwtToken對象 34 var token = new JwtSecurityToken(_issuer, 35 audienceId, 36 data.Identity.Claims, 37 issued.Value.UtcDateTime, 38 expires.Value.UtcDateTime, 39 signingCredentials); 40 //使用JwtSecurityTokenHandler將Token對象序列化成字元串 41 var handler = new JwtSecurityTokenHandler(); 42 var jwt = handler.WriteToken(token); 43 return jwt; 44 } 45 46 public AuthenticationTicket Unprotect(string protectedText) 47 { 48 throw new NotImplementedException(); 49 } 50 }View Code
上面代碼做了以下幾件事:
● 從AuthenticationTicket中獲取Audience信息(註:AuthenticationTicket是.Net中用來保存用戶信息的對象,它除了用戶信息,如用戶名以及用戶Claims之外還攜帶了身份驗證的有效期等附加信息,見下圖。AuthenticationTicket的創建方式有兩種,其一是登錄時,在判斷登錄信息無誤後,從資料庫中獲取相應的用戶信息以及從配置(或者預設)獲取身份驗證信息,如有效期等。另外就是通過反序列化身份Token獲取。這裡的Protect方法實際上就是序列化Token的方法,所以它得到的AuthenticationTicket是通過第一總方式創建的)
● 創建用於數字簽名的SignatureCredentials對象,該對象代表了用於數字簽名的演算法及其密鑰,創建該對象的原因僅僅是JwtSecurityToken對象需要它來完成Token創建。
● 通過JwtSecurityToken對象創建Token,該對象的創建需要發佈者(issuer)、觀眾(audience)、用戶Claims信息、發佈時間、有效期以及數字簽名需要的演算法及密鑰等。
● 通過JwtSecurityTokenHandler完成對Token的序列化。
3. 在AuthenticationTicket中加入Audience信息。
上面在創建Token時提到了需要Audience信息,而Token是通過AuthenticationTicket創建的,所以需要在創建AuthenticationTicket時加入Audience信息,另外上面也提到AuthenticationTicket的兩種創建方法,這裡使用的方法就是在“登錄”時創建的,而OAuth的“登錄”是通過不同類型的“授權”方式實現的,所以要加入Audience信息,只需要在相應方式的授權代碼中添加即可(以基於用戶名、密碼的模式為例,其它方法複製代碼即可):
4. 為Audience(Client)添加用於解析Token的JwtBearerAuthentication中間件:
Audience或者說Client包含了受限制的資源,當要訪問這些資源時就需要解析Token完成身份驗證。而Audience之間或者是Client之間是相對獨立的,所以它應該限制可訪問的Audience以及擁有自己的加密密鑰,甚至還需要驗證發佈者以確定token的安全性。(註:本例將身份驗證伺服器和Client都包含在同一個應用中,實際應用可將其分開,這樣就是一個簡單的單點登錄系統)。
5. 運行程式
使用該Token能夠正常訪問受限資源:
下麵是將Token Base64解碼後的結果,可以看到Jwt包含的信息:
如果使用test2這個Client獲取的Token,將無法訪問test1保護的資源:
身份驗證失敗,跳轉登錄頁面:
OAuth與OpenID Connect
OAuth與OpenID Connect是經常一起出現的兩個名詞,前者在本系列文章中已經進行過介紹,OAuth是一個授權協議,但是有點矛盾的就是身份驗證和授權實際上是兩個概念,前面文章也提到過的,身份驗證的目的是知道“你”是誰,而授權則是判斷“你”是否有許可權訪問資源。但是從上一篇文章開始介紹的OAuth相關的內容都是用來做身份驗證。授權協議用來做身份驗證,所以說是矛盾的。
OpenID Connect就是為了彌補OAuth協議的缺陷,而在OAuth協議基礎上進行補充拓展的一個身份驗證協議。它包含瞭如發現服務、動態註冊、Session管理、註銷機制等新的高級特性。
使用OAuth來做身份驗證,只是因為OAuth相對簡單,適合小型項目,這個與OAuth是授權協議還是身份驗證協議無關,它關註的是能否滿足需求,包括app.UseOAuthBearerAuthentication方法名稱都是Authentication而不是Authorization,通過添加OAuth Bearer身份驗證中間件來實現身份驗證。OpenID Connect更適合於大型項目,在這裡就不再深入介紹。
小結
本章介紹了Jwt以及Jwt在.Net中的實現,並介紹了在.Net中如何使用Jwt Token實現基於OAuth的身份驗證。使用Jwt Token最主要的是為瞭解決不同應用的Token識別問題。
最後簡單的說明瞭OAuth與OpenID Connect的區別,它們取捨的關鍵點在於需求,對於小型應用來說OAuth就能夠滿足,而由於OpenID Connect非常複雜,如果有需求時也可以先考慮使用如IdentityServer這些開源組件。
與身份驗證相關的內容暫時到此,關於.Net安全相關內容可以參考下麵的博客,非常全麵包含了身份驗證以及.Net中的加解密等內容:https://dotnetcodr.com/security-and-cryptography/
參考:
https://dzone.com/articles/whats-better-oauth-access-tokens-or-json-web-token
https://stackoverflow.com/questions/32964774/oauth-or-jwt-which-one-to-use-and-why
http://openid.net/specs/draft-jones-oauth-jwt-bearer-03.html
https://tools.ietf.org/html/rfc7523
https://auth0.com/learn/json-web-tokens/
https://stackoverflow.com/questions/39239051/rs256-vs-hs256-whats-the-difference
https://stackoverflow.com/questions/18677837/decoding-and-verifying-jwt-token-using-system-identitymodel-tokens-jwt
http://www.c-sharpcorner.com/UploadFile/4b0136/openid-connect-availability-in-owin-security-components/
https://security.stackexchange.com/questions/94995/oauth-2-vs-openid-connect-to-secure-api
https://www.cnblogs.com/linianhui/archive/2017/05/30/openid-connect-core.html
本文鏈接:http://www.cnblogs.com/selimsong/p/8184904.html