序
温故而知新,可以为师矣。
JWT的实现框架
从上一篇 JWT就是这么简单 知道JWT是一种标准,而不是具体的实现,那么在JAVA中实现了JWT的框架多不胜数(公司内部自己写的JWT框架)。
官方推荐是使用官方的Auth0,但是Auth0中功能远远满足不了各种需求。所以各路大神都献出自己写的JWT框架,目前得到官方认可的框架一共是6个 auth0、jose4j、nimbus-jose、jjwt、fusionauth、vertx
上手体验
上手体验前,朕水先写生成一个密钥、读取密钥的方法。 生成密钥代码:
public class CreatRsaKey {
//密钥长度 于原文长度对应 以及越长速度越慢 必须大于 512
private final static int KEY_SIZE = 2048;
public static void main(String[] args) throws Exception {
//一对密钥算法生成
KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA");
SecureRandom secureRandom = new SecureRandom();
//设置随机种子
secureRandom.setSeed(32);
keyPairGen.initialize(KEY_SIZE,secureRandom);
KeyPair keyPair = keyPairGen.generateKeyPair();
// 得到私钥
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
// 得到公钥
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
// 得到密钥字符串
byte[] publicKeyBytes = Base64.getEncoder().encode(publicKey.getEncoded());
byte[] privateKeyBytes = Base64.getEncoder().encode(privateKey.getEncoded());
//保存到文件中
Resource publicKeyResource = new FileSystemResource("src/main/resources/rsa/publickey.rsa");
FileOutputStream publicKeyOutputStream = new FileOutputStream(publicKeyResource.getFile());
publicKeyOutputStream.write(publicKeyBytes);
publicKeyOutputStream.close();
//保存到文件中
Resource privateKeyResource = new FileUrlResource("src/main/resources/rsa/privatekey.rsa");
FileOutputStream privateKeyOutputStream = new FileOutputStream(privateKeyResource.getFile());
privateKeyOutputStream.write(privateKeyBytes);
privateKeyOutputStream.close();
}
}
读取密钥代码:
public class PairKey {
//公钥路径 src/main/resources/ec/publickey.ec
private static final String PUBLIC_URI = "src/main/resources/rsa/publickey.rsa";
//私钥路径 src/main/resources/ec/privatekey.ec
private static final String PRIVATE_URI = "src/main/resources/rsa/privatekey.rsa";
//加密类型 可以换为EC
private static final String ALGORITHM_TYPE = "RSA";
//存放RSA密钥对
public static KeyPair keyPair; //注意这里嗷,这里是非对称加密的容器
static {
try{
//初始化数据
FileInputStream publicInputStream =
new FileInputStream(PUBLIC_URI);
//读取公钥
X509EncodedKeySpec bobPubKeySpec = new X509EncodedKeySpec(
new BASE64Decoder().decodeBuffer(publicInputStream));
publicInputStream.close();
//读取私钥
FileInputStream privateInputStream =
new FileInputStream(PRIVATE_URI);
PKCS8EncodedKeySpec bobPriKeySpec = new PKCS8EncodedKeySpec(
new BASE64Decoder().decodeBuffer(privateInputStream));
privateInputStream.close();
// 密钥工厂
KeyFactory keyFactory = KeyFactory.getInstance(ALGORITHM_TYPE);
// 取公钥对象
PublicKey publicKey = keyFactory.generatePublic(bobPubKeySpec);
// 取私钥对象
PrivateKey privateKey = keyFactory.generatePrivate(bobPriKeySpec);
// 放入容器
keyPair = new KeyPair(publicKey,privateKey);
} catch (IOException | NoSuchAlgorithmException | InvalidKeySpecException e) {
e.printStackTrace();
}
}
}
auth0
auth0是最为简单的根据密钥(或HMAC)生成Algorithm对象,直接使用Algorithm对象加密解密。 个人认为单例生成Algorithm对象再适合不过了。
public static void main(String[] args) throws Exception {
//RSA
Algorithm algorithm = Algorithm
.RSA256((RSAPublicKey) PairKey.keyPair.getPublic(), (RSAPrivateKey) PairKey.keyPair.getPrivate());
//Algorithm algorithm = Algorithm.HMAC256("zhenshuizhenshui");
//EC
//Algorithm algorithm = Algorithm
// .ECDSA256((ECPublicKey) keyPair.getPublic(), (ECPrivateKey) keyPair.getPrivate());
//生产token
String token = JWT.create()
//签发人
.withIssuer("zhenshui")
//自定义信息
.withClaim("username","zhenshuizhenshui")
.withClaim("isAuth","0")
.sign(algorithm);
System.out.println(token);
System.out.println("--------校验token---------------");
JWTVerifier verifier = JWT.require(algorithm)
.withIssuer("zhenshui")
.build(); //Reusable verifier instance
DecodedJWT jwt = verifier.verify(token);
Map<String, Claim> claims = jwt.getClaims();
//获取签发的字段
Claim username = claims.get("username");
System.out.println(username.asString());
}
jjwt
jjwt可以说是把’极简‘一词给表达出来,token的生成全部一手包办。没有多余的操作余地。
public static void main(String[] args) {
//jjwt支持RSA-PSS算法。但是朕水不会生成PSS算法的的密钥对
String token = Jwts.builder()
.claim("username","zhenshui")
.claim("isAuth","0")
.signWith(SignatureAlgorithm.RS256, PairKey.keyPair.getPrivate())
//.signWith(SignatureAlgorithm.HS256,"secret")
.compact();
//私钥加密,公钥解密
Jwt parse = Jwts.parser()
//设置公钥
.setSigningKey(PairKey.keyPair.getPublic())
//.signWith(SignatureAlgorithm.HS256,"secret")
.parse(token);
//得到Json数据,所有数据
Object body = parse.getBody();
System.out.println(body.toString());
}
funsionauth
可以说,这个框架是令朕水最失望的框架。繁杂的操作,只能接收密钥字符串,密钥需要开始和结束标识,速度也比不上前面两个、设置头部信息需要使用函数式编程等等问题。。
public static void main(String[] args) {
//这是一个SMAC256的例子
String secret ="secret";
//设置密钥和算法SHA256
Signer signer = HMACSigner.newSHA256Signer(secret);
JWT jwt = new JWT()
//设置载荷基本信息
.setIssuer("zhenshui")
.addClaim("username","朕水真水");
//对JWT进行编码
String token = JWT.getEncoder().encode(jwt, signer,(header)->{
header.set("key","header"); //设置头部信息
});
System.out.println(token);
//校验token使用的校验密钥和算法
Verifier verifier = HMACVerifier.newVerifier(secret);
//进行解码(base64)
JWT decodedJwt = JWT.getDecoder().decode(token, verifier);
//得到所有载荷参数
Map<String, Object> allClaims = decodedJwt.getAllClaims();
System.out.println(allClaims.toString());
//得到自定义参数
Map<String, Object> otherClaims = decodedJwt.getOtherClaims();
System.out.println(otherClaims);
//读取某个参数名的值
String key = decodedJwt.getString("username");
System.out.println(key);
}
public static void main(String[] args) throws IOException {
JWT jwt = new JWT()
.setIssuer("zhenshui")
.addClaim("username","朕水真水");
String str = Base64.encodeBase64String(PairKey.keyPair.getPrivate().getEncoded());
//没测试RSA成功。因为PEMDecoder的decode方法强制要求RSA的公钥私钥必须有 开始的标识和结束的标识
// -----BEGIN RSA PRIVATE KEY-----
//-----END RSA PRIVATE KEY-----
RSASigner rsaSigner = RSASigner.newSHA256Signer(str);
//对JWT进行编码
String encodedJwt = JWT.getEncoder().encode(jwt, rsaSigner,(header)->{
header.set("key","header");
});
//校验token使用的校验密钥和算法
Verifier verifier = RSAVerifier.newVerifier((RSAPublicKey) PairKey.keyPair.getPublic());
//进行解码(base64)
JWT decodedJwt = JWT.getDecoder().decode(encodedJwt, verifier);
//得到所有载荷参数
Map<String, Object> allClaims = decodedJwt.getAllClaims();
System.out.println(allClaims.toString());
}
上面两个是funsionauth的HMAC256(对称加密算法)和RSA(非对称加密算法)两种算法的例子。如果说使用HMAC256只是复杂一点的话。那使用非对称加密之类的算法就是给朕水当头一棒。 先说说funsionauth的非对称加密的API,它里面设置私钥只能设置字符串类型。好吧,朕水忍住了,转字符串进去。拿字符串去干嘛?一看代码不得了,拿朕水给的字符串去转化为RSAPrivateKey...好吧,这是作者的设计,朕水再忍。运行一下代码,报错了。仔细看了看源码,必须要有BEGIN PRIVATE KEY.... 可能是朕水不能理解作者的思想,朕水告辞了。后续有机会再研究吧,这里朕水只做简单上手的测试。整体来说,这个框架被约束的比较厉害。
贴一下funsionauth的源码
jose4j
可以说jose4j很是最规矩的一个框架。在载荷放什么数据全看用户设置的Map中,符合规范的字段(sub、iss、jti、aud等)就调用对应的校验器进行校验,提供一个JsonUtil将Map转化为JSON格式的字符串。
public static void main(String[] args) throws JoseException {
//自带生成RSA算法密钥工具
//RsaJsonWebKey jwk = RsaJwkGenerator.generateJwk(1025);
//加密解密使用的都是JsonWebSignature对象
Map<String, String> payload = new HashMap<>();
payload.put("username","zhenshui");
payload.put("sub","开party!!");
//设置载荷
JsonWebSignature jws = new JsonWebSignature();
jws.setPayload(JsonUtil.toJson(payload));
//设置密钥和加密方式
jws.setKey(PairKey.keyPair.getPrivate());
jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.RSA_USING_SHA256);
//jws.setKey(new HmacKey(secret.getBytes()));
//jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.HMAC_SHA256);
String token = jws.getCompactSerialization();
System.out.println(token);
//JsonWebSignature校验
JsonWebSignature jwsvif = new JsonWebSignature();
//设置token
jwsvif.setCompactSerialization(token);
//设置公钥
jwsvif.setKey(PairKey.keyPair.getPublic());
//校验
boolean v = jwsvif.verifySignature();
//得到载荷
if (v) {
System.out.println(jwsvif.getPayload());
}
}
jose4j中jwe的实现简单使用。生成与校验基本与JWS使用方式一致。改为JsonWebEncryption对象生成
public static void main(String[] args) throws JoseException {
//使用的都是JsonWebEncryption
//加密
JsonWebEncryption jwe = new JsonWebEncryption();
Map<String, String> payload = new HashMap<>();
payload.put("username","zhenshui");
//设置载荷
jwe.setPayload(JsonUtil.toJson(payload));
//加密算法
jwe.setAlgorithmHeaderValue(KeyManagementAlgorithmIdentifiers.RSA_OAEP_256);
//消息摘要算法
jwe.setEncryptionMethodHeaderParameter(ContentEncryptionAlgorithmIdentifiers.AES_128_CBC_HMAC_SHA_256);
//Key key = new AesKey(secret.getBytes()); 使用对称加密算法时的Key
jwe.setKey(PairKey.keyPair.getPublic());
//序列化token
String token = jwe.getCompactSerialization();
System.out.println( token);
//重新设置一个 对象
jwe = new JsonWebEncryption();
//设置两层算法
jwe.setAlgorithmConstraints(new AlgorithmConstraints(AlgorithmConstraints.ConstraintType.PERMIT,
KeyManagementAlgorithmIdentifiers.RSA_OAEP_256));
jwe.setContentEncryptionAlgorithmConstraints(new AlgorithmConstraints(AlgorithmConstraints.ConstraintType.PERMIT,
ContentEncryptionAlgorithmIdentifiers.AES_128_CBC_HMAC_SHA_256));
//设置密钥
jwe.setKey(PairKey.keyPair.getPrivate());
//设置token
jwe.setCompactSerialization(token);
System.out.println( jwe.getPayload());
//得到的token为5段//在请求头设置两个算法
}
nimbusds-jose
这个框架对比jose4j来说并没有那么流畅,给朕水的感觉就是使用起来比jose4j复杂,但是能明显的感受JWT的设计理念。
public static void main(String[] args) throws MalformedURLException, ParseException, JOSEException, BadJOSEException {
//JWSSigner jwsSigner = new MACSigner(secret); //SMAC256算法
JWSSigner jwsSigner = new RSASSASigner(PairKey.keyPair.getPrivate());
JWSHeader jwsHeader = new JWSHeader
.Builder(JWSAlgorithm.RS256)
.type(JOSEObjectType.JWT).build();
//参数
JWTClaimsSet jwtClaimsSet =new JWTClaimsSet.Builder()
.issuer("admin")
.subject("sub")
.claim("username","zhenshui")
.build();
Payload payload = new Payload(jwtClaimsSet.toJSONObject());
JWSObject jwsObject = new JWSObject(jwsHeader, payload);
jwsObject.sign(jwsSigner);
//base64
String token = jwsObject.serialize();
System.out.println(token);
//解密
JWSVerifier verifier = new RSASSAVerifier((RSAPublicKey) PairKey.keyPair.getPublic());
//不校验获取头部和载荷
JWSObject parseJWS = JWSObject.parse(token);
//校验
boolean verify = parseJWS.verify(verifier);
//获取载荷
if (verify) {
String decryptPayload = parseJWS.getPayload().toString();
System.out.println(decryptPayload);
}
}
在nimbus-jose中的JWE实现基本与JWS无差。改为JWEObject对象生成
public static void main(String[] args) throws ParseException, JOSEException {
//载荷字段
JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
.issuer("admin")
.subject("sub")
.claim("username","zhenshui")
.build();
//双重加密
JWEHeader header = new JWEHeader(JWEAlgorithm.RSA_OAEP_256, EncryptionMethod.A128CBC_HS256);
//载荷可以设置字符串
//Payload payload = new Payload("Hello world!");
Payload payload = new Payload(claimsSet.toJSONObject());
JWEObject jwt = new JWEObject(header, payload);
//加密
RSAEncrypter encrypter = new RSAEncrypter((RSAPublicKey) PairKey.keyPair.getPublic());
//加密
jwt.encrypt(encrypter);
String token = jwt.serialize();
System.out.println(token);
//设置RSA解密对象私钥
RSADecrypter decrypter = new RSADecrypter(PairKey.keyPair.getPrivate());
//解密//公钥加密私钥解密
EncryptedJWT parseJWT = EncryptedJWT.parse(token);
//解密
parseJWT.decrypt(decrypter);
//获取载荷的值
JWTClaimsSet jwtClaimsSet = parseJWT.getJWTClaimsSet();
System.out.println(jwtClaimsSet.toString());
//得到的token为5段//在请求头设置两个算法
}
JWT框架简评
朕水对5个框架进行简单的上手的使用感受给各位参考,结果如下:
上手难度:
auth0 > jjwt > jose4j > fusionauth > nimbus-jose > vertx
vertx排在最后只是朕水个人的使用感受(实际上朕水试用了一天都没有生成Token成功)。
功能完整性:
只有jose4j和nimbus-jose有JWE的相关API,其他4个没有。 只有jose4j和nimbus-jose才有密钥(非对称加密)的生成器, auth0、jjwt、fusionauth要自己生成私钥和公钥。 只有jose4j和nimbus-jose不强制载荷设置为键值对,可以设置为字符串。auth0、jjwt和funsionauth强制使用键值对。 只有auth0和jjwt没有密钥管理服务的使用API 朕水怀疑jose4j和nimbus-jose是一伙的!!!功能都非常的相似) 从上手难度、功能完整性来看,推荐使用:nimbus-jose > jose4j。 推荐原因:nimbus-jose和jose4j功能是最全的。他们两个的功能相差不大,区别点:jose4j的载荷参数要用户设置,nimbus-jose可以使用JWTClaimsSet构造出来,nimbus-jose生成和解析Token速度快,不过nimbus-jose不校验sub
、nbf
、iat
、jti
。 如果只使用JWS功能推荐使用:auth0 > jjwt。 推荐原因:官方推荐、上手简单、文档全,轻量,生成校验token快。jjwt与auth0类似,但是jjwt则尽量不依赖外部类库,使用自己内部的写的方法。 funsionauth要哭了,毫无地位(funsionauth:喵?喵?喵?) 目前,auth0和jjwt都没有密钥管理服务的API。所以当朕水只想使用JWS的token生成和远程检索密钥的时候,朕水就只有3个选择funsionauth、nimbus-jose、jose4j。在这三个中,只有funsionauth功能没有冗余。 上面的上手感受的简评并不严谨,实际使用以项目需求为准(用不到JWE功能,JWT端点的API等功能就没必要使用nimbus-jose、jose4j),如果有误可指正。
PS:
浅谈一下关于JSON。JWT是对JSON数据根据某种加密得到的字符串。为什么朕水要把 "可以设置为字符串"归为功能完整性。主要是因为字符串也是JSON数据格式!! 朕水发觉很多同学都有一个误区,就是只有下面这种格式才是JSON格式:
{"key":"value"}
实际上JSON可以有很多种,只是常用的格式是上面的那种形式。其他的形式例如:
"" //字符串类型
["1","2","3"] //数组类型
74.74 //数字、浮点类型
后记
微信关注"朕水真水",一起学习更多后端技术。看到发送给我的消息我尽量查看。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。