点评--day02--2.1 短信登录--基于Session实现短信登录流程

6 阅读4分钟

这一节的核心是先利用传统的 Tomcat Session 机制把登录跑通,为后续引入 Redis 解决分布式 Session 问题做铺垫。

1. 整体业务流程分析

基于 Session 的登录流程主要分为三个核心核心阶段:

  • 发送验证码:用户提交手机号 -> 后台校验手机号是否合法 -> 合法的话生成验证码 -> 将验证码保存到 Session 中 -> 发送验证码给用户。
  • 短信验证码登录/注册:用户提交手机号和验证码 -> 后台从 Session 拿到保存的验证码进行比对 -> 校验一致则根据手机号查询数据库 -> 判断用户是否存在 -> 若不存在则创建新用户并保存到数据库 -> 将最终的用户信息保存到 Session 中,方便后续获取。
  • 校验登录状态(拦截器) :用户发起请求携带 cookie(JsessionId) -> 后台通过 JsessionId 找到对应 Session 并获取用户信息 -> 如果没有信息则拦截请求 -> 如果有信息则将用户信息保存到 ThreadLocal 中并放行请求。

2. 核心功能实现代码

2.1 发送短信验证码

UserController 和对应的 Service 中,主要是校验手机号(通过正则表达式)生成 6 位随机验证码,并存入 Session:

// 3.符合,生成验证码
String code = RandomUtil.randomNumbers(6);
// 4.保存验证码到 session
session.setAttribute("code",code);
// 5.发送验证码 (控制台打印模拟即可)

以上逻辑参考自源文档

2.2 短信验证码登录与注册

根据前端传来的验证码和 Session 中存储的验证码进行比对,如果成功则处理登录逻辑:

// 3.从session获取验证码并校验
Object cacheCode = session.getAttribute("code");
String code = loginForm.getCode();
if(cacheCode == null || !cacheCode.toString().equals(code)){
    return Result.fail("验证码错误");
}
// 4.一致,根据手机号查询用户
User user = query().eq("phone", phone).one();
// 5.判断用户是否存在
if(user == null){
    // 不存在,则创建并保存数据库
    user = createUserWithPhone(phone);
}
// 7.保存用户信息到session中
session.setAttribute("user", user);
return Result.ok();

以上逻辑参考自源文档,

3. 使用 ThreadLocal 与拦截器校验登录状态

为什么使用 ThreadLocal? 当用户发起请求时,Tomcat 会从线程池中取出一个线程来执行请求。每个用户的请求都是独立的,所以我们可以使用 ThreadLocal 来做到线程隔离,让每个线程操作自己独立的一份数据,。

3.1 编写登录拦截器

创建一个 LoginInterceptor 实现 HandlerInterceptor 接口,在 preHandle 方法中拦截请求并校验:

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((User)user);
        // 6.放行
        return true;
    }
}

以上逻辑参考自源文档,

3.2 配置拦截器生效

在 SpringBoot 中需要编写配置类将刚才的拦截器注册进去,并排除不需要登录的路径:

@Configuration
public class MvcConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 登录拦截器
        registry.addInterceptor(new LoginInterceptor())
                .excludePathPatterns(
                        "/shop-type/**",
                        "/upload/**",
                        "/blog/hot",
                        "/user/code",
                        "/user/login"
                ).order(1);
    }
}

以上逻辑参考自源文档,,

4. 优化点:隐藏用户敏感信息

如果我们直接把数据库查询出来的 User 对象(包含密码等信息)存入 Session 并返回给前端,是非常不安全的。 解决方案: 核心思路是书写一个去除了敏感信息的 UserDTO 对象。在存入 Session 之前,将 User 对象转化为 UserDTO 对象:

// 保存时进行对象拷贝转换
session.setAttribute("user", BeanUtils.copyProperties(user, UserDTO.class));
// ThreadLocal 中保存的也换成 UserDTO
UserHolder.saveUser((UserDTO) user);

以上逻辑参考自源文档,

5. 抛出问题:Session 共享问题

按照以上步骤,单体架构下的登录功能就完美实现了。但是黑马点评是一个面向高并发的项目,我们会搭建 Tomcat 集群。 存在的问题: 多台 Tomcat 并不共享 Session 存储空间。每个 Tomcat 中都有自己的 Session。假设用户在 Tomcat1 上登录了(信息存放在了 Tomcat1 的 Session 中),但后续的请求被 Nginx 负载均衡分配到了 Tomcat2 上,由于 Tomcat2 中没有该用户的 Session 数据,就会导致用户状态丢失,被拦截器拦截。 解决方案的演进: 早期的方案是 Session 拷贝(服务器之间互相拷贝 Session 数据),但这会导致服务器内存压力过大且存在数据延迟,。 因此,在接下来的课程中,我们将会采用 Redis 来代替 Session。因为 Redis 数据本身就是独立部署且共享的,完美契合集群架构下的状态管理。