2026-02-02-auth-refactoring.md 20.8 KB

鉴权功能重构规划

📋 文档信息

  • 创建日期: 2026-02-02
  • 最后更新: 2026-02-02
  • 作者: Claude Code
  • 状态: 规划阶段

🎯 需求概述

重构项目的鉴权功能,采用简化设计

核心原则

  1. sessionid 只存储和传递:前端存储 sessionid 并在请求中发送给后端,但前端不依赖它来判断是否登录
  2. 401 统一跳转登录页:所有接口返回 401 时,跳转到登录页
  3. 不需要白名单:后端自己决定哪些接口需要鉴权,返回 401 即可
  4. 启动时检查状态:通过 loginStatusAPI 检查 openid 和登录状态

核心概念

概念 说明 状态字段
微信授权(openid) 后端通过 miniProgramAuthAPI 授权获取用户的 openid loginStatusAPI.data.is_openid
用户登录 用户通过 loginAPI 输入账号密码后绑定 openid loginStatusAPI.data.is_login
401 响应 表示用户未登录,跳转到登录页 后端接口响应

关键理解

  • sessionid 只是传递给后端的凭证,前端不依赖它做判断
  • 所有请求直接发送,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/
│   ├── auth/
│   │   └── index.vue        # 删除:不再需要单独的授权页
│   └── 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)

// 请求拦截器
instance.interceptors.request.use((config) => {
  // 后端通过 cookie 自动处理 sessionid
  // 前端不需要添加任何 sessionid 相关的逻辑
  return config
})

// 响应拦截器
instance.interceptors.response.use(
  (response) => {
    // 正常响应
    return response
  },
  (error) => {
    // 错误响应
    if (error.response?.status === 401) {
      // 401 表示用户未登录
      // 跳转到登录页
      Taro.navigateTo({
        url: '/pages/login/index'
      })
    }

    return Promise.reject(error)
  }
)

关键点

  • 不需要白名单:所有接口直接发送
  • 不处理 sessionid:后端通过 cookie 自动处理
  • 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 - 移除旧的授权逻辑
  • 删除 src/pages/auth/index.vue - 不再需要单独的授权页

第 3 步:更新登录页

  • 修改 src/pages/login/index.vue - 使用新的登录逻辑
    • 调用 userStore.login()
    • 处理登录成功/失败

第 4 步:测试验证

  • 测试首次启动流程(无 openid)
  • 测试 openid 授权流程
  • 测试自动登录(openid 已绑定账号)
  • 测试手动登录流程
  • 测试 401 处理
  • 测试已登录用户启动

第 5 步:文档更新

  • 更新 CLAUDE.md 鉴权部分
  • 更新 docs/lessons-learned.md
  • 添加鉴权流程图

⚠️ 注意事项

1. sessionid 的作用

  • miniProgramAuthAPI 后端自动处理 sessionid(如通过 cookie),前端不需要保存
  • loginAPI 不返回 sessionid,后端通过 cookie 或其他方式自动处理
  • 前端不依赖 sessionid 判断用户是否登录
  • 是否登录由后端通过 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,登录流程应该是:

  1. 小程序启动 → 检查 is_openid
  2. 如果 is_openid=false → 调用 wx.login → 调用 miniProgramAuthAPI
  3. 如果返回 user=null → 跳转登录页
  4. 用户输入账号密码 → 调用 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
                                              
                                       [跳转登录页]

实际字段映射

  • UNAUTHORIZEDis_openid = false
  • AUTH_NOT_LOGINis_openid = true && is_login = false
  • LOGGED_INis_openid = true && is_login = true

🔗 相关文档


最后更新: 2026-02-02 维护者: Claude Code