用户微服务用户注册功能实现
用户注册之前需要先给注册的手机号发送一条验证码,我们把验证码存储在Redis中。发送的时候我们先把验证码存储到Redis,然后用户发起注册的时候取出验证。这个异常显然不够友好,接下来我们定义全局异常配置。发送验证码接口,需要配置网关放放行发送验证码接口。
用户注册之前需要先给注册的手机号发送一条验证码,我们把验证码存储在Redis中。
发送的时候我们先把验证码存储到Redis,然后用户发起注册的时候取出验证。
发送验证码
Redis配置如下:
package com.zjq.users.config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* redis配置类
* @author zjq
*/
@Configuration
public class RedisTemplateConfiguration {
/**
* redisTemplate 序列化使用的jdkSerializeable, 存储二进制字节码, 所以自定义序列化类
* @param redisConnectionFactory
* @return
*/
@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
// 使用Jackson2JsonRedisSerialize 替换默认序列化
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
// 设置key和value的序列化规则
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
新建一个service实现发送验证码功能:
- 根据手机号查询是否已生成验证码,已生成直接返回
- 没有生成则生成6位验证码
- 调用短信服务发送短信
- 发送成功,将code保存至Redis,失效时间60s
代码实现如下:
/**
* 发送验证码业务逻辑层
* @author zjq
*/
@Service
public class SendVerifyCodeService {
@Resource
private RedisTemplate<String, String> redisTemplate;
/**
* 发送验证码
*
* @param phone
*/
public void send(String phone) {
// 检查非空
AssertUtil.isNotEmpty(phone, "手机号不能为空");
// 根据手机号查询是否已生成验证码,已生成直接返回
if (!checkCodeIsExpired(phone)) {
return;
}
// 生成 6 位验证码
String code = RandomUtil.randomNumbers(6);
// 调用短信服务发送短信
// 发送成功,将 code 保存至 Redis,失效时间 60s
String key = RedisKeyConstant.verify_code.getKey() + phone;
redisTemplate.opsForValue().set(key, code, 60, TimeUnit.SECONDS);
}
/**
* 根据手机号查询是否已生成验证码
*
* @param phone
* @return
*/
private boolean checkCodeIsExpired(String phone) {
String key = RedisKeyConstant.verify_code.getKey() + phone;
String code = redisTemplate.opsForValue().get(key);
return StrUtil.isBlank(code) ? true : false;
}
/**
* 根据手机号获取验证码
*
* @param phone
* @return
*/
public String getCodeByPhone(String phone) {
String key = RedisKeyConstant.verify_code.getKey() + phone;
return redisTemplate.opsForValue().get(key);
}
}
使用到的枚举类RedisKeyConstant
:
@Getter
public enum RedisKeyConstant {
verify_code("verify_code:", "验证码");
private String key;
private String desc;
RedisKeyConstant(String key, String desc) {
this.key = key;
this.desc = desc;
}
}
发送验证码的控制层代码如下:
/**
* 发送验证码控制层
* @author zjq
*/
@RestController
public class SendVerifyCodeController {
@Resource
private SendVerifyCodeService sendVerifyCodeService;
@Resource
private HttpServletRequest request;
/**
* 发送验证码
*
* @param phone
* @return
*/
@GetMapping("send")
public ResultInfo send(String phone) {
sendVerifyCodeService.send(phone);
return ResultInfoUtil.buildSuccess("发送成功", request.getServletPath());
}
}
发送验证码接口,需要配置网关放放行发送验证码接口/users/send
,配置如下:
secure:
ignore:
urls: # 配置白名单路径
- /actuator/**
- /auth/oauth/**
- /users/signin
- /users/send
验证发送:
在Redis中也可以查看到该手机号发送的验证码信息:
接下来继续走用户注册流程…
用户注册
校验手机号是否已注册或者不是可用状态
在mapper中新建一个通过手机号查询用户的方法:
/**
* 根据手机号查询用户信息
* @param phone
* @return
*/
@Select("select id, username, phone, email, is_valid " +
" from t_users where phone = #{phone}")
Users selectByPhone(@Param("phone") String phone);
在service中创建校验手机号相关方法:
/**
* 校验手机号是否已注册
*/
public void checkPhoneIsRegistered(String phone) {
AssertUtil.isNotEmpty(phone, "手机号不能为空");
Users diners = usersMapper.selectByPhone(phone);
AssertUtil.isTrue(diners == null, "该手机号未注册");
AssertUtil.isTrue(diners.getIsValid() == 0, "该用户已锁定,请先解锁");
}
控制层创建相关接口,提供验证:
/**
* 校验手机号是否已注册
*
* @param phone
* @return
*/
@GetMapping("checkPhone")
public ResultInfo checkPhone(String phone) {
userService.checkPhoneIsRegistered(phone);
return ResultInfoUtil.buildSuccess(request.getServletPath());
}
网关同样需要配置放行该接口:
secure:
ignore:
urls: # 配置白名单路径
- /actuator/**
- /auth/oauth/**
- /users/signin
- /users/send
- /users/checkPhone
此时数据库信息如下:
测试验证:
已存在的手机号:
不存在的手机号:
这个异常显然不够友好,接下来我们定义全局异常配置。
全局异常配置
添加全局异常处理类,代码如下:
/**
* 全局异常处理类
* @author zjq
*/
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
@Resource
private HttpServletRequest request;
@ExceptionHandler(ParameterException.class)
public ResultInfo<Map<String, String>> handlerParameterException(ParameterException ex) {
String path = request.getRequestURI();
ResultInfo<Map<String, String>> resultInfo =
ResultInfoUtil.buildError(ex.getErrorCode(), ex.getMessage(), path);
return resultInfo;
}
@ExceptionHandler(Exception.class)
public ResultInfo<Map<String, String>> handlerException(Exception ex) {
log.info("未知异常:{}", ex);
String path = request.getRequestURI();
ResultInfo<Map<String, String>> resultInfo =
ResultInfoUtil.buildError(path);
return resultInfo;
}
}
再次请求不存在的手机号,或者已经被锁定的手机号,返回如下:
查看用户名是否已经注册
在mapper中添加根据用户名查询用户:
/**
* 根据用户名查询用户信息
* @param username
* @return
*/
@Select("select id, username, phone, email, is_valid " +
" from t_users where username = #{username}")
Users selectByUsername(@Param("username") String username);
用户注册验证都通过后需要把新用户添加到数据库,mapper中添加用户新增信息:
/**
* 新增用户信息
* @param userDTO
* @return
*/
@Insert("insert into " +
" t_users (username, password, phone, roles, is_valid, create_date, update_date) " +
" values (#{username}, #{password}, #{phone}, \"ROLE_USER\", 1, now(), now())")
int saveUser(UserDTO userDTO);
UserDTO内容如下:
package com.imooc.commons.model.dto;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Getter;
import lombok.Setter;
import java.io.Serializable;
@Getter
@Setter
@ApiModel(description = "注册用户信息")
public class DinersDTO implements Serializable {
@ApiModelProperty("用户名")
private String username;
@ApiModelProperty("密码")
private String password;
@ApiModelProperty("手机号")
private String phone;
@ApiModelProperty("验证码")
private String verifyCode;
}
用户注册逻辑实现
用户注册步骤如下:
- 参数非空校验
- 验证码一致性校验
- 验证用户名是否已注册
- 注册
- 密码加密
- 自动登录
代码实现如下:
/**
* 用户注册
*
* @param userDTO
* @param path
* @return
*/
public ResultInfo register(UserDTO userDTO, String path) {
// 参数非空校验
String username = userDTO.getUsername();
AssertUtil.isNotEmpty(username, "请输入用户名");
String password = userDTO.getPassword();
AssertUtil.isNotEmpty(password, "请输入密码");
String phone = userDTO.getPhone();
AssertUtil.isNotEmpty(phone, "请输入手机号");
String verifyCode = userDTO.getVerifyCode();
AssertUtil.isNotEmpty(verifyCode, "请输入验证码");
// 获取验证码
String code = sendVerifyCodeService.getCodeByPhone(phone);
// 验证是否过期
AssertUtil.isNotEmpty(code, "验证码已过期,请重新发送");
// 验证码一致性校验
AssertUtil.isTrue(!userDTO.getVerifyCode().equals(code), "验证码不一致,请重新输入");
// 验证用户名是否已注册
Users users = usersMapper.selectByUsername(username.trim());
AssertUtil.isTrue(users != null, "用户名已存在,请重新输入");
// 注册
// 密码加密
userDTO.setPassword(DigestUtil.md5Hex(password.trim()));
usersMapper.saveUser(userDTO);
// 自动登录
return signIn(username.trim(), password.trim(), path);
}
控制层代码如下:
/**
* 注册
*
* @param userDTO
* @return
*/
@PostMapping("register")
public ResultInfo register(@RequestBody UserDTO userDTO) {
return userService.register(userDTO, request.getServletPath());
}
验证
新用户发起注册。
校验手机号是否已注册:
发送验证码:
执行注册操作:
可以看到验证码为 807596:
第一次故意等待验证码失效再执行,返回如下:
然后重新发送验证码:
再次输入错误验证码,返回如下:
输入正确的,返回了自动登录的token信息:
本文内容到此结束了,
如有收获欢迎点赞👍收藏💖关注✔️,您的鼓励是我最大的动力。
如有错误❌疑问💬欢迎各位指出。
主页:共饮一杯无的博客汇总👨💻保持热爱,奔赴下一场山海。🏃🏃🏃
更多推荐
所有评论(0)