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(); } }
三、整体架构设计思路
如果是构建一个只处理请求、返回信息的服务器,架构可以分成这三层:
请求接入层:用Spring MVC的拦截器(
HandlerInterceptor)处理会话校验。在请求到达Controller之前,检查请求里的会话标识(Cookie或者Header里的会话ID):如果有效,就把用户信息存入请求上下文(比如ThreadLocal),方便后续业务使用;如果无效,直接返回未登录的响应(比如401)。业务逻辑层:处理具体业务,比如登录校验、数据查询。登录时,校验用户名密码通过后,生成会话ID并将用户信息存入持久化存储(Redis/数据库),然后把会话ID返回给客户端(可以通过Cookie,也可以让客户端存在本地存储,后续请求在Header里携带)。
会话存储层:负责会话数据的增删改查,用Redis或者数据库实现。如果用Spring Session,这一层几乎不用自己写代码,框架会自动处理会话的存储与读取。
举个登录流程的实际例子:
- 用户发送POST请求到
/login,携带username和password参数 - Controller调用业务层校验账号密码,成功后Spring Session自动创建
HttpSession,并将用户信息存入Redis - 服务器返回带有
JSESSIONIDCookie的响应给客户端 - 后续用户请求时,客户端自动带上
JSESSIONIDCookie,拦截器检查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




