接入微信刷脸支付(Android端独立完成)
更新: 2019年11月20日 10点47分
1.升级微信arr至2.11
2.增加扫码
目录
前提:
- 在微信开放平台申请接入获取appid (此过程省略.....)
- 下载wxpayface.apk文件装到相关机器内,下载地址:下载地址
- 获取微信支付jia包,这里微信提供的是aar包,下载地址:下载地址(导入到libs下引用即可)
- 想要支付成功,付款人的信息(身份证)必须要在微信服务器有记录。如果没有微信不能完成人脸对比也就是开通不了微信支付。此情况可在微信端完成实名认证并上传身份证正反面
开始写代码:
参考文档:微信人脸支付官方文档 : 微信人脸支付官方文档
微信支付公共错误码:(参考微信文档)
错误码 | 描述 | 解决方案 |
---|---|---|
SUCCESS | 接口成功 | 无 |
ERROR | 接口失败 | 展示错误原因(该请求无法通过重试解决) |
PARAM_ERROR | 参数错误 | 参照错误提示 |
SYSTEMERROR | 接口返回错误 | 系统异常,可重试该请求 |
人脸支付时序图(从微信官方文档截取)
按照时序图可把微信人脸支付分为5个步骤进行操作
- 初始化微信SDK操作
- 获取Rawdata
- 根据RawData获取人脸识别凭证authinfo
- 根据authinfo开启刷脸摄像头进行人脸识别操作获取到faceCode
- 通过faceCode调用微信支付API做扣款动作
第一步:初始化微信微信人脸支付
参考代码:
/**
* 版 本 : 1.0
* 操作人 : yzhg
* 描 述 : 初始化微信人脸支付SDK操作
* 初始化使用的map 用于设置商户代理 配置刷脸走商户内部代理 若不需要,则不用填写
*/
@Override
public void initWxPay() {
Map<String, String> agencyMap = new HashMap<>();
// agencyMap.put("ip",""); //HTTP代理IP 192.168.1.1
// agencyMap.put("port",""); //HTTP代理端口 8888
// agencyMap.put("user",""); //HTTP代理的用户名
// agencyMap.put("passwd",""); //HTTP代理的密码
WxPayFace.getInstance().initWxpayface(Tools.getContext(), agencyMap, new IWxPayfaceCallback() {
@Override
public void response(Map map) throws RemoteException {
/**
* 版 本 : 1.0
* 操作人 : yzhg
* 描 述 : 拿到初始化结果
* 对比code是否为SUCCESS 详细可以查看官方错误码
*/
if (map != null) {
String code = (String) map.get(WxConstant.RETURN_CODE);
String msg = (String) map.get(WxConstant.RETURN_MSG);
if (!(code != null && code.equals(WxfacePayCommonCode.VAL_RSP_PARAMS_SUCCESS))) {
mView.initWxPayFailed(Tools.getString(R.string.wx_face_init_failed), NetStateEnum.one);
} else {
mView.initWxPaySuccess();
}
}
}
});
}
注意: 目前我们没有在initPayFace()中做app保活的自启措施,所以当您的应用在启动过程中遇到重启/更新的问题,您必须重新调用initPayFace(),相信我们会在下一个最新的版本中对initPayFace()做进一步的完善。
第二步:获取RawData
/**
* 作 者: yzhg
* 历 史: (版本) 1.0
* 描 述:
* 获取微信人脸支付RawData
*/
@Override
public void getWxPayFaceRawData() {
WxPayFace.getInstance().getWxpayfaceRawdata(new IWxPayfaceCallback() {
@Override
public void response(Map map) throws RemoteException {
if (!Tools.isSuccessInfo(map)) {
mView.initWxPayFailed(Tools.getString(R.string.raw_data_failed), NetStateEnum.two);
} else {
//获取到RAW_DATA
if (map.get(WxConstant.RAW_DATA) != null) {
String rawdata = map.get(WxConstant.RAW_DATA).toString();
mView.wxPayFaceRawDataSuccess(rawdata);
} else {
mView.initWxPayFailed(Tools.getString(R.string.raw_data_failed), NetStateEnum.two);
}
}
}
});
}
注:前两步骤没什么悬念,不需要传参数(如果不用代理)Tools.isSuccessInfo()方法下面展示
第三步:根据RawData获取人脸识别凭证authinfo
注意:这里微信给了一个http地址:https://payapp.weixin.qq.com/face/get_wxpayface_authinfo 正式项目中应该由我们的服务端(比如java .net php等后端)调用此接口,以便填写商户号等信息。这里我们不通过后端直接调用此接口
代码:代码中的 appId 商户号 商户支付秘钥在微信开放平台申请获取
/**
* 作 者: yzhg
* 历 史: (版本) 1.0
* 描 述:
* 获取微信人脸支付凭证
*/
@Override
public void getWxFacePayVoucher(String rawdata) {
Map<String, String> map = new HashMap<>();
map.put("appid", "申请下来的appID");
map.put("mch_id", "商户号");
map.put("sub_mch_id", "子商户号(服务商模式)");
map.put("sub_appid", "子商户绑定的公众号/小程序 appid(服务商模式)");
map.put("now", "" + (System.currentTimeMillis() / 1000));
map.put("version", "1");
map.put("sign_type", "MD5");
map.put("nonce_str", "" + (System.currentTimeMillis() / 100000));
map.put("store_id", "可随便写");
map.put("store_name", "小杨店铺");
map.put("device_id", "" + (System.currentTimeMillis() / 100000));
map.put("rawdata", rawdata);
// map.put("spbill_create_ip", (null == ipAddress) ? "" : ipAddress);
/**sign签名的规则生成*/
//按字典顺序排序
List<Map.Entry<String, String>> infoIds = new ArrayList<>(map.entrySet());
Collections.sort(infoIds, (o1, o2) -> (o1.getKey()).compareTo(o2.getKey()));
//使用&符号进行频率
String sbR = Tools.getStringBuffer(infoIds) + "&key=商户支付秘钥";
//进行MD5加密之后 转大写
String sign = Tools.encode(sbR).toUpperCase();
map.put("sign", sign);
LogUtils.d("认证参数" + map.toString());
try {
/*将map集合转为xml*/
String toXml = Tools.mapToXml(map);
LogUtils.d("认证参数XMl" + toXml);
RequestBody body = RequestBody.create(null, toXml);
OkGo.<String>post(Api.getFaceAuthInfo)
.tag(Api.getFaceAuthInfo)
.upRequestBody(body)
.execute(new StringCallback() {
@Override
public void onSuccess(Response<String> response) {
String payVoucher = response.body();
LogUtils.d("获取到的微信支付凭证" + payVoucher);
try {
String return_code = Tools.parseGetAuthInfoXML(payVoucher, WxConstant.RETURN_CODE);
if ("SUCCESS".equals(return_code)) {
String authinfo = Tools.parseGetAuthInfoXML(payVoucher, "authinfo");
LogUtils.d("获取到的微信支付凭证infoXML" + authinfo);
mView.wxFacePayVoucherSuccess(authinfo);
} else {
String return_msg = Tools.parseGetAuthInfoXML(payVoucher, WxConstant.RETURN_MSG);
mView.initWxPayFailed("获取微信凭证失败" + return_msg, NetStateEnum.three);
}
} catch (Exception e) {
e.printStackTrace();
mView.initWxPayFailed("获取微信凭证失败" + e.getMessage(), NetStateEnum.three);
}
}
@Override
public void onError(Response<String> response) {
mView.initWxPayFailed("获取微信凭证失败" + response.body().toString(), NetStateEnum.three);
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
值得注意的是微信提供的接口都是XML形式的
首先看传参数:
参数 | 必填 | 类型 | 说明 |
---|---|---|---|
store_id | 是 | string(32) | 门店编号, 由商户定义, 各门店唯一。 |
store_name | 是 | string(128) | 门店名称,由商户定义。(可用于展示) |
device_id | 是 | string(32) | 终端设备编号,由商户定义。 |
attach | 否 | string | 附加字段。字段格式使用Json |
rawdata | 是 | string(2048) | 初始化数据。由微信人脸SDK的接口返回。 获取方式参见: [获取数据 getWxpayfaceRawdata](#获取数据 getWxpayfaceRawdata) [获取数据 getWxpayfaceRawdata](#获取数据 getWxpayfaceRawdata) |
appid | 是 | string(32) | 商户号绑定的公众号/小程序 appid |
mch_id | 是 | string(32) | 商户号 |
sub_appid | 否 | string(32) | 子商户绑定的公众号/小程序 appid(服务商模式) |
sub_mch_id | 否 | string(32) | 子商户号(服务商模式) |
now | 是 | int | 取当前时间,10位unix时间戳。 例如:1239878956 |
version | 是 | string | 版本号。固定为1 |
sign_type | 是 | string | 签名类型,目前支持HMAC-SHA256和MD5,默认为MD5 |
nonce_str | 是 | string(32) | 随机字符串,不长于32位 |
sign | 是 | string | 参数签名。详见微信支付签名算法 |
- storeId,StoreName 每个商店不唯一。这里只是演示随意填写
- device_id 终端设备编号,可使用设备的UUID
- rawData 主要参数,由第二步中获得传入
- sign签名,提供微信签名说明 sign生成规范
代码中sign签名说明:
- 将所有的参数放入到map集合中
- 将map转为list集合用于排序
List<Map.Entry<String, String>> infoIds = new ArrayList<>(map.entrySet());
- 使用sort将list集合按照字典顺序排序
Collections.sort(infoIds, (o1, o2) -> (o1.getKey()).compareTo(o2.getKey()));
- 按照要求拼接字符串后将Key拼接到后面
/** * 拼接 * @param infoIds * @return */ @NonNull public static String getStringBufferObject(List<Map.Entry<String, Object>> infoIds) { StringBuffer sb = new StringBuffer(); for (int i = 0; i < infoIds.size(); i++) { Map.Entry<String, Object> stringStringEntry = infoIds.get(i); if (stringStringEntry.getKey() == null) { stringStringEntry.getKey(); } String key = stringStringEntry.getKey(); Object val = stringStringEntry.getValue(); if (i != infoIds.size() - 1) { if (val != null && !TextUtils.equals("", val.toString())) { sb.append(key).append("=").append(val).append("&"); } } else { if (val != null && !TextUtils.equals("", val.toString())) { sb.append(key).append("=").append(val); } } } return sb.toString(); }
- 进行MD5加密转大写 得到sign
/**
* 作 者: yzhg
* 历 史: (版本) 1.0
* 描 述: md5加密
*/
public static String encode(String password) {
try {
MessageDigest digest = MessageDigest.getInstance("md5");
byte[] result = digest.digest(password.getBytes());
StringBuilder sb = new StringBuilder();
for (byte b : result) {
int number = b & 0xff;
String str = Integer.toHexString(number);
if (str.length() == 1) {
sb.append("0");
}
sb.append(str);
}
return sb.toString();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
return "";
}
}
6.将做好的参数转为XML传给微信(提供map集合转xml方法)
/**
* * 将Map转换为XML格式的字符串
* *
* * @param data Map类型数据
* * @return XML格式的字符串
* * @throws Exception
*
*/
public static String mapToXml(Map<String, String> data) throws Exception {
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
org.w3c.dom.Document document = documentBuilder.newDocument();
org.w3c.dom.Element root = document.createElement("xml");
document.appendChild(root);
for (String key : data.keySet()) {
String value = data.get(key);
if (value == null) {
value = "";
}
value = value.trim();
org.w3c.dom.Element filed = document.createElement(key);
filed.appendChild(document.createTextNode(value));
root.appendChild(filed);
}
TransformerFactory tf = TransformerFactory.newInstance();
Transformer transformer = tf.newTransformer();
DOMSource source = new DOMSource(document);
transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
StringWriter writer = new StringWriter();
StreamResult result = new StreamResult(writer);
transformer.transform(source, result);
String output = writer.getBuffer().toString(); //.replaceAll("\n|\r", "");
try {
writer.close();
} catch (Exception ex) {
}
return output;
}
7. 接口调用完毕得到反参后解析参数。获取到autoInfo(提供解析XML方法)
public static String parseGetAuthInfoXML(String resultText, String indexText) throws Exception {
InputStream is = new ByteArrayInputStream(resultText.getBytes());
String result = null;
XmlPullParser parser = Xml.newPullParser();
parser.setInput(is, "UTF-8");
int eventType = parser.getEventType();
while (eventType != XmlPullParser.END_DOCUMENT) {
switch (eventType) {
case XmlPullParser.START_TAG:
if (parser.getName().equals(indexText)) {
eventType = parser.next();
result = parser.getText();
}
}
eventType = parser.next();
}
return result;
}
至此第三步完成。
其他说明:autoInfo不需要一直获取,反参中expires_in为autoInfo过期时间,一般为3600秒(一小时)。我们可以在获取autoInfo前储存获取的时间,再次发起支付时只需要使用当前时间减去储存的autoInfo得到时间差。与expines_in做对比。如果没有过期,则不需要再获取rawData,autoInfo。直接走第四步调用人脸识别方法(详细请参考DEMO)
注意:这里需要储存的时autoInfo,,,不是rawData
总结:我们第三步主要的目的就是通过RawData获取autoInfo
其中牵涉到sign签名生成,网络请求(飘过)。XML传参
第四步:调用人脸识别APP,识别人脸
传参展示
appid | 是 | string | 商户号绑定的公众号/小程序 appid |
mch_id | 是 | string | 商户号 |
sub_appid | 否 | string(32) | 子商户绑定的公众号/小程序 appid(可不填) |
sub_mch_id | 否 | string(32) | 子商户号(非服务商模式不填) |
store_id | 是 | string | 门店编号 |
telephone | 否 | string | 用户手机号。用于传递会员手机,此手机将作为默认值, 填写到手机输入栏。 |
out_trade_no | 是 | string | 商户订单号,须与调用支付接口时字段一致,该字段在在face_code_type 为"1"时可不填,为"0"时必填 |
total_fee | 是 | string | 订单金额(数字), 单位分. 该字段在在face_code_type 为"1"时可不填,为"0"时必填 |
face_authtype | 是 | string | 可选值:FACEPAY : 人脸凭证,常用于人脸支付FACEPAY_DELAY : 延迟支付(提供商户号信息联系微信支付开通权限) |
authinfo | 是 | string | 调用凭证。获取方式参见: get_wxpayface_authinfo |
ask_face_permit | 是 | string | 支付成功页是否需要展示人脸识别授权项。 展示:1 不展示:0 人脸识别授权项: 用户授权后用于1:N识别,可返回用户信息openid,建议商户有自己会员系统时,填1。 |
ask_ret_page | 否 | string | 是否展示微信支付成功页,可选值:"0",不展示;"1",展示 |
face_code_type | 否 | string | 目标face_code类型,可选值:"0",人脸付款码:数字字母混合,通过「刷脸支付」接口完成支付;"1",刷卡付款码:18位数字,通过「付款码支付/被扫支付」接口完成支付。如果不填写则默认为"0" |
ignore_update_pay_result | 否 | string | 商户端是否对SDK返回支付结果,可选值:"0",返回支付结果,商户需在确认⽀付结果后调⽤[updateWxpayfacePayResult]通知SDK;"1",不返回支付结果。如果不填写则默认为"0"。 |
注意:total_fee 的单位为分 即: 如果传的是1则表示 0.01元
telephone : 此参数要格外注意。此为可不传参数如果不传微信会调用手机号输入页面让用户自己去取输入手机号
坑:如果当前没有手机号 不要写这个参数。如果写 telephone="" 则不会拉起人脸识别
out_trade_no : 商户订单号,须与调用支付接口时字段一致,该字段在在face_code_type
为"1"时可不填,为"0"时必填
坑:一定要看文档,这里与支付接口字段必须一致。如果不一致则会支付失败
传参示例:
HashMap<String, String> map = new HashMap<>();
map.put("appid", "appID");
map.put("mch_id", "商户号");
//这里区分服务商模式和商户模式,服务商模式子商户信息必填
map.put("sub_appid", "子商户ID");
map.put("sub_mch_id", "子商户mchID");
map.put("store_id", "随便写");
if(!edPhone.isEmpty()){
map.put("telephone", edPhone);
}
map.put("out_trade_no", anInt + ""); //这里有坑
map.put("total_fee", money);
map.put("face_code_type", "0");
map.put("ignore_update_pay_result", "0");
map.put("face_authtype", "FACEPAY");
map.put("authinfo", authinfo);
map.put("ask_face_permit", "0");
map.put("ask_ret_page", "1");
调用人脸识别API示例:
/**
* 版 本 : 1.0
* 操作人 : yzhg
* 描 述 : 调用人脸识别
*/
@Override
public void getWxPayFaceCode(HashMap<String, String> map) {
WxPayFace.getInstance().getWxpayfaceCode(map, new IWxPayfaceCallback() {
@Override
public void response(Map map) {
if (!Tools.isSuccessInfo(map)) {
ToastUtils.show("支付失败");
return;
}
final String code = (String) map.get(WxConstant.RETURN_CODE);
ThreadUtils.runOnMainThread(() -> {
if (TextUtils.equals(code, WxfacePayCommonCode.VAL_RSP_PARAMS_SUCCESS)) {
String faceCode = (String) map.get(WxConstant.FACE_CODE);
String openId = (String) map.get(WxConstant.OPEN_ID);
String sub_open_id = (String) map.get(WxConstant.SUB_OPEN_ID);
mView.getWxPayFaceCodeSuccess(map.toString(), faceCode, openId, sub_open_id);
} else if (TextUtils.equals(code, WxfacePayCommonCode.VAL_RSP_PARAMS_USER_CANCEL)) {
ToastUtils.show("用户取消");
} else if (TextUtils.equals(code, WxfacePayCommonCode.VAL_RSP_PARAMS_SCAN_PAYMENT)) {
ToastUtils.show("扫码支付");
} else if (TextUtils.equals(code, "FACEPAY_NOT_AUTH")) {
ToastUtils.show("无即时支付无权限");
} else {
ToastUtils.show("失败");
}
});
}
});
}
反参说明:这里接收的 face_code openid是调用最终支付的重要参数
参数 | 是否必然返回 | 类型 | 说明 |
---|---|---|---|
return_code | 是 | string | 错误码。公共定义见 公共错误码 |
return_msg | 是 | string(128) | 对错误码的描述 |
face_code | S | string | 人脸凭证, 用于刷脸支付。 |
openid | S | string | openid(相当于用户身份) |
sub_openid | 否 | string | 子商户号下的openid(服务商模式) |
telephone_used | 否 | int | 获取的face_code ,是否使用了请求参数中的telephone 可取值: 0:表示没有使用 telephone ;1: 表示使用了 telephone ; |
underage_state | 否 | int | 用户年年龄信息,使用需要联系微信支付开通权限 可取值: 0:状态不明确,或权限未开通; 1: 成年年人; 2: 未成年人 |
第五步:调用最终支付信息,进行扣款
支付接口说明文档 :人脸支付文档(最终支付)扣款
参数配置:详细参数看文档。这是后端的活这里只做简单演示
Map<String, String> map = new HashMap<>();
map.put("appid", "appID");
map.put("mch_id", "MCH_ID");
map.put("sub_appid", "子商户号");
map.put("sub_openid", "");
map.put("sub_mch_id", "子商户MCH_ID");
map.put("device_info", "" + (System.currentTimeMillis() / 100000));
map.put("nonce_str", "" + (System.currentTimeMillis() / 100000));
map.put("body", "小杨服饰专卖店");
map.put("out_trade_no", anInt + "");
map.put("total_fee", money);
map.put("spbill_create_ip", "192.168.56.1");
map.put("openid", openId);
map.put("face_code", faceCode);
//按字典顺序排序
List<Map.Entry<String, String>> infoIds = new ArrayList<>(map.entrySet());
Collections.sort(infoIds, (o1, o2) -> (o1.getKey()).compareTo(o2.getKey()));
//使用&符号进行频率
String sbR = Tools.getStringBuffer(infoIds) + "&key=e930916ee9359718c1465df37cb15601";
//进行MD5加密之后 转大写
String sign = Tools.encode(sbR).toUpperCase();
map.put("sign", sign);
sb.append("加密后传参:").append(map.toString()).append("\n\n");
showResult();
String toXml = Tools.mapToXml(map);
参数说明:out_trade_no 要与第三步中的一致 否则掉不起刷脸支付
sign生成参考第三步
支付接口 : https://api.mch.weixin.qq.com/pay/facepay
将生成好的xml传入此接口中,即可完成扣款
第六步:扫码支付:
/**
* 操作人 : yzhg
* 描 述 : 开启扫码支付
*/
private void initWxScanBar() {
WxPayFace.getInstance().startCodeScanner(new IWxPayfaceCallback() {
@Override
public void response(Map info) throws RemoteException {
if (Tools.isSuccessInfo(info)) {
String code_msg = (String) info.get(WxConstant.CODE_MSG);
ToastUtils.show("获取到二维码信息" + code_msg);
LogUtils.d("获取到二维码信息" + code_msg);
/**
* 操作人 : yzhg
* 描 述 : 拿到二维码信息之后,去做支付操作
*/
ThreadUtils.runOnMainThread(() -> scanBarCode.setText("获取到二维码信息" + code_msg));
} else {
ToastUtils.show("扫码失败");
LogUtils.d("扫码失败");
}
}
});
}
扫码只是调起摄像头等待回调即可。
其他说明:
- 微信刷脸支付处于测试阶段,如果想使用,必须去申请加入白名单
- 我们写的app只是对接微信的端口。所有的麻烦事(比如调用摄像头)微信全都搞定了。也就是说我们必须使用微信指定的摄像头。必须使用微信提供的wxfacepay.apk,并预装到刷脸设备中。而且必须是支持静默升级 点我下载APK
- 如果设备中没有提前预装wxfacepay.apk初始化时报错。所以要判断是否已经安装此APP 提供包名:
com.tencent.wxpayface
- 在第三步和第五步中要进行加签和验签操作。具体如何验签为后端事宜。由于篇幅原因,这里只做demo。只做加签并没有验签。实际项目中Android接收到微信的同步通知即可证明支付成功。
- 省略。。。。。。。
DEMO在github查看 : https://github.com/yzhg0854/WxFacePay
DEMO如何运行:
请在demo中找到 WxConstant 类。找到下面的常量进行配置即可
/*****************************以下配置均在微信开放平台获取************************************/
/*配置APPID*/
public static final String APP_ID = "APP_ID";
/*商户号*/
public static final String MCH_ID = "MCH_ID";
//子商户ID
public static final String SUB_APP_ID = "SUB_APP_ID";
//子商户MCH_ID
public static final String SUB_MCH_ID = "SUB_MCH_ID";
/*商户支付秘钥*/
public static final String MCH_KEY_ID = "MCH_KEY_ID";
检测是否安装某一个APK
/**
* 检测是否安装APK
* @param context
* @return
*/
fun isWxFacePayAvilible(context: Context): Boolean {
val packageManager: PackageManager = context.packageManager
val pinfo: MutableList<PackageInfo>? = packageManager.getInstalledPackages(0)
if (pinfo != null) {
for (index in pinfo.indices) {
var packageName = pinfo[index].packageName
if (TextUtils.equals("com.tencent.wxpayface", packageName)) {
return true
}
}
}
return false
}
至此微信人脸支付已经完成,很简单,