本项目仅仅支持手机号注册

  1. 填入手机号,请求校验账号;
  2. 完善账号昵称和密码,发送验证码;
  3. 完成注册。

手机号校验

前端和后端都需要对手机号格式进行校验,同时后端也应该检查账户是否已经注册。

接口信息

路径地址 http://localhost:9000/users/phone/exists
请求方式 GET
请求参数 phone=手机号
返回结果 true|false

定义Service

找到 UserInfoService,定义检查手机号方法:

UserInfoService
public interface UserInfoService extends IService<UserInfo> {
/**
* 基于手机号查询用户对象
*
* @param phone 手机号
* @return 用户对象
*/
UserInfo findByPhone(String phone);
}

在 UserServiceImpl 中实现该方法:

UserServiceImpl
@Service
public class UserServiceImpl extends ServiceImpl<UserInfoMapper, UserInfo> implements UserInfoService {
/**
* 基于手机号查询用户对象
*
* @param phone 手机号
* @return 用户对象
*/
@Override
public UserInfo findByPhone(String phone) {
return getOne(Wrappers.<UserInfo>lambdaQuery().eq(UserInfo::getPhone, phone));
}
}

定义Controller

在 UserInfoController 下定义接口

UserInfoController
@RestController
@RequestMapping("/users")
public class UserInfoController {

private final UserInfoService userInfoService;

public UserInfoController(UserInfoService userInfoService) {
this.userInfoService = userInfoService;
}

@GetMapping("/phone/exists")
public R<Boolean> checkExists(String phone){
return R.ok(userInfoService.findByPhone(phone) != null);
}
}

验证码服务

验证码服务需要提供如下服务:

  1. 首先生成验证码

  2. 然后将验证码保存到Redis中

  3. 调用三方接口向手机号发送验证码

  4. 校验验证码是否一致

验证码存储方案:

技术方案 效率 时效性 跨服务共享
mysql 一般 支持
session 不支持
map 不支持
redis 支持

验证码存入 Redis 的数据结构

  • STRING: 直接就是 key/value 形式,操作简单,redis中外部key数量增加,会导致redis整体性能收到影响
  • MAP:一个外部key,可以保存多个内部key/value键值对,可以避免外部key占用过多
    • 单个map对象数据量过大 => 大key
    • map 的时效性只针对外部key,无法针对内部key做过期时间

采用 STRING 结构存储验证码,key需要满足唯一性/可读性/扩展性

  • USERS:REGIST:VERIFY_CODE:手机号

集成Redis

代码部分参考集成Redis部分

在模块trip-users-api的pom文件中添加Redis模块的依赖

<!-- Redis模块 -->
<dependency>
<groupId>com.swx</groupId>
<artifactId>trip-common-redis</artifactId>
</dependency>

接口信息

路径地址 http://localhost:9000/sms/register
请求方式 POST
请求参数 phone=手机号
返回结果

定义Service

新建 SmsService,定义发送短信方法:

SmsService
public interface SmsService {

/**
* 注册发送短信功能
*
* @param phone 手机号
*/
void registerSmsSend(String phone);
}

创建 SmsServiceImpl 并实现该方法:

SmsServiceImpl
@Slf4j
@Service
public class SmsServiceImpl implements SmsService {

private final RedisService redisService;

public SmsServiceImpl(RedisService redisService) {
this.redisService = redisService;
}

/**
* 注册发送短信功能
*
* @param phone 手机号
*/
@Override
public void registerSmsSend(String phone) {
// TODO 1. 验证手机合法性
// TODO 如何实现60s限制
// TODO 针对发送短信类的接口,是否需要进行限流?限流的频率设置多少合适?
// 2. 生成验证码
String code = this.generateVerifyCode("LETTER", 4);
// 3. 缓存验证码
UserRedisKeyPrefix keyPrefix = UserRedisKeyPrefix.USER_REGISTER_VERIFY_CODE_STRING;
redisService.setCacheObject(keyPrefix.fullKey(phone), code, keyPrefix.getTimeout(), keyPrefix.getUnit());
// TODO 4. 调用第三方接口,发送验证码
}

private String generateVerifyCode(String type, int len) {
String uuid = UUID.randomUUID().toString().replaceAll("-", "");
String code = uuid.substring(0, len);
log.info("[短信服务] 生成验证码 ===== type={}, len={}, code={}", type, len, code);
return code;
}
}

定义Controller

新建 SmsController

SmsController
@RestController
@RequestMapping("/sms")
public class SmsController {

private final SmsService smsService;

public SmsController(SmsService smsService) {
this.smsService = smsService;
}

@PostMapping("register")
public R<?> registerVerifyCode(String phone) {
smsService.registerSmsSend(phone);
return R.ok();
}
}

账号注册

实现步骤:

  1. 基于手机号查询是否已经存在该手机号,如果存在则返回异常
  2. 从 redis 中获取验证码与前端传入的验证码进行校验是否一致,如果不一致则抛出异常
  3. 将验证码从 redis 中删除
  4. 创建用户对象,填入参数并补充其他默认值
  5. 对密码进行加密操作
  6. 保存用户对象到数据库

接口信息

路径地址 http://localhost:9000/users/register
请求方式 POST
请求参数 phone, nickname, password, code
返回结果

请求参数

使用请求参数而非完整用户对象,可以避免接收多余参数,防止小人加入其他参数。

在 trip-users-api 模块中新建包 com.swx.user.vo,在该包下定义 RegisterRequest 类,用于接收请求参数

RegisterRequest
/**
* 用于接收注册请求传递的参数
*/
@Getter
@Setter
public class RegisterRequest {

private String phone;
private String nickname;
private String password;
private String verifyCode;
}

定义Service

找到 UserInfoService,定义注册账号方法:

UserInfoService
/**
* 注册接口
* @param req 注册请求对象
*/
void register(RegisterRequest req);

在 UserServiceImpl 中实现该方法:

UserServiceImpl
@Service
public class UserServiceImpl extends ServiceImpl<UserInfoMapper, UserInfo> implements UserInfoService {

private final RedisService redisService;

public UserServiceImpl(RedisService redisService) {
this.redisService = redisService;
}

/**
* 注册接口
*
* @param req 注册请求对象
*/
@Override
public void register(RegisterRequest req) {
// 1. 基于手机号查询是否已经存在该手机号,如果存在则返回异常
UserInfo byPhone = findByPhone(req.getPhone());
if (byPhone != null) {
throw new BizException(R.CODE_REGISTER_ERROR, "手机号已存在,请不要重复注册");
}
// 2. 从 redis 中获取验证码与前端传入的验证码进行校验是否一致,如果不一致则抛出异常
UserRedisKeyPrefix keyPrefix = UserRedisKeyPrefix.USER_REGISTER_VERIFY_CODE_STRING;
String code = redisService.getCacheObject(keyPrefix.fullKey(req.getPhone()));
if (!req.getVerifyCode().equalsIgnoreCase(code)) {
throw new BizException(R.CODE_REGISTER_ERROR, "验证码错误");
}
// 3. 将验证码从 redis 中删除
redisService.deleteObject(keyPrefix.fullKey(req.getPhone()));
// 4. 创建用户对象,填入参数并补充其他默认值
UserInfo userInfo = this.buildUserInfo(req);
// 5. 对密码进行加密操作,加盐 + 散列(hash)次数
String encryptPassword = Md5Utils.getMD5(userInfo.getPassword() + userInfo.getPhone());
userInfo.setPassword(encryptPassword);
// 6. 保存用户对象到数据库
super.save(userInfo);
}

/**
* 构建用户对象
* @param req 请求对象参数
* @return 用户对象
*/
private UserInfo buildUserInfo(RegisterRequest req) {
UserInfo userInfo = new UserInfo();
BeanUtils.copyProperties(req, userInfo);
userInfo.setInfo("这个人很懒,什么都没写");
userInfo.setHeadImgUrl("/images/default.jpg");
return userInfo;
}
}

定义Controller

在 UserInfoController 下定义接口

UserInfoController
@PostMapping("register")
public R<?> register(RegisterRequest req) {
userInfoService.register(req);
return R.ok();
}