uni-starter V2.1.4(三)登录逻辑

现在我们来分析这个uni-starter的核心逻辑,这个核心逻辑当然就是登陆了。这个例子项目的主要功能就是演示如何登录!

一、uniIdRouter自动路由

uniIdRouter 是一个运行在前端的、对前端页面访问权限路由进行控制的方案。大多数应用,都会指定某些页面需要登录才能访问。以往开发者需要写不少代码。现在,只需在项目的 pages.json内配置登录页路径、需要登录才能访问的页面等信息,uni-app框架的路由跳转,会自动在需要登录且客户端登录状态过期或未登录时跳转到登录页面。结合以下代码及注释了解如何使用 uniIdRouter

...
	"subPackages": [
		{
			"root": "uni_modules/uni-feedback",
			"pages": [{
				"path": "pages/opendb-feedback/opendb-feedback",
				"style": {
					"navigationBarTitleText": "意见反馈",
					"enablePullDownRefresh": false
				}
			}]
		},
		{
			"root": "uni_modules/uni-id-pages/pages",
			"pages": [
				{"path": "userinfo/userinfo","style": {"navigationBarTitleText": "个人资料"}},
				{"path": "login/login-withoutpwd"},
				{"path": "login/login-withpwd"},
				{"path": "userinfo/deactivate/deactivate","style": {"navigationBarTitleText": "注销账号"}},
				{"path": "userinfo/bind-mobile/bind-mobile","style": {"navigationBarTitleText": "绑定手机号码"}},
...
	"uniIdRouter": {
		"loginPage": "uni_modules/uni-id-pages/pages/login/login-withoutpwd",
		"needLogin": [
			"/uni_modules/uni-id-pages/pages/userinfo/userinfo"
		],
		"resToLogin": true
	}

1、uniIdRouter的数据结构:

subPackages与pages是相似的,关于uniIdRouter的 官方解释 与 LoginDemo的代码是有区别的,不过大同小异。

  • loginPage 登录页面路径
  • needLogin 需要登录才可访问的页面列表,可以使用正则语法
  • resToLogin 自动解析云对象及clientDB的错误码,如果是客户端token不正确或token过期则自动跳转配置的登录页面,配置为false则关闭此行为,默认true

本例中只有一个页面需要登录,在实战中我们可能有许多页面

2、uniCloud客户端api

uniCloud.onNeedLogin()和uniCloud.offNeedLogin(),开发者在监听onNeedLogin事件后,框架就不再自动跳转到登录页面,而是由开发者在onNeedLogin事件内自行处理。

在页面中无法使用 uniCloud.onNeedLogin ,只能是App.vue之中使用,我们在 /common/appInit.js 之中,加入如下代码:

...
const db = uniCloud.database()
export default async function() {
	const debug = uniStarterConfig.debug;
	...
	//需要登录的页面被使用时,触发
	uniCloud.onNeedLogin(function(event) {
		// event格式见下方说明
		console.log(event);
	})

运行之后,打印出:

{
    "errCode": "uni-id-check-token-failed",
    "errMsg": "token无效,跳转登录页面",
    "uniIdRedirectUrl": "/pages/grid/grid"
}

如果在代码之中,使用了 uniCloud.onNeedLogin,则系统优先使用其中的逻辑,时行跳转。

  • errCode: number | string, 出错码
  • errMsg:string, 出错信息
  • uniIdRedirectUrl: string // 触发onNeedLogin页面前的页面地址(包含路径和参数的完整页面地址)

这个不稳定???!!!

一个半成员,可以用,不会自动跳转,会进入onNeedLogin

	//需要登录的页面被使用时,触发
	uniCloud.onNeedLogin(function(event) {
		// event格式见下方说明
		console.log(event);
		if (event.errCode=='uni-id-check-token-failed'){
			uni.navigateTo({
				url:"/uni_modules/uni-id-pages/pages/login/login-withoutpwd",
			})
		}else{
			//这里根本进不来,从不触发!
			uni.navigateTo({
				url:event.uniIdRedirectUrl
			})
		}
	})

二、login-withoutpwd

无口令的登录 /uni_modules/uni-id-pages/pages/login-withoutpwd

模板

<!-- 免密登录页 -->
<template>
	<view class="uni-content">
		<view class="login-logo">
			<image :src="logo"></image>
		</view>
		<!-- 顶部文字 -->
		<text class="title">请选择登录方式</text>
		<!-- 快捷登录框 当url带参数时有效 -->
		<template v-if="['apple','weixin', 'weixinMobile'].includes(type)">
			<text class="tip">将根据第三方账号服务平台的授权范围获取你的信息</text>
			<view class="quickLogin">
				<image v-if="type !== 'weixinMobile'" @click="quickLogin" :src="imgSrc" mode="widthFix"
					class="quickLoginBtn"></image>
				<button v-else type="primary" open-type="getPhoneNumber" @getphonenumber="quickLogin"
					class="uni-btn">微信授权手机号登录</button>
				<uni-id-pages-agreements scope="register" ref="agreements"></uni-id-pages-agreements>
			</view>
		</template>
		<template v-else>
			<text class="tip">未注册的账号验证通过后将自动注册</text>
			<view class="phone-box">
				<view @click="chooseArea" class="area">+86</view>
				<uni-easyinput :focus="focusPhone" @blur="focusPhone = false" class="input-box" type="number"
					:inputBorder="false" v-model="phone" maxlength="11" placeholder="请输入手机号" />
			</view>
			<uni-id-pages-agreements scope="register" ref="agreements"></uni-id-pages-agreements>
			<button class="uni-btn" type="primary" @click="toSmsPage">获取验证码</button>
		</template>
		<!-- 固定定位的快捷登录按钮 -->
		<uni-id-pages-fab-login ref="uniFabLogin"></uni-id-pages-fab-login>
	</view>
</template>

脚本

<script>
	let currentWebview; //当前窗口对象
	import config from '@/uni_modules/uni-id-pages/config.js'
	import mixin from '@/uni_modules/uni-id-pages/common/login-page.mixin.js';
	export default {
		mixins: [mixin],
		data() {
			return {
				type: "", //快捷登录方式
				phone: "", //手机号码
				focusPhone: false,
				logo: "/static/logo.png"
			}
		},
		computed: {
			async loginTypes() { //读取配置的登录优先级
				return config.loginTypes
			},
			isPhone() { //手机号码校验正则
				return /^1\d{10}$/.test(this.phone);
			},
			imgSrc() { //大快捷登录按钮图
				return this.type == 'weixin' ? '/uni_modules/uni-id-pages/static/login/weixin.png' :
					'/uni_modules/uni-id-pages/static/app-plus/apple.png'
			}
		},
		async onLoad(e) {
			//获取通过url传递的参数type设置当前登录方式,如果没传递直接默认以配置的登录
			let type = e.type || config.loginTypes[0]
			this.type = type

			// console.log("this.type: -----------",this.type);
			if (type != 'univerify') {
				this.focusPhone = true
			}
			this.$nextTick(() => {
				//关闭重复显示的登录快捷方式
				if (['weixin', 'apple'].includes(type)) {
					this.$refs.uniFabLogin.servicesList = this.$refs.uniFabLogin.servicesList.filter(item =>
						item.id != type)
				}
			})
			uni.$on('uni-id-pages-setLoginType', type => {
				this.type = type
			})
		},
		onShow() {
			// #ifdef H5
			document.onkeydown = event => {
				var e = event || window.event;
				if (e && e.keyCode == 13) { //回车键的键值为13
					this.toSmsPage()
				}
			};
			// #endif
		},
		onUnload() {
			uni.$off('uni-id-pages-setLoginType')
		},
		onReady() {
			// 是否优先启动一键登录。即:页面一加载就启动一键登录
			//#ifdef APP-PLUS
			if (this.type == "univerify") {
				const pages = getCurrentPages();
				currentWebview = pages[pages.length - 1].$getAppWebview();
				currentWebview.setStyle({
					"top": "2000px" // 隐藏当前页面窗体
				})
				this.type == this.loginTypes[1]
				// console.log('开始一键登录');
				this.$refs.uniFabLogin.login_before('univerify')
			}
			//#endif
		},
		methods: {
			showCurrentWebview(){
				// 恢复当前页面窗体的显示 一键登录,默认不显示当前窗口
				currentWebview.setStyle({
					"top": 0
				})
			},
			quickLogin(e) {
				let options = {}

				if (e.detail?.code) {
					options.phoneNumberCode = e.detail.code
				}

				if (this.type === 'weixinMobile' && !e.detail?.code) return

				this.$refs.uniFabLogin.login_before(this.type, true, options)
			},
			toSmsPage() {
				if (!this.isPhone) {
					this.focusPhone = true
					return uni.showToast({
						title: "手机号码格式不正确",
						icon: 'none',
						duration: 3000
					});
				}
				if (this.needAgreements && !this.agree) {
					return this.$refs.agreements.popup(this.toSmsPage)
				}
				// 发送验证吗
				uni.navigateTo({
					url: '/uni_modules/uni-id-pages/pages/login/login-smscode?phoneNumber=' + this.phone
				});
			},
			//去密码登录页
			toPwdLogin() {
				uni.navigateTo({
					url: '../login/password'
				})
			},
			chooseArea() {
				uni.showToast({
					title: '暂不支持其他国家',
					icon: 'none',
					duration: 3000
				});
			},
		}
	}
</script>

1、配置文件config.js

位置:/uni_modules/uni-id-pages

export default {
  // 调试模式
  debug: false,
  /*
		登录类型 未列举到的或运行环境不支持的,将被自动隐藏。
		如果需要在不同平台有不同的配置,直接用条件编译即可
	*/
  isAdmin: false, // 区分管理端与用户端
  loginTypes: [
    // "qq",
    // "xiaomi",
    // "sinaweibo",
    // "taobao",
    // "facebook",
    // "google",
    // "alipay",
    // "douyin",

    // #ifdef APP
    'univerify',
    // #endif
    'weixin',
    'username',
    // #ifdef APP
    'apple',
    // #endif
    'smsCode'
  ],
  // 政策协议
  agreements: {
    serviceUrl: 'https://xxx', // 用户服务协议链接
    privacyUrl: 'https://xxx', // 隐私政策条款链接
    // 哪些场景下显示,1.注册(包括登录并注册,如:微信登录、苹果登录、短信验证码登录)、2.登录(如:用户名密码登录)
    scope: [
      'register', 'login', 'realNameVerify'
    ]
  },
  // 提供各类服务接入(如微信登录服务)的应用id
  appid: {
    weixin: {
      // 微信公众号的appid,来源:登录微信公众号(https://mp.weixin.qq.com)-> 设置与开发 -> 基本配置 -> 公众号开发信息 -> AppID
      h5: 'xxxxxx',
      // 微信开放平台的appid,来源:登录微信开放平台(https://open.weixin.qq.com) -> 管理中心 -> 网站应用 -> 选择对应的应用名称,点击查看 -> AppID
      web: 'xxxxxx'
    }
  },
  /**
	 * 密码强度
	 * super(超强:密码必须包含大小写字母、数字和特殊符号,长度范围:8-16位之间)
	 * strong(强: 密密码必须包含字母、数字和特殊符号,长度范围:8-16位之间)
	 * medium (中:密码必须为字母、数字和特殊符号任意两种的组合,长度范围:8-16位之间)
	 * weak(弱:密码必须包含字母和数字,长度范围:6-16位之间)
	 * 为空或false则不验证密码强度
	 */
  passwordStrength: false,
  /**
	 * 登录后允许用户设置密码(只针对未设置密码得用户)
	 * 开启此功能将 setPasswordAfterLogin 设置为 true 即可
	 * "setPasswordAfterLogin": false
	 *
	 * 如果允许用户跳过设置密码 将 allowSkip 设置为 true
	 * "setPasswordAfterLogin": {
	 *   "allowSkip": true
	 * }
	 * */
  setPasswordAfterLogin: false
}

注意:页面一加载就启动一键登录

2、一键登录

<script>
	let currentWebview; //当前窗口对象
	...
		onReady() {
			// 是否优先启动一键登录。即:页面一加载就启动一键登录
			//#ifdef APP-PLUS
			if (this.type == "univerify") {
				const pages = getCurrentPages();
				currentWebview = pages[pages.length - 1].$getAppWebview();
				currentWebview.setStyle({
					"top": "2000px" // 隐藏当前页面窗体
				})
				this.type == this.loginTypes[1]
				// console.log('开始一键登录');
				this.$refs.uniFabLogin.login_before('univerify')
			}
			//#endif
		},
	...

部份工作 this.$refs.uniFabLogin.login_before(‘univerify’) 中进行。

3、uniFabLogin.login_before

位置:/uni_modules/uni-id-pages/components/uni-id-pages-fab-login/uni-id-pages-fab-login.vue

<template>
	<view>
		<view class="fab-login-box">
			<view class="item" v-for="(item,index) in servicesList" :key="index"
				@click="item.path?toPage(item.path):login_before(item.id,false)">
				<image class="logo" :src="item.logo" mode="scaleToFill"></image>
				<text class="login-title">{{item.text}}</text>
			</view>
		</view>
	</view>
</template>
<script>
	import config from '@/uni_modules/uni-id-pages/config.js'
	//前一个窗口的页面地址。控制点击切换快捷登录方式是创建还是返回
	import {store,mutations} from '@/uni_modules/uni-id-pages/common/store.js'
	let allServicesList = []
	export default {
		computed: {
			agreements() {
				if (!config.agreements) {
					return []
				}
				let {
					serviceUrl,
					privacyUrl
				} = config.agreements
				return [{
						url: serviceUrl,
						title: "用户服务协议"
					},
					{
						url: privacyUrl,
						title: "隐私政策条款"
					}
				]
			},
			agree: {
				get() {
					return this.getParentComponent().agree
				},
				set(agree) {
					return this.getParentComponent().agree = agree
				}
			}
		},
		data() {
			return {
				servicesList: [{
						"id": "username",
						"text": "账号登录",
						"logo": "/uni_modules/uni-id-pages/static/login/uni-fab-login/user.png",
						"path": "/uni_modules/uni-id-pages/pages/login/login-withpwd"
					},
					{
						"id": "smsCode",
						"text": "短信验证码",
						"logo": "/uni_modules/uni-id-pages/static/login/uni-fab-login/sms.png",
						"path": "/uni_modules/uni-id-pages/pages/login/login-withoutpwd?type=smsCode"
					},
					{
						"id": "weixin",
						"text": "微信登录",
						"logo": "/uni_modules/uni-id-pages/static/login/uni-fab-login/weixin.png",
					},
					// #ifndef MP-WEIXIN
					{
						"id": "apple",
						"text": "苹果登录",
						"logo": "/uni_modules/uni-id-pages/static/app-plus/uni-fab-login/apple.png",
					},
					{
						"id": "univerify",
						"text": "一键登录",
						"logo": "/uni_modules/uni-id-pages/static/app-plus/uni-fab-login/univerify.png",
					},
					{
						"id": "taobao",
						"text": "淘宝登录", //暂未提供该登录方式的接口示例
						"logo": "/uni_modules/uni-id-pages/static/app-plus/uni-fab-login/taobao.png",
					},
					{
						"id": "facebook",
						"text": "脸书登录", //暂未提供该登录方式的接口示例
						"logo": "/uni_modules/uni-id-pages/static/app-plus/uni-fab-login/facebook.png",
					},
					{
						"id": "alipay",
						"text": "支付宝登录", //暂未提供该登录方式的接口示例
						"logo": "/uni_modules/uni-id-pages/static/app-plus/uni-fab-login/alipay.png",
					},
					{
						"id": "qq",
						"text": "QQ登录", //暂未提供该登录方式的接口示例
						"logo": "/uni_modules/uni-id-pages/static/app-plus/uni-fab-login/qq.png",
					},
					{
						"id": "google",
						"text": "谷歌登录", //暂未提供该登录方式的接口示例
						"logo": "/uni_modules/uni-id-pages/static/app-plus/uni-fab-login/google.png",
					},
					{
						"id": "douyin",
						"text": "抖音登录", //暂未提供该登录方式的接口示例
						"logo": "/uni_modules/uni-id-pages/static/app-plus/uni-fab-login/douyin.png",
					},
					{
						"id": "sinaweibo",
						"text": "新浪微博", //暂未提供该登录方式的接口示例
						"logo": "/uni_modules/uni-id-pages/static/app-plus/uni-fab-login/sinaweibo.png",
					}
					// #endif
				],
				univerifyStyle: { //一键登录弹出窗的样式配置参数
					"fullScreen": true, // 是否全屏显示,true表示全屏模式,false表示非全屏模式,默认值为false。
					"backgroundColor": "#ffffff", // 授权页面背景颜色,默认值:#ffffff
					"buttons": { // 自定义登录按钮
						"iconWidth": "45px", // 图标宽度(高度等比例缩放) 默认值:45px
						"list": []
					},
					"privacyTerms": {
						"defaultCheckBoxState": false, // 条款勾选框初始状态 默认值: true
						"textColor": "#BBBBBB", // 文字颜色 默认值:#BBBBBB
						"termsColor": "#5496E3", //  协议文字颜色 默认值: #5496E3
						"prefix": "我已阅读并同意", // 条款前的文案 默认值:“我已阅读并同意”
						"suffix": "并使用本机号码登录", // 条款后的文案 默认值:“并使用本机号码登录”
						"privacyItems": []
					}
				}
			}
		},
		watch: {
			agree(agree) {
				this.univerifyStyle.privacyTerms.defaultCheckBoxState = agree
			}
		},
		async created() {
			let servicesList = this.servicesList
			let loginTypes = config.loginTypes

			servicesList = servicesList.filter(item => {

				// #ifndef APP
				//非app端去掉apple登录
				if (item.id == 'apple') {
					return false
				}
				// #endif

				// #ifdef APP
				//去掉非ios系统上的apple登录
				if (item.id == 'apple' && uni.getSystemInfoSync().osName != 'ios') {
					return false
				}
				// #endif

				return loginTypes.includes(item.id)
			})
			//处理一键登录
			if (loginTypes.includes('univerify')) {
				this.univerifyStyle.privacyTerms.privacyItems = this.agreements
				//设置一键登录功能底下的快捷登录按钮
				servicesList.forEach(({
					id,
					logo,
					path
				}) => {
					if (id != 'univerify') {
						this.univerifyStyle.buttons.list.push({
							"iconPath": logo,
							"provider": id,
							path //路径用于点击快捷按钮时判断是跳转页面
						})
					}
				})
			}
			//	console.log(servicesList);

			//去掉当前页面对应的登录选项
			this.servicesList = servicesList.filter(item => {
				let path = item.path ? item.path.split('?')[0] : '';
				return path != this.getRoute(1)
			})
		},
		methods: {
			getParentComponent(){
				// #ifndef H5
				return this.$parent;
				// #endif

				// #ifdef H5
				return this.$parent.$parent;
				// #endif
			},
			setUserInfo(e) {
				console.log('setUserInfo', e);
			},
			getRoute(n = 0) {
				let pages = getCurrentPages();
				if (n > pages.length) {
					return ''
				}
				return '/' + pages[pages.length - n].route
			},
			toPage(path,index = 0) {
				//console.log('比较', this.getRoute(1),this.getRoute(2), path)
				if (this.getRoute(1) == path.split('?')[0] && this.getRoute(1) ==
					'/uni_modules/uni-id-pages/pages/login/login-withoutpwd') {
					//如果要被打开的页面已经打开,且这个页面是 /uni_modules/uni-id-pages/pages/index/index 则把类型参数传给他
					let loginType = path.split('?')[1].split('=')[1]
					uni.$emit('uni-id-pages-setLoginType', loginType)
				} else if (this.getRoute(2) == path) { // 如果上一个页面就是,马上要打开的页面,直接返回。防止重复开启
					uni.navigateBack();
				} else if (this.getRoute(1) != path) {
					if(index === 0){
						uni.navigateTo({
							url: path,
							animationType: 'slide-in-left',
							complete(e) {
								// console.log(e);
							}
						})
					}else{
						uni.redirectTo({
							url: path,
							animationType: 'slide-in-left',
							complete(e) {
								// console.log(e);
							}
						})
					}
				} else {
					console.log('出乎意料的情况,path:' + path);
				}
			},
			async login_before(type, navigateBack = true, options = {}) {
				console.log(type);
				//提示空实现
				if (["qq",
						"xiaomi",
						"sinaweibo",
						"taobao",
						"facebook",
						"google",
						"alipay",
						"douyin",
					].includes(type)) {
					return uni.showToast({
						title: '该登录方式暂未实现,欢迎提交pr',
						icon: 'none',
						duration: 3000
					});
				}

				//检查当前环境是否支持这种登录方式
				// #ifdef APP
				let isAppExist = true
				await new Promise((callback) => {
					plus.oauth.getServices(oauthServices => {
						let index = oauthServices.findIndex(e => e.id == type)
						if(index != -1){
							isAppExist = oauthServices[index].nativeClient
							callback()
						}else{
							return uni.showToast({
								title: '当前设备不支持此登录,请选择其他登录方式',
								icon: 'none',
								duration: 3000
							});
						}
					}, err => {
						throw new Error('获取服务供应商失败:' + JSON.stringify(err))
					})
				})
				// #endif

				if (
					// #ifdef APP
					!isAppExist
					// #endif

					//非app端使用了,app特有登录方式
					// #ifndef APP
					["univerify","apple"].includes(type)
					// #endif

				) {
					return uni.showToast({
						title: '当前设备不支持此登录,请选择其他登录方式',
						icon: 'none',
						duration: 3000
					});
				}

				//判断是否需要弹出隐私协议授权框
				let needAgreements = (config?.agreements?.scope || []).includes('register')
				if (type != 'univerify' && needAgreements && !this.agree) {
					let agreementsRef = this.getParentComponent().$refs.agreements
					return agreementsRef.popup(() => {
						this.login_before(type, navigateBack, options)
					})
				}

				// #ifdef H5
					if(type == 'weixin'){
						// console.log('开始微信网页登录');
						// let redirectUrl = location.protocol +'//'+
						// 				document.domain +
						// 				(window.location.href.includes('#')?'/#':'') +
						// 				'/uni_modules/uni-id-pages/pages/login/login-withoutpwd?is_weixin_redirect=true&type=weixin'
            // #ifdef VUE2
            const baseUrl = process.env.BASE_URL
            // #endif
            // #ifdef VUE3
            const baseUrl = import.meta.env.BASE_URL
            // #endif

            let redirectUrl = location.protocol +
                '//' +
                location.host +
                baseUrl.replace(/\/$/, '') +
                (window.location.href.includes('#')?'/#':'') +
                '/uni_modules/uni-id-pages/pages/login/login-withoutpwd?is_weixin_redirect=true&type=weixin'

						// console.log('redirectUrl----',redirectUrl);
						let ua = window.navigator.userAgent.toLowerCase();
						if (ua.match(/MicroMessenger/i) == 'micromessenger'){
							// console.log('在微信公众号内');
							return window.open(`https://open.weixin.qq.com/connect/oauth2/authorize?
										appid=${config.appid.weixin.h5}
										&redirect_uri=${encodeURIComponent(redirectUrl)}
										&response_type=code
										&scope=snsapi_userinfo
										&state=STATE&connect_redirect=1#wechat_redirect`);

						}else{
							// console.log('非微信公众号内');
							return location.href = `https://open.weixin.qq.com/connect/qrconnect?appid=${config.appid.weixin.web}
											&redirect_uri=${encodeURIComponent(redirectUrl)}
											&response_type=code&scope=snsapi_login&state=STATE#wechat_redirect`
						}
					}
				// #endif

				uni.showLoading({
					mask: true
				})

				if (type == 'univerify') {
					let univerifyManager = uni.getUniverifyManager()
					let clickAnotherButtons = false
					let onButtonsClickFn = async res => {
						console.log('点击了第三方登录,provider:', res, res.provider, this.univerifyStyle.buttons.list);
						clickAnotherButtons = true
						let checkBoxState = await uni.getCheckBoxState();
						// 同步一键登录弹出层隐私协议框是否打勾
						// #ifdef VUE2
						this.agree = checkBoxState[1].state
						// #endif
						// #ifdef VUE3
						this.agree = checkBoxState.state
						// #endif
						let {
							path
						} = this.univerifyStyle.buttons.list[res.index]
						if (path) {
							if( this.getRoute(1).includes('login-withoutpwd') && path.includes('login-withoutpwd') ){
								this.getParentComponent().showCurrentWebview()
							}
							this.toPage(path,1)
							closeUniverify()
						} else {
							if (this.agree) {
								closeUniverify()
								setTimeout(() => {
									this.login_before(res.provider)
								}, 500)
							} else {
								uni.showToast({
									title: "你未同意隐私政策协议",
									icon: 'none',
									duration: 3000
								});
							}
						}
					}

					function closeUniverify() {
						uni.hideLoading()
						univerifyManager.close()
						// 取消订阅自定义按钮点击事件
						univerifyManager.offButtonsClick(onButtonsClickFn)
					}
					// 订阅自定义按钮点击事件
					univerifyManager.onButtonsClick(onButtonsClickFn)
					// 调用一键登录弹框
					return univerifyManager.login({
						"univerifyStyle": this.univerifyStyle,
						success: res => {
							this.login(res.authResult, 'univerify')
						},
						fail(err) {
							console.log(err)
							if(!clickAnotherButtons){
								uni.navigateBack()
							}
							// uni.showToast({
							// 	title: JSON.stringify(err),
							// 	icon: 'none',
							// 	duration: 3000
							// });
						},
						complete: async e => {
							uni.hideLoading()
							//同步一键登录弹出层隐私协议框是否打勾
							// this.agree = (await uni.getCheckBoxState())[1].state
							// 取消订阅自定义按钮点击事件
							univerifyManager.offButtonsClick(onButtonsClickFn)
						}
					})
				}

				if (type === 'weixinMobile') {
					return this.login({
						phoneCode: options.phoneNumberCode
					}, type)
				}

				uni.login({
					"provider": type,
					"onlyAuthorize": true,
					// #ifdef APP
					"univerifyStyle": this.univerifyStyle,
					// #endif
					success: async e => {
						if (type == 'apple') {
							let res = await this.getUserInfo({
								provider: "apple"
							})
							Object.assign(e.authResult, res.userInfo)
							uni.hideLoading()
						}
						this.login(type == 'weixin' ? {
							code: e.code
						} : e.authResult, type)
					},
					fail: async (err) => {
						console.log(err);
						uni.hideLoading()
					}
				})
			},
			login(params, type) { //联网验证登录
				// console.log('执行登录开始----');
				console.log({params,type});
				//toLowerCase
				let action = 'loginBy' + type.trim().replace(type[0], type[0].toUpperCase())
				const uniIdCo = uniCloud.importObject("uni-id-co",{
					customUI:true
				})
				uniIdCo[action](params).then(result => {
					uni.showToast({
						title: '登录成功',
						icon: 'none',
						duration: 2000
					});
					// #ifdef H5
					result.loginType = type
					// #endif
					mutations.loginSuccess(result)
				})
				.catch(e=>{
					uni.showModal({
						content: e.message,
						confirmText:"知道了",
						showCancel: false
					});
				})
				.finally(e => {
					if (type == 'univerify') {
						uni.closeAuthView()
					}
					uni.hideLoading()
				})
			},
			async getUserInfo(e) {
				return new Promise((resolve, reject) => {
					uni.getUserInfo({
						...e,
						success: (res) => {
							resolve(res);
						},
						fail: (err) => {
							uni.showModal({
								content: JSON.stringify(err),
								showCancel: false
							});
							reject(err);
						}
					})
				})
			}
		}
	}
</script>

<style lang="scss">
	/* #ifndef APP-NVUE */
	.fab-login-box,
	.item {
		display: flex;
		box-sizing: border-box;
		flex-direction: column;
	}
	/* #endif */

	.fab-login-box {
		flex-direction: row;
		flex-wrap: wrap;
		width: 750rpx;
		justify-content: space-around;
		position: fixed;
		left: 0;
	}

	.item {
		flex-direction: column;
		justify-content: center;
		align-items: center;
		height: 200rpx;
		cursor: pointer;
	}

	/* #ifndef APP-NVUE */
	@media screen and (min-width: 690px) {
		.fab-login-box {
			max-width: 500px;
			margin-left: calc(50% - 250px);
		}
		.item {
			height: 160rpx;
		}
	}

	@media screen and (max-width: 690px) {
		.fab-login-box {
			bottom: 10rpx;
		}
	}

	/* #endif */

	.logo {
		width: 60rpx;
		height: 60rpx;
		max-width: 40px;
		max-height: 40px;
		border-radius: 100%;
		border: solid 1px #F6F6F6;
	}

	.login-title {
		text-align: center;
		margin-top: 6px;
		color: #999;
		font-size: 10px;
		width: 70px;
	}
</style>

这是个非常复杂,而且支持多种

{
“id”: “username”,
“text”: “账号登录”,
“logo”: “/uni_modules/uni-id-pages/static/login/uni-fab-login/user.png”,
“path”: “/uni_modules/uni-id-pages/pages/login/login-withpwd”
},
{
“id”: “smsCode”,
“text”: “短信验证码”,
“logo”: “/uni_modules/uni-id-pages/static/login/uni-fab-login/sms.png”,
“path”: “/uni_modules/uni-id-pages/pages/login/login-withoutpwd?type=smsCode”
},
{
“id”: “weixin”,
“text”: “微信登录”,
“logo”: “/uni_modules/uni-id-pages/static/login/uni-fab-login/weixin.png”,
},
// #ifndef MP-WEIXIN
{
“id”: “apple”,
“text”: “苹果登录”,
“logo”: “/uni_modules/uni-id-pages/static/app-plus/uni-fab-login/apple.png”,
},
{
“id”: “univerify”,
“text”: “一键登录”,
“logo”: “/uni_modules/uni-id-pages/static/app-plus/uni-fab-login/univerify.png”,
},
{
“id”: “taobao”,
“text”: “淘宝登录”, //暂未提供该登录方式的接口示例
“logo”: “/uni_modules/uni-id-pages/static/app-plus/uni-fab-login/taobao.png”,
},
{
“id”: “facebook”,
“text”: “脸书登录”, //暂未提供该登录方式的接口示例
“logo”: “/uni_modules/uni-id-pages/static/app-plus/uni-fab-login/facebook.png”,
},
{
“id”: “alipay”,
“text”: “支付宝登录”, //暂未提供该登录方式的接口示例
“logo”: “/uni_modules/uni-id-pages/static/app-plus/uni-fab-login/alipay.png”,
},
{
“id”: “qq”,
“text”: “QQ登录”, //暂未提供该登录方式的接口示例
“logo”: “/uni_modules/uni-id-pages/static/app-plus/uni-fab-login/qq.png”,
},
{
“id”: “google”,
“text”: “谷歌登录”, //暂未提供该登录方式的接口示例
“logo”: “/uni_modules/uni-id-pages/static/app-plus/uni-fab-login/google.png”,
},
{
“id”: “douyin”,
“text”: “抖音登录”, //暂未提供该登录方式的接口示例
“logo”: “/uni_modules/uni-id-pages/static/app-plus/uni-fab-login/douyin.png”,
},
{
“id”: “sinaweibo”,
“text”: “新浪微博”, //暂未提供该登录方式的接口示例
“logo”: “/uni_modules/uni-id-pages/static/app-plus/uni-fab-login/sinaweibo.png”,
}

三、服务端

1、index.obj.js

位置:云/uni-id-co/index.obj.js

const uniIdCommon = require('uni-id-common')
const uniCaptcha = require('uni-captcha')
const {getType,checkIdCard} = require('./common/utils')
const {checkClientInfo,Validator} = require('./common/validator')
const ConfigUtils = require('./lib/utils/config')
const {isUniIdError,ERROR} = require('./common/error')
const middleware = require('./middleware/index')
const universal = require('./common/universal')

const {registerAdmin,registerUser,registerUserByEmail} = require('./module/register/index')
const {addUser,updateUser} = require('./module/admin/index')
const {login,loginBySms,loginByUniverify,loginByWeixin,loginByAlipay,
       loginByQQ,loginByApple,loginByWeixinMobile} = require('./module/login/index')

const {logout} = require('./module/logout/index')

const {	bindMobileBySms,bindMobileByUniverify,bindMobileByMpWeixin,bindAlipay,bindApple,bindQQ,bindWeixin,
  	unbindWeixin,unbindAlipay,unbindQQ,unbindApple} = require('./module/relate/index')

const {setPwd,updatePwd,resetPwdBySms,resetPwdByEmail,closeAccount,getAccountInfo,getRealNameInfo} 
	= require('./module/account/index')

const {createCaptcha,refreshCaptcha,sendSmsCode,sendEmailCode} = require('./module/verify/index')

const {refreshToken,setPushCid,secureNetworkHandshakeByWeixin} = require('./module/utils/index')

const {
  getInvitedUser,
  acceptInvite
} = require('./module/fission')
const {
  authorizeAppLogin,
  removeAuthorizedApp,
  setAuthorizedApp
} = require('./module/multi-end')
const {
  getSupportedLoginType
} = require('./module/dev/index')
const {
  externalRegister,
  externalLogin,
  updateUserInfoByExternal
} = require('./module/external')
const {
  getFrvCertifyId,
  getFrvAuthResult
} = require('./module/facial-recognition-verify')

module.exports = {
  async _before () {
    // 支持 callFunction 与 URL化
    universal.call(this)

    const clientInfo = this.getUniversalClientInfo()
    /**
     * 检查clientInfo,无appId和uniPlatform时本云对象无法正常运行
     * 此外需要保证用到的clientInfo字段均经过类型检查
     * clientInfo由客户端上传并非完全可信,clientInfo内除clientIP、userAgent、source外均为客户端上传参数
     * 否则可能会出现一些意料外的情况
     */
    checkClientInfo(clientInfo)
    let clientPlatform = clientInfo.uniPlatform
    // 统一platform名称
    switch (clientPlatform) {
      case 'app':
      case 'app-plus':
        clientPlatform = 'app'
        break
      case 'web':
      case 'h5':
        clientPlatform = 'web'
        break
      default:
        break
    }

    this.clientPlatform = clientPlatform

    // 挂载uni-id实例到this上,方便后续调用
    this.uniIdCommon = uniIdCommon.createInstance({
      clientInfo
    })

    // 包含uni-id配置合并等功能的工具集
    this.configUtils = new ConfigUtils({
      context: this
    })
    this.config = this.configUtils.getPlatformConfig()
    this.hooks = this.configUtils.getHooks()

    this.validator = new Validator({
      passwordStrength: this.config.passwordStrength
    })

    // 扩展 validator 增加 验证身份证号码合法性
    this.validator.mixin('idCard', function (idCard) {
      if (!checkIdCard(idCard)) {
        return {
          errCode: ERROR.INVALID_ID_CARD
        }
      }
    })
    this.validator.mixin('realName', function (realName) {
      if (
        typeof realName !== 'string' ||
        realName.length < 2 ||
        !/^[\u4e00-\u9fa5]{1,10}(·?[\u4e00-\u9fa5]{1,10}){0,5}$/.test(realName)
      ) {
        return {
          errCode: ERROR.INVALID_REAL_NAME
        }
      }
    })
    /**
     * 示例:覆盖密码验证规则
     */
    // this.validator.mixin('password', function (password) {
    //   if (typeof password !== 'string' || password.length < 10) {
    //     // 调整为密码长度不能小于10
    //     return {
    //       errCode: ERROR.INVALID_PASSWORD
    //     }
    //   }
    // })
    /**
     * 示例:新增验证规则
     */
    // this.validator.mixin('timestamp', function (timestamp) {
    //   if (typeof timestamp !== 'number' || timestamp > Date.now()) {
    //     return {
    //       errCode: ERROR.INVALID_PARAM
    //     }
    //   }
    // })
    // // 新增规则同样可以在数组验证规则中使用
    // this.validator.valdate({
    //   timestamp: 123456789
    // }, {
    //   timestamp: 'timestamp'
    // })
    // this.validator.valdate({
    //   timestampList: [123456789, 123123123123]
    // }, {
    //   timestampList: 'array<timestamp>'
    // })
    // // 甚至更复杂的写法
    // this.validator.valdate({
    //   timestamp: [123456789, 123123123123]
    // }, {
    //   timestamp: 'timestamp|array<timestamp>'
    // })

    // 挂载uni-captcha到this上,方便后续调用
    this.uniCaptcha = uniCaptcha
    Object.defineProperty(this, 'uniOpenBridge', {
      get () {
        return require('uni-open-bridge-common')
      }
    })

    // 挂载中间件
    this.middleware = {}
    for (const mwName in middleware) {
      this.middleware[mwName] = middleware[mwName].bind(this)
    }

    // 国际化
    const messages = require('./lang/index')
    const fallbackLocale = 'zh-Hans'
    const i18n = uniCloud.initI18n({
      locale: clientInfo.locale,
      fallbackLocale,
      messages: JSON.parse(JSON.stringify(messages))
    })
    if (!messages[i18n.locale]) {
      i18n.setLocale(fallbackLocale)  
    }
    this.t = i18n.t.bind(i18n)

    this.response = {}

    // 请求鉴权验证
    await this.middleware.verifyRequestSign()

    // 通用权限校验模块
    await this.middleware.accessControl()
  },
  _after (error, result) {
    if (error) {
      // 处理中间件内抛出的标准响应对象
      if (error.errCode && getType(error) === 'object') {
        const errCode = error.errCode
        if (!isUniIdError(errCode)) {
          return error
        }
        return {
          errCode,
          errMsg: error.errMsg || this.t(errCode, error.errMsgValue)
        }
      }
      throw error
    }
    return Object.assign(this.response, result)
  },
  /**
   * 注册管理员
   * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#register-admin
   * @param {Object} params
   * @param {String} params.username   用户名
   * @param {String} params.password   密码
   * @param {String} params.nickname   昵称
   * @returns
   */
  registerAdmin,
  /**
   * 新增用户
   * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#add-user
   * @param {Object}  params
   * @param {String}  params.username       用户名
   * @param {String}  params.password       密码
   * @param {String}  params.nickname       昵称
   * @param {Array}   params.authorizedApp  允许登录的AppID列表
   * @param {Array}   params.role           用户角色列表
   * @param {String}  params.mobile         手机号
   * @param {String}  params.email          邮箱
   * @param {Array}   params.tags           用户标签
   * @param {Number}  params.status         用户状态
   * @returns
   */
  addUser,
  /**
   * 修改用户
   * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#update-user
   * @param {Object}  params
   * @param {String} params.id              要更新的用户id
   * @param {String}  params.username       用户名
   * @param {String}  params.password       密码
   * @param {String}  params.nickname       昵称
   * @param {Array}   params.authorizedApp  允许登录的AppID列表
   * @param {Array}   params.role           用户角色列表
   * @param {String} params.mobile          手机号
   * @param {String} params.email           邮箱
   * @param {Array}  params.tags            用户标签
   * @param {Number} params.status          用户状态
   * @returns
   */
  updateUser,
  /**
   * 授权用户登录应用
   * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#authorize-app-login
   * @param {Object} params
   * @param {String} params.uid   用户id
   * @param {String} params.appId 授权的应用的AppId
   * @returns
   */
  authorizeAppLogin,
  /**
   * 移除用户登录授权
   * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#remove-authorized-app
   * @param {Object} params
   * @param {String} params.uid   用户id
   * @param {String} params.appId 取消授权的应用的AppId
   * @returns
   */
  removeAuthorizedApp,
  /**
   * 设置用户允许登录的应用列表
   * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#set-authorized-app
   * @param {Object} params
   * @param {String} params.uid       用户id
   * @param {Array} params.appIdList 允许登录的应用AppId列表
   * @returns
   */
  setAuthorizedApp,
  /**
   * 注册普通用户
   * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#register-user
   * @param {Object} params
   * @param {String} params.username    用户名
   * @param {String} params.password    密码
   * @param {String} params.captcha     图形验证码
   * @param {String} params.nickname    昵称
   * @param {String} params.inviteCode  邀请码
   * @returns
   */
  registerUser,
  /**
   * 通过邮箱+验证码注册用户
   * @param {Object} params
   * @param {String} params.email    邮箱
   * @param {String} params.password      密码
   * @param {String} params.nickname    昵称
   * @param {String} params.code  邮箱验证码
   * @param {String} params.inviteCode  邀请码
   * @returns
   */
  registerUserByEmail,
  /**
   * 用户名密码登录
   * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#login
   * @param {Object} params
   * @param {String} params.username  用户名
   * @param {String} params.mobile    手机号
   * @param {String} params.email     邮箱
   * @param {String} params.password  密码
   * @param {String} params.captcha   图形验证码
   * @returns
   */
  login,
  /**
   * 短信验证码登录
   * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#login-by-sms
   * @param {Object} params
   * @param {String} params.mobile      手机号
   * @param {String} params.code        短信验证码
   * @param {String} params.captcha     图形验证码
   * @param {String} params.inviteCode  邀请码
   * @returns
   */
  loginBySms,
  /**
   * App端一键登录
   * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#login-by-univerify
   * @param {Object} params
   * @param {String} params.access_token  APP端一键登录返回的access_token
   * @param {String} params.openid        APP端一键登录返回的openid
   * @param {String} params.inviteCode    邀请码
   * @returns
   */
  loginByUniverify,
  /**
   * 微信登录
   * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#login-by-weixin
   * @param {Object} params
   * @param {String} params.code          微信登录返回的code
   * @param {String} params.inviteCode    邀请码
   * @returns
   */
  loginByWeixin,
  /**
   * 支付宝登录
   * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#login-by-alipay
   * @param {Object} params
   * @param {String} params.code        支付宝小程序客户端登录返回的code
   * @param {String} params.inviteCode  邀请码
   * @returns
   */
  loginByAlipay,
  /**
   * QQ登录
   * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#login-by-qq
   * @param {Object} params
   * @param {String} params.code                  QQ小程序登录返回的code参数
   * @param {String} params.accessToken           App端QQ登录返回的accessToken参数
   * @param {String} params.accessTokenExpired    accessToken过期时间,由App端QQ登录返回的expires_in参数计算而来,单位:毫秒
   * @param {String} params.inviteCode            邀请码
   * @returns
   */
  loginByQQ,
  /**
   * 苹果登录
   * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#login-by-apple
   * @param {Object} params
   * @param {String} params.identityToken 苹果登录返回的identityToken
   * @param {String} params.nickname      用户昵称
   * @param {String} params.inviteCode    邀请码
   * @returns
   */
  loginByApple,
  loginByWeixinMobile,
  /**
   * 用户退出登录
   * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#logout
   * @returns
   */
  logout,
  /**
   * 通过短信验证码绑定手机号
   * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#bind-mobile-by-sms
   * @param {Object} params
   * @param {String} params.mobile    手机号
   * @param {String} params.code      短信验证码
   * @param {String} params.captcha   图形验证码
   * @returns
   */
  bindMobileBySms,
  /**
   * 通过一键登录绑定手机号
   * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#bind-mobile-by-univerify
   * @param {Object} params
   * @param {String} params.openid        APP端一键登录返回的openid
   * @param {String} params.access_token  APP端一键登录返回的access_token
   * @returns
   */
  bindMobileByUniverify,
  /**
   * 通过微信绑定手机号
   * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#bind-mobile-by-mp-weixin
   * @param {Object} params
   * @param {String} params.encryptedData   微信获取手机号返回的加密信息
   * @param {String} params.iv              微信获取手机号返回的初始向量
   * @returns
   */
  bindMobileByMpWeixin,
  /**
   * 绑定微信
   * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#bind-weixin
   * @param {Object} params
   * @param {String} params.code  微信登录返回的code
   * @returns
   */
  bindWeixin,
  /**
   * 绑定QQ
   * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#bind-qq
   * @param {Object} params
   * @param {String} params.code          小程序端QQ登录返回的code
   * @param {String} params.accessToken   APP端QQ登录返回的accessToken
   * @param {String} params.accessTokenExpired    accessToken过期时间,由App端QQ登录返回的expires_in参数计算而来,单位:毫秒
   * @returns
   */
  bindQQ,
  /**
   * 绑定支付宝账号
   * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#bind-alipay
   * @param {Object} params
   * @param {String} params.code  支付宝小程序登录返回的code参数
   * @returns
   */
  bindAlipay,
  /**
   * 绑定苹果账号
   * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#bind-apple
   * @param {Object} params
   * @param {String} params.identityToken 苹果登录返回identityToken
   * @returns
   */
  bindApple,
  /**
   * 更新密码
   * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#update-pwd
   * @param {object} params
   * @param {string} params.oldPassword 旧密码
   * @param {string} params.newPassword 新密码
   * @returns {object}
   */
  updatePwd,
  /**
   * 通过短信验证码重置密码
   * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#reset-pwd-by-sms
   * @param {object} params
   * @param {string} params.mobile   手机号
   * @param {string} params.mobile   短信验证码
   * @param {string} params.password 密码
   * @param {string} params.captcha  图形验证码
   * @returns {object}
   */
  resetPwdBySms,
  /**
   * 通过邮箱验证码重置密码
   * @param {object} params
   * @param {string} params.email   邮箱
   * @param {string} params.code   邮箱验证码
   * @param {string} params.password 密码
   * @param {string} params.captcha  图形验证码
   * @returns {object}
   */
  resetPwdByEmail,
  /**
   * 注销账户
   * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#close-account
   * @returns
   */
  closeAccount,
  /**
   * 获取账户账户简略信息
   * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#get-account-info
   */
  getAccountInfo,
  /**
   * 创建图形验证码
   * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#create-captcha
   * @param {Object} params
   * @param {String} params.scene   图形验证码使用场景
   * @returns
   */
  createCaptcha,
  /**
   * 刷新图形验证码
   * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#refresh-captcha
   * @param {Object} params
   * @param {String} params.scene   图形验证码使用场景
   * @returns
   */
  refreshCaptcha,
  /**
   * 发送短信验证码
   * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#send-sms-code
   * @param {Object} params
   * @param {String} params.mobile    手机号
   * @param {String} params.captcha   图形验证码
   * @param {String} params.scene     短信验证码使用场景
   * @returns
   */
  sendSmsCode,
  /**
   * 发送邮箱验证码
   * @tutorial 需自行实现功能
   * @param {Object} params
   * @param {String} params.email    邮箱
   * @param {String} params.captcha   图形验证码
   * @param {String} params.scene     短信验证码使用场景
   * @returns
   */
  sendEmailCode,
  /**
   * 刷新token
   * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#refresh-token
   */
  refreshToken,
  /**
   * 接受邀请
   * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#accept-invite
   * @param {Object} params
   * @param {String} params.inviteCode  邀请码
   * @returns
   */
  acceptInvite,
  /**
   * 获取受邀用户
   * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#get-invited-user
   * @param {Object} params
   * @param {Number} params.level       获取受邀用户的级数,1表示直接邀请的用户
   * @param {Number} params.limit       返回数据大小
   * @param {Number} params.offset      返回数据偏移
   * @param {Boolean} params.needTotal  是否需要返回总数
   * @returns
   */
  getInvitedUser,
  /**
   * 更新device表的push_clien_id
   * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#set-push-cid
   * @param {object} params
   * @param {string} params.pushClientId  客户端pushClientId
   * @returns
   */
  setPushCid,
  /**
   * 获取支持的登录方式
   * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#get-supported-login-type
   * @returns
   */
  getSupportedLoginType,

  /**
   * 解绑微信
   * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#unbind-weixin
   * @returns
   */
  unbindWeixin,
  /**
   * 解绑支付宝
   * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#unbind-alipay
   * @returns
   */
  unbindAlipay,
  /**
   * 解绑QQ
   * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#unbind-qq
   * @returns
   */
  unbindQQ,
  /**
   * 解绑Apple
   * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#unbind-apple
   * @returns
   */
  unbindApple,
  /**
   * 安全网络握手,目前仅处理微信小程序安全网络握手
   */
  secureNetworkHandshakeByWeixin,
  /**
   * 设置密码
   * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#set-pwd
   * @returns
   */
  setPwd,
  /**
   * 外部注册用户
   * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#external-register
   * @param {object} params
   * @param {string} params.externalUid   业务系统的用户id
   * @param {string} params.nickname  昵称
   * @param {string} params.gender  性别
   * @param {string} params.avatar  头像
   * @returns {object}
   */
  externalRegister,
  /**
   * 外部用户登录
   * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#external-login
   * @param {object} params
   * @param {string} params.userId  uni-id体系用户id
   * @param {string} params.externalUid   业务系统的用户id
   * @returns {object}
   */
  externalLogin,
  /**
   * 使用 userId 或 externalUid 获取用户信息
   * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#external-update-userinfo
   * @param {object} params
   * @param {string} params.userId   uni-id体系的用户id
   * @param {string} params.externalUid   业务系统的用户id
   * @param {string} params.nickname  昵称
   * @param {string} params.gender  性别
   * @param {string} params.avatar  头像
   * @returns {object}
   */
  updateUserInfoByExternal,
  /**
   * 获取认证ID
   * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#get-frv-certify-id
   * @param {Object} params
   * @param {String} params.realName  真实姓名
   * @param {String} params.idCard    身份证号码
   * @returns
   */
  getFrvCertifyId,
  /**
   * 查询认证结果
   * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#get-frv-auth-result
   * @param {Object} params
   * @param {String} params.certifyId       认证ID
   * @param {String} params.needAlivePhoto  是否获取认证照片,Y_O (原始图片)、Y_M(虚化,背景马赛克)、N(不返图)
   * @returns
   */
  getFrvAuthResult,
  /**
   * 获取实名信息
   * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#get-realname-info
   * @param {Object} params
   * @param {Boolean} params.decryptData 是否解密数据
   * @returns
   */
  getRealNameInfo
}

2、index.js

位置:云/uni-id-co/module/login/index.js

module.exports = {
  login: require('./login'), 
  loginBySms: require('./login-by-sms'),
  loginByUniverify: require('./login-by-univerify'),
  loginByWeixin: require('./login-by-weixin'),
  loginByAlipay: require('./login-by-alipay'),
  loginByQQ: require('./login-by-qq'),
  loginByApple: require('./login-by-apple'),
  loginByBaidu: require('./login-by-baidu'),
  loginByDingtalk: require('./login-by-dingtalk'),
  loginByToutiao: require('./login-by-toutiao'),
  loginByDouyin: require('./login-by-douyin'),
  loginByWeibo: require('./login-by-weibo'),
  loginByTaobao: require('./login-by-taobao'),
  loginByEmailLink: require('./login-by-email-link'),
  loginByEmailCode: require('./login-by-email-code'),
  loginByFacebook: require('./login-by-facebook'),
  loginByGoogle: require('./login-by-google'),
  loginByWeixinMobile: require('./login-by-weixin-mobile')
}

3、loginByUniverify.js

位置:云/uni-id-co/module/login

const {
  getPhoneNumber
} = require('../../lib/utils/univerify')
const {
  preUnifiedLogin,
  postUnifiedLogin
} = require('../../lib/utils/unified-login')
const {
  LOG_TYPE
} = require('../../common/constants')

/**
 * App端一键登录
 * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#login-by-univerify
 * @param {Object} params
 * @param {String} params.access_token  APP端一键登录返回的access_token
 * @param {String} params.openid        APP端一键登录返回的openid
 * @param {String} params.inviteCode    邀请码
 * @returns
 */
module.exports = async function (params = {}) {
  const schema = {
    access_token: 'string',
    openid: 'string',
    inviteCode: {
      required: false,
      type: 'string'
    }
  }
  this.middleware.validate(params, schema)
  const {
    // eslint-disable-next-line camelcase
    access_token,
    openid,
    inviteCode
  } = params

  let mobile
  try {
    const phoneInfo = await getPhoneNumber.call(this, {
      // eslint-disable-next-line camelcase
      access_token,
      openid
    })
    mobile = phoneInfo.phoneNumber
  } catch (error) {
    await this.middleware.uniIdLog({
      success: false,
      type: LOG_TYPE.LOGIN
    })
    throw error
  }
  const {
    user,
    type
  } = await preUnifiedLogin.call(this, {
    user: {
      mobile
    }
  })
  return postUnifiedLogin.call(this, {
    user,
    extraData: {
      mobile_confirmed: 1
    },
    type,
    inviteCode
  })
}

四、登录过程分析

1、客户端请求代码

位置:/uni_modules/uni-id-pages/components/uni-id-pages-fab-login/uni-id-pages-fab-login.vue(452)

			login(params, type) { //联网验证登录
				// console.log('执行登录开始----');
				console.log({params,type});
				//toLowerCase
				let action = 'loginBy' + type.trim().replace(type[0], type[0].toUpperCase())
				const uniIdCo = uniCloud.importObject("uni-id-co",{
					customUI:true
				})
				uniIdCo[action](params).then(result => {
					uni.showToast({
						title: '登录成功',
						icon: 'none',
						duration: 2000
					});
					// #ifdef H5
					result.loginType = type
					// #endif
					mutations.loginSuccess(result)
				})
				.catch(e=>{
					uni.showModal({
						content: e.message,
						confirmText:"知道了",
						showCancel: false
					});
				})
				.finally(e => {
					if (type == 'univerify') {
						uni.closeAuthView()
					}
					uni.hideLoading()
				})
			},

2、服务端响应代码

位置:云/uni-id-co/index.obj.js(377)

  loginByUniverify,

这一行代码来自:./module/login/index

module.exports = {
  login: require('./login'), 
  loginBySms: require('./login-by-sms'),
  loginByUniverify: require('./login-by-univerify'),
  loginByWeixin: require('./login-by-weixin'),
  loginByAlipay: require('./login-by-alipay'),
  loginByQQ: require('./login-by-qq'),
  loginByApple: require('./login-by-apple'),
  loginByBaidu: require('./login-by-baidu'),
  loginByDingtalk: require('./login-by-dingtalk'),
  loginByToutiao: require('./login-by-toutiao'),
  loginByDouyin: require('./login-by-douyin'),
  loginByWeibo: require('./login-by-weibo'),
  loginByTaobao: require('./login-by-taobao'),
  loginByEmailLink: require('./login-by-email-link'),
  loginByEmailCode: require('./login-by-email-code'),
  loginByFacebook: require('./login-by-facebook'),
  loginByGoogle: require('./login-by-google'),
  loginByWeixinMobile: require('./login-by-weixin-mobile')
}

/login-by-univerify

const {
  getPhoneNumber
} = require('../../lib/utils/univerify')
const {
  preUnifiedLogin,
  postUnifiedLogin
} = require('../../lib/utils/unified-login')
const {
  LOG_TYPE
} = require('../../common/constants')

/**
 * App端一键登录
 * @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#login-by-univerify
 * @param {Object} params
 * @param {String} params.access_token  APP端一键登录返回的access_token
 * @param {String} params.openid        APP端一键登录返回的openid
 * @param {String} params.inviteCode    邀请码
 * @returns
 */
module.exports = async function (params = {}) {
  const schema = {
    access_token: 'string',
    openid: 'string',
    inviteCode: {
      required: false,
      type: 'string'
    }
  }
  this.middleware.validate(params, schema)
  const {
    // eslint-disable-next-line camelcase
    access_token,
    openid,
    inviteCode
  } = params

  let mobile
  try {
    const phoneInfo = await getPhoneNumber.call(this, {
      // eslint-disable-next-line camelcase
      access_token,
      openid
    })
    mobile = phoneInfo.phoneNumber
  } catch (error) {
    await this.middleware.uniIdLog({
      success: false,
      type: LOG_TYPE.LOGIN
    })
    throw error
  }
  const {
    user,
    type
  } = await preUnifiedLogin.call(this, {
    user: {
      mobile
    }
  })
  return postUnifiedLogin.call(this, {
    user,
    extraData: {
      mobile_confirmed: 1
    },
    type,
    inviteCode
  })
}

3、getPhoneNumber

‘…/…/lib/utils/univerify’


async function getPhoneNumber ({
  // eslint-disable-next-line camelcase
  access_token,
  openid
} = {}) {
  const requiredParams = ['apiKey', 'apiSecret']
  const univerifyConfig = (this.config.service && this.config.service.univerify) || {}
  for (let i = 0; i < requiredParams.length; i++) {
    const key = requiredParams[i]
    if (!univerifyConfig[key]) {
      throw new Error(`Missing config param: service.univerify.${key}`)
    }
  }
  return uniCloud.getPhoneNumber({
    provider: 'univerify',
    appid: this.getUniversalClientInfo().appId,
    apiKey: univerifyConfig.apiKey,
    apiSecret: univerifyConfig.apiSecret,
    // eslint-disable-next-line camelcase
    access_token,
    openid
  })
}

module.exports = {
  getPhoneNumber
}

4、preUnifiedLogin、postUnifiedLogin

位置:/uni-id-co/lib/utils/unified-login.js

const {
  checkLoginUserRecord,
  postLogin
} = require('./login')
const {
  postRegister
} = require('./register')
const {
  findUser
} = require('./account')
const {
  ERROR
} = require('../../common/error')

async function realPreUnifiedLogin (params = {}) {
  const {
    user,
    type
  } = params
  const appId = this.getUniversalClientInfo().appId
  const {
    total,
    userMatched
  } = await findUser({
    userQuery: user,
    authorizedApp: appId
  })
  if (userMatched.length === 0) {
    if (type === 'login') {
      if (total > 0) {
        throw {
          errCode: ERROR.ACCOUNT_NOT_EXISTS_IN_CURRENT_APP
        }
      }
      throw {
        errCode: ERROR.ACCOUNT_NOT_EXISTS
      }
    }
    return {
      type: 'register',
      user
    }
  } if (userMatched.length === 1) {
    if (type === 'register') {
      throw {
        errCode: ERROR.ACCOUNT_EXISTS
      }
    }
    const userRecord = userMatched[0]
    checkLoginUserRecord(userRecord)
    return {
      type: 'login',
      user: userRecord
    }
  } else if (userMatched.length > 1) {
    throw {
      errCode: ERROR.ACCOUNT_CONFLICT
    }
  }
}

async function preUnifiedLogin (params = {}) {
  try {
    const result = await realPreUnifiedLogin.call(this, params)
    return result
  } catch (error) {
    await this.middleware.uniIdLog({
      success: false
    })
    throw error
  }
}

async function postUnifiedLogin (params = {}) {
  const {
    user,
    extraData = {},
    isThirdParty = false,
    type,
    inviteCode
  } = params
  let result
  if (type === 'login') {
    result = await postLogin.call(this, {
      user,
      extraData,
      isThirdParty
    })
  } else if (type === 'register') {
    result = await postRegister.call(this, {
      user,
      extraData,
      isThirdParty,
      inviteCode
    })
  }
  return {
    ...result,
    type
  }
}

module.exports = {
  preUnifiedLogin,
  postUnifiedLogin
}

5、login

位置:/uni-id-co/lib/utils/login.js

const {findUser} = require('./account')
const {userCollection,LOG_TYPE} = require('../../common/constants')
const {ERROR} = require('../../common/error')
const {logout} = require('./logout')
const PasswordUtils = require('./password')

async function realPreLogin (params = {}) {
  const {
    user
  } = params
  const appId = this.getUniversalClientInfo().appId
  const {
    total,
    userMatched
  } = await findUser({
    userQuery: user,
    authorizedApp: appId
  })
  if (userMatched.length === 0) {
    if (total > 0) {
      throw {
        errCode: ERROR.ACCOUNT_NOT_EXISTS_IN_CURRENT_APP
      }
    }
    throw {
      errCode: ERROR.ACCOUNT_NOT_EXISTS
    }
  } else if (userMatched.length > 1) {
    throw {
      errCode: ERROR.ACCOUNT_CONFLICT
    }
  }
  const userRecord = userMatched[0]
  checkLoginUserRecord(userRecord)
  return userRecord
}

async function preLogin (params = {}) {
  const {
    user
  } = params
  try {
    const user = await realPreLogin.call(this, params)
    return user
  } catch (error) {
    await this.middleware.uniIdLog({
      success: false,
      data: user,
      type: LOG_TYPE.LOGIN
    })
    throw error
  }
}

async function preLoginWithPassword (params = {}) {
  const {
    user,
    password
  } = params
  try {
    const userRecord = await realPreLogin.call(this, params)
    const {
      passwordErrorLimit,
      passwordErrorRetryTime
    } = this.config
    const {
      clientIP
    } = this.getUniversalClientInfo()
    // 根据ip地址,密码错误次数过多,锁定登录
    let loginIPLimit = userRecord.login_ip_limit || []
    // 清理无用记录
    loginIPLimit = loginIPLimit.filter(item => item.last_error_time > Date.now() - passwordErrorRetryTime * 1000)
    let currentIPLimit = loginIPLimit.find(item => item.ip === clientIP)
    if (currentIPLimit && currentIPLimit.error_times >= passwordErrorLimit) {
      throw {
        errCode: ERROR.PASSWORD_ERROR_EXCEED_LIMIT
      }
    }
    const passwordUtils = new PasswordUtils({
      userRecord,
      clientInfo: this.getUniversalClientInfo(),
      passwordSecret: this.config.passwordSecret
    })

    const {
      success: checkPasswordSuccess,
      refreshPasswordInfo
    } = passwordUtils.checkUserPassword({
      password
    })
    if (!checkPasswordSuccess) {
      // 更新用户ip对应的密码错误记录
      if (!currentIPLimit) {
        currentIPLimit = {
          ip: clientIP,
          error_times: 1,
          last_error_time: Date.now()
        }
        loginIPLimit.push(currentIPLimit)
      } else {
        currentIPLimit.error_times++
        currentIPLimit.last_error_time = Date.now()
      }
      await userCollection.doc(userRecord._id).update({
        login_ip_limit: loginIPLimit
      })
      throw {
        errCode: ERROR.PASSWORD_ERROR
      }
    }
    const extraData = {}
    if (refreshPasswordInfo) {
      extraData.password = refreshPasswordInfo.passwordHash
      extraData.password_secret_version = refreshPasswordInfo.version
    }

    const currentIPLimitIndex = loginIPLimit.indexOf(currentIPLimit)
    if (currentIPLimitIndex > -1) {
      loginIPLimit.splice(currentIPLimitIndex, 1)
    }
    extraData.login_ip_limit = loginIPLimit
    return {
      user: userRecord,
      extraData
    }
  } catch (error) {
    await this.middleware.uniIdLog({
      success: false,
      data: user,
      type: LOG_TYPE.LOGIN
    })
    throw error
  }
}

function checkLoginUserRecord (user) {
  switch (user.status) {
    case undefined:
    case 0:
      break
    case 1:
      throw {
        errCode: ERROR.ACCOUNT_BANNED
      }
    case 2:
      throw {
        errCode: ERROR.ACCOUNT_AUDITING
      }
    case 3:
      throw {
        errCode: ERROR.ACCOUNT_AUDIT_FAILED
      }
    case 4:
      throw {
        errCode: ERROR.ACCOUNT_CLOSED
      }
    default:
      break
  }
}

async function thirdPartyLogin (params = {}) {
  const {
    user
  } = params
  return {
    mobileConfirmed: !!user.mobile_confirmed,
    emailConfirmed: !!user.email_confirmed
  }
}

async function postLogin (params = {}) {
  const {
    user,
    extraData,
    isThirdParty = false
  } = params
  const {
    clientIP
  } = this.getUniversalClientInfo()
  const uniIdToken = this.getUniversalUniIdToken()
  const uid = user._id
  const updateData = {
    last_login_date: Date.now(),
    last_login_ip: clientIP,
    ...extraData
  }
  const createTokenRes = await this.uniIdCommon.createToken({
    uid
  })

  const {
    errCode,
    token,
    tokenExpired
  } = createTokenRes
  if (errCode) {
    throw createTokenRes
  }

  if (uniIdToken) {
    try {
      await logout.call(this)
    } catch (error) {}
  }

  await userCollection.doc(uid).update(updateData)
  await this.middleware.uniIdLog({
    data: {
      user_id: uid
    },
    type: LOG_TYPE.LOGIN
  })
  return {
    errCode: 0,
    newToken: {
      token,
      tokenExpired
    },
    uid,
    ...(
      isThirdParty
        ? thirdPartyLogin({
          user
        })
        : {}
    ),
    passwordConfirmed: !!user.password
  }
}

module.exports = {
  preLogin,
  postLogin,
  checkLoginUserRecord,
  preLoginWithPassword
}

6、register

位置:/uni-id-co/lib/utils/register.js

const {
  userCollection,
  LOG_TYPE
} = require('../../common/constants')
const {
  ERROR
} = require('../../common/error')
const {
  findUser
} = require('./account')
const {
  getValidInviteCode,
  generateInviteInfo
} = require('./fission')
const {
  logout
} = require('./logout')
const PasswordUtils = require('./password')
const merge = require('lodash.merge')

async function realPreRegister (params = {}) {
  const {
    user
  } = params
  const {
    userMatched
  } = await findUser({
    userQuery: user,
    authorizedApp: this.getUniversalClientInfo().appId
  })
  if (userMatched.length > 0) {
    throw {
      errCode: ERROR.ACCOUNT_EXISTS
    }
  }
}

async function preRegister (params = {}) {
  try {
    await realPreRegister.call(this, params)
  } catch (error) {
    await this.middleware.uniIdLog({
      success: false,
      type: LOG_TYPE.REGISTER
    })
    throw error
  }
}

async function preRegisterWithPassword (params = {}) {
  const {
    user,
    password
  } = params
  await preRegister.call(this, {
    user
  })
  const passwordUtils = new PasswordUtils({
    clientInfo: this.getUniversalClientInfo(),
    passwordSecret: this.config.passwordSecret
  })
  const {
    passwordHash,
    version
  } = passwordUtils.generatePasswordHash({
    password
  })
  const extraData = {
    password: passwordHash,
    password_secret_version: version
  }
  return {
    user,
    extraData
  }
}

async function thirdPartyRegister ({
  user = {}
} = {}) {
  return {
    mobileConfirmed: !!(user.mobile && user.mobile_confirmed) || false,
    emailConfirmed: !!(user.email && user.email_confirmed) || false
  }
}

async function postRegister (params = {}) {
  const {
    user,
    extraData = {},
    isThirdParty = false,
    inviteCode
  } = params
  const {
    appId,
    appName,
    appVersion,
    appVersionCode,
    channel,
    scene,
    clientIP,
    osName
  } = this.getUniversalClientInfo()
  const uniIdToken = this.getUniversalUniIdToken()

  merge(user, extraData)

  const registerChannel = channel || scene
  user.register_env = {
    appid: appId || '',
    uni_platform: this.clientPlatform || '',
    os_name: osName || '',
    app_name: appName || '',
    app_version: appVersion || '',
    app_version_code: appVersionCode || '',
    channel: registerChannel ? registerChannel + '' : '', // channel可能为数字,统一存为字符串
    client_ip: clientIP || ''
  }

  user.register_date = Date.now()
  user.dcloud_appid = [appId]

  if (user.username) {
    user.username = user.username.toLowerCase()
  }
  if (user.email) {
    user.email = user.email.toLowerCase()
  }

  const {
    autoSetInviteCode, // 注册时自动设置邀请码
    forceInviteCode, // 必须有邀请码才允许注册,注意此逻辑不可对admin生效
    userRegisterDefaultRole // 用户注册时配置的默认角色
  } = this.config
  if (autoSetInviteCode) {
    user.my_invite_code = await getValidInviteCode()
  }

  // 如果用户注册默认角色配置存在且不为空数组
  if (userRegisterDefaultRole && userRegisterDefaultRole.length) {
    // 将用户已有的角色和配置的默认角色合并成一个数组,并去重
    user.role = Array.from(new Set([...(user.role || []), ...userRegisterDefaultRole]))
  }

  const isAdmin = user.role && user.role.includes('admin')

  if (forceInviteCode && !isAdmin && !inviteCode) {
    throw {
      errCode: ERROR.INVALID_INVITE_CODE
    }
  }

  if (inviteCode) {
    const {
      inviterUid,
      inviteTime
    } = await generateInviteInfo({
      inviteCode
    })
    user.inviter_uid = inviterUid
    user.invite_time = inviteTime
  }

  if (uniIdToken) {
    try {
      await logout.call(this)
    } catch (error) { }
  }

  const beforeRegister = this.hooks.beforeRegister
  let userRecord = user
  if (beforeRegister) {
    userRecord = await beforeRegister({
      userRecord,
      clientInfo: this.getUniversalClientInfo()
    })
  }

  const {
    id: uid
  } = await userCollection.add(userRecord)

  const createTokenRes = await this.uniIdCommon.createToken({
    uid
  })

  const {
    errCode,
    token,
    tokenExpired
  } = createTokenRes

  if (errCode) {
    throw createTokenRes
  }

  await this.middleware.uniIdLog({
    data: {
      user_id: uid
    },
    type: LOG_TYPE.REGISTER
  })

  return {
    errCode: 0,
    uid,
    newToken: {
      token,
      tokenExpired
    },
    ...(
      isThirdParty
        ? thirdPartyRegister({
          user: {
            ...userRecord,
            _id: uid
          }
        })
        : {}
    ),
    passwordConfirmed: !!userRecord.password
  }
}

module.exports = {
  preRegister,
  preRegisterWithPassword,
  postRegister
}

7、account

位置:/uni-id-co/lib/untils/account.js

const {
  dbCmd,
  userCollection
} = require('../../common/constants')
const {
  USER_IDENTIFIER
} = require('../../common/constants')
const {
  batchFindObjctValue,
  getType,
  isMatchUserApp
} = require('../../common/utils')

/**
 * 查询满足条件的用户
 * @param {Object} params
 * @param {Object} params.userQuery 用户唯一标识组成的查询条件
 * @param {Object} params.authorizedApp 用户允许登录的应用
 * @returns userMatched 满足条件的用户列表
 */
async function findUser (params = {}) {
  const {
    userQuery,
    authorizedApp = []
  } = params
  const condition = getUserQueryCondition(userQuery)
  if (condition.length === 0) {
    throw new Error('Invalid user query')
  }
  const authorizedAppType = getType(authorizedApp)
  if (authorizedAppType !== 'string' && authorizedAppType !== 'array') {
    throw new Error('Invalid authorized app')
  }

  let finalQuery

  if (condition.length === 1) {
    finalQuery = condition[0]
  } else {
    finalQuery = dbCmd.or(condition)
  }
  const userQueryRes = await userCollection.where(finalQuery).get()
  return {
    total: userQueryRes.data.length,
    userMatched: userQueryRes.data.filter(item => {
      return isMatchUserApp(item.dcloud_appid, authorizedApp)
    })
  }
}

function getUserIdentifier (userRecord = {}) {
  const keys = Object.keys(USER_IDENTIFIER)
  return batchFindObjctValue(userRecord, keys)
}

function getUserQueryCondition (userRecord = {}) {
  const userIdentifier = getUserIdentifier(userRecord)
  const condition = []
  for (const key in userIdentifier) {
    const value = userIdentifier[key]
    if (!value) {
      // 过滤所有value为假值的条件,在查询用户时没有意义
      continue
    }
    const queryItem = {
      [key]: value
    }
    // 为兼容用户老数据用户名及邮箱需要同时查小写及原始大小写数据
    if (key === 'mobile') {
      queryItem.mobile_confirmed = 1
    } else if (key === 'email') {
      queryItem.email_confirmed = 1
      const email = userIdentifier.email
      if (email.toLowerCase() !== email) {
        condition.push({
          email: email.toLowerCase(),
          email_confirmed: 1
        })
      }
    } else if (key === 'username') {
      const username = userIdentifier.username
      if (username.toLowerCase() !== username) {
        condition.push({
          username: username.toLowerCase()
        })
      }
    } else if (key === 'identities') {
      queryItem.identities = dbCmd.elemMatch(value)
    }
    condition.push(queryItem)
  }
  return condition
}

module.exports = {
  findUser,
  getUserIdentifier
}

8、logout

位置:/uni-id-co/lib/utils/logout.js

const {
  dbCmd,
  LOG_TYPE,
  deviceCollection,
  userCollection
} = require('../../common/constants')

async function logout () {
  const {
    deviceId
  } = this.getUniversalClientInfo()
  const uniIdToken = this.getUniversalUniIdToken()
  const payload = await this.uniIdCommon.checkToken(
    uniIdToken,
    {
      autoRefresh: false
    }
  )
  if (payload.errCode) {
    throw payload
  }
  const uid = payload.uid

  // 删除token
  await userCollection.doc(uid).update({
    token: dbCmd.pull(uniIdToken)
  })

  // 仅当device表的device_id和user_id均对应时才进行更新
  await deviceCollection.where({
    device_id: deviceId,
    user_id: uid
  }).update({
    token_expired: 0
  })
  await this.middleware.uniIdLog({
    data: {
      user_id: uid
    },
    type: LOG_TYPE.LOGOUT
  })
  return {
    errCode: 0
  }
}

module.exports = {
  logout
}

9、password

位置:/uni-id-co/lib/utils/password.js

const {
  getType
} = require('../../common/utils')
const crypto = require('crypto')
const createConfig = require('uni-config-center')
const shareConfig = createConfig({
  pluginId: 'uni-id'
})
let customPassword = {}
if (shareConfig.hasFile('custom-password.js')) {
  customPassword = shareConfig.requireFile('custom-password.js') || {}
}

const passwordAlgorithmMap = {
  UNI_ID_HMAC_SHA1: 'hmac-sha1',
  UNI_ID_HMAC_SHA256: 'hmac-sha256',
  UNI_ID_CUSTOM: 'custom'
}

const passwordAlgorithmKeyMap = Object.keys(passwordAlgorithmMap).reduce((res, item) => {
  res[passwordAlgorithmMap[item]] = item
  return res
}, {})

const passwordExtMethod = {
  [passwordAlgorithmMap.UNI_ID_HMAC_SHA1]: {
    verify ({ password }) {
      const { password_secret_version: passwordSecretVersion } = this.userRecord

      const passwordSecret = this._getSecretByVersion({
        version: passwordSecretVersion
      })

      const { passwordHash } = this.encrypt({
        password,
        passwordSecret
      })

      return passwordHash === this.userRecord.password
    },
    encrypt ({ password, passwordSecret }) {
      const { value: secret, version } = passwordSecret
      const hmac = crypto.createHmac('sha1', secret.toString('ascii'))

      hmac.update(password)

      return {
        passwordHash: hmac.digest('hex'),
        version
      }
    }
  },
  [passwordAlgorithmMap.UNI_ID_HMAC_SHA256]: {
    verify ({ password }) {
      const parse = this._parsePassword()
      const passwordHash = crypto.createHmac(parse.algorithm, parse.salt).update(password).digest('hex')

      return passwordHash === parse.hash
    },
    encrypt ({ password, passwordSecret }) {
      const { version } = passwordSecret

      // 默认使用 sha256 加密算法
      const salt = crypto.randomBytes(10).toString('hex')
      const sha256Hash = crypto.createHmac(passwordAlgorithmMap.UNI_ID_HMAC_SHA256.substring(5), salt).update(password).digest('hex')
      const algorithm = passwordAlgorithmKeyMap[passwordAlgorithmMap.UNI_ID_HMAC_SHA256]
      // B 为固定值,对应 PasswordMethodMaps 中的 sha256算法
      // hash 格式 $[PasswordMethodFlagMapsKey]$[salt size]$[salt][Hash]
      const passwordHash = `$${algorithm}$${salt.length}$${salt}${sha256Hash}`

      return {
        passwordHash,
        version
      }
    }
  },
  [passwordAlgorithmMap.UNI_ID_CUSTOM]: {
    verify ({ password, passwordSecret }) {
      if (!customPassword.verifyPassword) throw new Error('verifyPassword method not found in custom password file')

      // return true or false
      return customPassword.verifyPassword({
        password,
        passwordSecret,
        userRecord: this.userRecord,
        clientInfo: this.clientInfo
      })
    },
    encrypt ({ password, passwordSecret }) {
      if (!customPassword.encryptPassword) throw new Error('encryptPassword method not found in custom password file')

      // return object<{passwordHash: string, version: number}>
      return customPassword.encryptPassword({
        password,
        passwordSecret,
        clientInfo: this.clientInfo
      })
    }
  }
}

class PasswordUtils {
  constructor ({
    userRecord = {},
    clientInfo,
    passwordSecret
  } = {}) {
    if (!clientInfo) throw new Error('Invalid clientInfo')
    if (!passwordSecret) throw new Error('Invalid password secret')

    this.clientInfo = clientInfo
    this.userRecord = userRecord
    this.passwordSecret = this.prePasswordSecret(passwordSecret)
  }

  /**
   * passwordSecret 预处理
   * @param passwordSecret
   * @return {*[]}
   */
  prePasswordSecret (passwordSecret) {
    const newPasswordSecret = []
    if (getType(passwordSecret) === 'string') {
      newPasswordSecret.push({
        value: passwordSecret,
        type: passwordAlgorithmMap.UNI_ID_HMAC_SHA1
      })
    } else if (getType(passwordSecret) === 'array') {
      for (const secret of passwordSecret.sort((a, b) => a.version - b.version)) {
        newPasswordSecret.push({
          ...secret,
          // 没有 type 设置默认 type hmac-sha1
          type: secret.type || passwordAlgorithmMap.UNI_ID_HMAC_SHA1
        })
      }
    } else {
      throw new Error('Invalid password secret')
    }

    return newPasswordSecret
  }

  /**
   * 获取最新加密密钥
   * @return {*}
   * @private
   */
  _getLastestSecret () {
    return this.passwordSecret[this.passwordSecret.length - 1]
  }

  _getOldestSecret () {
    return this.passwordSecret[0]
  }

  _getSecretByVersion ({ version } = {}) {
    if (!version && version !== 0) {
      return this._getOldestSecret()
    }
    if (this.passwordSecret.length === 1) {
      return this.passwordSecret[0]
    }
    return this.passwordSecret.find(item => item.version === version)
  }

  /**
   * 获取密码的验证/加密方法
   * @param passwordSecret
   * @return {*[]}
   * @private
   */
  _getPasswordExt (passwordSecret) {
    const ext = passwordExtMethod[passwordSecret.type]
    if (!ext) {
      throw new Error(`暂不支持 ${passwordSecret.type} 类型的加密算法`)
    }

    const passwordExt = Object.create(null)

    for (const key in ext) {
      passwordExt[key] = ext[key].bind(Object.assign(this, Object.keys(ext).reduce((res, item) => {
        if (item !== key) {
          res[item] = ext[item].bind(this)
        }
        return res
      }, {})))
    }

    return passwordExt
  }

  _parsePassword () {
    const [algorithmKey = '', cost = 0, hashStr = ''] = this.userRecord.password.split('$').filter(key => key)
    const algorithm = passwordAlgorithmMap[algorithmKey] ? passwordAlgorithmMap[algorithmKey].substring(5) : null
    const salt = hashStr.substring(0, Number(cost))
    const hash = hashStr.substring(Number(cost))

    return {
      algorithm,
      salt,
      hash
    }
  }

  /**
   * 生成加密后的密码
   * @param {String} password 密码
   */
  generatePasswordHash ({ password }) {
    if (!password) throw new Error('Invalid password')

    const passwordSecret = this._getLastestSecret()
    const ext = this._getPasswordExt(passwordSecret)

    const { passwordHash, version } = ext.encrypt({
      password,
      passwordSecret
    })

    return {
      passwordHash,
      version
    }
  }

  /**
   * 密码校验
   * @param {String} password
   * @param {Boolean} autoRefresh
   * @return {{refreshPasswordInfo: {version: *, passwordHash: *}, success: boolean}|{success: boolean}}
   */
  checkUserPassword ({ password, autoRefresh = true }) {
    if (!password) throw new Error('Invalid password')

    const { password_secret_version: passwordSecretVersion } = this.userRecord
    const passwordSecret = this._getSecretByVersion({
      version: passwordSecretVersion
    })
    const ext = this._getPasswordExt(passwordSecret)

    const success = ext.verify({ password, passwordSecret })

    if (!success) {
      return {
        success: false
      }
    }

    let refreshPasswordInfo
    if (autoRefresh && passwordSecretVersion !== this._getLastestSecret().version) {
      refreshPasswordInfo = this.generatePasswordHash({ password })
    }

    return {
      success: true,
      refreshPasswordInfo
    }
  }
}

module.exports = PasswordUtils

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值