一、流程

二、开发

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);
}

三、其他文章推荐

Redis实现登录认证与JWT令牌的区别

文章作者: 落叶知秋
本文链接:
版权声明: 本站所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 落叶知秋
学习笔记 Redis 后端 Java SpringBoot
喜欢就支持一下吧