动手写一个 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"}。我们用一个常量定义之:
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-iam/aj-iam-server/src/main/java/com/ajaxjs/iam/jwt

参考

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

sp42a

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值