实现流程
-
发送短信验证码,并将验证码保存在redis。
-
根据用户登录数据完成登录。
- 如果是新用户还要创建用户。
- 如果查询到则转化为DTO保存。
将用户数据被保存到redis,并将token保存到redis,同时返回给前端。
-
创建登录校验拦截器。
注册登录校验拦截器。
发送验证码接口
请求参数:手机号phone
无结果对象
service层实现
java
@Override
public Result sendCode(String phone) {
//1.校验手机号
if (RegexUtils.isPhoneInvalid(phone)) {
return Result.fail("手机号格式错误!");
}
//2.生成验证码
String code = RandomUtil.randomNumbers(6);
//3.将验证码保存在redis
stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES);
//4.模拟发送验证码
log.debug("发送短信验证码成功,验证码:{}", code);
return Result.ok();
}
接口介绍
- 根据正则表达式检验手机号格式。
- 通过hutool生成6位随机数作为验证码。
- 将验证码保存在redis中。
- 将验证码发送给客户端(此处应使用相关云服务完成,笔者使用日志只为记录数据)。
登录接口
请求参数:已经封装好的LoginFormDTO对象
结果对象:token
service层实现
java
@Override
public Result login(LoginFormDTO loginForm) {
//1.校验手机号格式
String phone = loginForm.getPhone();
if (RegexUtils.isPhoneInvalid(phone)) {
return Result.fail("手机号格式错误!");
}
//2.校验验证码
String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
String code = loginForm.getCode();
if (cacheCode == null || !cacheCode.equals(code)) {
return Result.fail("验证码错误!");
}
//3.根据手机号查询用户
User user = query().eq("phone", phone).one();
//4.判断用户是否存在
if (user == null) {
//5.没有用户则创建新用户
user = createUserWithPhone(phone);
}
//6.保存用户信息到redis,(以哈希储存)
//1.通过UUID拼装tokenKey
String token = UUID.randomUUID().toString(true);
String tokenKey = LOGIN_USER_KEY + token;
//2.将UserDTO对象转为HashMap存储, 由于使用的是stringRedisTemplate,要保证所有的值都是字符串类型,要把lang转换为string
UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),
CopyOptions
.create()//创建默认数据拷贝选项
.setIgnoreNullValue(true) //忽略null值
/*字段值修改器,接收实现修改的方法*/
.setFieldValueEditor(
(fieldName, fieldValue) -> fieldValue.toString()
)
);
stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);
//3.设置token的有效期
stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.SECONDS);
//7.返回token
return Result.ok(tokenKey);
}
接口介绍
- 校验手机号格式。
- 校验验证码。
- 根据手机号查询用户信息(使用MybatisPlus)。
- 通过UUID生成随机token,所为key,将查询后得到的UserDTO(将User转换为UserDTO,避免暴漏敏感数据 ),封装为hash(使用hutool相关API),存入redis,并设置有效期。
- 返回token。
手动创建拦截器
在实现了HandlerInterceptor的类中重写preHandle和afterCompletion来实现对每一次请求的处理。
java
public class LoginInInterceptor implements HandlerInterceptor {
private StringRedisTemplate stringRedisTemplate;
public LoginInInterceptor(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//获取请求头中的token
String token = request.getHeader("authorization");
if (token == null) {
response.setStatus(401);
return false;
}
//根据token从redis中获取用户信息
String key = LOGIN_CODE_KEY + token;
Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);
if (userMap.isEmpty()) {
response.setStatus(401);
return false;
}
//将map转换为UserDTO对象
UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
//保存用户信息到ThreadLocal
UserHolder.saveUser(userDTO);
//刷新token有效期
stringRedisTemplate.expire(key, LOGIN_USER_TTL, TimeUnit.SECONDS);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {
UserHolder.removeUser();
}
}
用前注意:
该自定义拦截器未交给Spring容器管理,不能使用@Autowired或者@Resource自动注入依赖,此处使用构造器注入stringRedisTemplate。
执行逻辑:
preHandle执行拦截校验
- getHeader获取请求头中的token,并进行非空判断。
- 根据token从redis中获取userMap,再次使用hutool包的API将map集合转换为实体类。
- 使用工具类将userDTO保存到ThreadLocal,实现跨层级数据共享。
- 刷新token有效期,提升用户体验。
afterCompletion执行清理ThreadLocal
请求完成后清理ThreadLocal中的数据,防止内存泄漏和数据污染。
注册拦截器
在含有@Configuration 且实现了WebMvcConfigurer 的配置类中添加自定义拦截器
java
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInInterceptor(stringRedisTemplate))
.excludePathPatterns(
"/user/code",
"/user/login",
"/blog/hot",
"/shop/**",
"/shop-type/**",
"/voucher/**",
"/upload/**"
);
}
}
执行逻辑:
- 使用@Resource注解注入stringRedisTemplate
- 重写addInterceptors方法
- 调用registry的addInterceptor方法添加拦截器(形参中new出LoginInInterceptor,并将正确注入的stringRedisTemplate传递给LoginInInterceptor,完成自定义拦截器中的依赖注入)。
- 调用excludePathPatterns指明不拦截的请求