Redis进阶学习02---Redis替代Session和Redis缓存

Posted 大忽悠爱忽悠

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Redis进阶学习02---Redis替代Session和Redis缓存相关的知识,希望对你有一定的参考价值。

Redis进阶学习02---Redis替代Session和Redis缓存


参考b站虎哥redis视频

本系列项目源码将会保存在gitee上面,仓库链接如下:

https://gitee.com/DaHuYuXiXi/redis-combat-project


基于Session登录流程

我们先来看一下基于Session实现登录的模板流程是什么样子的:

  • 发送短信验证码


核心逻辑:

    public Result sendCode(String phone, HttpSession session) 
        //1.校验手机号
        if(RegexUtils.isPhoneInvalid(phone))
        
            //2.如果不符合,返回错误信息
            return Result.fail(getErrMsg("01",UserServiceImpl.class));
        
        //3.符合,生成验证码
        String code = RandomUtil.randomNumbers(6);
        //4.保存验证码到session
        session.setAttribute("code",code);
        //5.发送验证码
         log.debug("发送短信验证码成功,code ",code);
         //6.返回ok
        return Result.ok();
    
  • 短信验证码的登录和注册

    @Override
    public Result login(LoginFormDTO loginForm, HttpSession session) 
        //1.校验手机号
        String phone = loginForm.getPhone();
        if (RegexUtils.isPhoneInvalid(phone)) 
            //2.如果不符合,返回错误信息
            return Result.fail(getErrMsg("01", UserServiceImpl.class));
        
        //3.校验验证码
        Object cacheCode = session.getAttribute("code");
        String code = loginForm.getCode();
        if (cacheCode == null || !cacheCode.toString().equals(code)) 
            //4.不一致报错
            return Result.fail(getErrMsg("02", UserServiceImpl.class));
        
        //5.一致,根据手机号查询用户
        User user = query().eq("phone", phone).one();

        //6.判断用户是否存在
        if (user == null) 
            //7.不存在,创建新用户并保存
            user = createUserWithPhone(phone);
        
        //8.保存用户信息到session中
        session.setAttribute("user", user);
        return Result.ok();
    

    private User createUserWithPhone(String phone) 
        //1.创建用户
        User user = new User();
        user.setPhone(phone);
        user.setNickName(USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));
        //2.保存用户
        save(user);
        return user;
    
  • 校验登录状态

我们需要把验证功能放到拦截器中实现:

public class LoginInterceptor implements HandlerInterceptor 
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception 
      //1.获取session
        HttpSession session = request.getSession();
      //2.获取session中的用户
        Object user = session.getAttribute("user");
      //3.判断用户是否存在
        if(user==null)
        
            //4.不存在,拦截,返回401状态码
            response.setStatus(401);
            return false;
        
        //5.存在,保存用户信息到ThreadLocal
        UserHolder.saveUser(getUserDTO(user));
        //6.放行
        return true;
    

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception 
       //清空ThreadLocal
       UserHolder.removeUser();
    


    private UserDTO getUserDTO(Object user) 
        UserDTO userDTO = new UserDTO();
        BeanUtils.copyProperties(user,userDTO);
        return userDTO;
    


保存用户信息到ThreadLocal可以确保当前请求从开始到结束这段时间,我们可以轻松从ThreadLocal中获取当前用户信息,而不需要每次用到的时候,还去查询一遍


本节项目完整代码,参考2.0版本


集群session共享问题


既然多台tomcat之间的session存在隔离问题,那么我们是否可以将session中存储的内容移动到redis中进行存放,即用redis代替session


基于Redis实现session共享


这里说一下: 登录成功后,会将用户保存到redis中,这和上面讲用户保存到session中的思想是一致的,都是一种缓存思想,防止每次都需要拦截器拦截请求时,都需要去数据库查找,而是直接通过token去redis中获取即可


注意,这里的token不是jwt的token,这里的token只是随机生成的一段字符串,我们无法通过解析这个字符串拿到用户信息,而是只能通过这个token作为key,去redis中获取到对应用户的信息。

个人想法:即便是jwt的token,因为一般不会在里面token中保存完整的用户信息,并且每次请求打进拦截器的时候,还是需要去解析token,并去数据库查一下,防止token伪造,但是这样太浪费性能了,可以考虑在登录成功后,将用户信息存入redis,并且规定过期时间,然后拦截器每次根据token去redis获取用户完整信息,如果成功获取,那么刷新token过期时间,否则,从数据库重新获取,然后再放入缓存中。


我们这里选用HASH来存储User对象的信息:


UserServiceImpl代码:

@Service
@Slf4j
@RequiredArgsConstructor
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService 
    private final StringRedisTemplate stringRedisTemplate;

    @Override
    public Result sendCode(String phone, HttpSession session) 
        //1.校验手机号
        if (RegexUtils.isPhoneInvalid(phone)) 
            //2.如果不符合,返回错误信息
            return Result.fail(getErrMsg("01", UserServiceImpl.class));
        
        //3.符合,生成验证码
        String code = RandomUtil.randomNumbers(6);
        //4.保存验证码到redis
        stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY+phone,code,2, TimeUnit.MINUTES);
        //5.发送验证码
        log.debug("phone code ", code);
        //6.返回ok
        return Result.ok();
    

    @Override
    public Result login(LoginFormDTO loginForm, HttpSession session) 
        //1.校验手机号
        String phone = loginForm.getPhone();
        if (RegexUtils.isPhoneInvalid(phone)) 
            //2.如果不符合,返回错误信息
            return Result.fail(getErrMsg("01", UserServiceImpl.class));
        
        //3.从redis中获取验证码然后进行校验
        String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY+phone);
        String code = loginForm.getCode();
        if (cacheCode == null || !cacheCode.toString().equals(code)) 
            //4.不一致报错
            return Result.fail(getErrMsg("02", UserServiceImpl.class));
        
        //5.一致,根据手机号查询用户
        User user = query().eq("phone", phone).one();

        //6.判断用户是否存在
        if (user == null) 
            //7.不存在,创建新用户并保存
            user = createUserWithPhone(phone);
        

        //7.保存用户信息到redis
        //7.1 随机生成token,作为登录令牌
        String token = generateToken();
        //7.2 将User对象转换为Hash对象
        Map map = beanToMap(BeanUtil.copyProperties(user, UserDTO.class));
        String key=LOGIN_USER_KEY+token;
        stringRedisTemplate.opsForHash().putAll(key,map);
        //设置有效期
        stringRedisTemplate.expire(key,LOGIN_USER_TTL,TimeUnit.MINUTES);
        //7.3 存储
        return Result.ok();
    

    private Map<String, Object> beanToMap(UserDTO user) 
        return BeanUtil.beanToMap(user, new HashMap<>(), CopyOptions.create().setIgnoreNullValue(true)
                //解决long转String报错的问题
                .setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));
    

    private String generateToken() 
        return UUID.randomUUID().toString(true);
    


    private User createUserWithPhone(String phone) 
        //1.创建用户
        User user = new User();
        user.setPhone(phone);
        user.setNickName(USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));
        //2.保存用户
        save(user);
        return user;
    


LoginInterceptor 代码:

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");
        if (StrUtil.isBlank(token)) 
            //不存在,拦截,返回401状态码
            response.setStatus(401);
            return false;
        
        token = LOGIN_USER_KEY + token;
        //2.基于Token获取redis中的用户
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(token);
        //3.判断用户是否存在
        if (userMap.isEmpty()) 
            //不存在,拦截,返回401状态码
            response.setStatus(401);
            return false;
        
        //5.map转换为userDto
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);

        //6.用户信息保存到threadLocal
        UserHolder.saveUser(userDTO);

        //7.刷新token的有效期
        stringRedisTemplate.expire(token, LOGIN_USER_TTL, TimeUnit.MINUTES);
        return true;
    

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception 
        UserHolder.removeUser();
    


解决状态登录刷新问题

上面的代码设计思路: 如果用户长时间都在请求不需要拦截的请求,那么token就不会被刷新,进而导致用户浏览浏览着,token就过期了

优化后:分离拦截器职责,用一个单独的拦截器拦截所有请求,每次都刷新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");
        if (StrUtil.isBlank(token)) 
            //不存在,拦截,返回401状态码
            response.setStatus(401);
            return false;
        
        token = LOGIN_USER_KEY + token;

        //2.基于Token获取redis中的用户
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(token);
        //3.判断用户是否存在
        if (userMap.isEmpty()) 
            //不存在,拦截,返回401状态码
            response.setStatus(401);
            return false;
        
        //5.map转换为userDto
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);

        //6.用户信息保存到threadLocal
        UserHolder.saveUser(userDTO);

        //7.刷新token的有效期
        stringRedisTemplate.expire(token, LOGIN_USER_TTL, TimeUnit.MINUTES);
        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 
        //判断是否需要去拦截
        if(UserHolder.getUser()==null)
        
            response.setStatus(401);
            return false;
        
        return true;
    

RefreshTokenInterceptor 要先于LoginInterceptor 执行,否则LoginInterceptor 中无法中ThreadLocal中获取用户信息

@Configuration
@RequiredArgsConstructor
public class WebMvcConfig implements WebMvcConfigurer 
    private final 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(2);
        
      registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).order(1);
    


还有一点需要注意:如果用户信息被修改了,需要清空redis中的缓存信息,让用户重新进行登录
Redis02Redis基础:List相关操作

00. Redis 学习开篇

00. Redis 学习开篇

Redis02——Redis单节点安装

Linux-基础学习-Redis的进阶学习

Redis02 Redis客户端之Java