You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何通过Next.js Link组件安全传递敏感数据至getServerSideProps(不使用查询字符串)

Next.js中安全传递敏感数据至getServerSideProps(不使用查询字符串)

针对你不想通过查询字符串传递category_id这类敏感数据到getServerSideProps的需求,下面是两种靠谱的安全实现方案:


方案1:用Iron Session加密会话存储

Iron Session是Next.js生态里常用的安全会话工具,它会把会话数据加密后存在Cookie里,客户端和服务端都能安全读写,数据不会被明文暴露或篡改。

步骤实现:

  1. 安装依赖

    npm install iron-session
    
  2. 配置会话
    在项目根目录创建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)
    }
    
  3. 客户端跳转前存储数据
    因为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>
      )
    }
    
  4. 服务端读取数据
    在目标页面的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里,服务端解密获取。

步骤实现:

  1. 写加密解密工具函数
    创建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()
    }
    
  2. 客户端加密并设置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>
      )
    }
    
  3. 服务端解密并验证
    在目标页面的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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.31 12:46:15