更新时间:2024-03-05 GMT+08:00

开发Authorization签名生成机制

您需要开发一个生成Authorization签名的认证机制,云客服需要用您生成的authorization签名来进行鉴权。

在开发前,请您先了解生成Authorization签名的内部规则,如图1所示。

图1 Authorization签名生成机制的主要规则

了解完以上原理与规则后,请您依据以下步骤进行Authorization签名认证机制的开发:

  1. 生成SignedHeaders:遍历参与编码的HttpHead里面的Header name。

    1. 将Header name都改为小写,即调用lowerCase()函数,SignerUtils工具类请参考1,参考如下:
      private Map<String, String> lowerCaseSignedHeaders(Map<String, String> signedHeaders) {
           if ((null == signedHeaders) || signedHeaders.isEmpty()) {
               throw new IllegalArgumentException("signedHeaders cann't be null.");
           }
           Map<String, String> headers = new HashMap<>(SignerUtils.HASH_MAP_INITIALIZATION_SIZE);
           for (Map.Entry<String, String> e : signedHeaders.entrySet()) {
               String name = e.getKey();
               String value = e.getValue();
               headers.put(name.toLowerCase(Locale.ENGLISH), value.trim());
           }
           return headers; 
      }
    2. 步骤1-1中Header name转换后的字符后面追加分割符";",生成一条记录,注意最后一个字段不追加";"。
    3. 步骤1-1中所有记录按照字典排序,然后按照顺序连接成一个大字符串,参考如下。
      private String appendSignedHeaders(StringBuilder buffer) {
           int start = buffer.length();
            Set<String> headerNames = new TreeSet<>(this.signedHeaders.keySet());
           for (String name : headerNames) {
               buffer.append(name).append(';');
           }
           buffer.deleteCharAt(buffer.length() - 1);
            int end = buffer.length();
           String signedHeadersStr = buffer.substring(start, end);
           return signedHeadersStr; 
      }

      请对以下Header进行编码:

      Content-Length="***"

      Content-Type="application/json;charset=UTF-8"

  2. 生成authStringPrefix。

    将以下字段用“/”进行拼接authVersion、accessKey、timestamp、SignedHeaders。格式如下:

    authStringPrefix="auth-v2/{accessKey}/{timestamp}/{SignedHeaders}";
    • auth-v2:鉴权版本号,当期版本为固定字符串“auth-v2”。
    • accessKey:第三方系统使用configId(渠道ID)作为唯一标识。
    • timestamp:取第三方发起服务时的时间,timestamp为String类型。时间字符串格式化为"yyyy-MM-dd'T'HH:mm:ss.SSS'Z"。
    • SignedHeaders:参与编码的HttpHead里面的Header name,由步骤1生成。

  3. 生成signingKey。

    步骤2生成的authStringPrefix按照sha256Hex进行加密,其中密钥SecretKey为第三方系统在渠道配置页面配置的密钥。sha256Hex算法参考如下,SignerUtils工具类请参考1

    public static String sha256Hex(String key, String toSigned) throws NoSuchAlgorithmException,InvalidKeyException, UnsupportedEncodingException {
         Mac mac = Mac.getInstance("HmacSHA256");
         mac.init(new SecretKeySpec(key.getBytes(SignerUtils.CHARSET), "HmacSHA256"));
         String digit = new String(SignerUtils.encodeHex(mac.doFinal(toSigned.getBytes(SignerUtils.CHARSET))));
         return digit; 
    }

  4. 生成CanonicalHeaders。

    要求计算的编码规则与SignedHeaders一致,但增加了head value的编码。

    1. 遍历参与编码的HttpHead里面的head name,将Header name都改为小写,即调用lowerCase()函数(请参考步骤1)。
    2. 调用normalize函数,对刚才转换后的小写字符串进行格式化,PathUtils工具类请参考2
      /**
        * normalize
        * @param value payload信息
        * @return builder
        */
      public static String normalize(String value) {
           try {
               StringBuilder builder = new StringBuilder(PathUtils.DEFAULT_CAPACIT);
               for (byte b : value.getBytes(PathUtils.CHARSET)) {
                   if (PathUtils.URI_UNRESERVED_CHARACTERS.get(b & 0xFF)) {
                       builder.append((char) b);
                   } else {
                       builder.append(PathUtils.PERCENT_ENCODED_STRINGS[b & 0xFF]);
                   }
               }
               return builder.toString();
           } catch (UnsupportedEncodingException e) {
               throw new RuntimeException(e);
           } 
      }
    3. 步骤4-1中的记录按照字典排序,进行排序。
    4. 遍历排序后的记录,中间追加字符串"\n"连接成一个大的字符串,最后一条记录不追加"\n"。

  5. 生成canonicalRequest。

    将字段“HttpMethod”“HttpURI”“SignedHeaders”“CanonicalHeaders”“NormalizePath”用“\n”拼接,最后一条记录不追加"\n",PathUtils工具类请参考2。。

    private String canonicalRequest() {
         StringBuilder buffer = new StringBuilder(PathUtils.DEFAULT_CAPACITY);
         buffer.append(this.httpMethod).append(System.lineSeparator());
         buffer.append(this.uri).append(System.lineSeparator());
           this.appendSignedHeaders(buffer);
         buffer.append(System.lineSeparator());
           this.appendCanonicalHeaders(buffer);
         buffer.append(System.lineSeparator());
           if (this.isNotEmpty(this.payload))
          {
             buffer.append(PathUtils.normalize(this.payload));
          }
          return buffer.toString();
     }

    格式参考如下:

    CanonicalRequest = $HttpMethod + "\n" + $HttpURI+ "\n" + SignedHeaders($HttpHeaders) + "\n" + CanonicalHeaders ($HttpHeaders) + "\n" + NormalizePath($HttpBody)
    • CanonicalRequest参数说明:

      $HttpMethod:指HTTPS协议中定义的GET、PUT、POST等请求,必须使用全大写的形式。

      $HttpURI:指接口请求的URI,必须以“/”开头,不以“/”开头的需要补充上,空路径为“/”,样例:/service-cloud/webclient/chat_client/js/newThirdPartyClient.js。

      SignedHeaders:步骤1生成的SignedHeaders。

      CanonicalHeaders:步骤4生成的CanonicalHeaders。

      NormalizePath:格式化处理后的Body体。

    • 仅对NormalizePath中的如下参数进行编码:

      thirdUserName:企业的用户名。

      thirdUserId:企业用户ID。

      tenantSpaceId:企业系统提供的租间ID。

      channelConfigId:企业接入的渠道ID。

  6. 生成signature。将步骤5生成的canonicalRequest再按照sha256Hex进行加密,此处加密的密钥key为步骤3生成的signingKey。
  7. 生成签名authorization:将步骤2生成的authStringPrefix和步骤6生成的signature用“/”进行拼接。格式参考如下:

    Authorization:$authStringPrefix/$Signature

相关参考

生成Authorization签名的认证机制过程中,涉及到SignerUtils与PathUtils工具类,格式参考如下:

  1. SignerUtils
    import java.nio.charset.StandardCharsets;
    import java.util.HashMap;
    import java.util.Locale;
    import java.util.Map;
    
    public class SignerUtils {
        private static final int HASH_MAP_INITIALIZATION_SIZE = 5;
        private static final int ONE_CHAR_BITS_NUM = 4;
        private static final String CHARSET = "UTF-8";
        private static final char[] DIGITS_LOWERS = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a',
            'b', 'c', 'd', 'e', 'f'};
        private SignerUtils() {
        }
    
        private static char[] encodeHex(final byte[] data) {
            final int le = data.length;
            final char[] outs = new char[le << 1];
            for (int i = 0, j = 0; i < le; i++) {
                outs[j++] = SignerUtils.DIGITS_LOWERS[(0xF0 & data[i]) >>> ONE_CHAR_BITS_NUM];
                outs[j++] = SignerUtils.DIGITS_LOWERS[0x0F & data[i]];
            }
            return outs;
        }
    }
  2. PathUtils
    import java.io.UnsupportedEncodingException;
    import java.util.BitSet;
    import java.util.Locale;
    import java.util.concurrent.CompletionException;
    
    public class PathUtils {
        private static final String CHARSET = "UTF-8";
        private static final int NUM_256 = 256;
        private static final int DEFAULT_CAPACITY = 16;
        private static final BitSet URI_UNRESERVED_CHARACTERS = new BitSet();
        private static final String[] PERCENT_ENCODED_STRINGS = new String[NUM_256];
        static {
            for (int i = 97; i <= 122; i++) {
                PathUtils.URI_UNRESERVED_CHARACTERS.set(i);
            }
            for (int i = 65; i <= 90; i++) {
                PathUtils.URI_UNRESERVED_CHARACTERS.set(i);
            }
            for (int i = 48; i <= 57; i++) {
                PathUtils.URI_UNRESERVED_CHARACTERS.set(i);
            }
            PathUtils.URI_UNRESERVED_CHARACTERS.set(45);
            PathUtils.URI_UNRESERVED_CHARACTERS.set(46);
            PathUtils.URI_UNRESERVED_CHARACTERS.set(95);
            PathUtils.URI_UNRESERVED_CHARACTERS.set(126);
    
            for (int i = 0; i < PathUtils.PERCENT_ENCODED_STRINGS.length; i++) {
                PathUtils.PERCENT_ENCODED_STRINGS[i] = String.format(Locale.ROOT, "%%%02X", new Object[]{Integer.valueOf(i)});
            }
        }
        private PathUtils() {}
    }