各位观众老爷,大家好!今天咱们聊聊 Vue SSR 应用中那些磨人的小妖精:Cookie、Session 和用户身份验证。别怕,听我慢慢道来,保证让你从一脸懵圈到自信满满。
开场白:SSR 里的状态管理,可不是闹着玩的!
SSR (Server-Side Rendering) 听起来很美好,但实际操作起来,状态管理绝对是个头疼的问题。在传统的 SPA (Single-Page Application) 里,状态都在浏览器里,爱咋折腾咋折腾。但 SSR 就不一样了,服务器要先渲染页面,然后客户端再接管。如果服务器和客户端的状态不一致,那画面简直太美不敢看。
想象一下,用户在服务器端已经登录了,结果客户端一接管,又变成未登录状态了,这用户不得骂娘?所以,保持服务器和客户端的状态一致性,是 SSR 应用的重中之重。
第一幕:Cookie 的那些事儿
Cookie,这玩意儿大家都不陌生,它就像浏览器的小便签,用来存储一些小数据。在 SSR 里,Cookie 的处理稍微复杂一点,因为服务器端和客户端都可以设置和读取 Cookie。
-
服务器端设置 Cookie:
在 Vue SSR 应用中,我们通常使用
express
或者koa
作为服务器。以express
为例,我们可以这样设置 Cookie:const express = require('express'); const app = express(); app.get('/set-cookie', (req, res) => { res.cookie('username', 'zhangsan', { maxAge: 900000, httpOnly: true }); // 设置一个名为 username 的 Cookie res.send('Cookie 已设置'); }); app.listen(3000, () => { console.log('Server is running on port 3000'); });
这段代码的意思是,当访问
/set-cookie
路由时,服务器会设置一个名为username
,值为zhangsan
的 Cookie。maxAge
表示 Cookie 的有效期,单位是毫秒。httpOnly
表示该 Cookie 只能通过 HTTP 协议访问,不能被 JavaScript 脚本访问,这样可以提高安全性。 -
客户端读取 Cookie:
在客户端,我们可以使用
js-cookie
这样的库来读取 Cookie:import Cookies from 'js-cookie'; const username = Cookies.get('username'); // 获取名为 username 的 Cookie if (username) { console.log('Username:', username); } else { console.log('Cookie username 不存在'); }
这段代码很简单,就是使用
Cookies.get()
方法来获取指定名称的 Cookie。 -
SSR 中的 Cookie 同步:
重点来了,如何在 SSR 中同步 Cookie 呢?关键在于在服务器端渲染之前,将客户端的 Cookie 传递给服务器,并在服务器端设置相应的 Cookie。
-
客户端发送 Cookie:
在客户端,我们需要在每次请求时,将 Cookie 通过 HTTP Header 发送给服务器。可以使用
axios
这样的 HTTP 客户端库:import axios from 'axios'; import Cookies from 'js-cookie'; const instance = axios.create({ baseURL: '/', // 你的 API 地址 withCredentials: true, // 允许携带 Cookie headers: { 'X-CSRF-TOKEN': Cookies.get('XSRF-TOKEN') // 携带CSRF token } }); instance.get('/api/data') .then(response => { console.log(response.data); });
withCredentials: true
这个配置非常重要,它告诉浏览器允许携带 Cookie。 -
服务器端接收 Cookie:
在服务器端,我们需要使用
cookie-parser
中间件来解析 Cookie:const express = require('express'); const cookieParser = require('cookie-parser'); const app = express(); app.use(cookieParser()); // 使用 cookie-parser 中间件 app.get('/api/data', (req, res) => { const username = req.cookies.username; // 从请求中获取 Cookie if (username) { res.send(`Hello, ${username}!`); } else { res.send('Hello, Guest!'); } }); app.listen(3000, () => { console.log('Server is running on port 3000'); });
req.cookies
对象包含了客户端发送过来的所有 Cookie。 -
传递Cookie给Vue实例:
在
entry-server.js
中,我们需要读取请求中的cookie,然后传递给Vue实例。// entry-server.js import { createApp } from './app' export default context => { return new Promise((resolve, reject) => { const { app, router, store } = createApp() // 设置服务器端 router 的位置 router.push(context.url) router.onReady(() => { const matchedComponents = router.getMatchedComponents() // 匹配不到的路由,执行 reject 函数,并返回 404 if (!matchedComponents.length) { return reject({ code: 404 }) } // 对所有匹配的路由组件调用 `asyncData()` Promise.all(matchedComponents.map(({ asyncData }) => asyncData && asyncData({ store, route: router.currentRoute, cookies: context.req.headers.cookie // 传递cookie }))).then(() => { // 在所有预取钩子(preFetch hook) resolve 后, // 我们的 store 现在已经填充入渲染应用程序所需的状态。 // 当我们将状态附加到上下文, // 并且 `template` 选项用于 renderer 时, // 状态将自动序列化为 `window.__INITIAL_STATE__`,并注入 HTML。 context.state = store.state resolve(app) }).catch(reject) }, reject) }) }
在Vue组件中,我们就可以通过
this.$ssrContext.cookies
访问到cookie了。<template> <div> Welcome, {{ username }}! </div> </template> <script> export default { computed: { username() { if (this.$ssrContext && this.$ssrContext.cookies) { const cookies = this.$ssrContext.cookies.split(';'); for (let i = 0; i < cookies.length; i++) { const cookie = cookies[i].trim(); // Does this cookie string begin with the name we want? if (cookie.startsWith('username=')) { return cookie.substring('username='.length, cookie.length); } } } return 'Guest'; } } } </script>
注意: 这种方式需要手动解析cookie字符串。更优雅的方式是使用
cookie-parser
中间件在服务器端解析cookie,然后将解析后的cookie对象传递给Vue实例。
-
第二幕:Session 的管理艺术
Session 比 Cookie 更高级一点,它把用户的数据存储在服务器端,然后在客户端只存储一个 Session ID。这样可以提高安全性,因为用户的数据不会暴露在客户端。
-
服务器端创建 Session:
使用
express-session
中间件可以很方便地管理 Session:const express = require('express'); const session = require('express-session'); const app = express(); app.use(session({ secret: 'your secret key', // 用于加密 Session ID 的密钥 resave: false, saveUninitialized: true, cookie: { secure: false } // 在 HTTPS 环境下设置为 true })); app.get('/login', (req, res) => { req.session.username = 'zhangsan'; // 将 username 存储到 Session 中 res.send('登录成功'); }); app.get('/get-session', (req, res) => { const username = req.session.username; // 从 Session 中获取 username if (username) { res.send(`Hello, ${username}!`); } else { res.send('未登录'); } }); app.listen(3000, () => { console.log('Server is running on port 3000'); });
secret
是一个非常重要的配置,它用于加密 Session ID。一定要设置一个足够复杂的secret
,否则 Session 很容易被破解。resave
和saveUninitialized
是一些高级配置,可以参考express-session
的文档。cookie.secure
用于指定 Session Cookie 是否只能在 HTTPS 环境下传输。 -
客户端访问 Session:
客户端不需要直接访问 Session 的内容,只需要在每次请求时,将 Session ID 通过 Cookie 发送给服务器。
express-session
会自动处理 Session ID 的设置和读取。 -
SSR 中的 Session 同步:
SSR 中 Session 的同步和 Cookie 的同步类似,都需要在服务器端渲染之前,将客户端的 Cookie 传递给服务器。
-
客户端发送 Cookie:
和 Cookie 的处理方式一样,使用
axios
设置withCredentials: true
。 -
服务器端接收 Cookie:
express-session
会自动从请求中读取 Session ID,并加载对应的 Session 数据。 -
传递Session给Vue实例:
与Cookie类似,在
entry-server.js
中,我们需要读取请求中的cookie,然后传递给Vue实例。但是,我们不应该直接将整个session对象传递给客户端,这存在安全风险。更好的做法是,只传递Vue组件需要的部分session数据。// entry-server.js import { createApp } from './app' export default context => { return new Promise((resolve, reject) => { const { app, router, store } = createApp() // 设置服务器端 router 的位置 router.push(context.url) router.onReady(() => { const matchedComponents = router.getMatchedComponents() // 匹配不到的路由,执行 reject 函数,并返回 404 if (!matchedComponents.length) { return reject({ code: 404 }) } // 对所有匹配的路由组件调用 `asyncData()` Promise.all(matchedComponents.map(({ asyncData }) => asyncData && asyncData({ store, route: router.currentRoute, session: context.req.session // 传递session }))).then(() => { // 在所有预取钩子(preFetch hook) resolve 后, // 我们的 store 现在已经填充入渲染应用程序所需的状态。 // 当我们将状态附加到上下文, // 并且 `template` 选项用于 renderer 时, // 状态将自动序列化为 `window.__INITIAL_STATE__`,并注入 HTML。 context.state = store.state resolve(app) }).catch(reject) }, reject) }) }
在Vue组件中,我们就可以通过
this.$ssrContext.session
访问到session了。<template> <div> Welcome, {{ username }}! </div> </template> <script> export default { computed: { username() { return this.$ssrContext && this.$ssrContext.session ? this.$ssrContext.session.username : 'Guest'; } } } </script>
-
第三幕:用户身份验证的正确姿势
用户身份验证是 Web 应用的基石。在 SSR 应用中,我们需要确保用户在服务器端和客户端都处于正确的登录状态。
-
传统的身份验证流程:
- 用户提交用户名和密码。
- 服务器验证用户名和密码。
- 如果验证成功,服务器创建一个 Session,并将用户的信息存储到 Session 中。
- 服务器设置一个 Session ID 的 Cookie。
- 客户端在后续的请求中,携带 Session ID 的 Cookie。
- 服务器根据 Session ID 查找对应的 Session 数据,从而判断用户的登录状态。
-
JWT (JSON Web Token) 的应用:
JWT 是一种轻量级的身份验证方案。它使用 JSON 对象来表示用户信息,并使用数字签名来保证信息的完整性。
-
服务器端生成 JWT:
const jwt = require('jsonwebtoken'); app.post('/login', (req, res) => { const { username, password } = req.body; // 验证用户名和密码 if (username === 'zhangsan' && password === '123456') { // 生成 JWT const token = jwt.sign({ username: username }, 'your secret key', { expiresIn: '1h' }); // 将 JWT 存储到 Cookie 中 res.cookie('token', token, { httpOnly: true }); res.send('登录成功'); } else { res.status(401).send('用户名或密码错误'); } });
jwt.sign()
方法用于生成 JWT。第一个参数是包含用户信息的 JSON 对象,第二个参数是用于签名的密钥,第三个参数是一些配置选项,例如expiresIn
用于指定 JWT 的有效期。 -
客户端存储 JWT:
客户端可以将 JWT 存储到 Cookie 或者 Local Storage 中。推荐使用 Cookie,并设置
httpOnly
属性,这样可以提高安全性。 -
服务器端验证 JWT:
app.get('/api/data', (req, res) => { const token = req.cookies.token; if (!token) { return res.status(401).send('未登录'); } jwt.verify(token, 'your secret key', (err, decoded) => { if (err) { return res.status(401).send('Token 无效'); } const username = decoded.username; res.send(`Hello, ${username}!`); }); });
jwt.verify()
方法用于验证 JWT 的有效性。如果 JWT 有效,decoded
对象会包含 JWT 中存储的用户信息。 -
SSR 中 JWT 的同步:
SSR 中 JWT 的同步和 Cookie 的同步类似,都需要在服务器端渲染之前,将客户端的 Cookie 传递给服务器。
(1) 客户端发送 Cookie:
和 Cookie 的处理方式一样,使用
axios
设置withCredentials: true
。(2) 服务器端接收 Cookie:
服务器端从请求中读取 JWT,并验证其有效性。
(3) 传递用户信息给Vue实例:
// entry-server.js import { createApp } from './app' import jwt from 'jsonwebtoken' export default context => { return new Promise((resolve, reject) => { const { app, router, store } = createApp() // 设置服务器端 router 的位置 router.push(context.url) router.onReady(() => { const matchedComponents = router.getMatchedComponents() // 匹配不到的路由,执行 reject 函数,并返回 404 if (!matchedComponents.length) { return reject({ code: 404 }) } let userInfo = null; if (context.req.headers.cookie) { const token = context.req.headers.cookie.split(';').find(cookie => cookie.trim().startsWith('token=')).split('=')[1]; try { const decoded = jwt.verify(token, 'your secret key'); userInfo = decoded; } catch (err) { // Token 无效,可以进行一些处理,例如清除客户端的 Cookie console.error('Token 无效:', err); } } Promise.all(matchedComponents.map(({ asyncData }) => asyncData && asyncData({ store, route: router.currentRoute, userInfo: userInfo // 传递用户信息 }))).then(() => { // 在所有预取钩子(preFetch hook) resolve 后, // 我们的 store 现在已经填充入渲染应用程序所需的状态。 // 当我们将状态附加到上下文, // 并且 `template` 选项用于 renderer 时, // 状态将自动序列化为 `window.__INITIAL_STATE__`,并注入 HTML。 context.state = store.state resolve(app) }).catch(reject) }, reject) }) }
<template> <div> Welcome, {{ username }}! </div> </template> <script> export default { computed: { username() { return this.$ssrContext && this.$ssrContext.userInfo ? this.$ssrContext.userInfo.username : 'Guest'; } } } </script>
-
第四幕:状态持久化与同步
仅仅传递数据是不够的,我们需要确保服务器渲染的状态能够同步到客户端,避免出现闪烁或者数据不一致的情况。Vue SSR 官方推荐使用 window.__INITIAL_STATE__
来进行状态持久化。
-
服务器端设置
__INITIAL_STATE__
:在
entry-server.js
中,将 Vuex store 的状态序列化为 JSON,并赋值给context.state
:// entry-server.js export default context => { // ... context.state = store.state; // 重要:将 store 的状态赋值给 context.state // ... }
-
客户端读取
__INITIAL_STATE__
:在客户端,我们需要在 Vue 应用创建之前,从
window.__INITIAL_STATE__
中读取状态,并将其替换到 Vuex store 中:// entry-client.js import { createApp } from './app' const { app, router, store } = createApp() if (window.__INITIAL_STATE__) { store.replaceState(window.__INITIAL_STATE__) // 重要:替换 store 的状态 } router.onReady(() => { app.$mount('#app') })
这样,客户端就可以使用服务器端渲染的状态了。
总结:SSR 状态管理的葵花宝典
技术点 | 描述 | 注意事项 |
---|---|---|
Cookie | 小型的文本文件,用于存储少量数据。 | 注意设置 httpOnly 属性,提高安全性。 |
Session | 将用户数据存储在服务器端,客户端只存储 Session ID。 | 使用 express-session 管理 Session,设置复杂的 secret 。 不要将敏感数据存储在 Session 中。 |
JWT | 使用 JSON 对象来表示用户信息,并使用数字签名来保证信息的完整性。 | 使用安全的密钥,并设置 JWT 的有效期。 |
状态持久化 | 使用 window.__INITIAL_STATE__ 将服务器端渲染的状态同步到客户端。 |
确保序列化和反序列化的过程正确无误。 |
CSRF 防护 | 在使用 Cookie 认证时,需要进行 CSRF (Cross-Site Request Forgery) 防护。 | 客户端和服务器端都需要进行相应的配置。 |
彩蛋:一些实用的小技巧
- 使用 HTTPS: 在生产环境中,一定要使用 HTTPS,这样可以保证数据的安全性。
- 设置 Cookie 的 Domain 和 Path: 合理设置 Cookie 的 Domain 和 Path 可以限制 Cookie 的作用范围,提高安全性。
- 定期清理 Session: 定期清理过期的 Session 可以释放服务器资源。
- 监控和日志: 监控和日志可以帮助你及时发现和解决问题。
好了,今天的讲座就到这里。希望大家能够掌握 Vue SSR 中 Cookie、Session 和用户身份验证的处理技巧,写出更加健壮和安全的 Web 应用。 感谢大家!