You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

Spring MVC会话处理:持久化方案、架构设计及请求注解选型问询

嘿,我来帮你把Spring MVC会话这事儿捋明白~

Spring MVC会话机制与持久化架构设计

一、先搞懂Spring MVC的会话基础

Spring MVC底层基于Servlet,所以它的会话本质就是Servlet的HttpSession。用户第一次访问服务器时(没带任何会话标识),服务器会自动创建一个HttpSession实例,生成唯一的会话ID,默认通过名为JSESSIONID的Cookie返回给客户端。之后客户端每次请求都会带上这个Cookie,服务器就能通过会话ID识别出是同一个用户。

二、会话持久化的可行方案

默认的HttpSession存在服务器内存里,一旦服务器重启或者部署集群,会话就会丢失,所以要做持久化的话,有这几个常用方案:

  • Cookie持久化(轻量场景):把用户的非敏感会话数据加密后存在Cookie里,客户端每次请求自动携带。优点是不用服务器存储数据,成本低;缺点是Cookie有4KB左右的大小限制,而且必须加密防止数据被篡改。你可以用Spring的@CookieValue注解读取Cookie,或者自己写拦截器处理。

    划重点:绝对不能把密码这类敏感数据存在Cookie里!只存用户ID、权限标识这类非敏感信息。

  • 数据库持久化:把会话ID和对应的用户数据存在关系型数据库(比如MySQL)里。每次请求过来,服务器拿到会话ID后去数据库查询用户信息。Spring里可以通过HttpSessionListener监听会话的创建、更新、销毁事件,手动处理数据的CRUD;更简单的方式是用Spring Session,它支持JDBC存储,配置好后直接用HttpSession就行,不用改业务代码。

  • Redis持久化(推荐集群/高并发场景):Redis是内存数据库,读写速度快,非常适合存储会话这种高频访问的数据。Spring Session对Redis的支持非常完善,配置后会话数据会自动存在Redis里,集群环境下也能轻松实现会话共享。举个简单的配置例子:

    @Configuration
    @EnableRedisHttpSession(maxInactiveIntervalInSeconds = 1800) // 设置会话30分钟过期
    public class RedisSessionConfig {
        @Bean
        public LettuceConnectionFactory connectionFactory() {
            return new LettuceConnectionFactory();
        }
    }
    

三、整体架构设计思路

如果是构建一个只处理请求、返回信息的服务器,架构可以分成这三层:

  1. 请求接入层:用Spring MVC的拦截器(HandlerInterceptor)处理会话校验。在请求到达Controller之前,检查请求里的会话标识(Cookie或者Header里的会话ID):如果有效,就把用户信息存入请求上下文(比如ThreadLocal),方便后续业务使用;如果无效,直接返回未登录的响应(比如401)。

  2. 业务逻辑层:处理具体业务,比如登录校验、数据查询。登录时,校验用户名密码通过后,生成会话ID并将用户信息存入持久化存储(Redis/数据库),然后把会话ID返回给客户端(可以通过Cookie,也可以让客户端存在本地存储,后续请求在Header里携带)。

  3. 会话存储层:负责会话数据的增删改查,用Redis或者数据库实现。如果用Spring Session,这一层几乎不用自己写代码,框架会自动处理会话的存储与读取。

举个登录流程的实际例子:

  • 用户发送POST请求到/login,携带usernamepassword参数
  • Controller调用业务层校验账号密码,成功后Spring Session自动创建HttpSession,并将用户信息存入Redis
  • 服务器返回带有JSESSIONID Cookie的响应给客户端
  • 后续用户请求时,客户端自动带上JSESSIONID Cookie,拦截器检查Redis中是否存在该会话,存在则允许访问,不存在则返回401未登录

四、登录后要不要每个请求都用@RequestParam?

完全不需要!会话建立后,你有很多更优雅的方式获取用户信息,不用每次让客户端传用户名密码:

  • 直接注入HttpSession:在Controller方法里直接注入HttpSession,从里面取出用户信息:

    @GetMapping("/user/info")
    public ResponseEntity<UserInfo> getUserInfo(HttpSession session) {
        UserInfo user = (UserInfo) session.getAttribute("user");
        return ResponseEntity.ok(user);
    }
    
  • 使用@SessionAttribute注解:直接在方法参数上用@SessionAttribute获取会话中的属性:

    @GetMapping("/user/info")
    public ResponseEntity<UserInfo> getUserInfo(@SessionAttribute("user") UserInfo user) {
        return ResponseEntity.ok(user);
    }
    
  • 通过ThreadLocal简化:在拦截器里把用户信息存入ThreadLocal,Controller里直接从ThreadLocal取,连方法参数都不用加:

    // 自定义拦截器的preHandle方法
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        HttpSession session = request.getSession(false);
        if (session != null) {
            UserInfo user = (UserInfo) session.getAttribute("user");
            UserContext.setUser(user); // UserContext是自定义类,内部维护ThreadLocal<UserInfo>
        }
        return true;
    }
    
    // Controller中使用
    @GetMapping("/user/info")
    public ResponseEntity<UserInfo> getUserInfo() {
        UserInfo user = UserContext.getUser();
        return ResponseEntity.ok(user);
    }
    

这样客户端后续请求只需要带上会话标识,不用每次传身份信息,代码也更简洁。

内容的提问来源于stack exchange,提问作者Daniel

火山引擎 最新活动