小程序静默登录-登录拦截实现方案【全局loginPromis加页面拦截】
'# 小程序静默登录-登录拦截实现方案【全局loginPromis加页面拦截】
一、背景与问题
在小程序开发中,用户登录是核心功能之一。传统的登录流程需要用户主动点击登录按钮,通过微信的授权接口获取用户信息,再通过后端接口换取登录凭证(如Token)。但随着业务复杂度提升,频繁的登录操作会显著影响用户体验。
静默登录(Silent Login)通过Token自动刷新机制,实现用户在未主动操作时自动完成登录。这要求开发者在前端维护Token的有效性,并在Token失效时自动刷新。同时,需要通过登录拦截机制防止未授权用户访问敏感页面。
核心问题包括:
- 如何在Token失效时自动刷新
- 如何在页面加载时进行登录状态校验
- 如何处理登录状态变更后的页面刷新
- 如何在不同业务场景中合理使用该方案
二、基本原理
1. Token生命周期管理
Token通常包含有效期(如15天),在用户首次登录时获取,后续通过静默刷新机制延长有效期。需要维护以下状态:
- 当前Token
- Token过期时间戳
- 登录状态(是否已登录)
2. 全局Promise机制
通过loginPromise封装登录逻辑,确保所有页面共享同一登录状态。关键点在于:
- 一次登录流程
- 自动刷新机制
- 异步状态管理
3. 页面拦截机制
通过覆盖onLoad/onShow生命周期,检查当前登录状态:
- 已登录:正常加载
- 未登录:跳转到登录页并等待登录完成
三、环境准备
1. 开发工具
- 微信开发者工具(最新版本)
- Node.js 16+
需要配置后端接口:
- 获取Token接口
/login - 刷新Token接口
/refresh-token
- 获取Token接口
2. 项目结构示例
project-root/
├── app.js
├── app.json
├── pages/
│ ├── index/
│ │ ├── index.js
│ │ └── index.json
│ └── login/
│ ├── login.js
│ └── login.json
├── utils/
│ ├── auth.js
│ └── http.js
└── config/
└── env.js四、核心实现
1. 全局登录状态管理(utils/auth.js)
// utils/auth.js
const wx = require('wx');
let loginPromise = null;
let isRefreshing = false;
let refreshCallbacks = [];
// 获取Token
async function getToken() {
// 检查本地缓存
const cachedToken = wx.getStorageSync('token');
const expiredAt = wx.getStorageSync('tokenExpiredAt');
// 如果Token已过期或不存在
if (!cachedToken || Date.now() > expiredAt) {
// 若正在刷新,等待刷新完成
if (isRefreshing) {
return new Promise(resolve => {
refreshCallbacks.push(resolve);
});
}
// 否则启动刷新流程
isRefreshing = true;
try {
const res = await refreshToken();
if (res.code === 200) {
wx.setStorageSync('token', res.data.accessToken);
wx.setStorageSync('tokenExpiredAt', Date.now() + res.data.expiresIn * 1000);
isRefreshing = false;
// 触发所有等待中的回调
refreshCallbacks.forEach(cb => cb(res.data.accessToken));
refreshCallbacks = [];
return res.data.accessToken;
}
} catch (err) {
console.error('Token刷新失败:', err);
isRefreshing = false;
}
}
return cachedToken;
}
// 刷新Token接口
async function refreshToken() {
const res = await http.post('/refresh-token');
return res;
}
// 登录接口
async function login() {
if (loginPromise) return loginPromise;
loginPromise = new Promise(async (resolve, reject) => {
try {
const res = await http.post('/login');
if (res.code === 200) {
wx.setStorageSync('token', res.data.accessToken);
wx.setStorageSync('tokenExpiredAt', Date.now() + res.data.expiresIn * 1000);
resolve(res.data.accessToken);
} else {
reject('登录失败');
}
} catch (err) {
reject('登录异常');
}
});
return loginPromise;
}
module.exports = {
login,
getToken
};2. 页面拦截逻辑(pages/index/index.js)
// pages/index/index.js
const auth = require('../../utils/auth');
Page({
data: {
isLogin: false
},
onLoad() {
this.checkLogin();
},
onShow() {
this.checkLogin();
},
checkLogin() {
auth.getToken().then(token => {
this.setData({ isLogin: true });
// 可选:触发页面初始化逻辑
}).catch(() => {
this.setData({ isLogin: false });
wx.redirectTo({ url: '/pages/login/login' });
});
}
});3. 登录流程处理(pages/login/login.js)
// pages/login/login.js
const auth = require('../../utils/auth');
Page({
data: {
loading: false
},
onLoad() {
this.checkLoginStatus();
},
checkLoginStatus() {
auth.getToken().then(token => {
// 已登录,跳转回首页
wx.reLaunch({ url: '/pages/index/index' });
}).catch(() => {
this.setData({ loading: true });
this.login();
});
},
login() {
auth.login().then(token => {
this.setData({ loading: false });
wx.showToast({ title: '登录成功' });
wx.reLaunch({ url: '/pages/index/index' });
}).catch(err => {
this.setData({ loading: false });
wx.showToast({ title: '登录失败', icon: 'none' });
});
}
});五、完整案例
1. 项目结构说明
project-root/
├── app.js
├── app.json
├── pages/
│ ├── index/
│ │ ├── index.js
│ │ └── index.json
│ └── login/
│ ├── login.js
│ └── login.json
├── utils/
│ ├── auth.js
│ └── http.js
└── config/
└── env.js2. 全局配置(app.js)
// app.js
const wx = require('wx');
App({
onLaunch() {
// 初始化登录状态
this.auth = require('./utils/auth');
this.auth.login().catch(() => {
// 未登录时处理
});
}
});3. HTTP工具类(utils/http.js)
// utils/http.js
const wx = require('wx');
const http = {
async post(url, data) {
const res = await wx.request({
url: `${process.env.BASE_URL}${url}`,
method: 'POST',
data,
header: {
'Authorization': `Bearer ${wx.getStorageSync('token')}`
}
});
if (res.data.code === 200) {
return res.data;
} else {
throw new Error(res.data.message || '请求失败');
}
}
};
module.exports = http;4. 环境配置(config/env.js)
// config/env.js
const env = {
BASE_URL: 'https://api.example.com'
};
module.exports = env;六、源码解析
1. auth.js关键逻辑解析
- 全局Promise:通过
loginPromise确保所有页面共享同一登录状态 - Token刷新机制:当Token过期时自动触发刷新流程
- 异步处理:使用
refreshCallbacks处理多个等待中的请求 - 异常处理:在刷新失败时标记
isRefreshing状态
2. 页面拦截逻辑
- 双重检查:同时检查登录状态和Token有效性
- 页面跳转:未登录时强制跳转至登录页
- 避免重复登录:通过
checkLogin方法统一处理登录状态
七、进阶使用
1. 增强版登录状态管理
// utils/auth.js
let loginPromise = null;
let refreshCallbacks = [];
function getLoginStatus() {
return new Promise((resolve, reject) => {
wx.getStorage({
key: 'token',
success: (res) => {
const token = res.data;
const expiredAt = wx.getStorageSync('tokenExpiredAt');
if (Date.now() < expiredAt) {
resolve({ status: 'valid', token });
} else {
// 需要刷新
if (isRefreshing) {
refreshCallbacks.push(resolve);
} else {
isRefreshing = true;
refreshToken().then(token => {
isRefreshing = false;
resolve({ status: 'valid', token });
}).catch(err => {
isRefreshing = false;
reject(err);
});
}
}
},
fail: () => {
reject('Token不存在');
}
});
});
}2. 页面权限控制扩展
// pages/index/index.js
checkLogin() {
getLoginStatus().then(({ status, token }) => {
if (status === 'valid') {
this.setData({ isLogin: true });
// 可选:触发页面初始化逻辑
} else {
this.setData({ isLogin: false });
wx.redirectTo({ url: '/pages/login/login' });
}
}).catch(() => {
this.setData({ isLogin: false });
wx.redirectTo({ url: '/pages/login/login' });
});
}八、性能与工程实践
1. 性能优化策略
- Token缓存:使用
wx.getStorageSync避免频繁读取 - 异步节流:在Token刷新时使用
debounce防止频繁请求 - 网络请求优化:对刷新Token接口进行重试机制
- 错误处理:捕获并处理Token刷新失败的情况
2. 安全风险分析
- Token泄露风险:需通过HTTPS加密传输
- 本地存储安全:建议使用加密存储(如
wx.setStorageSync配合加密算法) - Token有效期控制:设置合理的过期时间(通常15天)
- 防止暴力破解:对登录接口进行频率限制
九、常见问题与踩坑
1. 常见错误及解决办法
错误1:Token未正确刷新
- 原因:未处理
isRefreshing标志位 - 解决:在刷新Token时设置
isRefreshing = true,刷新完成后重置
错误2:页面拦截失效
- 原因:未覆盖所有页面的
onLoad/onShow生命周期 - 解决:在
app.js中统一注册页面拦截逻辑
错误3:登录状态不一致
- 原因:未处理多页面同时请求的情况
- 解决:使用全局
loginPromise统一管理状态
2. 典型问题场景
场景1:Token突然失效
- 现象:用户未主动操作时页面突然跳转至登录页
- 原因:Token过期未自动刷新
- 解决:检查
tokenExpiredAt时间戳是否正确计算
场景2:登录后页面未更新
- 现象:用户登录后页面仍显示未登录状态
- 原因:未触发页面重载
- 解决:在登录成功后调用
wx.reLaunch强制刷新页面
十、最佳实践
1. 推荐方案
- 使用
loginPromise统一管理登录状态 - 在
onLoad/onShow中进行登录状态检查 - 对Token刷新接口添加重试机制
- 使用
wx.getStorageSync进行本地缓存 - 对敏感接口添加权限校验
2. 避免使用的场景
- 对性能敏感的页面(如实时数据展示)
- 需要立即响应的业务场景(如即时通讯)
- 允许匿名访问的页面(如注册页)
3. 方案比较
| 方案类型 | 优点 | 缺点 |
|---|---|---|
| 全局Promise | 状态统一管理 | 代码复杂度较高 |
| 单页面拦截 | 简单易实现 | 状态管理不统一 |
| 框架拦截器 | 与框架深度集成 | 依赖具体框架实现 |
| 本地缓存+检查 | 实现简单 | 需要手动处理状态同步 |
十一、总结
小程序静默登录+页面拦截方案是维护用户登录状态的核心机制。通过全局loginPromise和页面拦截机制,可以有效保障用户在未主动操作时的登录状态。该方案适用于需要频繁访问的业务场景,但需注意避免在性能敏感或需要立即响应的场景中使用。
关键实践包括:
- 使用全局状态管理确保状态一致性
- 通过异步机制处理Token刷新
- 在页面生命周期中进行状态检查
- 合理设置Token有效期和刷新策略
- 处理网络异常和刷新失败情况
在实际开发中,应根据业务需求选择合适的方案。对于复杂业务场景,建议结合本地缓存、服务器端验证和安全机制,构建完整的登录保护体系。同时,注意在关键路径添加日志记录,便于后续问题排查和性能优化。
评论已关闭