之前做过金融支付这块儿。到过北京石景山区银行卡检测中心过检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 签到成功