2026-02-02-auth-refactoring.md
22.7 KB
鉴权功能重构规划
📋 文档信息
- 创建日期: 2026-02-02
- 最后更新: 2026-02-02
- 作者: Claude Code
- 状态: 规划阶段
🎯 需求概述
重构项目的鉴权功能,采用简化设计:
核心原则
-
sessionid 动态获取并设置到请求头:每次请求前动态读取
localStorage.sessionid,设置到请求头的cookie字段 - 401 统一跳转登录页:所有接口返回 401 时,跳转到登录页
- 不需要白名单:后端自己决定哪些接口需要鉴权,返回 401 即可
-
启动时检查状态:通过
loginStatusAPI检查 openid 和登录状态
核心概念
| 概念 | 说明 | 状态字段 |
|---|---|---|
| 微信授权(openid) | 后端通过 miniProgramAuthAPI 授权获取用户的 openid |
loginStatusAPI.data.is_openid |
| 用户登录 | 用户通过 loginAPI 输入账号密码后绑定 openid |
loginStatusAPI.data.is_login |
| sessionid | 存储在 localStorage.sessionid,每次请求动态读取并设置到请求头 cookie 字段 |
localStorage.sessionid |
| 401 响应 | 表示用户未登录,跳转到登录页 | 后端接口响应 |
关键理解:
- ⭐ sessionid 必须动态获取并设置到请求头:每次请求前从
localStorage.sessionid读取,设置到config.headers.cookie - 前端不依赖 sessionid 判断是否登录(由后端通过 401 判断)
- 所有请求直接发送,401 统一处理
- 不需要白名单机制
📊 API 数据结构
1. miniProgramAuthAPI - 小程序授权
接口地址:/srv/?a=openid
功能:小程序授权,获取 openid 并尝试自动登录
请求方式:POST
请求参数:
{
code: string // wx.login 获取的 code
}
响应结构:
{
code: 1,
msg: "success",
data: {
user: {
id: integer, // 用户ID(可能为空)
avatar_url: string, // 头像
name: string // 姓名
} || null // 如果为空,表示需要调用登录接口
}
}
登录流程设计(来自注释):
- 先小程序授权
- 如果返回 用户为空,则需要调用登录接口(
loginAPI) - 如果返回 用户非空,则不需要调用登录接口,授权接口内部按照 OpenID 绑定的账号,自动登录
2. loginAPI - 账号密码登录
接口地址:/srv/?a=user&t=login
功能:登录并绑定 openid
请求方式:POST
请求参数:
{
uuid: string, // 账号(手机号或其他)
password: string // 密码
}
响应结构:
{
code: 1,
msg: "success",
data: any // 没有返回值,只有 code 和 msg
}
3. loginStatusAPI - 查询登录状态
接口地址:/srv/?a=user&t=login_status
功能:查询用户的微信授权状态和登录状态
请求方式:GET
响应结构:
{
code: 1,
msg: "success",
data: {
is_login: boolean, // true=已登录,false=未登录
is_openid: boolean // true=已授权(有 openid),false=未授权
}
}
示例响应:
// 场景 1:已授权 + 已登录
{
code: 1,
msg: "success",
data: {
is_login: true,
is_openid: true
}
}
// 场景 2:已授权 + 未登录
{
code: 1,
msg: "success",
data: {
is_login: false,
is_openid: true
}
}
// 场景 3:未授权
{
code: 1,
msg: "success",
data: {
is_login: false,
is_openid: false
}
}
4. getProfileAPI - 获取个人信息
接口地址:/srv/?a=user&t=get_profile
功能:获取当前登录用户的个人信息
请求方式:GET
响应结构:
{
code: 1,
msg: "success",
data: {
user: {
id: integer,
avatar_url: string,
name: string
}
}
}
5. logoutAPI - 退出登录
接口地址:/srv/?a=user&t=logout
功能:退出登录并解绑 openid
请求方式:POST
响应结构:
{
code: 1,
msg: "success",
data: any
}
🏗️ 鉴权架构设计
简化的鉴权模型
┌─────────────────────────────────────────────────────────────┐
│ 小程序启动 │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 调用 loginStatusAPI │
│ 检查 is_openid + is_login │
└─────────────────────────────────────────────────────────────┘
│
┌───────────────┴───────────────┐
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ is_openid │ │ is_login │
│ = false │ │ = false │
└─────────────────┘ └─────────────────┘
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ 调用 wx.login │ │ 跳转到登录页 │
│ 获取 code │ │ (/pages/login) │
│ 调用 │ │ │
│ miniProgramAuthAPI│ │ │
└─────────────────┘ └─────────────────┘
│
▼
┌─────────────────┐
│ 返回 user? │
└─────────────────┘
│ │
有值 │ │ 为空
│ └──────────→ 跳转到登录页
▼
正常进入小程序
请求处理流程
所有接口请求
│
├─→ 添加 sessionid 到请求头(如果有)
│
├─→ 发送请求
│
└─→ 响应处理
├─→ 200 → 正常处理
├─→ 401 → 清除 sessionid → 跳转登录页
└─→ 其他错误 → 显示错误提示
🔄 鉴权流程设计
场景 1:小程序启动
flowchart TD
A[小程序启动] --> B[调用 loginStatusAPI]
B --> C{检查响应}
C -->|网络错误| D[显示错误提示]
C -->|成功| E{is_openid?}
E -->|false| F[调用 wx.login 获取 code]
F --> G[调用 miniProgramAuthAPI]
G --> H{返回 user?}
H -->|为空| I[跳转到登录页]
H -->|有值| J[正常进入小程序]
E -->|true| K{is_login?}
K -->|false| I
K -->|true| L[正常进入小程序]
代码流程:
// 1. 检查状态
const { is_openid, is_login } = await loginStatusAPI()
// 2. 处理未授权
if (!is_openid) {
const { code } = await Taro.login()
const res = await miniProgramAuthAPI({ code })
if (res.data.user) {
// 已自动登录
return res.data.user
} else {
// 需要登录
Taro.navigateTo({ url: '/pages/login/index' })
return
}
}
// 3. 处理未登录
if (!is_login) {
Taro.navigateTo({ url: '/pages/login/index' })
}
场景 2:用户登录
flowchart TD
A[用户在登录页] --> B[输入账号密码]
B --> C[调用 loginAPI]
C --> D{登录结果}
D -->|成功| E[保存 sessionid]
E --> F[保存用户信息]
F --> G[返回上一页或首页]
D -->|失败| H[显示错误提示]
场景 3:接口请求 401
flowchart TD
A[发起接口请求] --> B[添加 sessionid 到请求头]
B --> C[发送请求]
C --> D{响应状态}
D -->|200| E[正常处理]
D -->|401| F[清除 sessionid]
F --> G[跳转到登录页]
D -->|其他错误| H[显示错误提示]
关键点:
- 401 只表示用户未登录,不表示 openid 未授权
- 401 时清除 sessionid,跳转到登录页
- 不在 401 时重新调用 wx.login 或 miniProgramAuthAPI
📁 文件结构规划
核心文件
src/
├── utils/
│ ├── openid.js # 新增:微信授权(openid)管理
│ ├── request.js # 修改:HTTP 请求拦截器(移除白名单和 sessionid)
│ └── authRedirect.js # 删除:完全替换为新逻辑
│
├── api/
│ ├── user.js # 已存在:用户相关 API
│ └── wechat.js # 已存在:微信授权 API
│
├── pages/
│ └── login/
│ └── index.vue # 保留:用户登录页(账号密码登录)
│
├── stores/
│ └── user.js # 新增:用户状态管理(Pinia)
│
└── app.js # 修改:启动时检查登录状态
文件职责
| 文件 | 职责 | 状态 |
|---|---|---|
utils/openid.js |
微信授权逻辑(wx.login、miniProgramAuthAPI) | 新增 |
utils/request.js |
HTTP 拦截器(401 处理,移除白名单和 sessionid) | 修改 |
utils/authRedirect.js |
旧授权逻辑 | 删除 |
api/user.js |
用户相关 API | 已存在 |
api/wechat.js |
微信授权 API | 已存在 |
stores/user.js |
用户信息状态管理 | 新增 |
app.js |
启动时检查登录状态 | 修改 |
🔧 实现细节
1. 微信授权管理 (utils/openid.js)
import Taro from '@tarojs/taro'
import { miniProgramAuthAPI } from '@/api/wechat'
import { loginStatusAPI } from '@/api/user'
/**
* 小程序授权
* @description 调用 wx.login 获取 code,由后端授权获取 openid
* @returns {Promise<{user: Object|null}>} 返回用户信息(如果已自动登录)
*/
export async function miniProgramAuth() {
try {
// 1. 调用 wx.login 获取 code
const { code } = await Taro.login()
if (!code) {
throw new Error('获取微信 code 失败')
}
// 2. 调用后端授权接口
const res = await miniProgramAuthAPI({ code })
if (res.code === 1) {
return res.data.user || null
} else {
throw new Error(res.msg || '小程序授权失败')
}
} catch (err) {
console.error('小程序授权失败:', err)
throw err
}
}
/**
* 检查 openid 状态
* @description 调用 loginStatusAPI 检查 is_openid
* @returns {Promise<boolean>} 是否已授权
*/
export async function checkOpenidStatus() {
try {
const res = await loginStatusAPI()
if (res.code === 1) {
return res.data.is_openid
} else {
return false
}
} catch (err) {
console.error('检查 openid 状态失败:', err)
return false
}
}
/**
* 确保 openid 已授权并尝试自动登录
* @description 如果未授权,则调用 wx.login 授权
* @returns {Promise<{user: Object|null}>} 返回用户信息(如果已自动登录)
*/
export async function ensureOpenidAuthorized() {
const isOpenid = await checkOpenidStatus()
if (!isOpenid) {
// 未授权,调用 wx.login 授权
return await miniProgramAuth()
}
// 已授权,返回 null(需要检查登录状态)
return null
}
2. sessionid 管理 (utils/session.js)
const SESSION_KEY = 'sessionid'
/**
* 保存 sessionid
* @param {string} sessionid
*/
export function setSessionId(sessionid) {
if (!sessionid) {
console.warn('sessionid 为空,无法保存')
return
}
localStorage.setItem(SESSION_KEY, sessionid)
}
/**
* 获取 sessionid
* @returns {string|null}
*/
export function getSessionId() {
return localStorage.getItem(SESSION_KEY)
}
/**
* 清除 sessionid
*/
export function clearSessionId() {
localStorage.removeItem(SESSION_KEY)
}
/**
* 检查是否有 sessionid
* @description 注意:这只是检查本地是否有 sessionid,不代表用户已登录
* @returns {boolean}
*/
export function hasSessionId() {
return !!getSessionId()
}
3. 请求拦截器 (utils/request.js)
⭐ sessionid 动态获取并设置到请求头(重要):
// 请求拦截器
service.interceptors.request.use(config => {
// 合并默认参数...
/**
* ⭐ 动态获取 sessionid 并设置到请求头
* - 确保每个请求都带上最新的 sessionid
* - 从 localStorage.sessionid 读取
* - 设置到 config.headers.cookie 字段
*/
const sessionid = getSessionId() // 从 localStorage 读取
if (sessionid) {
config.headers = config.headers || {}
config.headers.cookie = sessionid // 设置到 cookie 字段
}
// 增加时间戳
if (config.method === 'get') {
config.params = { ...config.params, timestamp: (new Date()).valueOf() }
}
return config
})
响应拦截器:
// 响应拦截器
service.interceptors.response.use(
async response => {
const res = response.data
// 401 未授权处理
if (res.code === 401) {
// 跳转到登录页
Taro.navigateTo({
url: '/pages/login/index'
}).catch(() => {
// 如果跳转失败(如已经在登录页),则忽略
console.warn('跳转登录页失败,可能已在登录页')
})
}
return response
},
async error => {
// 处理弱网/断网
if (await should_handle_bad_network(error)) {
handle_request_timeout()
}
return Promise.reject(error)
}
)
关键点:
-
⭐ sessionid 动态获取:每次请求前从
localStorage.sessionid读取 -
⭐ 设置到 cookie 字段:
config.headers.cookie = sessionid - 不需要白名单:所有接口直接发送
- 401 统一处理:跳转登录页
4. 用户状态管理 (stores/user.js)
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { loginStatusAPI, loginAPI, getProfileAPI, logoutAPI } from '@/api/user'
import { ensureOpenidAuthorized } from '@/utils/openid'
export const useUserStore = defineStore('user', () => {
// 状态
const userInfo = ref(null) // 用户信息
const isOpenid = ref(false) // 是否已授权(openid)
const isLoggedIn = ref(false) // 是否已登录
const loading = ref(false) // 加载状态
/**
* 检查登录状态
* @description 检查 openid 和登录状态,处理相应的逻辑
*/
async function checkLoginStatus() {
loading.value = true
try {
// 1. 确保 openid 已授权并尝试自动登录
const user = await ensureOpenidAuthorized()
if (user) {
// miniProgramAuthAPI 返回了用户信息,说明已自动登录
userInfo.value = user
isOpenid.value = true
isLoggedIn.value = true
return
}
// 2. 查询登录状态
const res = await loginStatusAPI()
if (res.code === 1) {
isOpenid.value = res.data.is_openid
isLoggedIn.value = res.data.is_login
// 3. 如果已登录,获取用户信息
if (isLoggedIn.value) {
await fetchUserInfo()
} else {
// 未登录,跳转到登录页
Taro.navigateTo({
url: '/pages/login/index'
})
}
} else {
throw new Error(res.msg || '查询登录状态失败')
}
} catch (err) {
console.error('检查登录状态失败:', err)
throw err
} finally {
loading.value = false
}
}
/**
* 获取用户信息
*/
async function fetchUserInfo() {
try {
const res = await getProfileAPI()
if (res.code === 1) {
userInfo.value = res.data.user
} else {
throw new Error(res.msg || '获取用户信息失败')
}
} catch (err) {
console.error('获取用户信息失败:', err)
throw err
}
}
/**
* 用户登录
* @param {Object} loginData 登录数据
* @param {string} loginData.uuid 账号
* @param {string} loginData.password 密码
*/
async function login(loginData) {
loading.value = true
try {
const res = await loginAPI(loginData)
if (res.code === 1) {
// 登录成功,获取用户信息
await fetchUserInfo()
isLoggedIn.value = true
return { success: true }
} else {
throw new Error(res.msg || '登录失败')
}
} catch (err) {
console.error('登录失败:', err)
return { success: false, message: err.message }
} finally {
loading.value = false
}
}
/**
* 用户登出
*/
async function logout() {
try {
// 调用登出接口
await logoutAPI()
// 清除本地状态
userInfo.value = null
isOpenid.value = false
isLoggedIn.value = false
} catch (err) {
console.error('登出失败:', err)
}
}
return {
// 状态
userInfo,
isOpenid,
isLoggedIn,
loading,
// 方法
checkLoginStatus,
fetchUserInfo,
login,
logout
}
})
5. 应用启动逻辑 (app.js)
import { useUserStore } from '@/stores/user'
function App(props) {
const userStore = useUserStore()
useLaunch(() => {
console.log('小程序启动')
// 检查登录状态
userStore.checkLoginStatus().catch(err => {
console.error('启动时检查登录状态失败:', err)
})
})
return props.children
}
🚀 实施步骤
第 1 步:创建新文件
-
创建
src/utils/openid.js- 微信授权(openid)管理 -
创建
src/stores/user.js- 用户状态管理
第 2 步:修改现有文件
-
修改
src/utils/request.js- 更新请求拦截器- 移除白名单配置
- 移除 sessionid 相关逻辑
- 更新 401 响应处理
-
修改
src/app.js- 启动时检查登录状态 -
删除
src/utils/authRedirect.js- 移除旧的授权逻辑 - 确认不再需要单独的授权页(当前仅保留登录页)
第 3 步:更新登录页
-
修改
src/pages/login/index.vue- 使用新的登录逻辑- 调用
userStore.login() - 处理登录成功/失败
- 调用
第 4 步:测试验证
- 测试首次启动流程(无 openid)
- 测试 openid 授权流程
- 测试自动登录(openid 已绑定账号)
- 测试手动登录流程
- 测试 401 处理
- 测试已登录用户启动
第 5 步:文档更新
-
更新
CLAUDE.md鉴权部分 -
更新
docs/lessons-learned.md - 添加鉴权流程图
⚠️ 注意事项
1. sessionid 的处理(⭐ 重要)
-
动态获取:每次请求前从
localStorage.sessionid读取,确保使用最新的 sessionid -
设置到请求头:将 sessionid 设置到
config.headers.cookie字段 -
存储位置:
localStorage.sessionid(注意:不是localStorage.user_info) -
设置时机:
-
miniProgramAuthAPI授权成功后(如果后端返回 sessionid) -
loginAPI登录成功后(如果后端返回 sessionid)
-
- 清除时机:401 响应时清除、用户登出时清除
代码示例(已在 src/utils/request.js 中实现):
// 请求拦截器
service.interceptors.request.use(config => {
// 动态获取 sessionid 并设置到请求头
const sessionid = getSessionId() // 从 localStorage 读取
if (sessionid) {
config.headers = config.headers || {}
config.headers.cookie = sessionid // 设置到 cookie 字段
}
return config
})
2. 不需要白名单
- 所有接口直接发送
- 后端自己决定哪些接口需要鉴权
- 返回 401 的接口统一跳转登录页
2. 不需要白名单
- 所有接口直接发送
- 后端自己决定哪些接口需要鉴权
- 返回 401 的接口统一跳转登录页
3. miniProgramAuthAPI 的特殊逻辑
根据注释:
如果返回 用户为空,则需要调用登录接口(
loginAPI) 如果返回 用户非空,则不需要调用登录接口,授权接口内部按照 OpenID 绑定的账号,自动登录
这意味着:
- 后端自动处理 sessionid(如通过 cookie),前端不需要保存
- 用户第一次使用:
is_openid=false→ 调用miniProgramAuthAPI→ 返回user=null→ 跳转登录页 - 用户已绑定账号:
is_openid=true→ 调用miniProgramAuthAPI→ 返回user→ 自动登录 -
loginAPI 不返回 sessionid 和 user,登录成功后需要单独调用
getProfileAPI获取用户信息
4. 401 错误处理
- 401 只表示用户未登录,不表示 openid 未授权
- 401 时清除 sessionid,跳转到登录页
- 不要在 401 时重新调用 wx.login 或 miniProgramAuthAPI
5. 登录流程
根据现有 API,登录流程应该是:
- 小程序启动 → 检查
is_openid - 如果
is_openid=false→ 调用wx.login→ 调用miniProgramAuthAPI - 如果返回
user=null→ 跳转登录页 - 用户输入账号密码 → 调用
loginAPI绑定
📊 状态机
// 用户状态枚举
const UserState = {
// 未授权(未绑定 openid)
UNAUTHORIZED: 'unauthorized',
// 已授权,未登录(已绑定 openid,但未绑定业务账号)
AUTH_NOT_LOGIN: 'auth_not_login',
// 已登录(已绑定业务账号)
LOGGED_IN: 'logged_in'
}
// 状态转换
UNAUTHORIZED → [miniProgramAuthAPI] → AUTH_NOT_LOGIN → [loginAPI] → LOGGED_IN
↓
[跳转登录页]
实际字段映射:
-
UNAUTHORIZED↔is_openid = false -
AUTH_NOT_LOGIN↔is_openid = true && is_login = false -
LOGGED_IN↔is_openid = true && is_login = true
🔗 相关文档
最后更新: 2026-02-02 维护者: Claude Code