springboot 集成jwt

原文地址:https://mp.weixin.qq.com/s/Jo3PZoa7nL99c8UCxPiTTA
以前一直使用的是jjwt这个JWT库,虽然小巧够用, 但对JWT的一些细节封装的不是很好。最近发现了一个更好用的JWT库nimbus-jose-jwt,简单易用,API非常易于理解,对称加密和非对称加密算法都支持,推荐给大家!
简介
nimbus-jose-jwt是最受欢迎的JWT开源库,基于Apache 2.0开源协议,支持所有标准的签名(JWS)和加密(JWE)算法。

JWT概念关系
这里我们需要了解下JWT、JWS、JWE三者之间的关系,其实JWT(JSON Web Token)指的是一种规范,这种规范允许我们使用JWT在两个组织之间传递安全可靠的信息。而JWS(JSON Web Signature)和JWE(JSON Web Encryption)是JWT规范的两种不同实现,我们平时最常使用的实现就是JWS。

使用
接下来我们将介绍下nimbus-jose-jwt库的使用,主要使用对称加密(HMAC)和非对称加密(RSA)两种算法来生成和解析JWT令牌。

对称加密(HMAC)
对称加密指的是使用相同的秘钥来进行加密和解密,如果你的秘钥不想暴露给解密方,考虑使用非对称加密。

要使用nimbus-jose-jwt库,首先在pom.xml添加相关依赖;

<!--JWT解析库-->
<dependency>
    <groupId>com.nimbusds</groupId>
    <artifactId>nimbus-jose-jwt</artifactId>
    <version>8.16</version>
</dependency>

创建JwtTokenServiceImpl作为JWT处理的业务类,添加根据HMAC算法生成和解析JWT令牌的方法,可以发现nimbus-jose-jwt库操作JWT的API非常易于理解;

/**
 * Created by macro on 2020/6/22.
 */
@Service
public class JwtTokenServiceImpl implements JwtTokenService {
    @Override
    public String generateTokenByHMAC(String payloadStr, String secret) throws JOSEException {
        //创建JWS头,设置签名算法和类型
        JWSHeader jwsHeader = new JWSHeader.Builder(JWSAlgorithm.HS256).
                type(JOSEObjectType.JWT)
                .build();
        //将负载信息封装到Payload中
        Payload payload = new Payload(payloadStr);
        //创建JWS对象
        JWSObject jwsObject = new JWSObject(jwsHeader, payload);
        //创建HMAC签名器
        JWSSigner jwsSigner = new MACSigner(secret);
        //签名
        jwsObject.sign(jwsSigner);
        return jwsObject.serialize();
    }

    @Override
    public PayloadDto verifyTokenByHMAC(String token, String secret) throws ParseException, JOSEException {
        //从token中解析JWS对象
        JWSObject jwsObject = JWSObject.parse(token);
        //创建HMAC验证器
        JWSVerifier jwsVerifier = new MACVerifier(secret);
        if (!jwsObject.verify(jwsVerifier)) {
            throw new JwtInvalidException("token签名不合法!");
        }
        String payload = jwsObject.getPayload().toString();
        PayloadDto payloadDto = JSONUtil.toBean(payload, PayloadDto.class);
        if (payloadDto.getExp() < new Date().getTime()) {
            throw new JwtExpiredException("token已过期!");
        }
        return payloadDto;
    }
}

创建PayloadDto实体类,用于封装JWT中存储的信息;

/**
 * Created by macro on 2020/6/22.
 */
@Data
@EqualsAndHashCode(callSuper = false)
@Builder
public class PayloadDto {
    @ApiModelProperty("主题")
    private String sub;
    @ApiModelProperty("签发时间")
    private Long iat;
    @ApiModelProperty("过期时间")
    private Long exp;
    @ApiModelProperty("JWT的ID")
    private String jti;
    @ApiModelProperty("用户名称")
    private String username;
    @ApiModelProperty("用户拥有的权限")
    private List<String> authorities;
}

在JwtTokenServiceImpl类中添加获取默认的PayloadDto的方法,JWT过期时间设置为60s;

/**
 * Created by macro on 2020/6/22.
 */
@Service
public class JwtTokenServiceImpl implements JwtTokenService {
    @Override
    public PayloadDto getDefaultPayloadDto() {
        Date now = new Date();
        Date exp = DateUtil.offsetSecond(now, 60*60);
        return PayloadDto.builder()
                .sub("macro")
                .iat(now.getTime())
                .exp(exp.getTime())
                .jti(UUID.randomUUID().toString())
                .username("macro")
                .authorities(CollUtil.toList("ADMIN"))
                .build();
    }
}

创建JwtTokenController类,添加根据HMAC算法生成和解析JWT令牌的接口,由于HMAC算法需要长度至少为32个字节的秘钥,所以我们使用MD5加密下;
/**

  • JWT令牌管理Controller

  • Created by macro on 2020/6/22.
    */
    @Api(tags = "JwtTokenController", description = "JWT令牌管理")
    @Controller
    @RequestMapping("/token")
    public class JwtTokenController {

    @Autowired
    private JwtTokenService jwtTokenService;

    @ApiOperation("使用对称加密(HMAC)算法生成token")
    @RequestMapping(value = "/hmac/generate", method = RequestMethod.GET)
    @ResponseBody
    public CommonResult generateTokenByHMAC() {
    try {
    PayloadDto payloadDto = jwtTokenService.getDefaultPayloadDto();
    String token = jwtTokenService.generateTokenByHMAC(JSONUtil.toJsonStr(payloadDto), SecureUtil.md5("test"));
    return CommonResult.success(token);
    } catch (JOSEException e) {
    e.printStackTrace();
    }
    return CommonResult.failed();
    }

    @ApiOperation("使用对称加密(HMAC)算法验证token")
    @RequestMapping(value = "/hmac/verify", method = RequestMethod.GET)
    @ResponseBody
    public CommonResult verifyTokenByHMAC(String token) {
    try {
    PayloadDto payloadDto = jwtTokenService.verifyTokenByHMAC(token, SecureUtil.md5("test"));
    return CommonResult.success(payloadDto);
    } catch (ParseException | JOSEException e) {
    e.printStackTrace();
    }
    return CommonResult.failed();

    }
    }

调用使用HMAC算法生成JWT令牌的接口进行测试;
![image.png](https://upload-images.jianshu.io/upload_images/20120146-beb8493a0df1a181.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
调用使用HMAC算法解析JWT令牌的接口进行测试。
![image.png](https://upload-images.jianshu.io/upload_images/20120146-47ce6cd51cab3363.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
非对称加密(RSA)
非对称加密指的是使用公钥和私钥来进行加密解密操作。对于加密操作,公钥负责加密,私钥负责解密,对于签名操作,私钥负责签名,公钥负责验证。非对称加密在JWT中的使用显然属于签名操作。

如果我们需要使用固定的公钥和私钥来进行签名和验证的话,我们需要生成一个证书文件,这里将使用Java自带的keytool工具来生成jks证书文件,该工具在JDK的bin目录下;
![image.png](https://upload-images.jianshu.io/upload_images/20120146-0a36f7043dda733d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
打开CMD命令界面,使用如下命令生成证书文件,设置别名为jwt,文件名为jwt.jks;

keytool -genkey -alias jwt -keyalg RSA -keystore jwt.jks

输入密码为123456,然后输入各种信息之后就可以生成证书jwt.jks文件了;
![image.png](https://upload-images.jianshu.io/upload_images/20120146-eeeac286f618dffc.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
将证书文件jwt.jks复制到项目的resource目录下,然后需要从证书文件中读取RSAKey,这里我们需要在pom.xml中添加一个Spring Security的RSA依赖;


<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-rsa</artifactId>
<version>1.0.7.RELEASE</version>
</dependency>

然后在JwtTokenServiceImpl类中添加方法,从类路径下读取证书文件并转换为RSAKey对象;

/**

  • Created by macro on 2020/6/22.
    */
    @Service
    public class JwtTokenServiceImpl implements JwtTokenService {
    @Override
    public RSAKey getDefaultRSAKey() {
    //从classpath下获取RSA秘钥对
    KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("jwt.jks"), "123456".toCharArray());
    KeyPair keyPair = keyStoreKeyFactory.getKeyPair("jwt", "123456".toCharArray());
    //获取RSA公钥
    RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
    //获取RSA私钥
    RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
    return new RSAKey.Builder(publicKey).privateKey(privateKey).build();
    }
    }
我们可以在JwtTokenController中添加一个接口,用于获取证书中的公钥;

/**

  • JWT令牌管理Controller

  • Created by macro on 2020/6/22.
    */
    @Api(tags = "JwtTokenController", description = "JWT令牌管理")
    @Controller
    @RequestMapping("/token")
    public class JwtTokenController {

    @Autowired
    private JwtTokenService jwtTokenService;

    @ApiOperation("获取非对称加密(RSA)算法公钥")
    @RequestMapping(value = "/rsa/publicKey", method = RequestMethod.GET)
    @ResponseBody
    public Object getRSAPublicKey() {
    RSAKey key = jwtTokenService.getDefaultRSAKey();
    return new JWKSet(key).toJSONObject();
    }
    }

调用该接口,查看公钥信息,公钥是可以公开访问的;
![image.png](https://upload-images.jianshu.io/upload_images/20120146-c7e4d0eb5021b01d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
在JwtTokenServiceImpl中添加根据RSA算法生成和解析JWT令牌的方法,可以发现和上面的HMAC算法操作基本一致;

/**

  • Created by macro on 2020/6/22.
    */
    @Service
    public class JwtTokenServiceImpl implements JwtTokenService {
    @Override
    public String generateTokenByRSA(String payloadStr, RSAKey rsaKey) throws JOSEException {
    //创建JWS头,设置签名算法和类型
    JWSHeader jwsHeader = new JWSHeader.Builder(JWSAlgorithm.RS256)
    .type(JOSEObjectType.JWT)
    .build();
    //将负载信息封装到Payload中
    Payload payload = new Payload(payloadStr);
    //创建JWS对象
    JWSObject jwsObject = new JWSObject(jwsHeader, payload);
    //创建RSA签名器
    JWSSigner jwsSigner = new RSASSASigner(rsaKey, true);
    //签名
    jwsObject.sign(jwsSigner);
    return jwsObject.serialize();
    }

    @Override
    public PayloadDto verifyTokenByRSA(String token, RSAKey rsaKey) throws ParseException, JOSEException {
    //从token中解析JWS对象
    JWSObject jwsObject = JWSObject.parse(token);
    RSAKey publicRsaKey = rsaKey.toPublicJWK();
    //使用RSA公钥创建RSA验证器
    JWSVerifier jwsVerifier = new RSASSAVerifier(publicRsaKey);
    if (!jwsObject.verify(jwsVerifier)) {
    throw new JwtInvalidException("token签名不合法!");
    }
    String payload = jwsObject.getPayload().toString();
    PayloadDto payloadDto = JSONUtil.toBean(payload, PayloadDto.class);
    if (payloadDto.getExp() < new Date().getTime()) {
    throw new JwtExpiredException("token已过期!");
    }
    return payloadDto;
    }
    }

在JwtTokenController类,添加根据RSA算法生成和解析JWT令牌的接口,使用默认的RSA钥匙对;

/**

  • JWT令牌管理Controller

  • Created by macro on 2020/6/22.
    */
    @Api(tags = "JwtTokenController", description = "JWT令牌管理")
    @Controller
    @RequestMapping("/token")
    public class JwtTokenController {

    @Autowired
    private JwtTokenService jwtTokenService;

    @ApiOperation("使用非对称加密(RSA)算法生成token")
    @RequestMapping(value = "/rsa/generate", method = RequestMethod.GET)
    @ResponseBody
    public CommonResult generateTokenByRSA() {
    try {
    PayloadDto payloadDto = jwtTokenService.getDefaultPayloadDto();
    String token = jwtTokenService.generateTokenByRSA(JSONUtil.toJsonStr(payloadDto),jwtTokenService.getDefaultRSAKey());
    return CommonResult.success(token);
    } catch (JOSEException e) {
    e.printStackTrace();
    }
    return CommonResult.failed();
    }

    @ApiOperation("使用非对称加密(RSA)算法验证token")
    @RequestMapping(value = "/rsa/verify", method = RequestMethod.GET)
    @ResponseBody
    public CommonResult verifyTokenByRSA(String token) {
    try {
    PayloadDto payloadDto = jwtTokenService.verifyTokenByRSA(token, jwtTokenService.getDefaultRSAKey());
    return CommonResult.success(payloadDto);
    } catch (ParseException | JOSEException e) {
    e.printStackTrace();
    }
    return CommonResult.failed();
    }
    }

调用使用RSA算法生成JWT令牌的接口进行测试;
![image.png](https://upload-images.jianshu.io/upload_images/20120146-d31c0565a9ad3a9c.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
调用使用RSA算法解析JWT令牌的接口进行测试。
![image.png](https://upload-images.jianshu.io/upload_images/20120146-0633824877c1a96c.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
参考资料
官方文档:https://connect2id.com/products/nimbus-jose-jwt

项目源码地址
https://github.com/macrozheng/mall-learning/tree/master/mall-tiny-jwt


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