SESSIONID_MANAGEMENT.md 12.1 KB

Sessionid 管理最佳实践

适用于:微信小程序、H5 等需要后端 session 认证的前端项目

📋 核心原则

⚠️ 前端必须主动管理 sessionid

错误认知:后端会自动设置 cookie,前端无需处理 正确做法前端必须从响应中提取 sessionid 并写入本地存储

原因

  • 小程序没有浏览器的自动 cookie 管理机制
  • 每次请求需要手动从本地存储读取 sessionid 并添加到请求头

🔴 常见问题

问题 1:后端返回重复的 Set-Cookie

现象

Set-Cookie: PHPSESSID=xxx; expires=...; path=/
Set-Cookie: PHPSESSID=xxx; expires=...; path=/

原因

  • PHP/Nginx 的正常行为
  • 可能是多层代理导致的重复设置

后果: 如果前端直接用 , 连接所有 cookie,会导致:

cookie: PHPSESSID=xxx; ...,PHPSESSID=xxx; ...

每次请求都携带重复的 sessionid,增加请求头大小。


✅ 解决方案

步骤 1:从响应中提取 cookie

关键点

  • 必须直接使用 axios(不要用 fn() 包装)
  • fn() 只返回 { code, data, msg },无法访问响应头
  • ✅ axios 返回完整响应对象,包含 headerscookies

示例代码src/utils/openid.js):

import axios from '@/utils/request'
import { setSessionId } from '@/utils/request'

export async function miniProgramAuth() {
  try {
    // 1. 调用 wx.login 获取 code
    const { code } = await Taro.login()

    // 2. 直接使用 axios 调用授权接口
    const response = await axios.post('/srv/?a=openid', { code })

    // 3. 从响应中提取 cookie
    const cookie = extractCookieFromResponse(response)

    // 4. 写入本地存储
    if (cookie) {
      setSessionId(cookie)
    }

    return response.data.data?.user || null
  } catch (err) {
    console.error('授权失败:', err)
    throw err
  }
}

步骤 2:处理 cookie 去重

场景:后端返回重复的 set-cookie 数组

解决方案:使用 Set 去重,只保留第一个

function normalizeCookies(cookies) {
  if (!cookies) return null

  // 如果是单个 cookie 字符串,直接返回
  if (typeof cookies === 'string') {
    return cookies
  }

  // 如果是数组,使用 Set 去重
  if (Array.isArray(cookies)) {
    const uniqueCookies = [...new Set(cookies)]

    // 只返回第一个唯一的 cookie
    return uniqueCookies[0] || null
  }

  return null
}

步骤 3:兼容多种响应格式

不同环境的 cookie 位置

function extractCookieFromResponse(response) {
  // 尝试从 response.headers 中提取
  if (response?.headers) {
    // 标准的 set-cookie 头
    if (response.headers['set-cookie']) {
      return normalizeCookies(response.headers['set-cookie'])
    }

    // 小写版本(某些环境)
    if (response.headers['Set-Cookie']) {
      return normalizeCookies(response.headers['Set-Cookie'])
    }

    // 小程序环境中可能在 cookies 字段
    if (response.headers.cookies) {
      return normalizeCookies(response.headers.cookies)
    }
  }

  // 尝试从 response.cookies 中提取(axios-miniprogram)
  if (response?.cookies) {
    const cookies = response.cookies
    // 如果是对象数组格式 [{name, value}, ...]
    if (Array.isArray(cookies) && cookies.length > 0 && cookies[0].name) {
      return cookies.map(c => `${c.name}=${c.value}`).join('; ')
    }
    // 如果是字符串数组
    return normalizeCookies(cookies)
  }

  return null
}

步骤 4:自动清理已存在的重复 cookie

问题:如果 localStorage.sessionid 已经存储了重复的旧值怎么办?

解决方案:在 setSessionId() 时自动清理

// src/utils/request.js
export const setSessionId = sessionid => {
  try {
    if (!sessionid) {
      return
    }

    // 自动清理重复的 cookie
    const cleaned = cleanupDuplicateCookies(sessionid)

    Taro.setStorageSync('sessionid', cleaned)
  } catch (error) {
    console.error('设置sessionid失败:', error)
  }
}

function cleanupDuplicateCookies(cookies) {
  if (!cookies || typeof cookies !== 'string') {
    return cookies
  }

  // 按逗号分割(重复的 cookie 用逗号连接)
  const parts = cookies.split(',')

  // 如果只有一个部分,直接返回
  if (parts.length === 1) {
    return cookies
  }

  // 提取 cookie 名称(用于去重)
  const extractCookieName = (cookieStr) => {
    const match = cookieStr.match(/^([^=]+)=/)
    return match ? match[1].trim() : null
  }

  // 去重:只保留第一次出现的 cookie
  const seen = new Set()
  const uniqueParts = []

  for (const part of parts) {
    const name = extractCookieName(part)
    if (name && !seen.has(name)) {
      seen.add(name)
      uniqueParts.push(part)
    }
  }

  // 如果去重后只剩一个,直接返回
  if (uniqueParts.length === 1) {
    return uniqueParts[0]
  }

  // 否则用逗号连接
  return uniqueParts.join(',')
}

🔄 完整流程

授权流程

┌─────────────────────────────────────────────────────────┐
│ 1. 前端调用 Taro.login() 获取微信 code                    │
└────────────────────┬────────────────────────────────────┘
                     ↓
┌─────────────────────────────────────────────────────────┐
│ 2. 调用后端授权接口                                      │
│    POST /srv/?a=openid                                   │
│    Body: { code: "xxx" }                                 │
└────────────────────┬────────────────────────────────────┘
                     ↓
┌─────────────────────────────────────────────────────────┐
│ 3. 后端返回响应                                          │
│    Data: { code: 1, data: { user: {...} } }            │
│    Set-Cookie: PHPSESSID=xxx; ... (重复)                │
└────────────────────┬────────────────────────────────────┘
                     ↓
┌─────────────────────────────────────────────────────────┐
│ 4. 前端提取 cookie 并去重                                │
│    extractCookieFromResponse()                           │
│    normalizeCookies()                                    │
└────────────────────┬────────────────────────────────────┘
                     ↓
┌─────────────────────────────────────────────────────────┐
│ 5. 写入本地存储                                          │
│    setSessionId(cookie)                                  │
│    localStorage.sessionid = "PHPSESSID=xxx; ..."         │
└────────────────────┬────────────────────────────────────┘
                     ↓
┌─────────────────────────────────────────────────────────┐
│ 6. 后续请求自动携带 sessionid                            │
│    request.js 拦截器自动读取并添加到请求头               │
└─────────────────────────────────────────────────────────┘

请求流程(自动携带 sessionid)

// src/utils/request.js 拦截器
service.interceptors.request.use(config => {
  // 动态获取 sessionid 并设置到请求头
  const sessionid = getSessionId()
  if (sessionid) {
    config.headers = config.headers || {}
    config.headers.cookie = sessionid
  }
  return config
})

🎯 最佳实践

1. 统一使用 axios 直接调用

❌ 错误:通过 fn() 包装的 API

const res = await miniProgramAuthAPI({ code })  // ❌ 无法访问响应头

✅ 正确:直接使用 axios

const response = await axios.post('/srv/?a=openid', { code })  // ✅ 可访问 headers

2. 封装 cookie 提取逻辑

位置src/utils/openid.js

函数

  • extractCookieFromResponse() - 从响应中提取 cookie
  • normalizeCookies() - 标准化并去重

3. 自动清理重复 cookie

位置src/utils/request.js

函数

  • setSessionId() - 自动调用 cleanupDuplicateCookies()
  • cleanupDuplicateCookies() - 清理重复的 cookie

4. 统一的 sessionid 管理

工具函数src/utils/request.js):

// 获取 sessionid
export const getSessionId = () => {
  return Taro.getStorageSync('sessionid') || null
}

// 设置 sessionid(自动清理重复)
export const setSessionId = (sessionid) => {
  const cleaned = cleanupDuplicateCookies(sessionid)
  Taro.setStorageSync('sessionid', cleaned)
}

// 清空 sessionid
export const clearSessionId = () => {
  Taro.removeStorageSync('sessionid')
}

🧪 测试验证

验证点 1:检查响应中的 cookie

// 在授权接口调用后添加日志
const response = await axios.post('/srv/?a=openid', { code })
console.log('响应头:', response.headers)
console.log('Cookie:', response.cookies)

预期

  • 小程序:response.cookiesresponse.headers['set-cookie']
  • H5:response.headers['set-cookie']

验证点 2:检查本地存储

// 控制台执行
console.log(Taro.getStorageSync('sessionid'))

预期

  • ✅ 单个 cookie:PHPSESSID=xxx; expires=...; path=/
  • ❌ 重复 cookie:PHPSESSID=xxx; ...,PHPSESSID=xxx; ...

验证点 3:检查请求头

打开微信开发者工具 → Network 面板:

  • 查看任意请求的 Request Headers
  • 确认 cookie 字段只有单个 sessionid

🔧 故障排查

问题:请求头中的 cookie 还是重复的

原因localStorage.sessionid 中存储的是旧的重复值

解决方案

// 方法 1:手动清理
Taro.removeStorageSync('sessionid')
// 然后重新登录

// 方法 2:在控制台执行自动清理脚本
const oldSessionid = Taro.getStorageSync('sessionid')
const cleaned = cleanupDuplicateCookies(oldSessionid)
Taro.setStorageSync('sessionid', cleaned)

问题:授权成功后接口还是返回 401

排查步骤

  1. 检查 localStorage.sessionid 是否有值
  2. 检查请求头中是否携带了 cookie 字段
  3. 检查 cookie 值是否正确(没有重复)
  4. 检查后端是否验证了这个 sessionid

📚 相关文档

  • 授权流程auth-debug-guide.md
  • 请求封装src/utils/request.js
  • 授权实现src/utils/openid.js

📝 总结

场景 解决方案
提取 cookie 直接使用 axios,不用 fn() 包装
重复的 set-cookie 使用 Set 去重,只保留第一个
兼容多种格式 尝试多个可能的 cookie 位置
自动清理重复 setSessionId() 时自动清理
携带 sessionid 请求拦截器自动读取并添加

核心原则

  1. ✅ 前端必须主动管理 sessionid
  2. ✅ 使用 axios 直接调用授权接口
  3. ✅ 提取 cookie 时自动去重
  4. ✅ 写入时自动清理重复项
  5. ✅ 请求时自动携带到请求头