
一、基于Session的登陆实现Session是什么?Session 是服务端保存会话状态的机制。客户端通过JSESSIONID标识自己的 Session服务器根据JSESSIONID找到对应 Session从而获取验证码和当前登录用户实现流程客户端 ↓ 发送手机号 ↓ 服务器生成验证码 ↓ 验证码保存到 Session ↓ 响应 JSESSIONID 客户端再次请求 ↓ 携带 JSESSIONID 验证码 ↓ 服务器根据 JSESSIONID 找到 Session ↓ 取出验证码校验 ↓ 登录成功 ↓ 用户信息保存到 Session 后续请求 ↓ 携带 JSESSIONID ↓ 服务器找到 Session ↓ 获取当前登录用户短信验证码-发送if (RegexUtils.isPhoneInvalid(phone)) { return Result.fail(手机号格式错误); } String code RandomUtil.randomNumbers(6); session.setAttribute(code, code); }① 手机号校验属于公共逻辑因此封装到RegexUtils。② 验证码属于临时会话数据因此可以暂存在 Session。③/user/code使用 POST因为它是在触发“生成并发送验证码”这一业务操作而不是查询资源。验证码校验 自动注册LoginFromDTO登录接口只需要phone、code等登录参数而数据库实体User代表完整用户数据因此使用LoginFormDTO接收登录参数避免接口层直接依赖数据库实体。JSESSIONID 到底干什么用户登录时服务器生成code保存到Session中返回JSESSIONID给客户端后续请求携带 JSESSIONID服务器通过 JSESSIONID 找到该浏览器对应的 Session如果 Session 中保存了登录用户信息就可以确定当前请求对应哪个已登录用户JSESSIONID用来索引查询每个Session和JWT的区别Session 客户端保存 JSESSIONID 用户信息主要保存在服务器 JWT 客户端保存 Token 用户身份信息编码在 Token 中 服务器解析 Token 获取身份登陆成功后保存带Session// 从Session获取验证码进行校验 Object cacheCode session.getAttribute(code); if (cacheCode null || !cacheCode.toString().equals(loginForm.getCode())) { return Result.fail(验证码错误); } // 登录成功将精简后的用户信息保存到Session session.setAttribute( user, BeanUtil.copyProperties(user, UserDTO.class) );拦截器实现登录校验Override public boolean preHandle( HttpServletRequest request, HttpServletResponse response, Object handler) { // 从Session获取当前登录用户 UserDTO user (UserDTO) request.getSession() .getAttribute(user); // 未登录 if (user null) { response.setStatus(401); return false; } // 保存到ThreadLocal供后续业务使用 UserHolder.saveUser(user); return true; } Override public void afterCompletion(...) { UserHolder.removeUser(); }用ThreadLocal实现用户信息的保存方便Controller和Service层获取用户信息afterCompletion要将用户信息从ThreadLocal中去除使用ThreadLocal要定义一个Holder工具类public class UserHolder { private static final ThreadLocalUserDTO 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(); } }使用拦截器要用mvc配置类实现拦截器的配置Configuration public class MvcConfig implements WebMvcConfigurer { Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoginIntercepter()) .excludePathPatterns( /shop/**, /voucher/**, /shop-type/**, /upload/**, /blog/hot, /user/code, /user/login ); } }排除路径中添加可以直接放行的请求路径User中有很密码敏感信息和无关信息是不需要返回给前端的所以在往Session中保存用户信息是保存UserDTO使用BeadUtil.copyProperties实现复制属性session.setAttribute(user, BeanUtil.copyProperties(user, UserDTO.class));集群Session共享问题多台Tomcat并不共享session存储空间去切换到不同tomcat服务时导致数据丢失二、基于Redis实现登录为什么改用Session为RedisRedis 可以作为多个服务实例共享的登录状态存储解决Session不能在服务器共享的问题生成的验证码保存在Redis中key是phonevalue是code使用String类型设置存储有效期为 2min// 3.保存验证码到 redis中,设置 key-value,保存时间为2min stringRedisTemplate.opsForValue() .set(LOGIN_CODE_KEY phone,code,LOGIN_CODE_TTL, TimeUnit.MINUTES);登陆时如何验证Redis的验证码cacheCodeRedis中服务器保存的正确验证码code用户提交的验证码判断两者是否一致String cacheCode stringRedisTemplate.opsForValue() .get(LOGIN_CODE_KEY phone); String code loginForm.getCode(); if(cacheCode null || !cacheCode.equals(code)){ return Result.fail(验证码错误); }验证码通过后利用MyBatis-Plus查询用户User user query() .eq(phone, phone) .one();用户不存在就创建新用户将属性传入然后保存到数据库中自动注册save(newUser);Redis登录状态保存拿到用户信息后不能直接整个 User 存进Redis因为User中包含敏感信息密码或无关字段所以需要UserDTO安全的用户信息DTO的意义只保存/传递当前场景真正需要的数据同时减少存储占用3.1 先生成TokenString token UUID.randomUUID().toString();作为客户端访问 Redis 登录信息的凭证/索引本身不携带用户SESSIONID、随机 Token、JWT的区别SESSIONID、随机 Token、JWT 都用于让服务器识别请求所属用户但实现机制不同。JSESSIONID 和 Redis Token 本身主要是身份索引需要到服务端 Session/Redis 中查询登录状态JWT 本身携带用户声明服务器通过验签和解析 JWT 获取用户信息。Redis Token 属于有状态认证便于服务端主动失效和管理登录状态JWT 通常属于无状态认证减少服务端会话存储但主动注销和失效控制更复杂。3.2 将用户信息 UserDTO 转为 MapRedis中使用Map存储用户信息实现MapString,Object userMap BeanUtil.beanToMap( userDTO, new HashMap(), CopyOptions.create() .setIgnoreNullValue(true) .setFieldValueEditor( (fieldName, fieldValue) - fieldValue.toString() ) );在 StringRedisTemplate中Key、Value、HashKey、HashValue 都主要按照字符串方式处理需要将所有 Java对象的属性都转为StringRedis可保存的形式所以需要fieldValue.toString()3.3 把整个 Java Map 写入一个 Redis HashstringRedisTemplate.opsForHash() .putAll(tokenKey, userMap);3.4 设置登录状态有效期 TTLstringRedisTemplate.expire( tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES );3.5 将Token返回客户端return Result.ok(token);下次用户请求时携带 Tokenauthorization: token3.6 数据链的转换MySQL ↓ User实体 ↓ BeanUtil.copyProperties() ↓ UserDTO ↓ BeanUtil.beanToMap() ↓ MapString,Object ↓ 字段统一转String ↓ Redis Hash在拦截器中Redis Hash ↓ MapObject,Object ↓ BeanUtil.fillBeanWithMap() ↓ UserDTO ↓ ThreadLocal形成了一个闭环登录 UserDTO → Map → Redis 请求 Redis → Map → UserDTORedis登陆状态设计总结登录成功后服务器随机生成 Token并将数据库中的User转换为只包含必要登录信息的UserDTO。为了使用 Redis Hash 保存用户信息再将UserDTO转换成 Map同时过滤 null 字段并将字段值统一转换成 String以适配StringRedisTemplate。使用LOGIN_USER_KEY token作为 Redis Key以 Hash 结构保存用户的 id、昵称、头像等登录信息并设置 TTL。Token 返回客户端客户端后续请求通过请求头携带 Token服务器利用 Token 从 Redis 中找到当前用户。因此Token 是访问登录状态的凭证Redis 是登录状态真正的存储位置UserDTO 用于控制保存的数据范围Hash 用于保存结构化用户信息TTL 用于控制登录有效期。拦截器的设计5.1 拦截器设计的核心思想把所有请求都会遇到的公共逻辑从 Controller 中抽离出来在请求真正进入 Controller 之前统一处理拦截器用于抽取多个接口共有的前置/后置处理逻辑并根据不同 URL 配置不同的处理规则。Redis 登录中采用两个拦截器进行职责分离RefreshTokenInterceptor拦截所有请求只负责尝试解析 Token、恢复当前用户和刷新 TTL没有 Token 时也应该放行LoginInterceptor只拦截需要登录的接口负责根据 ThreadLocal 判断用户是否登录未登录时才真正返回 401。5.1 链路客户端请求 ↓ 携带 authorization: token ↓ RefreshTokenInterceptor order(1) ↓ 根据Token去Redis查询用户 ↓ 查询到用户 → 保存到ThreadLocal → 刷新Token有效期 ↓ LoginInterceptor order(2) ↓ 判断ThreadLocal是否存在用户 ↓ 有用户 → 放行 无用户 → 401 ↓ Controller → Service ↓ 请求结束 ↓ 清理ThreadLocal5.2RefreshTokenInterceptor负责“解析身份 刷新登录状态”先拿 TokenString token request.getHeader(authorization);从Redis中取出用户信息MapObject,Object userMap stringRedisTemplate.opsForHash() .entries(LOGIN_USER_KEY token);如果 Redis 有用户转为UserDTO再保存带ThreadLocal中UserDTO userDTO BeanUtil.fillBeanWithMap( userMap, new UserDTO(), false ); UserHolder.saveUser(userDTO);Controller Service AOP都可以直接UserHolder.getUser();不需要再次访问 Redis。还要刷新 TTLstringRedisTemplate.expire( LOGIN_USER_KEY token, LOGIN_USER_TTL, TimeUnit.MINUTES );5.2LoginInterceptor负责真正的登录校验if(UserHolder.getUser() null){ response.setStatus(401); return false; } return true;其他6.1 Spring原则只有交给 Spring IOC 容器管理的对象Spring 才能帮它完成Autowired、Resource等依赖注入。自己手动new出来的对象不属于 Spring BeanSpring 不会自动给它注入依赖因此需要由一个 Spring Bean 先获取所需依赖再通过构造器将依赖传递给手动创建的对象举例Resouce private StringRedisTemplate stringRedisTemplate; ... ...(new LoginInterceptor(stringRedisTemplate))6.2 BeanUtil常用API转换方向常用方法典型场景Bean → BeancopyProperties()Entity → DTO / VOBean → MapbeanToMap()Redis Hash、动态数据Map → BeanfillBeanWithMap()Redis Hash → DTOMap → 新 BeantoBean()Map 转实体List → ListcopyToList()Entity 列表 → DTO 列表6.3 Mybatis-plusUser user query() .eq(phone, phone) .one();查询tb_user表中 phone 等于当前手机号的一条用户数据并封装成 UserAPI作用类似 MyBatisBaseMapperTMapper 通用 CRUD自己写 Mapper SQLIServiceTService 通用 CRUD 接口自己定义 Service 方法ServiceImplM,T通用 Service 实现自己实现 Service CRUDquery()开始构建查询SELECT ... WHERE ...eq()等值条件WHERE xxx ?one()查询一条返回一个实体list()查询多条ListTsave()新增INSERT