动手写一个 Java JWT Token 生成组件

news2025/1/10 20:32:35

OAuth2 中默认使用 Bearer Tokens (一般用 UUID 值)作为 token 的数据格式,但也支持升级使用 JSON Web Token(JWT) 来作为 token 的数据格式。实际来说,OAuth 规范中并无限制 Token 采取何种格式。今天我们就采用 JWT 来作为 Token,它的一个好处是自描述 Token,包含了用户信息而并不需要通过额外的接口获取用户信息。

所谓 JWT Token,本身是明文的,前端得到之后进行 Base64 解码,即可获取用户信息(JSON)。——此时你认为可以直接使用吗?——那岂可值得信任?放心,我们还有一个 signature 参数用于校验这段 Token 是否合法,还是伪造的。即使假设这是个假的 Token,调用业务逻辑时候传入到后端,我们根据签名就能知道这个 Token 真实性。

故所以,我们必须在服务端校验过后才能用于前端的显示。因为密钥是在后端的——验证 JWT 的完整性和真实性应该在服务器端进行,使用密钥进行签名验证。

网上关于 JWT 的文章很多,但无非都是库的使用方式介绍,再深一点就研究 JWT 原理。其实如果只是生成 JWT,Java 代码是很简单的,不需要依赖什么库。今天我们就发挥一下动手能力,自己写个 JWT Token 的生成器。实际网上写 Java JWT 的轮子不是很多,我看到的有 cn.hutool.jwt.JWT 和老外一个例子。

认识 JWT

JWT 令牌由这三部分组成:

  • header 头部,明文是 JSON 格式,它确定了是何种加密算法。目前采用 HmacSHA256算法,于是 header 就是 {"alg":"HS256","typ":"JWT"}。我们用一个常量定义之:
private static final String JWT_HEADER = "{\"alg\":\"HS256\",\"typ\":\"JWT\"}";
  • Payload(负载):也是一个 JSON 对象,用来存放实际需要传递的数据,JWT 规定了七个官方字段:
    • iss (issuer):签发人
    • exp (expiration time):过期时间
    • sub (subject):主题,这理解有点别扭,相当于用户 ID
    • aud (audience):受众,这理解有点别扭,实际上就是角色的意思,可为多个
    • nbf (Not Before):生效时间
    • iat (Issued At):签发时间
    • jti (JWT ID):编号
  • Signature(签名):对前两部分的签名,防止数据篡改

重点是 Payload。其中最关键的三个字段是:exp、sub、aud。当然可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息。但不建议添加敏感信息,因为该部分在客户端可解密。

我们定义个一个 Java Bean,说明这个 Payload 如何:

import lombok.Data;

import java.util.List;

/**
 * JWT 基础载荷
 */
@Data
public class Payload {
    /**
     * 主题
     */
    private String sub;

    /**
     * 受众
     */
    private List<String> aud;

    /**
     * 过期时间
     */
    private Long exp;

    /**
     * 签发人
     */
    private String iss;

    /**
     * 签发时间
     */
    private Long iat;

//    /**
//     * 编号,似乎不需要
//     */
//    private String jti;
}

进而描绘出 JWT Token 的结构,如是 JWebToken

import com.ajaxjs.util.map.JsonHelper;
import lombok.Data;

/**
 * JWT Token
 */
@Data
public class JWebToken {
    /**
     * 头部
     */
    public static final String JWT_HEADER = "{\"alg\":\"HS256\",\"typ\":\"JWT\"}";

    /**
     * 头部的 Base64 编码
     */
    public static final String encodedHeader = Utils.encode(JWT_HEADER);

    /**
     * 载荷
     */
    private Payload payload;

    /**
     * 签名部分
     */
    private String signature;

    public JWebToken(Payload payload) {
        this.payload = payload;
    }

    /**
     * 头部 + Payload
     *
     * @return 头部 + Payload
     */
    public String headerPayload() {
        String p = Utils.encode(JsonHelper.toJson(payload));
        return encodedHeader + "." + p;
    }

    /**
     * 返回 Token 的字符串形式
     *
     * @return Token
     */
    @Override
    public String toString() {
        return headerPayload() + "." + signature;
    }

}

创建 Token

结构清楚了,我们就试着创建 Token。

首先对 Header 和 Payload 分别 base64 编码,然后通过 HMACSHA256算法得到签名(Signature )部分,这样还可以防止数据被篡改。

String signature = HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload),  secret);
String jwtToken = base64(header) + "." + base64(payload) + "." + signature;

然后把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点"(.)分隔,就可以返回给用户。

JWebTokenMgr

具体执行过程参见下面测试代码:

JWebTokenMgr mgr = new JWebTokenMgr();

@Test
public void testMakeToken() {
    JWebToken token = mgr.tokenFactory("user01", Collections.singletonList("admin"), Utils.setExpire(24));
    System.out.println(token.toString());
}

这里出现了 JWebTokenMgr,这是封装好的 JWT 管理器,一般情况下要对其初始化,传入最关键的密钥 secretKey 信息,还有其他颁发者等的信息。

/**
 * JWT 管理器
 */
public class JWebTokenMgr {
    private String secretKey = "Df87sD#$%#A";
    private String issuer = "foo@bar.net";

    public JWebTokenMgr(String secretKey, String issuer) {
        this.secretKey = secretKey;
        this.issuer = issuer;
    }

    public JWebTokenMgr() {
    }
    ……

当然不传也行,就是默认的密钥(无安全性可言)。

通过工厂方法创建 Token

mgr.tokenFactory() 分别传入了 sub、aud、exp 这三个 Payload 最基本的参数。最后执行 token.toString() 返回 Token 字符串。

JWebToken token = mgr.tokenFactory("user01", Collections.singletonList("admin"), Utils.setExpire(24));
System.out.println(token.toString());

在这里插入图片描述
当然你也可以传入 Payload 实例或其子类。

/**
 * 创建 JWT Token
 *
 * @param payload Payload 实例或其子类
 * @return JWT Token
 */
public JWebToken tokenFactory(Payload payload);

Base64 编码问题

JWT 作为一个令牌(token),有些场合可能会放到 URL(比如 api.example.com/?token=xxx)。Base64 有三个字符+/=,在 URL 里面有特殊含义,所以要被替换掉:=被省略、+替换成-/替换成_。于是这个 Base64 算法是 Base64URL,跟 Base64 算法基本类似,但有一些小的不同。在 jdk8 之后提供了这样 Base64.getUrlEncoder().withoutPadding() 的 Base64URL 方式。

在这里插入图片描述

Token 校验

还是一位行家说得好:

先说签名验证。当接收方接收到一个JWT的时候,首先要对这个JWT的完整性进行验证,这个就是签名认证。它验证的方法其实很简单,只要把
header 做 base64 url解码,就能知道 JWT 用的什么算法做的签名,然后用这个算法,再次用同样的逻辑对 header 和
payload 做一次签名,并比较这个签名是否与 JWT 本身包含的第三个部分的串是否完全相同,只要不同,就可以认为这个 JWT
是一个被篡改过的串,自然就属于验证失败了。接收方生成签名的时候必须使用跟 JWT 发送方相同的密钥,意味着要做好密钥的安全传递或共享。

话不多说,直接给代码:

/**
 * 解析 Token
 *
 * @param tokenStr JWT Token
 */
public JWebToken parse(String tokenStr) {
    String[] parts = tokenStr.split("\\.");
    if (parts.length != 3)
        throw new IllegalArgumentException("无效 Token 格式");

    if (!JWebToken.encodedHeader.equals(parts[0]))
        throw new IllegalArgumentException("非法的 JWT Header: " + parts[0]);

    String json = Utils.decode(parts[1]);
    Payload payload = JsonHelper.parseMapAsBean(json, Payload.class);

    if (payload == null)
        throw new RuntimeException("Payload is Empty: ");

    if (payload.getExp() == null)
        throw new RuntimeException("Payload 不包含过期字段 exp:" + payload);

    JWebToken token = new JWebToken(payload);
    token.setSignature(parts[2]);

    return token;
}

/**
 * 校验是否合法的 Token
 *
 * @param token 待检验的 Token
 * @return 是否合法
 */
public boolean isValid(JWebToken token) {
    String _token = signature(token);
    System.out.println(">>>" + token.getSignature());
    System.out.println(":::" + _token);

    return token.getPayload().getExp() > Utils.now() //token not expired
            && token.getSignature().equals(_token); //signature matched
}

小结

JWT 我也是刚接触,如果有不对的地方敬请提出!

源码:https://gitee.com/sp42_admin/ajaxjs/tree/master/aj-backend/aj-user/src/main/java/com/ajaxjs/jwt。

参考

  • JWT实现token-based会话管理
  • 教你玩转JWT认证—从一个优惠券聊起
  • OAuth2.0、OpenID Connect和JWT

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/680385.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

四种缓存的避坑总结

背景 分布式、缓存、异步和多线程被称为互联网开发的四大法宝。今天我总结一下项目开发中常接触的四种缓存实际项目中遇到过的问题。 JVM堆内缓存 JVM堆内缓存因为可以避免memcache、redis等集中式缓存网络通信故障问题&#xff0c;目前还在项目中广泛使用。 堆内缓存需要注…

FFmpeg5.0源码阅读——avformat_find_stream_info

摘要&#xff1a;在使用FFmpeg库时通常使用avformat_find_stream_info相关函数来探测流的基本信息&#xff0c;为了更加深入理解FFmpeg的基本流程&#xff0c;本文根据FFmpeg 5.0的源码详细描述了该函数的具体实现。   关键字&#xff1a;FFmpeg   读者须知&#xff1a;读者…

数学之美:神奇的杨辉三角形,比帕斯卡早了近600年,致敬中国古代数学家(63)

小朋友们好&#xff0c;大朋友们好&#xff01; 我是猫妹&#xff0c;一名爱上Python编程的小学生。 和猫妹学Python&#xff0c;一起趣味学编程。 今日主题 什么是杨辉三角形&#xff1f; 杨辉三角形有什么规律&#xff1f; 中国古代数学家杨辉。 西方科学家帕斯卡。 杨…

【开源与项目实战:开源实战】81 | 开源实战三(上):借Google Guava学习发现和开发通用功能模块

上几节课&#xff0c;我们拿 Unix 这个超级大型开源软件的开发作为引子&#xff0c;从代码设计编写和研发管理两个角度&#xff0c;讲了如何应对大型复杂项目的开发。接下来&#xff0c;我们再讲一下 Google 开源的 Java 开发库 Google Guava。 Google Guava 是一个非常成功、…

智能指针类模板:auto_ptr、unique_ptr、shared_ptr的原理与使用

1. 什么是智能指针 智能指针是行为类似于指针的类对象&#xff0c;通常用于管理动态内存分配。C程序通常手动动态分配堆内存&#xff0c;但如果动态分配的内存没有释放&#xff0c;则会发生内存泄漏。 例如代码段1.1。 // 代码段1.1 void demo() {double *pd new double;*pd…

使用STM32F103的串口实现IAP程序升级功能

使用STM32F103的串口实现IAP程序升级功能 &#x1f3ac;IAP程序烧录全过程演示&#xff1a; ✨这几天折腾IAP升级功能&#xff0c;狂补了很多相关BootLoader相关的知识。本来最想实现IAP升级程序的方式是&#xff0c;基于SPI通讯的SD卡&#xff0c;借助挂载的FatFS文件系统&am…

C++中的内存分区

目录 操作系统的内存区域 C内存分区模型 1. 程序运行前 2. 程序运行后 3. new 操作符的使用 操作系统的内存区域 text段&#xff1a;存储程序的二进制指令&#xff0c;即程序源码编译后的二进制代码data段&#xff1a;存储已被初始化的全局变量、常量bss段&#xff1a;存储…

ES-工作原理

前言 ​ 搜索引擎是对数据的检索&#xff0c;而数据总体分为两种&#xff1a;结构化数据和非结构化数据。而对于结构化数据&#xff0c;因为他们具有特定的结构&#xff0c;所以一般都是可以通过关系型数据库MySQL/oracle的二维表的方式存储和搜索&#xff0c;也可以建立索引。…

Redis的简单使用 (实现Session持久化)

&#x1f389;&#x1f389;&#x1f389;点进来你就是我的人了博主主页&#xff1a;&#x1f648;&#x1f648;&#x1f648;戳一戳,欢迎大佬指点! 欢迎志同道合的朋友一起加油喔&#x1f93a;&#x1f93a;&#x1f93a; 目录 一、Redis数据类型的使用 1. 字符串&#xff…

Redis【入门篇】---- Redis的Java客户端-Jedis

Redis【入门篇】---- Redis的Java客户端-Jedis 1. Jedis快速入门2. Jedis连接池1. 创建Jedis连接池2. 改造原始代码 在Redis官网中提供了各种语言的客户端&#xff0c;地址&#xff1a;https://redis.io/docs/clients/ 其中Java客户端也包含很多&#xff1a; 标记为❤的就是推荐…

密码学证明方案寒武纪大爆发——扩容、透明性和隐私的变革潜力

1. 引言 前序博客有&#xff1a; ZKP大爆炸 本文主要参考&#xff1a; StarkWare 2023年6月博客 Cambrian Explosion of Cryptographic Proofs----The transformative potential for scalability, transparency, and privacy2023年3月Eli Ben-Sasson在The 13th BIU Winter …

JavaWeb之Cookie和Session

文章目录 CookieCookie基本介绍Cookie的基本使用Cookie的创建从服务器获取CookieCookie值的修改方案一方案二 浏览器查看CookieCookie声明控制Cookie有效路径Path的设置 SessionSession基本介绍Session的创建和获取&#xff08;id号&#xff0c;是否为新&#xff09;Session域数…

【SQL server关键字】

目录&#xff1a; 前言一、CREATE -- 创建二、INSERT INTO VALUES -- 插入数据三、SELECT FROM -- 查找数据1.SEKECT简单了解2.函数的使用3.选择列表与group by子句的对应4.exists子查询 四、UPDATE SET -- 更改数据五、ALTER -- 修改属性六、JOIN ON-- 链接多个表1. join初…

JDBC 望舒客栈项目 万字详解

目录 一、前言 二、项目结构 三、准备工作 1.建立子包 : 2.导入jar包 : 3.工具类 : 1 Utility工具类 2 JDBCUtilsDruid工具类 4.导入配置文件 : 5.引入BasicDAO : 四、项目主体 1.界面显示 : 1 代码演示 2 运行测试 2.用户登录 : 1 创建员工表employee 2 创建Ja…

JavaScript 事件加载有哪些应用场景?

&#x1f482; 个人网站:【海拥】【游戏大全】【神级源码资源网】&#x1f91f; 前端学习课程&#xff1a;&#x1f449;【28个案例趣学前端】【400个JS面试题】&#x1f485; 寻找学习交流、摸鱼划水的小伙伴&#xff0c;请点击【摸鱼学习交流群】 目录 前言什么是JavaScript事…

VUE L MVVM模型 ③

目录 文章有误请指正&#xff0c;如果觉得对你有用&#xff0c;请点三连一波&#xff0c;蟹蟹支持✨ V u e j s Vuejs Vuejs M V V M MVVM MVVM模型Data与El的2种写法总结 文章有误请指正&#xff0c;如果觉得对你有用&#xff0c;请点三连一波&#xff0c;蟹蟹支持✨ ⡖⠒⠒⠒…

多线程中的wait和notify

1、wait和notify 由于线程之间是抢占式执行的&#xff0c;所以线程之间的执行先后顺序难以预知。但实际上是希望合理的协调多个线程之间的执行先后顺序。 完成这个协调工作&#xff0c;主要涉及到三个方法 *wait()/wait(long timeout);让当前线程进入等待状态。 *notify()/n…

【每日算法 数据结构(C++)】—— 02 | 数组的并交集(解题思路、流程图、代码片段)

文章目录 01 | &#x1f451; 题目描述02 | &#x1f50b; 解题思路交集并集 03 | &#x1f9e2; 代码片段交集并集 When you feel like giving up, remember why you started. 当你想放弃时&#xff0c;请记住为什么你开始 01 | &#x1f451; 题目描述 给你两个数组&#xff…