如何通过Next.js Link组件安全传递敏感数据至getServerSideProps(不使用查询字符串)
Next.js中安全传递敏感数据至getServerSideProps(不使用查询字符串)
针对你不想通过查询字符串传递category_id这类敏感数据到getServerSideProps的需求,下面是两种靠谱的安全实现方案:
方案1:用Iron Session加密会话存储
Iron Session是Next.js生态里常用的安全会话工具,它会把会话数据加密后存在Cookie里,客户端和服务端都能安全读写,数据不会被明文暴露或篡改。
步骤实现:
安装依赖
npm install iron-session配置会话
在项目根目录创建lib/session.js,写入会话配置:import { withIronSessionApiRoute, withIronSessionSsr } from 'iron-session/next' const sessionOptions = { password: process.env.IRON_SESSION_PASSWORD, // 至少32位的密钥,存在环境变量里 cookieName: 'next-category-session', cookieOptions: { secure: process.env.NODE_ENV === 'production', // 生产环境必须开HTTPS sameSite: 'strict', }, } export function withSessionRoute(handler) { return withIronSessionApiRoute(handler, sessionOptions) } export function withSessionSsr(handler) { return withIronSessionSsr(handler, sessionOptions) }客户端跳转前存储数据
因为Link的客户端跳转是同步的,要先异步完成会话存储再跳转,用按钮模拟点击逻辑:// components/CategoryLink.js import { useSession } from 'iron-session/react' export default function CategoryLink({ categoryId }) { const [session, setSession] = useSession() const goToCategoryPage = async () => { // 把敏感ID存到加密会话 await setSession({ category_id: categoryId }) // 跳转到目标页面 window.location.href = '/category-detail' } return ( <button onClick={goToCategoryPage} style={{ cursor: 'pointer' }}> 查看分类详情 </button> ) }服务端读取数据
在目标页面的getServerSideProps里读取会话数据,用完记得清理:// pages/category-detail.js import { withSessionSsr } from '../lib/session' async function getServerSideProps({ req }) { // 从加密会话中取ID const categoryId = req.session.category_id if (!categoryId) { return { redirect: { destination: '/', permanent: false } } } // 用ID拉取数据,这里替换成你的业务逻辑 const categoryData = await fetch(`https://your-api.com/categories/${categoryId}`).then(res => res.json()) // 用完删除会话里的ID,避免残留 delete req.session.category_id await req.session.save() return { props: { categoryData } } } export default function CategoryDetail({ categoryData }) { return <div>分类名称:{categoryData.name}</div> } // 用withSessionSsr包装,让会话在getServerSideProps中可用 export const getServerSidePropsWithSession = withSessionSsr(getServerSideProps)
方案2:手动加密Cookie存储
如果不想依赖第三方库,可以自己用Node.js的crypto模块加密数据,存在Cookie里,服务端解密获取。
步骤实现:
写加密解密工具函数
创建lib/crypto.js:import crypto from 'crypto' // 密钥要存在环境变量,32位长度,转成Buffer const ENCRYPTION_KEY = Buffer.from(process.env.AES_ENCRYPTION_KEY, 'hex') const IV_LENGTH = 16 // AES-256-CBC要求16位初始向量 export function encrypt(text) { const iv = crypto.randomBytes(IV_LENGTH) const cipher = crypto.createCipheriv('aes-256-cbc', ENCRYPTION_KEY, iv) let encrypted = cipher.update(text) encrypted = Buffer.concat([encrypted, cipher.final()]) return `${iv.toString('hex')}:${encrypted.toString('hex')}` } export function decrypt(text) { const [ivHex, encryptedHex] = text.split(':') const iv = Buffer.from(ivHex, 'hex') const encryptedText = Buffer.from(encryptedHex, 'hex') const decipher = crypto.createDecipheriv('aes-256-cbc', ENCRYPTION_KEY, iv) let decrypted = decipher.update(encryptedText) decrypted = Buffer.concat([decrypted, decipher.final()]) return decrypted.toString() }客户端加密并设置Cookie
同样用按钮触发跳转逻辑,先加密ID再存Cookie:// components/CategoryLink.js import { encrypt } from '../lib/crypto' export default function CategoryLink({ categoryId }) { const goToCategoryPage = () => { // 加密ID const encryptedId = encrypt(categoryId.toString()) // 设置Cookie,有效期设5分钟,用完就清 document.cookie = `category_id=${encryptedId}; path=/; max-age=300; ${process.env.NODE_ENV === 'production' ? 'secure; SameSite=Strict' : ''}` // 跳转页面 window.location.href = '/category-detail' } return ( <button onClick={goToCategoryPage}>查看分类详情</button> ) }服务端解密并验证
在目标页面的getServerSideProps里读取Cookie解密,还要处理解密失败的情况:// pages/category-detail.js import { decrypt } from '../lib/crypto' export async function getServerSideProps({ req, res }) { // 解析Cookie const cookies = req.headers.cookie?.split(';').reduce((acc, cookie) => { const [key, value] = cookie.trim().split('=') acc[key] = value return acc }, {}) || {} const encryptedId = cookies.category_id if (!encryptedId) { return { redirect: { destination: '/', permanent: false } } } try { const categoryId = decrypt(encryptedId) // 拉取分类数据 const categoryData = await fetch(`https://your-api.com/categories/${categoryId}`).then(res => res.json()) // 清除Cookie res.setHeader('Set-Cookie', 'category_id=; path=/; max-age=0') return { props: { categoryData } } } catch (err) { // 解密失败,说明数据可能被篡改,直接跳转首页 return { redirect: { destination: '/', permanent: false } } } } export default function CategoryDetail({ categoryData }) { return <div>分类名称:{categoryData.name}</div> }
关键注意事项
- 所有加密密钥必须存在环境变量中,绝对不能硬编码在代码里
- 会话或Cookie的有效期要尽可能短,用完立即清除,减少敏感数据泄露风险
- 生产环境必须启用HTTPS,防止Cookie被中间人劫持
- 服务端拿到
category_id后,一定要额外验证它的合法性(比如检查是否属于当前登录用户),避免越权访问
内容的提问来源于stack exchange,提问作者Abdelghani Adel
相关产品推荐
相关产品推荐

