Skip to content

登录状态与 Token 机制 ​

S3-040建议学进阶约 45 分钟JDK 21企业开发基础 · 前端基础与 Vue

1. 本节定位 ​

《Spring Security 认证与授权》 讲了服务端怎么"验 token";《Filter、Listener 与会话管理》 讲了三种方案的取舍。本节站在前端视角补最后一环:

text
token 拿到后 → 存在哪?怎么带?过期了怎么办?刷新页面还在吗?

2. 学习目标 ​

  • 说出 token 的三种存储位置及各自的 XSS/CSRF 风险 ★★★;
  • 会做"登录 → 存 token → 自动携带 → 401 跳登录"的完整闭环 ★★;
  • 理解 access token + refresh token 的双 token 机制 ★★;
  • 知道前端路由守卫的作用;
  • 能解释"token 放 localStorage 会被 XSS 偷"与"放 Cookie 有 CSRF 风险"的权衡 ★★★。

3. 前置知识检查 ​

4. 为什么需要这个知识 ​

登录态是"前后端契约"里最容易出安全事故的一环 ✗

事故根因
token 被 XSS 偷走存在了 localStorage 且页面有 XSS ✗
登录后刷新页面又变未登录token 没持久化,或没在启动时恢复 ✓(这是 bug 不是安全)
token 永不过期泄漏后无法止损 ✗✗

5. 核心概念 ​

5.1 三种存储位置的对比 ★★★ ​

存储位置XSS 能偷吗CSRF 风险刷新后还在典型场景
localStorage✅ 能 ✗✗❌ 无 ✓✅ 在SPA(接受 XSS 风险的取舍)⚠️
内存(JS 变量)✅ 能(运行期)❌ 无❌ 刷新即失 ✗最安全(配 refresh token)✓
Cookie(HttpOnly)❌ 不能 ✓✓⚠️ 有(要防 CSRF)✅ 在同域传统应用 ✓

★ 没有完美方案,只有取舍 ✓

派别逻辑
HttpOnly Cookie 派"XSS 偷不走会话" ✓——代价是要防 CSRF(《Filter、Listener 与会话管理》)⚠️
localStorage 派"跨域/多端方便" ✓——代价是"有 XSS 就全丢" ⚠️ ——所以要把 XSS 防线做牢(《JSP 与 JSTL(技术演进理解)》 的默认转义)✓

实务建议:

场景推荐
同域单体HttpOnly Cookie + Session(《Filter、Listener 与会话管理》)✓
跨域 SPA内存 access token(短)+ HttpOnly refresh token(长) ★★
移动端Secure 存储(系统级 keystore)✓

5.2 双 Token 机制 ★★ ​

text
access token :短命(15 分钟),放内存 ★,每次请求携带
refresh token:长命(7 天),放 HttpOnly Cookie ★,仅用于"换新 access token"

access 过期 → 用 refresh 换新的 access → 用户无感 ✓
refresh 失效 → 真正重新登录 ✓

为什么这么设计:

设计收益
access 放内存刷新页面就没了 → 即使被 XSS 偷走,最多用 15 分钟 ✓
refresh 放 HttpOnly CookieJS 摸不到 ✓ + 后端可主动吊销 ✓
access 短命"改密码/封号"的生效延迟最多 15 分钟 ✓

★ 这就是"无状态的便利"与"可吊销的安全"的组合拳 ✓

5.3 完整闭环的实现 ★★ ​

javascript
// auth.js —— 登录态的统一封装
const tokens = { access: null };

export async function login(username, password) {
  const r = await fetch('/api/auth/login', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    credentials: 'include',                        // refresh token 在 HttpOnly Cookie 里 ★
    body: JSON.stringify({ username, password }),
  });
  if (!r.ok) throw new Error((await r.json()).message);
  const body = await r.json();
  tokens.access = body.token;                      // ★ access 只放内存
}

export async function api(path, options = {}) {
  const r = await fetch(path, {
    ...options,
    headers: {
      'Content-Type': 'application/json',
      ...(tokens.access ? { Authorization: `Bearer ${tokens.access}` } : {}),
      ...options.headers,
    },
  });

  if (r.status === 401 && tokens.access) {
    // ★ access 过期:用 refresh(Cookie 自动带上)换新 token,再重试一次
    const rr = await fetch('/api/auth/refresh', { method: 'POST', credentials: 'include' });
    if (rr.ok) {
      tokens.access = (await rr.json()).token;
      return api(path, options);                   // 重试原请求 ✓
    }
    tokens.access = null;
    location.href = '/login';                      // refresh 也失效 → 真正重新登录
  }
  if (!r.ok) throw new Error((await r.json().catch(() => ({}))).message || `HTTP ${r.status}`);
  return r.status === 204 ? null : r.json();
}

闭环里的五个关键点 ★:

#设计为什么
①access 放内存、refresh 放 HttpOnly Cookie双方的风险各自最小(5.1)✓
②401 时先尝试 refresh 再重试用户无感续期 ✓
③refresh 也失效才跳登录避免误踢 ✓
④封装在 api() 里业务代码不碰 token 细节 ✓(关注点分离)
⑤credentials: 'include'Cookie 自动带上(《Ajax 与前后端交互》)✓

5.4 前端路由守卫 ★ ​

"没登录就不让进某些页面"(前端的"权限检查"):

javascript
// Vue Router
router.beforeEach((to) => {
  const auth = useAuthStore();
  if (to.meta.requiresAuth && !auth.isLoggedIn) {
    return { path: '/login', query: { redirect: to.fullPath } };   // ★ 记住"想去哪"
  }
});
// 登录成功后:router.push(route.query.redirect ?? '/')          ← 体验细节 ✓

★ 守卫只是"体验层" ⚠️:

真正的权限控制必须在后端(《Spring Security 认证与授权》)✓

前端守卫"关掉"只影响体验 ——直接 curl 接口照样能调 ✗

这与 《Filter、Listener 与会话管理》 的"前端守卫只是体验层"一致 ✓

5.5 退出登录 ​

javascript
export function logout() {
  tokens.access = null;                            // 清内存 ✓
  fetch('/api/auth/logout', { method: 'POST', credentials: 'include' });   // ★ 服务端吊销 refresh
  // Cookie 由后端 Set-Cookie 过期指令清除 ✓
}

★ 关键:"退出"必须同时做两件事:前端清凭证 + 后端吊销 ✓

只清前端 → token 如果被偷了仍在有效期内可用 ✗

这与 《Filter、Listener 与会话管理》 的"Session invalidate"是同一件事的两端 ✓

6. 递进示例 ​

6.1 最小示例 ​

javascript
localStorage.setItem('token', 'xxx');              // 简单但 XSS 可偷 ⚠️
fetch('/api/me', { headers: { Authorization: `Bearer ${localStorage.getItem('token')}` } });

6.2 业务示例 ​

见 5.3 的闭环 ✓

6.3 反例:token 放在 URL 参数里 ​

bash
GET /api/orders?token=eyJhbGciOi...

问题:

问题说明
URL 会进日志/历史/Referer ✗✗token 泄漏面极大 ✗
无法设置 HttpOnly ✗—

修:放 Authorization 头 ✓(《Filter、Listener 与会话管理》)✓

7. Debug 观察点 ​

  1. F12 → Application → Cookies:确认 refresh token 是 HttpOnly ✓;
  2. F12 → Network 看请求头:确认 Authorization 真的带了 ✓("以为带了"是常见错觉)⚠️;
  3. 401 的重试:在 Network 里观察"401 → refresh → 重试原请求"三步 ✓;
  4. 清 Cookie 后刷新:应被守卫拦到登录页 ✓。

8. 常见错误 ​

#错误说明
1refresh token 放 localStorage ✗✗长命凭证被 XSS 偷走 = 长期沦陷 ✗
2access token 永不过期 ✗✗无法止损(《Spring Security 认证与授权》)✗
3401 时无限 refresh 重试 ✗refresh 也失效时要停 ✗(死循环)
4登出只清前端后端 refresh 还有效 ✗(5.5)
5把用户角色存进 JWT 且信任 ⚠️JWT 过期前无法收回角色变更 ——敏感操作要再查一次 ✓

9. 动手实验 ​

  1. 实现 5.3 的 auth.js(配合 《Spring Security 认证与授权》 的登录接口)✓;
  2. 观察三步链路:401 → refresh → 重试(F12 Network)✓;
  3. 验证 HttpOnly:尝试 document.cookie 读取 refresh token——读不到 ✓;
  4. 缩短 access 有效期到 1 分钟:观察"无感续期"的频率 ✓。

扩展任务:实现"记住我":勾选时 refresh token 有效期 7 天,不勾选时关闭浏览器即失效——说明你把它实现在"前端存储"还是"后端签发",为什么 ✓

10. 练习题 ​

  1. 判断:把 JWT 放在 localStorage 是最安全的方案。(对 / 错)
  2. 填空:access token 放 ______,refresh token 放 ______ Cookie。
  3. 简答:为什么 access token 要短命?
  4. (代码阅读)credentials: 'include' 的作用是什么?后端需要哪两个响应头配合?
  5. (编程)实现"登录后跳回原页面"(守卫 + redirect 参数)。
参考答案(要点)
  1. 错。XSS 可读 ✗——它是"方便与安全的权衡"(5.1)⚠️
  2. 内存(JS 变量);HttpOnly ✓
  3. 缩短"被偷后的可用时间" ——泄漏的损失窗口 = 有效期 ✓(配合 refresh 实现无感续期 ✓)
  4. 让浏览器跨域请求携带 Cookie ✓;后端需 Allow-Credentials: true 且 Allow-Origin 不能是 * ✓(《Filter、Listener 与会话管理》)★
  5. 守卫里 query: { redirect: to.fullPath };登录成功后 router.push(route.query.redirect ?? '/') ✓

12. 掌握标准 ​

  • [ ] 能说出三种存储位置的取舍 ★★★;
  • [ ] 理解双 token(access + refresh)的设计与收益 ★★;
  • [ ] 会实现"401 → refresh → 重试"闭环;
  • [ ] 知道路由守卫只是体验层(后端才是权限的权威)✓;
  • [ ] 知道"登出要前后端同时做" ✓。

13. 扩展资源 ​

14. 下一步学习 ​

前端五节(《HTML 与 CSS 基础》 / 《JavaScript 基础》 / 《Ajax 与前后端交互》 / 《Vue 基础与组件开发》 / 《登录状态与 Token 机制》)到此完成。接下来:

  • S3-029 Gradle 概览:另一种构建工具
  • T2-001 HTTP 接口测试设计:为这些接口写自动化测试

后续知识

暂无

进度只保存在本机浏览器,可随时清除

个人学习项目:AI 生成内容均经人工复核,第三方资源仅作索引