提交 c499600d 编写于 作者: C chenruilong

feat(uni-id-co): 新增 实人认证相关接口

上级 118ea01d
......@@ -8,8 +8,11 @@ const deviceCollectionName = 'uni-id-device'
const deviceCollection = db.collection(deviceCollectionName)
const openDataCollectionName = 'opendb-open-data'
const openDataCollection = db.collection(openDataCollectionName)
const frvLogsCollectionName = 'opendb-frv-logs'
const frvLogsCollection = db.collection(frvLogsCollectionName)
const USER_IDENTIFIER = {
_id: 'uid',
username: 'username',
mobile: 'mobile',
email: 'email',
......@@ -22,7 +25,8 @@ const USER_IDENTIFIER = {
'qq_openid.app': 'qq-account',
'qq_openid.mp': 'qq-account',
ali_openid: 'alipay-account',
apple_openid: 'alipay-account'
apple_openid: 'alipay-account',
identities: 'idp'
}
const USER_STATUS = {
......@@ -76,6 +80,15 @@ const EMAIL_SCENE = {
BIND_EMAIL: 'bind-email'
}
const REAL_NAME_STATUS = {
NOT_CERTIFIED: 0,
WAITING_CERTIFIED: 1,
CERTIFIED: 2,
CERTIFY_FAILED: 3
}
const EXTERNAL_DIRECT_CONNECT_PROVIDER = 'externalDirectConnect'
module.exports = {
db,
dbCmd,
......@@ -83,10 +96,13 @@ module.exports = {
verifyCollection,
deviceCollection,
openDataCollection,
frvLogsCollection,
USER_IDENTIFIER,
USER_STATUS,
CAPTCHA_SCENE,
LOG_TYPE,
SMS_SCENE,
EMAIL_SCENE
EMAIL_SCENE,
REAL_NAME_STATUS,
EXTERNAL_DIRECT_CONNECT_PROVIDER
}
......@@ -38,7 +38,15 @@ const ERROR = {
UNBIND_PASSWORD_NOT_EXISTS: 'uni-id-unbind-password-not-exists',
UNBIND_MOBILE_NOT_EXISTS: 'uni-id-unbind-mobile-not-exists',
UNSUPPORTED_REQUEST: 'uni-id-unsupported-request',
ILLEGAL_REQUEST: 'uni-id-illegal-request'
ILLEGAL_REQUEST: 'uni-id-illegal-request',
FRV_FAIL: 'uni-id-frv-fail',
FRV_PROCESSING: 'uni-id-frv-processing',
REAL_NAME_VERIFIED: 'uni-id-realname-verified',
ID_CARD_EXISTS: 'uni-id-idcard-exists',
INVALID_ID_CARD: 'uni-id-invalid-idcard',
INVALID_REAL_NAME: 'uni-id-invalid-realname',
UNKNOWN_ERROR: 'uni-id-unknown-error',
REAL_NAME_VERIFY_UPPER_LIMIT: 'uni-id-realname-verify-upper-limit'
}
function isUniIdError (errCode) {
......
const crypto = require('crypto')
function checkSecret (secret) {
if (!secret) {
throw {
errCode: '请在config.json中配置sensitiveInfoEncryptSecret字段'
}
}
if (secret.length < 32) {
throw {
errCode: 'sensitiveInfoEncryptSecret字段长度不能小于32位'
}
}
}
function encryptData (text = '') {
if (!text) return text
const encryptSecret = this.config.sensitiveInfoEncryptSecret
checkSecret(encryptSecret)
const iv = encryptSecret.slice(-16)
const cipher = crypto.createCipheriv('aes-256-cbc', encryptSecret, iv)
const encrypted = Buffer.concat([
cipher.update(Buffer.from(text, 'utf-8')),
cipher.final()
])
return encrypted.toString('base64')
}
function decryptData (text = '') {
if (!text) return text
const encryptSecret = this.config.sensitiveInfoEncryptSecret
checkSecret(encryptSecret)
const iv = encryptSecret.slice(-16)
const cipher = crypto.createDecipheriv('aes-256-cbc', encryptSecret, iv)
const decrypted = Buffer.concat([
cipher.update(Buffer.from(text, 'base64')),
cipher.final()
])
return decrypted.toString('utf-8')
}
module.exports = {
encryptData,
decryptData
}
......@@ -183,7 +183,7 @@ function isMatchUserApp (userAppList, matchAppList) {
return true
}
if (getType(userAppList) !== 'array') {
return false
return false
}
if (userAppList.includes('*')) {
return true
......@@ -194,6 +194,51 @@ function isMatchUserApp (userAppList, matchAppList) {
return userAppList.some(item => matchAppList.includes(item))
}
function checkIdCard (idCardNumber) {
if (!idCardNumber) return false
const coefficient = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]
const checkCode = [1, 0, 'x', 9, 8, 7, 6, 5, 4, 3, 2]
const code = idCardNumber.substring(17)
let sum = 0
for (let i = 0; i < 17; i++) {
sum += Number(idCardNumber.charAt(i)) * coefficient[i]
}
return checkCode[sum % 11].toString() === code.toLowerCase()
}
function catchAwait (fn, finallyFn) {
if (!fn) return [new Error('no function')]
return fn
.then((data) => [undefined, data])
.catch((error) => [error])
.finally(() => typeof finallyFn === 'function' && finallyFn())
}
function dataDesensitization (value = '', options = {}) {
const { onlyLast = false } = options
const [firstIndex, middleIndex, lastIndex] = onlyLast ? [0, 0, -1] : [0, 1, -1]
if (!value) return value
const first = value.slice(firstIndex, middleIndex)
const middle = value.slice(middleIndex, lastIndex)
const last = value.slice(lastIndex)
const star = Array.from(new Array(middle.length), (v) => '*').join('')
return first + star + last
}
function getCurrentDate () {
const date = new Date()
const year = date.getFullYear()
const month = date.getMonth() + 1
const day = date.getDate()
return new Date(`${year}/${month}/${day}`)
}
module.exports = {
getType,
isValidString,
......@@ -210,5 +255,9 @@ module.exports = {
getVerifyCode,
coverMobile,
getNonceStr,
isMatchUserApp
isMatchUserApp,
checkIdCard,
catchAwait,
dataDesensitization,
getCurrentDate
}
......@@ -77,5 +77,14 @@ module.exports = {
},
setPwd: {
auth: true
},
getFrvCertifyId: {
auth: true
},
getFrvAuthResult: {
auth: true
},
getRealNameInfo: {
auth: true
}
}
const uniIdCommon = require('uni-id-common')
const uniCaptcha = require('uni-captcha')
const {
getType
getType,
checkIdCard
} = require('./common/utils')
const {
checkClientInfo,
......@@ -9,7 +10,8 @@ const {
} = require('./common/validator')
const ConfigUtils = require('./lib/utils/config')
const {
isUniIdError
isUniIdError,
ERROR
} = require('./common/error')
const middleware = require('./middleware/index')
const universal = require('./common/universal')
......@@ -55,7 +57,8 @@ const {
resetPwdBySms,
resetPwdByEmail,
closeAccount,
getAccountInfo
getAccountInfo,
getRealNameInfo
} = require('./module/account/index')
const {
createCaptcha,
......@@ -80,11 +83,15 @@ const {
const {
getSupportedLoginType
} = require('./module/dev/index')
const {
externalRegister,
externalLogin
externalLogin,
updateUserInfoByExternal
} = require('./module/external')
const {
getFrvCertifyId,
getFrvAuthResult
} = require('./module/facial-recognition-verify')
module.exports = {
async _before () {
......@@ -131,6 +138,22 @@ module.exports = {
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 (!/^[\u4e00-\u9fa5]+$/.test(realName)) {
return {
errCode: ERROR.INVALID_REAL_NAME
}
}
})
/**
* 示例:覆盖密码验证规则
*/
......@@ -602,15 +625,61 @@ module.exports = {
*/
setPwd,
/**
* 外部用户注册,将自身系统的用户账号导入uniId,为其创建一个对应uniId的账号(unieid),使得该账号可以使用依赖uniId的系统及功能。
* 外部注册用户
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#external-register
* @returns
* @param {object} params
* @param {string} params.externalUid 业务系统的用户id
* @param {string} params.nickname 昵称
* @param {string} params.gender 性别
* @param {string} params.avatar 头像
* @returns {object}
*/
externalRegister,
/**
* 外部用户登录,使用unieid即可登录
* 外部用户登录
* @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
* */
externalLogin
*/
getRealNameInfo
}
......@@ -43,7 +43,15 @@ const sentence = {
'uni-id-unbind-mobile-not-exists': 'This is the only way to login at the moment, please bind your phone number and then try to unbind',
'uni-id-unbind-password-not-exists': 'Please set a password first',
'uni-id-unsupported-request': 'Unsupported request',
'uni-id-illegal-request': 'Illegal request'
'uni-id-illegal-request': 'Illegal request',
'uni-id-frv-fail': 'Real name certify failed',
'uni-id-frv-processing': 'Waiting for face recognition',
'uni-id-realname-verified': 'This account has been verified',
'uni-id-idcard-exists': 'The ID number has been bound to the account',
'uni-id-invalid-idcard': 'ID number is invalid',
'uni-id-invalid-realname': 'The name can only be Chinese characters',
'uni-id-unknown-error': 'unknown error',
'uni-id-realname-verify-upper-limit': 'The number of real-name certify on the day has reached the upper limit'
}
module.exports = {
......
......@@ -43,7 +43,15 @@ const sentence = {
'uni-id-unbind-mobile-not-exists': '这是当前唯一登录方式,请绑定手机号后再尝试解绑',
'uni-id-unbind-password-not-exists': '请先设置密码在尝试解绑',
'uni-id-unsupported-request': '不支持的请求方式',
'uni-id-illegal-request': '非法请求'
'uni-id-illegal-request': '非法请求',
'uni-id-frv-fail': '实名认证失败',
'uni-id-frv-processing': '等待人脸识别',
'uni-id-realname-verified': '该账号已实名认证',
'uni-id-idcard-exists': '该证件号码已绑定账号',
'uni-id-invalid-idcard': '身份证号码不合法',
'uni-id-invalid-realname': '姓名只能是汉字',
'uni-id-unknown-error': '未知错误',
'uni-id-realname-verify-upper-limit': '当日实名认证次数已达上限'
}
module.exports = {
......
const { userCollection } = require('../../common/constants')
const { ERROR } = require('../../common/error')
const { decryptData } = require('../../common/sensitive-aes-cipher')
const { dataDesensitization } = require('../../common/utils')
/**
* 获取实名信息
* @tutorial https://uniapp.dcloud.net.cn/uniCloud/uni-id-pages.html#get-realname-info
* @param {Object} params
* @param {Boolean} params.decryptData 是否解密数据
* @returns
*/
module.exports = async function (params = {}) {
const schema = {
decryptData: {
required: false,
type: 'boolean'
}
}
this.middleware.validate(params, schema)
const { decryptData: isDecryptData = true } = params
const {
uid
} = this.authInfo
const getUserRes = await userCollection.doc(uid).get()
const userRecord = getUserRes && getUserRes.data && getUserRes.data[0]
if (!userRecord) {
throw {
errCode: ERROR.ACCOUNT_NOT_EXISTS
}
}
const { realname_auth: realNameAuth = {} } = userRecord
return {
errCode: 0,
type: realNameAuth.type,
authStatus: realNameAuth.auth_status,
realName: isDecryptData ? dataDesensitization(decryptData.call(this, realNameAuth.real_name), { onlyLast: true }) : realNameAuth.real_name,
identity: isDecryptData ? dataDesensitization(decryptData.call(this, realNameAuth.identity)) : realNameAuth.identity
}
}
......@@ -4,5 +4,6 @@ module.exports = {
resetPwdBySms: require('./reset-pwd-by-sms'),
resetPwdByEmail: require('./reset-pwd-by-email'),
closeAccount: require('./close-account'),
getAccountInfo: require('./get-account-info')
getAccountInfo: require('./get-account-info'),
getRealNameInfo: require('./get-realname-info')
}
const { userCollection, REAL_NAME_STATUS, frvLogsCollection } = require('../../common/constants')
const { dataDesensitization, catchAwait } = require('../../common/utils')
const { encryptData, decryptData } = require('../../common/sensitive-aes-cipher')
const { ERROR } = require('../../common/error')
/**
* 查询认证结果
* @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
*/
module.exports = async function (params) {
const schema = {
certifyId: 'string',
needAlivePhoto: {
required: false,
type: 'string'
}
}
this.middleware.validate(params, schema)
const { uid } = this.authInfo
const { certifyId, needAlivePhoto } = params
const user = await userCollection.doc(uid).get()
const userInfo = user.data && user.data[0]
const { realname_auth: realNameAuth = {} } = userInfo
// 已认证的用户不可再次认证
if (realNameAuth.auth_status === REAL_NAME_STATUS.CERTIFIED) {
throw {
errCode: ERROR.REAL_NAME_VERIFIED
}
}
const frvManager = uniCloud.getFacialRecognitionVerifyManager({
requestId: this.getUniCloudRequestId()
})
const [error, res] = await catchAwait(frvManager.getAuthResult({
certifyId,
needAlivePhoto
}))
if (error) {
console.log(ERROR.UNKNOWN_ERROR, 'error: ', error)
throw error
}
if (res.authState === 'PROCESSING') {
throw {
errCode: ERROR.FRV_PROCESSING
}
}
if (res.authState === 'FAIL') {
await frvLogsCollection.where({
certify_id: certifyId
}).update({
status: REAL_NAME_STATUS.CERTIFY_FAILED
})
throw {
errCode: ERROR.FRV_FAIL
}
}
if (res.authState === 'SUCCESS') {
const frvLogs = await frvLogsCollection.where({
certify_id: certifyId
}).get()
const log = frvLogs.data && frvLogs.data[0]
const updateData = {
realname_auth: {
auth_status: REAL_NAME_STATUS.CERTIFIED,
real_name: log.real_name,
identity: log.identity,
auth_date: Date.now(),
type: 0
}
}
if (res.base64Photo) {
const {
fileID
} = await uniCloud.uploadFile({
cloudPath: `user/id-card/${uid}.bin`,
fileContent: Buffer.from(encryptData.call(this, res.base64Photo))
})
updateData.realname_auth.in_hand = fileID
}
await Promise.all([
userCollection.doc(uid).update(updateData),
frvLogsCollection.where({
certify_id: certifyId
}).update({
status: REAL_NAME_STATUS.CERTIFIED
})
])
return {
errCode: 0,
authStatus: REAL_NAME_STATUS.CERTIFIED,
realName: dataDesensitization(decryptData.call(this, log.real_name), { onlyLast: true }),
identity: dataDesensitization(decryptData.call(this, log.identity))
}
}
console.log(ERROR.UNKNOWN_ERROR, 'source res: ', res)
throw {
errCode: ERROR.UNKNOWN_ERROR
}
}
const { userCollection, REAL_NAME_STATUS, frvLogsCollection, dbCmd } = require('../../common/constants')
const { ERROR } = require('../../common/error')
const { encryptData } = require('../../common/sensitive-aes-cipher')
const { getCurrentDate } = require('../../common/utils')
/**
* 获取认证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
*/
module.exports = async function (params) {
const schema = {
realName: 'realName',
idCard: 'idCard'
}
this.middleware.validate(params, schema)
const { realName: originalRealName, idCard: originalIdCard } = params
const realName = encryptData.call(this, originalRealName)
const idCard = encryptData.call(this, originalIdCard)
const { uid } = this.authInfo
const idCardCertifyLimit = this.config.idCardCertifyLimit || 1
const readNameCertifyLimit = this.config.readNameCertifyLimit || 5
const user = await userCollection.doc(uid).get()
const userInfo = user.data && user.data[0]
const { realname_auth: realNameAuth = {} } = userInfo
// 已认证的用户不可再次认证
if (realNameAuth.auth_status === REAL_NAME_STATUS.CERTIFIED) {
throw {
errCode: ERROR.REAL_NAME_VERIFIED
}
}
const idCardAccount = await userCollection.where({
realname_auth: {
type: 0,
auth_status: REAL_NAME_STATUS.CERTIFIED,
identity: idCard
}
}).get()
// 限制一个身份证可以认证几个账号
if (idCardAccount.data.length >= idCardCertifyLimit) {
throw {
errCode: ERROR.ID_CARD_EXISTS
}
}
const frvLogs = await frvLogsCollection.where({
real_name: realName,
identity: idCard,
status: REAL_NAME_STATUS.WAITING_CERTIFIED,
created_date: dbCmd.gt(Date.now() - (22 * 60 * 60 * 1000))
}).get()
// 用户发起了人脸识别但未刷脸并且 certifyId 还在有效期内就还可以用上次的 certifyId
if (frvLogs.data.length) {
const record = frvLogs.data[0]
if (realName === record.real_name && idCard === record.identity) {
return {
certifyId: record.certify_id
}
}
}
const userFrvLogs = await frvLogsCollection.where({
user_id: uid,
created_date: dbCmd.gt(getCurrentDate().getTime())
}).get()
// 限制用户每日认证次数
if (userFrvLogs.data && userFrvLogs.data.length >= readNameCertifyLimit) {
throw {
errCode: ERROR.REAL_NAME_VERIFY_UPPER_LIMIT
}
}
const frvManager = uniCloud.getFacialRecognitionVerifyManager({
requestId: this.getUniCloudRequestId()
})
const res = await frvManager.getCertifyId({
realName: originalRealName,
idCard: originalIdCard
})
await frvLogsCollection.add({
user_id: uid,
certify_id: res.certifyId,
real_name: realName,
identity: idCard,
status: REAL_NAME_STATUS.WAITING_CERTIFIED,
created_date: Date.now()
})
return {
certifyId: res.certifyId
}
}
module.exports = {
getFrvCertifyId: require('./get-certify-id'),
getFrvAuthResult: require('./get-auth-result')
}
......@@ -14,10 +14,11 @@
"uni-open-bridge-common": "file:../../../../uni-open-bridge-common/uniCloud/cloudfunctions/common/uni-open-bridge-common"
},
"extensions": {
"uni-cloud-redis": {},
"uni-cloud-sms": {},
"uni-cloud-redis": {}
"uni-cloud-verify": {}
},
"cloudfunction-config": {
"keepRunningAfterReturn": false
}
}
}
\ No newline at end of file
{
"bsonType": "object",
"permission": {
"read": "doc._id == auth.uid || 'CREATE_UNI_ID_USERS' in auth.permission",
"create": "'CREATE_UNI_ID_USERS' in auth.permission",
"update": "doc._id == auth.uid || 'UPDATE_UNI_ID_USERS' in auth.permission",
"delete": "'DELETE_UNI_ID_USERS' in auth.permission"
},
"properties": {
"_id": {
"description": "存储文档 ID(用户 ID),系统自动生成"
},
"certify_id": {
"bsonType": "string",
"description": "认证id"
},
"user_id": {
"bsonType": "string",
"description": "用户id"
},
"real_name": {
"bsonType": "string",
"description": "姓名"
},
"identity": {
"bsonType": "string",
"description": "身份证号码"
},
"status": {
"bsonType": "int",
"description": "认证状态:0 未认证 1 等待认证 2 认证通过 3 认证失败",
"maximum": 3,
"minimum": 0
},
"created_date": {
"bsonType": "timestamp",
"description": "创建时间",
"forceDefaultValue": {
"$env": "now"
}
}
},
"required": []
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册