微信小程序双登录模式完整实现指南
一、概述
本项目采用静默登录 + 授权登录双模式配合的设计方案,实现老用户无感登录、新用户授权注册的完整用户体验。
核心设计目的
| 登录方式 | 适用场景 | 用户体验 | 目的 |
|---|---|---|---|
| 静默登录 | 老用户打开小程序 | 无感知自动登录 | 提升老用户体验,减少操作步骤 |
| 授权登录 | 新用户首次使用 | 需点击授权按钮 | 获取用户信息完成注册 |
二、静默登录流程
2.1 触发时机
静默登录不是 在 App.vue 的 onLaunch 中直接触发的,而是通过请求拦截器间接触发的。具体触发链路如下:
任意 API 请求返回 code = -1(token 过期/未登录)
↓
响应拦截器 (utils/request.js) 匹配 APICodeEnum[-1] = 'redirect'
↓
调用 events.redirect() → toLogin()
↓
toLogin() → _toLogin() → mnpLogin()(小程序环境)
此外,路由守卫也会在用户访问需要登录的页面时,跳转到登录页:
用户访问 meta.auth = true 的页面
↓
router.beforeEach 检测无 token
↓
next('/pages/login/login') 跳转登录页
2.2 触发机制详解
触发点 1:请求拦截器(主要触发方式)
当任意 API 请求返回 code = -1 时,表示 token 无效或用户未登录,响应拦截器会自动触发静默登录:
javascript
// utils/enum.js - 接口返回码枚举
export const APICodeEnum = {
1: 'success', // 成功
0: 'fail', // 失败
'-1': 'redirect', // 重定向(token失效,需要重新登录)
1020: 'closeShop', // 商城关闭
10: 'tips' // 提示
}
javascript
// utils/request.js - 响应拦截器
import { APICodeEnum } from './enum'
import { toLogin } from './login'
const events = {
success({ data }) {
return Promise.resolve(data)
},
fail({ msg }) {
return Promise.reject(msg)
},
// 当 code = -1 时触发
redirect({ msg }) {
// #ifdef H5
if (store.getters.appConfig.h5_status) {
toLogin() // 触发静默登录
}
// #endif
// #ifdef MP-WEIXIN
if (store.getters.appConfig.mnp_status) {
toLogin() // 触发静默登录
}
// #endif
store.commit('logout') // 清除本地登录状态
return Promise.reject(msg)
},
closeShop({ msg }) { /* ... */ },
tips({ code, msg }) { /* ... */ }
}
// 响应拦截器中根据 code 分发到对应的 events 方法
service.interceptors.response.use((response) => {
const { msg, code, data, show } = response.data
if (show && msg && code !== 10) {
toast({ title: msg })
}
return events[APICodeEnum[code]](response.data) // code=-1 → events.redirect()
})
触发点 2:路由守卫(辅助触发方式)
当用户访问需要登录的页面(meta.auth = true)时,路由守卫会拦截并跳转到登录页:
javascript
// router.js - 路由守卫
const whiteList = ['register', 'login', 'forget_pwd']
router.beforeEach((to, from, next) => {
// 保存登录前的路径(排除白名单页面)
const index = whiteList.findIndex((item) => from.path.includes(item))
if (index == -1 && !store.getters.token) {
Cache.set(BACK_URL, from.fullPath)
}
// 需要登录的页面 + 无 token → 跳转登录页
if (to.meta.auth && !store.getters.token &&
to.path !== '/bundle/pages/business_suspended/business_suspended') {
next('/pages/login/login')
return
} else {
next()
}
})
toLogin() 中转函数
toLogin 是静默登录的入口函数,在小程序环境中调用 mnpLogin():
javascript
// utils/login.js
export const toLogin = trottle(_toLogin, 2000) // 节流,2秒内只触发一次
function _toLogin() {
// #ifdef MP
mnpLogin() // 小程序环境:触发静默登录
// #endif
// #ifndef MP
const { currentRoute } = router
if (currentRoute.meta.auth) {
router.push("/pages/login/login") // 非小程序环境:跳转登录页
}
// #endif
}
2.3 完整流程图
用户打开小程序,触发任意 API 请求
↓
后端返回 code = -1(token无效/未登录)
↓
响应拦截器匹配 APICodeEnum[-1] = 'redirect'
↓
调用 events.redirect() → toLogin()
↓
toLogin() → _toLogin() → mnpLogin()(小程序环境)
↓
获取配置 (coerce_mobile, mnp_auto_wechat_auth)
↓
检查是否开启自动授权 (mnp_auto_wechat_auth)
├─ 未开启 → 终止流程
└─ 已开启 → 继续
↓
调用 uni.login() 获取 code
↓
请求后端接口 login/silentLogin
↓
后端使用 code 换取 openid
↓
查询数据库:该 openid 是否存在?
├─ 存在(老用户)→ 返回 is_new_user: false, token
└─ 不存在(新用户)→ 返回 is_new_user: true
↓
前端判断 !loginData.is_new_user
├─ true(老用户)→ 保存 token,刷新页面 ✅
└─ false(新用户)→ 不执行登录,保持未登录状态 ❌
↓
(新用户后续操作)
├─ 访问 meta.auth=true 的页面 → 路由守卫拦截 → 跳转登录页
└─ 再次触发 API 请求 → code=-1 → toLogin() → mnpLogin() → 仍然不登录
2.3 代码实现(按流程顺序)
步骤 1:获取登录凭证 code
javascript
// utils/login.js
export function getCode() {
return new Promise((resolve, reject) => {
uni.login({
success(res) {
resolve(res.code);
},
fail(res) {
reject(res);
},
});
});
}
步骤 2:定义静默登录 API 接口
javascript
// api/app.js
// 微信小程序静默登录
export const apiSilentLogin = (params) => request.post('login/silentLogin', params)
步骤 3:静默登录核心逻辑
javascript
// utils/login.js
export async function mnpLogin() {
// 1. 获取系统配置
const { coerce_mobile, mnp_auto_wechat_auth, toutiao_auto_auth } =
store.getters.appConfig;
// 2. 检查是否开启自动授权
//#ifdef MP-WEIXIN
if (!mnp_auto_wechat_auth) return;
//#endif
//#ifdef MP-TOUTIAO
if (!toutiao_auto_auth) return;
//#endif
// 3. 获取 code
const code = await getCode();
// 4. 调用静默登录接口
//#ifdef MP-WEIXIN
const loginData = await apiSilentLogin({
code,
});
//#endif
//#ifdef MP-TOUTIAO
const loginData = await apiToutiaoSilentLogin({
code,
});
//#endif
// 5. 获取当前页面信息
const { options, onLoad, onShow, route } = currentPage();
// 6. 如果需要强制绑定手机号且用户未绑定,则终止
if (coerce_mobile && !loginData.mobile) {
return;
}
// 7. 【关键判断】只有老用户才完成登录
if (loginData.token && !loginData.is_new_user) {
store.commit("login", loginData); // 保存 token 到 store
store.dispatch("getUser"); // 获取用户信息
store.dispatch("getCartNum"); // 获取购物车数量
onLoad && onLoad(options); // 刷新当前页面
onShow && onShow();
}
// 新用户不会执行以上代码,保持未登录状态
}
2.4 后端返回数据结构
javascript
// 老用户返回
{
token: "eyJ0eXAiOiJKV1Qi...",
is_new_user: false,
mobile: "13800138000", // 可能为空
// 其他用户信息...
}
// 新用户返回
{
token: "eyJ0eXAiOiJKV1Qi...",
is_new_user: true,
mobile: null,
// 其他用户信息...
}
三、授权登录流程
3.1 触发时机
用户主动点击登录页面的"用户一键登录"按钮。
3.2 完整流程图
用户点击"用户一键登录"
↓
检查是否同意服务协议和隐私协议
├─ 未同意 → 弹出协议提示框
└─ 已同意 → 继续
↓
调用 uni.getUserProfile() 获取用户信息
↓
显示 loading "登录中..."
↓
调用 uni.login() 获取 code
↓
请求后端接口 login/authLogin
↓
后端处理注册/登录逻辑
↓
返回 is_new_user 标识
├─ true(新用户)→ 弹出 mplogin-popup 完善信息
└─ false(老用户)→ 直接完成登录
↓
弹出完善信息弹窗(仅新用户)
↓
用户填写信息并提交
↓
调用 login/updateUser 更新用户信息
↓
调用 loginHandle() 完成登录
↓
跳转回原页面或首页
3.3 代码实现(按流程顺序)
步骤 1:定义授权登录 API 接口
javascript
// api/app.js
// 微信小程序授权登录
export const apiAuthLogin = (params) => request.post('login/authLogin', params)
// 更新小程序用户信息
export const apiUpdateUser = (params, token) => {
return request.post('login/updateUser', params, { headers: { token } })
}
步骤 2:登录页面 - 一键登录按钮
vue
<!-- pages/login/login.vue -->
<template>
<!-- #ifdef MP-WEIXIN -->
<button
class="login-btn white login-btn-user"
v-if="isMnpWxAuth"
@tap="mnpLogin"
>
用户一键登录
</button>
<!-- #endif -->
</template>
步骤 3:授权登录核心逻辑
javascript
// pages/login/login.vue
methods: {
// 小程序微信登录
async mnpLogin() {
// 1. 检查是否同意协议
if (!this.isAgree) {
this.showModel = true;
return;
}
// 2. 获取用户信息(头像、昵称等)
const {
userInfo: { avatarUrl, nickName, gender }
} = await getUserProfile();
// 3. 显示 loading
uni.showLoading({
title: '登录中...',
mask: true
});
// 4. 获取 code
const wxCode = await getCode();
// 5. 调用授权登录接口
const data = await apiAuthLogin({
code: wxCode,
nickname: nickName,
headimgurl: avatarUrl
});
// 6. 判断是否为新用户
if (data.is_new_user) {
// 新用户:显示完善信息弹窗
uni.hideLoading();
this.showLoginPop = true;
this.loginData = data;
} else {
// 老用户:直接完成登录
this.loginHandle(data);
}
},
}
步骤 4:登录结果处理
javascript
// pages/login/login.vue
methods: {
// 登录结果处理
async loginHandle(data) {
// 1. 保存 token 到 store
this.login(data);
// 2. 获取用户信息
this.getUser();
// 3. 更新购物车
this.$store.dispatch('getCartNum');
// 4. 隐藏 loading
uni.hideLoading();
// 5. 如果需要绑定手机号
if (this.isBindMobile && !data.mobile) {
return this.$Router.replace('/pages/bind_mobile/bind_mobile');
}
// 6. 返回上一页或首页
this.goBack();
},
goBack() {
if (getCurrentPages().length > 1) {
this.$Router.back(1, {
success: () => {
const { onLoad, options } = currentPage();
onLoad && onLoad(options); // 刷新上一页
}
});
} else if (Cache.get(BACK_URL)) {
this.$Router.replace(Cache.get(BACK_URL));
} else {
this.$Router.replaceAll('/pages/index/index');
}
Cache.remove(BACK_URL);
}
}
步骤 5:完善用户信息弹窗(仅新用户)
vue
<!-- pages/login/login.vue -->
<!-- #ifdef MP-WEIXIN -->
<mplogin-popup
v-model="showLoginPop"
:logo="appConfig.logo"
:title="appConfig.shop_name"
:login-data="loginData"
@close="closePopup"
@update="handleSubmitInfo"
/>
<!-- #endif -->
javascript
// pages/login/login.vue
methods: {
// 提交完善的信息
async handleSubmitInfo(e) {
const loginData = this.loginData;
const res = await apiUpdateUser(e, loginData.token);
this.loginHandle(loginData);
},
closePopup() {
this.showLoginPop = false;
}
}
四、两种登录方式的配合机制
4.1 配合流程图
用户打开小程序,触发任意 API 请求
↓
后端返回 code = -1(未登录/token无效)
↓
响应拦截器 → toLogin() → mnpLogin()(静默登录)
↓
后端判断新老用户
├─ 老用户 → 自动登录成功 ✅
└─ 新用户 → 不执行登录,保持未登录状态 ❌
↓
(如果是新用户)后续两种触发方式:
├─ 方式1:再次触发 API 请求 → code=-1 → toLogin() → mnpLogin() → 仍不登录
└─ 方式2:访问 meta.auth=true 的页面 → 路由守卫拦截 → 跳转登录页
↓
跳转到登录页 /pages/login/login
↓
用户点击"用户一键登录"
↓
触发授权登录 apiAuthLogin()
↓
获取用户信息,完成注册
↓
(如果是新用户)弹出完善信息弹窗
↓
用户提交信息,更新用户资料
↓
登录成功,跳转回原页面
4.2 关键配合点
1. 静默登录的拦截机制
javascript
// 静默登录中,只有老用户才会执行登录逻辑
if (loginData.token && !loginData.is_new_user) {
store.commit("login", loginData); // 仅老用户执行
// ...
}
目的:防止新用户自动登录,确保新用户必须经过授权流程。
2. 授权登录的新用户识别
javascript
// 授权登录中,根据 is_new_user 决定后续流程
if (data.is_new_user) {
// 新用户:显示完善信息弹窗
this.showLoginPop = true;
this.loginData = data;
} else {
// 老用户:直接完成登录
this.loginHandle(data);
}
目的:新用户需要完善信息,老用户直接登录。
4.3 为什么需要两种登录方式?
| 问题 | 仅用静默登录 | 仅用授权登录 | 双模式配合 |
|---|---|---|---|
| 老用户体验 | ✅ 无感知 | ❌ 每次都要点击 | ✅ 无感知 |
| 新用户注册 | ❌ 无法获取头像昵称 | ✅ 可获取 | ✅ 可获取 |
| 隐私合规 | ❌ 新用户未授权就注册 | ✅ 用户明确授权 | ✅ 用户明确授权 |
| 转化率 | ❌ 新用户无法完善信息 | ❌ 老用户操作繁琐 | ✅ 最优体验 |
五、后端接口说明
5.1 接口清单
| 接口 | 方法 | 用途 |
|---|---|---|
login/silentLogin |
POST | 静默登录(新老用户判断) |
login/authLogin |
POST | 授权登录(新用户注册) |
login/updateUser |
POST | 更新用户信息(头像、昵称等) |
5.2 静默登录接口 (login/silentLogin)
请求参数:
javascript
{
code: "0a1s7M1w34ZqW63oy20w35APn20s7M10" // uni.login() 获取的临时凭证
}
处理逻辑:
- 使用 code 向微信服务器换取 openid
- 查询数据库中该 openid 是否存在
- 存在 → 返回
is_new_user: false - 不存在 → 返回
is_new_user: true
返回数据:
javascript
{
code: 1,
msg: "success",
data: {
token: "eyJ0eXAiOiJKV1Qi...",
is_new_user: false, // 关键标识
mobile: "13800138000",
// 其他用户信息...
}
}
5.3 授权登录接口 (login/authLogin)
请求参数:
javascript
{
code: "0a1s7M1w34ZqW63oy20w35APn20s7M10",
nickname: "张三",
headimgurl: "https://..."
}
处理逻辑:
- 使用 code 向微信服务器换取 openid
- 查询数据库中该 openid 是否存在
- 存在 → 老用户直接登录
- 不存在 → 创建新用户,完成注册
返回数据:
javascript
{
code: 1,
msg: "success",
data: {
token: "eyJ0eXAiOiJKV1Qi...",
is_new_user: true, // 关键标识
mobile: null,
// 其他用户信息...
}
}
六、系统配置说明
6.1 配置项说明
在系统后台或配置接口中,有以下关键配置:
javascript
{
mnp_auto_wechat_auth: true, // 是否开启小程序微信自动授权(静默登录)
mnp_wechat_auth: true, // 是否显示小程序微信登录按钮
coerce_mobile: false, // 是否强制绑定手机号
login_way: [1, 2], // 登录方式:1-密码登录,2-验证码登录
register_way: [1], // 注册方式
}
6.2 配置对登录流程的影响
| 配置项 | true | false |
|---|---|---|
mnp_auto_wechat_auth |
小程序启动时自动静默登录 | 不自动静默登录 |
mnp_wechat_auth |
显示"用户一键登录"按钮 | 隐藏该按钮 |
coerce_mobile |
未绑定手机号则终止流程 | 允许继续 |
七、快速接入指南
7.1 在新项目中实现双登录模式
步骤 1:创建工具文件
utils/
└─ login.js // 登录相关工具函数
步骤 2:创建 API 文件
api/
└─ app.js // 登录相关接口
步骤 3:创建登录页面
pages/
└─ login/
└─ login.vue // 登录页面
步骤 4:配置请求拦截器(关键步骤)
静默登录的触发入口在请求拦截器中,当 API 返回 code = -1 时自动触发:
javascript
// utils/request.js
import { APICodeEnum } from './enum'
import { toLogin } from './login'
const events = {
success({ data }) { return Promise.resolve(data) },
fail({ msg }) { return Promise.reject(msg) },
redirect({ msg }) {
// #ifdef MP-WEIXIN
if (store.getters.appConfig.mnp_status) {
toLogin() // 触发静默登录
}
// #endif
store.commit('logout')
return Promise.reject(msg)
},
closeShop({ msg }) { /* ... */ },
tips({ code, msg }) { return { code, msg } }
}
// 响应拦截器
service.interceptors.response.use((response) => {
const { msg, code, data, show } = response.data
return events[APICodeEnum[code]](response.data)
})
步骤 5:配置路由守卫(辅助步骤)
当用户访问需要登录的页面时,路由守卫会拦截并跳转到登录页:
javascript
// router.js
router.beforeEach((to, from, next) => {
if (to.meta.auth && !store.getters.token) {
next('/pages/login/login')
return
}
next()
})
步骤 6:配置页面路由
json
// pages.json
{
"pages": [
{
"path": "pages/login/login",
"style": {
"navigationBarTitleText": "登录"
}
}
]
}
7.2 必要依赖
javascript
// store/index.js (Vuex)
export default new Vuex.Store({
state: {
token: '',
userInfo: {}
},
mutations: {
login(state, data) {
state.token = data.token;
// 保存 token 到本地存储
}
},
actions: {
getUser({ commit }) {
// 获取用户信息
},
getCartNum({ commit }) {
// 获取购物车数量
}
},
getters: {
appConfig: state => state.app.config
}
})
八、常见问题与排查
8.1 静默登录失败
现象:老用户也无法自动登录
排查步骤:
- 检查后端 AppID 和 AppSecret 配置是否正确
- 检查
mnp_auto_wechat_auth配置是否为true - 检查后端返回的
is_new_user值是否正确
8.2 新用户无法注册
现象:点击授权登录后没有反应
排查步骤:
- 检查是否同意服务协议和隐私协议
- 检查
mnp_wechat_auth配置是否为true - 检查后端是否正确处理新用户注册逻辑
8.3 静默登录报错 "appid missing"
原因:后端未正确配置微信小程序的 AppID
解决:在服务器后台配置正确的 AppID 和 AppSecret
九、总结
9.1 核心要点
- 静默登录:老用户无感登录,新用户不执行登录
- 授权登录:新用户注册,获取头像昵称
- 配合机制 :通过
is_new_user字段区分新老用户 - 用户体验:老用户打开即用,新用户一次授权
9.2 关键代码片段
javascript
// 静默登录核心判断
if (loginData.token && !loginData.is_new_user) {
// 仅老用户执行登录
}
// 授权登录核心判断
if (data.is_new_user) {
// 新用户:显示完善信息弹窗
} else {
// 老用户:直接完成登录
}
9.3 迁移到其他项目
- 复制
utils/login.js到目标项目 - 复制
utils/enum.js到目标项目(APICodeEnum 枚举定义) - 复制
api/app.js中的登录接口定义 - 复制
pages/login/login.vue登录页面 - 在
utils/request.js响应拦截器中配置code=-1时调用toLogin() - 在
router.js中配置路由守卫,拦截需要登录的页面 - 确保后端接口返回正确的
is_new_user字段 - 配置系统参数
mnp_auto_wechat_auth和mnp_wechat_auth
附录:完整文件清单
| 文件 | 用途 |
|---|---|
utils/login.js |
登录工具函数(getCode、mnpLogin、toLogin 等) |
utils/enum.js |
接口返回码枚举(APICodeEnum,定义 code=-1 为 redirect) |
utils/request.js |
请求拦截器(响应拦截中 code=-1 触发 toLogin) |
api/app.js |
登录相关 API 接口定义 |
pages/login/login.vue |
登录页面(授权登录入口) |
router.js |
路由守卫(meta.auth 页面拦截跳转登录页) |
store/index.js |
Vuex 状态管理(login、getUser 等) |