URL中携带签名
URL中携带签名:OBS服务支持用户构造一个特定操作的URL,这个URL中会包含用户AK、签名、有效期、资源等信息,任何拿到这个URL的人均可执行这个操作,OBS服务收到这个请求后认为该请求就是签发URL用户自己在执行操作。例如构造一个携带签名信息的下载对象的URL,拿到相应URL的人能下载这个对象,但该URL只在Expires指定的失效时间内有效。URL中携带签名主要用于在不提供给其他人Secret Access Key的情况下,让其他人能用预签发的URL来进行身份认证,并执行预定义的操作。
URL中携带签名请求的消息格式如下:
GET /ObjectKey?AccessKeyId=AccessKeyID&Expires=ExpiresValue&Signature=signature HTTP/1.1 Host: bucketname.obs.region.example.com
URL中使用临时AK,SK和securitytoken下载对象消息格式如下:
GET /ObjectKey?AccessKeyId=AccessKeyID&Expires=ExpiresValue&Signature=signature&x-obs-security-token=securitytoken HTTP/1.1
Host: bucketname.obs.region.example.com
参数具体意义如表1所示。
参数名称 |
描述 |
是否必选 |
---|---|---|
AccessKeyId |
签发者的AK信息。OBS根据AK确定签发者的身份,并认为URL就是签发者在访问。 类型:String |
是 |
Expires |
临时授权失效的时间;UTC时间,1970年1月1日零时之后的指定的Expires时间内有效(以秒为单位)。 类型:String |
是 |
Signature |
根据用户SK、Expires等参数计算出的签名信息。 类型:String |
是 |
x-obs-security-token |
使用临时AK/SK鉴权时,临时AK/SK和securitytoken必须同时使用,请求头中需要添加“x-obs-security-token”字段; |
否 |
签名的计算过程如下:
- 构造请求字符串(StringToSign)。
- 对第一步的结果进行UTF-8编码。
- 使用SK对第二步的结果进行HMAC-SHA1签名计算。
- 对第三步的结果进行Base64编码。
- 对第四步的结果进行URL编码,得到签名。
请求字符串(StringToSign)按照如下规则进行构造,各个参数的含义如表2所示:
StringToSign = HTTP-Verb + "\n" + Content-MD5 + "\n" + Content-Type + "\n" + Expires + "\n" + CanonicalizedHeaders + CanonicalizedResource;
根据请求字符串(StringToSign)和用户SK使用如下算法生成Signature,生成过程使用HMAC算法(hash-based authentication code algorithm)。
Signature = URL-Encode( Base64( HMAC-SHA1( YourSecretAccessKeyID, UTF-8-Encoding-Of( StringToSign ) ) ) )
URL中的Signature计算方法和Header中携带的Authorization签名计算方法有两处不同:
- URL中签名在Base64编码后还要经过URL编码。
- StringToSign中的Expires和原来Authorization消息中的消息头Date对应。
使用URL携带签名方式为浏览器生成预定义的URL实例:
请求消息头 |
StringToSign |
---|---|
GET /objectkey?AccessKeyId=MFyfvK41ba2giqM7Uio6PznpdUKGpownRZlmVmHc&Expires=1532779451&Signature=0Akylf43Bm3mD1bh2rM3dmVp1Bo%3D HTTP/1.1 Host: examplebucket.obs.region.example.com |
GET \n \n \n 1532779451\n /examplebucket/objectkey |
请求消息头 |
StringToSign |
---|---|
GET /objectkey?AccessKeyId=MFyfvK41ba2giqM7Uio6PznpdUKGpownRZlmVmHc&Expires=1532779451&Signature=0Akylf43Bm3mD1bh2rM3dmVp1Bo%3D&x-obs-security-token=YwkaRTbdY8g7q.... HTTP/1.1 Host: examplebucket.obs.region.example.com |
GET \n \n \n 1532779451\n /examplebucket/objectkey?x-obs-security-token=YwkaRTbdY8g7q.... |
根据签名计算规则
Signature = URL-Encode( Base64( HMAC-SHA1( YourSecretAccessKeyID, UTF-8-Encoding-Of( StringToSign ) ) ) )
计算出签名,然后将Host作为URL的前缀,可以生成预定义的URL:
http(s)://examplebucket.obs.region.example.com/objectkey?AccessKeyId=AccessKeyID&Expires=1532779451&Signature=0Akylf43Bm3mD1bh2rM3dmVp1Bo%3D
在浏览器中直接输入该地址则可以下载examplebucket桶中的objectkey对象。这个链接的有效期是1532779451(Sat Jul 28 20:04:11 CST 2018)。
在Linux环境上使用curl命令访问注意&字符需要\转义,如下命令将对象objectkey下载到output文件中:
curl http(s)://examplebucket.obs.region.example.com/objectkey?AccessKeyId=AccessKeyID\&Expires=1532779451\&Signature=0Akylf43Bm3mD1bh2rM3dmVp1Bo%3D -X GET -o output
如果想要在浏览器中使用URL中携带签名生成的预定义URL,则计算签名时不要使用只能携带在头域部分的“Content-MD5”、“Content-Type”、“CanonicalizedHeaders”来计算签名。否则浏览器不能携带这些参数,请求发送到服务端之后,会提示签名错误。
Java中签名的计算方法
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 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 |
import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Arrays; import java.util.Base64; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.TreeMap; import java.util.regex.Pattern; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; public class SignDemo { private static final String SIGN_SEP = "\n"; private static final String OBS_PREFIX = "x-obs-"; private static final String DEFAULT_ENCODING = "UTF-8"; private static final List<String> SUB_RESOURCES = Collections.unmodifiableList(Arrays.asList( "CDNNotifyConfiguration", "acl", "append", "attname", "cors", "customdomain", "delete", "deletebucket", "inventory", "length", "lifecycle", "location", "logging", "metadata", "mirrorBackToSource", "modify", "name", "notification", "obscompresspolicy", "partNumber", "policy", "position", "quota","rename", "replication", "response-cache-control", "response-content-disposition","response-content-encoding", "response-content-language", "response-content-type", "response-expires","storagePolicy", "storageinfo", "tagging", "torrent", "truncate", "uploadId", "uploads", "versionId", "versioning", "versions", "website", "x-obs-security-token")); private String ak; private String sk; private boolean isBucketNameValid(String bucketName) { if (bucketName == null || bucketName.length() > 63 || bucketName.length() < 3) { return false; } if (!Pattern.matches("^[a-z0-9][a-z0-9.-]+$", bucketName)) { return false; } if (Pattern.matches("(\\d{1,3}\\.){3}\\d{1,3}", bucketName)) { return false; } String[] fragments = bucketName.split("\\."); for (int i = 0; i < fragments.length; i++) { if (Pattern.matches("^-.*", fragments[i]) || Pattern.matches(".*-$", fragments[i]) || Pattern.matches("^$", fragments[i])) { return false; } } return true; } public String encodeUrlString(String path) throws UnsupportedEncodingException { return URLEncoder.encode(path, DEFAULT_ENCODING) .replaceAll("\\+", "%20") .replaceAll("\\*", "%2A") .replaceAll("%7E", "~"); } public String encodeObjectName(String objectName) throws UnsupportedEncodingException { StringBuilder result = new StringBuilder(); String[] tokens = objectName.split("/"); for (int i = 0; i < tokens.length; i++) { result.append(this.encodeUrlString(tokens[i])); if (i < tokens.length - 1) { result.append("/"); } } return result.toString(); } private String join(List<?> items, String delimiter) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < items.size(); i++) { String item = items.get(i).toString(); sb.append(item); if (i < items.size() - 1) { sb.append(delimiter); } } return sb.toString(); } private boolean isValid(String input) { return input != null && !input.equals(""); } public String hmacSha1(String input) throws NoSuchAlgorithmException, InvalidKeyException, UnsupportedEncodingException { SecretKeySpec signingKey = new SecretKeySpec(this.sk.getBytes(DEFAULT_ENCODING), "HmacSHA1"); Mac mac = Mac.getInstance("HmacSHA1"); mac.init(signingKey); return Base64.getEncoder().encodeToString(mac.doFinal(input.getBytes(DEFAULT_ENCODING))); } private String stringToSign(String httpMethod, Map<String, String[]> headers, Map<String, String> queries, String bucketName, String objectName, long expires) throws Exception { String contentMd5 = ""; String contentType = ""; TreeMap<String, String> canonicalizedHeaders = new TreeMap<String, String>(); String key; List<String> temp = new ArrayList<String>(); for (Map.Entry<String, String[]> entry : headers.entrySet()) { key = entry.getKey(); if (key == null || entry.getValue() == null || entry.getValue().length == 0) { continue; } key = key.trim().toLowerCase(Locale.ENGLISH); if (key.equals("content-md5")) { contentMd5 = entry.getValue()[0]; continue; } if (key.equals("content-type")) { contentType = entry.getValue()[0]; continue; } if (key.startsWith(OBS_PREFIX)) { for (String value : entry.getValue()) { if (value != null) { temp.add(value.trim()); } } canonicalizedHeaders.put(key, this.join(temp, ",")); temp.clear(); } } // handle method/content-md5/content-type StringBuilder stringToSign = new StringBuilder(); stringToSign.append(httpMethod).append(SIGN_SEP) .append(contentMd5).append(SIGN_SEP) .append(contentType).append(SIGN_SEP) .append(expires).append(SIGN_SEP); // handle canonicalizedHeaders for (Map.Entry<String, String> entry : canonicalizedHeaders.entrySet()) { stringToSign.append(entry.getKey()).append(":").append(entry.getValue()).append(SIGN_SEP); } // handle CanonicalizedResource stringToSign.append("/"); if (this.isValid(bucketName)) { stringToSign.append(bucketName).append("/"); if (this.isValid(objectName)) { stringToSign.append(this.encodeObjectName(objectName)); } } TreeMap<String, String> canonicalizedResource = new TreeMap<String, String>(); for (Map.Entry<String, String> entry : queries.entrySet()) { key = entry.getKey(); if (key == null) { continue; } if (SUB_RESOURCES.contains(key)) { canonicalizedResource.put(key, entry.getValue()); } } if (canonicalizedResource.size() > 0) { stringToSign.append("?"); for (Map.Entry<String, String> entry : canonicalizedResource.entrySet()) { stringToSign.append(entry.getKey()); if (this.isValid(entry.getValue())) { stringToSign.append("=").append(entry.getValue()); } stringToSign.append("&"); } stringToSign.deleteCharAt(stringToSign.length() - 1); } // System.out.println(String.format("StringToSign:%s%s", SIGN_SEP, stringToSign.toString())); return stringToSign.toString(); } public String querySignature(String httpMethod, Map<String, String[]> headers, Map<String, String> queries, String bucketName, String objectName, long expires) throws Exception { if (!isBucketNameValid(bucketName)) { throw new IllegalArgumentException("the bucketName is illegal"); } //1. stringToSign String stringToSign = this.stringToSign(httpMethod, headers, queries, bucketName, objectName, expires); //2. signature return this.encodeUrlString(this.hmacSha1(stringToSign)); } public String getURL(String endpoint, Map<String, String> queries, String bucketName, String objectName, String signature, long expires) throws UnsupportedEncodingException { StringBuilder URL = new StringBuilder(); URL.append("https://").append(bucketName).append(".").append(endpoint).append("/"). append(this.encodeObjectName(objectName)).append("?"); String key; for (Map.Entry<String, String> entry : queries.entrySet()) { key = entry.getKey(); if (key == null) { continue; } if (SUB_RESOURCES.contains(key)) { String value = entry.getValue(); URL.append(key); if (value != null) { URL.append("=").append(value).append("&"); } else { URL.append("&"); } } } URL.append("AccessKeyId=").append(this.ak).append("&Expires=").append(expires). append("&Signature=").append(signature); return URL.toString(); } public static void main(String[] args) throws Exception { SignDemo demo = new SignDemo(); /* 认证用的ak和sk硬编码到代码中或者明文存储都有很大的安全风险,建议在配置文件或者环境变量中密文存放,使用时解密,确保安全; 本示例以ak和sk保存在环境变量中为例,运行本示例前请先在本地环境中设置环境变量YOUR_AK和YOUR_SK。*/ demo.ak = System.getenv("YOUR_AK"); demo.sk = System.getenv("YOUR_SK"); String endpoint = "<your-endpoint>"; String bucketName = "bucket-test"; String objectName = "hello.jpg"; // 如果直接使用URL在浏览器地址栏中访问,无法带上头域,此处headers加入头域会导致签名不匹配,使用headers需要客户端处理 Map<String, String[]> headers = new HashMap<String, String[]>(); Map<String, String> queries = new HashMap<String, String>(); // 请求消息参数Expires,设置24小时后失效 long expires = (System.currentTimeMillis() + 86400000L) / 1000; String signature = demo.querySignature("GET", headers, queries, bucketName, objectName, expires); System.out.println(signature); String URL = demo.getURL(endpoint, queries, bucketName, objectName, signature, expires); System.out.println(URL); } } |