基于Redis实现短信登录功能
一、流程
二、开发
2.1 token刷新
刷新规则:
用户每进行一次操作(发起请求),都会把token的有效期重置。当用户长时间(这里我们设置了30分钟)没有操作时,token就会过期。
设计:
由于开发中并不是所有的接口都要进行拦截的,如果把token刷新的逻辑,写在登录拦截器里,在用户发起请求的时候判断用户有没有登录,然后再刷新token的话。那么当用户在发起一些不需要做登录拦截的请求的时候,就不会刷新token。这就与我们的刷新token的需求违背了。
所有我们不能把刷新token的逻辑写到登录拦截器里。要自己写一个全局拦截器,拦截所有的请求,并刷新token。而且这个拦截器要先比登录拦截器先拦截到请求。
缺点:
登录拦截器中,拦截的接口都是需要登录校验的,而被不登录拦截器拦截的接口是不做登录校验的,也就是说,用户其实不用登录也可以访问不被登录拦截器拦截接口的。
如果token刷新拦截器把所有请求都拦截了,那么我未登录的用户其实是根本无法访问任何接口的,这有点与我们的需求不一致了。
(待改进)
实现
MvcConfig 注册拦截器,使拦截器生效
其中order()是用来指定自定义拦截器的顺序的,值越小,优先级越高
如果你对Spring的过滤器、拦截器的区别、顺序或者原理感兴趣,可以看看我的这篇文章
@Configuration public class MvcConfig implements WebMvcConfigurer { @Resource private StringRedisTemplate stringRedisTemplate; @Override public void addInterceptors(InterceptorRegistry registry) { // 登录拦截器 registry.addInterceptor(new LoginInterceptor()) .excludePathPatterns( "/shop/**", "/voucher/**", "/shop-type/**", "/upload/**", "/blog/hot", "/user/code", "/user/login" ).order(1); //token刷新的拦截器 registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0); } }
RefreshTokenInterceptor
如果你对“为什么这里的stringRedisTemplate 不能使用@Autowired或者@Resource注解来注入“有疑惑的话,那么请参考我的这篇文章
当然你也可以直接在RefreshTokenInterceptor类上加@Component注解后,再使用@Autowired或者@Resource注解来注入StringRedisTemplate
但是,我们实际上并不需要把拦截器交给Ioc来管理
所以我们这里使用的解决方案是
由于RefreshTokenInterceptor 类中需要用到StringRedisTemplate 类
而RefreshTokenInterceptor 类是在 WebConfig类中被调用的
而WebConfig类上已经有了@Configuration注解了
所以我们把stringRedisTemplate从WebConfig类中传递过来
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 { // 1.获取请求头中的token String token = request.getHeader("authorization"); if (StrUtil.isBlank(token)) { return true; } // 2.基于TOKEN获取redis中的用户 String key = LOGIN_USER_KEY + token; Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key); // 3.判断用户是否存在 if (userMap.isEmpty()) { return true; } // 5.将查询到的hash数据转为UserDTO UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false); // 6.存在,保存用户信息到 ThreadLocal UserHolder.saveUser(userDTO); // 7.刷新token有效期 stringRedisTemplate.expire(key, LOGIN_USER_TTL, TimeUnit.MINUTES); // 8.放行 return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { // 移除用户 UserHolder.removeUser(); } }
LoginInterceptor
public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 1.判断是否需要拦截(ThreadLocal中是否有用户) if (UserHolder.getUser() == null) { // 没有,需要拦截,设置状态码 response.setStatus(401); // 拦截 return false; } // 有用户,则放行 return true; } }
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();
}
}
2.2 token生成和Redsi存储
关于发送验证码,可以参考我的这篇文章的2.2部分:基于Session实现短信登录 - 落叶知秋 (preke.top)
我们这里重点关注 token生成并存入redis
controller和service部分省略
重点看 代码注释中7. 部分
1-5部分都是数据校验
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
// 1.校验手机号
String phone = loginForm.getPhone();
if (RegexUtils.isPhoneInvalid(phone)) {
// 2.如果不符合,返回错误信息
return Result.fail("手机号格式错误!");
}
// 3.从redis获取验证码并校验
String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
String code = loginForm.getCode();
if (cacheCode == null || !cacheCode.equals(code)) {
// 不一致,报错
return Result.fail("验证码错误");
}
// 4.一致,根据手机号查询用户 select * from tb_user where phone = ?
User user = query().eq("phone", phone).one();
// 5.判断用户是否存在
if (user == null) {
// 6.不存在,创建新用户并保存
user = createUserWithPhone(phone);
}
// 7.保存用户信息到 redis中
// 7.1.随机生成token,作为登录令牌
String token = UUID.randomUUID().toString(true);
// 7.2.将User对象转为HashMap存储
UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),
CopyOptions.create()
.setIgnoreNullValue(true)
.setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));
// 7.3.存储
String tokenKey = LOGIN_USER_KEY + token;
stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);
// 7.4.设置token有效期
stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);
// 8.返回token
return Result.ok(token);
}