# 开票签名

# 简介

API签名使用JWT(Java Web Token)进行签名,详见https://jwt.io/。 各类编程语言参考官网的支持列表。 一个JWT实际上就是一个字符串,它由三部分组成,头部、载荷与签名。

注意事项

TIP

  1. 签名使用的密钥必须必须与平台appid注册的一致
  2. 具体使用方式参考样例代码示例
  3. JWT名词说明
  • iss: jwt签发者。使用appid
  • sub: 主题。例如:billing=开票;reimbursement=报销;
  • aud: 接收jwt的一方。固定值: einvoice.yonyoucloud.com
  • exp: jwt的过期时间,这个过期时间必须要大于签发时间
  • nbf: 定义在什么时间之前,该jwt都是不可用的.
  • iat: jwt的签发时间
  • jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。

# 样例代码

# JAVA(JDK1.6及其以上)

# Maven配置文件依赖

依赖配置如下,其中bcprov-jdk15on和使用的签名私钥格式有关,使用PEM格式的签名私钥时需要使用此引用。

        <!-- jjwt,Java Web Token签名工具包 -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.6.0</version>
        </dependency>
        <!-- bcprov-jdk15on,PEM格式私钥证书读取工具类 -->
        <dependency>
            <groupId>org.bouncycastle</groupId>
            <artifactId>bcprov-jdk15on</artifactId>
            <version>1.52</version>
        </dependency>

# 签名代码

签名工具类

package com.yonyou.einvoice.test;

import java.io.FileInputStream;
import java.io.FileReader;
import java.io.IOException;
import java.security.KeyFactory;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Map;

import org.bouncycastle.util.io.pem.PemReader;

import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.impl.compression.CompressionCodecs;

/**
 * @author wangweir
 *
 */
public class SignUtils {

      private String sign(String paramsMap) throws Exception {

        // 读取CA证书与PEM格式证书需要根据实际证书使用情况而定,目前这两种都支持
        PrivateKey privateKey = loadPrivateKeyOfCA();
        // PrivateKey privateKey = loadPrivateKeyOfPem();

        Map<String, Object> claims =
                JwtParamBuilder.build().setSubject("tester").setIssuer("einvoice").setAudience("einvoice")
                        .addJwtId().addIssuedAt().getClaims();

        // 需要将表单参数requestdatas的数据进行md5加密,然后放到签名数据的requestdatas中。
        // 此签名数据必须存在,否则在验证签名时会不通过。
        System.out.println(getMD5(paramsMap));
        claims.put("requestdatas", getMD5(paramsMap));
        // 使用jdk1.6版本时,删除下面代码的中.compressWith(CompressionCodecs.DEFLATE)
        String compactJws = Jwts.builder().signWith(SignatureAlgorithm.RS512, privateKey)
                .setClaims(claims).compressWith(CompressionCodecs.DEFLATE).compact();

        return compactJws;
    }
  /**
   * 读取PEM编码格式
   * 
   * @return
   * @throws IOException
   * @throws NoSuchAlgorithmException
   * @throws InvalidKeySpecException
   */
  protected static PrivateKey loadPrivateKeyOfPem()
      throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
    PemReader reader = new PemReader(new FileReader("D:\\CA\\keystore\\测试.private"));
    byte[] privateKeyBytes = reader.readPemObject().getContent();
    reader.close();
    PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(privateKeyBytes);
    KeyFactory kf = KeyFactory.getInstance("RSA");
    PrivateKey privateKey = kf.generatePrivate(spec);
    return privateKey;
  }
    /**
     * 计算MD5
     *
     * @param str
     * @return
     * @throws UnsupportedEncodingException
     * @throws NoSuchAlgorithmException
     */
    private String getMD5(String str) throws UnsupportedEncodingException, NoSuchAlgorithmException {
        byte[] buf = null;
        buf = str.getBytes("utf-8");
        MessageDigest md5 = null;
        md5 = MessageDigest.getInstance("MD5");
        md5.update(buf);
        byte[] tmp = md5.digest();
        StringBuilder sb = new StringBuilder();
        for (byte b : tmp) {
            sb.append(String.format("%02x", b & 0xff));
        }
        return sb.toString();
    }
  /**
   * 读取证书私钥
   * 
   * @return
   * @throws UnrecoverableKeyException
   * @throws KeyStoreException
   * @throws NoSuchAlgorithmException
   * @throws CertificateException
   * @throws IOException
   */
  protected static PrivateKey loadPrivateKeyOfCA() throws UnrecoverableKeyException,
      KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException {
    String keypath = "D:\\CA\\keystore\\NC_65_SCM.p12";
    FileInputStream in = new FileInputStream(keypath);
    KeyStore ks = KeyStore.getInstance("pkcs12");
    String pwd = "123456";
    ks.load(in, pwd.toCharArray());
    String alias = ks.aliases().nextElement();
    PrivateKey caprk = (PrivateKey) ks.getKey(alias, pwd.toCharArray());
    return caprk;
  }

}

签名辅助工具类

package com.yonyou.einvoice.test;

import java.util.HashMap;
import java.util.Map;
import java.util.UUID;


/**
 * @author wangweir
 *
 */
public class JwtParamBuilder {

  /** JWT {@code Issuer} claims parameter name: <code>"iss"</code> */
  public static final String ISSUER = "iss";

  /** JWT {@code Subject} claims parameter name: <code>"sub"</code> */
  public static final String SUBJECT = "sub";

  /** JWT {@code Audience} claims parameter name: <code>"aud"</code> */
  public static final String AUDIENCE = "aud";

  /** JWT {@code Expiration} claims parameter name: <code>"exp"</code> */
  public static final String EXPIRATION = "exp";

  /** JWT {@code Not Before} claims parameter name: <code>"nbf"</code> */
  public static final String NOT_BEFORE = "nbf";

  /** JWT {@code Issued At} claims parameter name: <code>"iat"</code> */
  public static final String ISSUED_AT = "iat";

  /** JWT {@code JWT ID} claims parameter name: <code>"jti"</code> */
  public static final String ID = "jti";

  private Map<String, Object> claims;

  private final long now;

  private JwtParamBuilder() {
    claims = new HashMap<>();
    now = System.currentTimeMillis() / 1000l;
  }

  public static JwtParamBuilder build() {
    return new JwtParamBuilder();
  }

  public JwtParamBuilder addIssuedAt() {
    claims.put(ISSUED_AT, now);
    return this;
  }

  public JwtParamBuilder setExpirySeconds(final Integer expirySeconds) {
    claims.put(EXPIRATION, now + expirySeconds);
    return this;
  }

  public JwtParamBuilder setNotBeforeSeconds(final Integer beforeSeconds) {
    claims.put(NOT_BEFORE, now - beforeSeconds);
    return this;
  }

  public JwtParamBuilder setSubject(String sub) {
    addOneClaim(SUBJECT, sub);
    return this;
  }

  public JwtParamBuilder setIssuer(String iss) {
    addOneClaim(ISSUER, iss);
    return this;
  }

  public JwtParamBuilder setAudience(String aud) {
    addOneClaim(AUDIENCE, aud);
    return this;
  }

  public JwtParamBuilder addJwtId() {
    return setJwtId(UUID.randomUUID().toString());
  }

  public JwtParamBuilder setJwtId(String jwtid) {
    addOneClaim(ID, UUID.randomUUID().toString());
    return this;
  }

  public JwtParamBuilder claim(String name, Object value) {
    if (value == null) {
      this.claims.remove(name);
    } else {
      this.claims.put(name, value);
    }
    return this;
  }

  private void addOneClaim(String key, String value) {
    if (value != null && value.length() > 0) {
      claims.put(key, value);
    }
  }


  /**
   * @return the claims
   */
  public Map<String, Object> getClaims() {
    return claims;
  }


}

# JAVA(JDK1.5)

# Maven配置文件依赖

依赖配置如下,其中bcprov-jdk15on和使用的签名私钥格式有关,使用PEM格式的签名私钥时需要使用此引用,签名jar包联系接口调试人获取。

# 签名代码

签名工具类

package com.yonyou.einvoice.test;

import com.auth0.jwt.Algorithm;
import com.auth0.jwt.JWTSigner;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.*;
import java.security.cert.CertificateException;
import java.security.interfaces.RSAPrivateKey;
import java.util.*;

/**
 * @author wangweir
 *
 */
public class SignUtils {

    private String sign(String json) throws Exception {

        // 读取CA证书
        PrivateKey privateKey = loadPrivateKeyOfCA();
        Map<String, Object> claims =
                JwtParamBuilder.build().setSubject("tester").setIssuer("einvoice").setAudience("einvoice")
                        .addJwtId().addIssuedAt().getClaims();
        // 需要将表单参数requestdatas的数据进行md5加密,然后放到签名数据的requestdatas中。
        // 此签名数据必须存在,否则在验证签名时会不通过。
        claims.put("requestdatas", getMD5(json));
        JWTSigner signer = new JWTSigner(privateKey);
        String compactJws = signer.sign(claims, new JWTSigner.Options().setExpirySeconds(300)
                .setNotValidBeforeLeeway(5).setIssuedAt(true).setJwtId(true).setAlgorithm(Algorithm.RS256));
        return compactJws;
    }
 /**
     * 计算MD5
     *
     * @param str
     * @return
     * @throws UnsupportedEncodingException
     * @throws NoSuchAlgorithmException
     */
    private static String getMD5(String str) throws UnsupportedEncodingException,
            NoSuchAlgorithmException {
        byte[] buf = null;
        buf = str.getBytes("utf-8");
        MessageDigest md5 = null;
        md5 = MessageDigest.getInstance("MD5");
        md5.update(buf);
        byte[] tmp = md5.digest();
        StringBuilder sb = new StringBuilder();
        for (byte b : tmp) {
            sb.append(String.format("%02x", b & 0xff));
        }
        return sb.toString();
    }

  /**
   * 读取PEM编码格式
   * 
   * @return
   * @throws IOException
   * @throws NoSuchAlgorithmException
   * @throws InvalidKeySpecException
   */
  protected static PrivateKey loadPrivateKeyOfPem()
      throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
    PemReader reader = new PemReader(new FileReader("D:\\CA\\keystore\\测试.private"));
    byte[] privateKeyBytes = reader.readPemObject().getContent();
    reader.close();
    PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(privateKeyBytes);
    KeyFactory kf = KeyFactory.getInstance("RSA");
    PrivateKey privateKey = kf.generatePrivate(spec);
    return privateKey;
  }

  /**
   * 读取证书私钥
   * 
   * @return
   * @throws UnrecoverableKeyException
   * @throws KeyStoreException
   * @throws NoSuchAlgorithmException
   * @throws CertificateException
   * @throws IOException
   */
    protected RSAPrivateKey loadPrivateKeyOfCA() throws UnrecoverableKeyException, KeyStoreException,
            NoSuchAlgorithmException, CertificateException, IOException {
        FileInputStream in = new FileInputStream(KEYPATH);
        KeyStore ks = KeyStore.getInstance("pkcs12");
        ks.load(in, PASSWORD.toCharArray());
        String alias = ks.aliases().nextElement();
        RSAPrivateKey caprk = (RSAPrivateKey) ks.getKey(alias, PASSWORD.toCharArray());
        return caprk;
    }

}

签名辅助工具类

package test;

import java.util.HashMap;
import java.util.Map;
import java.util.UUID;


/**
 * @author wangweir
 *
 */
public class JwtParamBuilder {

    /** JWT {@code Issuer} claims parameter name: <code>"iss"</code> */
    public static final String ISSUER = "iss";

    /** JWT {@code Subject} claims parameter name: <code>"sub"</code> */
    public static final String SUBJECT = "sub";

    /** JWT {@code Audience} claims parameter name: <code>"aud"</code> */
    public static final String AUDIENCE = "aud";

    /** JWT {@code Expiration} claims parameter name: <code>"exp"</code> */
    public static final String EXPIRATION = "exp";

    /** JWT {@code Not Before} claims parameter name: <code>"nbf"</code> */
    public static final String NOT_BEFORE = "nbf";

    /** JWT {@code Issued At} claims parameter name: <code>"iat"</code> */
    public static final String ISSUED_AT = "iat";

    /** JWT {@code JWT ID} claims parameter name: <code>"jti"</code> */
    public static final String ID = "jti";

    private Map<String, Object> claims;

    private final long now;

    private JwtParamBuilder() {
        claims = new HashMap();
        now = System.currentTimeMillis() / 1000l;
    }

    public static JwtParamBuilder build() {
        return new JwtParamBuilder();
    }

    public JwtParamBuilder addIssuedAt() {
        claims.put(ISSUED_AT, now);
        return this;
    }

    public JwtParamBuilder setExpirySeconds(final Integer expirySeconds) {
        claims.put(EXPIRATION, now + expirySeconds);
        return this;
    }

    public JwtParamBuilder setNotBeforeSeconds(final Integer beforeSeconds) {
        claims.put(NOT_BEFORE, now - beforeSeconds);
        return this;
    }

    public JwtParamBuilder setSubject(String sub) {
        addOneClaim(SUBJECT, sub);
        return this;
    }

    public JwtParamBuilder setIssuer(String iss) {
        addOneClaim(ISSUER, iss);
        return this;
    }

    public JwtParamBuilder setAudience(String aud) {
        addOneClaim(AUDIENCE, aud);
        return this;
    }

    public JwtParamBuilder addJwtId() {
        return setJwtId(UUID.randomUUID().toString());
    }

    public JwtParamBuilder setJwtId(String jwtid) {
        addOneClaim(ID, UUID.randomUUID().toString());
        return this;
    }

    public JwtParamBuilder claim(String name, Object value) {
        if (value == null) {
            this.claims.remove(name);
        } else {
            this.claims.put(name, value);
        }
        return this;
    }

    private void addOneClaim(String key, String value) {
        if (value != null && value.length() > 0) {
            claims.put(key, value);
        }
    }


    /**
     * @return the claims
     */
    public Map<String, Object> getClaims() {
        return claims;
    }


}

# C#

# 添加引用

使用包管理开发工具NuGet添加依赖。NuGet具体使用方法请自行Google。 在程序包管理器控制台输入下列命令安装依赖 1、Install-Package Newtonsoft.Json 2、Install-Package jose-jwt Newtonsoft.Json用来进行json转换 jose-jwt用来进行jwt签名

# API调用代码

using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Web;
using Jose;
using Newtonsoft.Json;

namespace Invoicing
{
    internal class Program
    {
        private static void Main(string[] args)
        {

        }

        /// <summary>
        ///     数据签名
        /// </summary>
        /// <param name="requestdatas"></param>
        /// <returns></returns>
        private static string Sign(string requestdatas)
        {
            var ts = DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0);
            //5分钟内有效
            var exp = ts.TotalMilliseconds + 360000;

            var payload = new Dictionary<string, object>
            {
                {"sub", "tester"},
                {"exp", exp},
                {"requestdatas", GetMd5(requestdatas)}
            };

            var privateKey =
                new X509Certificate2("D:/CA/keystore/pro22.pfx", "password",
                    X509KeyStorageFlags.Exportable | X509KeyStorageFlags.MachineKeySet).PrivateKey as
                    RSACryptoServiceProvider;

            var token = JWT.Encode(payload, privateKey, JwsAlgorithm.PS256);

            return token;
        }

        /// <summary>
        ///     获取md5值
        /// </summary>
        /// <param name="requestdatas"></param>
        /// <returns></returns>
        private static string GetMd5(string requestdatas)
        {
            using (var md5Hash = MD5.Create())
            {
                var hash = GetMd5Hash(md5Hash, requestdatas);
                return hash;
            }
        }

        private static string GetMd5Hash(MD5 md5Hash, string input)
        {
            // Convert the input string to a byte array and compute the hash.
            var data = md5Hash.ComputeHash(Encoding.UTF8.GetBytes(input));

            // Create a new Stringbuilder to collect the bytes
            // and create a string.
            var sBuilder = new StringBuilder();

            // Loop through each byte of the hashed data 
            // and format each one as a hexadecimal string.
            for (var i = 0; i < data.Length; i++)
            {
                sBuilder.Append(data[i].ToString("x2"));
            }

            // Return the hexadecimal string.
            return sBuilder.ToString();
        }

    }
}
Last Updated: 5/3/2021, 1:05:16 AM