9.7 KiB
9.7 KiB
用户登录与信息授权方案
版本:v1.0 最后更新:2026-05-06 适用范围:微信小程序云开发环境下的用户身份管理
一、方案概述
采用静默登录策略:用户进入小程序后无感知地完成身份识别与用户创建,不弹出任何授权弹窗。昵称和头像等个人信息在后续需要时(如个人中心、评论等场景)再引导用户主动授权。
核心流程:
用户打开小程序
│
├─ 本地 Storage 有用户数据且未过期?
│ │
│ ├─ 是 → 直接使用本地数据,跳过登录
│ │
│ └─ 否 → 调用云函数 silentLogin
│ │
│ ├─ cloud.getWXContext() 获取 openid/unionid
│ │
│ ├─ 查询 users 集合(openid)
│ │ │
│ │ ├─ 存在 → 更新 lastActiveAt,返回用户数据
│ │ │
│ │ └─ 不存在 → 创建用户(nickName/avatarUrl 为空),返回用户数据
│ │
│ └─ 前端收到用户数据 → 写入内存 + Storage
│
└─ 完成,进入正常使用
二、技术细节
2.1 云函数:userLogin
职责:静默登录,upsert 用户记录。
// cloudfunctions/userLogin/index.js
const cloud = require('wx-server-sdk');
cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV });
const db = cloud.database();
exports.main = async (event, context) => {
const wxContext = cloud.getWXContext();
const openid = wxContext.OPENID;
const unionid = wxContext.UNIONID || null;
const usersCollection = db.collection('users');
// 查询是否已存在
const { data } = await usersCollection.where({ openid }).limit(1).get();
const now = db.serverDate();
if (data.length > 0) {
// 已存在:更新 lastActiveAt 和 unionid(如果有新值)
const user = data[0];
const updateData = { lastActiveAt: now };
if (unionid && !user.unionid) {
updateData.unionid = unionid;
}
await usersCollection.doc(user._id).update({ data: updateData });
return {
code: 0,
data: { ...user, ...updateData, lastActiveAt: new Date() },
};
} else {
// 不存在:创建新用户
const newUser = {
openid,
unionid,
nickName: null,
avatarUrl: null,
totalDownloads: 0,
createdAt: now,
lastActiveAt: now,
};
const { _id } = await usersCollection.add({ data: newUser });
return {
code: 0,
data: {
_id,
...newUser,
createdAt: new Date(),
lastActiveAt: new Date(),
},
};
}
};
要点:
cloud.getWXContext()在云函数中自动获取调用者的OPENID,无需前端传 codeUNIONID仅在小程序绑定了开放平台且满足条件时才有值,所以设为可选- 使用
db.serverDate()确保时间由服务端生成,避免客户端时间不准 _id由云数据库自动生成(add()时不指定),为随机唯一字符串;业务上通过openid唯一索引定位用户,_id作为主键供子表(favorites、download_logs)的userId外键引用
2.2 前端登录逻辑
位置:建议封装为 miniprogram/utils/auth.ts
// miniprogram/utils/auth.ts
interface UserInfo {
_id: string;
openid: string;
unionid: string | null;
nickName: string | null;
avatarUrl: string | null;
totalDownloads: number;
createdAt: string;
lastActiveAt: string;
}
const STORAGE_KEY = 'user_info';
const STORAGE_EXPIRE_KEY = 'user_info_expire';
const EXPIRE_DURATION = 7 * 24 * 60 * 60 * 1000; // 7 天
/** 内存中的用户数据(App 生命周期内有效) */
let currentUser: UserInfo | null = null;
/**
* 获取当前用户,优先内存 → Storage → 云函数登录
*/
export async function getUser(): Promise<UserInfo> {
// 1. 内存中有,直接返回
if (currentUser) return currentUser;
// 2. 尝试从 Storage 读取
const cached = loadFromStorage();
if (cached) {
currentUser = cached;
return cached;
}
// 3. 静默登录
return await silentLogin();
}
/**
* 静默登录:调用云函数
*/
async function silentLogin(): Promise<UserInfo> {
const { result } = await wx.cloud.callFunction({
name: 'userLogin',
});
if (result.code !== 0) {
throw new Error('登录失败');
}
const user = result.data as UserInfo;
currentUser = user;
saveToStorage(user);
return user;
}
/**
* 从 Storage 读取用户数据(检查过期)
*/
function loadFromStorage(): UserInfo | null {
try {
const expire = wx.getStorageSync(STORAGE_EXPIRE_KEY);
if (!expire || Date.now() > expire) {
// 已过期,清除
wx.removeStorageSync(STORAGE_KEY);
wx.removeStorageSync(STORAGE_EXPIRE_KEY);
return null;
}
const user = wx.getStorageSync(STORAGE_KEY);
return user || null;
} catch {
return null;
}
}
/**
* 写入 Storage
*/
function saveToStorage(user: UserInfo): void {
try {
wx.setStorageSync(STORAGE_KEY, user);
wx.setStorageSync(STORAGE_EXPIRE_KEY, Date.now() + EXPIRE_DURATION);
} catch {
// Storage 写入失败不影响主流程
}
}
/**
* 清除登录状态(用于退出登录或需要强制刷新时)
*/
export function clearUser(): void {
currentUser = null;
wx.removeStorageSync(STORAGE_KEY);
wx.removeStorageSync(STORAGE_EXPIRE_KEY);
}
/**
* 强制刷新用户数据(跳过缓存)
*/
export async function refreshUser(): Promise<UserInfo> {
clearUser();
return await silentLogin();
}
2.3 调用时机
在 app.ts 的 onLaunch 中触发登录:
// app.ts
import { getUser } from './utils/auth';
App({
onLaunch() {
// 初始化云开发
wx.cloud.init({ env: 'your-env-id' });
// 静默登录(不阻塞页面渲染)
getUser().catch((err) => {
console.error('静默登录失败', err);
});
},
});
注意:登录是异步的,不阻塞页面渲染。页面中需要用户数据时通过 await getUser() 获取,如果登录已完成会立即返回内存数据。
2.4 缓存过期策略
| 策略 | 说明 |
|---|---|
| 有效期 | 7 天 |
| 过期后行为 | 下次 getUser() 时自动触发静默登录 |
| 过期目的 | 定期刷新 lastActiveAt,保持活跃数据准确 |
| 强制刷新 | 调用 refreshUser() 可随时强制重新登录 |
三、昵称与头像授权(后续实现)
静默登录不获取用户昵称和头像。当需要展示或使用这些信息时(如个人中心页),引导用户主动填写:
// 使用微信头像昵称填写能力(基础库 2.21.2+)
// 在 wxml 中使用 <button open-type="chooseAvatar"> 获取头像
// 使用 <input type="nickname"> 获取昵称
export async function updateProfile(
nickName: string,
avatarUrl: string,
): Promise<void> {
const { result } = await wx.cloud.callFunction({
name: 'userLogin',
data: {
action: 'updateProfile',
nickName,
avatarUrl,
},
});
if (result.code === 0) {
// 更新本地缓存
const user = await getUser();
user.nickName = nickName;
user.avatarUrl = avatarUrl;
saveToStorage(user);
}
}
注:微信已废弃
wx.getUserProfile和wx.getUserInfo获取真实用户信息的能力,现在只能通过头像昵称填写组件让用户主动填写。
四、云函数目录结构
cloudfunctions/
└── userLogin/
├── index.js ← 入口,支持 action 路由
├── package.json
└── config.json ← 云函数配置
若后续 updateProfile 等操作较多,可在同一个云函数内通过 event.action 路由:
exports.main = async (event, context) => {
const { action } = event;
switch (action) {
case 'updateProfile':
return await handleUpdateProfile(event, context);
default:
return await handleSilentLogin(event, context);
}
};
五、安全考虑
| 项目 | 措施 |
|---|---|
| openid 来源 | 仅从 cloud.getWXContext() 获取,不信任前端传入 |
| unionid | 服务端获取,不暴露给前端 |
| Storage 数据 | 不存储敏感信息(openid 不算高敏感,但也不主动展示给用户) |
| 接口幂等 | upsert 逻辑天然幂等,重复调用不会创建多条记录 |
六、与现有架构的关系
- 数据库:使用
users集合,字段设计见 小程序云开发方案和数据库设计.md §3.5 - 其他云函数:需要用户身份的云函数(如收藏、下载记录)通过
cloud.getWXContext().OPENID查询users集合获取_id,作为userId关联 - 前端:各页面通过
getUser()获取用户数据,无需关心登录细节