银联卡8583协议小额免密免签交易总结

之前做过金融支付这块儿。到过北京石景山区银行卡检测中心过检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连接分为两个阶段,即握手和数据传输阶段;传输任何应用数据之前必须先完成握手。

握手协议:

  1. 对服务器进行认证;
  2. 确立用于保护数据传输的加密密钥;
  3. 记录协议:
  4. 传输数据;

银联通信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 签到成功
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

特立独行的猫a

您的鼓励是我的创作动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值