import { Injectable, Logger } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import * as crypto from 'crypto'; /** * 二要素验证结果 */ export interface IdCardVerificationResult { success: boolean; errorMessage?: string; rawResponse?: Record; } /** * 人脸活体认证初始化结果 */ export interface FaceVerifyInitResult { success: boolean; certifyId?: string; // 认证ID,用于客户端SDK certifyUrl?: string; // H5认证链接(可选) errorMessage?: string; rawResponse?: Record; } /** * 人脸活体认证查询结果 */ export interface FaceVerifyQueryResult { success: boolean; passed: boolean; // 是否通过认证 status: 'PENDING' | 'PASSED' | 'FAILED' | 'EXPIRED'; errorMessage?: string; rawResponse?: Record; } /** * OCR识别结果 */ export interface IdCardOcrResult { success: boolean; name?: string; idNumber?: string; address?: string; ethnicity?: string; birthDate?: string; sex?: string; issueAuthority?: string; // 签发机关 validPeriod?: string; // 有效期限 errorMessage?: string; rawResponse?: Record; } /** * 阿里云实人认证服务 - 支持三层认证 * * 层级1: 实名认证 - 三要素验证(姓名+身份证号+手机号) * 层级2: 实人认证 - 人脸活体检测 * 层级3: KYC - 证件照OCR识别验证 * * 文档: https://help.aliyun.com/product/60032.html */ @Injectable() export class AliyunKycProvider { private readonly logger = new Logger(AliyunKycProvider.name); private readonly accessKeyId: string; private readonly accessKeySecret: string; private readonly enabled: boolean; private readonly endpoint: string; private readonly sceneId: string; constructor(private readonly configService: ConfigService) { this.accessKeyId = this.configService.get('ALIYUN_ACCESS_KEY_ID', ''); this.accessKeySecret = this.configService.get('ALIYUN_ACCESS_KEY_SECRET', ''); this.enabled = this.configService.get('ALIYUN_KYC_ENABLED', false); this.endpoint = this.configService.get('ALIYUN_KYC_ENDPOINT', 'cloudauth.aliyuncs.com'); this.sceneId = this.configService.get('ALIYUN_KYC_SCENE_ID', ''); if (this.enabled && (!this.accessKeyId || !this.accessKeySecret)) { this.logger.warn('[AliyunKYC] KYC is enabled but credentials are not configured'); } } /** * ======================================== * 层级1: 实名认证 - 三要素验证 * ======================================== * 验证姓名、身份证号和手机号是否匹配 */ async verifyIdCard( realName: string, idCardNumber: string, phoneNumber: string, requestId: string, ): Promise { this.logger.log(`[AliyunKYC] [Level1] Starting ID card verification (3-factor), requestId: ${requestId}`); // 开发/测试环境:模拟验证 if (!this.enabled) { this.logger.warn('[AliyunKYC] KYC is disabled, using mock verification'); return this.mockIdCardVerification(realName, idCardNumber, phoneNumber); } try { // 调用阿里云身份三要素核验 API const params = { Action: 'VerifyMaterial', Version: '2019-03-07', Format: 'JSON', BizType: 'ID_CARD_THREE', Name: realName, IdCardNumber: idCardNumber, PhoneNumber: phoneNumber, }; const response = await this.callAliyunApi(params); if (response.Code === 'OK' || response.Code === '200') { this.logger.log(`[AliyunKYC] [Level1] Verification SUCCESS for requestId: ${requestId}`); return { success: true, rawResponse: response, }; } else { this.logger.warn(`[AliyunKYC] [Level1] Verification FAILED: ${response.Message}`); return { success: false, errorMessage: this.mapErrorMessage(response.Code, response.Message), rawResponse: response, }; } } catch (error) { this.logger.error(`[AliyunKYC] [Level1] API call failed: ${error.message}`, error.stack); return { success: false, errorMessage: '实名认证服务暂时不可用', rawResponse: { error: error.message }, }; } } /** * ======================================== * 层级2: 实人认证 - 初始化人脸活体检测 * ======================================== * 返回认证ID供客户端SDK使用 */ async initFaceVerify( userId: string, realName: string, idCardNumber: string, returnUrl?: string, ): Promise { this.logger.log(`[AliyunKYC] [Level2] Initializing face verify for user: ${userId}`); if (!this.enabled) { this.logger.warn('[AliyunKYC] KYC is disabled, using mock face verify init'); return this.mockFaceVerifyInit(userId); } try { const outerOrderNo = `FACE_${userId}_${Date.now()}`; // 调用阿里云金融级实人认证初始化 API const params = { Action: 'InitFaceVerify', Version: '2019-03-07', Format: 'JSON', SceneId: this.sceneId, OuterOrderNo: outerOrderNo, ProductCode: 'ID_PLUS', // 金融级实人认证 CertType: 'IDENTITY_CARD', CertName: realName, CertNo: idCardNumber, ReturnUrl: returnUrl || '', MetaInfo: JSON.stringify({ zimVer: '3.0.0', appVersion: '1.0.0', bioMetaInfo: 'mock', }), }; const response = await this.callAliyunApi(params); if (response.Code === 'OK' || response.Code === '200') { const resultObject = response.ResultObject || {}; this.logger.log(`[AliyunKYC] [Level2] Init SUCCESS, certifyId: ${resultObject.CertifyId}`); return { success: true, certifyId: resultObject.CertifyId, certifyUrl: resultObject.CertifyUrl, rawResponse: response, }; } else { this.logger.warn(`[AliyunKYC] [Level2] Init FAILED: ${response.Message}`); return { success: false, errorMessage: this.mapErrorMessage(response.Code, response.Message), rawResponse: response, }; } } catch (error) { this.logger.error(`[AliyunKYC] [Level2] Init failed: ${error.message}`, error.stack); return { success: false, errorMessage: '实人认证服务暂时不可用', rawResponse: { error: error.message }, }; } } /** * ======================================== * 层级2: 实人认证 - 查询认证结果 * ======================================== */ async queryFaceVerify(certifyId: string): Promise { this.logger.log(`[AliyunKYC] [Level2] Querying face verify result, certifyId: ${certifyId}`); if (!this.enabled) { this.logger.warn('[AliyunKYC] KYC is disabled, using mock face verify query'); return this.mockFaceVerifyQuery(certifyId); } try { const params = { Action: 'DescribeFaceVerify', Version: '2019-03-07', Format: 'JSON', SceneId: this.sceneId, CertifyId: certifyId, }; const response = await this.callAliyunApi(params); if (response.Code === 'OK' || response.Code === '200') { const resultObject = response.ResultObject || {}; const passed = resultObject.Passed === 'T' || resultObject.Passed === true; this.logger.log(`[AliyunKYC] [Level2] Query result: passed=${passed}`); return { success: true, passed, status: passed ? 'PASSED' : 'FAILED', rawResponse: response, }; } else { return { success: false, passed: false, status: 'FAILED', errorMessage: this.mapErrorMessage(response.Code, response.Message), rawResponse: response, }; } } catch (error) { this.logger.error(`[AliyunKYC] [Level2] Query failed: ${error.message}`, error.stack); return { success: false, passed: false, status: 'FAILED', errorMessage: '查询认证结果失败', rawResponse: { error: error.message }, }; } } /** * ======================================== * 层级3: KYC - 证件照OCR识别 * ======================================== */ async ocrIdCard( imageUrl: string, side: 'front' | 'back', ): Promise { this.logger.log(`[AliyunKYC] [Level3] Starting ID card OCR, side: ${side}`); if (!this.enabled) { this.logger.warn('[AliyunKYC] KYC is disabled, using mock OCR'); return this.mockIdCardOcr(side); } try { const params = { Action: 'RecognizeIdCard', Version: '2019-03-07', Format: 'JSON', Side: side === 'front' ? 'face' : 'back', ImageUrl: imageUrl, }; const response = await this.callAliyunApi(params); if (response.Code === 'OK' || response.Code === '200') { const data = response.Data || {}; this.logger.log(`[AliyunKYC] [Level3] OCR SUCCESS`); if (side === 'front') { return { success: true, name: data.Name, idNumber: data.IdNumber, address: data.Address, ethnicity: data.Ethnicity, birthDate: data.BirthDate, sex: data.Sex, rawResponse: response, }; } else { return { success: true, issueAuthority: data.Issue, validPeriod: data.ValidPeriod || `${data.StartDate}-${data.EndDate}`, rawResponse: response, }; } } else { return { success: false, errorMessage: this.mapErrorMessage(response.Code, response.Message), rawResponse: response, }; } } catch (error) { this.logger.error(`[AliyunKYC] [Level3] OCR failed: ${error.message}`, error.stack); return { success: false, errorMessage: '证件识别服务暂时不可用', rawResponse: { error: error.message }, }; } } // ============ 私有方法 ============ /** * 调用阿里云 API (签名方式) */ private async callAliyunApi(params: Record): Promise { const timestamp = new Date().toISOString().replace(/\.\d{3}/, ''); const nonce = crypto.randomUUID(); const commonParams: Record = { AccessKeyId: this.accessKeyId, Timestamp: timestamp, SignatureMethod: 'HMAC-SHA1', SignatureVersion: '1.0', SignatureNonce: nonce, ...params, }; // 计算签名 const signature = this.calculateSignature(commonParams); commonParams['Signature'] = signature; // 发起请求 const queryString = Object.entries(commonParams) .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`) .join('&'); const url = `https://${this.endpoint}/?${queryString}`; const response = await fetch(url, { method: 'GET', headers: { 'Content-Type': 'application/json', }, }); return response.json(); } /** * 计算阿里云 API 签名 */ private calculateSignature(params: Record): string { const sortedKeys = Object.keys(params).sort(); const canonicalizedQueryString = sortedKeys .map((k) => `${this.percentEncode(k)}=${this.percentEncode(params[k])}`) .join('&'); const stringToSign = `GET&${this.percentEncode('/')}&${this.percentEncode(canonicalizedQueryString)}`; const hmac = crypto.createHmac('sha1', `${this.accessKeySecret}&`); hmac.update(stringToSign); return hmac.digest('base64'); } private percentEncode(str: string): string { return encodeURIComponent(str) .replace(/\+/g, '%20') .replace(/\*/g, '%2A') .replace(/~/g, '%7E'); } /** * 映射错误消息 */ private mapErrorMessage(code: string, message: string): string { const errorMap: Record = { 'InvalidParameter': '参数格式错误', 'IdCardNotMatch': '姓名与身份证号不匹配', 'IdCardNotExist': '身份证号不存在', 'IdCardExpired': '身份证已过期', 'FaceNotMatch': '人脸比对不通过', 'LivenessCheckFail': '活体检测失败', 'SystemError': '系统错误,请稍后重试', }; return errorMap[code] || message || '认证失败'; } // ============ Mock 方法 (开发/测试环境) ============ private mockIdCardVerification(realName: string, idCardNumber: string, phoneNumber: string): IdCardVerificationResult { this.logger.log('[AliyunKYC] Using mock ID card verification (3-factor)'); // 基本格式验证 if (!realName || realName.length < 2) { return { success: false, errorMessage: '姓名格式不正确', rawResponse: { mock: true, reason: 'invalid_name' }, }; } // 身份证号格式验证 const idCardRegex = /^[1-9]\d{5}(18|19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dXx]$/; if (!idCardRegex.test(idCardNumber)) { return { success: false, errorMessage: '身份证号格式不正确', rawResponse: { mock: true, reason: 'invalid_id_card_format' }, }; } // 校验码验证 if (!this.validateIdCardChecksum(idCardNumber)) { return { success: false, errorMessage: '身份证号校验码不正确', rawResponse: { mock: true, reason: 'invalid_checksum' }, }; } // 手机号格式验证 const phoneRegex = /^1[3-9]\d{9}$/; if (!phoneNumber || !phoneRegex.test(phoneNumber)) { return { success: false, errorMessage: '手机号格式不正确', rawResponse: { mock: true, reason: 'invalid_phone' }, }; } return { success: true, rawResponse: { mock: true, verifyTime: new Date().toISOString(), verifyType: '3-factor' }, }; } private mockFaceVerifyInit(userId: string): FaceVerifyInitResult { this.logger.log('[AliyunKYC] Using mock face verify init'); const mockCertifyId = `MOCK_CERTIFY_${userId}_${Date.now()}`; return { success: true, certifyId: mockCertifyId, certifyUrl: `https://mock.aliyun.com/face-verify?certifyId=${mockCertifyId}`, rawResponse: { mock: true }, }; } private mockFaceVerifyQuery(certifyId: string): FaceVerifyQueryResult { this.logger.log('[AliyunKYC] Using mock face verify query'); // 模拟环境中,假设所有以 MOCK_ 开头的认证都通过 const passed = certifyId.startsWith('MOCK_'); return { success: true, passed, status: passed ? 'PASSED' : 'PENDING', rawResponse: { mock: true }, }; } private mockIdCardOcr(side: 'front' | 'back'): IdCardOcrResult { this.logger.log('[AliyunKYC] Using mock ID card OCR'); if (side === 'front') { return { success: true, name: '测试用户', idNumber: '110101199001011234', address: '北京市东城区测试街道1号', ethnicity: '汉', birthDate: '1990-01-01', sex: '男', rawResponse: { mock: true }, }; } else { return { success: true, issueAuthority: '北京市公安局东城分局', validPeriod: '2020.01.01-2040.01.01', rawResponse: { mock: true }, }; } } private validateIdCardChecksum(idCard: string): boolean { if (idCard.length !== 18) return false; const weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]; const checksumChars = '10X98765432'; let sum = 0; for (let i = 0; i < 17; i++) { sum += parseInt(idCard[i], 10) * weights[i]; } const expectedChecksum = checksumChars[sum % 11]; const actualChecksum = idCard[17].toUpperCase(); return expectedChecksum === actualChecksum; } }