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}