什么是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>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.3.0.RELEASE<version>
</dependency>
<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算法生成和验证令牌方法。
public interface JwtTokenService {
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;
@Service
public class JwtTokenServiceImpl implements JwtTokenService {
@Override
public String generateTokenByHMAC(String payloadStr, String secret) {
try {
JWSHeader jwsHeader = new JWSHeader.Builder(JWSAlgorithm.HS256)
.type(JOSEObjectType.JWT).build();
Payload payload = new Payload(payloadStr);
JWSObject jwsObject = new JWSObject(jwsHeader, payload);
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);
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进行了加密充当密钥。
@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类中实现其业务
RSAKey loadJKSByClassPath();
String generateTokenByRSA(String payloadStr, RSAKey rsaKey) throws JOSEException;
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 {
JWSHeader jwsHeader = new JWSHeader.Builder(JWSAlgorithm.RS256).type(JOSEObjectType.JWT).build();
Payload payload = new Payload(payloadStr);
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);
}
- 测试
|