基于Redis缓存:
@Override
public Result sendCode(String phone) {
//TODO 1.校验手机号:不符合是true
if (RegexUtils.isPhoneInvalid(phone)) {
//2.如果不符合 , 返回错误信息
return Result.fail("手机号格式错误");
}
// TODO 3.符合 , 生成一个随机验证码 , 使用的是huTool中的工具类 ,
String code = RandomUtil.randomNumbers(6);
// TODO 4.保存验证码到Redis当中 , 使用手机号加前缀作为key来保存 , 保证可以的唯一性 , 同时 , 设置有效期为两分钟
stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES);
//5.模拟发送验证码
log.debug("发送短信验证码成功, 验证码:{" + code + "}");
//6.返回前端数据ok
return Result.ok();
}
@Override
public Result login(LoginFormDTO loginForm) {
//1.校验手机号
String phone = loginForm.getPhone();
if (RegexUtils.isPhoneInvalid(phone)) {
return Result.fail("手机号格式错误");
}
//2. TODO 校验验证码 , 从redis中获取验证码
String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
String code = loginForm.getCode();
if (cacheCode == null || !cacheCode.equals(code)) {
//3.判断验证码是否失效(也就是验证码查询不出来) 或者 校验用户输入验证码和Redis中的验证码是否一致
return Result.fail("验证码错误");
// TODO 使用反证的方式 , 可以减少if语句的判断次数
}
//4.一致 , 根据手机号查询用户
// TODO 使用的是MyBatisPlus中的方法 , 进行查询的
User user = query().eq("phone", phone).one();
//5.判断用户是否存在
if (user == null) {
//6.不存在 , 创建新用户并保存
user = createUserWithPhone(phone);
}
// TODO 7.保存用户信息到redis中
// TODO 7.1 随机生成token作为登录令牌
// TODO 使用huTool提供的UUID , 下边的写法是生成不带下划线的UUID , 默认值为false,带下划线的UUID
String token = UUID.randomUUID().toString(true);
// TODO 使用BeanUtil中的copyProperties方法 , 可以将user中的属性自动拷贝到UserDTO中 , 对于没有的属性,不进行拷贝
UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
// TODO 7.2 将User对象转为Hash集合 , 将UserDTO转换为一个Map集合 ,
// 这个时候 , 进行转换的时候 ,会出现异常 , 因为我们使用的是String类型的redis对象 , 在转换的时候 , key值只能是String类型的
// 但是 , 这个BeanUtil工具类 , 允许我们进行自定义 ,
// 添加两个参数 , 一个是new HashMap<>() ,
// 一个是CopyOptions , 定义自定义的操作
Map<String, Object> userMap = BeanUtil.beanToMap(userDTO,new HashMap<>(),
CopyOptions.create()
.setIgnoreNullValue(true) // TODO 设置是否忽略空值
// TODO 对字段值的修改器 , 需要两个参数 , 修改前的字段名和字段值 , 修改后的字段值
.setFieldValueEditor((fileName,fileValue) -> fileValue.toString()));
// TODO 7.3 存储
String tokenKey = LOGIN_USER_KEY + token;
stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);
// TODO 7.4 设置token的有效期 , redis中不能在上一个方法中直接设置有效期 , 可以在下边设置
stringRedisTemplate.expire(tokenKey,LOGIN_USER_TTL,TimeUnit.MINUTES);
// TODO 8.返回token , 用来将这个token保存在浏览器中 , 下次登录的时候 , 会携带这个token进行访问
return Result.ok(token);
}
//TODO 创建用户
private User createUserWithPhone(String phone) {
//1.创建用户 :
User user = new User();
user.setPhone(phone);
//生成随机的字符串 , 用来当做用户名
user.setNickName("user_" + RandomUtil.randomString(10));
save(user);
return user;
}
使用拦截器 , 负责检测用户登录的时间 , 使用缓存 token来定义用户的登录时间 , 只要有操作 , 就刷新token的时间 ,
只负责检测请求中有没有携带token , 没有就直接放行 , 有了就刷新token ,
/**
* 拦截器类 , 负责刷新token保存时间的 , 只有用户登录了才进行操作 , 其他的一概放行
*/
public class RefreshTokenInterceptor implements HandlerInterceptor {
// TODO 注意: 拦截器是我们自己创建的类 , 不受Spring容器管理 , 所以 , 不能直接注入RedisTemplate
// TODO 我们只能使用构造函数的方式进行注入 , 谁调用这个拦截器 ,谁负责注入这个RedisTemplate
private StringRedisTemplate stringRedisTemplate;
public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
@Override
//前置拦截
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// TODO 1.获取请求头中的token
String token = request.getHeader("authorization");
// TODO StrUtil中的isBlank方法就是判断是否是空值
if (StrUtil.isBlank(token)){
// TODO 为空,说明没有登录 , 直接放行
return true;
}
// TODO 2.基于token获取redis中的用户信息
// TODO 不能简单的使用get来获取值了 , 使用get获取的只是hash中的map中的一个值 , 而我们想获取的是全部的值 , 使用entries这个方法
Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(RedisConstants.LOGIN_USER_KEY + token);
// TODO 不用判断是否为null了,entries会做判断 , 如果为null会返回一个空的map , 所以这里只用判断是否为空就可以了
if (userMap.isEmpty()){
// TODO 为空说明用户没有登录, 直接放行 , 不作操作
return true;
}
// TODO 不为空 , 进行token以及数据的保存工作
// TODO 5.将查询到的Hash数据转为UserDTO对象 , 最后一个参数是否忽略转换中的异常 , false是不忽略
UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
// TODO 5.存在 , 保存用户信息到ThreadLocal , 这是一个工具类 , 内部创建了一个ThreadLocal对象,来进行操作
UserHolder.saveUser(userDTO);
// TODO 7.刷新token的有效期 , 也就是从新设置对应key的有效时间
stringRedisTemplate.expire(RedisConstants.LOGIN_USER_KEY+ token,LOGIN_USER_TTL, TimeUnit.MINUTES);
//6.放行
return true;
}
@Override
//后置拦截
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//删除用户
UserHolder.removeUser();
}
有了第一个拦截器 , 这个类 就只用拦截是否登录即可 ,
/**
* 拦截器类 , 负责拦截是否登录的
*/
public class LoginInterceptor implements HandlerInterceptor {
@Override
//前置拦截
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// TODO 1.判断是否需要拦截 , ThreadLocal中是否有用户信息
if (UserHolder.getUser() == null){
// 没有 , 需要拦截 , 设置状态码 ,
response.setStatus(401);
// 拦截
return false;
}
//有用户 , 直接放行
return true;
}
}
@Configuration
// TODO 配置拦截器
public class MvcConfig implements WebMvcConfigurer {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// TODO token 刷新拦截器
registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate))
.addPathPatterns("/**")
.order(0); // TODO 设置优先级 ,值越小 , 优先级越高
registry.addInterceptor(new LoginInterceptor())
.excludePathPatterns(
"/blog/hot",
"/shop-type/**",
"/upload/**",
"/voucher/**",
"/shop/**",
"/user/code",
"/user/login"
).order(1);
}
}
.excludePathPatterns(
"/blog/hot",
"/shop-type/**",
"/upload/**",
"/voucher/**",
"/shop/**",
"/user/code",
"/user/login"
).order(1);
}
}
---