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

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"}。我们用一个常量定义之:
java 复制代码
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 如何:

java 复制代码
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

java 复制代码
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 )部分,这样还可以防止数据被篡改。

java 复制代码
String signature = HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload),  secret);
String jwtToken = base64(header) + "." + base64(payload) + "." + signature;

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

JWebTokenMgr

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

java 复制代码
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 信息,还有其他颁发者等的信息。

java 复制代码
/**
 * 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 字符串。

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

当然你也可以传入 Payload 实例或其子类。

java 复制代码
/**
 * 创建 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 发送方相同的密钥,意味着要做好密钥的安全传递或共享。

话不多说,直接给代码:

java 复制代码
/**
 * 解析 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 第一版就这么出来了------如果有不对的地方敬请提出!

源码

源码:gitcode.com/lightweight...

参考

相关推荐
xuejianxinokok15 分钟前
解惑rust中的 Send/Sync(译)
后端·rust
Siler25 分钟前
Oracle利用数据泵进行数据迁移
后端
用户67570498850235 分钟前
3分钟,手摸手教你用OpenResty搭建高性能隧道代理(附完整配置!)
后端
coding随想1 小时前
网络世界的“快递站”:深入浅出OSI七层模型
后端·网络协议
skeletron20111 小时前
🚀AI评测这么玩(2)——使用开源评测引擎eval-engine实现问答相似度评估
前端·后端
shark_chili1 小时前
颠覆认知!这才是synchronized最硬核的打开方式
后端
就是帅我不改1 小时前
99%的Java程序员都写错了!高并发下你的Service层正在拖垮整个系统!
后端·架构
Apifox1 小时前
API 文档中有多种参数结构怎么办?Apifox 里用 oneOf/anyOf/allOf 这样写
前端·后端·测试
似水流年流不尽思念1 小时前
如何实现一个线程安全的单例模式?
后端·面试
楽码1 小时前
了解HMAC及实现步骤
后端·算法·微服务