Jwt通過源碼揭秘隱藏大坑
前言
JWT是目前最為流行的接口認(rèn)證方案之一,有關(guān)JWT協(xié)議的詳細(xì)內(nèi)容,請參考:https://jwt.io/introduction
今天分享一下在使用JWT
在項(xiàng)目中遇到的一個問題,主要是一個協(xié)議的細(xì)節(jié),非常容易被忽略,如果不是自己遇到,或者去看源碼的實(shí)現(xiàn),我估計(jì)至少80%的人都會栽在這里,下面來還原一下這個問題的過程,由于這個問題出現(xiàn)有一定的概率,不是每次都會出現(xiàn),所以才容易掉坑里。
集成JWT
在Asp.Net Core中集成JWT
認(rèn)證的方式在網(wǎng)絡(luò)上隨便一搜就能找到一堆,主要有兩個步驟:
1.在IOC容器中注入依賴
public void ConfigureServices(IServiceCollection services) { // 添加這一行添加jwt驗(yàn)證: services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true,//是否驗(yàn)證Issuer ValidateAudience = true,//是否驗(yàn)證Audience ValidateLifetime = true,//是否驗(yàn)證失效時間 ClockSkew = TimeSpan.FromSeconds(30), ValidateIssuerSigningKey = true,//是否驗(yàn)證SecurityKey ValidAudience = Const.Domain,//Audience ValidIssuer = Const.Domain,//Issuer,這兩項(xiàng)和前面簽發(fā)jwt的設(shè)置一致 IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Const.SecurityKey))//拿到SecurityKey }; }); }
2.應(yīng)用認(rèn)證中間件
public void Configure(IApplicationBuilder app, IHostingEnvironment env) { // 添加這一行 使用認(rèn)證中間件 app.UseAuthentication(); if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseMvc(routes => { routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); }); }
3.在Controller
[Route("api/[controller]")] [ApiController] // 添加這一行 public class MyBaseController : ControllerBase { }
4.提供一個認(rèn)證的接口,用于前端獲取token
[AllowAnonymous] [HttpGet] public IActionResult Get(string userName, string pwd) { if (!string.IsNullOrEmpty(userName) && !string.IsNullOrEmpty(pwd)) { var claims = new[] { new Claim(JwtRegisteredClaimNames.Nbf,$"{new DateTimeOffset(DateTime.Now).ToUnixTimeSeconds()}") , new Claim (JwtRegisteredClaimNames.Exp,$"{new DateTimeOffset(DateTime.Now.AddMinutes(30)).ToUnixTimeSeconds()}"), new Claim(ClaimTypes.Name, userName) }; var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Const.SecurityKey)); var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); var token = new JwtSecurityToken( issuer: Const.Domain, audience: Const.Domain, claims: claims, expires: DateTime.Now.AddMinutes(30), signingCredentials: creds); return Ok(new { token = new JwtSecurityTokenHandler().WriteToken(token) }); } else { return BadRequest(new { message = "username or password is incorrect." }); } }
至此,你的應(yīng)用已經(jīng)完成了集成JWT
認(rèn)證。
坑在哪里
直接上代碼,下面這段代碼是我用來能復(fù)現(xiàn)該大坑的示例,有空的可以按照該代碼重現(xiàn)下面的問題。
using Microsoft.IdentityModel.Tokens; using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Text; var SecurityKey = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDI2a2EJ7m872v0afyoSDJT2o1+SitIeJSWtLJU8/Wz2m7gStexajkeD+Lka6DSTy8gt9UwfgVQo6uKjVLG5Ex7PiGOODVqAEghBuS7JzIYU5RvI543nNDAPfnJsas96mSA7L/mD7RTE2drj6hf3oZjJpMPZUQI/B1Qjb5H3K3PNwIDAQAB"; var Domain = "http://localhost:5000"; var email = "username@qq.com"; var userName = "阿哈"; var claims = new[] { new Claim(JwtRegisteredClaimNames.Nbf,$"{new DateTimeOffset(DateTime.Now).ToUnixTimeSeconds()}") , new Claim (JwtRegisteredClaimNames.Exp,$"{new DateTimeOffset(DateTime.Now.AddMinutes(30)).ToUnixTimeSeconds()}"), new Claim("Name", userName), new Claim("Email", email), }; var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(SecurityKey)); var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); var token = new JwtSecurityToken( issuer: Domain, audience: Domain, claims: claims, expires: DateTime.Now.AddMinutes(30), signingCredentials: creds); var JWTToken = new JwtSecurityTokenHandler().WriteToken(token); Console.WriteLine(JWTToken); Console.ReadLine();
上面代碼運(yùn)行的結(jié)果是:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImN0eSI6IkpXVCJ9.eyJuYmYiOiIxNjUzNDAwNjk0IiwiZXhwIjoxNjUzNDAyNDk0LCJOYW1lIjoi6Zi_5ZOIIiwiRW1haWwiOiJ1c2VybmFtZUBxcS5jb20iLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjUwMDAiLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjUwMDAifQ.RBtP7zroK7YueGlDdZNHGy3tT8-xcGkf8ZyiTL81w2I
我們知道Token由三部分組成,使用.
分割,如果是標(biāo)準(zhǔn)的Jwt協(xié)議加密的,那這三部分均為Base64加密(此處不準(zhǔn)確,下文解釋為什么),也可以說就是明文,我們將三部分內(nèi)容進(jìn)行Base64解密看看。
我們在線驗(yàn)證一下我們的Jwt是否符合標(biāo)準(zhǔn):
打開網(wǎng)站:https://jwt.io/
,選擇頂部菜單的Debugger
,將我們的token填進(jìn)去:
然后將代碼中用的SecurityKey
填到圖中標(biāo)記的位置
顯示簽名認(rèn)證通過。
頭
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImN0eSI6IkpXVCJ9
{ "alg": "HS256", "typ": "JWT", "cty": "JWT" }
載荷
eyJuYmYiOiIxNjUzNDAwNjk0IiwiZXhwIjoxNjUzNDAyNDk0LCJOYW1lIjoi6Zi_5ZOIIiwiRW1haWwiOiJ1c2VybmFtZUBxcS5jb20iLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjUwMDAiLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjUwMDAifQ
{ "nbf": "1653400694", "exp": 1653402494, "Name": "阿哈", "Email": "username@qq.com", "iss": "http://localhost:5000", "aud": "http://localhost:5000" }
簽名
RBtP7zroK7YueGlDdZNHGy3tT8-xcGkf8ZyiTL81w2I
到目前未知一切都十分順利。
既然Token的內(nèi)容前端直接可以通過base64解密出來,那在需要展示用戶名的地方,我們就可以直接解析token的載荷,然后獲得Name
,下面是使用在線base64工具解密上面的token載荷內(nèi)容,可以看到用戶名為啊哈
。
邏輯沒有任何問題,那就開始前端進(jìn)行解析token中的用戶名用于展示在個人中心吧。
下面是在Vue3
框架和Piana
中的演示,window.atob
是瀏覽器自帶base64decode的方法
export const useUserStore = defineStore({ id: 'user', state: () => { return { token: '', } }, getters: { accessToken: (state) => { return state.accesstoken || localStorage.getItem("accesstoken"); }, /** * 獲取token中解密后的用戶信息 */ userInfo(state) { var token = state.token || localStorage.getItem("accesstoken"); if (!token || token == '') { return null; } var json = window.atob(token.split(".")[1]); return JSON.parse(json); } } })
在需要獲取用戶名的地方使用
computed:{ ...mapState(useUserStore, ["userInfo"]), }
感覺一切都很優(yōu)雅的寫完了代碼,但是實(shí)際運(yùn)行會報錯:
這里為了方便是直接在瀏覽器的調(diào)式器中執(zhí)行的
報錯的意思來看是說我們的字符串沒用正確的加密(就是它說咱這個字符串不是合法的base64加密)。
可是我們通過一些在線base64解密工具,還有Jwt的debugger工具都能解密出來明文。而且這不是我第一次將token拿出來進(jìn)行解密了,之前也都沒問題。
是不是token有問題?
經(jīng)過測試,調(diào)用接口完全不會有問題,只是前端解密時報錯,排除token不合法。前端的atob函數(shù)存在bug?
那我們在后端用c#的base64解密一下看看:
居然后端解密也報錯了,頭部解密成功,載荷部分解密異常,和前端報錯一樣都是說字符串不是合法的base64內(nèi)容,不知道你是不是偶爾遇到過這個問題,如果沒有,那你更要往下看了,不然以后遇到了,要耽誤不少時間去排查了。
查看源碼探索問題原因
上面遇到的問題曾經(jīng)花了我不少時間去排查,關(guān)鍵是有工具能解密的還有工具不能解密,一時不知道到底是誰的問題了,抱著試試看的態(tài)度,看看源碼生成token三部分的字符串過程。
1.既然token是這個函數(shù)生成的,那就直接看它的實(shí)現(xiàn),直接F12即可,這個包是不是框架自帶的,所以能直接通過vs看源碼,比較方便的。
2.源碼如下,encodedPayload
根據(jù)它的命名不難看出是機(jī)密后的載荷,我們需要看的是它如何加密的
3.查看jwtToken.EncodedPayload
這個屬性怎么來的(F12)
圖中標(biāo)記了三個數(shù)字:
- 上一步我們逆向找到加密后的屬性
EncodedPayload
EncodedPayload
屬性里面用到了另一個屬性Payload
,我們需要找Payload
哪里賦值的Payload
是在構(gòu)造函數(shù)中根據(jù)傳參內(nèi)容進(jìn)行初始化的。
上一步我們已經(jīng)鎖定進(jìn)加密的邏輯在Payload.Base64UrlEncode()
中,看JwtPayload
的類定義
可以看出,載荷的加密和我們想象的一樣簡單,把JwtPayload
對象轉(zhuǎn)成Json
,然后進(jìn)行Base64Url
加密
5. 現(xiàn)在只剩Base64UrlEncoder.Encode
的實(shí)現(xiàn)能為我們揭秘了
整體看下類定義,我們調(diào)用的Encode
按標(biāo)記順序,依次調(diào)用了三個重載方法,最終實(shí)現(xiàn)都標(biāo)記為3的那個方法。
6. 不知道你有沒有注意到這些內(nèi)容
看到這里我恍然大悟了一點(diǎn),再看看他這里面的decode方法
看見了吧,我們因?yàn)槭菃渭兊腂ase64加解密,其實(shí)不然,在進(jìn)行Convert.FromBase64String(decodedString)
解密前還需要進(jìn)行一些字符串的替換,我趕緊看下上面出問題的載荷內(nèi)容,發(fā)現(xiàn)其中有_
這個字符,我趕緊將其進(jìn)行替換成+
,在次在嘗試:
eyJuYmYiOiIxNjUzNDAwNjk0IiwiZXhwIjoxNjUzNDAyNDk0LCJOYW1lIjoi6Zi_5ZOIIiwiRW1haWwiOiJ1c2VybmFtZUBxcS5jb20iLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjUwMDAiLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjUwMDAifQ // 替換后 eyJuYmYiOiIxNjUzNDAwNjk0IiwiZXhwIjoxNjUzNDAyNDk0LCJOYW1lIjoi6Zi+5ZOIIiwiRW1haWwiOiJ1c2VybmFtZUBxcS5jb20iLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjUwMDAiLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjUwMDAifQ
果然如此,替換后解密成功了,只有一個漢字的編碼問題。
這下找到問題了,優(yōu)化下前端的解密代碼
userInfo(state) { var token = state.token || localStorage.getItem("accesstoken"); if (!token || token == '') { return null; } token = token.replace("_", "/").replace("-", "+") // 添加這一行 var json = window.atob(token.split(".")[1]); return JSON.parse(json); }
問題解決了。
注意官方對加密過程的描述
哈哈,是不是草率了,并不是Base64
加密~~
總結(jié)
我們都以為Jwt三部分是用Base64
加密,其實(shí)不完全對,因?yàn)樗_切的加密方式是Base64Url
加密,沒有深入理解的我們只以為就是純粹的base64,而且在大部分情況下確實(shí)是這樣,更加堅(jiān)定了我們這種錯誤認(rèn)知。而只有當(dāng)Base64加密后出現(xiàn)字符+
或/
時,才會有所不同,希望對大家有幫助。
到此這篇關(guān)于Jwt隱藏大坑,通過源碼揭秘 的文章就介紹到這了,更多相關(guān)Jwt源碼內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
在DataTable中執(zhí)行Select("條件")后,返回DataTable的方法
在DataTable中執(zhí)行Select("條件")后,返回DataTable的方法...2007-09-09ASP.NET項(xiàng)目開發(fā)中日期控件DatePicker如何使用
這篇文章主要為大家詳細(xì)介紹了ASP.NET項(xiàng)目開發(fā)中日期控件DatePicker的使用方法,感興趣的小伙伴們可以參考一下2016-04-04發(fā)布一個基于TokyoTyrant的C#客戶端開源項(xiàng)目
目前在網(wǎng)上關(guān)于TokyoCabinet(以下簡稱TC)和TokyoTyrant(以下簡稱TT)的資料已相對豐富了,但在.NET平臺上的客戶端軟件卻相對匱乏,因?yàn)樽鯠iscuz!NT企業(yè)版的關(guān)系,兩個月前開始接觸TC和TT,開始寫相關(guān)的客戶端代碼。2010-07-07ASP.NET實(shí)現(xiàn)用圖片進(jìn)度條顯示投票結(jié)果
ASP.NET實(shí)現(xiàn)用圖片進(jìn)度條顯示投票結(jié)果...2007-06-06詳解在Azure上部署Asp.NET Core Web App
這篇文章主要介紹了詳解在Azure上部署Asp.NET Core Web App,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-12-12asp.net mvc4 mysql制作簡單分頁組件(部分視圖)
這篇文章主要介紹了asp.net mvc4 mysql制作簡單分頁組件,附部分視圖,具有一定的參考價值,感興趣的小伙伴們可以參考一下2016-10-10