当前位置:   article > 正文

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

jwt简单生成

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\"}";
  • 1
  • 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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39

进而描绘出 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;
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53

创建 Token

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

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

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

然后把 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());
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

这里出现了 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() {
    }
    ……
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

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

通过工厂方法创建 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());
  • 1
  • 2

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

/**
 * 创建 JWT Token
 *
 * @param payload Payload 实例或其子类
 * @return JWT Token
 */
public JWebToken tokenFactory(Payload payload);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

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
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42

小结

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

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

参考

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/菜鸟追梦旅行/article/detail/544439
推荐阅读
相关标签
  

闽ICP备14008679号