什么是nimbus-jose-jwt?

nimbus-jose-jwt是基于Apache2.0开源协议的JWT开源库,支持所有的签名(JWS)和加密(JWE)算法。

对于JWT、JWS、JWE介绍

  • JWT是一种规范,它强调了两个组织之间传递安全的信息
  • JWS是JWT的一种实现,包含三部分header(头部)、payload(载荷)、signature(签名)
  • JWE也是JWT的一种实现,包含五部分内容。

在这里插入图片描述

接下来我们将使用对称加密(HMAC)和非对称加密(RSA)两种算法生成和解析JWT令牌。

1.对称加密(HMAC)

对称加密使用相同的密钥进行加密和解密。

  • 首先在pom.xml添加nimbus-jose-jwt依赖库
    <dependencies>
        <!-- web组件-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>2.3.0.RELEASE<version>
        </dependency>
        <!--jwt工具-->
        <dependency>
            <groupId>com.nimbusds</groupId>
            <artifactId>nimbus-jose-jwt</artifactId>
            <version>8.16<version>
        </dependency>
    </dependencies>
  • 创建PayloadDto实体类,用于封装JWT中存储的用户信息;
@Data
@ApiModel("信息实体类")
@Builder
@EqualsAndHashCode(callSuper = false)
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;
}
  • 创建JwtTokenService接口以及JwtTokenServiceImpl逻辑实现类,在其中添加根据HMAC算法生成和验证令牌方法。
/**
 * Created by zsh on 2022/3/9
 */
public interface JwtTokenService {
    /**
     * 使用HMAC对称加密算法生成token
     */
    String generateTokenByHMAC(String payloadStr, String secret) throws KeyLengthException;

    /**
     * 模拟生成用户数据
     */
    PayloadDto getDefaultPayloadDto();

    /**
     * 验证令牌
     */
    PayloadDto verifyTokenByHMAC(String token, String secret);
}
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.json.JSONUtil;
import com.nimbusds.jose.*;
import com.nimbusds.jose.crypto.MACSigner;
import com.nimbusds.jose.crypto.MACVerifier;
import com.zsh.mall.domain.PayloadDto;
import com.zsh.mall.exception.JwtExpireException;
import com.zsh.mall.exception.JwtInvalidException;
import com.zsh.mall.service.JwtTokenService;
import org.springframework.stereotype.Service;

import java.text.ParseException;
import java.util.Date;
import java.util.UUID;

/**
 * Created by zsh on 2022/3/9
 */
@Service
public class JwtTokenServiceImpl implements JwtTokenService {

    @Override
    public String generateTokenByHMAC(String payloadStr, String secret) {
        try {
            //准备JWS-header
            JWSHeader jwsHeader = new JWSHeader.Builder(JWSAlgorithm.HS256)
                    .type(JOSEObjectType.JWT).build();
            //将负载信息装载到payload
            Payload payload = new Payload(payloadStr);
            //封装header和payload到JWS对象
            JWSObject jwsObject = new JWSObject(jwsHeader, payload);
            //创建HMAC签名器
            JWSSigner jwsSigner = new MACSigner(secret);
            //签名
            jwsObject.sign(jwsSigner);
            return jwsObject.serialize();
        } catch (KeyLengthException e) {
            e.printStackTrace();
        } catch (JOSEException e) {
            e.printStackTrace();
        }
        return null;
    }

    @Override
    public PayloadDto getDefaultPayloadDto() {
        Date now = new Date();
        Date exp = DateUtil.offsetSecond(now, 60 * 60);
        return PayloadDto.builder()
                .sub("zsh")
                .iat(now.getTime())
                .exp(exp.getTime())
                .jti(UUID.randomUUID().toString())
                .username("zsh")
                .authorities(CollUtil.toList("ADMIN"))
                .build();
    }

    @Override
    public PayloadDto verifyTokenByHMAC(String token, String secret) {
        try {
            JWSObject jwsObject = JWSObject.parse(token);
            //创建HMAC验证器
            JWSVerifier jwsVerifier = new MACVerifier(secret);
            if (!jwsObject.verify(jwsVerifier)) {
                throw new JwtInvalidException(401, "token签名不合法!");
            }
            String payload = jwsObject.getPayload().toString();
            PayloadDto payloadDto = JSONUtil.toBean(payload, PayloadDto.class);
            if (payloadDto.getExp() < new Date().getTime()) {
                throw new JwtExpireException(401, "token已过期!");
            }
            return payloadDto;
        } catch (ParseException | JOSEException e) {
            e.printStackTrace();
        } catch (JwtInvalidException e) {
            e.printStackTrace();
        } catch (JwtExpireException e) {
            e.printStackTrace();
        }
        return null;
    }
}
  • 创建JwtTokenController类,编写依据HMAC算法生成和解析令牌的接口;注意HMAC算法要求密钥的长度至少为32个字节,所以这里我使用了MD5进行了加密充当密钥。
/**
 * Created by zsh on 2022/3/9
 */
@Api(tags = "JwtTokenController", value = "JWT令牌管理")
@RestController
public class JwtTokenController {

    @Resource
    private JwtTokenService jwtTokenService;

    @ApiOperation("使用HMAC对称加密生成token")
    @GetMapping(value = "/hmac/generate")
    public CommonResult generateTokenByHMAC() throws KeyLengthException {
        PayloadDto payloadDto = jwtTokenService.getDefaultPayloadDto();
        String token = jwtTokenService.generateTokenByHMAC(JSONUtil.toJsonStr(payloadDto), SecureUtil.md5("test"));
        return CommonResult.success(token);
    }

    @ApiOperation("验签")
    @GetMapping(value = "/hmac/verify")
    public CommonResult verifyTokenByHMAC(String token) {
        PayloadDto payloadDto = jwtTokenService.verifyTokenByHMAC(token, SecureUtil.md5("test"));
        return CommonResult.success(payloadDto);
    }
}

其他swagger相关的配置和异常类的编写不再展示。

  • 启动访问localhost:8080/swagger-ui.html

在这里插入图片描述
点击测试,发现生成令牌接口正常。
在这里插入图片描述
根据生成的令牌进行解析

在这里插入图片描述
在这里插入图片描述
成功,HMAC对称加密算法生成令牌和解析令牌已经结束!

2.非对称加密(RSA)

非对称加密采用公钥和私钥进行加密和解密。对于加密操作,公钥负责加密,私钥负责解密。对于签名操作,私钥负责签名,公钥负责验签。

对于加密和签名的理解
例如两个端A和B进行通信,A向B发送了一条经过签名和加密的信息;涉及了四个密钥:A公钥、A私钥、B公钥、B私钥。

  • 签名:是为了让B确认这个信息就是A发出的,不是别人
  • 加密:对传输的内容进行保护,即使信息被恶意截取,也无法进行解析。只有B可以查看。

A向B发送信息进行签名和加密的具体流程。

A使用自己的私钥对信息进行签名
A使用B的公钥对信息进行加密

B接收到A发送的信息进行如下处理:

B用自己的私钥对信息进行解密
B使用A的公钥对进行验签操作

所以整个流程保证了端到端的唯一性!

  • 使用JDK提供的keytool工具或者使用git生成jwt.jks,放到resources目录下

在这里插入图片描述

  • 在JwtTokenService接口中定义方法,并在JwtTokenServiceImpl类中实现其业务
    /**
     * 从类路径下加载jwt.jk
     */
    RSAKey loadJKSByClassPath();

    /**
     * 使用RSA非对称算法生成token
     */
    String generateTokenByRSA(String payloadStr, RSAKey rsaKey) throws JOSEException;

    /**
     * 根据RSA非对称算法验证token
     */
    PayloadDto verifyTokenByRSA(String token, RSAKey rsaKey) throws ParseException, JOSEException, JwtInvalidException;
	@Override
    public RSAKey loadJKSByClassPath() {
        //从类路径下加载证书
        KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("jwt.jks"), "123456".toCharArray());
        KeyPair keyPair = keyStoreKeyFactory.getKeyPair("jwt", "123456".toCharArray());
        //获取公钥
        RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
        //获取私钥
        RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
        return new RSAKey.Builder(publicKey).privateKey(privateKey).build();
    }

    @Override
    public String generateTokenByRSA(String payloadStr, RSAKey rsaKey) throws JOSEException {
        //构建JWS头
        JWSHeader jwsHeader = new JWSHeader.Builder(JWSAlgorithm.RS256).type(JOSEObjectType.JWT).build();
        //构建载荷
        Payload payload = new Payload(payloadStr);
        //将JWS-header和payload封装成JWS对象中
        JWSObject jwsObject = new JWSObject(jwsHeader, payload);
        //创建签名器
        JWSSigner signer = new RSASSASigner(rsaKey, true);
        jwsObject.sign(signer);
        return jwsObject.serialize();
    }

    @Override
    public PayloadDto verifyTokenByRSA(String token, RSAKey rsaKey) throws ParseException, JOSEException, JwtInvalidException {
        JWSObject jwsObject = JWSObject.parse(token);
        RSAKey verifyKey = rsaKey.toPublicJWK();
        JWSVerifier verifier = new RSASSAVerifier(verifyKey);
        if (!jwsObject.verify(verifier)) {
            throw new JwtInvalidException(401, "签名不合法!");
        }
        String payload = jwsObject.getPayload().toString();
        String substring = payload.substring(11, payload.length() - 1);
        String[] strings = substring.split(",");
        PayloadDto payloadDto = PayloadDto.builder()
                .sub(strings[0])
                .iat(100L)
                .exp(100L)
                .jti(strings[3])
                .username(strings[4])
                .authorities(CollUtil.toList(strings[5]))
                .build();
        return payloadDto;
    }
  • 在JwtTokenController中加入以下接口
    @ApiOperation("获取公钥")
    @GetMapping("/rsa/publicKey")
    public CommonResult getRsaPublicKey() {
        RSAKey key = jwtTokenService.loadJKSByClassPath();
        return CommonResult.success(new JWKSet(key).toJSONObject());
    }

    @ApiOperation("使用RSA非对称加密算法生成token")
    @GetMapping("/rsa/generate")
    public CommonResult generateTokenByRSA() throws JOSEException {
        PayloadDto payloadDto = jwtTokenService.getDefaultPayloadDto();
        RSAKey rsaKey = jwtTokenService.loadJKSByClassPath();
        String token = jwtTokenService.generateTokenByRSA(payloadDto.toString(), rsaKey);
        return CommonResult.success(token);
    }

    @ApiOperation("RSA验签")
    @GetMapping("/rsa/verify")
    public CommonResult verifyTokenByRSA(String token) throws ParseException, JOSEException, JwtInvalidException {
        PayloadDto payloadDto = jwtTokenService.verifyTokenByRSA(token, jwtTokenService.loadJKSByClassPath());
        return CommonResult.success(payloadDto);
    }
  • 测试
    在这里插入图片描述
    在这里插入图片描述
Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐