Redis项目:短信验证码登录
这是黑马的黑马点评项目,短信验证码的业务。一开始是使用session做的,后来重构,使用redis缓存来完成。
第一层拦截器:
public class RefreshTokenInterceptor implements HandlerInterceptor {
private StringRedisTemplate stringRedisTemplate;
public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//获取请求头中的token
String token = request.getHeader("authorization");
if (StrUtil.isBlank(token)) {
return true;
}
// 基于token获取redis中的用户
Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(RedisConstants.LOGIN_USER_KEY + token);
//判断用户是否存在
// 不存在,拦截,返回401状态码
if (userMap.isEmpty()) {
return true;
}
// 查询到的hash数据转换成UserDTO对象
UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
// 存在 保存用户信息到Threadlocal
UserHolder.saveUser(userDTO);
// 刷新token有效期
stringRedisTemplate.expire(RedisConstants.LOGIN_USER_KEY + token, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
//放行
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//移除用户
UserHolder.removeUser();
}
}
第一次拦截器的逻辑:
- preHandle 方法:
- 从请求头中获取 authorization token。
- 如果 token 为空,直接放行(返回 true)。
- 根据 token 从 Redis 中查询用户信息(存储在 Hash 结构中)。
- 如果用户信息不存在,直接放行(返回 true)。
- 如果用户信息存在:
- 将 Redis 查询到的 Hash 数据转换为 UserDTO 对象。
- 将用户信息保存到 UserHolder(ThreadLocal)。
- 刷新 Redis 中 token 的有效期。
- 返回 true 放行请求。
- afterCompletion 方法:
- 在请求处理完成后,移除 UserHolder 中的用户信息,清理线程局部变量
在authorization 中它记录的是客户端发送的身份验证凭证
刷新 Redis 中 token 的有效期是通过调用 Redis 的 EXPIRE 命令完成的
在转成对象的时候 参数false:一个布尔参数,通常用于控制填充行为.如果 Map 中的键无法匹配目标对象的属性,或者类型转换失败,会抛出异常(例如 BeanException),填充过程中止
第二次拦截器
第二次拦截就只需要判断线程里有没有用户了
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//这个拦截器只需要判断是不是需要拦截(ThreadLocal中有没有用户)
if (UserHolder.getUser() == null) {
//没有用户信息 拦截 返回401
response.setStatus(UNAUTHORIZED_401);
return false;
}
//有用户 放行
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//移除用户
UserHolder.removeUser();
}
}
- preHandle:
- 检查 UserHolder(ThreadLocal)中是否有用户信息。
- 如果没有(UserHolder.getUser() == null),设置响应状态码为 401(未授权)并拦截请求(返回 false)。
- 如果有用户信息,放行请求(返回 true)。
- afterCompletion:
- 在请求处理完成后,清理 UserHolder 中的用户信息。
public class UserHolder {
private static final ThreadLocal<UserDTO> tl = new ThreadLocal<>();
public static void saveUser(UserDTO user){
tl.set(user);
}
public static UserDTO getUser(){
return tl.get();
}
public static void removeUser(){
tl.remove();
}
}
ThreadLocal 是 Java 中的一种线程隔离机制,用于在多线程环境中为每个线程提供独立的变量副本
每个线程访问 ThreadLocal 时,获取的是自己线程的独立副本,互不干扰.
ThreadLocal 中的数据与线程绑定,线程结束时数据自动销毁(如果正确清理)
- ThreadLocal 维护一个 ThreadLocalMap,存储在每个 Thread 对象中。
- set 方法将值存入当前线程的 ThreadLocalMap,键是 ThreadLocal 对象,值是用户设置的数据。
- get 方法从当前线程的 ThreadLocalMap 中取值。
- remove 方法删除当前线程的 ThreadLocalMap 中的对应键值对
发送验证码
@Override
public Result sendCode(String phone, HttpSession session) {
//1、校验手机号
if (RegexUtils.isPhoneInvalid(phone)) {
//2、如果不符合,返回错误信息
return Result.fail(cacheConstants.ERROR_USER_PHONE);
}
//3、符合,生成验证码
String code = RandomUtil.randomNumbers(6);
//4、保存验证码 把验证码存入redis中
//session.setAttribute(cacheConstants.USER_LOGIN_CAPTCHA_KEY, code);
stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES);
//5、发送验证码
log.info("验证码发送成功{}", code);
//6、返回结果对象
return Result.ok();
}
验证码被存在了redis中,而不是session中
校验验证码
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
//校验手机号
String phone = loginForm.getPhone();
if (RegexUtils.isPhoneInvalid(phone)) {
//如果不符合,返回错误信息
return Result.fail(cacheConstants.ERROR_USER_PHONE);
}
//校验验证码 从redis中获取
String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
String code = loginForm.getCode();
//验证码不一致 报错
if (cacheCode == null || !cacheCode.equals(code))
return Result.fail(cacheConstants.ERROR_USER_CAPTCHA_KEY);
//验证码一致 查找用户 SELECT * FROM tb_user where phone = ?
User user = query().eq(cacheConstants.USER_LOGIN_PHONE, phone).one();
// 查询用户 是否存在 不存在 创建用户
if (user == null)
user = createUserWithPhone(phone);
//保存用户信息到redis中
//生成token作为key
String token = UUID.randomUUID().toString();
//将User对象转为hash存储
UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
String tokenKey = LOGIN_USER_KEY + token;
Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),
CopyOptions.create()
.setIgnoreNullValue(true)
.setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));
//存储 这里注意 user中id属性是long 类型 需要的是string类型 利用putAll参数进行转换
stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);
//设置有效期
stringRedisTemplate.expire(LOGIN_USER_KEY + token, LOGIN_USER_TTL, TimeUnit.MINUTES);
//返回token
return Result.ok(token);
}
private User createUserWithPhone(String phone) {
//创建用户
User user = new User();
user.setPhone(phone);
user.setNickName(cacheConstants.USER_LOGIN_USER + RandomUtil.randomString(10));
save(user);
return user;
}
先是校验一下手机号是不是合法 也可以再校验一下验证码
然后就是从redis中取得验证码 进行比对
验证码一致就可以从数据库中查找是否有用户
有用户 用户数据会通过mybatisPlus自动映射到对象user上
没有用户 调用创建用户
然后会生成一个token,作为key的一部分(客户端在后续请求中携带 token(通常放在 Authorization 请求头中))
将user的部分信息封装到UserDto之中 然后将对象转成map集合的形式
之后以hash的形式存入redis 设置有效期
trips
CopyOptions 配置了一些转换选项 ,CopyOptions.create() 返回一个默认的 CopyOptions 对象,可以通过链式调用设置选项。.setIgnoreNullValue(true),忽略值为 null 的属性。.setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString())自定义字段值的编辑器,通过 Lambda 表达式处理每个属性值,将每个属性值转换为字符串
用putAll可以一次性把数据全部传进去
疑问:
authorization基于什么生成的?
......