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 @@ ...@@ -6,12 +6,14 @@
6 */ 6 */
7 7
8 import Taro from '@tarojs/taro' 8 import Taro from '@tarojs/taro'
9 -import { miniProgramAuthAPI } from '@/api/wechat' 9 +import axios from '@/utils/request'
10 +import { setSessionId } from '@/utils/request'
10 import { loginStatusAPI } from '@/api/user' 11 import { loginStatusAPI } from '@/api/user'
11 12
12 /** 13 /**
13 * 小程序授权 14 * 小程序授权
14 * @description 调用 wx.login 获取 code,由后端授权获取 openid 15 * @description 调用 wx.login 获取 code,由后端授权获取 openid
16 + * @description 授权成功后会自动将 sessionid 写入本地存储
15 * @returns {Promise<{user: Object|null}>} 返回用户信息(如果已自动登录) 17 * @returns {Promise<{user: Object|null}>} 返回用户信息(如果已自动登录)
16 * 18 *
17 * @example 19 * @example
...@@ -31,14 +33,23 @@ export async function miniProgramAuth() { ...@@ -31,14 +33,23 @@ export async function miniProgramAuth() {
31 throw new Error('获取微信 code 失败') 33 throw new Error('获取微信 code 失败')
32 } 34 }
33 35
34 - // 2. 调用后端授权接口 36 + // 2. 调用后端授权接口(直接使用 axios 以访问响应头)
35 - const res = await miniProgramAuthAPI({ code }) 37 + const response = await axios.post('/srv/?a=openid', { code })
36 38
37 - if (res.code === 1) { 39 + // 检查响应状态
38 - return res.data.user || null 40 + if (!response?.data || response.data.code !== 1) {
39 - } else { 41 + throw new Error(response?.data?.msg || '小程序授权失败')
40 - throw new Error(res.msg || '小程序授权失败')
41 } 42 }
43 +
44 + // 3. 从响应中提取 cookie 并写入本地存储
45 + const cookie = extractCookieFromResponse(response)
46 + if (cookie) {
47 + setSessionId(cookie)
48 + console.log('授权成功,sessionid 已写入本地存储')
49 + }
50 +
51 + // 4. 返回用户信息(如果已自动登录)
52 + return response.data.data?.user || null
42 } catch (err) { 53 } catch (err) {
43 console.error('小程序授权失败:', err) 54 console.error('小程序授权失败:', err)
44 throw err 55 throw err
...@@ -46,6 +57,88 @@ export async function miniProgramAuth() { ...@@ -46,6 +57,88 @@ export async function miniProgramAuth() {
46 } 57 }
47 58
48 /** 59 /**
60 + * 从 axios 响应中提取 cookie
61 + * @description 尝试从多个可能的位置提取 cookie,并去重
62 + * @param {Object} response axios 响应对象
63 + * @returns {string|null} cookie 字符串或 null
64 + *
65 + * @example
66 + * const cookie = extractCookieFromResponse(response)
67 + * if (cookie) {
68 + * setSessionId(cookie)
69 + * }
70 + */
71 +function extractCookieFromResponse(response) {
72 + // 尝试从 response.headers 中提取
73 + if (response?.headers) {
74 + // 标准的 set-cookie 头
75 + if (response.headers['set-cookie']) {
76 + const cookies = response.headers['set-cookie']
77 + return normalizeCookies(cookies)
78 + }
79 +
80 + // 小写版本(某些环境)
81 + if (response.headers['Set-Cookie']) {
82 + const cookies = response.headers['Set-Cookie']
83 + return normalizeCookies(cookies)
84 + }
85 +
86 + // 小程序环境中可能在 cookies 字段
87 + if (response.headers.cookies) {
88 + const cookies = response.headers.cookies
89 + return normalizeCookies(cookies)
90 + }
91 + }
92 +
93 + // 尝试从 response.cookies 中提取(axios-miniprogram)
94 + if (response?.cookies) {
95 + const cookies = response.cookies
96 + // 如果是对象数组格式 [{name, value}, ...]
97 + if (Array.isArray(cookies) && cookies.length > 0 && cookies[0].name) {
98 + return cookies.map(c => `${c.name}=${c.value}`).join('; ')
99 + }
100 + // 如果是字符串数组
101 + return normalizeCookies(cookies)
102 + }
103 +
104 + console.warn('未能从响应中提取 cookie')
105 + return null
106 +}
107 +
108 +/**
109 + * 标准化 cookie 数组并去重
110 + * @description 将 cookie 数组转换为字符串,只提取唯一的 cookie(忽略重复项)
111 + * @param {string|string[]} cookies cookie 字符串或数组
112 + * @returns {string|null} 标准化后的 cookie 字符串
113 + *
114 + * @example
115 + * normalizeCookies(['PHPSESSID=xxx; path=/', 'PHPSESSID=xxx; path=/'])
116 + * // 返回: 'PHPSESSID=xxx; path=/'
117 + *
118 + * normalizeCookies('PHPSESSID=xxx; path=/')
119 + * // 返回: 'PHPSESSID=xxx; path=/'
120 + */
121 +function normalizeCookies(cookies) {
122 + if (!cookies) return null
123 +
124 + // 如果是单个 cookie 字符串,直接返回
125 + if (typeof cookies === 'string') {
126 + return cookies
127 + }
128 +
129 + // 如果是数组,只取第一个 cookie(忽略重复项)
130 + if (Array.isArray(cookies)) {
131 + // 使用 Set 去重
132 + const uniqueCookies = [...new Set(cookies)]
133 +
134 + // 只返回第一个唯一的 cookie
135 + return uniqueCookies[0] || null
136 + }
137 +
138 + return null
139 +}
140 +
141 +/**
49 * 检查 openid 状态 142 * 检查 openid 状态
50 * @description 调用 loginStatusAPI 检查 is_openid 143 * @description 调用 loginStatusAPI 检查 is_openid
51 * @returns {Promise<boolean>} 是否已授权 144 * @returns {Promise<boolean>} 是否已授权
......
...@@ -29,6 +29,7 @@ export const getSessionId = () => { ...@@ -29,6 +29,7 @@ export const getSessionId = () => {
29 * @description 设置 sessionid(一般不需要手动调用) 29 * @description 设置 sessionid(一般不需要手动调用)
30 * - 正常情况下由 authRedirect.refreshSession 写入 30 * - 正常情况下由 authRedirect.refreshSession 写入
31 * - 保留该方法用于极端场景的手动修复/兼容旧逻辑 31 * - 保留该方法用于极端场景的手动修复/兼容旧逻辑
32 + * - 自动清理重复的 cookie(防止后端返回重复的 set-cookie)
32 * @param {string} sessionid cookie 字符串 33 * @param {string} sessionid cookie 字符串
33 * @returns {void} 无返回值 34 * @returns {void} 无返回值
34 */ 35 */
...@@ -37,13 +38,68 @@ export const setSessionId = sessionid => { ...@@ -37,13 +38,68 @@ export const setSessionId = sessionid => {
37 if (!sessionid) { 38 if (!sessionid) {
38 return 39 return
39 } 40 }
40 - Taro.setStorageSync('sessionid', sessionid) 41 +
42 + // 自动清理重复的 cookie
43 + // 例如:"PHPSESSID=xxx; ...,PHPSESSID=xxx; ..." -> "PHPSESSID=xxx; ..."
44 + const cleaned = cleanupDuplicateCookies(sessionid)
45 +
46 + Taro.setStorageSync('sessionid', cleaned)
41 } catch (error) { 47 } catch (error) {
42 console.error('设置sessionid失败:', error) 48 console.error('设置sessionid失败:', error)
43 } 49 }
44 } 50 }
45 51
46 /** 52 /**
53 + * @description 清理重复的 cookie
54 + * @description 如果 cookie 字符串中有重复项,只保留第一个
55 + * @param {string} cookies cookie 字符串
56 + * @returns {string} 清理后的 cookie 字符串
57 + *
58 + * @example
59 + * cleanupDuplicateCookies('PHPSESSID=xxx; ...,PHPSESSID=xxx; ...')
60 + * // 返回: 'PHPSESSID=xxx; ...'
61 + */
62 +function cleanupDuplicateCookies(cookies) {
63 + if (!cookies || typeof cookies !== 'string') {
64 + return cookies
65 + }
66 +
67 + // 按逗号分割(重复的 cookie 用逗号连接)
68 + const parts = cookies.split(',')
69 +
70 + // 如果只有一个部分,直接返回
71 + if (parts.length === 1) {
72 + return cookies
73 + }
74 +
75 + // 提取 cookie 名称(用于去重)
76 + const extractCookieName = (cookieStr) => {
77 + const match = cookieStr.match(/^([^=]+)=/)
78 + return match ? match[1].trim() : null
79 + }
80 +
81 + // 去重:只保留第一次出现的 cookie
82 + const seen = new Set()
83 + const uniqueParts = []
84 +
85 + for (const part of parts) {
86 + const name = extractCookieName(part)
87 + if (name && !seen.has(name)) {
88 + seen.add(name)
89 + uniqueParts.push(part)
90 + }
91 + }
92 +
93 + // 如果去重后只剩一个,直接返回
94 + if (uniqueParts.length === 1) {
95 + return uniqueParts[0]
96 + }
97 +
98 + // 否则用逗号连接
99 + return uniqueParts.join(',')
100 +}
101 +
102 +/**
47 * @description 清空 sessionid(一般不需要手动调用) 103 * @description 清空 sessionid(一般不需要手动调用)
48 * @returns {void} 无返回值 104 * @returns {void} 无返回值
49 */ 105 */
......