content: /**
   * Alipay.com Inc. Copyright (c) 2004-2019 All Rights Reserved.
   */
  package com.alipay.easysdk.kernel.util;
  
  import com.alipay.easysdk.kernel.AlipayConstants;
  
  import java.util.LinkedList;
  
  /**
   * 待验签原文提取器
   * <p>
   * 注：此处不可使用JSON反序列化工具进行提取，会破坏原有格式，对于签名而言差个空格都会验签不通过
   *
   * @author zhongyu
   * @version $Id: SignContentExtractor.java, v 0.1 2019年12月19日 9:07 PM zhongyu Exp $
   */
  public class SignContentExtractor {
      /**
       * 左大括号
       */
      public static final char LEFT_BRACE = '{';
  
      /**
       * 右大括号
       */
      public static final char RIGHT_BRACE = '}';
  
      /**
       * 双引号
       */
      public static final char DOUBLE_QUOTES = '"';
  
      /**
       * 获取待验签的原文
       *
       * @param body   网关的整体响应字符串
       * @param method 本次调用的OpenAPI接口名称
       * @return 待验签的原文
       */
      public static String getSignSourceData(String body, String method) {
          // 加签源串起点
          String rootNode = method.replace('.', '_') + AlipayConstants.RESPONSE_SUFFIX;
          String errorRootNode = AlipayConstants.ERROR_RESPONSE;
  
          int indexOfRootNode = body.indexOf(rootNode);
          int indexOfErrorRoot = body.indexOf(errorRootNode);
  
          if (indexOfRootNode > 0) {
              return parseSignSourceData(body, rootNode, indexOfRootNode);
          } else if (indexOfErrorRoot > 0) {
              return parseSignSourceData(body, errorRootNode, indexOfErrorRoot);
          } else {
              return null;
          }
      }
  
      private static String parseSignSourceData(String body, String rootNode, int indexOfRootNode) {
          //第一个字母 + 长度 + 冒号 + 引号
          int signDataStartIndex = indexOfRootNode + rootNode.length() + 2;
  
          int indexOfSign = body.indexOf("\"" + AlipayConstants.SIGN_FIELD + "\"");
          if (indexOfSign < 0) {
              return null;
          }
  
          SignSourceData signSourceData = extractSignContent(body, signDataStartIndex);
  
          //如果提取的待验签原始内容后还有rootNode
          if (body.lastIndexOf(rootNode) > signSourceData.getEndIndex()) {
              throw new RuntimeException("检测到响应报文中有重复的" + rootNode + "，验签失败。");
          }
  
          return signSourceData.getSourceData();
      }
  
      private static SignSourceData extractSignContent(String str, int begin) {
          if (str == null) {
              return null;
          }
  
          int beginIndex = extractBeginPosition(str, begin);
          if (beginIndex >= str.length()) {
              return null;
          }
  
          int endIndex = extractEndPosition(str, beginIndex);
          return new SignSourceData(str.substring(beginIndex, endIndex), beginIndex, endIndex);
      }
  
      private static int extractBeginPosition(String responseString, int begin) {
          int beginPosition = begin;
          //找到第一个左大括号（对应响应的是JSON对象的情况：普通调用OpenAPI响应明文）
          //或者双引号（对应响应的是JSON字符串的情况：加密调用OpenAPI响应Base64串），作为待验签内容的起点
          while (beginPosition < responseString.length()
                  && responseString.charAt(beginPosition) != LEFT_BRACE
                  && responseString.charAt(beginPosition) != DOUBLE_QUOTES) {
              ++beginPosition;
          }
          return beginPosition;
      }
  
      private static int extractEndPosition(String responseString, int beginPosition) {
          //提取明文验签内容终点
          if (responseString.charAt(beginPosition) == LEFT_BRACE) {
              return extractJsonObjectEndPosition(responseString, beginPosition);
          }
          //提取密文验签内容终点
          else {
              return extractJsonBase64ValueEndPosition(responseString, beginPosition);
          }
      }
  
      private static int extractJsonBase64ValueEndPosition(String responseString, int beginPosition) {
          for (int index = beginPosition; index < responseString.length(); ++index) {
              //找到第2个双引号作为终点，由于中间全部是Base64编码的密文，所以不会有干扰的特殊字符
              if (responseString.charAt(index) == DOUBLE_QUOTES && index != beginPosition) {
                  return index + 1;
              }
          }
          //如果没有找到第2个双引号，说明验签内容片段提取失败，直接尝试选取剩余整个响应字符串进行验签
          return responseString.length();
      }
  
      private static int extractJsonObjectEndPosition(String responseString, int beginPosition) {
          //记录当前尚未发现配对闭合的大括号
          LinkedList<Character> braces = new LinkedList<Character>();
          //记录当前字符是否在双引号中
          boolean inQuotes = false;
          //记录当前字符前面连续的转义字符个数
          int consecutiveEscapeCount = 0;
          //从待验签字符的起点开始遍历后续字符串，找出待验签字符串的终止点，终点即是与起点{配对的}
          for (int index = beginPosition; index < responseString.length(); ++index) {
              //提取当前字符
              char currentChar = responseString.charAt(index);
  
              //如果当前字符是"且前面有偶数个转义标记（0也是偶数）
              if (currentChar == DOUBLE_QUOTES && consecutiveEscapeCount % 2 == 0) {
                  //是否在引号中的状态取反
                  inQuotes = !inQuotes;
              }
              //如果当前字符是{且不在引号中
              else if (currentChar == LEFT_BRACE && !inQuotes) {
                  //将该{加入未闭合括号中
                  braces.push(LEFT_BRACE);
              }
              //如果当前字符是}且不在引号中
              else if (currentChar == RIGHT_BRACE && !inQuotes) {
                  //弹出一个未闭合括号
                  braces.pop();
                  //如果弹出后，未闭合括号为空，说明已经找到终点
                  if (braces.isEmpty()) {
                      return index + 1;
                  }
              }
  
              //如果当前字符是转义字符
              if (currentChar == '\\') {
                  //连续转义字符个数+1
                  ++consecutiveEscapeCount;
              } else {
                  //连续转义字符个数置0
                  consecutiveEscapeCount = 0;
              }
          }
  
          //如果没有找到配对的闭合括号，说明验签内容片段提取失败，直接尝试选取剩余整个响应字符串进行验签
          return responseString.length();
      }
  
      private static class SignSourceData {
          /**
           * 待验签原始内容
           */
          private final String sourceData;
          /**
           * 待验签原始内容在响应字符串中的起始位置
           */
          private final int    beginIndex;
          /**
           * 待验签原始内容在响应字符串中的结束位置
           */
          private final int    endIndex;
  
          SignSourceData(String sourceData, int beginIndex, int endIndex) {
              this.sourceData = sourceData;
              this.beginIndex = beginIndex;
              this.endIndex = endIndex;
          }
  
          String getSourceData() {
              return sourceData;
          }
  
          public int getBeginIndex() {
              return beginIndex;
          }
  
          int getEndIndex() {
              return endIndex;
          }
      }
  }