最近项目上由于集成需求,需要实现单点登录,经过考虑后选择了JWT,RS256公私玥加密方式实现,搜索后发现基于RS256的实现不太多,大多基于HS256对称加密,加密解密用同一SecretKey,泄漏后安全方面彻底崩坏,有些提到RS256的都是一些支离破碎的代码,没有什么参考价值。经过google,加上自己的整理,关键demo代码整理如下:
import junit.framework.TestCase;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.io.IOUtils;
import org.jose4j.jws.AlgorithmIdentifiers;
import org.jose4j.jws.JsonWebSignature;
import org.jose4j.jwt.JwtClaims;
import org.jose4j.jwt.consumer.JwtConsumer;
import org.jose4j.jwt.consumer.JwtConsumerBuilder;
import org.jose4j.lang.JoseException;
import sun.security.util.DerInputStream;
import sun.security.util.DerValue;
import java.io.FileInputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.security.*;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.RSAPrivateCrtKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.text.SimpleDateFormat;
import java.util.UUID;
public class LocalTest extends TestCase{
public void testCreateToken() throws IOException {
System.out.println(createToken());
}
public void testVerifyToken() throws Exception {
String token = createToken();
System.out.println(token);
JwtClaims jwtClaims = verifyToken(token);
System.out.println(jwtClaims.getClaimValue("name"));
System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(jwtClaims.getIssuedAt().getValueInMillis()));
System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(jwtClaims.getExpirationTime().getValueInMillis()));
}
/**
* 生成jwt,RS256(采用SHA-256 的 RSA 签名)加密
* @return
* @throws IOException
*/
private String createToken() throws IOException {
PrivateKey privateKey = getPrivateKey(getPrivateKeyString());
final JwtClaims claims = new JwtClaims();
claims.setClaim("name", "jack");
claims.setSubject("a@a.com");
claims.setAudience("test");//用于验证签名是否合法,验证方必须包含这些内容才验证通过
claims.setExpirationTimeMinutesInTheFuture(60*24*30);
claims.setIssuedAtToNow();
// Generate the payload
final JsonWebSignature jws = new JsonWebSignature();
jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.RSA_USING_SHA256);
jws.setPayload(claims.toJson());
jws.setKeyIdHeaderValue(UUID.randomUUID().toString());
// Sign using the private key
jws.setKey(privateKey);
try {
return jws.getCompactSerialization();
} catch (JoseException e) {
return null;
}
}
/**
* 验证jwt
* @param token
* @return
* @throws Exception
*/
private JwtClaims verifyToken(String token) throws Exception {
try {
PublicKey publicKey = getPublicKey(getPEMPublicKeyString());
JwtConsumer jwtConsumer = new JwtConsumerBuilder()
.setRequireExpirationTime()
.setVerificationKey(publicKey)
.setExpectedAudience("test")//用于验证签名是否合法,可以设置多个,且可设置必须存在项,如果jwt中不包含这些内容则不通过
.build();
return jwtConsumer.processToClaims(token);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private String getPrivateKeyString() throws IOException {
// 生成方法:安装openssl,执行 openssl genrsa -out private.pem 2048
return IOUtils.toString(new FileInputStream("/home/leen/.ssh/private.pem"));
}
private String getPEMPublicKeyString() throws IOException {
// 导出公钥方法:生成私钥(private.pem)后,执行 openssl rsa -in private.pem -outform PEM -pubout -out public.pem
return IOUtils.toString(new FileInputStream("/home/leen/.ssh/public.pem"));
}
/**
* 获取PublicKey对象
* @param publicKeyBase64
* @return
* @throws NoSuchAlgorithmException
* @throws InvalidKeySpecException
*/
private PublicKey getPublicKey(String publicKeyBase64) throws NoSuchAlgorithmException, InvalidKeySpecException {
String pem = publicKeyBase64
.replaceAll("\\-*BEGIN.*KEY\\-*", "")
.replaceAll("\\-*END.*KEY\\-*", "");
java.security.Security.addProvider(
new org.bouncycastle.jce.provider.BouncyCastleProvider()
);
X509EncodedKeySpec pubKeySpec = new X509EncodedKeySpec(Base64.decodeBase64(pem));
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PublicKey publicKey = keyFactory.generatePublic(pubKeySpec);
System.out.println(publicKey);
return publicKey;
}
/**
* 获取PrivateKey对象
* @param privateKeyBase64
* @return
*/
private PrivateKey getPrivateKey(String privateKeyBase64) {
String privKeyPEM = privateKeyBase64
.replaceAll("\\-*BEGIN.*KEY\\-*", "")
.replaceAll("\\-*END.*KEY\\-*", "");
// Base64 decode the data
byte[] encoded = Base64.decodeBase64(privKeyPEM);
try {
DerInputStream derReader = new DerInputStream(encoded);
DerValue[] seq = derReader.getSequence(0);
if (seq.length < 9) {
throw new GeneralSecurityException("Could not read private key");
}
// skip version seq[0];
BigInteger modulus = seq[1].getBigInteger();
BigInteger publicExp = seq[2].getBigInteger();
BigInteger privateExp = seq[3].getBigInteger();
BigInteger primeP = seq[4].getBigInteger();
BigInteger primeQ = seq[5].getBigInteger();
BigInteger expP = seq[6].getBigInteger();
BigInteger expQ = seq[7].getBigInteger();
BigInteger crtCoeff = seq[8].getBigInteger();
RSAPrivateCrtKeySpec keySpec = new RSAPrivateCrtKeySpec(modulus, publicExp, privateExp,
primeP, primeQ, expP, expQ, crtCoeff);
KeyFactory factory = KeyFactory.getInstance("RSA");
return factory.generatePrivate(keySpec);
} catch (IOException | GeneralSecurityException e) {
e.printStackTrace();
}
return null;
}
}
pom依赖:
<dependency>
<groupId>org.bitbucket.b_c</groupId>
<artifactId>jose4j</artifactId>
<version>0.6.3</version>
</dependency>