之前做过金融支付这块儿。到过北京石景山区银行卡检测中心过检PBOC的level2认证,去过上海银联总部和湖南银联对接银联卡和扫码支付。对金融支付和卡交易这块儿熟悉。现在这块儿知识用不上了总结下留作备忘,同时分享给有需要的人。
关于免密免密交易
免密支付指的是在支付一定金额时,不用输入密码,即可完成交易。小额免密免签,也叫小额双免。是中国银联提供的一种小额快速支付服务。银联闪付就是免密支付的应用,通过联机方式,使用具有“闪付”标识的银行银联芯信用卡,或承载个人信用卡信息的移动支付产品,将银联芯片卡靠近终端“闪付”感应区即可完成支付。
这是大概在2015年10月银联为了紧跟时代潮流和方便用户推出的新业务。我们知道原来的银行卡用起来很不方便,随后它推出的有一个"电子现金"功能,但还是需要先到ATM机上往卡里圈存一部分钱才能用,这部分圈存到卡片上的钱俗称电子现金,消费通过带有闪付标识的银联pos机使用,只需挥卡不用输入密码和签名即可消费,用在一些小额支付场景如公交,地铁和商超。
现在这种小额支付免密交易很流行,各大平台和支付结构都有一定小额的免密交易。银联小额免密免签不需要密码也能保障资金安全,因为小额免密免签使用高安全性的金融ic卡,同时银联联合银行为持卡人提供风险控制和赔付机制。
银联8583协议
8583协议在金融系统中很常用,报文简短,定义清晰规范。是基于ISO8583报文国际标准的包格式的通讯协议,8583包最多由128个字段域组成,每个域都有统一的规定,并有定长与变长之分。8583包前面一段为位图,它是打包解包确定字段域的关键代替。
曾写过一个java版的8583解析协议,全互联网最简单好用的,地址在这里:
java版银联8583协议解析,超简单超直观的实现及示例(全互联网最简单)_特立独行的猫a的博客-CSDN博客_银联8583demo
银联公网接入规范
银联提供了公网https接入规范。HTTP交易报文由http头加传统交易报文数据组成。http头中主要定义了交易报文类型及长度,同时需符合http协议规范。
POST /mjc/webtrans/XX HTTP/1.1 HOST: 145.4.206.XX:XX User-Agent: XXXX Cache-Control: no-cache Content-Type:x-ISO-TPDU/x-auth Accept: */* Content-Length: length
https握手流程
HTTPS(全称:Hyper Text Transfer Protocol over Secure Socket Layer),是以安全为目标的HTTP通道,简单讲是HTTP的安全版。即HTTP下加入SSL层,HTTPS的安全基础是SSL。在发送HTTP报文之前需要建立SSL连接,以此加密HTTP数据。关系如下图所示:
SSL连接分为两个阶段,即握手和数据传输阶段;传输任何应用数据之前必须先完成握手。
握手协议:
- 对服务器进行认证;
- 确立用于保护数据传输的加密密钥;
- 记录协议:
- 传输数据;
银联通信MAC算法
8583通信有个MAC校验,相关算法参照博主博文:
ANSI-X99MAC算法和PBOC的3DES MAC算法_特立独行的猫a的博客-CSDN博客
Java的HTTP接口封装
没有使用第三方的jar包,使用java自带的net.HttpURLConnection包的封装,简单易用。支持http和https,可以让https不验证本地证书。
package com.yang.utils; import java.io.BufferedReader; import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.URL; import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.Arrays; import java.util.Map; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSession; import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.TrustManager; import javax.net.ssl.X509TrustManager; /** * https请求工具类 * * @author yang * @date 2018/6/6 19:12 */ public class HttpsUtils { /* * https请求是在http请求的基础上加上一个ssl层 */ public static String doPost(String requestUrl, String bodyStr, Map<String, String> header, String charset, String contentType) throws Exception { System.out.printf("https post 请求地址:%s 内容:%s", requestUrl, bodyStr); charset = null == charset ? "utf-8" : charset; // 创建SSLContext SSLContext sslContext = SSLContext.getInstance("SSL"); TrustManager[] trustManagers = {new X509TrustManager() { /* * 实例化一个信任连接管理器 * 空实现是所有的连接都能访问 */ @Override public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException { } @Override public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException { } @Override public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; } }}; // 初始化 sslContext.init(null, trustManagers, new SecureRandom()); SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory(); URL url = new URL(requestUrl); HttpsURLConnection httpsURLConnection = (HttpsURLConnection) url.openConnection(); httpsURLConnection.setSSLSocketFactory(sslSocketFactory); // 以下参照http请求 httpsURLConnection.setDoOutput(true); httpsURLConnection.setDoInput(true); httpsURLConnection.setUseCaches(false); httpsURLConnection.setRequestMethod("POST"); httpsURLConnection.setRequestProperty("Accept-Charset", charset); if (null != bodyStr) { httpsURLConnection.setRequestProperty("Content-Length", String.valueOf(bodyStr.length())); } if (contentType == null) { httpsURLConnection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); } else { httpsURLConnection.setRequestProperty("Content-Type", contentType); } if (!header.isEmpty()) { for (Map.Entry<String, String> entry : header.entrySet()) { httpsURLConnection.setRequestProperty(entry.getKey(), entry.getValue()); } } httpsURLConnection.connect(); // 读写内容 OutputStream outputStream = null; InputStream inputStream = null; InputStreamReader streamReader = null; BufferedReader bufferedReader = null; StringBuffer stringBuffer; try { if (null != bodyStr) { outputStream = httpsURLConnection.getOutputStream(); outputStream.write(bodyStr.getBytes(charset)); outputStream.close(); } if (httpsURLConnection.getResponseCode() >= 300) { throw new Exception("https post failed, response code " + httpsURLConnection.getResponseCode()); } inputStream = httpsURLConnection.getInputStream(); streamReader = new InputStreamReader(inputStream, charset); bufferedReader = new BufferedReader(streamReader); stringBuffer = new StringBuffer(); String line; while ((line = bufferedReader.readLine()) != null) { stringBuffer.append(line); } } catch (Exception e) { throw e; } finally { if (outputStream != null) { outputStream.close(); } if (inputStream != null) { inputStream.close(); } if (streamReader != null) { streamReader.close(); } if (bufferedReader != null) { bufferedReader.close(); } } System.out.printf("--- https post 返回内容:%s", stringBuffer.toString()); return stringBuffer.toString(); } public static byte[] doUpHttpsPost(String requestUrl, byte[] bodyHex) throws Exception { System.out.printf("请求地址:%s\n", requestUrl); // 创建SSLContext SSLContext sslContext = SSLContext.getInstance("SSL"); TrustManager[] trustManagers = {new X509TrustManager() { /* * 实例化一个信任连接管理器 * 空实现是所有的连接都能访问 */ @Override public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException { } @Override public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException { } @Override public X509Certificate[] getAcceptedIssuers() { //return new X509Certificate[0]; return new X509Certificate[]{}; } }}; // 初始化 sslContext.init(null, trustManagers, new SecureRandom()); SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory(); URL url = new URL(requestUrl); HttpsURLConnection httpsURLConnection = (HttpsURLConnection) url.openConnection(); httpsURLConnection.setSSLSocketFactory(sslSocketFactory); HostnameVerifier hv = new HostnameVerifier(){ @Override public boolean verify(String string, SSLSession ssls) { return true;//To change body of generated methods, choose Tools | Templates. } }; httpsURLConnection.setHostnameVerifier(hv); // 以下参照http请求 httpsURLConnection.setDoOutput(true); httpsURLConnection.setDoInput(true); httpsURLConnection.setUseCaches(false); httpsURLConnection.setRequestMethod("POST"); String strLen = String.valueOf(bodyHex.length); httpsURLConnection.setRequestProperty("User-Agent", "Donjin Http 0.1"); httpsURLConnection.setRequestProperty("Content-Length", strLen); httpsURLConnection.setRequestProperty("Accept", "*/*"); httpsURLConnection.setRequestProperty("Content-Type", "x-ISO-TPDU/x-auth"); httpsURLConnection.setRequestProperty("Cache-Control", "no-cache"); httpsURLConnection.connect(); // 读写内容 OutputStream outputStream = null; InputStream inputStream = null; ByteArrayOutputStream byteArrayOutputStream = null; try { outputStream = httpsURLConnection.getOutputStream(); outputStream.write(bodyHex); outputStream.flush(); outputStream.close(); System.out.printf("\n--- https ResponseCode:%d\n", httpsURLConnection.getResponseCode()); if (httpsURLConnection.getResponseCode() >= 300) { throw new Exception("https post failed, response code " + httpsURLConnection.getResponseCode()); } inputStream = httpsURLConnection.getInputStream(); byteArrayOutputStream = new ByteArrayOutputStream(); int len = 0; byte[] bytes = new byte[inputStream.available()]; while ((len = inputStream.read(bytes)) != -1) { byteArrayOutputStream.write(bytes, 0, len); }//写入数据 return byteArrayOutputStream.toByteArray(); //byte[] nbytes = bytes.c } catch (Exception e) { throw e; } finally { if (outputStream != null) { outputStream.close(); } if (inputStream != null) { inputStream.close(); } if (byteArrayOutputStream != null) { byteArrayOutputStream.close(); } } } }
免密免签交易流程
测试源码在这里:
https://download.csdn.net/download/qq8864/87280073
报文示例
test... 2022/12/13 18:57:16 Server:XXX.XX.XX.XXX 2022/12/13 18:57:16 Port:0 2022/12/13 18:57:16 Url:https://202.xx.25.xx:xx/mjc/webtrans/ABC pack 8583 fields Print fields... ========================================== Len: 0057 Tpdu: 6002100000 Head: 613100311108 Msge: 0800 Bitmap: 0020000000c00016 ========================================== [field:11] [000001] ------------------------------ [field:41] [3134313030303033] ------------------------------ [field:42] [313431343230313431313130303033] ------------------------------ [field:60] [len:0011] [000000000030] ------------------------------ [field:62] [len:0025] [53657175656e6365204e6f3132333036303134313030303033] ------------------------------ [field:63] [len:0003] [303031] ------------------------------ 2022/12/13 18:57:16 connect:server=https://2xx.101.25.18x:2x141/mjc/webtrans/ABC send:0057600210000061310031110808000020000000c0001600000131343130303030333134313432303134313131303030330011000000000030002553657175656e6365204e6f31323330363031343130303030330003303031 ReadFile err: open UP.pem: no such file or directory 2022/12/13 18:57:16 begin post... User-Agent : Donjin Http 0.1 Content-Type : x-ISO-TPDU/x-auth Cache-Control : no-cache 2022/12/13 18:57:16 recv ok!len=123 recv:007960000002106131003111080810003800010ac00014000001185716121308000952103138353731363035373232373030313431303030303331343134323031343131313030303300110000001700300040b31544e6cf8a5d091450ad5970c2143e7e960636afd9761ce25f41f70000000000000000c7e1867b ans 8583 fields 解析成功 Print fields... ========================================== Len: 0079 Tpdu: 6002100000 Head: 613100311108 Msge: 0810 Bitmap: 003800010ac00014 ========================================== [field:11] [000001] ------------------------------ [field:12] [185716] ------------------------------ [field:13] [1213] ------------------------------ [field:32] [len:08] [00095210] ------------------------------ [field:37] [313835373136303537323237] ------------------------------ [field:39] [3030] ------------------------------ [field:41] [3134313030303033] ------------------------------ [field:42] [313431343230313431313130303033] ------------------------------ [field:60] [len:0011] [000000170030] ------------------------------ [field:62] [len:0040] [b31544e6cf8a5d091450ad5970c2143e7e960636afd9761ce25f41f70000000000000000c7e1867b] ------------------------------ mackey:e5986862d3efefae 2022/12/13 18:57:16 签到成功