JWT(二):使用 Java 实现 JWT

JWT(一):认识 JSON WebToken
JWT(二):使用 Java 实现 JWT

介绍

原理在上篇《JWT(一):认识 JSON Web Token》已经说过了,实现起来并不难,你可以自己写一个 jwt 工具类(如果你有兴趣的话)

当然了,重复造轮子不是程序员的风格,我们主张拿来主义!


鲁迅-又拿我说事儿.jpg

JWT 官网提供了多种语言的 JWT 库,详情可以参考 https://jwt.io/#debugger 页面下半部分

建议使用 jjwt库 ,它的github地址 https://github.com/jwtk/jjwt

jjwt 版本 0.10.7,它和 0.9.x 有很大的区别,一定要注意!!!

本文分5部分

  • 第1部分:以简单例子演示生成、验证、解析 jwt 过程
  • 第2部分:介绍 jjwt 的常用方法
  • 第3部分:封装一个常用的 jwt 工具类
    如果只是拿来主义,看到这里就可以了
  • 第4部分:介绍 jjwt 的各种签名算法
  • 第5部分:对 jwt 进行安全加密

<a name="simpleExample"></a>

简单例子

引入 MAVN 依赖

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.10.7</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.10.7</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.10.7</version>
    <scope>runtime</scope>
</dependency>

一个例子

    
    // 生成密钥
    String key = "0123456789_0123456789_0123456789";
    SecretKey secretKey = new SecretKeySpec(key.getBytes(), SignatureAlgorithm.HS256.getJcaName());

    // 1. 生成 token
    String token = Jwts.builder()     // 创建 JWT 对象
            .setSubject("JSON Web Token")   // 设置主题(声明信息)
            .signWith(secretKey)    // 设置安全密钥(生成签名所需的密钥和算法)
            .compact(); // 生成token(1.编码 Header 和 Payload 2.生成签名 3.拼接字符串)
    System.out.println(token);

    //token = token + "s";

    // 2. 验证token,如果验证token失败则会抛出异常
    try {
        Jwts.parser()
                .setSigningKey(secretKey)
                .parseClaimsJws(token);
        // OK, we can trust this token
        System.out.println("验证成功");
    } catch (JwtException e) {
        //don't trust the token!
        System.out.println("验证失败");
    }

    // 3. 解析token
    Claims body = Jwts.parser()     // 创建解析对象
            .setSigningKey(secretKey)   // 设置安全密钥(生成签名所需的密钥和算法)
            .parseClaimsJws(token)  // 解析token
            .getBody(); // 获取 payload 部分内容
    System.out.println(body);

输出结果:

eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJKU09OIFdlYiBUb2tlbiJ9.QwmY_0qXW4BhAHcDpxz62v3xqkFYbg5lsZQhM2t-kVs
验证成功
{sub=JSON Web Token}

<a name="commonMethods"></a>

常用方法

以下内容建议参考源码获知更多详情

Jwts.builder() 创建了 DefaultJwtBuilder 对象,该对象的常用方法如下:

Header

compact() 方法中会自动根据签名算法设置头部信息,当然也可以手动设置

  • setHeader(Header header): JwtBuilder
  • setHeader(Map<String, Object> header): JwtBuilder
  • setHeaderParams(Map<String, Object> params): JwtBuilder
  • setHeaderParam(String name, Object value): JwtBuilder

参数 Header 对象 可通过 Jwts.header(); 创建,它简单得就像一个 map (把它当做 map 使用即可)

Payload

至少设置一个 claims,否则在生成签名时会抛出异常

  • setClaims(Claims claims): JwtBuilder
  • setClaims(Map<String, Object> claims): JwtBuilder
  • addClaims(Map<String, Object> claims): JwtBuilder
  • setIssuer(String iss): JwtBuilder
  • setSubject(String sub): JwtBuilder
  • setAudience(String aud): JwtBuilder
  • setExpiration(Date exp): JwtBuilder
  • setNotBefore(Date nbf): JwtBuilder
  • setIssuedAt(Date iat): JwtBuilder
  • setId(String jti): JwtBuilder
  • claim(String name, Object value: JwtBuilder

参数对象 Claims 同 Header 类似,通过 Jwts.claims() 创建,同样简单得就像一个 map

值得注意的一点是:不要在 setXxx 之后调用 setClaims(Claims claims) 或 setClaims(Map<String, Object> claims),因为这两个方法会覆盖所有已设置的 claim

Signature

  • signWith(Key key)
  • signWith(Key key, SignatureAlgorithm alg)
  • signWith(SignatureAlgorithm alg, byte[] secretKeyBytes)
  • signWith(SignatureAlgorithm alg, String base64EncodedSecretKey)
  • signWith(SignatureAlgorithm alg, Key key)

以上方法最终就是设置两个对象:key 和 algorithm,分别代表密钥和算法
方法内部生成密钥使用的方法的和演示中的一样

SecretKey key = new SecretKeySpec(secretKeyBytes, alg.getJcaName());

注意:key 的长度必须符合签名算法的要求(避免生成弱密钥)
HS256:bit 长度要>=256,即字节长度>=32
HS384:bit 长度要>=384,即字节长度>=48
HS512:bit 长度要>=512,即字节长度>=64
在 secret key algorithms 名称中的数字代表了最小bit长度

更多签名算法的详情,请参考签名算法小节

<a name="jwtUtils"></a>

封装 JWT 工具类

package com.liuchuanv.jwt;

import io.jsonwebtoken.*;
import io.jsonwebtoken.security.SignatureException;

import javax.crypto.spec.SecretKeySpec;
import java.security.Key;
import java.util.Date;
import java.util.Map;
import java.util.UUID;

/**
 * JSON Web Token 工具类
 *
 * @author LiuChuanWei
 * @date 2019-12-11
 */
public class JwtUtils {

    /**
     * key(按照签名算法的字节长度设置key)
     */
    private final static String SECRET_KEY = "0123456789_0123456789_0123456789";
    /**
     * 过期时间(毫秒单位)
     */
    private final static long TOKEN_EXPIRE_MILLIS = 1000 * 60 * 60;

    /**
     * 创建token
     * @param claimMap
     * @return
     */
    public static String createToken(Map<String, Object> claimMap) {
        long currentTimeMillis = System.currentTimeMillis();
        return Jwts.builder()
                .setId(UUID.randomUUID().toString())
                .setIssuedAt(new Date(currentTimeMillis))    // 设置签发时间
                .setExpiration(new Date(currentTimeMillis + TOKEN_EXPIRE_MILLIS))   // 设置过期时间
                .addClaims(claimMap)
                .signWith(generateKey())
                .compact();
    }

    /**
     * 验证token
     * @param token
     * @return 0 验证成功,1、2、3、4、5 验证失败
     */
    public static int verifyToken(String token) {
        try {
            Jwts.parser().setSigningKey(generateKey()).parseClaimsJws(token);
            return 0;
        } catch (ExpiredJwtException e) {
            e.printStackTrace();
            return 1;
        } catch (UnsupportedJwtException e) {
            e.printStackTrace();
            return 2;
        } catch (MalformedJwtException e) {
            e.printStackTrace();
            return 3;
        } catch (SignatureException e) {
            e.printStackTrace();
            return 4;
        } catch (IllegalArgumentException e) {
            e.printStackTrace();
            return 5;
        }
    }

    /**
     * 解析token
     * @param token
     * @return
     */
    public static Map<String, Object> parseToken(String token) {
        return Jwts.parser()  // 得到DefaultJwtParser
                .setSigningKey(generateKey()) // 设置签名密钥
                .parseClaimsJws(token)
                .getBody();
    }

    /**
     * 生成安全密钥
     * @return
     */
    public static Key generateKey() {
       return new SecretKeySpec(SECRET_KEY.getBytes(), SignatureAlgorithm.HS256.getJcaName());
    }
}

测试代码如下:

  //Map<String, Object> map = new HashMap<String, Object>();
        //map.put("userId", 1002);
        //map.put("userName", "张晓明");
        //map.put("age", 12);
        //map.put("address", "山东省青岛市李沧区");
        //String token = JwtUtils.createToken(map);
        //System.out.println(token);

        String token = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI0ZWM2NWNhNC0wZjVmLTRlOTktOTI5NS1mYWUyN2UwODIzYzQiLCJpYXQiOjE1NzY0OTI4NjYsImV4cCI6MTU3NjQ5NjQ2NiwiYWRkcmVzcyI6IuWxseS4nOecgemdkuWym-W4guadjuayp-WMuiIsInVzZXJOYW1lIjoi5byg5pmT5piOIiwidXNlcklkIjoxMDAyLCJhZ2UiOjEyfQ.6Z18aIA6y52ntQkV3BwlYiVK3hL3R2WFujjTmuvimww";
        int result = JwtUtils.verifyToken(token);
        System.out.println(result);

        Map<String, Object> map = JwtUtils.parseToken(token);
        System.out.println(map);

输出结果:

0
{jti=4ec65ca4-0f5f-4e99-9295-fae27e0823c4, iat=1576492866, exp=1576496466, address=山东省青岛市李沧区, userName=张晓明, userId=1002, age=12}

<a name="signatureAlgorithms"></a>

签名算法

12 种签名算法

JWT 规范定义了12种标准签名算法:3种 secret key 算法和9种非对称密钥算法

  • HS256: HMAC using SHA-256
  • HS384: HMAC using SHA-384
  • HS512: HMAC using SHA-512
  • ES256: ECDSA using P-256 and SHA-256
  • ES384: ECDSA using P-384 and SHA-384
  • ES512: ECDSA using P-521 and SHA-512
  • RS256: RSASSA-PKCS-v1_5 using SHA-256
  • RS384: RSASSA-PKCS-v1_5 using SHA-384
  • RS512: RSASSA-PKCS-v1_5 using SHA-512
  • PS256: RSASSA-PSS using SHA-256 and MGF1 with SHA-256
  • PS384: RSASSA-PSS using SHA-384 and MGF1 with SHA-384
  • PS512: RSASSA-PSS using SHA-512 and MGF1 with SHA-512

根据算法名称可分为四类:HSxxx(secret key 算法)、ESxxx、RSxxx、PSxxx

HSxxx、ESxxx 中的 xxx 表示算法 key 最小 Bit 长度
RSxxx、PSxxx 中的 xxx 表示算法 key 最小 Byte 长度

规定key的最小长度是为了避免因 key 过短生成弱密钥

生成密钥

jjwt 生成 secret key 两种方法

String key = "1234567890_1234567890_1234567890";
// 1. 根据key生成密钥(会根据字节参数长度自动选择相应的 HMAC 算法)
SecretKey secretKey1 = Keys.hmacShaKeyFor(key.getBytes());
// 2. 根据随机数生成密钥
SecretKey secretKey2 = Keys.secretKeyFor(SignatureAlgorithm.HS256);
  • 方法 Keys.hmacShaKeyFor(byte[]) 内部也是使用 new SecretKeySpec(bytes, alg.getJcaName()) 来生成密钥的
  • 方法 Keys.secretKeyFor(SignatureAlgorithm) 内部使用 KeyGenerator.generateKey() 生成密钥

jjwt 也提供了非对称密钥对的生成方法

// 1. 使用jjwt提供的方法生成
KeyPair keyPair = Keys.keyPairFor(SignatureAlgorithm.RS256);    //or RS384, RS512, PS256, PS384, PS512, ES256, ES384, ES512

// 2. 手动生成
int keySize = 1024;
// RSA算法要求有一个可信任的随机数源
SecureRandom secureRandom = new SecureRandom();
// 为RSA算法创建一个KeyPairGenerator对象 
KeyPairGenerator keyPairGenerator = null;
try {
    keyPairGenerator = KeyPairGenerator.getInstance("RSA");
} catch (NoSuchAlgorithmException e) {
    e.printStackTrace();
}
// 利用上面的随机数据源初始化这个KeyPairGenerator对象
keyPairGenerator.initialize(keySize, secureRandom);
// 生成密钥对
KeyPair keyPair2 = keyPairGenerator.generateKeyPair();

  • Keys.keyPairFor(SignatureAlgorithm) 会根据算法自动生成相应长度的
  • signWith(secretKey) 会根据密钥长度自动选择相应算法,也可以指定任意算法(指定的算法不受密钥长度限制,可任意选择,即用 RS256生成的密钥,可以 signWith(secretKey, SignatureAlgorithm.RS512),但是 JJWT 并不建议这么做)
  • 在加密时使用 keyPair.getPrivate() ,解密时使用 keyPair.getPublic()

不同密钥生成token

以上都是使用同一密钥签名生成所有的token,下面我们使用不同的密钥

这一个特性可以应用于不同用户/角色使用不同的密钥生成的 token,帮助你更好的构建权限系统

  1. 首先在 Header(或 claims)中设置一个 keyId

  2. 定义一个类,继承 SigningKeyResolverAdapter,并重写 resolveSigningKey() 或 resolveSigningKeyBytes() 方法

    public class MySigningKeyResolver extends SigningKeyResolverAdapter {
        @Override
        public Key resolveSigningKey(JwsHeader header, Claims claims) {
            // 除了从 header 中获取 keyId 外,也可以从 claims 中获取(前提是在 claims 中设置了 keyId 声明)
            String keyId = header.getKeyId();
            // 根据 keyId 查找相应的 key
            Key key = lookupVerificationKey(keyId);
            return key;
        }
    
        public Key lookupVerificationKey(String keyId) {
            // TODO 根据 keyId 获取 key,比如从数据库中获取
            // 下面语句仅做演示用,绝对不可用于实际开发中!!!
            String key = "qwertyuiopasdfghjklzxcvbnm2019_" + keyId;
            return Keys.hmacShaKeyFor(key.getBytes());
        }
    }
    
  3. 解析时,不再调用 setSigningKey(SecretKey) ,而是调用 setSigningKeyResolver(SigningKeyResolver)

    // 生成密钥
            // TODO 此处 keyId 仅做演示用,实际开发中可以使用 UserId、RoleId 等作为 keyId
            String keyId = new Long(System.currentTimeMillis()).toString();
            System.out.println("keyId=" + keyId);
    
            String key = "qwertyuiopasdfghjklzxcvbnm2019_" + keyId;
            SecretKey secretKey = new SecretKeySpec(key.getBytes(), SignatureAlgorithm.HS256.getJcaName());
    
            // 1. 生成 token
            String token = Jwts.builder()
                    .setHeaderParam(JwsHeader.KEY_ID, keyId)    // 设置 keyId(当然也可以在 claims 中设置)
                    .setSubject("JSON Web Token")
                    .signWith(secretKey)
                    .compact();
            System.out.println("token=" + token);
    
            // 2. 验证token
            // token 使用了不同的密钥生成签名,在解析时就不用调用 setSigningKey(SecretKey) 了
            // 而是调用 setSigningKeyResolver(SigningKeyResolver)
            try {
                Jwts.parser()
                        .setSigningKeyResolver(new MySigningKeyResolver())
                        .parseClaimsJws(token);
                // OK, we can trust this token
                System.out.println("token验证成功");
            } catch (JwtException e) {
                //don't trust the token!
                System.out.println("token验证失败");
            }
    
    

<a name="encrypt"></a>

安全加密

敬请期待 .....

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342