目录
1. MD5 加密算法
MD5 是 Message Digest Algorithm 的缩写,译为信息摘要算法,它是 Java 语言中使用很广泛的一种加密算法。MD5 可以将任意字符串,通过不可逆的字符串变换算法,生成一个唯一的 MD5 信息摘要,这个信息摘要也就是我们通常所说的 MD5 字符串。那么问题来了,MD5 安全吗?
答案是 MD5 并不安全 。
原因是 md 5 的每一位 原始密码都对应者着一位固定的密码,也就是说一个字符串的 MD5 永远是不变的,虽然说MD5并不可逆,但是可以被穷举出来,这里就 不得不介绍一下彩虹表了,当我们把每一位数的原始值对应的加密值在同一个列表中列举出来,就构成了一个彩虹表,
当我们知晓了 1 的 MD5 的值为 :c4ca4238a0b923820dcc509a6f75849b
2 的 MD5 的值为 :c81e728d9d4c2f636f067f89cc14862c ,这样我们就可以得到一张彩虹表用来存储
原始值 | 加密值 |
---|---|
1 | c4ca4238a0b923820dcc509a6f75849b |
2 | c81e728d9d4c2f636f067f89cc14862c2 |
彩虹表通常用于恢复由有限集字符组成的固定长度的纯文本密码,也就是说,我们可以使用彩虹表来逐一穷举出原密码的每一位,所以在实际使用过程中数据库如果只是用 md5 进行加密,一把插了钥匙的锁一样不安全。
那么如何解决这种现象来实现加密呢?
只需要使同一个字符串每一次生成的密码都不一样,实现的关键是使用随机数,也就是 加盐
2. 加盐加密流程
之前使用 MD5 不安全的原因是,每一个原始密码所对应的MD5 的值是固定的,那我们只需要让密码每次加盐之后,生成的最终的密码都不相同,这样就能解决加密不安全的问题了。
- 使用 MD5 进行加密的流程是: 明文密码 --> md5(明文密码) ---> 密码
- 使用加盐加密的流程是 :
1. 构建期 :明文密码 + 随机盐值
2 . md5(明文密码+随机盐值)
3.将盐值和加密之后的密码存储到数据库(存储到一个字段里边,自己约定一个规则来分离盐值和加密之后的密码)
在实际项目的开发过程中,因为需要将输入的密码和数据库中加盐加密的密码进行验证,所以我们需要在数据库中存入两样东西,一个是加盐加密之后的密码,另一个就是随机盐值
每一个随机盐值就对应了一个彩虹数据库,如果有n个随机盐值,就需要N个不同的彩虹数据库。
那使用这样的约定规则是否有泄漏密码的风险呢?
答案是肯定的,从理论上来讲,目前所有的安全手段都不是绝对安全的,破解上述的加盐密码数据库的破解成本远远高于收益,所以加盐加密的密码相对来说是比较安全的。
- 模拟实现加盐加密:
实现规则:使用分隔符 $ 来分隔盐值和加密之后的密码
实现思路:将数据库中的 dbpassword 进行解密得到盐值,让用户输入的密码与该盐值进行加盐加密,最后和数据库中存的加盐加密之后的密码进行对比,如果两者相同说明密码匹配,如果两者不同,则说明密码不匹配。
加盐的具体实现步骤:
- 使用 UUID 产生一个随机盐值;
- 将随机盐值 + 原始密码一起 MD5,产生一个新密码(相同的原始密码,每次都会生成一个不同的新密码);
- 将随机盐值 + "$"+上一步生成的新密码加在一起,就是最终生成的密码。
验证加盐加密的具体实现:
- 获取数据库中 dbpassword 中的盐值
- 用相同的加密方式和步骤,生成一个最终密码和和数据库中保存的加密密码进行对比。
- 对比的结果相同,则密码输入正确
/**
*
* 密码工具类
* 实现加盐加密 / 加盐解密
*/
public class PasswordTools {
/**
* 加密(加盐处理)
* @param password 待加密密码(需要加密的密码)
* @return 加密后的密码
*/
public static String encrypt(String password) {
// 随机盐值 UUID
String salt = UUID.randomUUID().toString().replaceAll("-", "");
// 密码=md5(随机盐值+密码)
String finalPassword = DigestUtils.md5DigestAsHex((salt + password).getBytes());
// return salt + "$" + finalPassword;
String dbPassword = salt + "$" + finalPassword;
return dbPassword;
}
/**
* 解密
* @param password 要验证的密码(未加密)
* @param dbPassword 数据库中的加了盐值的密码
* @return 对比结果 true OR false
*/
public static boolean decrypt(String password, String dbPassword) {
boolean result = false;
if (StringUtils.hasLength(password) && StringUtils.hasLength(dbPassword)) {
if (dbPassword.length() == 65 && dbPassword.contains("$")) {
String[] dbPasswordArr = dbPassword.split("\\$");
// 盐值
String salt = dbPasswordArr[0];
String finalPassword = dbPasswordArr[1];
// 使用同样的加密算法和随机盐值生成最终加密的密码
password = DigestUtils.md5DigestAsHex((salt + password).getBytes());
if (finalPassword.equals(password)) {
result = true;
}
}
}
return result;
}
}
下面使用一组用例来测试上述代码是否实现了加盐加密和解密验证的过程,此处假定用户的明文密码为 :1234567
public class Main{
public static void main(String[] args){
String password = "1234567";
String dbPassword = encrypt(password);
System.out.println(dbPassword);
Scanner s = new Scanner(System.in);
while(s.hasNext()){
String str = s.nextLine();
boolean flg = decrypt(str,dbPassword);
System.out.println(flg);
}
}
}
以上程序模拟实现了密码加盐加密的过程和进行解密验证的过程,Spring 中内置了 Spring Security 框架,可以很方便的使用相应的方法。
3. Spring Security 实现加盐加密
1. 添加 Spring Security 框架
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
2. 关闭 Spring Security 认证
如果不关闭 Spring Security 认证的话,就会访问 Spring Security 提供的内置的登录页面,用户名和默认的密码会在控制台打印。
但是此处我们不需要Spring Security 的登录页面,所以需要在启动类中修改,来关闭 Spring Security 的 授权(自动类加载)
3.实现加盐加密
使用 BCryptPasswordEncoder 类中的 encode() 方法来实现项目的加盐加密,使用 matches ()方法来实现项目的解密验证。
public class Main{
public static void main(String[] args) {
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
String password = "12345";
String dbPassword = passwordEncoder.encode(password);
System.out.println(dbPassword);
Scanner s = new Scanner(System.in);
while(s.hasNext()){
String str = s.nextLine();
System.out.println(passwordEncoder.matches(str,dbPassword));
}
}
}
项目登录注册功能实现加盐加密和验证:
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@Autowired
private PasswordEncoder passwordEncoder; // 注入密码加密器
/**
* 实现项目注册功能
* @param userInfo
* @return
*/
@RequestMapping("/reg")
public AjaxResult reg(UserInfo userInfo) {
// 进行非空判断
if (userInfo == null || !StringUtils.hasLength(userInfo.getUsername()) ||
!StringUtils.hasLength(userInfo.getPassword()))
return AjaxResult.fail(-1, "参数有误");
// 对密码进行加盐加密
String salt = generateSalt(); // 生成盐值
String encryptedPassword = passwordEncoder.encode(userInfo.getPassword() + salt); // 加盐加密密码
userInfo.setPassword(encryptedPassword); // 更新用户对象的密码为加密后的密码
userInfo.setSalt(salt); // 设置盐值
// 调用 UserService 执行添加方法,并将返回的结果添加 AjaxResult.data 进行返回
int result = userService.reg(userInfo);
return AjaxResult.success(result);
}
/**
* 实现项目登录功能
* @param username
* @param password
* @param request
* @return
*/
@RequestMapping("/login")
public AjaxResult login(String username, String password,
HttpServletRequest request) {
if (!StringUtils.hasLength(username) || !StringUtils.hasLength(password))
return AjaxResult.fail(-1, "参数有误");
UserInfo userInfo = userService.login(username);
if (userInfo == null || userInfo.getId() <= 0)
return AjaxResult.fail(-2, "用户名或密码输入错误!");
// 使用密码加密器验证密码是否匹配(包括盐值和加密密码的比较)
boolean passwordMatch = passwordEncoder.matches(password + userInfo.getSalt(), userInfo.getPassword());
if (!passwordMatch)
return AjaxResult.fail(-2, "用户名或密码输入错误!");
// 将当前成功登录的用户信息,存储到 session 中去
HttpSession session = request.getSession();
session.setAttribute(ApplicationVariable.SESSION_KEY_USERINFO, userInfo);
return AjaxResult.success(1);
}
// 生成随机盐值的方法
private String generateSalt() {
// 根据需要选择适合您的盐值生成方法,这里使用UUID作为示例
return UUID.randomUUID().toString().replace("-", "");
}
}
当我们注册新用户时,用户的明文密码就会以加盐加密的密码来进行存储,同时也会存储密码加盐时使用的盐值,登录界面时使用存储的盐值来进行用户的登录验证。
当用户登陆时输入的密码,与存储的盐值进行加盐加密之后的密码 和数据库中存储的用户注册时存储的加盐加密的密码相匹配时,用户登录验证成功。
总结:
进行项目的密存储时,不能只是简单的使用 md5 进行加密处理,需要使用随机盐值结合原始密码一起进行 md5 得到加盐加密之后的密码,然后将加盐加密之后的密码和盐值分别存储到数据库当中,这样才真正实现了项目的加盐加密。