以前一直使用的是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令牌的接口進行測試;
- 調用使用HMAC算法解析JWT令牌的接口進行測試。
非對稱加密(RSA)
非對稱加密指的是使用公鑰和私鑰來進行加密解密操作。對於加密操作,公鑰負責加密,私鑰負責解密,對於簽名操作,私鑰負責簽名,公鑰負責驗證。非對稱加密在JWT中的使用顯然屬於簽名操作。
- 如果我們需要使用固定的公鑰和私鑰來進行簽名和驗證的話,我們需要生成一個證書文件,這裏將使用Java自帶的keytool工具來生成jks證書文件,該工具在JDK的bin目錄下;
- 打開CMD命令界面,使用如下命令生成證書文件,設置別名爲jwt,文件名爲jwt.jks;
keytool -genkey -alias jwt -keyalg RSA -keystore jwt.jks
- 輸入密碼爲123456,然後輸入各種信息之後就可以生成證書jwt.jks文件了;
- 將證書文件jwt.jks複製到項目的resource目錄下,然後需要從證書文件中讀取RSAKey,這裏我們需要在pom.xml中添加一個Spring Security的RSA依賴;
<!--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();
}
}
- 調用該接口,查看公鑰信息,公鑰是可以公開訪問的;
- 在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令牌的接口進行測試;
- 調用使用RSA算法解析JWT令牌的接口進行測試。