用户调用sendCode()接口,把phone传到后端,后端对phone进行格式校验,如果通过校验,则生成6位数验证码,并保存到redis中,phone为key,code为value,注意设置有效期
/**
* 基于Redis的发送并保存验证码
*
* @param phone
* @param session
* @return
*/
@Override
public Result sendCode(final String phone, final HttpSession session) {
// 1.校验手机号是否正确,如果手机号不通过校验,则return
if (RegexUtils.isPhoneInvalid(phone)) {
return Result.fail("手机号码格式错误!请重新输入!");
}
// 2.如果手机号通过校验,则自动生成6位数的随机验证码
String code = RandomUtil.randomNumbers(6);
// 3.把验证码保存到redis中
stringRedisTemplate.opsForValue().set(RedisConstants.LOGIN_CODE_KEY + phone, code, RedisConstants.LOGIN_CODE_TTL, TimeUnit.MINUTES);
log.debug("验证码生成成功:" + code);
// 4.返回ok
return Result.ok("验证码发送成功!");
}
用户调用login(LoginData loginForm)接口,把phone跟code传到后端
后端先对phone进行格式校验,如果不通过校验,则返回提示信息
如果通过校验,则根据phone从redis中查询缓存的cacheCode,跟用户传过来的code进行匹配,如果不匹配或者cacheCode已经过期,那么提示用户验证码错误或者验证码已经过期
如果phone格式正确并且code跟缓存中的cacheCode匹配,则根据phone字段在数据库中查找用户的数据,如果用户不存在,则说明用户发送验证码是首次登录,需要进行信息注册,那么把user保存到数据库中
如果数据库中的用户已经存在,说明用户发送验证码和手机号是为了登录,则自动生成token作为key,用户信息user作为value,选择hash数据类型或者string类型存储到Redis中,并设置有效期
把生成的token返回给前端,以便前端把token保存到浏览器的缓存中,在每次发送请求之前在浏览器缓存中中取出token,放到请求头的authorization中,以便请求发送到后端时,可以在拦截器中根据token从redis缓存中获取用户信息,判断用户是否处于登录状态
/**
* 用户登录或者注册
*
* @param loginForm
* @return
*/
@Override
public Result login(LoginFormDTO loginForm) {
// 1.用户传来phone和code,先校验手机号是否正确
String phone = loginForm.getPhone();
// 2.如果手机号不通过校验,则返回手机号格式错误
if (RegexUtils.isPhoneInvalid(phone)) {
return Result.fail("手机号格式错误!请重新输入!");
}
String code = loginForm.getCode();
// 3.从redis中查询cacheCode,如果不匹配,则返回验证码错误
String cacheCode = stringRedisTemplate.opsForValue().get(RedisConstants.LOGIN_CODE_KEY + phone);
if (cacheCode == null || !cacheCode.equals(code)) {
return Result.fail("验证码错误!请重新输入!");
}
// 4.如果匹配,则可以登录,但是如果用户是首次登录,则数据库中没有该用户相关数据,需要创建一个新用户
// 5.根据phone从数据库中查询用户信息
User user = query().eq("phone", phone).one();
if (user == null) {
user = createUserWithPhone(phone);//此时数据库中一定有了用户
}
// 6.用token作为redis中保存用户信息的key
// 使用hutool的工具包生成token
String token = UUID.randomUUID().toString(true);
// 7.把user转换成UserDTO
UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
// 8.转换成Map进行存储,因为存储到Redis选择Hash结构,需要注意BeanUtil工具类只能转换类中String类型的字段,所以需要自定义字段转换器,把字段值转换成String类型
Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(), CopyOptions.create().setIgnoreNullValue(true).setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));
// 9.把userMap存储到Redis中
stringRedisTemplate.opsForHash().putAll(RedisConstants.LOGIN_USER_KEY + token, userMap);
// 10.设置token的有效期
stringRedisTemplate.expire(RedisConstants.LOGIN_USER_KEY + token,RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
// 11.返回token给前端,前端会把token保存到浏览器缓存中,然后每次发送请求事,请求拦截器会把token添加到请求头authorization中
// 这样每次发送请求,只要有token,就说明用户处于登录状态
return Result.ok(token);
}
private User createUserWithPhone(String phone) {
User user = new User();
user.setNickName(USER_NICK_NAME_PREFIX + RandomUtil.randomNumbers(10));
user.setPhone(phone);
save(user);
return user;
}
用户调用me()接口,从ThreadLocal中获取用户信息,返回给前端
@GetMapping("/me")
public Result me() {
// 返回UserDTO,省略掉了敏感字段
return Result.ok(UserHolder.getUser());
}
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Resource
private StringRedisTemplate stringRedisTemplate;
/**
* 注册拦截器
*
* @param registry
*/
@Override
public void addInterceptors(final InterceptorRegistry registry) {
// 添加登录拦截器
registry.addInterceptor(new LoginInterceptor()).excludePathPatterns(
"/user/login",//用户登录接口
"/shop/**",//购物模块的浏览
"/shop-type/**",//商品类型
"/upload/**",//用户上传博客模块
"/blog/hot",//博客热点接口
"/voucher/**",//优惠卷模块
"/user/code"//获取验证码接口
).order(1);
// 添加刷新token拦截器,拦截并校验所有页面
registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).order(0);
}
}
注册RedisFreshInterceptor拦截器,实现HandlerInterceptor接口,重写调用controller前的方法preHandle,对所有页面进行拦截,判断是否存在token,如果不存在,则直接放行给后面进行登录校验或者普通页面,如果Redis中有token,则刷新token的有效期,并从Redis中获取用户信息,保存到本地线程对象ThreadLocal中,以便me()接口进行用户登录状态校验
public class RefreshTokenInterceptor implements HandlerInterceptor {
private StringRedisTemplate stringRedisTemplate;
public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
/**
* 刷新用户token
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public boolean preHandle(final HttpServletRequest request, final HttpServletResponse response, final Object handler) throws Exception {
// 1.获取请求头中的token
String token = request.getHeader("authorization");
if (StrUtil.isBlank(token)) {//如果没有token,则一定没有用户信息,不需要刷新
return true;//放行给后面校验
}
// 2.从redis中获取token对应的用户信息
String userKey = RedisConstants.LOGIN_USER_KEY + token;
Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(userKey);
if(userMap.isEmpty()){//如果token已经过期,也直接放行
return true;
}
// 3.把userMap转换成UserDTO
UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
// 4.把UserDTO存放到ThreadLocal中,方便登录拦截器获取校验
UserHolder.saveUser(userDTO);
// 5.刷新token的有效期
stringRedisTemplate.expire(userKey, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
// 6.放行
return true;
}
@Override
public void afterCompletion(final HttpServletRequest request, final HttpServletResponse response, final Object handler, final Exception ex) throws Exception {
HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
}
}
对普通页面进行放行,对需要用户登录状态的页面进行拦截,从本地线程对象ThreadLocal中获取用户信息,如果用户信息不存在,则说明用户未进行登录,进行请求拦截不放行,提示用户进行登录,返回状态码401
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(final HttpServletRequest request, final HttpServletResponse response, final Object handler) throws Exception {
// 1.从本地线程对象中获取userDTO
UserDTO userDTO = UserHolder.getUser();
// 2.判断是否有用户信息,如果没有,则说明用户没有登录
if(userDTO == null){
response.setStatus(401);
return false;
}
// 3.放行
return true;
}
@Override
public void afterCompletion(final HttpServletRequest request, final HttpServletResponse response, final Object handler, final Exception ex) throws Exception {
HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
}
}