公众号:飞翔的代码
HTTP通信本身是无状态的,每次请求对于服务端来说都不知道是哪个用户的请求,因此才发明了cookie和session,用户登录后服务端保存session,并将session id作为cookie的一部分返回给用户,用户下次请求携带cookie,服务端根据cookie中的session id得到用户登录信息,这样服务端才能知道是哪个用户的请求
这是早期的实现方案,现在大部分都是分布式的系统,系统之间同步session是比较麻烦的,所以又有人发明了token,用户登录后服务端生成一个token字符串,将token返回给用户,用户每次请求携带token,在token中可以写入用户的基本信息,如用户名,服务端解析token得到用户名,这个过程可以使用AES或RSA加密技术来生成token
由于生成token后是直接返回给用户,服务端无法再对token进行限制,如果用户将token泄漏,那么别人拿着这个token就可以直接访问该用户的数据了,所以生成token时应该设置token的有效期,过期后不可使用。另一种方案是将token写入redis,这样可以在redis中删除某个token来起到让用户重新登录的功能。最终还是采用OAuth2.0方案更靠谱。
本文介绍通用的token方案JWT和JWK以及RSA256签名
JWT
全名Json Web Token
就是上面所说的token,由以下三部分组成:
- header 声明JWT的签名算法
- payload token中携带的明文数据
- signture 签名,一个JWT是否有效就看签名是否合法,防止伪造JWT
这三个部分各自base64后用点号拼接起来,第1和2部分base64解码后是明文的,因此千万不要在payload中写入重要的数据,通常用户ID也不要写入payload中,防止别人根据ID猜测用户量
JWK
全名Json Web Key
这是JWT的密钥信息,如果采用RSA加密方式签名,需要提供一个Public Key
给校验方,校验方根据Public Key
来验证JWT中的签名是否有效,如果直接将RSA的Public Key
发给校验方也可以
HMAC256签名方式
这种签名方式不推荐,没有RSA256方式安全,以下列举其使用方法
pom.xml
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.10.2</version>
</dependency>
复制代码
工具类
public class JwtUtils {
private static final String PREFIX = "密钥的前缀";
private static final String SUFFIX = "密钥的后缀";
/**
* 产生token,永不过期
*
* @param userId 用户ID
* @param ip IP
* @param secret 密钥
* @return
*/
public static String create(String userId, String ip, String secret) {
return create(userId, ip, secret, null);
}
public static String create(String userId, String ip, String secret, String other) {
return create(userId, ip, secret, other, null);
}
/**
* 产生token
*
* @param userId 用户ID
* @param ip IP
* @param secret 密钥
* @param expire 过期时间,单位:毫秒
* @return
*/
public static String create(String userId, String ip, String secret, String other, Long expire) {
JWTCreator.Builder builder = JWT.create().withAudience(userId, ip, String.valueOf(System.currentTimeMillis()), other);
if (expire != null && expire > 0) {
builder.withExpiresAt(new Date(System.currentTimeMillis() + expire));
}
return builder.sign(Algorithm.HMAC256(PREFIX + secret + SUFFIX));
}
/**
* 获取token中的数据
*
* @param token token
* @return
* @throws TokenException
*/
public static List<String> getAudienceList(String token) throws TokenException {
try {
return JWT.decode(token).getAudience();
} catch (JWTDecodeException e) {
throw new TokenException(e.getMessage());
}
}
/**
* 验证token是否有效
*
* @param token token
* @param secret 密钥
* @return
* @throws TokenException
*/
public static boolean verify(String token, String secret) throws TokenException {
try {
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(PREFIX + secret + SUFFIX)).build();
jwtVerifier.verify(token);
return true;
} catch (JWTVerificationException e) {
throw new TokenException(e.getMessage());
}
}
}
复制代码
用法
long expire = expireTime == null ? 7 * 24 * 3600 * 1000L : expireTime;
String token = JwtUtils.create(loginUser.getUsername(), HttpUtils.getIp(request), loginUser.getPassword(), other, expire);
// 验证token
if (JwtUtils.verify(token, loginUser.getPassword())) {
// 得到payload中的数据
List<String> data = JwtUtils.getAudienceList(token);
}
复制代码
将用户名、ip、当前时间和other写入payload,使用用户密码(加密后的密码)进行签名,这样的好处是每个用户的签名密钥都不相同
RSA256签名方式
pom.xml
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>9.0.1</version>
</dependency>
复制代码
Rsa工具类
主要是生成公私密钥和密钥转换(如字符串转密钥),一般长度至少要1024,推荐2048,对于安全要求更高的系统,可使用4096
@Slf4j
public class RsaUtils {
private static final int KEY_SIZE = 2048;
/**
* 生成密钥对
*
* @return
*/
public static KeyPair create() {
try {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(KEY_SIZE);
return keyPairGenerator.generateKeyPair();
} catch (NoSuchAlgorithmException e) {
return null;
}
}
/**
* 从字符串得到公钥
*
* @param key
* @return
*/
public static PublicKey getPublicKey(String key) throws InvalidKeySpecException {
try {
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(initKey(key));
KeyFactory factory = KeyFactory.getInstance("RSA");
return factory.generatePublic(keySpec);
} catch (InvalidKeySpecException e) {
log.debug("公钥错误", e);
throw e;
} catch (NoSuchAlgorithmException e) {
return null;
}
}
/**
* 从字符串得到私钥
*
* @param key
* @return
* @throws InvalidKeySpecException
*/
public static PrivateKey getPrivateKey(String key) throws InvalidKeySpecException {
try {
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(initKey(key));
KeyFactory factory = KeyFactory.getInstance("RSA");
return factory.generatePrivate(keySpec);
} catch (InvalidKeySpecException e) {
log.debug("私钥错误", e);
throw e;
} catch (NoSuchAlgorithmException e) {
return null;
}
}
/**
* 公钥转字符串
*
* @param key
* @return
*/
public static String toPublicKeyString(PublicKey key) {
return Base64.encodeBase64String(key.getEncoded());
}
/**
* 私钥转字符串
*
* @param key
* @return
*/
public static String toPrivateKeyString(PrivateKey key) {
return Base64.encodeBase64String(key.getEncoded());
}
/**
* 使用私钥加密
*
* @param string
* @param key
* @return
* @throws Exception
*/
public static String encryptByPrivateKey(String string, String key) throws Exception {
PrivateKey privateKey = getPrivateKey(key);
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE, privateKey);
// 由于长度限制,需要分开加密
byte[] data = string.getBytes();
int blockLength = KEY_SIZE / 8 - 11;
int offset = 0;
int i = 0;
int length = data.length;
ByteArrayOutputStream out = new ByteArrayOutputStream();
byte[] cache;
while (length - offset > 0) {
if (length - offset > blockLength) {
cache = cipher.doFinal(data, offset, blockLength);
} else {
cache = cipher.doFinal(data, offset, length - offset);
}
out.write(cache, 0, cache.length);
i++;
offset = i * blockLength;
}
byte[] bytes = out.toByteArray();
out.close();
return Base64.encodeBase64String(bytes);
}
/**
* 使用公钥加密
*
* @param string
* @param key
* @return
* @throws Exception
*/
public static String encryptByPublicKey(String string, String key) throws Exception {
PublicKey publicKey = getPublicKey(key);
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
// 由于长度限制,需要分开加密
byte[] data = string.getBytes();
int blockLength = KEY_SIZE / 8 - 11;
int offset = 0;
int i = 0;
int length = data.length;
ByteArrayOutputStream out = new ByteArrayOutputStream();
byte[] cache;
while (length - offset > 0) {
if (length - offset > blockLength) {
cache = cipher.doFinal(data, offset, blockLength);
} else {
cache = cipher.doFinal(data, offset, length - offset);
}
out.write(cache, 0, cache.length);
i++;
offset = i * blockLength;
}
byte[] bytes = out.toByteArray();
out.close();
return Base64.encodeBase64String(bytes);
}
/**
* 使用私钥解密
*
* @param string
* @param key
* @return
* @throws Exception
*/
public static String decryptByPrivateKey(String string, String key) throws Exception {
PrivateKey privateKey = getPrivateKey(key);
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, privateKey);
// 由于长度限制,需要分开加密
byte[] data = Base64.decodeBase64(string);
int length = data.length;
ByteArrayOutputStream out = new ByteArrayOutputStream();
int offset = 0;
byte[] cache;
int i = 0;
int blockLength = KEY_SIZE / 8;
while (length - offset > 0) {
if (length - offset > blockLength) {
cache = cipher.doFinal(data, offset, blockLength);
} else {
cache = cipher.doFinal(data, offset, length - offset);
}
out.write(cache, 0, cache.length);
i++;
offset = i * blockLength;
}
byte[] bytes = out.toByteArray();
out.close();
return new String(bytes, "utf-8");
}
/**
* 使用公钥解密
*
* @param string
* @param key
* @return
* @throws Exception
*/
public static String decryptByPublicKey(String string, String key) throws Exception {
PublicKey publicKey = getPublicKey(key);
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, publicKey);
// 由于长度限制,需要分开加密
byte[] data = Base64.decodeBase64(string);
int length = data.length;
ByteArrayOutputStream out = new ByteArrayOutputStream();
int offset = 0;
byte[] cache;
int i = 0;
int blockLength = KEY_SIZE / 8;
while (length - offset > 0) {
if (length - offset > blockLength) {
cache = cipher.doFinal(data, offset, blockLength);
} else {
cache = cipher.doFinal(data, offset, length - offset);
}
out.write(cache, 0, cache.length);
i++;
offset = i * blockLength;
}
byte[] bytes = out.toByteArray();
out.close();
return new String(bytes, "utf-8");
}
/**
* 去除密钥中的开头和结尾以及换行符,并转成byte[]
*
* @param key
* @return
*/
private static byte[] initKey(String key) {
if (key.contains("-----BEGIN PRIVATE KEY-----")) {
key = key.substring(key.indexOf("-----BEGIN PRIVATE KEY-----") + 27);
}
if (key.contains("-----BEGIN PUBLIC KEY-----")) {
key = key.substring(key.indexOf("-----BEGIN PUBLIC KEY-----") + 26);
}
if (key.contains("-----END PRIVATE KEY-----")) {
key = key.substring(0, key.indexOf("-----END PRIVATE KEY-----"));
}
if (key.contains("-----END PUBLIC KEY-----")) {
key = key.substring(0, key.indexOf("-----END PUBLIC KEY-----"));
}
key = key.replaceAll("\r\n", "");
key = key.replaceAll("\n", "");
return Base64.decodeBase64(key);
}
}
复制代码
JwtRsa工具类
@Slf4j
public class JwtRsaUtils {
/**
* 提供公钥字符串,返回RSAKey
*
* @param keyId
* @param publicKey
* @return
*/
public static RSAKey getRsaKey(String keyId, String publicKey) throws InvalidKeySpecException {
return getRsaKey(keyId, RsaUtils.getPublicKey(publicKey));
}
/**
* 提供公钥和私钥字符串,返回RSAKey
*
* @param keyId
* @param publicKey
* @param privateKey
* @return
*/
public static RSAKey getRsaKey(String keyId, String publicKey, String privateKey) throws InvalidKeySpecException {
return getRsaKey(keyId, RsaUtils.getPublicKey(publicKey), RsaUtils.getPrivateKey(privateKey));
}
/**
* 提供公钥,返回RSAKey
*
* @param keyId
* @param publicKey
* @return
*/
public static RSAKey getRsaKey(String keyId, PublicKey publicKey) {
return new RSAKey.Builder((RSAPublicKey) publicKey)
.keyUse(KeyUse.SIGNATURE)
.algorithm(JWSAlgorithm.RS256)
.keyID(keyId)
.build();
}
/**
* 提供公钥和私钥,返回RSAKey
*
* @param keyId
* @param publicKey
* @param privateKey
* @return
*/
public static RSAKey getRsaKey(String keyId, PublicKey publicKey, PrivateKey privateKey) {
return new RSAKey.Builder((RSAPublicKey) publicKey)
.privateKey(privateKey)
.keyUse(KeyUse.SIGNATURE)
.algorithm(JWSAlgorithm.RS256)
.keyID(keyId)
.build();
}
/**
* 根据RSAKey签名
*
* @param rsaKey
* @return
* @throws JOSEException
*/
public static String sign(RSAKey rsaKey) throws JOSEException {
return sign(rsaKey, new JWTClaimsSet.Builder().build());
}
/**
* 根据RSAKey签名
*
* @param rsaKey
* @param aud
* @return
* @throws JOSEException
*/
public static String sign(RSAKey rsaKey, String... aud) throws JOSEException {
JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
.audience(Arrays.asList(aud))
.build();
return sign(rsaKey, claimsSet);
}
/**
* 根据RSAKey签名,可设置过期时间
*
* @param rsaKey
* @param expire 过期时间,单位毫秒
* @param aud
* @return
* @throws JOSEException
*/
public static String sign(RSAKey rsaKey, long expire, String... aud) throws JOSEException {
JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
.audience(Arrays.asList(aud))
.expirationTime(new Date(System.currentTimeMillis() + expire))
.build();
return sign(rsaKey, claimsSet);
}
/**
* 根据RSAKey签名,可设置过期时间
*
* @param rsaKey
* @param issuer iss
* @param subject sub
* @param expire 过期时间,单位毫秒
* @param aud
* @return
* @throws JOSEException
*/
public static String sign(RSAKey rsaKey, String issuer, String subject, long expire, String... aud) throws JOSEException {
JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
.issuer(issuer)
.subject(subject)
.audience(Arrays.asList(aud))
.expirationTime(new Date(System.currentTimeMillis() + expire))
.build();
return sign(rsaKey, claimsSet);
}
/**
* 签名
*
* @param rsaKey
* @param claimsSet
* @return
* @throws JOSEException
*/
public static String sign(RSAKey rsaKey, JWTClaimsSet claimsSet) throws JOSEException {
SignedJWT signedJWT = new SignedJWT(
new JWSHeader.Builder(JWSAlgorithm.RS256).keyID(rsaKey.getKeyID()).build(),
claimsSet);
signedJWT.sign(new RSASSASigner(rsaKey));
return signedJWT.serialize();
}
/**
* 验证签名
*
* @param rsaKey
* @param token
* @return
*/
public static boolean verify(RSAKey rsaKey, String token) {
try {
SignedJWT signedJWT = SignedJWT.parse(token);
if (signedJWT.getJWTClaimsSet().getExpirationTime() != null) {
if (!new Date().before(signedJWT.getJWTClaimsSet().getExpirationTime())) {
return false;
}
}
RSASSAVerifier verifier = new RSASSAVerifier(rsaKey);
return signedJWT.verify(verifier);
} catch (ParseException e) {
log.debug("解析JWT失败", e);
return false;
} catch (JOSEException e) {
log.debug("解析JWT时密钥错误", e);
return false;
}
}
/**
* 验证签名并返回JWT对象
*
* @param rsaKey
* @param token
* @return 如果返回值为null就表示验证失败
*/
public static SignedJWT verifyWithData(RSAKey rsaKey, String token) {
try {
SignedJWT signedJWT = SignedJWT.parse(token);
if (signedJWT.getJWTClaimsSet().getExpirationTime() != null) {
if (!new Date().before(signedJWT.getJWTClaimsSet().getExpirationTime())) {
return null;
}
}
RSASSAVerifier verifier = new RSASSAVerifier(rsaKey);
if (signedJWT.verify(verifier)) {
return signedJWT;
} else {
return null;
}
} catch (ParseException e) {
log.debug("解析JWT失败", e);
return null;
} catch (JOSEException e) {
log.debug("解析JWT时密钥错误", e);
return null;
}
}
}
复制代码
用法
// keyId 是随便填的字符串
RSAKey rsaKey = JwtRsaUtils.getRsaKey(keyId, publicKey, privateKey);
String token = JwtRsaUtils.sign(rsaKey, issuer, subject, expireTime,
defaultAccount.getId().toString(),
defaultAccount.getUserId().toString(),
defaultAccount.getCustomerId().toString(),
defaultAccount.getName(),
defaultAccount.getAccountType());
// 直接解析不验证,这一步是不需要密钥的,因为是明文的
SignedJWT jwt = SignedJWT.parse(token);
List<String> audience = jwt.getJWTClaimsSet().getAudience();
// 验证
if (JwtRsaUtils.verify(rsaKey, token)) {
// 验证通过
}
// 验证并返回payload
SignedJWT jwt = JwtRsaUtils.verifyWithData(rsaKey, token);
if (jwt != null) {
// 验证通过
List<String> audience = jwt.getJWTClaimsSet().getAudience();
}
复制代码
公众号:飞翔的代码