上一篇文章介紹了OAuth2.0以及如何使用.Net來實現基於OAuth的身份驗證,本文是對上一篇文章的補充,主要是介紹OAuth與Jwt以及OpenID Connect之間的關係與區別。html
本文主要內容有:
● Jwt簡介
● .Net的Jwt實現
● OAuth與Jwt
● .Net中使用Jwt Bearer Token實現OAuth身份驗證
● OAuth與OpenID Connectweb
注:本章內容源碼下載:https://files.cnblogs.com/files/selimsong/OAuth2Demo_jwt.zip算法
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是被簽名的,能夠保證數據的完整性。另外就是用來攜帶用戶信息進行身份驗證。json
Jwt包含三個部分:
● Header:包含了簽名算法以及令牌類型(默認爲JWT)。如:api
注: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的例子:app
注:Jwt中的數據是透明的,既任何人拿到數據都能Base64Url反編碼的形式看到內容,簽名僅僅是保證內容不被纂改,因此不能在Jwt中包含敏感數據。以上例子均來自https://jwt.io/introduction/
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的Access Token有一種實現方式就是Jwt。
爲何要使用Jwt來做爲OAuth的Access Token?首先來看一下上一篇文章中生成的Access Token:
它是一個加密後的字符串,該字符串包含了用戶的相關信息,可是該字符串只可以被使用Microsoft.Owin.Security.OAuth組件的應用程序解密(不包括參照源碼的實現),而且還要保證加解密的密鑰是相同的。可是OAuth不少時候是用於一些分佈式的場景中,甚至還會使用不一樣語言來編寫不一樣的應用、服務。這樣的話上面這種Token的實現方式就沒法知足需求。
因此須要使用Jwt Bearer Token來解決不一樣應用中的Token識別問題。
在上一篇文章中提到了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的摘要計算。
下面就開始介紹如何來實現這個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 }
上面代碼作了如下幾件事:
● 從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是一個受權協議,可是有點矛盾的就是身份驗證和受權其實是兩個概念,前面文章也提到過的,身份驗證的目的是知道「你」是誰,而受權則是判斷「你」是否有權限訪問資源。可是從上一篇文章開始介紹的OAuth相關的內容都是用來作身份驗證。受權協議用來作身份驗證,因此說是矛盾的。
OpenID Connect就是爲了彌補OAuth協議的缺陷,而在OAuth協議基礎上進行補充拓展的一個身份驗證協議。它包含了如發現服務、動態註冊、Session管理、註銷機制等新的高級特性。
使用OAuth來作身份驗證,只是由於OAuth相對簡單,適合小型項目,這個與OAuth是受權協議仍是身份驗證協議無關,它關注的是可否知足需求,包括app.UseOAuthBearerAuthentication方法名稱都是Authentication而不是Authorization,經過添加OAuth Bearer身份驗證中間件來實現身份驗證。OpenID Connect更適合於大型項目,在這裏就再也不深刻介紹。
關於OAuth與OpenID Connect的內容可參考 blackheart的博客。感謝 blackheart給我提的意見。^_^
本章介紹了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