hookehuyr

fix(auth): 修复授权接口重复 cookie 问题并自动去重

- 修改 openid.js 使用 axios 直接调用授权接口以访问响应头
- 添加 extractCookieFromResponse() 从响应中提取 cookie
- 添加 normalizeCookies() 处理重复的 set-cookie
- 修改 request.js 的 setSessionId() 自动清理重复的 cookie
- 添加 cleanupDuplicateCookies() 函数处理重复 cookie
- 新增 SESSIONID_MANAGEMENT.md 文档记录 sessionid 管理最佳实践

问题:后端返回重复的 Set-Cookie 导致请求头携带重复的 sessionid
解决:前端提取 cookie 时自动去重,只保留第一个

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This diff is collapsed. Click to expand it.
......@@ -6,12 +6,14 @@
*/
import Taro from '@tarojs/taro'
import { miniProgramAuthAPI } from '@/api/wechat'
import axios from '@/utils/request'
import { setSessionId } from '@/utils/request'
import { loginStatusAPI } from '@/api/user'
/**
* 小程序授权
* @description 调用 wx.login 获取 code,由后端授权获取 openid
* @description 授权成功后会自动将 sessionid 写入本地存储
* @returns {Promise<{user: Object|null}>} 返回用户信息(如果已自动登录)
*
* @example
......@@ -31,14 +33,23 @@ export async function miniProgramAuth() {
throw new Error('获取微信 code 失败')
}
// 2. 调用后端授权接口
const res = await miniProgramAuthAPI({ code })
// 2. 调用后端授权接口(直接使用 axios 以访问响应头)
const response = await axios.post('/srv/?a=openid', { code })
if (res.code === 1) {
return res.data.user || null
} else {
throw new Error(res.msg || '小程序授权失败')
// 检查响应状态
if (!response?.data || response.data.code !== 1) {
throw new Error(response?.data?.msg || '小程序授权失败')
}
// 3. 从响应中提取 cookie 并写入本地存储
const cookie = extractCookieFromResponse(response)
if (cookie) {
setSessionId(cookie)
console.log('授权成功,sessionid 已写入本地存储')
}
// 4. 返回用户信息(如果已自动登录)
return response.data.data?.user || null
} catch (err) {
console.error('小程序授权失败:', err)
throw err
......@@ -46,6 +57,88 @@ export async function miniProgramAuth() {
}
/**
* 从 axios 响应中提取 cookie
* @description 尝试从多个可能的位置提取 cookie,并去重
* @param {Object} response axios 响应对象
* @returns {string|null} cookie 字符串或 null
*
* @example
* const cookie = extractCookieFromResponse(response)
* if (cookie) {
* setSessionId(cookie)
* }
*/
function extractCookieFromResponse(response) {
// 尝试从 response.headers 中提取
if (response?.headers) {
// 标准的 set-cookie 头
if (response.headers['set-cookie']) {
const cookies = response.headers['set-cookie']
return normalizeCookies(cookies)
}
// 小写版本(某些环境)
if (response.headers['Set-Cookie']) {
const cookies = response.headers['Set-Cookie']
return normalizeCookies(cookies)
}
// 小程序环境中可能在 cookies 字段
if (response.headers.cookies) {
const cookies = response.headers.cookies
return normalizeCookies(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)
}
console.warn('未能从响应中提取 cookie')
return null
}
/**
* 标准化 cookie 数组并去重
* @description 将 cookie 数组转换为字符串,只提取唯一的 cookie(忽略重复项)
* @param {string|string[]} cookies cookie 字符串或数组
* @returns {string|null} 标准化后的 cookie 字符串
*
* @example
* normalizeCookies(['PHPSESSID=xxx; path=/', 'PHPSESSID=xxx; path=/'])
* // 返回: 'PHPSESSID=xxx; path=/'
*
* normalizeCookies('PHPSESSID=xxx; path=/')
* // 返回: 'PHPSESSID=xxx; path=/'
*/
function normalizeCookies(cookies) {
if (!cookies) return null
// 如果是单个 cookie 字符串,直接返回
if (typeof cookies === 'string') {
return cookies
}
// 如果是数组,只取第一个 cookie(忽略重复项)
if (Array.isArray(cookies)) {
// 使用 Set 去重
const uniqueCookies = [...new Set(cookies)]
// 只返回第一个唯一的 cookie
return uniqueCookies[0] || null
}
return null
}
/**
* 检查 openid 状态
* @description 调用 loginStatusAPI 检查 is_openid
* @returns {Promise<boolean>} 是否已授权
......
......@@ -29,6 +29,7 @@ export const getSessionId = () => {
* @description 设置 sessionid(一般不需要手动调用)
* - 正常情况下由 authRedirect.refreshSession 写入
* - 保留该方法用于极端场景的手动修复/兼容旧逻辑
* - 自动清理重复的 cookie(防止后端返回重复的 set-cookie)
* @param {string} sessionid cookie 字符串
* @returns {void} 无返回值
*/
......@@ -37,13 +38,68 @@ export const setSessionId = sessionid => {
if (!sessionid) {
return
}
Taro.setStorageSync('sessionid', sessionid)
// 自动清理重复的 cookie
// 例如:"PHPSESSID=xxx; ...,PHPSESSID=xxx; ..." -> "PHPSESSID=xxx; ..."
const cleaned = cleanupDuplicateCookies(sessionid)
Taro.setStorageSync('sessionid', cleaned)
} catch (error) {
console.error('设置sessionid失败:', error)
}
}
/**
* @description 清理重复的 cookie
* @description 如果 cookie 字符串中有重复项,只保留第一个
* @param {string} cookies cookie 字符串
* @returns {string} 清理后的 cookie 字符串
*
* @example
* cleanupDuplicateCookies('PHPSESSID=xxx; ...,PHPSESSID=xxx; ...')
* // 返回: 'PHPSESSID=xxx; ...'
*/
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(',')
}
/**
* @description 清空 sessionid(一般不需要手动调用)
* @returns {void} 无返回值
*/
......