文章目录
- 1. Redis 发送并保存短信验证码
- 1. 枚举类 RedisKeyConstant
- 2. 配置key和value的序列化方式 RedisTemplateConfiguration
- 3. 发送验证码业务逻辑层 SendVerifyCodeService
- 4. 发送验证码控制层 SendVerifyCodeController
- 5. 在ms-gateway网关服务中放行发送验证码的请求
- 6. 启动项目测试发送验证码功能
- 2. 用户注册功能
- 1. 需求分析
- 2. 全局异常处理
- 3. 校验手机号是否注册
- 4. 用户注册
1. Redis 发送并保存短信验证码
将短信验证码以字符串保存到Redis,同时设置过期时间,确保跟需求一致,利用Redis不仅按需保存带有过期的验证码,而且还是进程级别的共享数据,能够保证在多个Diner微服务中读取。
1. 枚举类 RedisKeyConstant
package com.hh.commons.constant;
@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;
}
}
2. 配置key和value的序列化方式 RedisTemplateConfiguration
package com.hh.diners.config;
@Configuration
public class RedisTemplateConfiguration {
/**
* redisTemplate 序列化使用的jdkSerializeable, 存储二进制字节码, 所以自定义序列化类
*/
@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;
}
}
3. 发送验证码业务逻辑层 SendVerifyCodeService
package com.hh.diners.service;
/**
* 发送验证码业务逻辑层
*/
@Service
public class SendVerifyCodeService {
@Resource
private RedisTemplate<String, String> redisTemplate;
/**
* 发送验证码
*/
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;
}
/**
* 根据手机号获取验证码
*/
public String getCodeByPhone(String phone) {
String key = RedisKeyConstant.verify_code.getKey() + phone;
return redisTemplate.opsForValue().get(key);
}
}
4. 发送验证码控制层 SendVerifyCodeController
package com.hh.diners.controller;
/**
* 发送验证码控制层
*/
@RestController
public class SendVerifyCodeController {
@Resource
private SendVerifyCodeService sendVerifyCodeService;
@Resource
private HttpServletRequest request;
/**
* 发送验证码
*/
@GetMapping("send")
public ResultInfo send(String phone) {
sendVerifyCodeService.send(phone);
return ResultInfoUtil.buildSuccess("发送成功", request.getServletPath());
}
}
5. 在ms-gateway网关服务中放行发送验证码的请求
secure:
ignore:
urls: # 配置白名单路径
- /actuator/**
- /auth/oauth/**
- /diners/signin
- /diners/send
6. 启动项目测试发送验证码功能
redis 中存储了发送的验证码:
2. 用户注册功能
1. 需求分析
① 用户首先输入手机号,发送短信验证码,当用户输入手机号时需要校验手机号是否注册。
② 用户输入用户名,密码,手机号验证码完成注册功能
2. 全局异常处理
① 断言工具类 AssertUtil
/**
* 断言工具类
*/
public class AssertUtil {
/**
* 判断字符串非空
*/
public static void isNotEmpty(String str, String... message) {
if (StrUtil.isBlank(str)) {
execute(message);
}
}
/**
* 判断对象非空
*/
public static void isNotNull(Object obj, String... message) {
if (obj == null) {
execute(message);
}
}
/**
* 判断结果是否为真
*/
public static void isTrue(boolean isTrue, String... message) {
if (isTrue) {
execute(message);
}
}
private static void execute(String... message) {
String msg = ApiConstant.ERROR_MESSAGE;
if (message != null && message.length > 0) {
msg = message[0];
}
// 抛出 ParameterException
throw new ParameterException(msg);
}
}
② 全局异常处理 GlobalExceptionHandler
package com.hh.diners.handler;
@RestControllerAdvice // 将输出的内容写入 ResponseBody 中
@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;
}
}
3. 校验手机号是否注册
① DinersController
/**
* 食客服务控制层
*/
@RestController
@Api(tags = "食客相关接口")
public class DinersController {
@Resource
private DinersService dinersService;
/**
* 校验手机号是否已注册
*/
@GetMapping("checkPhone")
public ResultInfo checkPhone(String phone) {
dinersService.checkPhoneIsRegistered(phone);
return ResultInfoUtil.buildSuccess(request.getServletPath());
}
}
② DinersService
/**
* 食客服务业务逻辑层
*/
@Service
public class DinersService {
@Resource
private DinersMapper dinersMapper;
/**
* 校验手机号是否已注册
*/
public void checkPhoneIsRegistered(String phone) {
AssertUtil.isNotEmpty(phone, "手机号不能为空");
Diners diners = dinersMapper.selectByPhone(phone);
AssertUtil.isTrue(diners == null, "该手机号未注册");
AssertUtil.isTrue(diners.getIsValid() == 0, "该用户已锁定,请先解锁");
}
}
③ DinersMapper
/**
* 食客 Mapper
*/
public interface DinersMapper {
// 根据手机号查询食客信息
@Select("select id, username, phone, email, is_valid " +
" from t_diners where phone = #{phone}")
Diners selectByPhone(@Param("phone") String phone);
}
在ms-gateway网关服务中放行校验手机号是否注册的请求:
secure:
ignore:
urls: # 配置白名单路径
- /actuator/**
- /auth/oauth/**
- /diners/signin
- /diners/send
- /diners/checkPhone
启动项目测试:
4. 用户注册
① DinersController
@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;
}
package com.hh.diners.controller;
/**
* 食客服务控制层
*/
@RestController
@Api(tags = "食客相关接口")
public class DinersController {
@Resource
private DinersService dinersService;
/**
* 注册
*/
@PostMapping("register")
public ResultInfo register(@RequestBody DinersDTO dinersDTO) {
return dinersService.register(dinersDTO, request.getServletPath());
}
}
② DinersService
package com.hh.diners.service;
/**
* 食客服务业务逻辑层
*/
@Service
public class DinersService {
@Resource
private RestTemplate restTemplate;
@Value("${service.name.ms-oauth-server}")
private String oauthServerName;
@Resource
private OAuth2ClientConfiguration oAuth2ClientConfiguration;
@Resource
private DinersMapper dinersMapper;
@Resource
private SendVerifyCodeService sendVerifyCodeService;
/**
* 用户注册
*/
public ResultInfo register(DinersDTO dinersDTO, String path) {
// 参数非空校验
String username = dinersDTO.getUsername();
AssertUtil.isNotEmpty(username, "请输入用户名");
String password = dinersDTO.getPassword();
AssertUtil.isNotEmpty(password, "请输入密码");
String phone = dinersDTO.getPhone();
AssertUtil.isNotEmpty(phone, "请输入手机号");
// 从redis中获取验证码
String code = sendVerifyCodeService.getCodeByPhone(phone);
// 验证是否过期
AssertUtil.isNotEmpty(code, "验证码已过期,请重新发送");
// 验证码一致性校验
String verifyCode = dinersDTO.getVerifyCode();
AssertUtil.isNotEmpty(verifyCode, "请输入验证码");
AssertUtil.isTrue(!dinersDTO.getVerifyCode().equals(code), "验证码不一致,请重新输入");
// 验证用户名是否已注册
Diners diners = dinersMapper.selectByUsername(username.trim());
AssertUtil.isTrue(diners != null, "用户名已存在,请重新输入");
// 注册
// 密码加密
dinersDTO.setPassword(DigestUtil.md5Hex(password.trim()));
dinersMapper.save(dinersDTO);
// 自动登录
return signIn(username.trim(), password.trim(), path);
}
/**
* 用户登录
*
* @param account 帐号:用户名或手机或邮箱
* @param password 密码
* @param path 请求路径
*/
public ResultInfo signIn(String account, String password, String path) {
// 参数校验
AssertUtil.isNotEmpty(account, "请输入登录帐号");
AssertUtil.isNotEmpty(password, "请输入登录密码");
// 构建请求头
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
// 构建请求体(请求参数)
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
body.add("username", account);
body.add("password", password);
body.setAll(BeanUtil.beanToMap(oAuth2ClientConfiguration));
HttpEntity<MultiValueMap<String, Object>> entity = new HttpEntity<>(body, headers);
// 设置 Authorization
restTemplate.getInterceptors().add(new BasicAuthenticationInterceptor(oAuth2ClientConfiguration.getClientId(),
oAuth2ClientConfiguration.getSecret()));
// 发送请求
ResponseEntity<ResultInfo> result = restTemplate.postForEntity(oauthServerName + "oauth/token", entity, ResultInfo.class);
// 处理返回结果
AssertUtil.isTrue(result.getStatusCode() != HttpStatus.OK, "登录失败");
ResultInfo resultInfo = result.getBody();
if (resultInfo.getCode() != ApiConstant.SUCCESS_CODE) {
// 登录失败
resultInfo.setData(resultInfo.getMessage());
return resultInfo;
}
// 这里的 Data 是一个 LinkedHashMap 转成了域对象 OAuthDinerInfo
OAuthDinerInfo dinerInfo = BeanUtil.fillBeanWithMap((LinkedHashMap) resultInfo.getData(),
new OAuthDinerInfo(), false);
// 根据业务需求返回视图对象
LoginDinerInfo loginDinerInfo = new LoginDinerInfo();
loginDinerInfo.setToken(dinerInfo.getAccessToken());
loginDinerInfo.setAvatarUrl(dinerInfo.getAvatarUrl());
loginDinerInfo.setNickname(dinerInfo.getNickname());
return ResultInfoUtil.buildSuccess(path, loginDinerInfo);
}
}
③ DinersMapper
package com.hh.diners.mapper;
/**
* 食客 Mapper
*/
public interface DinersMapper {
// 根据手机号查询食客信息
@Select("select id, username, phone, email, is_valid " +
" from t_diners where phone = #{phone}")
Diners selectByPhone(@Param("phone") String phone);
// 根据用户名查询食客信息
@Select("select id, username, phone, email, is_valid " +
" from t_diners where username = #{username}")
Diners selectByUsername(@Param("username") String username);
// 新增食客信息
@Insert("insert into " +
" t_diners (username, password, phone, roles, is_valid, create_date, update_date) " +
" values (#{username}, #{password}, #{phone}, \"ROLE_USER\", 1, now(), now())")
int save(DinersDTO dinersDTO);
}
在ms-gateway网关服务中放行用户注册的请求:
secure:
ignore:
urls: # 配置白名单路径
- /actuator/**
- /auth/oauth/**
- /diners/signin
- /diners/send
- /diners/checkPhone
- /diners/register
启动项目测试:
Redis 中查看验证码 :
用户注册: