chengkun
2025-06-05 4080b5997b38ca84b3b203c7101dcadb97b76925
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
/**
 * 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;
        }
    }
}