应用场景
V2签名下,使用HttpURLConnection开发。
前提条件
已开通对象存储(经典版)Ⅰ型服务。
具体操作
可以参考下列示例进行HttpURLConnection开发。
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.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Base64;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.SortedMap;
import java.util.TimeZone;
import java.util.TreeMap;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
public class OOSDemoForV2Signer {
private static final String DATE_STR = "EEE, d MMM yyyy HH:mm:ss 'GMT'";
private static final SimpleDateFormat DATE_FMT = new SimpleDateFormat(DATE_STR, Locale.ENGLISH);
static {
TimeZone gmt = TimeZone.getTimeZone("GMT");
DATE_FMT.setTimeZone(gmt);
}
private static final String OOS_ACCESS_KEY = "your ak";
private static final String OOS_SECRET_KEY = "your sk";
private static final String OOS_ENDPOINT = "oos-cn.ctyunapi.cn";
private static final String OOS_OBJECT_CONTENT = "your_object_content";
private static final int CONN_TIMEOUT = 30000;
private static final int READ_TIMEOUT = 30000;
public void putObject(String bucket, String objectKey) {
try {
Map<String, String> headers = new HashMap<>();
headers.put("Content-Type", "text/plain");
headers.put("x-amz-date", "xxxx");
HttpURLConnection connection = generateConnection("PUT", bucket, objectKey, headers);
connection.setFixedLengthStreamingMode(OOS_OBJECT_CONTENT.length());
connection.setDoOutput(true);
connection.connect();
byte[] requestBody = OOS_OBJECT_CONTENT.getBytes();
try (OutputStream outputStream = connection.getOutputStream()) {
outputStream.write(requestBody);
}
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 getObject(String bucket, String objectKey) {
try {
HttpURLConnection connection = generateConnection("GET", bucket, objectKey, null);
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 deleteObject(String bucket, String objectKey) {
try {
HttpURLConnection connection = generateConnection("DELETE", bucket, objectKey, null);
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();
}
}
/**
* 1 并不是headers里的所有头域,都参与计算签名。详情请参照<a href=
* "https://oos-cn.ctyunapi.cn/docs/oos/OOS%E5%BC%80%E5%8F%91%E8%80%85%E6%96%87%E6%A1%A3-v6.pdf"> OOS开发者文档-v6
* </a>3.1.1章节StringToSign的构成说明 </br>
* 2 任何头以x-amz-meta-这个前缀开始都会被认为是用户的元数据,当用户检索时,它将会和对象一起被存储并返回。 PUT请求头大小限制为8KiB。在PUT请求头中,用户定义的元数据大小限制为2KiB。</br>
* 例:headers.put("x-amz-meta-test", "oos");
*/
private HttpURLConnection generateConnection(String method, String bucket, String objectKey,
Map<String, String> headers) throws Exception {
if (headers == null) {
headers = new TreeMap<>();
}
if (!headers.containsKey("Date")) {
String date = DATE_FMT.format(new Date());
headers.put("Date", date);
}
Map<String, String> querys = new HashMap<>(32);
// 设置查询参数示例,可按需选择是否在请求url上设置查询参数。更多接口参数请参考《OOS开发者文档-v6》
querys.put("response-content-type", "application/octet-stream");
String authorization = v2Sign(method, bucket, objectKey, headers, querys);
String requestUrl = "http://" + bucket + "." + OOS_ENDPOINT + "/" + urlEncode(objectKey, false);
if (querys.size() != 0) {
requestUrl += "?" + encodeParameters(querys);
}
URL url = new URL(requestUrl);
HttpURLConnection connection = (HttpURLConnection)url.openConnection();
connection.setRequestProperty("Authorization", authorization);
connection.setConnectTimeout(CONN_TIMEOUT);
connection.setReadTimeout(READ_TIMEOUT);
connection.setRequestMethod(method);
if (null != headers) {
headers.forEach(connection::setRequestProperty);
}
return connection;
}
private String encodeParameters(Map<String, String> querys) {
StringBuilder builder = new StringBuilder();
Iterator<Map.Entry<String, String>> pairs = querys.entrySet().iterator();
while (pairs.hasNext()) {
Map.Entry<String, String> pair = pairs.next();
builder.append(urlEncode(pair.getKey(), false));
builder.append("=");
builder.append(urlEncode(pair.getValue(), false));
if (pairs.hasNext()) {
builder.append("&");
}
}
return builder.toString();
}
// =============== 以下是签名计算相关方法 ===============
/**
* The set of request parameters which must be included in the canonical string to sign.
*/
private static final List<String> SIGNED_PARAMETERS =
Arrays.asList("acl", "torrent", "logging", "location", "policy", "requestPayment", "versioning", "versions",
"versionId", "notification", "uploadId", "uploads", "partNumber", "website", "delete", "lifecycle",
"tagging", "cors", "restore", "response-cache-control", "response-content-disposition",
"response-content-encoding", "response-content-language", "response-content-type", "response-expires");
private String v2Sign(String method, String bucket, String objectKey, Map<String, String> headers,
Map<String, String> querys) throws Exception {
String canonicalString =
getCanonicalString(method, toResourcePath(bucket, objectKey), headers, querys);
String signature = sign(canonicalString);
return "AWS " + OOS_ACCESS_KEY + ":" + signature;
}
private String sign(String data) throws Exception {
try {
Mac mac = Mac.getInstance("HmacSHA1");
mac.init(new SecretKeySpec(OOS_SECRET_KEY.getBytes(StandardCharsets.UTF_8), "HmacSHA1"));
byte[] bs = mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(bs);
} catch (Exception e) {
throw new Exception("Unable to calculate a request signature: " + e.getMessage(), e);
}
}
/**
* Calculate the canonical string for a REST/HTTP request to OOS.
*
* When expires is non-null, it will be used instead of the Date header.
*/
private String getCanonicalString(String method, String resource, Map<String, String> headers,
Map<String, String> querys) {
StringBuilder buf = new StringBuilder();
buf.append(method).append("\n");
SortedMap<String, String> interestingHeaders = new TreeMap<>();
if (headers != null && headers.size() > 0) {
for (Map.Entry<String, String> entry : headers.entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
if (key == null) {
continue;
}
String lk = key.toLowerCase(Locale.getDefault());
if ("content-type".equals(lk) || "content-md5".equals(lk) || "date".equals(lk)
|| lk.startsWith("x-amz-")) {
interestingHeaders.put(lk, value);
}
}
}
// Remove default date timestamp if "x-amz-date" is set.
if (interestingHeaders.containsKey("x-amz-date")) {
interestingHeaders.put("date", "");
}
// These headers require that we still put a new line in after them,
// even if they don't exist.
if (!interestingHeaders.containsKey("content-type")) {
interestingHeaders.put("content-type", "");
}
if (!interestingHeaders.containsKey("content-md5")) {
interestingHeaders.put("content-md5", "");
}
// Any parameters that are prefixed with "x-amz-" need to be included
// in the headers section of the canonical string to sign
if (querys != null) {
for (Map.Entry<String, String> parameter : querys.entrySet()) {
if (parameter.getKey().toLowerCase().startsWith("x-amz-")) {
interestingHeaders.put(parameter.getKey().toLowerCase(), parameter.getValue());
}
}
}
for (Map.Entry<String, String> entry : interestingHeaders.entrySet()) {
String key = entry.getKey();
Object value = entry.getValue();
if (key.toLowerCase().startsWith("x-amz-")) {
buf.append(key).append(':').append(value);
} else {
buf.append(value);
}
buf.append("\n");
}
buf.append(resource);
if (querys != null) {
String[] parameterNames = querys.keySet().toArray(new String[0]);
Arrays.sort(parameterNames);
char separator = '?';
for (String parameterName : parameterNames) {
// Skip any parameters that aren't part of the canonical signed string
if (!SIGNED_PARAMETERS.contains(parameterName)) {
continue;
}
buf.append(separator);
buf.append(parameterName);
String parameterValue = querys.get(parameterName);
if (parameterValue != null && !"".equals(parameterValue)) {
buf.append("=").append(parameterValue);
}
separator = '&';
}
}
return buf.toString();
}
private String toResourcePath(String bucket, String objectKey) {
String resourcePath =
"/" + ((bucket != null && !"".equals(bucket)) ? bucket : "") + ((objectKey != null) ? "/" + objectKey : "");
return urlEncode(resourcePath, true);
}
/**
* @param keepPathSlash 实际上,根据RFC 3986规范,url中的 '/'和'~' 可以不用转译,URLEncoder做了转译,但是为了兼容浏览器解析文件名,要求 '/'和'~'不能做转译。
* @param url 客户请求的url,也就是object key
*
* @return 转译后的url
*/
private String urlEncode(String url, boolean keepPathSlash) {
String encoded;
try {
encoded = URLEncoder.encode(url, StandardCharsets.UTF_8.toString());
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("UTF-8 encoding is not supported.", e);
}
if (keepPathSlash) {
// Web browsers do not always handle '+' characters well, use the well-supported '%20' instead.
encoded = encoded.replaceAll("\\+", "%20");
// Change all "%2F" back to "/", so that when users download a file in a virtual folder by the presigned
// URL,
// the web browsers won't mess up the filename. (e.g. 'folder1_folder2_filename' instead of 'filename')
encoded = encoded.replace("%2F", "/");
encoded = encoded.replace("%7E", "~");
}
return encoded;
}
public static void main(String[] args) {
String bucket = "your bucket";
String objectKey = "your object key";
OOSDemoForV2Signer v2 = new OOSDemoForV2Signer();
v2.putObject(bucket, objectKey);
v2.getObject(bucket, objectKey);
v2.deleteObject(bucket, objectKey);
}
}