Description or Example
# BUG修复
## 1. key重复问题
> bindingResult里面获取的校验错误, 可能会存在重复值, 即password既不存在长度也不够, 如果用stream流构建map会出现重复key的一张, 因此需要用增强for替代, 不存在才添加
# 核心代码
```java
@PostMapping("/register")
public String register(@Validated(RegisterGroup.class) AuthRegisterVO authRegisterVO,
BindingResult bindingResult,
RedirectAttributes redirectAttributes) {
Map<String, String> errors = new HashMap<>();
for (FieldError fieldError : bindingResult.getFieldErrors()) {
errors.putIfAbsent(fieldError.getField(), fieldError.getDefaultMessage());
}
if (!errors.isEmpty()) { // 普通校验出现了错误
redirectAttributes.addFlashAttribute("errors", errors);
return "redirect:http://auth.bitmall.com/reg.html";
}
// 进一步校验验证码
String phoneKey = PHONE_SMS_CODE + authRegisterVO.getPhone();
String emailKey = EMAIL_SMS_CODE + authRegisterVO.getEmail();
List<String> codes = redisTemplate.opsForValue().multiGet(Arrays.asList(phoneKey, emailKey));
List<String> res = codes.stream() // 这里有可能空指针异常
.filter(Objects::nonNull)
.map(code -> code.split("_")[0])
.filter(code -> code.equalsIgnoreCase(authRegisterVO.getCode())).collect(Collectors.toList());
// 验证码忽略大小写
if (res.isEmpty()) {
errors.put("code", "验证码错误");
redirectAttributes.addFlashAttribute("errors", errors);
return "redirect:http://auth.bitmall.com/reg.html";
}
// 都没有问题, 注册
RegisterTO registerTO = new RegisterTO();
BeanUtils.copyProperties(authRegisterVO, registerTO);
R info = authMemberService.register(registerTO);
if (info.getCode() != 0) { // 注册失败
String msg = info.getData("msg", new TypeReference<String>() {
});
errors.put("msg", msg);
redirectAttributes.addFlashAttribute("errors", errors);
return "redirect:http://auth.bitmall.com/reg.html";
}
// 注册成功, 删除验证码
redisTemplate.delete(Arrays.asList(phoneKey, emailKey));
return "redirect:http://auth.bitmall.com/login.html";
}
```
```java
@FeignClient("bitmall-member")
public interface AuthMemberService {
@RequestMapping("/member/member/register")
R register(@RequestBody RegisterTO registerTO);
}
```
```java
@RequestMapping("/register")
public R register(@RequestBody RegisterTO registerTO) {
try {
memberService.register(registerTO);
} catch (UsernameException | PhoneException | EmailException e) {
return R.error(e.getMessage());
}
return R.ok();
}
```
```java
@Override
@Cacheable(cacheNames = "member", key = "#root.methodName", sync = true)
public long getDefaultLevel() {
LambdaQueryWrapper<MemberLevelEntity> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(MemberLevelEntity::getDefaultStatus, 1);
return getOne(queryWrapper).getId();
}
```
```java
@Override
public void register(RegisterTO registerTO) {
MemberEntity memberEntity = new MemberEntity();
long defLevel = memberLevelService.getDefaultLevel();
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
String password = bCryptPasswordEncoder.encode(registerTO.getPassword());
isUsernameSignal(registerTO.getUserName()); // 用户名是否唯一
isMobilePhoneSignal(registerTO.getPhone()); // 手机是否唯一
isEmailSignal(registerTO.getEmail()); // 邮箱是否唯一
memberEntity.setUsername(registerTO.getUserName())
.setPassword(password)
.setMobile(registerTO.getPhone())
.setEmail(registerTO.getEmail())
.setLevelId(defLevel);
this.save(memberEntity); // 注册
}
private void isEmailSignal(String email) {
Integer count = this.baseMapper.selectCount(new LambdaQueryWrapper<MemberEntity>().eq(MemberEntity::getEmail, email));
if (count > 0) { // 存在对应的记录, 需要抛异常, 不能创建
throw new EmailException();
}
}
private void isMobilePhoneSignal(String phone) {
Integer count = this.baseMapper.selectCount(new LambdaQueryWrapper<MemberEntity>().eq(MemberEntity::getMobile, phone));
if (count > 0) { // 存在对应的记录, 需要抛异常, 不能创建
throw new PhoneException();
}
}
private void isUsernameSignal(String username) {
Integer count = this.baseMapper.selectCount(new LambdaQueryWrapper<MemberEntity>().eq(MemberEntity::getUsername, username));
if (count > 0) { // 存在对应的记录, 需要抛异常, 不能创建
throw new UsernameException();
}
}
```
# 要点说明
## 1. 这里不能用转发, 为什么
> 因为注册发的是post请求, 如果转发, 这个请求仍然是post请求, 而页面通过视图配置类配置, 需要GET请求, 这样转发的时候就会发生问题, 这样会导致405问题
> 而且转发还有一个问题, 即这个请求仍然是注册请求, 导致刷新页面就是注册, 用户体验很差
> 注意, 重定向的时候需要写重定向的全路径, 即需要带域名, 否则浏览器就会发送到当前项目对应的路径, 不利于负载均衡
## 2. RedirectAttributes的说明
> 为了让重定向后, 仍然可以获取到域中的数据, 就可以用上面的API往会话域放东西, addFlashAttribute添加暂时性数据, 使用一次立刻删除, 避免占用会话域
> 分布式场景下, 有一个很严重的问题, 会话域不一致问题
## 3. 注册成功删除验证码
> 由于令牌机制, 为了避免同一个验证码被重复验证, 一般验证成功了就删除验证码, 但是这样非常不划算, 因为如果注册失败, 白白浪费一个验证码, 从企业角度而言, 需要注册成功才删除验证码
> 注意: feign调用利用了JSON, 因此不要忘了@RequestBody
## 4. 获取会员等级
> 老师获取会员等级是直接从数据库里面查, 我觉得不好, 因为会员等级属于读多写少的数据, 几乎不改变, 因此, 每次都去查一次数据库效率很低, 因此我们可以将这类数据放到缓存里面, 提高效率
> 这里我只是用了过期时间, 允许一段时间不一致, 保证最终一致性即可
## 5. 判断唯一性问题
> 判断唯一性问题可以通过查询这个字段的值是否存在对应的记录, 获取个数, 如果存在则不唯一, 抛异常, 如果没有就唯一, 可以继续注册
> 这里有一个问题, 当数据量更加庞大后, 判断唯一性会耗时很久, 因此, 为了优化查询, 可以考虑添加唯一性索引
![image.png](https://cos.easydoc.net/13568421/files/llgcy87t.png)
> 异常机制: 这里的异常机制就是抛异常, 如果和预期不符就抛异常
## 6. 密码的存储
### 6.1 为什么不能用明文存储
> 如果采取明文存储, 一旦数据库被黑了, 所有的用户信息都会泄露出去, 这就完蛋了
### 6.2 为什么不能用可逆密文存储
> 可逆密文意味着可以用密文获取明文, 一旦找到了加密算法, 仍然逃不了数据泄露的命运, 和明文的后果一致
### 6.3 MD5算法的介绍
> MD5: 信息摘要算法, 本质上不是数据加密算法
1. 压缩性: 任意长度的数据, 经过MD5摘要算法后, 值的长度都是一致的
2. 容易计算: MD5摘要算法计算速度飞快, 获取MD5值很容易
3. 抗修改性: 对原数据做出修改, 哪怕是一个字节, 计算出来的MD5值都不一样
4. 强抗干扰性: 很难找出不同的两个数据的MD5值是一样的
5. 不可逆性: MD5摘要算法获取的MD5值无法获取原值
> 百度秒传利用了MD5摘要算法, 会首先计算文件的MD5值, 判断云服务器是否存在, 存在则不上传, 不存在则上传, 利用了强抗干扰性
### 6.4 为什么MD5不可逆
> 因为本质上MD5是数据摘要算法, 即将整个数据摘要出一部分获得的值, 这个值把原数据破坏了, 即只得到了一部分, 一部分不可能还原全部, 因此不可逆
### 6.5 MD5存储有什么问题?
> 因为MD5摘要算法具有强抗干扰性, 因此, 一个数据加密后的MD5值永远不变, 这样会有一个很严重的问题, 一旦使用彩虹表(类似于MD5和原数据的映射), 撞库很容易把明文撞出来, 很危险!
### 6.6 MD5盐值加密如何解决上述问题
> MD5盐值加密本质上和MD5加密没有什么不同, 仅仅在加密结果中加上一个随机数
> 通俗来说, 基于不同的盐值, 同一个明文的MD5加密的结果不同
> 这样, 如果使用了盐值加密, 破解者需要知道我们使用了什么盐值, 在这个基础上才能暴力撞库, 而盐值的获取异常困难, 即使得到了再撞库, 成本极高, 得不偿失, 因此使用MD5盐值加密
> 数据库不能存储盐值, 这样会给犯罪者减小撞库的工作量
### 6.7 bCryptPasswordEncoder的说明
#### 为什么每一次加密的结果都不同?
> 因为每一次加密都使用到了不同的盐值, 因此对同一数据每一次加密的结果都不同
#### 为什么不同的密文能匹配同一明文?
<font color="red">**前提: 统一盐值, 统一明文加密的结果相同**</font>
> 因此加密的结果内嵌了盐值, 通过特定的算法解析出盐值, 利用盐值加密即可获取明文, 因此可以匹配
## 事务问题
这里不需要事务, 因为一旦碰到异常, 就不会执行下面的保存方法