uni一键登陆官方文档,官网中有官方的代码,我们项目的代码基于官方的代码进行了改动,增加了一个校验方式。

        使用uni一键登陆的话你得需要开通unicloud云空间(我们app使用了uniadmin的在线热更新功能,所以我们选择了uni一键登陆,加上对比了阿里云的号码认证服务的费用,相比之下uni一键登陆会更加划算一点),与一键登陆服务。

        开通uni一键登陆可以参考官方文档

        以上前置条件完成后我们可以继续向下开发。

流程图
流程图
  1. App界面弹出请求授权,询问用户是否同意授权该App获取手机号。这个授权请求界面是运营商sdk弹出的,可以有限定制。
  2. 用户同意授权后,SDK底层访问运营商网关鉴权,获得当前设备access_token等信息。
  3. 在服务器侧通过 uniCloud 将access_token等信息 置换为当前设备的真实手机号码。然后服务器直接入库,避免手机号传递到前端发生的不可信情况。

用户侧想要正常使用此功能有前置条件:

  • 手机安装有sim卡
  • 手机开启数据流量(与wifi无关,不要求关闭wifi,但数据流量不能禁用。)
  • 开通uniCloud服务(但不要求所有后台代码都使用uniCloud)

先说我们代码的实现流程:

注意:HarmonyOS 不支持,请使用uni.getUniverifyManager,因为我们项目暂时不涉及HarmonyOS,所以本文章也不会有HarmonyOS系统的一键登陆功能。

1、app在onReady生命周期内使用uni.getProvider获取当前app平台中可用的服务商(当前环境中是否存在一键登陆的sdk)

// #ifdef APP
onReady() {
    this.examineLogin() //检查是否存在一键登陆的sdk
},
// #endif

2、在第一步中若是支持一键登陆的sdk,那么就可以选择是否预登陆。如果有预登陆的话会提高用户侧授权界面的速度。

examineLogin() {
	var _this = this
	uni.getProvider({
		service: 'oauth',
		success(res) {
			if (~res.provider.indexOf('univerify')) {
				uni.preLogin({
					provider: 'univerify',
					success() { //预登录成功
						// 此处可以编写预登陆成功后你的app做出的反应
                        // 例如:我们的app预登陆成功后,app会临时关闭账号密码登陆功能,只显示一键登陆的按钮,如果用户在后面的授权界面取消之后,再会把账号密码登陆区域重新展示出来。
					},
					fail(res) { // 预登录失败
						// 不显示一键登录选项(或置灰)
						// 根据错误信息判断失败原因,如有需要可将错误提交给统计服务器
					}
				})
			}
		}
	});
},

3、使用uni.login弹出用户授权界面。

uni.login({
	provider: 'univerify',
	univerifyStyle: { // 自定义登录框样式
    //参考`univerifyStyle 数据结构`
  },
	success(res){ // 登录成功
		console.log(res.authResult);  // {openid:'登录授权唯一标识',access_token:'接口返回的 token'}
	},
	fail(res){  // 登录失败
		console.log(res.errCode)
		console.log(res.errMsg)
	}
})

请求登录认证操作完成后,不管成功或失败都不会关闭一键登录界面,需要主动调用closeAuthView方法关闭,建议把uni.closeAuthView()加到finally中。

univerifyStyle 数据结构:

HamronyOS 仅支持 fullScreen(是否全屏显示)logoPath(自定义 logo 地址)backgroundColor(背景颜色)loginBtnText(登录按钮文本)

{
    "fullScreen": false, // 是否全屏显示,默认值: false
    "backgroundColor": "#ffffff",  // 授权页面背景颜色,默认值:#ffffff
    "backgroundImage": "", // 全屏显示的背景图片,默认值:"" (仅支持本地图片,只有全屏显示时支持)
    "icon": {
        "path": "static/xxx.png", // 自定义显示在授权框中的logo,仅支持本地图片 默认显示App logo
        "width":  "60px",  //图标宽度 默认值:60px
        "height": "60px"   //图标高度 默认值:60px
    },
    "closeIcon": {
        "path": "static/xxx.png", // 自定义显示在授权框中的logo,仅支持本地图片
        "width":  "60px",  //图标宽度 默认值:60px (HBuilderX 4.0+ 仅iOS支持)
        "height": "60px"   //图标高度 默认值:60px (HBuilderX 4.0+ 仅iOS支持)
    },
    "phoneNum": {
        "color": "#202020"  // 手机号文字颜色 默认值:#202020
    },
    "slogan": {
        "color": "#BBBBBB"  //  slogan 字体颜色 默认值:#BBBBBB
    },
    "authButton": {
        "normalColor": "#3479f5", // 授权按钮正常状态背景颜色 默认值:#3479f5
        "highlightColor": "#2861c5",  // 授权按钮按下状态背景颜色 默认值:#2861c5(仅ios支持)
        "disabledColor": "#73aaf5",  // 授权按钮不可点击时背景颜色 默认值:#73aaf5(仅ios支持)
        "textColor": "#ffffff",  // 授权按钮文字颜色 默认值:#ffffff
        "title": "本机号码一键登录", // 授权按钮文案 默认值:“本机号码一键登录”
        "borderRadius": "24px"	// 授权按钮圆角 默认值:"24px" (按钮高度的一半)
    },
    "otherLoginButton": {
        "visible": true, // 是否显示其他登录按钮,默认值:true
        "normalColor": "", // 其他登录按钮正常状态背景颜色 默认值:透明
        "highlightColor": "", // 其他登录按钮按下状态背景颜色 默认值:透明
        "textColor": "#656565", // 其他登录按钮文字颜色 默认值:#656565
        "title": "其他登录方式", // 其他登录方式按钮文字 默认值:“其他登录方式”
        "borderColor": "",  //边框颜色 默认值:透明(仅iOS支持)
        "borderRadius": "0px" // 其他登录按钮圆角 默认值:"24px" (按钮高度的一半)
    },
    "privacyTerms": {
        "defaultCheckBoxState":true, // 条款勾选框初始状态 默认值: true
        "isCenterHint":false, //未勾选服务条款时点击登录按钮的提示是否居中显示 默认值: false (3.7.13+ 版本支持)
        "uncheckedImage":"", // 可选 条款勾选框未选中状态图片(仅支持本地图片 建议尺寸 24x24px)(3.2.0+ 版本支持)
        "checkedImage":"", // 可选 条款勾选框选中状态图片(仅支持本地图片 建议尺寸24x24px)(3.2.0+ 版本支持)
        "checkBoxSize":12, // 可选 条款勾选框大小
        "textColor": "#BBBBBB", // 文字颜色 默认值:#BBBBBB
        "termsColor": "#5496E3", //  协议文字颜色 默认值: #5496E3
        "prefix": "我已阅读并同意", // 条款前的文案 默认值:“我已阅读并同意”
        "suffix": "并使用本机号码登录", // 条款后的文案 默认值:“并使用本机号码登录”
        "privacyItems": [  // 自定义协议条款,最大支持2个,需要同时设置url和title. 否则不生效
            {
                "url": "https://", // 点击跳转的协议详情页面
                "title": "用户服务协议" // 协议名称
            }
        ]
    },
    "buttons": {  // 自定义页面下方按钮仅全屏模式生效(3.1.14+ 版本支持)
        "iconWidth": "45px", // 图标宽度(高度等比例缩放) 默认值:45px
        "list": [
            {
                "provider": "apple",
                "iconPath": "/static/apple.png" // 图标路径仅支持本地图片
            },
            {
                "provider": "weixin",
                "iconPath": "/static/wechat.png" // 图标路径仅支持本地图片
            }
        ]
    }
}

uni.login返回的数据示例:

{
	"errMsg": "login:ok",
	"authResult": {
		"openid": "208E2FBE6EF64DF3B2D377D886EF2A14124262bfd7ae16465ea0f0634554dcee7636",
		"access_token": "ZGI4NjkxZWE4YjAyNGUzMjhiMmZiNDcwODBjYjc5MDF8fDJ8djJ8Mg=="
	}
}

我们根据uni.login返回的openid与access_token来换取用户的手机号码。

1、先创建一个unicloud云函数,因为我们要使用云函数中的uniCloud.getPhoneNumber方法来换取用户真实的手机号码。ps:我目前不知道除了这个方法以外还有什么其他方法可以换取到用户真实的手机号码。

官方云函数代码示例:

'use strict';
exports.main = async (event, context) => {
  // event里包含着客户端提交的参数
  const res = await uniCloud.getPhoneNumber({
  	appid: '_UNI_ABCDEFG', // 替换成自己开通一键登录的应用的DCloud appid
  	provider: 'univerify',
  	access_token: event.access_token,
  	openid: event.openid
  })

  console.log(res); // res里包含手机号
  // 执行用户信息入库等操作,正常情况下不要把完整手机号返回给前端
  // 如果数据库在uniCloud上,可以直接入库
  // 如果数据库不在uniCloud上,可以通过 uniCloud.httpclient API,将手机号通过http方式传递给其他服务器的接口,详见:https://doc.dcloud.net.cn/uniCloud/cf-functions?id=httpclient
  return {
    code: 0,
    message: '获取手机号成功'
  }
}

我们对此云函数进行了修改,以下是我们云函数的逻辑:

  • 我们开启了dcloud安全网络,所以在云函数内也加入了安全校验。但是后面发现开启了安全网络之后我们部署到云空间的uniadmin就无法访问了,所以后面我们也就关闭了安全网络。
  • 用户在app端调用云函数的时候会传入uni.login返回的openid、access_token和额外传入的baseApi,baseApi需要在云函数内拼接业务请求url。你们可根据自己的业务进行调整。
  • 在云函数内会先请求业务地址来拿取到一个登陆标识,1分钟内有效。

  • 使用云函数的uniCloud.getPhoneNumber方法来换取到用户真实的手机号码,我们再根据用户真实的手机号码与生成的登陆标识来进行登陆,换取业务token。

  • 注:此处仅是对我们业务的代码说明,仅供参考,你们可根据自己的业务逻辑进行调整,核心就是使用uniCloud.getPhoneNumber方法来换取用户手机号码,至于是否返回给前端就着重考虑一下。

云函数完整代码,写完记得及时上传部署。

'use strict';
exports.main = async (event, context) => {
	//event为客户端上传的参数
	console.log('event : ', event)

	try {
		const secretType = context.secretType
		// secretType 是客户端调用 uniCloud.callFunction 传递的参数 secretType

		if (secretType !== 'both' && secretType !== 'response') {
			throw new Error('secretType invalid') // 拒绝返回有效数据
		}

		// 生成登陆标识URL
		const generateLoginLabelUrl = event.baseApi + '/app/generateLoginIdentifier'
		const generateLoginLabel = await uniCloud.httpclient.request(generateLoginLabelUrl, {
			method: 'POST',
			contentType: 'json', // 指定以application/json发送data内的数据
			dataType: 'json' // 指定返回值为json格式,自动进行parse
		})
		
		console.log('登陆标识', generateLoginLabel.data.data)

		// 检查生成登录标识是否成功
		if (!generateLoginLabel.data || !generateLoginLabel.data.data) {
			throw new Error('Failed to generate login identifier')
		}

		// 登陆标识登陆URL
		const loginLabelLoginUrl = event.baseApi + '/app/identifierLogin'

		const res = await uniCloud.getPhoneNumber({
			appid: '', // 替换成自己开通一键登录的应用的DCloud appid
			provider: 'univerify',
			access_token: event.access_token,
			openid: event.openid
		})

		console.log('用户手机号:' + res.phoneNumber); // res里包含手机号
		
		// 检查获取手机号是否成功
		if (!res || !res.phoneNumber) {
			throw new Error('Failed to get phone number')
		}
		
		// 执行登录操作并等待结果
		const loginResponse = await uniCloud.httpclient.request(loginLabelLoginUrl, {
			method: 'POST',
			data: {
				username: res.phoneNumber,
				loginIdentifier: generateLoginLabel.data.data
			},
			contentType: 'json', // 指定以application/json发送data内的数据
			dataType: 'json' // 指定返回值为json格式,自动进行parse
		})
		
		console.log('登陆结果', loginResponse.data)
		
		// 返回登录结果
		return loginResponse.data
	} catch (error) {
		console.error('登录过程出错:', error)
		throw error
	}
};

贴上app端完整的业务代码:

// #ifdef APP
onReady() {
	this.examineLogin() //预登陆
},
// #endif

methods: {
// 预登陆
examineLogin() {
	var _this = this
	uni.getProvider({
		service: 'oauth',
		success(res) {
			if (~res.provider.indexOf('univerify')) {
				uni.preLogin({
					provider: 'univerify',
					success() { //预登录成功
						_this.preLogin = true
						_this.type = 1
						setTimeout(() => {
						 	_this.noPasswordLogin()
						}, 1500)
					},
					fail(res) { // 预登录失败
						// 不显示一键登录选项(或置灰)
						// 根据错误信息判断失败原因,如有需要可将错误提交给统计服务器
					}
				})
			}
		}
	});
},

// 免密登陆
noPasswordLogin() {
	var _this = this
	uni.login({
		provider: 'univerify',
		univerifyStyle: { // 自定义登录框样式
			//参考`univerifyStyle 数据结构`
			icon: {
				path: this.$config.logo,
			},
			phoneNum: {
				color: '#383838'
			},
			slogan: {
				color: '#808080'
			},
			authButton: {
				normalColor: '#007AFF',
				highlightColor: '#B2D7FF',
				borderRadius: '8px'
			},
			otherLoginButton: {
				textColor: '#808080'
			},
			privacyTerms: {
				uncheckedImage: `/static/icon/auth/uncheckedImage.png`,
				checkedImage: `/static/icon/auth/checkedImage.png`
			},
		},
		success(res) { // 登录成功
			uni.showLoading({
				title: '正在登陆',
				mask: true
			})
			uniCloud.callFunction({
				name: 'phoneLogin',
				data: {
					openid: res.authResult.openid,
					access_token: res.authResult.access_token,
					baseApi: _this.$config.baseApi
				},
				secretType: 'both'
			}).then((response) => {
				var data = response.result
				if (data.code !== 200) {
					uni.$u.showModal(data.msg)
				} else {
					uni.setStorage({
						key: 'token',
						data: data.token,
						success() {
							uni.setStorage({
								key: 'userInfo',
								data: data.user,
								success() {
									_this.navTo('/pages/TabBar/home', 'RH')
									// 获取OSS配置
									_this.initializeOSSConfig();
								}
							})
						}
					})
				}
			}).catch(err => {
				this.Cerror('云函数请求错误', err)
			}).finally(() => {
				uni.hideLoading()
				// 客户端关闭一键登录授权界面,需要在业务端完成登陆逻辑后通知客户端关闭登陆界面
				uni.closeAuthView()
			})
		},
		fail(res) { // 登录失败
			this.Cerror(res)
			uni.hideLoading()
			// 客户端关闭一键登录授权界面,需要在业务端完成登陆逻辑后通知客户端关闭登陆界面
			uni.closeAuthView()
		}
	})
},
}

以上仅供参考,欢迎交流学习。

Logo

网易易盾是国内领先的数字内容风控服务商,依托网易二十余年的先进技术和一线实践经验沉淀,为客户提供专业可靠的安全服务,涵盖内容安全、业务安全、应用安全、安全专家服务四大领域,全方位保障客户业务合规、稳健和安全运营。

更多推荐