在沃尔玛要求其供应商使用该协议之后,AS2(或Applicability Statement 2)快速流行起来。许多其他大型零售商也纷纷效仿,这意味着AS2迅速成为许多行业点对点连接的最流行EDI传输协议。

但是在国内,该协议使用的很少,资料也很稀缺,今天我把所学的知识整理一下,同时提供一个JAVA的实现版本,方便大家集成AS2。

数字摘要,加密,解密

在学习AS2协议之前,大家需要对基本的加密知识有所了解。

数字摘要

首先需要了解的是数字摘要,也叫数字指纹,数字签名,顾名思义,每段信息都有特定的指纹信息,就像人的指纹一样,通过指纹你就能确定人的身份。同理,通过数字指纹就能确定是哪一段信息。那这个有什么用呢?我们知道,网络传输是不安全的,比如你女朋友给你发消息,你怎么知道这段消息就是你女朋友发的?说不定是哪个骗子发的诈骗信息。或者你如何确定这段信息没被篡改过?说不定你的情敌在偷偷的篡改你们之间的通信来破坏你们间的感情。这时如果你女朋友通过某种算法根据这段信息生成一段数字摘要,然后随着这段信息发送给你,你通过比较原始信息和摘要是否能对应上,就能验证信息是否伪造,是否篡改了。

常见的摘要算法有MD5,SHA1,SHA256(也叫SHA2)。这几种算法安全性是递进的,耗时也是递进的。

MD5输出128bit,SHA1输出160bit,SHA256bit。

数字摘要的应用有签名,加密密码,校验文件是否损坏是否相同等。

数字摘要的缺点有两个,一个是会发生HASH碰撞,就是两段不同的信息会生成相同的指纹,虽然概率很低,MD5发生碰撞的概率为2的128次方之1,还有一个是会被暴力破解,比如使用字典记录所有信息和指纹的对应关系,就能通过指纹倒推原始信息,成本很高,但一些简单的信息都能查到。

对称加密

对称加密很容易理解,通过算法和秘钥对原文进行加密,得到密文,接受者拿到密文后再通过算法和秘钥进行解密,得到原文,这就是对称加密,对称加密的优点是简单,运算速度快,缺点是无法保证秘钥在传输过程中不被窃取,安全性不高。

常见的对称加密算法有DES,3DES,AES

DES是比较老的加密算法,已经不推荐使用了,3DES是DES的改进型,现在最常用的是AES

非对称加密

非对称加密的加密和解密用的不是同一个秘钥,它分为公钥和私钥,公钥加密私钥解密,或者私钥加密公钥解密,所以在使用非对称加密的时候需要和你的通信对象交换公钥,私钥自己保留,不对外开放,当你发送信息时使用自己的私钥加密后发送给对方,对方通过你的公钥解密,反之亦然。

常用的非对称加密算法是RSA。

非对称加密+对称加密

非对称加密的优点是安全性高,缺点是速度慢,对称加密的优点是速度快,缺点是安全性不高,有没有办法结合两方面的优缺点呢?答案当然是有的

比如使用RAS+3DES,首先随机生成3DES的秘钥,然后使用3DES加密你的内容,也就是长内容,再使用对方的公钥加密3DES的秘钥,也就是短内容,一起传输给对方,对方通过自己的私钥解密出3DES的秘钥,再通过3DES的秘钥解密长内容,是不是很巧妙。

生成证书库

要使用非对称加密,首先我们需要有证书,可以通过java的keytool工具来生成证书。

keytool -genkeypair -alias mykeystore -keyalg RSA -keysize 2048 -keypass mypassword -sigalg SHA256withRSA -dname "cn=www.justinqin.com,ou=justinqin,o=qjg,l=Shenzhen,st=Guangdong,c=CN" -validity 1095 -keystore D:/keys/mykeystore.keystore -storetype JKS -storepass mypassword


-genkeypair非对称密钥(密钥对,私钥签名、公钥验签),对称密钥为-gendeskey(单个密钥)

-alias mykeystore证书库别名为mykeystore

-keyalg RSA加密算法为RSA,加密算法可以分为对称加密、不对称加密和不可逆加密算法对称加密算法:加解密都用的同一把密钥,如DES、AES不对称加密算法:使用密钥对即公钥、私钥进行加解密,如RSA SHS不可逆加密算法:加密过程中不需要密钥,明文加密成密文后不可逆,如MD5MD5、SHS的加密都是用了哈希加密算法

-keysize 2048密钥长度,位数越大越安全,但同时加解密时间成正比增长

-keypass mypassword私钥密码为mypassword

-sigalg SHA256withRSA签名算法为SHA256withRSAkeyalg=RSA时,sigalg可选MD5withRSA、SHA1withRSA、SHA256withRSA、SHA384withRSA、SHA512withRSAkeyalg=DSA时,sigalg可选SHA1withDSA、SHA256withDSA

 -dname "cn=www.justinqin.com,ou=justinqin,o=qjg,l=Shenzhen,st=Guangdong,c=CN"证书相关信息CN=名字与姓氏/域名OU=组织单位名称O=组织名称L=城市或区域名称ST=州或省份名称C=单位的两字母国家代码

-keystore D:/keys/mykeystore.keystore生成的证书库文件为mykeystore.keystore,存储位置D:/keys/

-validity 1095证书有效天数为3年=1095

-storetype JKS证书库类型为JKS,JDK1.9以前默认JKS,JDK1.9及以后默认PKCS12

-storepass mypassword证书库密码为mypassword

这个就是生成证书库的命令,既然是是库,说明可以包含多个证书,通过别名来区分,当从证书库中取证书时,需要知道别名,证书库的密码,当取私钥时还需要私钥的密码

导出公钥

keytool -export -alias mykeystore -keystore D:/keys/mykeystore.keystore -storepass mypassword -file D:/keys/publickey.cer


这个公钥需要给通信对象,这样对方就能解密你发送的密文了。



 上一篇中主要讲解了加密的理论知识,这篇来上代码。

签名和验签

通过信息摘要算法和非对称加密,可以实现信息的防伪造,防篡改,通过我们的私钥来签名消息,接收方就能通过我们的公钥来校验该消息是否是我们发送的。

/**

 * 获取证书库

 */public static KeyStore getKeyStore(InputStream keyStoreInputStream, String keyStorePassword, String keyStoreType) throws Exception {

    return getKeyStore(keyStoreInputStream, keyStorePassword, keyStoreType);

}

public static KeyStore getKeyStore(InputStream keyStoreInputStream, String keyStorePassword, String keyStoreType, String provider) throws Exception {

    KeyStore keyStore;

    if (StringUtils.isNotBlank(provider)) {

        keyStore = KeyStore.getInstance(keyStoreType, provider);

    } else {

        keyStore = KeyStore.getInstance(keyStoreType);

    }

    keyStore.load(keyStoreInputStream, keyStorePassword.toCharArray());

    IoUtil.close(keyStoreInputStream);

    return keyStore;

}

/**

 * 从证书库中获取公钥

 */public static PublicKey getPublicKeyFromKeyStore(KeyStore keyStore, String alias) throws Exception {

    Certificate certificate = keyStore.getCertificate(alias);

    return certificate.getPublicKey();

}

/**

 * 从证书库获取私钥

 */public static PrivateKey getPrivateKeyFromKeyStore(KeyStore keyStore, String alias, String password) throws Exception {

    return (PrivateKey) keyStore.getKey(alias, password.toCharArray());

}

/**

 * 签名

 */public static byte[] sign(byte[] message, PrivateKey privateKey, String algorithm) throws Exception {

    Signature signature;

    signature = Signature.getInstance(algorithm);

    signature.initSign(privateKey);

    signature.update(message);

    return signature.sign();

}

/**

 * 验签

 */public static boolean verify(byte[] message, byte[] signMessage, PublicKey publicKey, String algorithm) throws Exception {

    Signature signature;

    boolean verifyResult;

    signature = Signature.getInstance(algorithm);

    signature.initVerify(publicKey);

    signature.update(message);

    verifyResult = signature.verify(signMessage);

    return verifyResult;

}


然后写个单元测试来验证下

@Testpublic void testVerify() throws Exception {

    FileInputStream fis = new FileInputStream(new File("d:/keys/testkeystore.keystore"));

    //获取证书库

    KeyStore keyStore = getKeyStore(fis, "mypassword", "JKS");

    //获取私钥

    PrivateKey privateKey = getPrivateKeyFromKeyStore(keyStore, "mykeystore", "mypassword");

    //摘要算法用SHA1,非对称加密算法用RSA进行签名

    byte[] signMessage = sign(CONTENT.getBytes(), privateKey, "SHA1withRSA");

    //获取公钥

    PublicKey publicKey = getPublicKeyFromKeyStore(keyStore, "mykeystore");

    //进行验签

    boolean verify = verify(CONTENT.getBytes(), signMessage, publicKey, "SHA1withRSA");

    System.out.println(verify);

}


加密和解密

/**

 * 使用私钥加密

 */public static byte[] encodeByPrivateKey(byte[] data, PrivateKey privateKey) throws Exception {

    // 对数据加密,加密算法由创建秘钥时指定,也可以自己指定,一般用RSA

    Cipher cipher = Cipher.getInstance(privateKey.getAlgorithm());

    cipher.init(Cipher.ENCRYPT_MODE, privateKey);

    return cipher.doFinal(data);

}

/**

 * 使用公钥解密

 */public static byte[] decodeByPublicKey(byte[] data, PublicKey publicKey)

        throws Exception {

    // 对数据加密

    Cipher cipher = Cipher.getInstance(publicKey.getAlgorithm());

    cipher.init(Cipher.DECRYPT_MODE, publicKey);

    return cipher.doFinal(data);

}

/**

 * 公钥加密

 */public static byte[] encodeByPublicKey(byte[] data, PublicKey publicKey)

        throws Exception {

    // 对数据加密

    Cipher cipher = Cipher.getInstance(publicKey.getAlgorithm());

    cipher.init(Cipher.ENCRYPT_MODE, publicKey);

    return cipher.doFinal(data);

}

/**

 * 私钥解密

 */public static byte[] decodeByPrivateKey(byte[] data, PrivateKey privateKey) throws Exception {

    // 对数据加密

    Cipher cipher = Cipher.getInstance(privateKey.getAlgorithm());

    cipher.init(Cipher.DECRYPT_MODE, privateKey);

    return cipher.doFinal(data);

}



单元测试

public static String CONTENT = "寥落古行宫,宫花寂寞红。白头宫女在,闲坐说玄宗";@Testpublic void testDecode() throws Exception {

    FileInputStream fis = new FileInputStream(new File("d:/keys/testkeystore.keystore"));

    KeyStore keyStore = getKeyStore(fis, "mypassword", "JKS");

    PrivateKey privateKey = getPrivateKeyFromKeyStore(keyStore, "mykeystore", "mypassword");

    PublicKey publicKey = getPublicKeyFromKeyStore(keyStore, "mykeystore");

    //加密

    byte[] encode = encodeByPrivateKey(CONTENT.getBytes(), privateKey);

    //解密

    byte[] message = decodeByPublicKey(encode, publicKey);

    System.out.println(new String(message));

}



AS2协议本身比较复杂,我们不需要了解其中太多细节,只需要知道一些重要概念就行了。

AS2是基于HTTP/HTTPS的,消息的格式使用MIME,就是邮件的格式,使用SHA1或SHA2加RSA进行签名,使用S/MIME进行加密。S/MIME加密本质就是3DES或者AES加RSA加密,有了之前的基础应该就很清楚了。

AS2的通信双方称为partner,每个partner有个partnerId,通信双方需要交互彼此的公钥。

自己实现S/MIME加密比较复杂,我们引入以下包来加密,jdk15on支持jdk1.5-jdk1.8,其他jdk版可去maven上搜索对应的版本

    <dependency>

        <groupId>org.bouncycastle</groupId>

        <artifactId>bcmail-jdk15on</artifactId>

        <version>1.70</version>

    </dependency>


接下来是实现AS2发送文件的JAVA版本

@Testpublic void testAS2() throws Exception {

    //注册证书提供者BC

    Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());

    String password = "testas2";

    //生成mime消息体,放入待发送文件

    MimeBodyPart finalMessage = new MimeBodyPart();

    File file = new File("D:\\workspace\\data\\a.xml");

    finalMessage.setDataHandler(new DataHandler(new FileDataSource(file)));

    finalMessage.setHeader("Content-Type", "application/xml");

    finalMessage.setHeader("Content-Transfer-Encoding", "base64");

    finalMessage.setFileName(file.getName());

    //加载证书

    FileInputStream fis = new FileInputStream(new File("D:\\workspace\\OpenAs2App\\Server\\src\\config\\as2_certs.p12"));

    KeyStore keyStore = getKeyStore(fis, password, "PKCS12", "BC");

    PrivateKey privateKey = getPrivateKeyFromKeyStore(keyStore, "mycompany", "password");

    //签名

    X509Certificate signCert = getCertificate(keyStore, "mycompany");

    List certList = new ArrayList();

    certList.add(signCert);

    Store certs = new JcaCertStore(certList);


    SMIMESignedGenerator signer = new SMIMESignedGenerator();

    signer.setContentTransferEncoding("base64");

    //使用SHA1进行签名

    signer.addSignerInfoGenerator(new JcaSimpleSignerInfoGeneratorBuilder().setProvider("BC")

            .build("SHA1WITHRSA", privateKey, signCert));

    signer.addCertificates(certs);

    MimeMultipart signedMimeMultipart = signer.generate(finalMessage);

    finalMessage = new MimeBodyPart();

    finalMessage.setContent(signedMimeMultipart);

    finalMessage.setHeader("Content-Type", signedMimeMultipart.getContentType());


    //加密

    // 加载partner的数字证书

    X509Certificate cert = getCertificate(keyStore, "partnera");

    // 创建加密器

    SMIMEEnvelopedGenerator encryptor = new SMIMEEnvelopedGenerator();

    encryptor.addRecipientInfoGenerator(new JceKeyTransRecipientInfoGenerator(cert).setProvider("BC"));

    encryptor.setContentTransferEncoding("base64");

    //3DES加密

    JceCMSContentEncryptorBuilder jceCMSContentEncryptorBuilder =

            new JceCMSContentEncryptorBuilder(new ASN1ObjectIdentifier(SMIMEEnvelopedGenerator.DES_EDE3_CBC)).setProvider("BC");

    jceCMSContentEncryptorBuilder.setSecureRandom(new SecureRandom());

    // 进行加密

    MimeBodyPart encryptedPart = encryptor.generate(finalMessage, jceCMSContentEncryptorBuilder.build());


    //准备头字段

    InternetHeaders ih = new InternetHeaders();

    ih.addHeader("Connection", "close, TE");

    ih.addHeader("Message-ID", UUID.randomUUID().toString());

    ih.addHeader("Mime-Version", "1.0");

    ih.addHeader("Content-Type", encryptedPart.getContentType());

    ih.addHeader("AS2-To", "PartnerA_OID");

    ih.addHeader("AS2-From", "MyCompany_OID");

    ih.addHeader("Subject", "Subject: File a.xml sent from MyCompany to PartnerA");

    ih.addHeader("Disposition-Notification-To", "edi@myCompany.com");

    ih.addHeader("Content-Disposition", "attachment");


    String url = "http://localhost:10080";

    execRequest("POST", url, ih.getAllHeaders(), null, encryptedPart.getInputStream());

}

private static X509Certificate getCertificate(KeyStore keyStore,

                                              String alias) throws Exception {

    Certificate certificate = keyStore.getCertificate(alias);

    return (X509Certificate) certificate;

}


发送http请求的代码

public static void execRequest(String method, String url, Enumeration<Header> headers, NameValuePair[] params, InputStream inputStream) throws Exception {

    HttpClientBuilder httpBuilder = HttpClientBuilder.create();

    URL urlObj = new URL(url);

    /*

     * httpClient is used for this request only,

     * set a connection manager that manages just one connection.

     */

    if (urlObj.getProtocol().equalsIgnoreCase("https")) {

        /*

         * Note: registration of a custom SSLSocketFactory via httpBuilder.setSSLSocketFactory is ignored when a connection manager is set.

         * The custom SSLSocketFactory needs to be registered together with the connection manager.

         */

        SSLConnectionSocketFactory sslCsf = buildSslFactory();

        httpBuilder.setConnectionManager(new BasicHttpClientConnectionManager(RegistryBuilder.<ConnectionSocketFactory>create().register("http", PlainConnectionSocketFactory.getSocketFactory()).register("https", sslCsf).build()));

    } else {

        httpBuilder.setConnectionManager(new BasicHttpClientConnectionManager());

    }

    RequestBuilder rb = getRequestBuilder(method, urlObj, params, headers);


    if (inputStream != null) {

        InputStreamEntity ise = new InputStreamEntity(inputStream);

        rb.setEntity(ise);

    }

    final HttpUriRequest request = rb.build();


    try (CloseableHttpClient httpClient = httpBuilder.build()) {

        try (CloseableHttpResponse response = httpClient.execute(request)) {

            HttpEntity entity = response.getEntity();

            String result = EntityUtils.toString(entity);

            System.out.println(result);

        }

    }

}

private static SSLConnectionSocketFactory buildSslFactory() throws Exception {

    boolean overrideSslChecks = true;

    SSLContext sslcontext;

    sslcontext = SSLContexts.createSystemDefault();

    // String [] protocols = Properties.getProperty(HTTP_PROP_SSL_PROTOCOLS,

    // "TLSv1").split("\\s*,\\s*");

    HostnameVerifier hnv = SSLConnectionSocketFactory.getDefaultHostnameVerifier();

    if (overrideSslChecks) {

        hnv = new HostnameVerifier() {

            @Override

            public boolean verify(String hostname, SSLSession session) {

                return true;

            }

        };

    }

    SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslcontext, null, null, hnv);

    return sslsf;

}

private static RequestBuilder getRequestBuilder(String method, URL urlObj, NameValuePair[] params, Enumeration<Header> headers) throws URISyntaxException {

    RequestBuilder req = null;

    if (method == null || method.equalsIgnoreCase(Method.GET)) {

        //default get

        req = RequestBuilder.get();

    } else if (method.equalsIgnoreCase(Method.POST)) {

        req = RequestBuilder.post();

    } else if (method.equalsIgnoreCase(Method.HEAD)) {

        req = RequestBuilder.head();

    } else if (method.equalsIgnoreCase(Method.PUT)) {

        req = RequestBuilder.put();

    } else if (method.equalsIgnoreCase(Method.DELETE)) {

        req = RequestBuilder.delete();

    } else if (method.equalsIgnoreCase(Method.TRACE)) {

        req = RequestBuilder.trace();

    } else {

        throw new IllegalArgumentException("Illegal HTTP Method: " + method);

    }

    req.setUri(urlObj.toURI());

    if (params != null && params.length > 0) {

        req.addParameters(params);

    }

    if (headers != null) {

        while (headers.hasMoreElements()) {

            Header header = headers.nextElement();

            String headerValue = header.getValue();

            req.setHeader(header.getName(), headerValue);

        }

    }

    return req;

}

public abstract static class Method {

    public static final String GET = "GET";

    public static final String HEAD = "HEAD";

    public static final String POST = "POST";

    public static final String PUT = "PUT";

    public static final String DELETE = "DELETE";

    public static final String TRACE = "TRACE";

    public static final String CONNECT = "CONNECT";

}



代码讲解

来看这段代码,这段代码是加载证书库,方法上篇给出了,类型是PKCS12,BC是什么鬼?BC是证书提供者,如果是自己生成的证书,提供者是SUN,就不需要传了,我们这个证书是BC提供的所以需要传。

KeyStore keyStore = getKeyStore(fis, password, "PKCS12", "BC");


生成mime消息体,放入待发送文件那段代码很简单,就是获取待发送文件的输入流然后生成mime消息体,没啥好讲的,加载证书库获取私钥的代码前篇都讲过了,也很简单。

接下来是签名和加密,都是固定写法,唯一有改动的可能是签名的算法是SHA1还是SHA2,加密算法是3DES还是AES,需要告知接收方。这边需要注意的是签名是使用自己的私钥,对方用我们的公钥进行验签,加密是使用对方的公钥,对方用自己的私钥进行解密。

接下去就是准备头字段,填写自己的partnerId和对方的partnerId就行了

其实有现成的代码,集成AS2不难,如果是自己去找协议去实现,难度就相当大了。