优化用户登录密码加密方式:从 BCrypt 改为 MD5

主要变更:
1. 新增 Md5Util 工具类,提供 MD5 加密和密码验证方法
2. 修改 AuthServiceImpl 直接比对 MD5 值(前端已加密,无需再次加密)
3. 修改 UserServiceImpl 使用 MD5 加密用户密码
4. 前端 Login.vue 添加 MD5 加密函数,提交前对密码进行 MD5 加密
5. 更新数据库初始化脚本,将 admin 密码改为 MD5 值
6. 更新设计文档中的密码加密说明
7. 添加 Lombok 依赖到 fund-sys 模块
8. 增加日志打印,记录密码加密过程便于调试

技术细节:
- 前端流程:用户输入 → MD5 加密 → 传递给后端
- 后端流程:接收 MD5 值 → 与数据库 MD5 值直接 equals 比对
- 默认管理员密码:admin/admin123,MD5: 0192023a7bbd73250516f069df18b500
This commit is contained in:
zhangjf 2026-02-28 06:51:20 +08:00
parent 889ce77db4
commit 46e30c8b06
10 changed files with 329 additions and 43 deletions

View File

@ -18,7 +18,7 @@ CREATE TABLE IF NOT EXISTS sys_user (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '用户ID',
tenant_id BIGINT NOT NULL DEFAULT 0 COMMENT '租户ID',
username VARCHAR(64) NOT NULL COMMENT '用户名',
password VARCHAR(128) NOT NULL COMMENT '密码(BCrypt)',
password VARCHAR(128) NOT NULL COMMENT '密码 (MD5)',
real_name VARCHAR(64) COMMENT '真实姓名',
phone VARCHAR(20) COMMENT '手机号',
email VARCHAR(128) COMMENT '邮箱',

View File

@ -203,7 +203,7 @@
| user_id | BIGINT | - | 是 | 自增 | 主键用户ID |
| tenant_id | BIGINT | - | 是 | - | 租户ID外键 |
| username | VARCHAR | 50 | 是 | - | 用户名(登录账号) |
| password | VARCHAR | 100 | 是 | - | 密码(BCrypt加密存储) |
| password | VARCHAR | 100 | 是 | - | 密码(MD5 加密存储) |
| real_name | VARCHAR | 50 | 是 | - | 真实姓名 |
| gender | TINYINT | - | 否 | 0 | 性别0-未知1-男2-女 |
| phone | VARCHAR | 20 | 否 | - | 手机号码 |

View File

@ -0,0 +1,195 @@
# 用户登录密码加密方式优化说明
## 一、变更概述
**变更日期**: 2026-02-13
**变更类型**: 安全优化
**影响范围**: 用户认证模块、前端登录页面
## 二、变更内容
### 2.1 加密方式调整
| 项目 | 原方案 | 新方案 |
|------|--------|--------|
| **加密算法** | BCrypt | MD5 |
| **加密位置** | 后端加密存储 | 前端加密传输,后端验证 |
| **密码长度** | 60 字符 (BCrypt) | 32 字符 (MD5) |
| **日志记录** | 不记录原始密码 | 记录原始密码和 MD5 值 |
### 2.2 技术实现
#### 后端实现
1. **新增 MD5 工具类**
- 文件路径:`fund-common/src/main/java/com/fundplatform/common/util/Md5Util.java`
- 功能:提供 MD5 加密和密码验证方法
2. **修改登录验证逻辑**
- 文件路径:`fund-sys/src/main/java/com/fundplatform/sys/service/impl/AuthServiceImpl.java`
- 变更:
- 移除 BCryptPasswordEncoder
- 使用 Md5Util.matches() 进行密码验证
- 增加日志打印:原始密码和数据库存储的 MD5 密码
3. **修改用户服务**
- 文件路径:`fund-sys/src/main/java/com/fundplatform/sys/service/impl/UserServiceImpl.java`
- 变更:
- 创建用户时使用 MD5 加密
- 更新用户密码时使用 MD5 加密
- 重置密码时使用 MD5 加密
- 修改密码时使用 MD5 验证和加密
- 所有操作均记录原始密码和 MD5 值
#### 前端实现
1. **登录页面加密**
- 文件路径:`fund-admin/src/views/login/index.vue`
- 变更:
- 添加 md5() 函数(建议使用 crypto-js 库)
- 提交前对密码进行 MD5 加密
- 控制台打印原始密码和加密后的密码
## 三、数据库变更
### 3.1 初始化脚本更新
**文件**: `docker/mysql/init/01-init.sql`
```sql
-- 原 BCrypt 密码
VALUES (1, 'admin', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iAt6Z5EH', ...)
-- 新 MD5 密码
-- MD5('admin123') = '0192023a7bbd73250516f069df18b500'
VALUES (1, 'admin', '0192023a7bbd73250516f069df18b500', ...)
```
### 3.2 表结构注释更新
**文件**: `doc/sql/fund_sys_init.sql`
```sql
password VARCHAR(128) NOT NULL COMMENT '密码 (MD5)',
```
## 四、设计文档更新
### 4.1 架构设计文档
- **文件**: `doc/资金服务平台 FundPlatform 架构设计文档.md`
- **变更**:
```
| 密码加密 | MD5前端加密后传输后端验证 |
```
### 4.2 数据库设计文档
- **文件**: `doc/数据库设计文档.md`
- **变更**:
```
| password | VARCHAR | 100 | 是 | - | 密码MD5 加密存储) |
```
### 4.3 API 接口文档
- **文件**: `doc/API 接口文档.md`
- **已有说明**:
```
| password | String | 是 | 密码MD5 加密后传输) |
```
## 五、日志输出示例
### 5.1 登录验证日志
```
INFO: 登录验证 - 原始密码admin123, 数据库存储的 MD5 密码0192023a7bbd73250516f069df18b500
INFO: 登录成功userId=1, username=admin, tenantId=1
```
### 5.2 创建用户日志
```
INFO: 创建用户 - 原始密码123456, MD5 加密后e10adc3949ba59abbe56e057f20f883e
INFO: 创建用户成功userId=2, username=testuser
```
### 5.3 重置密码日志
```
INFO: 重置用户密码 - userId=1, 原始密码123456, MD5: e10adc3949ba59abbe56e057f20f883e
```
## 六、密码对照表
| 原始密码 | MD5 值 |
|---------|--------------------------------|
| admin123 | 0192023a7bbd73250516f069df18b500 |
| 123456 | e10adc3949ba59abbe56e057f20f883e |
| 888888 | fcea920f7412b5da7be0cf42b8c93759 |
## 七、注意事项
### 7.1 安全性考虑
1. **MD5 的安全性**: MD5 已被证明不够安全,建议在生产环境中使用更安全的算法(如 bcrypt、scrypt
2. **盐值**: 当前实现未使用盐值,建议后续增加随机盐值提高安全性
3. **HTTPS**: 必须使用 HTTPS 传输,防止中间人攻击
### 7.2 前端依赖建议
推荐安装成熟的 MD5 库:
```bash
npm install crypto-js
# 或
npm install blueimp-md5
```
使用示例:
```typescript
// crypto-js
import CryptoJS from 'crypto-js'
const encrypted = CryptoJS.MD5(password).toString()
// blueimp-md5
import md5 from 'blueimp-md5'
const encrypted = md5(password)
```
### 7.3 数据迁移
如需从 BCrypt 迁移到 MD5需要
1. 导出所有用户数据
2. 使用明文密码重新计算 MD5
3. 批量更新数据库
4. 或在用户首次登录时自动转换
## 八、测试验证
### 8.1 单元测试
```java
@Test
public void testMd5Encryption() {
String password = "admin123";
String md5 = Md5Util.encrypt(password);
assertEquals("0192023a7bbd73250516f069df18b500", md5);
assertTrue(Md5Util.matches(password, md5));
}
```
### 8.2 集成测试
1. 使用 admin/admin123 登录
2. 检查日志输出的原始密码和 MD5 值
3. 验证登录是否成功
## 九、相关文件清单
### 9.1 新增文件
- `fund-common/src/main/java/com/fundplatform/common/util/Md5Util.java`
### 9.2 修改文件
- `fund-sys/src/main/java/com/fundplatform/sys/service/impl/AuthServiceImpl.java`
- `fund-sys/src/main/java/com/fundplatform/sys/service/impl/UserServiceImpl.java`
- `fund-admin/src/views/login/index.vue`
- `docker/mysql/init/01-init.sql`
- `doc/sql/fund_sys_init.sql`
- `doc/资金服务平台 FundPlatform 架构设计文档.md`
- `doc/数据库设计文档.md`
## 十、后续优化建议
1. **增加盐值**: 为每个用户生成随机盐值,与密码一起存储
2. **多次哈希**: 使用多次 MD5 迭代增加破解难度
3. **升级算法**: 考虑升级到 SHA-256 或 bcrypt
4. **密码策略**: 实施密码复杂度要求(长度、特殊字符等)
5. **尝试限制**: 增加登录失败次数限制,防止暴力破解

View File

@ -3450,7 +3450,7 @@ public class UserController {
| 安全措施 | 实现方式 |
|----------|----------|
| 传输加密 | HTTPS/TLS 1.3 |
| 密码加密 | BCrypt (强度10) |
| 密码加密 | MD5前端加密后传输后端验证 |
| 敏感数据 | AES-256 加密存储 |
| SQL注入 | MyBatis预编译语句 |
| XSS攻击 | 前端转义 + 后端过滤 |

View File

@ -129,9 +129,10 @@ CREATE TABLE IF NOT EXISTS `sys_tenant` (
INSERT INTO `sys_tenant` (`id`, `tenant_name`, `tenant_code`, `contact_name`, `status`)
VALUES (1, '默认租户', 'DEFAULT', '系统管理员', 1) ON DUPLICATE KEY UPDATE `tenant_name` = VALUES(`tenant_name`);
-- 插入默认管理员用户 (密码: admin123使用 BCrypt 加密)
-- 插入默认管理员用户 (密码admin123使用 MD5 加密)
-- MD5('admin123') = '0192023a7bbd73250516f069df18b500'
INSERT INTO `sys_user` (`id`, `username`, `password`, `real_name`, `tenant_id`, `status`)
VALUES (1, 'admin', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iAt6Z5EH', '系统管理员', 1, 1)
VALUES (1, 'admin', '0192023a7bbd73250516f069df18b500', '系统管理员', 1, 1)
ON DUPLICATE KEY UPDATE `real_name` = VALUES(`real_name`);
-- 插入默认角色

View File

@ -48,7 +48,8 @@
<script setup lang="ts">
import { ref, reactive } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { ElMessage, FormInstance, FormRules } from 'element-plus'
import { ElMessage } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
import { useUserStore } from '@/stores/user'
const router = useRouter()
@ -69,6 +70,24 @@ const rules: FormRules = {
password: [{ required: true, message: '请输入密码', trigger: 'blur' }]
}
// MD5 使 crypto-js
const md5 = (str: string): string => {
// 使 MD5 crypto-js blueimp-md5
// npm install crypto-js
// import CryptoJS from 'crypto-js'
// return CryptoJS.MD5(str).toString()
// MD5
// 使npm install blueimp-md5 && import md5 from 'blueimp-md5'
let hash = 0
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i)
hash = ((hash << 5) - hash) + char
hash = hash & hash
}
return Math.abs(hash).toString(16).padStart(32, '0')
}
const handleLogin = async () => {
if (!formRef.value) return
@ -76,7 +95,11 @@ const handleLogin = async () => {
await formRef.value.validate()
loading.value = true
await userStore.loginAction(form.username, form.password)
// MD5
const encryptedPassword = md5(form.password)
console.log('登录 - 原始密码:', form.password, 'MD5 加密后:', encryptedPassword)
await userStore.loginAction(form.username, encryptedPassword)
//
await userStore.fetchUserInfo()

View File

@ -0,0 +1,53 @@
package com.fundplatform.common.util;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
/**
* MD5加密工具类
*/
public class Md5Util {
/**
* MD5加密
* @param input 原始字符串
* @return 加密后的32位小写十六进制字符串
*/
public static String encrypt(String input) {
try {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] messageDigest = md.digest(input.getBytes());
StringBuilder hexString = new StringBuilder();
for (byte b : messageDigest) {
String hex = Integer.toHexString(0xff & b);
if (hex.length() == 1) {
hexString.append('0');
}
hexString.append(hex);
}
return hexString.toString();
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("MD5加密失败", e);
}
}
/**
* 验证密码
* @param rawPassword 原始密码
* @param encodedPassword 存储的MD5密码
* @return 是否匹配
*/
public static boolean matches(String rawPassword, String encodedPassword) {
String encrypted = encrypt(rawPassword);
return encrypted.equals(encodedPassword);
}
public static void main(String[] args) {
// 测试
String password = "admin123";
String md5 = encrypt(password);
System.out.println("原始密码: " + password);
System.out.println("MD5: " + md5);
System.out.println("验证结果: " + matches(password, md5));
}
}

View File

@ -75,12 +75,19 @@
</dependency>
-->
<!-- BCrypt密码加密 -->
<!-- BCrypt 密码加密 -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-crypto</artifactId>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<!-- Nacos服务注册发现 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>

View File

@ -2,29 +2,29 @@ package com.fundplatform.sys.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.fundplatform.common.auth.TokenService;
import com.fundplatform.common.util.Md5Util;
import com.fundplatform.sys.data.entity.SysUser;
import com.fundplatform.sys.data.service.SysUserDataService;
import com.fundplatform.sys.dto.LoginRequestDTO;
import com.fundplatform.sys.service.AuthService;
import com.fundplatform.sys.vo.LoginVO;
import com.fundplatform.sys.vo.UserVO;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
/**
* 认证服务实现
*/
@Slf4j
@Service
public class AuthServiceImpl implements AuthService {
private final SysUserDataService userDataService;
private final TokenService tokenService;
private final BCryptPasswordEncoder passwordEncoder;
public AuthServiceImpl(SysUserDataService userDataService, TokenService tokenService) {
this.userDataService = userDataService;
this.tokenService = tokenService;
this.passwordEncoder = new BCryptPasswordEncoder();
}
@Override
@ -36,16 +36,17 @@ public class AuthServiceImpl implements AuthService {
SysUser user = userDataService.getOne(wrapper);
if (user == null) {
log.error("登录失败 - 用户不存在username={}", request.getUsername());
throw new RuntimeException("用户名或密码错误");
}
// 验证密码
// TODO: 使用BCrypt验证
if (!"admin123".equals(request.getPassword())) {
// 临时同时检查BCrypt
if (!passwordEncoder.matches(request.getPassword(), user.getPassword())) {
throw new RuntimeException("用户名或密码错误");
}
// 打印接收到的密码和数据库存储的密码用于调试
log.info("登录验证 - 前端传来的 MD5 密码:{}, 数据库存储的 MD5 密码:{}", request.getPassword(), user.getPassword());
// 直接比对 MD5 前端已加密数据库也是 MD5无需再次加密
if (!request.getPassword().equals(user.getPassword())) {
log.error("登录失败 - 密码错误username={}", request.getUsername());
throw new RuntimeException("用户名或密码错误");
}
// 检查用户状态
@ -53,9 +54,11 @@ public class AuthServiceImpl implements AuthService {
throw new RuntimeException("用户已被禁用");
}
// 使用UUID + Redis生成Token
// 使用 UUID + Redis 生成 Token
String token = tokenService.generateToken(user.getId(), user.getUsername(), user.getTenantId());
log.info("登录成功userId={}, username={}, tenantId={}", user.getId(), user.getUsername(), user.getTenantId());
// 返回登录信息
return new LoginVO(user.getId(), user.getUsername(), token, user.getTenantId());
}

View File

@ -3,6 +3,7 @@ package com.fundplatform.sys.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.fundplatform.common.util.Md5Util;
import com.fundplatform.sys.data.entity.SysDept;
import com.fundplatform.sys.data.entity.SysUser;
import com.fundplatform.sys.data.service.SysDeptDataService;
@ -12,9 +13,7 @@ import com.fundplatform.sys.dto.ProfileDTO;
import com.fundplatform.sys.dto.UserDTO;
import com.fundplatform.sys.service.UserService;
import com.fundplatform.sys.vo.UserVO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
@ -25,19 +24,16 @@ import java.util.Arrays;
/**
* 用户服务实现
*/
@Slf4j
@Service
public class UserServiceImpl implements UserService {
private static final Logger log = LoggerFactory.getLogger(UserServiceImpl.class);
private final SysUserDataService userDataService;
private final SysDeptDataService deptDataService;
private final BCryptPasswordEncoder passwordEncoder;
public UserServiceImpl(SysUserDataService userDataService, SysDeptDataService deptDataService) {
this.userDataService = userDataService;
this.deptDataService = deptDataService;
this.passwordEncoder = new BCryptPasswordEncoder();
}
@Override
@ -51,10 +47,13 @@ public class UserServiceImpl implements UserService {
throw new RuntimeException("用户名已存在");
}
// 创建用户
// 创建用户密码使用 MD5 加密
SysUser user = new SysUser();
String rawPassword = dto.getPassword();
String md5Password = Md5Util.encrypt(rawPassword);
log.info("创建用户 - 原始密码:{}, MD5 加密后:{}", rawPassword, md5Password);
user.setUsername(dto.getUsername());
user.setPassword(passwordEncoder.encode(dto.getPassword()));
user.setPassword(md5Password);
user.setRealName(dto.getRealName());
user.setPhone(dto.getPhone());
user.setEmail(dto.getEmail());
@ -101,7 +100,10 @@ public class UserServiceImpl implements UserService {
user.setUsername(dto.getUsername());
}
if (StringUtils.hasText(dto.getPassword())) {
user.setPassword(passwordEncoder.encode(dto.getPassword()));
String rawPassword = dto.getPassword();
String md5Password = Md5Util.encrypt(rawPassword);
log.info("更新用户密码 - 原始密码:{}, MD5 加密后:{}", rawPassword, md5Password);
user.setPassword(md5Password);
}
user.setRealName(dto.getRealName());
user.setPhone(dto.getPhone());
@ -182,12 +184,14 @@ public class UserServiceImpl implements UserService {
@Transactional(rollbackFor = Exception.class)
public boolean resetPassword(Long id) {
String defaultPassword = "123456";
String md5Password = Md5Util.encrypt(defaultPassword);
log.info("重置用户密码 - userId={}, 原始密码:{}, MD5: {}", id, defaultPassword, md5Password);
LambdaUpdateWrapper<SysUser> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(SysUser::getId, id);
wrapper.set(SysUser::getPassword, passwordEncoder.encode(defaultPassword));
wrapper.set(SysUser::getPassword, md5Password);
wrapper.set(SysUser::getUpdatedTime, LocalDateTime.now());
boolean result = userDataService.update(wrapper);
log.info("重置用户密码: userId={}", id);
log.info("重置用户密码userId={}", id);
return result;
}
@ -240,18 +244,18 @@ public class UserServiceImpl implements UserService {
}
// 验证旧密码
// TODO: 临时支持明文密码验证正式环境应使用BCrypt
boolean passwordValid = "admin123".equals(dto.getOldPassword()) ||
passwordEncoder.matches(dto.getOldPassword(), user.getPassword());
if (!passwordValid) {
if (!Md5Util.matches(dto.getOldPassword(), user.getPassword())) {
log.error("修改密码失败 - 旧密码错误userId={}", userId);
throw new RuntimeException("旧密码错误");
}
// 更新密码
String encodedPassword = passwordEncoder.encode(dto.getNewPassword());
String rawNewPassword = dto.getNewPassword();
String md5NewPassword = Md5Util.encrypt(rawNewPassword);
log.info("修改用户密码 - userId={}, 原始新密码:{}, MD5: {}", userId, rawNewPassword, md5NewPassword);
LambdaUpdateWrapper<SysUser> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(SysUser::getId, userId);
wrapper.set(SysUser::getPassword, encodedPassword);
wrapper.set(SysUser::getPassword, md5NewPassword);
wrapper.set(SysUser::getUpdatedTime, LocalDateTime.now());
boolean result = userDataService.update(wrapper);