Redis-Day2实战篇-短信登录(基于Session实现登录, 集群的session共享问题, 基于Redis实现共享session登录)

Redis-Day2实战篇-短信登录

  • 基于Session实现登录
    • 业务流程
    • 实现发送短信验证码
    • 实现短信验证码登录, 注册
    • 实现登录检验拦截器
  • 集群的session共享问题
  • 基于Redis实现共享session登录
    • 业务流程
    • 项目实现
    • 解决状态登录刷新的问题
  • 来源
  • Gitee地址

基于Session实现登录

业务流程

Redis-Day2实战篇-短信登录(基于Session实现登录, 集群的session共享问题, 基于Redis实现共享session登录)_第1张图片

实现发送短信验证码

// Service
public Result sendCode(String phone, HttpSession session) {
    // 1. 检验手机号
    if(RegexUtils.isPhoneInvalid(phone)){
        // 2. 如果不符合, 返回错误信息
        return Result.fail("手机号格式错误!");
    }
    // 3. 符合, 生成验证码
    String code = RandomUtil.randomNumbers(6);
    // 4. 保存验证码到session
    session.setAttribute("code", code);
    // 5. 发送验证码
    log.debug("短信验证码, code: {}", code);
    // 6. 返回ok
    return Result.ok();
}

实现短信验证码登录, 注册

// Service
public Result login(LoginFormDTO loginForm, HttpSession session) {
    // 1. 检验手机号
    String phone = loginForm.getPhone();
    if(RegexUtils.isPhoneInvalid(phone)){
        // 2. 如果不符合, 返回错误信息
        return Result.fail("手机号格式错误!");
    }
    // 3. 检验验证码
    Object cacheCode = session.getAttribute("code");
    String code = loginForm.getCode();
    if(cacheCode == null || !cacheCode.toString().equals(code)){
        // 4. 如果不一致, 返回错误信息
        return Result.fail("验证码错误!");
    }
    // 5. 一致, 根据手机号查询用户
    User user = query().eq("phone", phone).one();
    // 6. 判断用户是否存在
    if(user == null){
        // 7. 不存在, 创建用户并保持
        user = createWithPhone(phone);
    }
    // 8. 保存用户到session
    session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class));
    return Result.ok();
}

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

实现登录检验拦截器

// Interceptor
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((UserDTO) user);
        // 6. 放行
        return true;
    }
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        UserHolder.removeUser();
    }
}
// Config
@Configuration
public class MvcConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor())
                .excludePathPatterns(
                        "/shop/**",
                        "/voucher/**",
                        "/shop-type/**",
                        "/upload/**",
                        "/blog/hot",
                        "/user/code",
                        "/user/login"
                );
    }
}

集群的session共享问题

  • session共享问题: 多台Tomcat并不共享session存储空间, 当请求切换到不同tomcat服务时导致数据丢失的问题
  • session的替代方案应该满足:
    • 数据共享
    • 内存存储
    • key, value结构
  • redis很适合

基于Redis实现共享session登录

业务流程

Redis-Day2实战篇-短信登录(基于Session实现登录, 集群的session共享问题, 基于Redis实现共享session登录)_第2张图片
Redis-Day2实战篇-短信登录(基于Session实现登录, 集群的session共享问题, 基于Redis实现共享session登录)_第3张图片

  • 保存登录的用户信息
    • String结构: 以JSON字符串保存, 比较直观
    • Hash结构: 将对象中的每个字段独立存储, 可以针对单个字段做CRUD, 并且内存占用更少

项目实现

// Service
@Slf4j
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public Result sendCode(String phone, HttpSession session) {
        // 1. 检验手机号
        if(RegexUtils.isPhoneInvalid(phone)){
            // 2. 如果不符合, 返回错误信息
            return Result.fail("手机号格式错误!");
        }
        // 3. 符合, 生成验证码
        String code = RandomUtil.randomNumbers(6);

        // // 4. 保存验证码到session
        // session.setAttribute("code", code);

        // 4. 保存验证码到redis
        stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY+phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES);

        // 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("手机号格式错误!");
        }
        // 3. 检验验证码
        String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
        String code = loginForm.getCode();
        if(cacheCode == null || !cacheCode.equals(code)){
            // 4. 如果不一致, 返回错误信息
            return Result.fail("验证码错误!");
        }
        // 5. 一致, 根据手机号查询用户
        User user = query().eq("phone", phone).one();
        // 6. 判断用户是否存在
        if(user == null){
            // 7. 不存在, 创建用户并保持
            user = createWithPhone(phone);
        }

        // // 8. 保存用户到session
        // session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class));

        // 8. 保存用户到redis
        // 8.1. 随机生成token, 作为登录令牌
        String token = UUID.randomUUID().toString(true);
        // 8.2. 将User对象转为Hash存储
        UserDTO userDTO = new UserDTO();
        BeanUtils.copyProperties(user, userDTO);
        String userJson = JSON.toJSONString(userDTO);
        Map userMap = JSON.parseObject(userJson, Map.class);
        userMap.forEach((key, value)->{
            userMap.put(key, String.valueOf(value));
        });
        // 8.3. 存储
        String tokenKey = LOGIN_USER_KEY + token;
        stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);
        // 8.4. 设置token有效期
        stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);
        // 9. 返回token
        return Result.ok(token);
    }

    ...
}
// Interceptor
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;
        }
        // 2. 基于token获取redis中的用户
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash()
                .entries(LOGIN_USER_KEY + token);
        // 3. 判断用户是否存在
        if(userMap.isEmpty()){
            // 4. 不存在, 拦截, 返回401状态码
            response.setStatus(401);
            return false;
        }
        // 5. 将查询到的Hash数据转为UserDTO对象
        String userJson = JSON.toJSONString(userMap);
        UserDTO userDTO = JSON.parseObject(userJson, UserDTO.class);
        // 6. 存在, 保存用户信息到ThreadLocal
        UserHolder.saveUser(userDTO);
        // 7. 刷新token有效期
        stringRedisTemplate.expire(LOGIN_USER_KEY + token, LOGIN_USER_TTL, TimeUnit.MINUTES);
        // 8. 放行
        return true;
    }
    ...
}
  • 思考
    • 类转map: 类->JSON字符串->map
    • 构造器注入: 如果一个类没有交给spring管理, 那么类中成员变量需要注入时, 就要手动的构造器注入

解决状态登录刷新的问题

  • 问题: 如果用户一直请求无需拦截的页面, 会导致token不刷新
  • 解决: 增加一个拦截器拦截所有请求
// Interceptor
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;
    }
}
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(StringUtils.isBlank(token)){
            return true;
        }
        // 2. 基于token获取redis中的用户
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash()
                .entries(LOGIN_USER_KEY + token);
        // 3. 判断用户是否存在
        if(userMap.isEmpty()){
            return false;
        }
        // 5. 将查询到的Hash数据转为UserDTO对象
        String userJson = JSON.toJSONString(userMap);
        UserDTO userDTO = JSON.parseObject(userJson, UserDTO.class);
        // 6. 存在, 保存用户信息到ThreadLocal
        UserHolder.saveUser(userDTO);
        // 7. 刷新token有效期
        stringRedisTemplate.expire(LOGIN_USER_KEY + token, LOGIN_USER_TTL, TimeUnit.MINUTES);
        // 8. 放行
        return true;
    }
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        UserHolder.removeUser();
    }
}
// Config
@Configuration
public class MvcConfig implements WebMvcConfigurer {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // token刷新拦截器
        registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).order(0);
        // 登录拦截器
        registry.addInterceptor(new LoginInterceptor())
                .excludePathPatterns(
                        "/shop/**",
                        "/voucher/**",
                        "/shop-type/**",
                        "/upload/**",
                        "/blog/hot",
                        "/user/code",
                        "/user/login"
                ).order(1);

    }
}

来源

黑马程序员. Redis入门到实战教程

Gitee地址

https://gitee.com/Y_cen/redis

你可能感兴趣的:(Redis,redis,java,数据库,spring,boot,中间件,session,集群)