传统 Java Web 的登录态治理,最核心的就是:
这套模型虽然不是现代前后端分离项目的唯一主流,但它仍然是理解登录态、会话安全、CSRF 风险和无状态 Token 方案的基础。
Cookie 是浏览器侧保存的一小段状态数据。它最常见的作用是:
Cookie 本身通常不负责保存完整用户状态,更常见的是保存:
1. 服务端通过 Set-Cookie 响应头设置 Cookie
2. 浏览器自动保存 Cookie
3. 浏览器后续请求自动携带 Cookie(Cookie 请求头)
4. 服务端从请求头中读取 CookieHTTP 响应(设置 Cookie):
HTTP/1.1 200 OK
Set-Cookie: JSESSIONID=abc123; Path=/; HttpOnly; Secure; SameSite=Lax
Content-Type: text/htmlHTTP 请求(携带 Cookie):
GET /user/profile HTTP/1.1
Cookie: JSESSIONID=abc123| 属性 | 说明 | 示例 |
|---|---|---|
Name=Value | Cookie 的名称和值 | JSESSIONID=abc123 |
Domain | Cookie 生效的域名 | .example.com |
Path | Cookie 生效的路径 | /app |
Max-Age | Cookie 过期时间(秒) | 3600 |
Expires | Cookie 过期的具体时间 | Wed, 09 Jun 2021 10:18:14 GMT |
HttpOnly | 禁止 JavaScript 访问(防 XSS) | - |
Secure | 仅 HTTPS 传输 | - |
SameSite | 跨站请求限制(防 CSRF) | Strict / Lax / None |
Session 更偏服务端会话存储。它的典型流程是:
所以 Session 模式本质上是:
| 存储方式 | 说明 | 适用场景 |
|---|---|---|
| 内存存储 | 默认方式,存在 JVM 内存中 | 单机、开发环境 |
| Redis 存储 | 分布式 Session,存在 Redis 中 | 多实例部署 |
| 数据库存储 | 存在数据库表中 | 低频访问、持久化需求 |
| 文件存储 | 序列化到磁盘 | 容器重启后恢复 |
最基础的 Session 登录流程通常如下:
@WebServlet("/login")
public class LoginServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String username = request.getParameter("username");
String password = request.getParameter("password");
// 1. 校验用户名和密码(实际应查数据库)
if (!"admin".equals(username) || !"123456".equals(password)) {
request.setAttribute("error", "用户名或密码错误");
request.getRequestDispatcher("/login.jsp").forward(request, response);
return;
}
// 2. 校验成功,创建 Session
HttpSession session = request.getSession(true);
session.setAttribute("loginUser", username);
session.setAttribute("loginTime", new Date());
// 3. 重定向到主页
response.sendRedirect(request.getContextPath() + "/home");
}
}登出处理:
@WebServlet("/logout")
public class LogoutServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 销毁 Session
HttpSession session = request.getSession(false);
if (session != null) {
session.invalidate(); // 使 Session 失效
}
// 重定向到登录页
response.sendRedirect(request.getContextPath() + "/login");
}
}权限校验(Filter 方式):
@WebFilter(urlPatterns = "/user/*")
public class LoginCheckFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
// 检查 Session 中是否有登录信息
HttpSession session = req.getSession(false);
if (session == null || session.getAttribute("loginUser") == null) {
// 未登录,重定向到登录页
resp.sendRedirect(req.getContextPath() + "/login");
return;
}
// 已登录,放行
chain.doFilter(request, response);
}
}一旦浏览器自动带上 Cookie,就要同时考虑:
CSRF 风险这也是为什么登录态问题从来不只是"前端有没有跳转成功",而是完整的会话安全问题。
<!-- web.xml 中配置 Session 超时时间(分钟) -->
<session-config>
<session-timeout>30</session-timeout>
</session-config>也可以在代码中动态设置:
// 设置 Session 超时时间为 1 小时(秒)
session.setMaxInactiveInterval(3600);
// 获取剩余有效时间
int remaining = session.getMaxInactiveInterval();Session 过时后的行为:
request.getSession(false) 返回 nullrequest.getSession(true) 创建新的 Session| 机制 | 存储位置 | 主要作用 |
|---|---|---|
| Cookie | 浏览器端 | 保存会话标识或轻量状态,随请求自动带上 |
| Session | 服务端 | 保存用户登录态和会话上下文 |
很多初学者容易把 Cookie 和 Session 当成同一个东西,其实它们是一对配合机制:
这是最经典的场景:
这类问题通常不是"前端没跳转",而是:
因此登出操作通常不仅要删前端标识,还要同步使服务端会话失效。
一旦系统多实例部署,如果 Session 只存在单机内存,就会出现:
这也是后来很多系统要么引入分布式 Session,要么走无状态 Token 模式的原因。
如果用户禁用了 Cookie,Session ID 无法通过 Cookie 传递,解决方案:
// 方式1:URL 重写(URL 中附带 Session ID)
String url = response.encodeURL("/user/profile");
// 输出: /user/profile;jsessionid=abc123
// 方式2:表单隐藏字段
// <input type="hidden" name="jsessionid" value="abc123">如果仍然使用 Session 模式,常见做法包括:
| 方案 | 实现复杂度 | 扩展性 | 推荐度 |
|---|---|---|---|
| Session 粘滞 | 低 | 差 | 一般(仅作过渡方案) |
| Redis Session | 中 | 好 | 推荐 |
| JWT/Token | 中 | 最好 | 推荐(前后端分离首选) |
| 数据库 Session | 中 | 一般 | 不推荐(性能差) |
<!-- pom.xml -->
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency># application.yml
spring:
session:
store-type: redis
redis:
host: localhost
port: 6379// 配置后,Session 自动存储到 Redis
// 多个实例共享同一个 Redis,实现 Session 共享
@EnableRedisHttpSession
@Configuration
public class SessionConfig {
}// 登录成功后返回 Token
@PostMapping("/login")
public Result login(@RequestBody LoginDTO dto) {
User user = userService.authenticate(dto);
String token = jwtUtil.generateToken(user.getId(), user.getUsername());
return Result.success(token);
}
// 后续请求在 Header 中携带 Token
@GetMapping("/profile")
public Result profile(@RequestHeader("Authorization") String token) {
Long userId = jwtUtil.getUserId(token);
User user = userService.getById(userId);
return Result.success(user);
}需要注意:
CSRF因为浏览器会自动携带 Cookie,所以攻击者可以诱导用户访问恶意页面,让浏览器在用户不知情的情况下向目标站点发请求。
这就是 CSRF 风险的核心来源:
CSRF 攻击示例:
<!-- 恶意网站上的页面 -->
<!-- 用户已登录银行网站,Session Cookie 会自动携带 -->
<img src="https://bank.example.com/transfer?to=hacker&amount=1000" style="display:none" />
<!-- 浏览器渲染该 img 时会向银行站点发起 GET 请求并自动携带 Cookie -->| 安全措施 | 说明 | 防护场景 |
|---|---|---|
| 会话固定防护 | 登录成功后重新生成 Session ID | 会话固定攻击 |
| HttpOnly | 禁止 JS 访问 Cookie | XSS 窃取 Cookie |
| Secure | 仅 HTTPS 传输 | 中间人窃听 |
| SameSite | 跨站请求限制 | CSRF 攻击 |
| 会话超时 | 合理设置过期时间 | 会话劫持 |
| 登出清理 | 销毁服务端 Session | 登出后残留访问 |
| CSRF Token | 每次请求携带随机 Token | CSRF 攻击 |
// 登录成功后迁移 Session(防止会话固定攻击)
HttpSession oldSession = request.getSession(false);
if (oldSession != null) {
oldSession.invalidate();
}
HttpSession newSession = request.getSession(true);
newSession.setAttribute("loginUser", username);会话问题排查时优先看:
用户反馈"未登录"或"登录态丢失":
□ 浏览器是否携带了 Cookie?
→ F12 → Network → 检查 Request Headers 中是否有 Cookie
□ Cookie 中的 JSESSIONID 是否正确?
→ F12 → Application → Cookies → 检查值
□ 服务端是否创建了 Session?
→ 服务端日志中搜索 Session 创建记录
□ Session 是否已过期?
→ 检查 Session 超时配置和最后访问时间
□ 是否多实例部署?
→ 检查 Session 存储方式是否支持共享
□ 是否有过滤器拦截了请求?
→ 检查 Filter 链配置和日志HttpOnly 和 Secure 属性Cookie 和 Session 分别存什么?
为什么 Session 模式更容易引出 CSRF 风险?
多实例部署下 Session 会遇到什么问题?
传统登录态和无状态 Token 模式的核心差异是什么?
如何防止 CSRF 攻击?
如何实现分布式 Session?
用户反馈"登录后刷新页面就回到登录页",如何排查?
Session 固定攻击如何防范?
session.invalidate() 重新创建 SessionchangeSessionId() 迁移 Session ID| 特性 | 旧版(本文编写时) | 当前(Servlet 6.0 / Spring Boot 3.5.x) |
|---|---|---|
| Cookie/Session 机制 | javax.servlet.http.* | 机制不变,包名迁移为 jakarta.servlet.http.* |
| 登录态方案 | 服务端 Session | 新项目多采用 JWT/Redis 会话(无状态) |
| Session 安全 | 固定攻击防护手段不变 | 此外:SameSite Cookie、HttpOnly 均需开启 |
| 虚拟线程 | 无 | 注意 Session 与虚拟线程的线程绑定关系(ThreadLocal 保留) |
Session/Cookie 是 Web 登录态的基础,机制多年不变;新项目建议优先考虑 JWT/Redis 方案并配置 Secure/HttpOnly/SameSite Cookie 属性。