基于Redis实现共享Session登录的实现

 更新时间:2025年03月06日 08:24:21   作者:小璐乱撞xllz  
本文主要介绍了基于Redis实现共享Session登录的实现,包括发送短信验证码、短信验证码登录和注册、以及登录状态校验的流程,具有一定的参考价值,感兴趣的可以了解一下

背景

session 共享问题:如果后端服务是集群模式,由于多台机器之间并不共享 session 存储空间,当请求切换到不同服务时会导致数据丢失的问题

session 的替代方案应该满足:

1.数据共享

2.内存存储

3.key、value 结构

Redis 能够满足以上的要求,因此可以采用 Redis 来实现共享登录

实现流程

这里以短信登录的业务作为示例,主要包括三个功能:

1.发送短信验证码的接口

2.短信验证码登录、注册接口

3.校验登录状态拦截器

流程图如下所示:

image-20241024152619663

image-20241024152633775

这里采用的策略是,发送验证码时,将对应的手机号作为 key,验证码作为 value

登录、注册时,需要使用手机号将验证码取出,并且以随机 token 作为 key,用户信息作为 value 保存用户数据,这里的用户数据用 hash 类型保存。最后还需要将这个 token 返回给前端

之后在校验登录状态时,前端的每次请求都需要携带这个 token 值,以便服务端能取出相应的用户信息

这里使用随机 token 而不使用手机号作为 key 的目的在于,浏览器是需要存储这个 key 的,以便校验登录状态,如果使用手机号会不安全

代码实现

实体类

@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("tb_user")
public class User implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 主键
     */
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;

    /**
     * 手机号码
     */
    private String phone;

    /**
     * 密码,加密存储
     */
    private String password;

    /**
     * 昵称,默认是随机字符
     */
    private String nickName;

    /**
     * 用户头像
     */
    private String icon = "";

    /**
     * 创建时间
     */
    private LocalDateTime createTime;

    /**
     * 更新时间
     */
    private LocalDateTime updateTime;


}

dto 类

@Data
public class UserDTO {
    private Long id;
    private String nickName;
    private String icon;
}

这里单独抽取 dto 的原因在于,我们不希望将密码等敏感字段返回给前端

@Data
public class LoginFormDTO {
    private String phone;
    private String code;
    private String password;
}

结果返回类

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result {
    private Boolean success;
    private String errorMsg;
    private Object data;
    private Long total;

    public static Result ok(){
        return new Result(true, null, null, null);
    }
    public static Result ok(Object data){
        return new Result(true, null, data, null);
    }
    public static Result ok(List<?> data, Long total){
        return new Result(true, null, data, total);
    }
    public static Result fail(String errorMsg){
        return new Result(false, errorMsg, null, null);
    }
}

常量类

public class RedisConstants {
    public static final String LOGIN_CODE_KEY = "login:code:";
    public static final Long LOGIN_CODE_TTL = 2L;
    public static final String LOGIN_USER_KEY = "login:token:";
    public static final Long LOGIN_USER_TTL = 30L;
}

工具类

public class ObjectMapUtils {

    // 将对象转为 Map
    public static Map<String, String> obj2Map(Object obj) throws IllegalAccessException {
        Map<String, String> result = new HashMap<>();
        Class<?> clazz = obj.getClass();
        Field[] fields = clazz.getDeclaredFields();
        for (Field field : fields) {
            // 如果为 static 且 final 则跳过
            if (Modifier.isStatic(field.getModifiers()) && Modifier.isFinal(field.getModifiers())) {
                continue;
            }
            field.setAccessible(true); // 设置为可访问私有字段
            Object fieldValue = field.get(obj);
            if (fieldValue != null) {
                result.put(field.getName(), field.get(obj).toString());
            }
        }
        return result;
    }

    // 将 Map 转为对象
    public static Object map2Obj(Map<Object, Object> map, Class<?> clazz) throws Exception {
        Object obj = clazz.getDeclaredConstructor().newInstance();
        for (Map.Entry<Object, Object> entry : map.entrySet()) {
            Object fieldName = entry.getKey();
            Object fieldValue = entry.getValue();
            Field field = clazz.getDeclaredField(fieldName.toString());
            field.setAccessible(true); // 设置为可访问私有字段
            String fieldValueStr = fieldValue.toString();
            // 根据字段类型进行转换
            if (field.getType().equals(int.class) || field.getType().equals(Integer.class)) {
                field.set(obj, Integer.parseInt(fieldValueStr));
            } else if (field.getType().equals(boolean.class) || field.getType().equals(Boolean.class)) {
                field.set(obj, Boolean.parseBoolean(fieldValueStr));
            } else if (field.getType().equals(double.class) || field.getType().equals(Double.class)) {
                field.set(obj, Double.parseDouble(fieldValueStr));
            } else if (field.getType().equals(long.class) || field.getType().equals(Long.class)) {
                field.set(obj, Long.parseLong(fieldValueStr));
            } else if (field.getType().equals(String.class)) {
                field.set(obj, fieldValueStr);
            } else if(field.getType().equals(LocalDateTime.class)) {
                field.set(obj, LocalDateTime.parse(fieldValueStr));
            }

        }
        return obj;
    }

}

控制层

@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {
    @Resource
    private IUserService userService;
    
    /**
     * 发送手机验证码
     */
    @PostMapping("code")
    public Result sendCode(@RequestParam("phone") String phone) {
        return userService.sendCode(phone);
    }
    
    /**
     * 登录功能
     * @param loginForm 登录参数,包含手机号、验证码;或者手机号、密码
     */
    @PostMapping("/login")
    public Result login(@RequestBody LoginFormDTO loginForm){
        return userService.login(loginForm);
    }
}

服务层

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

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Override
    public Result sendCode(String phone/*, HttpSession session*/) {
        // 校验手机号
        if(RegexUtils.isPhoneInvalid(phone)) {
            return Result.fail("手机号格式错误");
        }
        // 生成验证码
        String code = RandomUtil.randomNumbers(6);
        /*// 保存验证码到 session
        session.setAttribute("code", phone + "-" + code);*/
        // 保存验证码到 redis
        redisTemplate.opsForValue().set(RedisConstants.LOGIN_CODE_KEY + phone, code,
                RedisConstants.LOGIN_CODE_TTL, TimeUnit.MINUTES);
        // 发送验证码
        log.debug("发送验证码:" + code + ",手机号:" + phone);
        return Result.ok();
    }

    @Override
    public Result login(LoginFormDTO loginForm/*, HttpSession session*/) {
        String phone = loginForm.getPhone();
        String code = loginForm.getCode();
        /*// 从 session 取出手机号和验证码
        String[] phoneAndCode = session.getAttribute("code").toString().split("-");
        // 校验手机号和验证码
        if(!phoneAndCode[0].equals(phone) || !phoneAndCode[1].equals(code)) {
            return Result.fail("手机号或验证码错误");
        }*/
        // 从 redis 中取出验证码
        String realCode = redisTemplate.opsForValue().get(RedisConstants.LOGIN_CODE_KEY + phone);
        if(StringUtils.isBlank(realCode) || !realCode.equals(code)) {
            return Result.fail("验证码错误");
        }
        // 根据手机号查询用户
        LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(User::getPhone, phone);
        User user = this.getOne(queryWrapper);
        // 用户如果不存在,则创建新用户
        if(user == null) {
            user = createUserWithPhone(phone);
        }
        /*// session 保存用户信息
        session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class));*/
        // redis 保存用户信息
        String token = UUID.randomUUID().toString(true);
        String tokenKey = RedisConstants.LOGIN_USER_KEY + token;
        try {
            // 将 User 转为 UserDTO 再转为 Map
            Map<String, String> userMap = ObjectMapUtils.obj2Map(BeanUtil.copyProperties(user, UserDTO.class));
            redisTemplate.opsForHash().putAll(tokenKey, userMap);
            redisTemplate.expire(tokenKey, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        }
        // 将 token 返回
        return Result.ok(token);
    }

    // 根据手机号创建新用户
    public User createUserWithPhone(String phone) {
        User user = new User();
        user.setPhone(phone);
        user.setNickName(SystemConstants.USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));
        // 保存至数据库
        this.save(user);
        return user;
    }

}

拦截器及其配置类

这里会使用两个拦截器,一个是拦截一切路径的刷新拦截器,主要用途就是如果用户在 token 有效期内访问了系统,那么就会刷新超时时间;另一个是拦截部分路径的登录校验拦截器,主要就是检验用户是否登录

添加刷新拦截器的原因在于,如果用登录校验拦截器进行刷新工作,由于排除了部分路径,因此如果用户一直访问这些被排除的部分路径,会导致用户 token 的有效期不会被刷新。所以需要单独添加一个拦截所有路径的拦截器

@Configuration
public class MvcConfig implements WebMvcConfigurer {

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 刷新拦截器
        registry.addInterceptor(new RefreshTokenInterceptor(redisTemplate)).order(10);
        // 登录拦截器
        registry.addInterceptor(new LoginInterceptor())
                .excludePathPatterns( // 排除的拦截路径
            		   // 以下根据业务需求来写
                        "/shop/**",
                        "/voucher/**",
                        "/shop-type/**",
                        "/upload/**",
                        "/blog/hot",
                        "/user/code",
                        "/user/login"
                ).order(20);
    }
}
public class RefreshTokenInterceptor implements HandlerInterceptor {

    private StringRedisTemplate redisTemplate;

    public RefreshTokenInterceptor(StringRedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 获取用户
        String token = request.getHeader("authorization");
        String key = RedisConstants.LOGIN_USER_KEY + token;
        Map<Object, Object> entries = redisTemplate.opsForHash().entries(key);
        // 用户不存在,直接放行
        if(entries.isEmpty()) {
            return true;
        }
        // Map 转为 UserDTO
        UserDTO user = (UserDTO) ObjectMapUtils.map2Obj(entries, UserDTO.class);
        // 用户存在,放入 ThreadLocal
        UserHolder.saveUser(user);
        // 刷新 token 有效期
        redisTemplate.expire(key, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
        // 放行
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 销毁 ThreadLocal
        UserHolder.removeUser();
    }

}
public class LoginInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        // 用户未登录,拦截
        if(UserHolder.getUser() == null) {
            response.setStatus(HttpStatus.HTTP_UNAUTHORIZED);
            return false;
        }
        return true;
    }

}

到此这篇关于基于Redis实现共享Session登录的实现的文章就介绍到这了,更多相关Redis Session共享登录内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家! 

相关文章

  • 详解Redis使用认证密码登录

    详解Redis使用认证密码登录

    本篇文章主要介绍了详解Redis使用认证密码登录 。启用Redis的认证密码可以增加Redis服务器的安全性。有兴趣的可以了解下
    2017-06-06
  • Redis优化token校验主动失效的实现方案

    Redis优化token校验主动失效的实现方案

    在普通的token颁发和校验中 当用户发现自己账号和密码被暴露了时修改了登录密码后旧的token仍然可以通过系统校验直至token到达失效时间,所以系统需要token主动失效的一种能力,所以本文给大家介绍了Redis优化token校验主动失效的实现方案,需要的朋友可以参考下
    2024-03-03
  • 浅谈Redis 中的过期删除策略和内存淘汰机制

    浅谈Redis 中的过期删除策略和内存淘汰机制

    本文主要介绍了Redis 中的过期删除策略和内存淘汰机制,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2022-04-04
  • Redis连接池监控(连接池是否已满)与优化方法

    Redis连接池监控(连接池是否已满)与优化方法

    本文详细讲解了如何在Linux系统中监控Redis连接池的使用情况,以及如何通过连接池参数配置、系统资源使用情况、Redis命令监控、外部监控工具等多种方法进行检测和优化,以确保系统在高并发场景下的性能和稳定性,讨论了连接池的概念、工作原理、参数配置,以及优化策略等内容
    2024-09-09
  • Redis数据结构之intset整数集合使用学习

    Redis数据结构之intset整数集合使用学习

    这篇文章主要为大家介绍了Redis数据结构之整数集合使用学习,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-07-07
  • 如何保证Redis与数据库的数据一致性

    如何保证Redis与数据库的数据一致性

    这篇文章主要介绍了如何保证Redis与数据库的数据一致性,文中举了两个场景例子介绍的非常详细,需要的朋友可以参考下
    2023-05-05
  • springboot整合使用云服务器上的Redis方法

    springboot整合使用云服务器上的Redis方法

    这篇文章主要介绍了springboot整合使用云服务器上的Redis,整合步骤通过导入依赖,配置yml文件,注入redisTemplate结合实例代码给大家介绍的非常详细,文中给大家分享了可能遇到的坑,感兴趣的朋友跟随小编一起看看吧
    2022-09-09
  • Redis集群(cluster模式)搭建过程

    Redis集群(cluster模式)搭建过程

    文章介绍了Redis集群的概念、使用原因和搭建方法,Redis集群通过分区实现数据水平扩容,提供了一定的可用性,文章详细阐述了集群的连接方式,解释了如何分配节点,并提供了详细的集群搭建步骤,包括创建节点、清空数据、修改配置、启动节点、配置集群等
    2024-10-10
  • Redis类型type与编码encoding原理及使用示例

    Redis类型type与编码encoding原理及使用示例

    这篇文章主要为大家介绍了Redis类型type与编码encoding原理及使用示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-03-03
  • Redis面试必会的题目

    Redis面试必会的题目

    这篇文章主要介绍了Redis面试必会的题目,帮助大家更好的理解和学习redis数据库,感兴趣的朋友可以了解下
    2020-08-08

最新评论