Skip to content

uni-app 全局容器实战系列(五):登录鉴权模块设计

一、整体架构

1.1 模块定位

登录鉴权模块负责四大核心职责:

职责说明
用户身份认证登录、注册、登出
页面访问控制路由拦截,防止未授权访问
请求身份标识API Token 自动携带
用户状态管理统一管理用户信息

1.2 三层拦截体系

层级触发时机作用Token 无效后果
App.vue 初始化应用启动/恢复验证登录态,决定能否进入当前页面跳转登录页
permission.js 路由拦截每次路由跳转防止未授权访问受限页面弹框提示
request.js 请求拦截每次 API 调用携带凭证、处理 401弹框提示

二、存储层 (utils/auth.js)

Token 持久化是整个鉴权体系的基础:

javascript
// utils/auth.js
const TOKEN_KEY = 'App-Token'
const CHANNEL_ID = 'CHANNEL_ID'

export function getToken() {
    return uni.getStorageSync(TOKEN_KEY)
}

export function setToken(token) {
    uni.setStorageSync(TOKEN_KEY, token)
}

export function removeToken() {
    uni.removeStorageSync(TOKEN_KEY)
}

export function getChannelId() {
    return uni.getStorageSync(CHANNEL_ID)
}

export function setChannelId(channelId) {
    uni.setStorageSync(CHANNEL_ID, channelId)
}

注意uni.getStorageSync 直接返回存储的值,无需额外 .data 属性。


三、状态管理层 (store/modules/user.js)

使用 Pinia 统一管理用户状态:

3.1 State 结构

字段类型说明
tokenstring登录凭证
channelIdstring渠道标识
userInfoobject用户信息
abilitiesarray能力列表
gradesarray年级列表
subjectsarray科目列表

3.2 Actions

方法说明平台限制
login(phone, password)密码登录-
register(phone, password, smsCode)注册-
logout()退出登录-
getInfo()获取用户信息-
bindPhone(newPhone, smsCode)绑定手机号-
updateUserInfo(newUserInfo)更新用户信息-
wxH5Login()微信授权登录仅 H5

3.3 核心实现

javascript
// store/modules/user.js
import { defineStore } from 'pinia'
import { getToken, setToken, removeToken } from '@/utils/auth'
import { login as loginApi, register as registerApi, logout as logoutApi, member as memberApi } from '@/api/login'

export const useUserStore = defineStore('user', {
    state: () => ({
        token: getToken() || '',
        channelId: '',
        userInfo: null,
        abilities: [],
        grades: [],
        subjects: [],
    }),

    actions: {
        // 登录
        async login(phone, password) {
            const res = await loginApi({ phone, password })
            this.token = res.token
            setToken(res.token)
            await this.getInfo()
            return res
        },

        // 获取用户信息
        async getInfo() {
            try {
                const res = await memberApi()
                this.userInfo = res.data
                this.abilities = res.data.abilities || []
                this.grades = res.data.grades || []
                this.subjects = res.data.subjects || []
            } catch (e) {
                // Token 失效时保留 token,弹框提示让用户重新登录
                console.error('获取用户信息失败', e)
                throw e
            }
        },

        // 登出
        async logout() {
            try {
                await logoutApi(this.token)
            } catch (e) {
                // 忽略接口调用失败
            } finally {
                this.token = ''
                this.userInfo = null
                removeToken()
            }
        },
    },
})

3.4 微信登录(仅 H5 支持)

平台限制

微信授权登录仅支持 H5 平台使用,其他平台(App、小程序)需使用各自的登录方案。

H5 微信登录流程

前端实现

javascript
// #ifdef H5
import { getWechatLoginUrl, getQueryString } from '@/utils/wechat'

actions: {
    // 微信 H5 登录
    async wxH5Login() {
        // 检查是否在微信浏览器中
        const isWechatBrowser = navigator.userAgent.toLowerCase().includes('micromessenger')
        if (!isWechatBrowser) {
            uni.showToast({ title: '请在微信浏览器中打开', icon: 'none' })
            return
        }

        // 获取 URL 中的 code 参数(回调页携带的)
        const code = getQueryString('code')
        if (!code) {
            // 触发微信授权,跳转到授权页面
            const redirectUri = encodeURIComponent(window.location.href)
            const appId = import.meta.env.VITE_WECHAT_APPID
            const wxLoginUrl = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${appId}&redirect_uri=${redirectUri}&response_type=code&scope=snsapi_base#wechat_redirect`
            window.location.href = wxLoginUrl
            return
        }

        // 有 code,调用后端接口完成登录
        const res = await wxLoginApi({ code })
        this.token = res.token
        setToken(res.token)
        await this.getInfo()
        return res
    },
}
// #endif

后端交互接口

接口参数说明
wxLoginApi(code)微信授权码H5 专用,后端通过 code 换取 openid 并返回 token
getWechatLoginUrl()redirectUri获取微信授权跳转 URL
getQueryString(name)参数名从 URL 中解析查询参数

微信登录路由守卫

javascript
// permission.js
// 微信授权回调页需要加入白名单,否则会触发未登录提示
const whiteList = [
    loginPage,
    '/pages/register/index',
    '/pages/protocol/index',
    '/pages/privacy/index',
    '/pages/login/wx-callback',  // 微信授权回调页
]

四、路由拦截层 (permission.js)

4.1 拦截时机

跳转方式说明是否拦截
navigateTo普通页面跳转
redirectTo关闭当前页跳转
reLaunch关闭所有页面跳转
switchTabTabBar 页面切换
navigateBack页面返回

4.2 白名单配置

javascript
// permission.js
const loginPage = '/pages/login/index'

const whiteList = [
    loginPage,                    // 登录页
    '/pages/register/index',      // 注册页
    '/pages/protocol/index',      // 用户协议
    '/pages/privacy/index',       // 隐私政策
]

function checkWhite(url) {
    return whiteList.some(path => url.includes(path))
}

4.3 拦截逻辑

javascript
// 路由拦截器
const list = ['navigateTo', 'redirectTo', 'reLaunch', 'switchTab']
list.forEach(item => {
    uni.addInterceptor(item, {
        invoke(to) {
            // 已登录用户访问登录页 → 跳转首页
            if (getToken() && to.url === loginPage) {
                uni.reLaunch({ url: '/' })
                return false
            }

            // 已登录 → 允许访问
            if (getToken()) {
                return true
            }

            // 未登录 → 检查白名单
            if (checkWhite(to.url)) {
                return true
            }

            // 非白名单 → 弹框提示
            useAppLayout().showUnloginNotify()
            return false
        },
    })
})

五、请求拦截层 (utils/request.js)

5.1 Token 携带规则

javascript
// 普通接口 - 自动携带 Token
const config1 = {
    url: '/api/user/info',
    // isToken 默认为 true
}

// 第三方接口 - 不携带 Token
const config2 = {
    url: '/api/third/pay',
    headers: {
        isToken: false,
    },
}

5.2 请求拦截实现

javascript
// utils/request.js
export function request(options) {
    return new Promise((resolve, reject) => {
        const token = getToken()

        // 自动携带 Token(除非配置 isToken: false)
        if (options.headers?.isToken !== false && token) {
            options.header = {
                ...options.header,
                Authorization: `Bearer ${token}`,
            }
        }

        uni.request({
            ...options,
            success: (res) => {
                const { statusCode, data } = res

                if (statusCode === 200) {
                    resolve(data)
                } else if (statusCode === 401) {
                    // Token 失效 → 弹框提示
                    useAppLayout().showUnloginNotify()
                    reject(new Error('未授权'))
                } else {
                    uni.showToast({
                        title: data.message || '请求失败',
                        icon: 'none',
                    })
                    reject(new Error(data.message))
                }
            },
            fail: (err) => {
                uni.showToast({
                    title: '网络异常',
                    icon: 'none',
                })
                reject(err)
            },
        })
    })
}

5.3 响应状态码处理

状态码处理方式
200返回 res.data
401弹框提示,点击后跳转登录页
500Toast 提示错误信息
其他Toast 提示错误信息

六、登录与登出流程

6.1 登录流程

6.2 登出流程

6.3 关键设计

设计点实现方式
登录态验证getInfo() 静默验证,失败弹框不跳转
失败保活Token 失效时不清除,保留登录入口
登录页保护已登录访问登录页 → reLaunch 首页
401 处理弹框提示而非强制跳转,用户体验更平滑

七、UnloginNotify 组件

未登录提示弹框,由全局容器通过 useAppLayout().showUnloginNotify() 调用:

vue
<!-- components/AppLayout/UnloginNotify.vue -->
<template>
    <uni-popup ref="popup" type="center" :mask-click="false">
        <view class="unlogin-popup">
            <text class="title">提示</text>
            <text class="content">您还未登录,请先登录</text>
            <view class="actions">
                <button class="cancel" @click="onCancel">取消</button>
                <button class="confirm" @click="onConfirm">去登录</button>
            </view>
        </view>
    </uni-popup>
</template>

<script setup>
import { ref } from 'vue'

const popup = ref(null)

function show() {
    popup.value.open()
}

function hide() {
    popup.value.close()
}

function onCancel() {
    hide()
}

function onConfirm() {
    hide()
    uni.reLaunch({ url: '/pages/login/index' })
}

defineExpose({ show, hide })
</script>

<style scoped>
.unlogin-popup {
    width: 560rpx;
    padding: 48rpx;
    background: #fff;
    border-radius: 24rpx;
}
.title {
    display: block;
    font-size: 36rpx;
    font-weight: 600;
    text-align: center;
    margin-bottom: 24rpx;
}
.content {
    display: block;
    font-size: 28rpx;
    color: #666;
    text-align: center;
    margin-bottom: 48rpx;
}
.actions {
    display: flex;
    gap: 24rpx;
}
.cancel,
.confirm {
    flex: 1;
    height: 80rpx;
    line-height: 80rpx;
    border-radius: 40rpx;
    font-size: 28rpx;
}
.cancel {
    background: #f5f5f5;
    color: #666;
}
.confirm {
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    color: #fff;
}
</style>

八、错误处理策略

场景处理方式
登录失败Toast 提示错误信息
Token 过期 (401)弹框提示,点击后跳转登录页
请求超时Toast "系统接口请求超时"
网络错误Toast "后端接口连接异常"
getInfo 失败保留 Token,弹框提示
登出失败强制清除本地状态,确保一致
非微信浏览器调用微信登录提示"请在微信浏览器中打开"
微信授权失败/取消留在登录页,不跳转

九、总结

本篇文章阐述了登录鉴权模块的完整设计:

核心要点说明
三层拦截App 初始化 / 路由跳转 / 请求调用
统一状态管理Pinia store 集中管理 token 和 userInfo
白名单机制配置式声明,无需登录的页面统一管理
平滑降级401 不强制跳转,通过弹框让用户选择
持久化存储Token 通过 Storage 持久化,切换页面不丢失
多端登录密码登录(通用)/ 微信 H5 授权登录(仅 H5)

跨平台说明

  • 密码登录:全平台支持
  • 微信授权登录:仅 H5 平台支持,需在微信内置浏览器中打开
  • 其他平台(App、小程序)需使用各自平台的登录方案,如 App 端使用 uni.login({ provider: 'weixin' })

系列总览

章节主题核心问题
第一章全局容器的实现如何让每个页面自动包裹布局组件?
第二章Vite 虚拟模块如何在运行时读取 pages.json 配置?
第三章组件设计如何设计可配置的 NavBar 和 TabBar?
第四章动态调用设计页面如何与全局容器进行通信?
第五章登录鉴权模块如何设计完整的登录鉴权体系?