首先我们要思考:数据库密码为什么要加密?数据库密码如何加密?
为什么要加密
很多朋友在实现用户注册登录功能时,往往只是把用户注册的账号与密码直接存储在数据库中,在登录时直接把数据库中的账号与密码比对来进行校验。其实这样存储会有不少隐患。明文存储数据库密码意味着将用户密码直接以普通文本的形式存储在数据库中。这意味着任何人只要能够访问数据库,就能够轻易地获得你的密码。
下面是为什么数据库密码加密如此重要的几个理由:
- 防止明文泄露:数据库密码加密将密码转化为密文形式,即使黑客获得了数据库的访问权限,也无法直接获取真正的密码。这道"屏障"有效地保护了我们的数据免受外界的入侵。
- 保护用户隐私:用户的个人信息和敏感数据是我们应该高度保护的。加密数据库密码可以确保用户的隐私得到有效保护,从而增加用户对我们系统的信任。
- 合规要求的满足:在现代法规和合规要求中,对于个人隐私和敏感数据的保护有着严格的要求。加密数据库密码符合这些要求,可以让我们在数据安全方面更加合规。
数据库密码如何加密
数据库加密方法有很多,例如哈希加密,盐加密,对称加密,非对称加密等,每个加密方法都有各自的优缺点,今天我们来介绍一下盐加密。
什么是盐加密?
盐加密是一种在密码存储过程中增加额外随机字符串(盐)的加密方法。它被称为盐加密,是因为盐的作用类似于烹饪中的盐,可以增加密码的复杂性和安全性。
盐加密的基本原理是将明文密码与盐值进行字符串拼接后再进行加密。这个盐值是随机生成的,并且存储在数据库中。通过将盐值添加到明文密码中,可以使得每个用户的密码都存在差异性,即使两个用户的密码相同,由于使用了不同的盐值,加密后的结果也会不同。
为什么要使用盐加密?
使用盐加密的好处有几个:
- 阻止彩虹表攻击:彩虹表是一种通过预先计算哈希值以及对应的明文密码的表,用于快速破解哈希值的技术。通过使用盐加密,即使两个用户的密码相同,由于盐值的不同,加密后的结果也会不同,从而增加了彩虹表攻击的难度。
- 提高密码复杂性:通过添加盐值,密码的实际长度增加了,从而使加密后的密码更复杂。这使得攻击者更难以使用暴力破解等方法破解密码。
- 提高安全性:使用盐加密可以有效减少密码的碰撞风险。即使多个用户的密码相同,在加密过程中每个用户的盐值都是不同的,因此加密后的结果也会不同,提高了数据的安全性。
需要注意的是,盐值应该是随机和足够复杂的字符串,并且应该作为附加数据存储在安全的位置,以确保加密的安全性。同时,为每个用户生成独一无二的盐值是非常重要的,以免降低盐加密的安全性。
Java 后端加密实现
整体实现思路如下
技术选择:
- jdk8
- mysql8
- SpringBoot 2.7.2
- mybatis plus
- maven依赖
xml
<dependencies>
<!-- mybatis-plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.2</version>
</dependency>
<!-- knife4j -->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi2-spring-boot-starter</artifactId>
<version>4.3.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.8</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
- application.yml
yaml
# 开发模式下配置
spring:
application:
name: springboot-init
# 默认 dev 环境
profiles:
active: dev
# 支持 swagger3
mvc:
pathmatch:
matching-strategy: ant_path_matcher
# mysql 数据库配置
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/你的数据表名
username: root
password: 数据库密码
mybatis-plus:
configuration:
map-underscore-to-camel-case: false
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
logic-delete-field: isDelete # 全局逻辑删除的实体字段名
logic-delete-value: 1 # 逻辑已删除值(默认为 1)
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
- knife 接口文档配置
less
/**
* Knief4j Api文档配置
* 默认访问 <a href="http://localhost:8080/doc.html"/>
*/
@Configuration
@EnableSwagger2WebMvc
@Profile({"dev"})
public class Knife4jConfig {
@Bean
public Docket createApiDoc() {
return new Docket(DocumentationType.SWAGGER_2).apiInfo(buildApiInfo()).select()
// Controller 扫描包路径
.apis(RequestHandlerSelectors.basePackage("controller路径")).paths(PathSelectors.any()).build();
}
private ApiInfo buildApiInfo() {
return new ApiInfoBuilder().title("项目名").description("项目描述").termsOfServiceUrl("你的服务url").contact(new Contact("你的用户名", "https://github.com/limincai", "你的邮箱")).version("版本号").build();
}
}
- 创建 controller 层丶service 层以及 serviceImpl
less
/**
* 用户接口
*/
@RestController
@RequestMapping("/user")
@Slf4j
public class UserController {
@Resource
UserService userService;
}
csharp
public interface UserService extends IService<User> {
}
scala
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
}
层级关系如下图所示
- 在 UserController 中写一个注册接口用于用户注册,封装用户注册请求对象,并对用户传参进行简单的校验.
arduino
/**
* 用户注册请求类
*/
@Data
public class UserRegisterRequest implements Serializable {
String userAccount;
String userPassword;
String confirmedPassword;
private static final long serialVersionUID = 1L;
}
typescript
/**
* 用户注册接口
*
* @param userRegisterRequest 用户注册封装类
* @return 成功注册后的 id
*/
@PostMapping("/register")
public Response<Long> userRegister(@RequestBody UserRegisterRequest userRegisterRequest) {
if (userRegisterRequest == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
String userAccount = userRegisterRequest.getUserAccount();
String userPassword = userRegisterRequest.getUserPassword();
String confirmedPassword = userRegisterRequest.getConfirmedPassword();
// 不能为空
if (StringUtils.isAnyBlank(userAccount, userPassword, confirmedPassword)) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "账号、密码、确认密码不能为空");
}
return Response.ok(userService.userRegister(userAccount, userPassword, confirmedPassword));
}
- 在 UserService 中写 userRegister 方法
javascript
/**
* 用户注册
*
* @param userAccount 注册账号
* @param userPassword 注册密码
* @param confirmedPassword 确认密码
* @return 用户成功注册后的 id
*/
long userRegister(String userAccount, String userPassword, String confirmedPassword);
- 在 UserServiceImpl 中实现方法,并实现加密
scss
@Override
public long userRegister(String userAccount, String userPassword, String confirmedPassword) {
// 1. 参数校验
// 账号不能包含特殊字符
if (RegUtil.hasSpecialChar(userAccount)) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "账号不能包含特殊字符");
}
// 密码与确认密码不一致
if (!userPassword.equals(confirmedPassword)) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "密码不能与确认密码一致");
}
// 6 < 账号长度 < 16
if (userAccount.length() < 4 || userAccount.length() > 16) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户账号必须大于6个字符,少于16个字符");
}
// 8 < 用户密码 < 16
if (userPassword.length() < 8 || userPassword.length() > 16) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户密码必须大于8个字符,少于16个字符");
}
// 2. 账号不能重复
QueryWrapper<User> userQueryWrapper = new QueryWrapper<>();
userQueryWrapper.eq("userAccount", userAccount);
long count = this.count(userQueryWrapper);
if (count > 0) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "账号已存在");
}
// 3. 用户密码加密
String encryptedPassword = DigestUtils.md5DigestAsHex((UserConstant.USER_REGISTER_SALT + userPassword).getBytes());
// 4. 插入数据到数据库
User user = new User();
user.setUserAccount(userAccount);
user.setUserPassword(encryptedPassword);
boolean result = this.save(user);
if (!result) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR);
}
return user.getId();
}
其中 DigestUtils.md5DigestAsHex(InputStream inputStream) 是SpringBoot 自带的加密工具类,需要将盐值与密码进行混合最后得到加密后的密码
测试
因为我们引入了 Knife4j 在线接口文档,我们可以很轻松的调试后端接口,在浏览器中访问: http://localhost:8080/doc.html
找到 userRegister 接口,填写要输入的参数,最后发送即可
我们来看一下数据库,可以看到,数据库中的密码是经过加密后存储的密码,大功告成!