阶段五:HttpOnly Cookie 登录持久化

从零构建带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 小时。


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
})

直接取出 statecommit


为什么 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 服务

保存会话。

相关推荐
唐青枫7 小时前
Java Netty 实战指南:从 NIO 线程模型到 TCP 编解码和心跳机制
java
就是小王同学啊7 小时前
Spring小技巧之设计模式
sql·spring·设计模式
CoderYanger7 小时前
A.每日一题:1967题+1331题
java·程序人生·算法·leetcode·面试·职场和发展·学习方法
言乐67 小时前
Python实现基本搜索引擎
java·linux·开发语言·python·搜索引擎
萧瑟余晖7 小时前
JDK 15 新特性详解
java
weixin_436525077 小时前
Spring + Dubbo + Zookeeper项目依赖注入
spring·dubbo·java-zookeeper
L-影8 小时前
springboot启动流程
java·spring boot·spring
早川9198 小时前
有关线程池知识
spring
ch.ju8 小时前
Java Programming Chapter 4——lambda expression
java·开发语言