ASP.NET?Core?Web?API之Token驗證的實現(xiàn)
在實際開發(fā)中經(jīng)常需要對外提供接口以便客戶獲取數(shù)據(jù),由于數(shù)據(jù)屬于私密信息,并不能隨意供其他人訪問,所以就需要驗證客戶身份。那么如何才能驗證客戶的身份呢?一個簡單的小例子,簡述ASP.NET Core Web API開發(fā)過程中,常用的一種JWT身份驗證方式。
1.什么是JWT?
JSON WEB Token(JWT,讀作 [/d??t/]),是一種基于JSON的、用于在網(wǎng)絡(luò)上聲明某種主張的令牌(token)。主要用于認(rèn)證和保護(hù)API之間信息交換。
JWT通常由三部分組成: 頭信息(header), 消息體(payload)和簽名(signature)。
2.JWT 組成
JWT 由三部分組成:Header,Payload,Signature 三個部分組成,并且最后由.拼接而成 xxxxx.yyyyy.zzzzz。
2.1 頭部(Header)
Header 一般由alg 和 typ 兩個部分組成:
{ "alg": "HS256", (使用的hash算法,如:HMAC SHA256或RSA) "typ": "JWT" (Token的類型,在這里就是:JWT) }
然后使用Base64Url編碼成第一部分:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
2.2. 載荷(Payload)
這部分是JWT主要的信息存儲部分,其中包含了許多種的聲明(claims)。
Claims的實體一般包含用戶和一些元數(shù)據(jù),這些claims分成三種類型:
- reserved claims:預(yù)定義的 一些聲明,并不是強(qiáng)制的但是推薦,它們包括 iss (issuer), exp (expiration time), sub (subject),aud(audience) 等(這里都使用三個字母的原因是保證 JWT 的緊湊)。
- public claims: 公有聲明,這個部分可以隨便定義,但是要注意和 IANA JSON Web Token 沖突。
- private claims: 私有聲明,這個部分是共享被認(rèn)定信息中自定義部分。
Pyload可以是這樣子的:
{ -- 官方的字段 "iss" (issuer):簽發(fā)人, "exp" (expiration time):過期時間, "sub" (subject):主題, "aud" (audience):訂閱者, "nbf" (Not Before):生效時間, "iat" (Issued At):簽發(fā)時間, "jti" (JWT ID):編號, -- 自定義的字段 "user_id": 123456, "name": "John Doe", "admin": true }
這部分同樣使用Base64Url編碼成第二部分:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.
2.3 簽名(Signature)
Signature是對前兩部分的簽名,是用來驗證發(fā)送者的JWT的同時也能確保在期間不被篡改。
首先需要指定一個密鑰(secret)。這個密鑰只有服務(wù)器才知道,不能泄露給用戶。
這個密鑰(secret)是大于16位的隨機(jī)字符串(數(shù)字),
生成jwt:
// 由 HMACSHA256 算法進(jìn)行簽名,secret 不能外泄 const sign = HMACSHA256(base64.encode(header) + '.' + base64.encode(payload), secret) 算出簽名以后,把 Header、Payload、Signature 三個部分拼成一個字符串,每個部分之間用"點"(.)分隔. // jwt 由三部分拼接而成 const jwt = base64.encode(header) + '.' + base64.encode(payload) + '.' + sign
為啥要用base64編碼,由于 ASCII 碼稱為了國際標(biāo)準(zhǔn),所以我們要把其它字符轉(zhuǎn)成 ASCII 就要用到 base64。
utf-8 -> base64(編碼) -> ASCII ASCII -> base64(解碼) -> utf-8
這樣就可以讓只支持 ASCII 的計算機(jī)支持 utf-8 了。
2.4 JWT的結(jié)構(gòu)
eyJhbGci0iJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LZA1L21kZW50aXR5L2NsYW1tcy9zaWQi0iIxIiwiaHRBcDovL3NjaGVtYXMueG1sc29hcC5vcmcvd3MVMjAWNS8wNS9pZGVudG1Bes9jbGFpbXMvbmFtZSI6IuWFrOWtkOWwjWFrSIsImhBdHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vd3MvMjAw0C8wNi9pZGVudG1Bes9jbGFpbXMvcm9sZSI6IkFkbWluIiwiZXhwIjoxNjg3NZEyMDE1LCJpc3Mi0iLlhazlrzDlsI_lhagiLcJhdWQi0iLlhazlrZDlsI_1hagifQ.QeZ1Cy5JPV0s8fPfFR59g-rVI3SNKPNP2ZcODzr308Y
3.應(yīng)用JWT步驟
安裝JWT授權(quán)庫
采用JWT進(jìn)行身份驗證,需要安裝【Microsoft.AspNetCore.Authentication.JwtBearer】,可通過Nuget包管理器進(jìn)行安裝,如下所示:
添加JWT身份驗證服務(wù)
在啟動類Program.cs中,添加JWT身份驗證服務(wù),如下所示:
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidateAudience = true, ValidateLifetime = true, ValidateIssuerSigningKey = true, ValidIssuer = TokenParameter.Issuer, ValidAudience = TokenParameter.Audience, IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(TokenParameter.Secret)) }; });
應(yīng)用鑒權(quán)授權(quán)中間件
在啟動類Program.cs中,添加鑒權(quán)授權(quán)中間件,如下所示:
app.UseAuthentication(); app.UseAuthorization();
配置Swagger身份驗證輸入(可選)
在啟動類Program.cs中,添加Swagger服務(wù)時,配置Swagger可以輸入身份驗證方式,如下所示:
builder.Services.AddSwaggerGen(options => { options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme { Description = "請輸入token,格式為 Bearer xxxxxxxx(注意中間必須有空格)", Name = "Authorization", In = ParameterLocation.Header, Type = SecuritySchemeType.ApiKey, BearerFormat = "JWT", Scheme = "Bearer" }); //添加安全要求 options.AddSecurityRequirement(new OpenApiSecurityRequirement { { new OpenApiSecurityScheme{ Reference =new OpenApiReference{ Type = ReferenceType.SecurityScheme, Id ="Bearer" } },new string[]{ } } }); });
注意:此處配置主要是方便測試,如果采用Postman或者其他測試工具,此步驟可以省略。
創(chuàng)建JWT幫助類
創(chuàng)建JWT幫助類,主要用于生成Token,如下所示:
using DemoJWT.Models; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.IdentityModel.Tokens; using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Text; namespace DemoJWT.Authorization { public class JwtHelper { public static string GenerateJsonWebToken(User userInfo) { var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(TokenParameter.Secret)); var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256); var claimsIdentity = new ClaimsIdentity(CookieAuthenticationDefaults.AuthenticationScheme); claimsIdentity.AddClaim(new Claim(ClaimTypes.Sid, userInfo.Id)); claimsIdentity.AddClaim(new Claim(ClaimTypes.Name, userInfo.Name)); claimsIdentity.AddClaim(new Claim(ClaimTypes.Role, userInfo.Role)); var token = new JwtSecurityToken(TokenParameter.Issuer, TokenParameter.Audience, claimsIdentity.Claims, expires: DateTime.Now.AddMinutes(120), signingCredentials: credentials); return new JwtSecurityTokenHandler().WriteToken(token); } } }
其中用到的TokenParameter主要用于配置Token驗證的頒發(fā)者,接收者,簽名秘鑰等信息,如下所示:
namespace DemoJWT.Authorization { public class TokenParameter { public const string Issuer = "公子小六";//頒發(fā)者 public const string Audience = "公子小六";//接收者 public const string Secret = "1234567812345678";//簽名秘鑰 public const int AccessExpiration = 30;//AccessToken過期時間(分鐘) } }
創(chuàng)建Token獲取接口
創(chuàng)建對應(yīng)的AuthController/GetToken方法,用于獲取Token信息,如下所示:
using DemoJWT.Authorization; using DemoJWT.Models; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using System.IdentityModel.Tokens.Jwt; namespace DemoJWT.Controllers { [Route("api/[controller]/[Action]")] [ApiController] public class AuthController : ControllerBase { [HttpPost] public ActionResult GetToken(User user) { string token = JwtHelper.GenerateJsonWebToken(user); return Ok(token); } } }
創(chuàng)建測試接口
創(chuàng)建測試接口,用于測試Token身份驗證。如下所示:
using DemoJWT.Models; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using System.Security.Claims; namespace DemoJWT.Controllers { [Authorize] [Route("api/[controller]/[Action]")] [ApiController] public class TestController : ControllerBase { [HttpPost] public ActionResult GetTestInfo() { var claimsPrincipal = this.HttpContext.User; var name = claimsPrincipal.Claims.FirstOrDefault(r => r.Type == ClaimTypes.Name)?.Value; var role = claimsPrincipal.Claims.FirstOrDefault(r => r.Type == ClaimTypes.Role)?.Value; var test = new Test() { Id = 1, Name = name, Role = role, Author = "公子小六", Description = "this is a test.", }; return Ok(test); } } }
接口測試
運(yùn)行程序,看到公開了兩個接口,如下所示:
校驗邏輯
- 1)客戶端向授權(quán)服務(wù)系統(tǒng)發(fā)起請求,申請獲取“令牌”。
- 2)授權(quán)服務(wù)根據(jù)用戶身份,生成一張專屬“令牌”,并將該“令牌”以JWT規(guī)范返回給客戶端
- 3)客戶端將獲取到的“令牌”放到http請求的headers中后,向主服務(wù)系統(tǒng)發(fā)起請求。主服務(wù)系統(tǒng)收到請求后會從headers中獲取“令牌”,并從“令牌”中解析出該用戶的身份權(quán)限,然后做出相應(yīng)的處理(同意或拒絕返回資源)
1. 獲取Token運(yùn)行api/Auth/GetToken接口,輸入用戶信息,點擊Execute,在返回的ResponseBody中,就可以獲取接口返回的Token
2. 設(shè)置Token在Swagger上方,點擊Authorize,彈出身份驗證配置窗口,如下所示:
3. 接口測試配置好身份認(rèn)證信息后,調(diào)用/api/Test/GetTestInfo接口,獲取信息如下:
如果清除掉Token配置,再進(jìn)行訪問/api/Test/GetTestInfo接口,則會返回401未授權(quán)信息,如下所示:
JWT 與 Session
有無狀態(tài)對比
- Session 是一種記錄服務(wù)器和客戶端會話狀態(tài)的機(jī)制,需要在數(shù)據(jù)庫或者 Redis 中保存用戶信息和token信息,所以它是有狀態(tài)的。
- JWT 看完了前面的 JWT 結(jié)構(gòu)和 JWT 校驗原理,在后端并不需要存儲數(shù)據(jù),直接通過私有密鑰驗證就可以了。
當(dāng)有這樣的一個需求,一家公司下同時關(guān)聯(lián)了多個業(yè)務(wù),A業(yè)務(wù)網(wǎng)站,B業(yè)務(wù)網(wǎng)站,但是現(xiàn)在要求用戶在A網(wǎng)站登陸過,再訪問B網(wǎng)站的時候能夠自動登陸,JWT 就可以很快的實現(xiàn)這個需求,把 JWT 直接存儲在前端,后端只要校驗 JWT 就可以了。
注:這個需求用 session 也是可以實現(xiàn)的,只是會存儲狀態(tài),查詢存儲,沒有 JWT 方便而已。
適用場景對比
郵箱驗證
很多網(wǎng)站在注冊成功后添加了郵箱驗證功能,功能實現(xiàn):用戶注冊成功后,完善郵箱,服務(wù)端會給用戶郵箱發(fā)一個鏈接,用戶點開鏈接校驗成功,這個功能使用 JWT 是個不錯的選擇。
// 把郵箱以及用戶id綁定在一起,設(shè)置生效時間 const code = jwt.sign({ email, userId }, secret, { expiresIn: 60 * 30 }) // 在此鏈接校驗驗證碼 const link = `https://www.inode.club/code=$[code]`
做那些短期的驗證需求
比如在 BFF 層,用 JWT 去驗證傳遞一些數(shù)據(jù)還是不錯的選擇,可以把有效時間設(shè)置的短一些,過期了就需要重新去請求,我這么直接表述你可能還不太懂,舉個現(xiàn)實生活中的例子。
我們上學(xué)的時候,有班主任和學(xué)科老師這兩個概念,有一天你想請假,你需要先去找班主任開一個請假條,然后請教條你的班主任簽完字之后,你會將請假條交給你的學(xué)科課教師,學(xué)科教師確認(rèn)簽字無誤后,把請假條收了,并在請假記錄表中作出了相應(yīng)記錄。
上訴的例子中,“請假申請單”就是JWT中的payload,領(lǐng)導(dǎo)簽字就是base64后的數(shù)字簽名,領(lǐng)導(dǎo)是issuer,“學(xué)科教師的老王”即為JWT的audience,audience需要驗證班主任簽名是否合法,驗證合法后根據(jù)payload中請求的資源給予相應(yīng)的權(quán)限,同時將JWT收回。
放到一些系統(tǒng)集成的應(yīng)用場景中,例如我前面說的 BFF 中其實 JWT 更適合一次性操作的認(rèn)證:
服務(wù) B 你好, 服務(wù) A 告訴我,我可以操作 <JWT內(nèi)容>, 這是我的憑證(即 JWT )
在這里,服務(wù) A 負(fù)責(zé)認(rèn)證用戶身份(類似于上例班主任批準(zhǔn)請假),并頒布一個很短過期時間的JWT給瀏覽器(相當(dāng)于上例的請假單),瀏覽器(相當(dāng)于上例請假的我們)在向服務(wù) B 的請求中帶上該 JWT,則服務(wù) B(相當(dāng)于上例的任課教師)可以通過驗證該 JWT 來判斷用戶是否有權(quán)限執(zhí)行該操作。通過這樣,服務(wù) B 就成為一個安全的無狀態(tài)的服務(wù)。
個人還是認(rèn)為 JWT 更適合做一些一次性的安全認(rèn)證,好多其他場景考慮多了之后又做回了 session,傳統(tǒng)的 cookie-session 機(jī)制工作得更好,但是對于一次性的安全認(rèn)證,頒發(fā)一個有效期極短的JWT,即使暴露了危險也很小。上面的郵箱驗證其實也是一次性的安全認(rèn)證。
跨域認(rèn)證
因為 JWT 并不使用 Cookie ,所以你可以使用任何域名提供你的 API 服務(wù)而不需要擔(dān)心跨域資源共享問題(CORS)。JWT 確實是跨域認(rèn)證的一個解決方案,但是對于跨域場景時要注意一點。 客戶端收到服務(wù)器返回的 JWT,可以儲存在 Cookie 里面,也可以儲存在 localStorage。
此后,客戶端每次與服務(wù)器通信,都要帶上這個 JWT。你可以把它放在 Cookie 里面自動發(fā)送,但是這樣不能跨域,所以更好的做法是放在 HTTP 請求的頭信息Authorization字段里面。
Authorization: Bearer另一種做法是,跨域的時候,JWT 就放在 POST 請求的數(shù)據(jù)體里面。
跨域知識擴(kuò)展
跨域這兩個字就像一塊狗皮膏藥一樣黏在每一個開發(fā)者身上,無論你在工作上或者面試中無可避免會遇到這個問題。為了應(yīng)付面試,我們每次都隨便背幾個方案。但是如果突然問你為什么會有跨域這個問題出現(xiàn)? ...停頓幾秒,這里只是普及一下,知道的可以忽略掉。
登陸驗證
登陸驗證:不需要控制登錄設(shè)備數(shù)量以及注銷登陸情況,無狀態(tài)的 jwt 是一個不錯的選擇。具體實現(xiàn)流程,可以看上文中的校驗原理,校驗原理使用的登陸驗證例子。
當(dāng)需求中出現(xiàn)控制登陸設(shè)備數(shù)量,或者可以注銷掉用戶時,可以考慮使用原有的 session 模式,因為針對這種登陸需求,需要進(jìn)行的狀態(tài)存儲對 jwt 添加額外的狀態(tài)支持,增加了認(rèn)證的復(fù)雜度,此時選用 session 是一個不錯的選擇。 針對上面的特殊需求,可能也有小伙伴仍喜歡使用 jwt ,補(bǔ)充一下特殊案例
注銷登陸
用戶注銷時候要考慮 token 的過期時間。
session: 只需要把 user_id 對應(yīng)的 token 清掉即可 ;
jwt: 使用 redis,需要維護(hù)一張黑名單,用戶注銷時把該 token 加入黑名單,過期時間與 jwt 的過期時間保持一致。
用戶登陸設(shè)備控制
session: 使用 sql 類數(shù)據(jù)庫,維護(hù)一個用戶驗證token表,每次登陸重置表中 token 字段,每次請求需要權(quán)限接口時,根據(jù) token 查找 user_id(也可以使用 redis 維護(hù) token 數(shù)據(jù)的存儲)
jwt: 假使使用 sql 類數(shù)據(jù)庫,維護(hù)一個用戶驗證token表,表中添加 token 字段,每次登陸重置 token 字段,每次請求需要權(quán)限接口時,根據(jù) jwt 獲取 user_id,根據(jù) user_id 查用戶表獲取 token 判斷 token 是否一致。(也可以使用 redis 維護(hù) token 數(shù)據(jù)的存儲)
適合做那些事來講的,其實也就是針對JWT的優(yōu)勢來說的,還有一些辯證性的理解。接下來說說 JWT 的缺點。
JWT 缺點
- 更多的空間占用。如果將原存在服務(wù)端session中的信息都放在JWT中保存,會造成JWT占用的空間變大,需要考慮客戶端cookie的空間限制等因素,如果放在Local Storage,則可能會受到 XSS 攻擊。
- 無法作廢已頒布的令牌。JWT 使用時由于服務(wù)器不需要存儲 Session 狀態(tài),因此使用過程中無法廢棄某個 Token 或者更改 Token 的權(quán)限。也就是說一旦 JWT 簽發(fā)了,到期之前就會始終有效,除非服務(wù)器部署額外的邏輯。
- 用戶信息安全。通過J WT 的組成結(jié)構(gòu)可以看出,Payload 存儲的一些用戶信息,它是通過Base64加密的,可以直接解密,不能將秘密數(shù)據(jù)寫入 JWT,如果使用需要對 JWT 進(jìn)行二次加密。
到此這篇關(guān)于ASP.NET Core Web API之Token驗證的實現(xiàn)的文章就介紹到這了,更多相關(guān)ASP.NET Core Token驗證內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
ASP.NET2.0服務(wù)器控件之自定義狀態(tài)管理
ASP.NET2.0服務(wù)器控件之自定義狀態(tài)管理...2006-09-09C#實現(xiàn)支持?jǐn)帱c續(xù)傳多線程下載客戶端工具類
C#實現(xiàn)支持?jǐn)帱c續(xù)傳多線程下載的 Http Web 客戶端工具類 (C# DIY HttpWebClient),感興趣的朋友可以參考下本文,或許對你有所幫助2013-04-04ASP.NET?Core?MVC中的標(biāo)簽助手(TagHelper)用法
這篇文章介紹了ASP.NET?Core?MVC中標(biāo)簽助手(TagHelper)的用法,文中通過示例代碼介紹的非常詳細(xì)。對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2022-04-04Entity?Framework?Core關(guān)聯(lián)刪除
關(guān)聯(lián)刪除通常是一個數(shù)據(jù)庫術(shù)語,用于描述在刪除行時允許自動觸發(fā)刪除關(guān)聯(lián)行的特征;即當(dāng)主表的數(shù)據(jù)行被刪除時,自動將關(guān)聯(lián)表中依賴的數(shù)據(jù)行進(jìn)行刪除,或者將外鍵更新為NULL或默認(rèn)值。本文將為大家具體介紹一下Entity?Framework?Core關(guān)聯(lián)刪除,需要的可以參考一下2021-12-12Asp.net MVC 中利用jquery datatables 實現(xiàn)數(shù)據(jù)分頁顯示功能
這篇文章主要介紹了Asp.net MVC 中利用jquery datatables 實現(xiàn)數(shù)據(jù)分頁顯示功能,非常不錯,具有參考借鑒價值,需要的朋友可以參考下2017-06-06asp.net 文件路徑之獲得虛擬目錄的網(wǎng)站的根目錄
asp.net下獲取文件路徑常用代碼,獲得虛擬目錄的網(wǎng)站的根目錄2012-10-10