在之前的文章中写了如何发送email验证码和短信验证码,以及接口防刷。那么用户输入完验证码,点击注册后,会触发注册逻辑。
流程说明:页面点击发送验证码会调用权限认证系统,然后权限认证系统会远程调用邮件和短信服务系统。然后用户输入验证码后点击注册,会调用权限系统校验验证码,校验通过后,权限系统调用会员系统进行注册。
首先是编写权限服务中相关接口
一、(权限服务)定义注册的vo实体类(采用jsr303校验):
package com.***.***.auth.vo;
import lombok.Data;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Pattern;
/**
* @author guanghaocheng
* @version 1.0
* 翼以尘雾之微补益山海,荧烛末光增辉日月
* @date 2021/7/3 15:11
* 用户注册的vo
*/
@Data
public class UserRegistVo {
@NotEmpty(message = "用户名必须提交")
// @Length(min = 6,max = 18,message = "用户名必须是6-18位")
private String userName;
@NotEmpty(message = "密码不能为空")
private String password;
@Pattern(regexp = "^[1][3-9][0-9]{9}$]",message = "电话格式不正确")
private String phone;
@Pattern(regexp = "^([a-z0-9A-Z]+[-|\\\\.]?)+[a-z0-9A-Z]@([a-z0-9A-Z]+(-[a-z0-9A-Z]+)?\\\\.)+[a-zA-Z]{2,}$",message = "邮箱格式不正确")
private String mail;
@NotEmpty(message = "验证码必须填写")
private String code;
}
二、(权限服务)编写fegin接口,去调用会员服务的注册功能
@FeignClient("**-member")
public interface MemberFeignService {
@PostMapping("/member/member/regist")
public R regist(@RequestBody UserRegistVo vo);
}
三、(权限服务)校验能否注册的controller:
1、先判定jsr303校验是否通过,通过之后从redis中确认手机验证码是否正确,一致则删除验证码,(令牌机制),然后调用会员服务去真正注册。
2、会员服务调用成功后,重定向至登录页(防止表单重复提交),否则封装远程服务返回的错误信息返回至注册页面。
3、重定向的请求数据,可以利用RedirectAttributes参数转发。因为RedirectAttributes可以通过session保存信息并在重定向的时候携带过去,所以后期我们需要解决分布式的session问题。
4、重定向时,如果不指定host,就直接显示了注册服务的ip,所以我们重定义写http://
注:这里所用的返回值R,是封装的通用返回类,如果感兴趣的可以也使用这个R类,文章链接在这:https://blog.csdn.net/qq_42969135/article/details/111657050。使用自己系统定义的就可以。
注:JSR303校验怎么用:JSR303校验的结果,被封装到BindingResult,再结合BindingResult.getFieldErrors()方法获取错误信息,有错误就重定向至注册页面
@PostMapping("/regist")
public String regist(@Valid UserRegistVo userRegistVo, BindingResult bindingResult, RedirectAttributes model){
//前置校验 采用jsr303方式校验
if(bindingResult.hasErrors()){
//校验出错,转发到注册页
Map<String,String> errors = new HashMap<String, String>();
bindingResult.getFieldErrors().stream().map(fieldError -> {
String field = fieldError.getField();
String defaultMessage = fieldError.getDefaultMessage();
errors.put(field,defaultMessage);
return errors;
});
model.addFlashAttribute("erros",errors);
return "redirect:http://auth.***.com/register.html";
}
//前置校验通过,开始校验验证码是否正确
String code = userRegistVo.getCode();
if(!StringUtils.isEmpty(userRegistVo.getPhone())){//走手机验证码校验
String s = redisTemplate.opsForValue().get(AuthServerConstant.MAIL_CODE_CACHE_PREFIX + userRegistVo.getPhone());
if(StringUtils.isEmpty(s)){//redis中的验证码为空
Map<String,String> errors = new HashMap<>();
errors.put("code","验证码失效");
model.addFlashAttribute("errors",errors);
return "redirect:http://auth.***.com/register.html";
}else{//校验验证码是否一致
if(code.equals(s.split("_")[0])){
//只要验证码校验成功过一次后,就需要删除验证码(令牌机制)
redisTemplate.delete(AuthServerConstant.MAIL_CODE_CACHE_PREFIX + userRegistVo.getPhone());
//验证码通过--开始调用远程服务真正进行注册
R r = memberFeignService.regist(userRegistVo);
if(r.getCode() == 0){
//成功
return "redirect:http://auth.***.com/login.html";
}else{
Map<String,String> errors = new HashMap<>();
errors.put("msg",r.getData("msg",new TypeReference<String>(){}));
return "redirect:http://auth.***.com/register.html";
}
}else{
Map<String,String> errors = new HashMap<>();
errors.put("code","验证码错误");
model.addFlashAttribute("errors",errors);
return "redirect:http://auth.***.com/register.html";
}
}
}else if(!StringUtils.isEmpty(userRegistVo.getMail())){//走邮箱验证
String v = redisTemplate.opsForValue().get(AuthServerConstant.MAIL_CODE_CACHE_PREFIX + userRegistVo.getMail());
if(StringUtils.isEmpty(v)){//缓存中的验证码为空
Map<String,String> errors = new HashMap<>();
errors.put("code","验证码失效");
model.addFlashAttribute("errors",errors);
return "redirect:http://auth.***.com/register.html";
}else{
if(code.equals(v.split("_")[0])){
//删除验证码(令牌机制)
redisTemplate.delete(AuthServerConstant.MAIL_CODE_CACHE_PREFIX + userRegistVo.getMail());
//验证码通过,调用远程服务真正进行注册
R r = memberFeignService.regist(userRegistVo);
if(r.getCode() == 0){
//成功
return "redirect:http://auth.***.com/login.html";
}else{
Map<String,String> errors = new HashMap<>();
errors.put("msg",r.getData("msg",new TypeReference<String>(){}));
return "redirect:http://auth.***.com/register.html";
}
}else{//验证码不一致
Map<String,String> errors = new HashMap<>();
errors.put("code","验证码错误");
model.addFlashAttribute("errors",errors);
return "redirect:http://auth.***.com/register.html";
}
}
}else{
Map<String,String> errors = new HashMap<>();
errors.put("code","越过了JSR303校验的非法请求");
model.addFlashAttribute("errors",errors);
return "redirect:http://auth.***.com/register.html";
}
// return "redirect:/login.html";
}
此处之后是会员服务相关接口编写,也就是真正的注册逻辑相关内容
三、在会员服务中编写vo,用于接受上方权限服务远程调用传递过来的数据
package com.atguigu.***.member.vo;
import lombok.Data;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Pattern;
/**
* @author guanghaocheng
* @version 1.0
* 翼以尘雾之微补益山海,荧烛末光增辉日月
* @date 2021/7/6 20:01
*/
@Data
public class MemberRegistVo {
@NotEmpty(message = "用户名必须提交")
// @Length(min = 6,max = 18,message = "用户名必须是6-18位")
private String userName;
@NotEmpty(message = "密码不能为空")
private String password;
@Pattern(regexp = "^[1][3-9][0-9]{9}$]",message = "电话格式不正确")
private String phone;
@Pattern(regexp = "^([a-z0-9A-Z]+[-|\\\\.]?)+[a-z0-9A-Z]@([a-z0-9A-Z]+(-[a-z0-9A-Z]+)?\\\\.)+[a-zA-Z]{2,}$",message = "邮箱格式不正确")
private String mail;
}
四、异常机制编写
本项目使用的是异常机制,用于在注册前检验用户名、手机号、邮箱是否唯一。不唯一就抛异常,由controller中try住异常,去根据不同异常,给不同错误的返回值。
每个自定义异常都属于运行时异常,因此继承RuntimeException
三个异常的代码如下:
package com.****.**.member.exception;
/**
* @author guanghaocheng
* @version 1.0
* 翼以尘雾之微补益山海,荧烛末光增辉日月
* @date 2021/7/6 20:53
*/
public class EmailExsitException extends RuntimeException {
public EmailExsitException() {
super("邮箱已存在");
}
}
package com.*.*.member.exception;
/**
* @author guanghaocheng
* @version 1.0
* 翼以尘雾之微补益山海,荧烛末光增辉日月
* @date 2021/7/6 20:53
*/
public class PhoneExsitException extends RuntimeException {
//自定义异常都应该是运行时异常,也就是继承RuntimeException
public PhoneExsitException() {//使用alt+inster键(idea),生成RuntimeException的构造器
super("手机号已存在");
}
}
package com.*.*.member.exception;
/**
* @author guanghaocheng
* @version 1.0
* 翼以尘雾之微补益山海,荧烛末光增辉日月
* @date 2021/7/6 20:53
*/
public class UserNameExsitException extends RuntimeException {
public UserNameExsitException() {
super("用户名已存在");
}
}
五、(会员服务)从此处开始编写注册的代码(也就是上方controller远程调用的服务中的注册功能)
备注:
1、上方(一、二)功能在权限认证系统中。
2、流程是:页面点击发送验证码会调用权限认证系统,然后权限认证系统会远程调用邮件和短信服务系统。然后用户输入验证码后点击注册,会调用权限系统校验验证码,校验通过后,权限系统调用会员系统进行注册。
3、此处开始是会员服务,也就是被权限系统远程调用的系统,去真正进行注册。
通过异常机制判断当前注册会员名和电话号码电子邮箱是否已经注册,如果已经注册,则抛出对应的自定义异常,并在返回时封装对应的错误信息。
如果没有注册,则封装传递过来的会员信息,并设置默认的会员等级、创建时间。
会员服务注册的controller:
@PostMapping("/regist")
public R regist(@RequestBody MemberRegistVo vo){
try{//因为我们使用了异常机制去检查用户名、手机号、邮箱是否重复,并且throw出来了。因此此处需要try catch一下
memberService.regist(vo);
}catch (PhoneExsitException e){
return R.error(BizCodeEnume.PHONE_EXIST_EXCEPTION.getCode(),BizCodeEnume.PHONE_EXIST_EXCEPTION.getMsg());
}catch (EmailExsitException e){
return R.error(BizCodeEnume.EMAIL_EXIST_EXCEPTION.getCode(),BizCodeEnume.EMAIL_EXIST_EXCEPTION.getMsg());
}catch (UserNameExsitException e){
return R.error(BizCodeEnume.USER_EXIST_EXCEPTION.getCode(),BizCodeEnume.USER_EXIST_EXCEPTION.getMsg());
}catch (Exception e){
// System.out.println("未知异常:"+ e);
log.error("未知异常"+e);
}
return R.ok();
}
service如下:
void regist(MemberRegistVo vo);
void checkPhoneUnique(String phone) throws PhoneExistException;
void checkUserNameUnique(String userName) throws UserNameExistException;
impl如下:
@Override
public void regist(MemberRegistVo vo) {
MemberDao memberDao = this.baseMapper;
MemberEntity entity = new MemberEntity();
//设置默认等级
MemberLevelEntity levelEntity = memberLevelDao.getDefaultLevel();
entity.setLevelId(levelEntity.getId());
//检查用户名手机号电子邮箱是否唯一。为了让controller感知异常,此处采用异常机制
if(!StringUtils.isEmpty(vo.getPhone())){
checkPhoneUnique(vo.getPhone());
}
if(!StringUtils.isEmpty(vo.getMail())){
checkEmailUnique(vo.getMail());
}
checkUserNameUnique(vo.getUserName());
//确定都是唯一的,放入到实体类中
entity.setMobile(vo.getPhone());
entity.setEmail(vo.getMail());
entity.setUsername(vo.getUserName());
//保存密码,密码要进行加密存储 spring提供的密码自动加密算法,匹配时用match方法匹配就行了。就不用向数据库中存sign盐值加密了。
//这个加密方法非常安全,具体为什么安全请看:https://www.cnblogs.com/jpfss/p/11023906.html
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
String encode = passwordEncoder.encode(vo.getPassword());
entity.setPassword(encode);
//如有其他默认信息设置,请设置。
//保存
memberDao.insert(entity);
}
@Override
public void checkEmailUnique(String email) throws EmailExsitException{
MemberDao baseMapper = this.baseMapper;
Integer email1 = baseMapper.selectCount(new QueryWrapper<MemberEntity>().eq("email", email));
if(email1 > 0){
throw new EmailExsitException();
}
}
@Override
public void checkPhoneUnique(String phone) throws PhoneExsitException{
MemberDao baseMapper = this.baseMapper;
Integer mobile = baseMapper.selectCount(new QueryWrapper<MemberEntity>().eq("mobile", phone));
if(mobile > 0){
throw new PhoneExsitException();
}
}
@Override
public void checkUserNameUnique(String userName) throws UserNameExsitException{
MemberDao baseMapper = this.baseMapper;
Integer username = baseMapper.selectCount(new QueryWrapper<MemberEntity>().eq("username", userName));
if(username > 0){
throw new UserNameExsitException();
}
}
至此,大功告成!企业级系统的注册功能完整流程和代码实现已经全部完毕了。
如果想看前端如何实现的验证码倒计时或者想看如何发送邮件和短信验证码的,欢迎在本专栏查看,文章都写好了,都是经过实战检验的。
一起加油吧,少年!