在我们实现登录流程时,常常使用token作为key,user对象作为value存入到redis当中,并且设置对应的时间。同时在登录时通常使用拦截器,对我们访问的路径进行拦截,当用户访问被拦截的路径时就会去刷新对应的token有效期,以此来实现长时间的登录。
例如:
public class LoginInterceptor implements HandlerInterceptor {
private StringRedisTemplate stringRedisTemplate;
public LoginInterceptor(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1、从请求头中获取 token
String token = request.getHeader("authorization");
// 2、判断token是否为空
if (StrUtil.isBlank(token)) {
// 不存在报401
response.setStatus(401);
return false;
}
// 3、根据 token 从 redis 中获取用户信息
String tokenKey = RedisConstants.LOGIN_USER_KEY + token;
// 使用 entries 方法获取所有的 field-value
Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(tokenKey);
// 4、判断 userMap 是否为空
if (userMap.isEmpty()) {
// 不存在报401
response.setStatus(401);
return false;
}
// 4、将 userMap 转换为 UserDTO
UserDTO user = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
// 5、将 user 保存到 ThreadLocal 中
UserHolder.saveUser(user);
// 6、刷新 token 过期时间
stringRedisTemplate.expire(tokenKey, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
UserHolder.removeUser();
}
}
拦截器配置类:
package com.hmdp.config;
import com.hmdp.utils.LoginInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor(stringRedisTemplate))
.excludePathPatterns(
"/shop/**",
"/voucher/**",
"/user/login",
"/user/code",
"/shop-type/**",
"/upload/**"
);
}
}
登录功能是基于拦截器做的校验功能,但是当前拦截器拦截的并不是所有的路径,而是拦截的需要登录的路径,如果用户登录后,一直访问的是首页这种不需要拦截的路径,那么拦截器就会一直不执行,token 的过期时间就不会刷新,那么当 token 过期后,用户访问例如像个人主页时就会出现问题,很不友好。那如何解决?可以在当前拦截器的基础再添加一个拦截器,让新的拦截器拦截一切路径,在该拦截内做 token 的刷新动作。
实现两个拦截器:
标题RefreshTokenInterceptor
刷新token
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");
// 2、判断token是否为空
if (StrUtil.isBlank(token)) {
return true;
}
// 3、根据 token 从 redis 中获取用户信息
String tokenKey = RedisConstants.LOGIN_USER_KEY + token;
Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(tokenKey);
// 4、判断 userMap 是否为空
if (userMap.isEmpty()) {
return true;
}
// 4、将 userMap 转换为 UserDTO
UserDTO user = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
// 5、将 user 保存到 ThreadLocal 中
UserHolder.saveUser(user);
// 6、刷新 token 过期时间
stringRedisTemplate.expire(tokenKey, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
UserHolder.removeUser();
}
}
标题LoginInterceptor拦截器
判断是否登录,这里使用了自定义的threadLocal类来保存登录用户信息
public class UserHolder {
private static final ThreadLocal<User> tl = new ThreadLocal<>();
public static void saveUser(User user){
tl.set(user);
}
public static User getUser(){
return tl.get();
}
public static void removeUser(){
tl.remove();
}
}
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 判断当前用户是否登录
if (UserHolder.getUser() == null) {
// 未登录,设置状态码
response.setStatus(401);
// 拦截
return false;
}
return true;
}
}
标题MvcConfig 设置拦截规则
拦截器执行顺序应当是先执行 RefreshTokenInterceptor,而后再执行 LoginInterceptor,通过 orde 方法设置拦截器执行顺序,值越小,则执行顺序越优先。
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.excludePathPatterns(
"/shop/**",
"/voucher/**",
"/user/login",
"/user/code",
"/shop-type/**",
"/upload/**"
).order(1);
registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);
}
}
、