密码加密方式
1 可逆加密算法1_对称加密2_非对称加密3_RSA 算法
2 不可逆加密算法1_MD5算法2_Bcrypt算法
3 BcryptPasswordEncode原理介绍1_Bcrypt加密2_Bcrypt密码匹配
密码加密方式是指通过使用特定的算法将原始密码转换成一个(在理想情况下)无法还原的不可读形式,以确保即使数据被泄露,密码也不会轻易地被恶意用户获取。在计算机安全中,这个过程通常称为散列(Hashing)。
密码在存储时通常会使用散列函数处理。散列函数能够接收任意长度的输入(比如一个密码),并产生一个固定长度的输出(即散列值或哈希值)。这个过程应该是单向的,也就是说,从散列值获得原始输入应该是不可能的或极其困难的。
1 可逆加密算法
加密后, 密文可以反向解密得明文原文;
1_对称加密
加密和解密使用相同密钥的加密算法。
优点: 对称加密算法的优点是算法公开、计算量小、加密速度快、加密效率高。
缺点: 没有非对称加密安全。
常见的对称加密算法:DES、3DES、DESX、Blowfish、RC4、RC5、RC6和AES
说白了就是加密和解密都使用同一个秘钥处理;
2_非对称加密
加密和解密使用不同密钥的加密算法,也称为公私钥加密。假设两个用户要加密交换数据,双方交换公钥,使用时一方用对方的公钥加密,另一方即可用自己的私钥解密。
加密和解密:
私钥加密,持有私钥或公钥才可以解密公钥加密,持有私钥才可解密
优点: 非对称加密与对称加密相比,其安全性更好;
缺点: 非对称加密的缺点是加密和解密花费时间长、速度慢,只适合对少量数据进行加密。
常见的非对称加密算法如:
R
S
A
RSA
RSA(Rivest-Shamir-Adleman)、DSA(Digital Signature Algorithm)、ECC(Elliptic Curve Cryptography)。
3_RSA 算法
生成RSA公钥和私钥的过程基于数学原理,主要涉及质数的生成和模算术。
RSA加密算法的密钥生成过程大致如下:
选择两个大素数:首先随机选择两个大素数
p
p
p 和
q
q
q。这两个数的大小直接影响到密钥的安全性,通常选择2048位或更高位数的素数。
计算模数:计算模数
(
n
=
p
×
q
)
( n = p \times q)
(n=p×q)。公钥和私钥都会使用这个模数。
计算欧拉函数:
φ
(
n
)
=
(
p
−
1
)
×
(
q
−
1
)
\varphi(n) = (p-1) \times (q-1)
φ(n)=(p−1)×(q−1) 欧拉函数用于后续计算。
选择公钥指数:选择一个与
φ
(
n
)
\varphi(n)
φ(n) 互质的整数
e
e
e,通常选择常用的值如65537。
计算私钥指数:通过扩展的欧几里得算法计算
d
d
d,使得:
d
×
e
≡
1
m
o
d
φ
(
n
)
d \times e \equiv 1\mod \varphi(n)
d×e≡1modφ(n)
公钥和私钥:
公钥为
(
e
,
n
)
(e, n)
(e,n)私钥为
(
d
,
n
)
(d, n)
(d,n)
安全性
RSA的安全性依赖于以下几个因素:
大素数的选择:合适的素数选取非常重要,必须足够大,以抵抗暴力破解和质因数分解攻击。私钥的保密性:私钥不得泄露。如果私钥被泄露,加密数据的安全性将会受到威胁。算法的实现:使用经过验证的库和实现,防止常见的漏洞和攻击。
JAVA 也提供了生成私钥和公钥的方法,如下列使用
R
S
A
RSA
RSA的例子:
import java.io.FileWriter;
import java.io.IOException;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
public class RS256KeyPairGenerator {
public static void main(String[] args) {
try {
// 生成RSA密钥对
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048); // 秘钥长度可选:2048,3072,4096
KeyPair keyPair = keyPairGenerator.generateKeyPair();
// 获取公钥和私钥
PublicKey publicKey = keyPair.getPublic();
PrivateKey privateKey = keyPair.getPrivate();
// 将公钥和私钥转换为PEM格式
String publicKeyPEM = "-----BEGIN PUBLIC KEY-----\n"
+ Base64.getEncoder().encodeToString(publicKey.getEncoded())
+ "\n-----END PUBLIC KEY-----";
String privateKeyPEM = "-----BEGIN PRIVATE KEY-----\n"
+ Base64.getEncoder().encodeToString(privateKey.getEncoded())
+ "\n-----END PRIVATE KEY-----";
// 输出PEM格式的密钥
System.out.println("Public Key:\n" + publicKeyPEM);
System.out.println("Private Key:\n" + privateKeyPEM);
// 将密钥保存到文件中
saveKeyToFile("public_key.pem", publicKeyPEM);
saveKeyToFile("private_key.pem", privateKeyPEM);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
}
private static void saveKeyToFile(String fileName, String key) {
try (FileWriter fileWriter = new FileWriter(fileName)) {
fileWriter.write(key);
System.out.println(fileName + " saved successfully.");
} catch (IOException e) {
e.printStackTrace();
}
}
}
要增强密钥的安全性,可以考虑引入干扰因子,但在RSA算法的密钥生成过程中,直接参与生成的干扰因子并不常见,因为RSA本身的安全性已经足够。
常见的做法包括:
使用随机数生成器的种子: 可以在密钥生成过程中使用一个随机种子字符串来影响随机数生成器,例如:
SecureRandom secureRandom = new SecureRandom("your_random_seed_string".getBytes());
keyPairGenerator.initialize(2048, secureRandom);
结合用户输入或其他随机信息: 也可以在生成密钥前结合用户的输入(如密码)进行处理,增加生成随机数的复杂性。
// 结合用户提供的字符串
String userInput = "user_input_string";
byte[] seed = userInput.getBytes();
secureRandom.setSeed(seed);
keyPairGenerator.initialize(2048, secureRandom);
setSeed方法的说明:Reseeds this random object. The given seed supplements, rather than replaces, the existing seed. Thus, repeated calls are guaranteed never to reduce randomness。 大致意思就是只做增强而不是替换
2 不可逆加密算法
一旦加密就不能反向解密得到密码原文 。通常用于密码数据加密。
常见的不可逆加密算法有:
M
D
5
MD5
MD5 、
S
H
A
SHA
SHA、
H
M
A
C
HMAC
HMAC。
1_MD5算法
MD5是比较常见的加密算法,作为一种广泛使用的哈希函数,主要用于将任意长度的消息压缩成一个固定长度的消息摘要(通常是128位)。
曾经广泛应用于软件开发中的密码加密、数字签名、文件完整性校验中。通过MD5生成的密文,是无法解密得到明文密码的。
文件完整性校验:通过比对文件的MD5值来验证文件是否在传输过程中被篡改。密码加密:在一些不安全的环境中,MD5曾被用于存储用户密码的哈希值。数字签名:在某些系统中,MD5被用于生成消息摘要,以进行数字签名。
但是现在在大数据背景下(主要由于计算能力的提升和密码学研究的进展),很多的网站通过大数据可以将简单的MD5加密的密码破解。
因此,由于其安全性问题,尤其是在密码存储和数据完整性验证等敏感应用中,已经被广泛认为是不安全的。
哈希碰撞:MD5存在理论上和实际上的哈希碰撞(不同的输入产生相同的哈希值)。这种碰撞极大地削弱了其安全性。快速碰撞生成算法:通过特定的算法,有可能在较短时间内生成两个具有相同MD5值的文件。密码破解:由于MD5的设计缺陷,使得使用暴力破解或彩虹表攻击的成功率较高。
网址: https://www.cmd5.com/
可以在用户注册时,限制用户输入密码的长度及复杂度,从而增加破解难度,但是还是不能改变不再安全的事实。
MD5的工作流程可以分为以下几个步骤:
消息填充:将输入消息填充到满足特定条件的长度,使填充后的消息长度为512位的倍数。填充包括一个“1”位,加上若干“0”位,最后加上消息的长度信息(以64位表示)。
初始化缓冲区:使用四个32位的寄存器(A、B、C、D)来存储中间结果和最终结果。这些寄存器被初始化为特定的常数值。
处理消息块:将填充后的消息分成若干个512位的块。对每个块,分别进行一系列复杂的位操作和非线性函数处理。
输出:处理完所有的消息块后,寄存器中的内容就是最终的128位哈希值。
以下是一个使用Java生成MD5哈希值的示例代码:
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class MD5Example {
public static void main(String[] args) {
String input = "Hello, World!";
String md5Hash = generateMD5(input);
System.out.println("MD5 Hash: " + md5Hash);
}
public static String generateMD5(String input) {
try {
// 获取MD5实例
MessageDigest md = MessageDigest.getInstance("MD5");
// 计算哈希值
byte[] messageDigest = md.digest(input.getBytes());
// 转换为十六进制字符串
StringBuilder hexString = new StringBuilder();
for (byte b : messageDigest) {
hexString.append(String.format("%02x", b));
}
return hexString.toString();
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
}
鉴于MD5的安全性问题,推荐使用更安全的哈希函数,如SHA-256或SHA-3。
2_Bcrypt算法
用户表的密码通常使用 MD5 等不可逆算法加密后存储,为防止彩虹表破解,会先使用一个特定的字符串(如域名)加密,然后再使用一个随机的 salt(盐值)加密。 特定字符串是程序代码中固定的,salt 是每个密码单独随机,一般给用户表加一个字段单独存储,比较麻烦。
BCrypt 算法将 salt 随机并混入最终加密后的密码,验证时也无需单独提供之前的salt,从而无需单独处理 salt 问题。
在SecurityConfig配置类配置密码加密匹配器:
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder(){
return new BCryptPasswordEncoder();
}
加密密码:
@Autoware
private BCryptPasswordEncoder bCryptPasswordEncoder;
@Test
public void test01(){
for (int i = 0; i < 5; i++) {
System.out.println(bCryptPasswordEncoder.encode("123456"));
}
}
得到结果:
$2a$10$C6YynRFeJsSy7D/kg3d30OWnuwko7KQIEK5JrX0mWND.vuz2TqwpK
$2a$10$aSJfxH2oBtopFMbkMJ.PQ.sbSBXJH9g.9bv1mCyte/BtcU9VTs7lG
$2a$10$nVoB.eV5Uhc9FNUC36Pn0OosGh7aKlp7Sjfxaiml8NCSJ6PX1q6.m
$2a$10$2RM3mRNjz1LoZ5eeLdj.Hu15vlWIIj2zJC09vwTevBlIi5rjJStam
$2a$10$c2sZT/LtM1ExWfZjO0yIPeTGSqMSlX7oi.SvliMbeZpT9Y4qIBDue
验证密码:
boolean matches =
bCryptPasswordEncoder.matches("123456", "$2a$10$c2sZT/LtM1ExWfZjO0yIPeTGSqMSlX7oi.SvliMbeZpT9Y4qIBDue");
System.out.println(matches);//返回值为true, 则代表验证通过; 反之, 验证不通过
注意:此时重新启动security_demo测试工程,security底层会自动调用PasswordEncoder类型bean进行密码校验处理;
3 BcryptPasswordEncode原理介绍
SpringSecurity提供了实现PasswordEncoder接口的密码加密工具类:BcryptPasswordEncoder,该类基于Bcrypt强哈希算法来加密密码,更加安全;
Bcrypt强哈希方法每次加密相同的明文得到的密文结果都不一样,这样即使数据库泄露,黑客也很难破解密码;
1_Bcrypt加密
一般在注册用户时,Bcrypt使用SHA-256加密算法+随机盐值+秘钥(明文密码)进行加密处理,得到的密文存入数据库中;
@Test
public void testPwd(){
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
String enPwd1 = passwordEncoder.encode("123456");
System.out.println("enPwd1:"+enPwd1);
String enPwd2 = passwordEncoder.encode("123456");
System.out.println("enPwd2:"+enPwd2);
}
相同的明文加密得到的密文各不相同:
enPwd1:$2a$10$yZJPn5tlBVSW3Udt926HcO8AiyzBKcLi4gKaJrVNG1Yw6LlmYCZhm
enPwd2:$2a$10$H4P8eWCGM.kpy8/.ayYfruoJbFMyi4lnIxjBYP2yB7d6yqiQS8NTi
进入encode方法:
public String encode(CharSequence rawPassword) {
if (rawPassword == null) {
throw new IllegalArgumentException("rawPassword cannot be null");
} else {
String salt = this.getSalt();
return BCrypt.hashpw(rawPassword.toString(), salt);
}
}
生成加密的代码
public static String hashpw(String password, String salt) {
byte[] passwordb = password.getBytes(StandardCharsets.UTF_8);
return hashpw(passwordb, salt);
}
public static String hashpw(byte[] passwordb, String salt) {
char minor = 0;
StringBuilder rs = new StringBuilder();
if (salt == null) {
throw new IllegalArgumentException("salt cannot be null");
} else {
int saltLength = salt.length();
if (saltLength < 28) {
throw new IllegalArgumentException("Invalid salt");
} else if (salt.charAt(0) == '$' && salt.charAt(1) == '2') {
byte off;
if (salt.charAt(2) == '$') {
off = 3;
} else {
minor = salt.charAt(2);
if (minor != 'a' && minor != 'x' && minor != 'y' && minor != 'b' || salt.charAt(3) != '$') {
throw new IllegalArgumentException("Invalid salt revision");
}
off = 4;
}
if (salt.charAt(off + 2) > '$') {
throw new IllegalArgumentException("Missing salt rounds");
} else if (off == 4 && saltLength < 29) {
throw new IllegalArgumentException("Invalid salt");
} else {
int rounds = Integer.parseInt(salt.substring(off, off + 2));
String real_salt = salt.substring(off + 3, off + 25);
byte[] saltb = decode_base64(real_salt, 16);
if (minor >= 'a') {
passwordb = Arrays.copyOf(passwordb, passwordb.length + 1);
}
BCrypt B = new BCrypt();
byte[] hashed = B.crypt_raw(passwordb, saltb, rounds, minor == 'x', minor == 'a' ? 65536 : 0);
rs.append("$2");
if (minor >= 'a') {
rs.append(minor);
}
rs.append("$");
if (rounds < 10) {
rs.append("0");
}
rs.append(rounds);
rs.append("$");
encode_base64(saltb, saltb.length, rs);
encode_base64(hashed, bf_crypt_ciphertext.length * 4 - 1, rs);
return rs.toString();
}
} else {
throw new IllegalArgumentException("Invalid salt version");
}
}
}
我们发现明文密码加密时,Bcrypt底层生成一个随机盐值参与加密运算,同时也会将盐值和加密后的密文一并形成最终密文;
2_Bcrypt密码匹配
一般在用户认证登录时,业务方法首先根据用户信息获取对应的密文信息,然后将明文和密码经过matches方法获取匹配结果为true则表示密码一致;
Bcrypt底层是先根据密文获取生成的盐值,然后再将盐值与明文加密得到密文,这个密文如果与从数据库获取的密文一致,则说明输入的名称是正确的。
而且根据下面的分析,可以发现,解密的过程中会根据传递过来的明文和密文经过一系列计算获取real_salt真实的盐值————>真实的盐值是salt盐值的某一部分。而具体多少位是有效的需要根据你的明文进行判断得到,然后根据真实的盐值再将明文加密后与盐salt组合后获取密文。最后与密文信息进行匹配,这样大大提高了安全性。
@Test
public void testMatcher() {
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
String enPwd="$2a$10$yZJPn5tlBVSW3Udt926HcO8AiyzBKcLi4gKaJrVNG1Yw6LlmYCZhm";
boolean isSuccess = passwordEncoder.matches("123456", enPwd);
System.out.println(isSuccess);
}
public boolean matches(CharSequence rawPassword, String encodedPassword) {
if (rawPassword == null) {
throw new IllegalArgumentException("rawPassword cannot be null");
} else if (encodedPassword != null && encodedPassword.length() != 0) {
if (!this.BCRYPT_PATTERN.matcher(encodedPassword).matches()) {
this.logger.warn("Encoded password does not look like BCrypt");
return false;
} else {
//这里拿到了
return BCrypt.checkpw(rawPassword.toString(), encodedPassword);
}
} else {
this.logger.warn("Empty encoded password");
return false;
}
}