应用场景
V4签名下,使用HttpURLConnection开发。
前提条件
已开通对象存储(经典版)Ⅰ型服务。
具体操作
可以参考下列示例进行HttpURLConnection开发。
package cn.ctyun.oos.sample;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.security.MessageDigest;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.SortedMap;
import java.util.TimeZone;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
public class OOSDemoForV4Signer {
private static final SimpleDateFormat ISO8601_DATE_FORMAT = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'");
private static final SimpleDateFormat ISO8601_DAY_FORMAT = new SimpleDateFormat("yyyyMMdd");
private static final char[] HEX_CODE = "0123456789abcdef".toCharArray();
private static final String UNSIGNED_PAYLOAD = "UNSIGNED-PAYLOAD";
private static final String SCHEME = "AWS4";
private static final String ALGORITHM = "HMAC-SHA256";
private static final String TERMINATOR = "aws4_request";
private static final String HMAC_SHA256 = "HmacSHA256";
private static final String OOS_ACCESS_KEY = "your_access_key";
private static final String OOS_SECRET_KEY = "your_secret_key";
private static final String OOS_ENDPOINT = "oos-cn.ctyunapi.cn";
private static final String OOS_BUCKET = "your bucket name";
private static final String OOS_OBJECT_NAME = "your_object_name";
private static final String OOS_OBJECT_CONTENT = "your_object_content";
private static final int DEFAULT_TIMEOUT = 30000;
static {
TimeZone utc = TimeZone.getTimeZone("UTC");
ISO8601_DATE_FORMAT.setTimeZone(utc);
ISO8601_DAY_FORMAT.setTimeZone(utc);
}
public static void main(String[] args) throws Exception {
OOSDemoForV4Signer demo = new OOSDemoForV4Signer();
demo.putObject();
demo.getObject();
demo.deleteObject();
}
public void getObject() {
try {
HttpURLConnection connection = generateConnection("GET", OOS_BUCKET, OOS_OBJECT_NAME);
connection.connect();
int responseCode = connection.getResponseCode();
// 在responseCode为200 的情况下, 可将connection.getInputStream()的对象数据读出。
if(responseCode == 200) {
System.out.println("get object success");
}
try (InputStream inputStream =
responseCode == 200 ? connection.getInputStream() : connection.getErrorStream()) {
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
}
} catch (Exception e) {
// 异常可选择抛出或者处理掉。
e.printStackTrace();
}
}
public void putObject() {
try {
HttpURLConnection connection = generateConnection("PUT", OOS_BUCKET, OOS_OBJECT_NAME);
connection.setDoOutput(true);
connection.connect();
// Create the object
byte[] requestBody = OOS_OBJECT_CONTENT.getBytes();
try (OutputStream outputStream = connection.getOutputStream()) {
outputStream.write(requestBody);
}
// Execute the request and print the response
int responseCode = connection.getResponseCode();
if (responseCode == 200) {
System.out.println("put object success");
} else {
try (InputStream inputStream = connection.getErrorStream()) {
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
}
}
} catch (Exception e) {
// 异常可选择抛出或者处理掉。
e.printStackTrace();
}
}
public void deleteObject() {
try {
HttpURLConnection connection = generateConnection("DELETE", OOS_BUCKET, OOS_OBJECT_NAME);
connection.connect();
int responseCode = connection.getResponseCode();
if (responseCode == 204) {
System.out.println("delete object success");
} else {
try (InputStream inputStream = connection.getErrorStream()) {
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
}
}
} catch (Exception e) {
// 异常可选择抛出或者处理掉。
e.printStackTrace();
}
}
private HttpURLConnection generateConnection(String method, String bucket, String objectKey) throws Exception {
String requestUrl = "http://" + bucket + "." + OOS_ENDPOINT + "/" + urlEncode(objectKey, true);
Map<String, String> headers = new HashMap<>();
// 1 加在headers里的所有头域,都参与计算签名。
// 2 任何头以x-amz-meta-这个前缀开始都会被认为是用户的元数据,当用户检索时,它将会和对象一起被存储并返回。PUT请求头大小限制为8KiB。在PUT请求头中,用户定义的元数据大小限制为2KiB。
// headers.put("x-amz-meta-test", "oos");
Map<String, String> querys = new HashMap<String, String>();
URL url = new URL(requestUrl);
String authorization = v4Sign(headers, querys, UNSIGNED_PAYLOAD, url, method);
headers.put("Authorization", authorization);
HttpURLConnection connection = (HttpURLConnection)new URL(requestUrl).openConnection();
connection.setRequestMethod(method);
connection.setConnectTimeout(DEFAULT_TIMEOUT);
connection.setReadTimeout(DEFAULT_TIMEOUT);
headers.forEach(connection::setRequestProperty);
return connection;
}
/** =============== 以下是签名计算相关方法 =============== **/
private String v4Sign(Map<String, String> headers, Map<String, String> queryParameters, String bodyHash,
URL endpointUrl, String httpMethod) {
String host = endpointUrl.getHost();
String serviceName = parseServiceName(host);
String regionName = parseRegionName(host);
// first get the date and time for the subsequent request, and convert
// to ISO 8601 format for use in signature generation
Date now = new Date();
String dateTimeStamp = ISO8601_DATE_FORMAT.format(now);
// update the headers with required 'x-amz-date' and 'host' values
if (headers == null) {
headers = new HashMap<String, String>();
}
headers.put("x-amz-date", dateTimeStamp);
headers.put("x-amz-content-sha256", bodyHash);
int port = endpointUrl.getPort();
if (port > -1 && port != 80 && port != 443) {
host = host.concat(":" + Integer.toString(port));
}
headers.put("Host", host);
// canonicalize the headers; we need the set of header names as well as the
// names and values to go into the signature process
String canonicalizedHeaderNames = getCanonicalizeHeaderNames(headers);
String canonicalizedHeaders = getCanonicalizedHeaderString(headers);
// if any query string parameters have been supplied, canonicalize them
String canonicalizedQueryParameters = getCanonicalizedQueryString(queryParameters);
// canonicalize the various components of the request
String canonicalRequest = getCanonicalRequest(endpointUrl, httpMethod, canonicalizedQueryParameters,
canonicalizedHeaderNames, canonicalizedHeaders, bodyHash);
// construct the string to be signed
String dateStamp = ISO8601_DAY_FORMAT.format(now);
String scope = dateStamp + "/" + regionName + "/" + serviceName + "/" + TERMINATOR;
String stringToSign = getStringToSign(SCHEME, ALGORITHM, dateTimeStamp, scope, canonicalRequest);
// compute the signing key
byte[] kSigning = createSignatureKey(OOS_SECRET_KEY, dateStamp, regionName, serviceName);
byte[] signature = sign(stringToSign, kSigning, HMAC_SHA256);
String credentialsAuthorizationHeader = "Credential=" + OOS_ACCESS_KEY + "/" + scope;
String signedHeadersAuthorizationHeader = "SignedHeaders=" + canonicalizedHeaderNames;
String signatureAuthorizationHeader = "Signature=" + toHex(signature);
String authorizationHeader = SCHEME + "-" + ALGORITHM + " " + credentialsAuthorizationHeader + ", "
+ signedHeadersAuthorizationHeader + ", " + signatureAuthorizationHeader;
return authorizationHeader;
}
private String getCanonicalizedQueryString(Map<String, String> parameters) {
if (parameters == null || parameters.isEmpty()) {
return "";
}
SortedMap<String, String> sorted = new TreeMap<String, String>();
Iterator<Map.Entry<String, String>> pairs = parameters.entrySet().iterator();
while (pairs.hasNext()) {
Map.Entry<String, String> pair = pairs.next();
String key = pair.getKey();
String value = pair.getValue();
sorted.put(urlEncode(key, false), urlEncode(value, false));
}
StringBuilder builder = new StringBuilder();
pairs = sorted.entrySet().iterator();
while (pairs.hasNext()) {
Map.Entry<String, String> pair = pairs.next();
builder.append(pair.getKey());
builder.append("=");
builder.append(pair.getValue());
if (pairs.hasNext()) {
builder.append("&");
}
}
return builder.toString();
}
private String getCanonicalizedHeaderString(Map<String, String> headers) {
if (headers == null || headers.isEmpty()) {
return "";
}
// step1: sort the headers by case-insensitive order
List<String> sortedHeaders = new ArrayList<String>();
sortedHeaders.addAll(headers.keySet());
Collections.sort(sortedHeaders, String.CASE_INSENSITIVE_ORDER);
// step2: form the canonical header:value entries in sorted order.
// Multiple white spaces in the values should be compressed to a single
// space.
StringBuilder buffer = new StringBuilder();
for (String key : sortedHeaders) {
buffer.append(key.toLowerCase().replaceAll("\\s+", " ") + ":" + headers.get(key).replaceAll("\\s+", " "));
buffer.append("\n");
}
return buffer.toString();
}
private String getCanonicalizeHeaderNames(Map<String, String> headers) {
List<String> sortedHeaders = new ArrayList<String>();
sortedHeaders.addAll(headers.keySet());
Collections.sort(sortedHeaders, String.CASE_INSENSITIVE_ORDER);
StringBuilder buffer = new StringBuilder();
for (String header : sortedHeaders) {
if (buffer.length() > 0)
buffer.append(";");
buffer.append(header.toLowerCase());
}
return buffer.toString();
}
private String getCanonicalRequest(URL endpoint, String httpMethod, String queryParameters,
String canonicalizedHeaderNames, String canonicalizedHeaders, String bodyHash) {
String canonicalRequest;
if (bodyHash == null || bodyHash.equals("")) {
canonicalRequest = httpMethod + "\n" + getCanonicalizedResourcePath(endpoint) + "\n" + queryParameters
+ "\n" + canonicalizedHeaders + "\n" + canonicalizedHeaderNames;
} else {
canonicalRequest = httpMethod + "\n" + getCanonicalizedResourcePath(endpoint) + "\n" + queryParameters
+ "\n" + canonicalizedHeaders + "\n" + canonicalizedHeaderNames + "\n" + bodyHash;
}
return canonicalRequest;
}
private String getStringToSign(String scheme, String algorithm, String dateTime, String scope,
String canonicalRequest) {
String stringToSign =
scheme + "-" + algorithm + "\n" + dateTime + "\n" + scope + "\n" + toHex(hash(canonicalRequest));
return stringToSign;
}
private byte[] createSignatureKey(String key, String dateStamp, String regionName, String serviceName) {
byte[] kSecret = (SCHEME + key).getBytes();
byte[] kDate = sign(dateStamp, kSecret, HMAC_SHA256);
byte[] kRegion = sign(regionName, kDate, HMAC_SHA256);
byte[] kService = sign(serviceName, kRegion, HMAC_SHA256);
byte[] kSigning = sign(TERMINATOR, kService, HMAC_SHA256);
return kSigning;
}
private byte[] sign(String stringData, byte[] key, String algorithm) {
try {
byte[] data = stringData.getBytes("UTF-8");
Mac mac = Mac.getInstance(algorithm);
mac.init(new SecretKeySpec(key, algorithm));
return mac.doFinal(data);
} catch (Exception e) {
throw new RuntimeException("Unable to calculate a request signature: " + e.getMessage(), e);
}
}
private String getCanonicalizedResourcePath(URL endpoint) {
if (endpoint == null) {
return "/";
}
String path = endpoint.getPath();
if (path == null || path.isEmpty()) {
return "/";
}
// String encodedPath = urlEncode(path, true);
String encodedPath = path;
if (encodedPath.startsWith("/")) {
return encodedPath;
} else {
return "/".concat(encodedPath);
}
}
private String urlEncode(String url, boolean keepPathSlash) {
String encoded;
try {
encoded = URLEncoder.encode(url, "UTF-8");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("UTF-8 encoding is not supported.", e);
}
if (keepPathSlash) {
encoded = encoded.replace("%2F", "/");
}
return encoded;
}
private String toHex(byte[] data) {
StringBuilder r = new StringBuilder(data.length * 2);
for (byte b : data) {
r.append(HEX_CODE[(b >> 4) & 0xF]);
r.append(HEX_CODE[(b & 0xF)]);
}
return r.toString();
}
private byte[] hash(String text) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
md.reset();
md.update(text.getBytes("UTF-8"));
return md.digest();
} catch (Exception e) {
throw new RuntimeException("Unable to compute hash while signing request: " + e.getMessage(), e);
}
}
private String parseRegionName(String host) {
String pattern = "oos-([\\w-]*).ctyunapi.cn$";
Pattern r = Pattern.compile(pattern);
Matcher m = r.matcher(host);
if (m.find()) {
return m.group(1).toLowerCase();
} else {
throw new RuntimeException("parse region error, please check endpoint.");
}
}
private String parseServiceName(String host) {
if (host.endsWith("-iam.ctyunapi.cn")) {
return "sts";
} else {
return "s3";
}
}
}