文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

Jwt通过源码揭秘隐藏大坑

2024-04-02 19:55

关注

前言

JWT是目前最为流行的接口认证方案之一,有关JWT协议的详细内容,请参考:https://jwt.io/introduction

今天分享一下在使用JWT在项目中遇到的一个问题,主要是一个协议的细节,非常容易被忽略,如果不是自己遇到,或者去看源码的实现,我估计至少80%的人都会栽在这里,下面来还原一下这个问题的过程,由于这个问题出现有一定的概率,不是每次都会出现,所以才容易掉坑里。

集成JWT

在Asp.Net Core中集成JWT认证的方式在网络上随便一搜就能找到一堆,主要有两个步骤:

1.在IOC容器中注入依赖

public void ConfigureServices(IServiceCollection services)
{
    // 添加这一行添加jwt验证:
    services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
        .AddJwtBearer(options => {
            options.TokenValidationParameters = new TokenValidationParameters
            {
                ValidateIssuer = true,//是否验证Issuer
                ValidateAudience = true,//是否验证Audience
                ValidateLifetime = true,//是否验证失效时间
                ClockSkew = TimeSpan.FromSeconds(30),
                ValidateIssuerSigningKey = true,//是否验证SecurityKey
                ValidAudience = Const.Domain,//Audience
                ValidIssuer = Const.Domain,//Issuer,这两项和前面签发jwt的设置一致
                IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Const.SecurityKey))//拿到SecurityKey
            };
        });
}

2.应用认证中间件

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    // 添加这一行 使用认证中间件
    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.提供一个认证的接口,用于前端获取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." });
    }
}

至此,你的应用已经完成了集成JWT认证。

坑在哪里

直接上代码,下面这段代码是我用来能复现该大坑的示例,有空的可以按照该代码重现下面的问题。

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();

上面代码运行的结果是:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImN0eSI6IkpXVCJ9.eyJuYmYiOiIxNjUzNDAwNjk0IiwiZXhwIjoxNjUzNDAyNDk0LCJOYW1lIjoi6Zi_5ZOIIiwiRW1haWwiOiJ1c2VybmFtZUBxcS5jb20iLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjUwMDAiLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjUwMDAifQ.RBtP7zroK7YueGlDdZNHGy3tT8-xcGkf8ZyiTL81w2I

我们知道Token由三部分组成,使用.分割,如果是标准的Jwt协议加密的,那这三部分均为Base64加密(此处不准确,下文解释为什么),也可以说就是明文,我们将三部分内容进行Base64解密看看。

我们在线验证一下我们的Jwt是否符合标准:
打开网站:https://jwt.io/,选择顶部菜单的Debugger,将我们的token填进去:

然后将代码中用的SecurityKey填到图中标记的位置

显示签名认证通过。

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的内容前端直接可以通过base64解密出来,那在需要展示用户名的地方,我们就可以直接解析token的载荷,然后获得Name
,下面是使用在线base64工具解密上面的token载荷内容,可以看到用户名为啊哈

逻辑没有任何问题,那就开始前端进行解析token中的用户名用于展示在个人中心吧。
下面是在Vue3框架和Piana中的演示,window.atob是浏览器自带base64decode的方法

export const useUserStore = defineStore({
  id: 'user',
  state: () => {
    return {
      token: '',
    }
  },
  getters: {
    accessToken: (state) => {
      return state.accesstoken || localStorage.getItem("accesstoken");
    },
    
    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"]),
}

感觉一切都很优雅的写完了代码,但是实际运行会报错:
这里为了方便是直接在浏览器的调式器中执行的

报错的意思来看是说我们的字符串没用正确的加密(就是它说咱这个字符串不是合法的base64加密)。
可是我们通过一些在线base64解密工具,还有Jwt的debugger工具都能解密出来明文。而且这不是我第一次将token拿出来进行解密了,之前也都没问题。

是不是token有问题?
经过测试,调用接口完全不会有问题,只是前端解密时报错,排除token不合法。前端的atob函数存在bug?
那我们在后端用c#的base64解密一下看看:

居然后端解密也报错了,头部解密成功,载荷部分解密异常,和前端报错一样都是说字符串不是合法的base64内容,不知道你是不是偶尔遇到过这个问题,如果没有,那你更要往下看了,不然以后遇到了,要耽误不少时间去排查了。

查看源码探索问题原因

上面遇到的问题曾经花了我不少时间去排查,关键是有工具能解密的还有工具不能解密,一时不知道到底是谁的问题了,抱着试试看的态度,看看源码生成token三部分的字符串过程。

1.既然token是这个函数生成的,那就直接看它的实现,直接F12即可,这个包是不是框架自带的,所以能直接通过vs看源码,比较方便的。

2.源码如下,encodedPayload根据它的命名不难看出是机密后的载荷,我们需要看的是它如何加密的

3.查看jwtToken.EncodedPayload这个属性怎么来的(F12)

图中标记了三个数字:

上一步我们已经锁定进加密的逻辑在Payload.Base64UrlEncode()中,看JwtPayload的类定义

可以看出,载荷的加密和我们想象的一样简单,把JwtPayload对象转成Json,然后进行Base64Url加密
5. 现在只剩Base64UrlEncoder.Encode的实现能为我们揭秘了

整体看下类定义,我们调用的Encode按标记顺序,依次调用了三个重载方法,最终实现都标记为3的那个方法。
6. 不知道你有没有注意到这些内容

看到这里我恍然大悟了一点,再看看他这里面的decode方法

看见了吧,我们因为是单纯的Base64加解密,其实不然,在进行Convert.FromBase64String(decodedString)解密前还需要进行一些字符串的替换,我赶紧看下上面出问题的载荷内容,发现其中有_这个字符,我赶紧将其进行替换成+,在次在尝试:

eyJuYmYiOiIxNjUzNDAwNjk0IiwiZXhwIjoxNjUzNDAyNDk0LCJOYW1lIjoi6Zi_5ZOIIiwiRW1haWwiOiJ1c2VybmFtZUBxcS5jb20iLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjUwMDAiLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjUwMDAifQ

// 替换后
eyJuYmYiOiIxNjUzNDAwNjk0IiwiZXhwIjoxNjUzNDAyNDk0LCJOYW1lIjoi6Zi+5ZOIIiwiRW1haWwiOiJ1c2VybmFtZUBxcS5jb20iLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjUwMDAiLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjUwMDAifQ

果然如此,替换后解密成功了,只有一个汉字的编码问题。

这下找到问题了,优化下前端的解密代码

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加密~~

总结

我们都以为Jwt三部分是用Base64加密,其实不完全对,因为他确切的加密方式是Base64Url加密,没有深入理解的我们只以为就是纯粹的base64,而且在大部分情况下确实是这样,更加坚定了我们这种错误认知。而只有当Base64加密后出现字符+/时,才会有所不同,希望对大家有帮助。

到此这篇关于Jwt隐藏大坑,通过源码揭秘 的文章就介绍到这了,更多相关Jwt源码内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!

阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯