前两节博客主要是针对MD5和哈希算法,数字签名算法做了阐述和总结;
这篇文章主要是针对AES和RSA做一个概况和比较,以及相关的一些概念;
一、对称加密算法
对称加密算法是指加密和解密采用相同的密钥口,是可逆的(即可解密)。
1、AES
AES算法的发展可以追溯到1997年,是一种对称加密算法,其核心思想是将明文数据分成128位块,并通过多轮加密操作生成加密密文。
AES算法共包括10、12或14轮加密,根据密钥长度不同而有所变化。每轮加密包括四个步骤:置换、子密钥生成、异或操作和添加轮密钥。
优势
- 快;AES算法在保证安全性的同时,具有较高的性能优势
- 通用;对机器性能要求较低
劣势
- AES算法对密钥管理的严格要求,因为加解密都是一套秘钥
2、DES、3DES
DES 算法在现代加密标准中已经被认为是不安全的。由于 DES 算法的密钥长度较短,且密钥管理困难,因此容易受到暴力破解攻击。【不做过多的概述,了解即可】
3DES是针对DES算法密钥过短、存在安全性的问题而改进的一个措施,被称为“3DES”。其实只是通过简单的执行3次DES来达到增加密钥长度和安全而已;
- 密钥长度短
DES算法的密钥长度只有64位,安全性较低,易受到暴力破解攻击。 - 密钥弱化问题
DES算法存在一些密钥弱化问题,即某些密钥可以被用于多次加密和解密,这会导致密钥的安全性受到威胁。 - 安全性受到质疑
DES算法已经被成功攻击,证明其安全性受到威胁,已经不适合作为现代加密算法使用。 - 不适用于开放环境
DES算法需要共享密钥,因此不适用于开放的环境,例如互联网。 - 无法实现安全的密钥交换
DES算法需要事先共享密钥,因此在传输密钥的过程中容易被攻击者截获并破解。因此,密钥交换的安全性成为了DES算法的一个难点。
二、非对称加密算法
非对称加密算法是指加密和解密采用不同的密钥(公钥和私钥),因此非对称加密也叫公钥加密,是可逆的(即可解密)。
1976年,两位美国计算机学家Whitfield Diffie 和 Martin Hellman,提出了一种崭新构思,可以在不直接传递密钥的情况下,完成解密。这被称为"Diffie-Hellman密钥交换算法"。
(1)甲要传密信给乙,乙先根据某种算法得出本次与甲通信的公钥与私钥
(2)乙将公钥传给甲(公钥可以让任何人知道,即使泄露也没有任何关系)
(3)甲使用乙传给的公钥加密要发送的信息原文m,发送给乙密文c
(4)乙使用自己的私钥解密密文c,得到信息原文m
如果公钥加密的信息只有私钥解得开,那么只要私钥不泄漏,通信就是安全的。
RSA
RSA加密算法是基于一个十分简单的数论事实: 将两个大素数相乘十分容易,但是想要对其乘积进行因式分解极其困难;
因此可以将乘积公开作为加密密钥。虽然RSA的安全性一直未能得到理论上的证明,但它经历了各种攻击至今未被完全攻破。
网上不少例子说的是公钥用于加密,私钥用于解密,其实这个说法不对,私钥和公钥是一对,都可以加解密,配对使用,只不过公钥可以公布出去,而私钥是持有者自己保留的。
一般的用法是私钥加密用于签名防数据被篡改,公钥加密用于加密防敏感信息,防止泄露。
RSA用途
- 数据加密
发送者用公钥加密,接收者用私钥解密(只有拥有私钥的接收者才能解读加密的内容) - 数字签名
甲方用私钥加密,乙方用公钥解密(乙方解密成功说明就是甲方加的密,甲方就不可以抵赖)
优势
- 非常安全;加密和解密的密钥不一致,公钥是可以公开的,只需保证私钥不被泄露即可(私钥在开发时协定好)
- 密钥的传递变的简单很多,灵活高效;
劣势
- 扩展性不好;由于素数产生技术的限制,难以做到一次一密
- 加密速度慢;它的难度是基于大整数分解素数产生的难度,因此产生密钥耗时、也耗CPU【小数据量、低并发的场景】
三、线性散列算法
经典代表:MD5、SHA1、HMAC
MD5全称是Message-Digest Algorithm 5(信息摘要算法5),单向的算法不可逆(被MD5加密的数据不能被解密)。MD5加密后的数据长度要比加密数据小的多,且长度固定,且加密后的串是唯一的。【可以看一下我之前的MD5加密博文,不做过多描述,基本上如果对安全要求严苛的场景下,已经不适用了】
适用场景:常用在不可还原的密码存储、信息完整性校验等。
四、(RSA+AES)混合加密
由于以上加密算法都有各自的缺点(RSA加密速度慢、AES密钥存储问题、MD5加密不可逆),因此实际应用时常将几种加密算法混合使用。
采用RSA加密AES的密钥,采用AES对数据进行加密,这样集成了两种加密算法的优点,既保证了数据加密的速度,又实现了安全方便的密钥管理。
RSA(灵活高效的安全传输)+ AES(高效加解密)=》混合加密;
- RSA加密慢,但是安全、灵活(小数据量);AES加密效率高(大数据量),但是不安全
- RSA充当密码的载体,将AES的加密数据从前端=》服务端,再由RSA解码后,AES再去对加密数据解析;
3.RSA适合密钥交换和数字签名;AES适合做数据加解密;
五、Base64加密
格意义讲,Base64并不能算是一种加密算法,而是一种编码格式,是网络上最常见的用于传输8字节代码的编码方式之一。
Base64编码可用于在HTTP环境下传递较长的标识信息,Base编码不仅不仅比较简单,同时也据有不可读性(编码的数据不会被肉眼直接看到)。
适用场景:常用于前后台针对隐私或敏感数据做加密,比如手机号、跳转路由之类,只希望前端做中转的数据透传场景。屏蔽肉眼识别的一种编码
实战
1.RSA+AES数据传输【混合加密】
代码
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Map;
import java.util.Scanner;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import com.alibaba.fastjson.JSON;
import com.google.common.collect.Maps;
import io.jsonwebtoken.Claims;
import org.apache.commons.codec.binary.Base64;
/**
* RSA加密工具类
*/
public class RSAUtil {
private static Map<String, String> keyMap; // 用于封装随机产生的公钥与私钥
static {
// 如果是分布式项目,再加一个redis的分布式锁
synchronized (RSAUtil.class) {
// 1.生成秘钥
long start = System.currentTimeMillis();
keyMap = initRsaKeys();
System.out.println("服务端项目启动,公私秘钥生成...." + (System.currentTimeMillis() - start));// 这个耗时还是很长的,我本地大概是1.8s
System.out.println("写入到数据库或者redis....");
}
}
public static void main(String[] args) throws Exception {
// dataTransportCase();//测试AES的key:ABCDEFGHIJKLMNOP
jwtCase();
}
/**
* jwt登录实战
*/
public static void jwtCase() throws Exception {
// 1.用户登录[可以使用case1做账户密码的接收]
System.out.print("1.界面:用户输入用户名和密码:");
Scanner scanner = new Scanner(System.in);
String account = scanner.nextLine();
String privateKey = keyMap.get("private");
System.out.println("2.后台:判断用户存在,生成token给到前端....");
String token = JwtUtil.generateToken(account, "1000", privateKey);
// 2.token校验
System.out.print("3.前端:新发起一个请求,header中携带了token信息:" + token);
String publicKey = keyMap.get("public");
Claims playLoad = JwtUtil.getPlayLoad(token, publicKey);
System.out.print("\n4.后台:解析前端在header中携带的token信息中的playLoad:");
System.out.println(JSON.toJSONString(playLoad));
System.out.println("5.后台:通过用户名去redis拿登录用户的缓存信息...."); }
/**
* RSA+AES加密传输实战
*/
public static void dataTransportCase() throws Exception {
// 1.项目启动
String publicKey = keyMap.get("public");
String privateKey = keyMap.get("private");
// 2.用户输入密码
Scanner scanner = new Scanner(System.in);
System.out.print("1.界面:用户输入用户名和密码:");
String userInfo = scanner.nextLine();
System.out.println("2.全局约定:请设置一个登录相关的AES的加密key(也可以直接通过api从后台拿):");
String aesKey = scanner.next();
// 3.前端将密码进行AES加密
String aesEncryptPwd = AESUtil.encrypt(aesKey, userInfo);
System.out.println("3.前端:对用户输入的密码加密结果传递给后台:" + aesEncryptPwd);
// 4.前端将加密的AES密码,再使用RSA的公钥进行加密
String rsaEncryptData = encrypt(aesEncryptPwd, publicKey);
// 5.后台直接通过项目启动时生成的RSA公私秘钥对,使用私钥进行解密
String rsaDecryptData = decrypt(rsaEncryptData, privateKey);
System.out.println("4.后台:接收到经过RSA加密的AES数据,先对其使用RSA秘钥解密...." + rsaDecryptData);
String aesDecryptPwd = AESUtil.decrypt(aesKey, rsaDecryptData);
System.out.println("5.后台:RSA解密后,再使用AES对密码解密...." + aesDecryptPwd);
System.out.println(aesDecryptPwd);
}
/**
* 模拟项目启动生成密钥对
* - 1.直接本地生成一个,丢到redis或者存储到文件里面,再存入到redis【不然服务挂了公私钥没了】
* - 2.项目启动的时候,直接读redis的数据,如果没有值则直接生成一对公私密钥,用base64进行编码(公司秘钥的获取可以提供一个api接口被前端获取)
* - 3.此处对公私秘钥用了一个base64加解密,其实没什么影响,只是做一下肉眼的保护而已,防止有心人直接拿去乱来
*/
public static Map<String, String> initRsaKeys() {
Map<String, String> result = Maps.newHashMap();
KeyPairGenerator keyPairGen = null;
try {
keyPairGen = KeyPairGenerator.getInstance("RSA");
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
assert keyPairGen != null;
keyPairGen.initialize(1024, new SecureRandom());
// 生成一个密钥对,保存在keyPair中
KeyPair keyPair = keyPairGen.generateKeyPair();
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate(); // 得到私钥
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic(); // 得到公钥
String publicKeyString = new String(Base64.encodeBase64(publicKey.getEncoded()));
String privateKeyString = new String(Base64.encodeBase64((privateKey.getEncoded())));
result.put("public", publicKeyString);
result.put("private", privateKeyString);
return result;
}
/**
* RSA公钥加密
*
* @param str 加密字符串
* @param publicKey 公钥
* @return 密文
*/
public static String encrypt(String str, String publicKey) {
//base64编码的公钥
byte[] decoded = Base64.decodeBase64(publicKey);
RSAPublicKey pubKey = null;
String outStr = null;
try {
pubKey = (RSAPublicKey) KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(decoded));
Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-1AndMGF1Padding");
cipher.init(Cipher.ENCRYPT_MODE, pubKey);
outStr = Base64.encodeBase64String(cipher.doFinal(str.getBytes(StandardCharsets.UTF_8)));
} catch (InvalidKeySpecException | BadPaddingException | IllegalBlockSizeException | InvalidKeyException |
NoSuchPaddingException | NoSuchAlgorithmException e) {
e.printStackTrace();
}
//RSA加密
return outStr;
}
/**
* RSA私钥解密
*
* @param str 加密字符串
* @param privateKey 私钥
* @return 明文
*/
public static String decrypt(String str, String privateKey) {
//64位解码加密后的字符串
byte[] inputByte = Base64.decodeBase64(str.getBytes(StandardCharsets.UTF_8));
//base64编码的私钥
byte[] decoded = Base64.decodeBase64(privateKey);
RSAPrivateKey priKey;
//RSA解密
Cipher cipher;
String outStr = null;
try {
priKey = (RSAPrivateKey) KeyFactory.getInstance("RSA").generatePrivate(new PKCS8EncodedKeySpec(decoded));
cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-1AndMGF1Padding");
cipher.init(Cipher.DECRYPT_MODE, priKey);
outStr = new String(cipher.doFinal(inputByte));
} catch (InvalidKeySpecException | NoSuchAlgorithmException | NoSuchPaddingException | BadPaddingException |
IllegalBlockSizeException | InvalidKeyException e) {
e.printStackTrace();
}
return outStr;
}
/**
* 公钥获取
*/
public static PublicKey getPublicKey(byte[] publicKey) throws Exception {
X509EncodedKeySpec spec = new X509EncodedKeySpec(publicKey);
KeyFactory kf = KeyFactory.getInstance("RSA");
return kf.generatePublic(spec);
}
/**
* 私钥获取
*/
public static PrivateKey getPrivateKey(byte[] privateKey) throws Exception {
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(privateKey);
KeyFactory kf = KeyFactory.getInstance("RSA");
return kf.generatePrivate(spec);
}
}
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
public class AESUtil {
public static String encrypt(String key, String data) throws Exception {
SecretKeySpec secretKey = new SecretKeySpec(key.getBytes(), "AES");
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
byte[] encryptedBytes = cipher.doFinal(data.getBytes());
return Base64.getEncoder().encodeToString(encryptedBytes);
}
public static String decrypt(String key, String encryptedData) throws Exception {
SecretKeySpec secretKey = new SecretKeySpec(key.getBytes(), "AES");
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, secretKey);
byte[] decryptedBytes = cipher.doFinal(Base64.getDecoder().decode(encryptedData));
return new String(decryptedBytes);
}
}
运行结果
Jwt签名的机制和结构需要理解到才知道每一步做的事情,最终的是和项目的实际情况相结合;
2.JwtToken登录实战【签名算法】
JWT是一种数字签名算法;数字签名的具体实现,通常是先对数据进行一次 Hash 摘要(SHA1/SHA256/SHA512 等),然后再使用非对称加密算法(RSA/ECDSA 等)的私钥对这个摘要进行加密,这样得到的结果就是原始数据的一个签名。
用户在验证数据时,只需要使用公钥解密出 Hash 摘要,然后自己再对数据进行一次同样的摘要,对比两个摘要是否相同即可。
在介绍它之前,需要解释一下这几个概念;
相关概念
数字签名
数据的 Hash 值;主要用途也是用于身份认证,代表这份数据就是你自己的:真实、完整、不可伪造;
数字摘要/指纹
它是一个唯一对应一个消息或文本的固定长度的值,它由一个单项Hash函数对消息进行计算而产出;
不定长的数据===》数字摘要算法(Hash)====>定长的数据
加密算法
就是对jwt进行加密的算法;如果稍微有用过jwt的人,应该见过下面这个代码:
@SneakyThrows
private static String generateToken(Map<String, Object> claims) {
return Jwts.builder()
// 数字加密主体
.setClaims(claims)
// 过期时间
.setExpiration(generateExpirationDate())
// 数字签名算法
.signWith(SignatureAlgorithm.RS256, RsaKeyUtil.getPrivateKey(JwtRsaKeyAutoConfig.rsaKeyConfig.getPrivateKey()))
// 根据规则进行加密
.compact();
}
签名算法是使用私钥加密,确保得到的签名无法被伪造,同时所有人都可以使用公钥解密来验证签名。这和正常的数据加密算法是相反的
数字签名的部分,点开jwt
的源码进去其实有好几种算法:
package io.jsonwebtoken;
import io.jsonwebtoken.lang.RuntimeEnvironment;
public enum SignatureAlgorithm {
NONE("none", "No digital signature or MAC performed", "None", (String)null, false),
HS256("HS256", "HMAC using SHA-256", "HMAC", "HmacSHA256", true),
HS384("HS384", "HMAC using SHA-384", "HMAC", "HmacSHA384", true),
HS512("HS512", "HMAC using SHA-512", "HMAC", "HmacSHA512", true),
RS256("RS256", "RSASSA-PKCS-v1_5 using SHA-256", "RSA", "SHA256withRSA", true),
RS384("RS384", "RSASSA-PKCS-v1_5 using SHA-384", "RSA", "SHA384withRSA", true),
RS512("RS512", "RSASSA-PKCS-v1_5 using SHA-512", "RSA", "SHA512withRSA", true),
ES256("ES256", "ECDSA using P-256 and SHA-256", "Elliptic Curve", "SHA256withECDSA", false),
ES384("ES384", "ECDSA using P-384 and SHA-384", "Elliptic Curve", "SHA384withECDSA", false),
ES512("ES512", "ECDSA using P-512 and SHA-512", "Elliptic Curve", "SHA512withECDSA", false),
PS256("PS256", "RSASSA-PSS using SHA-256 and MGF1 with SHA-256", "RSA", "SHA256withRSAandMGF1", false),
PS384("PS384", "RSASSA-PSS using SHA-384 and MGF1 with SHA-384", "RSA", "SHA384withRSAandMGF1", false),
PS512("PS512", "RSASSA-PSS using SHA-512 and MGF1 with SHA-512", "RSA", "SHA512withRSAandMGF1", false);
......
HS256【默认,已过时】
SHA-256 的 HMAC,它使用同一个「secret_key」进行签名与验证(对称加密)。只适合集中式认证,签名和验证都必须由可信方进行。
传统的单体应用广泛使用这种算法,但是请不要在任何分布式的架构中使用它!
RS256【常用,推荐】
SHA-256 的 RSA 签名,使用 RSA 公钥进行验证。公钥即使泄漏也毫无影响,只要确保私钥安全就行。
ES256【资料不多】
使用 ECDSA 进行签名,它的安全性和运算速度目前和 RS256 差距不大,但是拥有更短的签名长度。
对于需要频繁发送的 JWT 而言,更短的长度长期下来可以节约大量流量;
PS256【资料不多】
数据结构
1.JWT头
是一个描述JWT元数据的JSON对象,通常如下所示
{"alg": "HS256","typ": "JWT"}
alg:表示签名使用的算法,默认为HMAC SHA256(写为HS256)
typ:表示令牌的类型,JWT令牌统一写为JWT
2.有效载荷playload
JWT的主体内容部分,也是一个JSON对象,包含需要传递的数据【划重点】
有效载荷部分规定有如下七个默认字段供选择:
iss:发行人
exp:到期时间
sub:主题
aud:用户
nbf:在此之前不可用
iat:发布时间
jti:JWT ID用于标识该JWT
除以上默认字段外,还可以自定义私有字段,可以放用户的基础信息进去;
3.签名
签名实际上是一个使用私钥加密的过程,是对上面两部分数据通过指定的算法生成哈希,以确保数据不会被篡改。
- 1.服务器指定一个密码:secret
- 2.使用jwt配置指定的加密算法: rsa256
- 3.对各部分进行加密
param1 = sceret
param2 = base64(header) + “.” + base64(payload)
rsa256(param2, param1)
代码
工具类用第一个case的就可以
import cn.hutool.core.codec.Base64;
import com.blue.base.common.exception.BaseException;
import io.jsonwebtoken.*;
import org.springframework.util.StringUtils;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
public class JwtUtil {
/**
* 生成token
*/
public static String generateToken(String account, String userId, String privateKey) throws Exception {
// 1.header部分,用的不多
Map<String, Object> claims = new HashMap<>(10);
// 2.playLoad部分【自带的字段】
claims.put("aud", account);
claims.put("iat", new Date());
// playLoad部分【自定义的字段】
claims.put("userId", userId);
// 3.拿到rsa的私钥(base64做下解密)
PrivateKey rsaPrivateKey = RSAUtil.getPrivateKey(Base64.decode(privateKey));
return Jwts.builder()
// 数字加密主体
.setClaims(claims)
// 过期时间
.setExpiration(new Date(System.currentTimeMillis() + 84000L * 1000))// 1天
// 数字签名算法:rs256,使用私钥加密
.signWith(SignatureAlgorithm.RS256, rsaPrivateKey)
// 根据规则进行加密
.compact();
}
/**
* 获取jwt的claims部分(公钥解密token)
*/
static Claims getPlayLoad(String token, String publicKey) throws Exception {
if (StringUtils.isEmpty(token)) {
throw new IllegalArgumentException("token参数为空!");
}
PublicKey rsaPublicKey = RSAUtil.getPublicKey(Base64.decode(publicKey));
try {
return Jwts.parser()
.setSigningKey(rsaPublicKey)
.parseClaimsJws(token)
.getBody();
} catch (ExpiredJwtException e) {
throw new BaseException(800, "登录已过期");
} catch (Exception e) {
throw new BaseException(800, "Token解析异常");
}
}
}
运行结果
3.SpringBoot配置文件加密
除了常规的传输加密,比如某些yml文件不想让非核心开发知道的太多,或者涉及到敏感信息,不想对编外人员开放,可以使用下面的策略;
https://www.cnblogs.com/eyewink/p/17933845.html,比较简单,贵在实用;
总结
- 不用纠结非对称加密的公私秘钥在前端or后台生成,关键是看场景和主导方
- RSA拿来做签名是:私钥加密,公钥解密
- RSA拿来做数据传输加密是:公钥加密,私钥解密
- jwt的token案例在了解RSA的逻辑之后,会变得非常简单
参考资料
- https://blog.csdn.net/qq_42210428/article/details/135317456
- https://zhuanlan.zhihu.com/p/688039336
- https://www.cnblogs.com/tuyile006/p/10873975.html
- https://www.jianshu.com/p/77ed9d7d4745
- https://cloud.tencent.com/developer/article/1594839
- https://www.anxinssl.com/12544.html
- https://www.cnblogs.com/eyewink/p/17933845.html
- https://www.cnblogs.com/kirito-c/p/12402066.html
- https://blog.csdn.net/cj151525/article/details/131433755