APP认证签名串生成
更新时间:2021-09-17
简介
API网关为API提供了APP认证方式,只有携带合法签名的请求,才能访问开启了APP认证的API。
当用户调用API时,需要使用已授权的APP对应的AccessKey、SecretKey对请求进行签名,并将签名串放置于X-Bce-Signature请求头中。
签名流程
签名流程及具体算法可参考:签名流程。
注意:上述参考流程中,放置签名的请求头为Authorization,APP认证放置签名的请求头为X-Bce-Signature,除此之外两者均一致。
Java签名代码
                Java
                
            
            1/*
2 * Copyright (C) 2020 Baidu, Inc. All Rights Reserved.
3 */
4package com.baidubce.apigw;
5
6import javax.crypto.Mac;
7import javax.crypto.spec.SecretKeySpec;
8import java.net.URI;
9import java.nio.charset.StandardCharsets;
10import java.time.Instant;
11import java.time.ZoneOffset;
12import java.time.format.DateTimeFormatter;
13import java.util.*;
14
15/**
16 * App signer using the BCE signing protocol.
17 */
18public class AppSigner {
19
20    private static final String BCE_AUTH_VERSION = "bce-auth-v1";
21    private static final String BCE_PREFIX_LOWER_CASE = "x-bce-";
22    private static final String SIGNATURE_HEADER_NAME = "X-Bce-Signature";
23
24    private static final Set<String> DEFAULT_HEADERS_TO_SIGN = new HashSet<>(Arrays.asList(
25            "host", "content-length", "content-type", "content-md5"));
26
27    private static final DateTimeFormatter DATE_TIME_FORMATTER =
28            DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'")
29                    .withZone(ZoneOffset.UTC);
30
31    private static final String HEADER_JOINER = "\n";
32    private static final String QUERY_STRING_JOINER = "&";
33    private static final String SIGNED_HEADER_STRING_JOINER = ";";
34
35    private final Credential credential;
36
37    public AppSigner(Credential credential) {
38        this.credential = credential;
39    }
40
41    /**
42     * Usage sample.
43     */
44    public static void main(String[] args) {
45        Credential credential = new Credential("<access key>", "<secret key>");
46        AppSigner appSigner = new AppSigner(credential);
47
48        String httpMethod = "GET";
49        URI uri = URI.create("https://www.baidu.com/path?query1=valu1&query2=value2");
50        Map<String, String> headers = new HashMap<>();
51        headers.put("Host", uri.getHost());
52
53        // Signature header will be added to headers.
54        appSigner.sign(httpMethod, uri, headers);
55    }
56
57    /**
58     * Sign the given request with the given credential.
59     * <p>
60     * Signature header will be added to the passed-in request.
61     *
62     * @param method http method
63     * @param uri path and query will be used
64     * @param headers must contain host header
65     */
66    public void sign(String method, URI uri, Map<String, String> headers) {
67        sign(method, uri, headers, null);
68    }
69
70    /**
71     * Sign the given request with the given credential.
72     * <p>
73     * Signature header will be added to the passed-in request.
74     *
75     * @param method http method
76     * @param uri path and query will be used to generate signature
77     * @param headers must contain host header
78     * @param options nullable
79     */
80    public void sign(String method, URI uri, Map<String, String> headers, SignOptions options) {
81        Map<String, String> httpHeaders = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
82        httpHeaders.putAll(headers);
83
84        Objects.requireNonNull(httpHeaders.get("host"), "Host header is required.");
85
86        if (options == null) {
87            options = SignOptions.DEFAULT;
88        }
89
90        String timestamp = options.getTimestamp();
91
92        if (timestamp == null) {
93            timestamp = DATE_TIME_FORMATTER.format(Instant.now());
94        }
95
96        String authString = BCE_AUTH_VERSION + "/" + credential.getAccessKey() + "/"
97                + timestamp + "/" + options.getExpirationInSeconds();
98
99        String signingKey = SignUtils.sha256Hex(credential.getSecretKey(), authString);
100        // Formatting the URL with signing protocol.
101        String canonicalURI = this.getCanonicalURIPath(uri.getPath());
102        // Formatting the query string with signing protocol.
103        String canonicalQueryString = getCanonicalQueryString(uri.getRawQuery());
104        // Sorted the headers should be signed from the request.
105        SortedMap<String, String> headersToSign =
106                getHeadersToSign(httpHeaders, options.getHeadersToSign());
107        // Formatting the headers from the request based on signing protocol.
108        String canonicalHeader = this.getCanonicalHeaders(headersToSign);
109        String signedHeaders = "";
110        if (options.getHeadersToSign() != null) {
111            signedHeaders = SignUtils.join(SIGNED_HEADER_STRING_JOINER, headersToSign.keySet());
112            signedHeaders = signedHeaders.trim().toLowerCase();
113        }
114
115        String canonicalRequest = method.toUpperCase() + "\n" + canonicalURI + "\n"
116                + canonicalQueryString + "\n" + canonicalHeader;
117
118        // Signing the canonical request using key with sha-256 algorithm.
119        String signature = SignUtils.sha256Hex(signingKey, canonicalRequest);
120
121        String signatureHeaderValue = authString + "/" + signedHeaders + "/" + signature;
122
123        System.out.println("CanonicalRequest:" + canonicalRequest.replace("\n", "[\\n]"));
124        System.out.println(SIGNATURE_HEADER_NAME + ":" + signatureHeaderValue);
125
126        headers.put(SIGNATURE_HEADER_NAME, signatureHeaderValue);
127    }
128
129    private String getCanonicalURIPath(String path) {
130        if (path == null) {
131            return "/";
132        } else if (path.startsWith("/")) {
133            return SignUtils.normalizePath(path);
134        } else {
135            return "/" + SignUtils.normalizePath(path);
136        }
137    }
138
139    private static String getCanonicalQueryString(String queryStr) {
140        if (queryStr == null || queryStr.isEmpty()) {
141            return "";
142        }
143
144        List<String> queryStrings = new ArrayList<>();
145
146        for (String pair : queryStr.split("&")) {
147            String[] kv = pair.split("=", 2);
148            String queryString = SignUtils.normalize(kv[0]) + "=";
149            if (kv.length == 2) {
150                queryString += SignUtils.normalize(kv[1]);
151            }
152            queryStrings.add(queryString);
153        }
154
155        Collections.sort(queryStrings);
156
157        return SignUtils.join(QUERY_STRING_JOINER, queryStrings);
158    }
159
160    private String getCanonicalHeaders(SortedMap<String, String> headers) {
161        if (headers.isEmpty()) {
162            return "";
163        }
164
165        List<String> headerStrings = new ArrayList<>();
166        for (Map.Entry<String, String> entry : headers.entrySet()) {
167            String key = entry.getKey();
168            if (key == null) {
169                continue;
170            }
171            String value = entry.getValue();
172            if (value == null || value.isEmpty()) {
173                throw new RuntimeException("Header to sign should have non-empty value.");
174            }
175            headerStrings.add(SignUtils.normalize(key.trim().toLowerCase()) + ':' + SignUtils.normalize(value.trim()));
176        }
177        Collections.sort(headerStrings);
178
179        return SignUtils.join(HEADER_JOINER, headerStrings);
180    }
181
182    private SortedMap<String, String> getHeadersToSign(Map<String, String> headers, Set<String> headersToSign) {
183        SortedMap<String, String> ret = new TreeMap<>();
184        if (headersToSign != null) {
185            Set<String> tempSet = new HashSet<>();
186            for (String header : headersToSign) {
187                tempSet.add(header.trim().toLowerCase());
188            }
189            headersToSign = tempSet;
190        }
191        for (Map.Entry<String, String> entry : headers.entrySet()) {
192            String key = entry.getKey();
193            if (entry.getValue() != null && !entry.getValue().isEmpty()) {
194                boolean isDefaultHeaderToSign = headersToSign == null && this.isDefaultHeaderToSign(key);
195                boolean isNeededHeaderToSign = (headersToSign != null && headersToSign.contains(key.toLowerCase())
196                        && !SIGNATURE_HEADER_NAME.equalsIgnoreCase(key));
197                if (isDefaultHeaderToSign || isNeededHeaderToSign) {
198                    ret.put(key, entry.getValue());
199                }
200            }
201        }
202        return ret;
203    }
204
205    private boolean isDefaultHeaderToSign(String header) {
206        header = header.trim().toLowerCase();
207        return header.startsWith(BCE_PREFIX_LOWER_CASE) || DEFAULT_HEADERS_TO_SIGN.contains(header);
208    }
209
210    public static class SignUtils {
211
212        private static final BitSet URI_UNRESERVED_CHARACTERS = new BitSet();
213        private static final String[] PERCENT_ENCODED_STRINGS = new String[256];
214        private static final char[] HEX_ARRAY = "0123456789abcdef".toCharArray();
215
216        static {
217            for (int i = 'a'; i <= 'z'; i++) {
218                URI_UNRESERVED_CHARACTERS.set(i);
219            }
220            for (int i = 'A'; i <= 'Z'; i++) {
221                URI_UNRESERVED_CHARACTERS.set(i);
222            }
223            for (int i = '0'; i <= '9'; i++) {
224                URI_UNRESERVED_CHARACTERS.set(i);
225            }
226            URI_UNRESERVED_CHARACTERS.set('-');
227            URI_UNRESERVED_CHARACTERS.set('.');
228            URI_UNRESERVED_CHARACTERS.set('_');
229            URI_UNRESERVED_CHARACTERS.set('~');
230
231            for (int i = 0; i < PERCENT_ENCODED_STRINGS.length; ++i) {
232                PERCENT_ENCODED_STRINGS[i] = String.format("%%%02X", i);
233            }
234        }
235
236        public static String join(String joinerStr, Iterable<String> iterable) {
237            StringJoiner joiner = new StringJoiner(joinerStr);
238            iterable.forEach(joiner::add);
239            return joiner.toString();
240        }
241
242        public static String normalizePath(String path) {
243            return normalize(path).replace("%2F", "/");
244        }
245
246        /**
247         * Normalize a string for use in BCE web service APIs. The normalization algorithm is:
248         * <p>
249         * <ol>
250         *   <li>Convert the string into a UTF-8 byte array.</li>
251         *   <li>Encode all octets into percent-encoding, except all URI unreserved characters per the RFC 3986.</li>
252         * </ol>
253         *
254         * <p>
255         * All letters used in the percent-encoding are in uppercase.
256         *
257         * @param value the string to normalize.
258         * @return the normalized string.
259         */
260        public static String normalize(String value) {
261            StringBuilder builder = new StringBuilder();
262            for (byte b : value.getBytes(StandardCharsets.UTF_8)) {
263                if (URI_UNRESERVED_CHARACTERS.get(b & 0xFF)) {
264                    builder.append((char) b);
265                } else {
266                    builder.append(PERCENT_ENCODED_STRINGS[b & 0xFF]);
267                }
268            }
269            return builder.toString();
270        }
271
272        private static String sha256Hex(String signingKey, String stringToSign) {
273            try {
274                Mac instance = Mac.getInstance("HmacSHA256");
275                instance.init(new SecretKeySpec(signingKey.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
276                return bytesToHex(instance.doFinal(stringToSign.getBytes(StandardCharsets.UTF_8)));
277            } catch (Exception e) {
278                throw new RuntimeException("Failed to generate signature.", e);
279            }
280        }
281
282        private static String bytesToHex(byte[] bytes) {
283            char[] hexChars = new char[bytes.length * 2];
284            for (int j = 0; j < bytes.length; j++) {
285                int v = bytes[j] & 0xFF;
286                hexChars[j * 2] = HEX_ARRAY[v >>> 4];
287                hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F];
288            }
289            return new String(hexChars);
290        }
291    }
292
293    public static class SignOptions {
294
295        public static final SignOptions DEFAULT = new SignOptions();
296
297        private String timestamp;
298        private int expirationInSeconds = 1800;
299        private Set<String> headersToSign;
300
301        public String getTimestamp() {
302            return timestamp;
303        }
304
305        public void setTimestamp(String timestamp) {
306            this.timestamp = timestamp;
307        }
308
309        public int getExpirationInSeconds() {
310            return expirationInSeconds;
311        }
312
313        public void setExpirationInSeconds(int expirationInSeconds) {
314            this.expirationInSeconds = expirationInSeconds;
315        }
316
317        public Set<String> getHeadersToSign() {
318            return headersToSign;
319        }
320
321        public void setHeadersToSign(Set<String> headersToSign) {
322            this.headersToSign = headersToSign;
323        }
324    }
325
326    public static class Credential {
327        private String accessKey;
328        private String secretKey;
329
330        public Credential(String accessKey, String secretKey) {
331            this.accessKey = accessKey;
332            this.secretKey = secretKey;
333        }
334
335        public String getAccessKey() {
336            return accessKey;
337        }
338
339        public void setAccessKey(String accessKey) {
340            this.accessKey = accessKey;
341        }
342
343        public String getSecretKey() {
344            return secretKey;
345        }
346
347        public void setSecretKey(String secretKey) {
348            this.secretKey = secretKey;
349        }
350    }
351
352}