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 返回完整响应对象,包含
headers和cookies
示例代码(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.cookies或response.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
排查步骤:
- 检查
localStorage.sessionid是否有值 - 检查请求头中是否携带了
cookie字段 - 检查 cookie 值是否正确(没有重复)
- 检查后端是否验证了这个 sessionid
📚 相关文档
- 授权流程:auth-debug-guide.md
-
请求封装:
src/utils/request.js -
授权实现:
src/utils/openid.js
📝 总结
| 场景 | 解决方案 |
|---|---|
| 提取 cookie | 直接使用 axios,不用 fn() 包装 |
| 重复的 set-cookie | 使用 Set 去重,只保留第一个 |
| 兼容多种格式 | 尝试多个可能的 cookie 位置 |
| 自动清理重复 | 在 setSessionId() 时自动清理 |
| 携带 sessionid | 请求拦截器自动读取并添加 |
核心原则:
- ✅ 前端必须主动管理 sessionid
- ✅ 使用 axios 直接调用授权接口
- ✅ 提取 cookie 时自动去重
- ✅ 写入时自动清理重复项
- ✅ 请求时自动携带到请求头