从零构建带AI流式对话的Vue2全栈应用-CSDN博客本文是对这篇文章阶段五的讲解
一、阶段五为什么要改 Token 保存方式
阶段四:
Token 保存在 Vuex
前端 JavaScript 可以读取 Token
刷新后 Token 消失
阶段五:
Token 保存在 HttpOnly Cookie
JavaScript 不能读取 Token
浏览器自动携带 Cookie
刷新后可以恢复登录
二、Cookie 是什么
Cookie 是浏览器保存的一小段数据。
服务器返回:
Set-Cookie: session_token=abc123
浏览器保存后,之后访问同一个网站会自动发送:
Cookie: session_token=abc123
前端页面不需要自己把 Token 加到请求头。
三、cookie-parser
安装:
javascript
npm install cookie-parser
代码:
javascript
const cookieParser =
require('cookie-parser')
app.use(cookieParser())
它会把请求 Cookie 解析到:
javascript
req.cookies
例如:
javascript
req.cookies.session_token
可以获取会话 Token。
四、Cookie 配置
javascript
const sessionCookieOptions = {
httpOnly: true,
secure: false,
sameSite: 'lax',
path: '/',
maxAge: 24 * 60 * 60 * 1000
}
1. httpOnly
javascript
httpOnly: true
表示前端 JavaScript 不能读取这个 Cookie。
下面代码读不到会话 Token:
javascript
document.cookie
这样可以降低恶意脚本直接窃取 Token 的风险。
2. secure
javascript
secure: false
表示 HTTP 也允许发送 Cookie。
本地项目使用:
http://localhost
所以必须暂时是 false。
正式上线使用 HTTPS 时应该改为:
javascript
secure: true
3. sameSite
javascript
sameSite: 'lax'
用于限制跨网站请求携带 Cookie。
它可以减少一部分 CSRF 风险。
4. path
javascript
path: '/'
表示这个 Cookie 对整个网站路径都有效。
包括:
/api/users
/api/auth/me
/api/ai/stream
5. maxAge
maxAge:
24 * 60 * 60 * 1000
计算过程:
24 小时
× 60 分钟
× 60 秒
× 1000 毫秒
所以保存 24 小时。
五、登录成功设置 Cookie
javascript
res.cookie(
SESSION_COOKIE_NAME,
token,
sessionCookieOptions
)
可以理解成让响应头包含:
Set-Cookie: session_token=token内容
然后后端只返回用户:
javascript
res.json({
code: 0,
data: {
user: codeRecord.user
}
})
不再返回:
javascript
token
因为前端不应该读取 HttpOnly Token。
六、阶段五鉴权中间件
javascript
const token =
req.cookies[SESSION_COOKIE_NAME]
不再读取:
javascript
req.headers.authorization
而是读取 Cookie。
完整流程:
浏览器请求 /api/users
↓
自动携带 Cookie
↓
cookie-parser 解析
↓
req.cookies.session_token
↓
loginSessions.get(token)
↓
找到当前用户
↓
next()
七、为什么还需要后端 loginSessions
Cookie 中只保存一个随机 Token:
abc123
真正用户信息保存在后端:
javascript
loginSessions.set(token, {
user: mockWechatUser,
expiresAt: ...
})
对应关系:
abc123
↓
微信学习者
不能只相信浏览器自己传来的用户名称。
应该由后端根据 Token 查找真实用户。
八、/api/auth/me 的作用
刷新页面后,Vuex 中:
javascript
currentUser === null
但浏览器可能仍有有效 Cookie。
前端调用:
GET /api/auth/me
浏览器自动带 Cookie。
后端返回:
javascript
req.currentUser
前端重新把用户放进 Vuex。
所以:
刷新页面
↓
Vuex 清空
↓
调用 /api/auth/me
↓
Cookie 仍有效
↓
后端返回用户
↓
Vuex 恢复 currentUser
九、authChecked 为什么必要
Vuex:
javascript
state: {
currentUser: null,
authChecked: false
}
页面刚启动时:
javascript
currentUser === null
此时不能立即判断用户没登录,因为还没有检查 Cookie。
所以增加:
javascript
authChecked
含义:
false:还没有询问后端
true:已经完成登录状态检查
十、Vuex Action initializeAuth
javascript
async initializeAuth({
state,
commit
}) {
if (state.authChecked) {
return Boolean(
state.currentUser
)
}
try {
const user =
await getCurrentUser()
commit(
'setCurrentUser',
user
)
return true
} catch (error) {
commit('clearAuth')
return false
} finally {
commit(
'setAuthChecked',
true
)
}
}
参数解构
原本 Vuex action 参数是:
javascript
context
可以写:
javascript
async initializeAuth(context) {
context.state
context.commit(...)
}
这里使用解构:
javascript
async initializeAuth({
state,
commit
})
直接取出 state 和 commit。
为什么 finally 设置 authChecked
无论:
Cookie 有效
Cookie 无效
后端错误
都说明检查已经完成。
所以放在 finally:
javascript
commit('setAuthChecked', true)
十一、路由守卫为什么变成 async
javascript
router.beforeEach(
async (to, from, next) => {
if (!store.state.authChecked) {
await store.dispatch(
'initializeAuth'
)
}
// 再判断是否登录
}
)
必须先等待 /api/auth/me 返回。
错误流程:
currentUser 初始 null
立即判断未登录
跳到登录页
之后 /auth/me 才返回
正确流程:
先等待 /auth/me
↓
恢复 currentUser
↓
再判断能否进入后台
十二、Axios 的 withCredentials
javascript
const service = axios.create({
withCredentials: true
})
它表示请求允许携带 Cookie 等凭证。
当前开发代理是同源形式,浏览器本来就会携带 Cookie。保留这个配置,可以让后续前后端分开部署时更容易调整。
十三、退出登录
后端:
javascript
loginSessions.delete(token)
删除服务器中的会话。
然后:
javascript
res.clearCookie(
SESSION_COOKIE_NAME,
cookieOptions
)
通知浏览器删除 Cookie。
前端:
javascript
commit('clearAuth')
清空当前用户。
完整退出流程:
点击退出
↓
POST /api/auth/logout
↓
后端删除 session
↓
后端清除 Cookie
↓
Vuex 清除用户
↓
跳转 /login
十四、阶段五的限制
目前会话保存在:
javascript
const loginSessions = new Map()
后端一旦重启:
Map 被清空
浏览器 Cookie 还在
后端找不到对应 session
返回登录失效
正式项目通常使用:
Redis
数据库
Session 服务
保存会话。