document
API test
POST

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> > 因此加密的结果内嵌了盐值, 通过特定的算法解析出盐值, 利用盐值加密即可获取明文, 因此可以匹配 ## 事务问题 这里不需要事务, 因为一旦碰到异常, 就不会执行下面的保存方法