Autenticação de assinatura em um URL
O OBS permite que os usuários criem uma URL para uma operação específica. A URL contém informações como AK do usuário, assinatura, período de validade e recursos. Qualquer usuário que obtenha o URL pode executar a operação. Após receber o pedido, o OBS considera que a operação é realizada pelo usuário que emite a URL. Por exemplo, se a URL de uma solicitação de download de objeto carrega informações de assinatura é construída, o usuário que obtém a URL pode baixar o objeto, mas a URL é válida apenas dentro do tempo de expiração especificado pelo parâmetro de Expires. O URL que transporta a assinatura é usado para permitir que outras pessoas usem o URL pré-emitido para autenticação de identidade quando o SK não for fornecido e executem a operação predefinida.
O formato da mensagem que contém a solicitação de assinatura no URL é o seguinte:
GET /ObjectKey?AccessKeyId=AccessKeyID&Expires=ExpiresValue&Signature=signature HTTP/1.1 Host: bucketname.obs.region.myhuaweicloud.com
O formato da mensagem que contém o AK/SK temporário e o token de segurança no URL para download de objetos é o seguinte:
GET /ObjectKey?AccessKeyId=AccessKeyID&Expires=ExpiresValue&Signature=signature&x-obs-security-token=securitytoken HTTP/1.1
Host: bucketname.obs.region.myhuaweicloud.com
Tabela 1 descreve os parâmetros.
Parâmetro |
Descrição |
Obrigatório |
---|---|---|
AccessKeyId |
AK informações do emissor. O OBS determina a identidade do emissor com base no AK e considera que o URL é acessado pelo emissor. Tipo: string |
Sim |
Expires |
Indica quando o URL autorizado temporariamente expira, em segundos. A hora deve estar no formato de Tempo Universal Coordenado (UTC) e posterior às 00:00:00 de 1o de janeiro de 1970. Tipo: string |
Sim |
Signature |
A assinatura gerada usando o SK e o tempo de expiração. Tipo: string |
Sim |
x-obs-security-token |
Durante a autenticação temporária, o AK/SK temporário e o token de segurança devem ser usados ao mesmo tempo e o campo x-obs-security-token deve ser adicionado ao cabeçalho da solicitação. Para obter detalhes sobre como obter um par AK/SK temporário e um token de segurança, consulte Obtenção de um par AK/SK temporário e um token de segurança. |
Não |
O processo de computação de assinatura é o seguinte:
1. Construa o StringToSign.
2. Execute a codificação UTF-8 no resultado obtido na etapa anterior.
3. Use o SK para executar o cálculo da assinatura HMAC-SHA1 no resultado obtido na etapa 2.
4. Execute a codificação Base64 no resultado obtido na etapa 3.
5. Execute a codificação de URL no resultado da etapa 4 para obter a assinatura.
O StringToSign é construído de acordo com as seguintes regras. Tabela 2 descreve os parâmetros.
StringToSign = HTTP-Verb + "\n" + Content-MD5 + "\n" + Content-Type + "\n" + Expires + "\n" + CanonicalizedHeaders + CanonicalizedResource;
Parâmetro |
Descrição |
---|---|
HTTP-Verb |
Indica um método de solicitação HTTP compatível com a API REST do OBS. O valor pode ser um verbo HTTP como PUT, GET, ou DELETE. |
Content-MD5 |
Base64-encoded 128-bit MD5 digest of the message according to RFC 1864. Este parâmetro pode estar vazio. |
Content-Type |
Especifica o tipo de mensagem, por exemplo, text/plain. Se uma solicitação não contiver esse campo de cabeçalho, esse parâmetro será considerado como uma string vazia. |
Expires |
Tempo de expiração da autorização temporária, ou seja, o valor do parâmetro Expires na mensagem de solicitação: ExpiresValue. |
CanonicalizedHeaders |
Campo de cabeçalho de solicitação OBS em um cabeçalho de solicitação HTTP, referindo-se a campos de cabeçalho iniciados com x-obs-, por exemplo, x-obs-date, x-obs-acl, e x-obs-meta-*.
|
CanonicalizedResource |
Indica o recurso OBS especificado por uma solicitação HTTP. Este parâmetro é construído da seguinte forma: <Bucket name + Object name> + [Subresource 1] + [Subresource 2] + ...
NOTA:
|
A assinatura é gerada da seguinte forma com base no StringToSign e SK. O algoritmo de código de autenticação de mensagem baseado em hash (algoritmo HMAC) é usado para gerar a assinatura.
Signature = URL-Encode( Base64( HMAC-SHA1( YourSecretAccessKeyID, UTF-8-Encoding-Of( StringToSign ) ) ) )
O método para calcular a assinatura realizada na URL é diferente do método para calcular a assinatura de autorização realizada em um cabeçalho.
- A assinatura no URL deve ser codificada usando o URL após a codificação Base64.
- Expires em StringToSign corresponde a Date nas informações de autorização.
Gere uma instância de URL predefinida para o navegador carregando a assinatura no URL.
Cabeçalhos de solicitação |
StringToSign |
---|---|
GET /objectkey?AccessKeyId=MFyfvK41ba2giqM7Uio6PznpdUKGpownRZlmVmHc&Expires=1532779451&Signature=0Akylf43Bm3mD1bh2rM3dmVp1Bo%3D HTTP/1.1 Host: examplebucket.obs.region.myhuaweicloud.com |
GET \n \n \n 1532779451\n /examplebucket/objectkey |
Cabeçalho da solicitação |
StringToSign |
---|---|
GET /objectkey?AccessKeyId=MFyfvK41ba2giqM7Uio6PznpdUKGpownRZlmVmHc&Expires=1532779451&Signature=0Akylf43Bm3mD1bh2rM3dmVp1Bo%3D&x-obs-security-token=YwkaRTbdY8g7q.... HTTP/1.1 Host: examplebucket.obs.region.myhuaweicloud.com |
GET \n \n \n 1532779451\n /examplebucket/objectkey?x-obs-security-token=YwkaRTbdY8g7q.... |
Regra de cálculo da assinatura
Signature = URL-Encode( Base64( HMAC-SHA1( YourSecretAccessKeyID, UTF-8-Encoding-Of( StringToSign ) ) ) )
Calcule a assinatura e use o host como o prefixo do URL para gerar um URL predefinido:
http(s)://examplebucket.obs.region.myhuaweicloud.com/objectkey?AccessKeyId=AccessKeyID&Expires=1532779451&Signature=0Akylf43Bm3mD1bh2rM3dmVp1Bo%3D
Se você inserir o endereço no navegador, o objeto objectkey no bucket examplebucket poderá ser baixado. O período de validade deste link é 1532779451 (indicando Sat Jul 28 20:04:11 CST 2018).
No sistema operacional Linux, ao executar o comando curl, você precisa adicionar uma barra (\) para escapar do caractere (&). O comando a seguir pode baixar o objeto objectkey para o arquivo de output:
curl http(s)://examplebucket.obs.region.myhuaweicloud.com/objectkey?AccessKeyId=AccessKeyID\&Expires=1532779451\&Signature=0Akylf43Bm3mD1bh2rM3dmVp1Bo%3D -X GET -o output
Se quiser usar o URL predefinido gerado pela assinatura transportada no URL no navegador, não use Content-MD5, Content-Type ou CanonicalizedHeaders que só podem ser carregados no cabeçalho para calcular a assinatura. Caso contrário, o navegador não pode carregar esses parâmetros. Depois que a solicitação é enviada para o servidor, uma mensagem é exibida indicando que a assinatura está incorreta.
Algoritmo de assinatura em 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 |
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 javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import org.omg.CosNaming.IstringHelper; 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", "backtosource", "cors", "customdomain", "delete", "deletebucket", "directcoldaccess", "encryption", "inventory", "length", "lifecycle", "location", "logging", "metadata", "modify", "name", "notification", "partNumber", "policy", "position", "quota", "rename", "replication", "response-cache-control", "response-content-disposition", "response-content-encoding", "response-content-language", "response-content-type", "response-expires", "restore", " storageClass", "storagePolicy", "storageinfo", "tagging", "torrent", "truncate", "uploadId", "uploads", "versionId", "versioning", "versions", "website", "x-image-process", "x-image-save-bucket", "x-image-save-object", "x-obs-security-token")); private String ak; private String sk; public String urlEncode(String input) throws UnsupportedEncodingException { return URLEncoder.encode(input, DEFAULT_ENCODING) .replaceAll("%7E", "~") //for browser .replaceAll("%2F", "/"); } 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 hamcSha1(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) throws Exception{ String contentMd5 = ""; String contentType = ""; String date = ""; 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.equals("date")) { date = 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(); } } if(canonicalizedHeaders.containsKey("x-obs-date")) { date = ""; } // handle method/content-md5/content-type/date StringBuilder stringToSign = new StringBuilder(); stringToSign.append(httpMethod).append(SIGN_SEP) .append(contentMd5).append(SIGN_SEP) .append(contentType).append(SIGN_SEP) .append(date).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.urlEncode(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 headerSignature(String httpMethod, Map<String, String[]> headers, Map<String, String> queries, String bucketName, String objectName) throws Exception { //1. stringToSign String stringToSign = this.stringToSign(httpMethod, headers, queries, bucketName, objectName); //2. signature return String.format("OBS %s:%s", this.ak, this.hamcSha1(stringToSign)); } public String querySignature(String httpMethod, Map<String, String[]> headers, Map<String, String> queries, String bucketName, String objectName, long expires) throws Exception { if(headers.containsKey("x-obs-date")) { headers.put("x-obs-date", new String[] {String.valueOf(expires)}); }else { headers.put("date", new String[] {String.valueOf(expires)}); } //1. stringToSign String stringToSign = this.stringToSign(httpMethod, headers, queries, bucketName, objectName); //2. signature return this.urlEncode(this.hamcSha1(stringToSign)); } public String getURL(String endpoint, Map<String, String> queries, String bucketName, String objectName, String signature, long expires) { StringBuilder URL = new StringBuilder(); URL.append("https://").append(bucketName).append(".").append(endpoint).append("/"). append(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(); demo.ak = "<your-access-key-id>"; demo.sk = "<your-secret-key-id>"; String endpoint = "<your-endpoint>"; String bucketName = "bucket-test"; String objectName = "hello.jpg"; // A header cannot be carried if you want to use a URL to access OBS through a browser. If a header is added to headers, the signature does not match. To use the headers, it must be processed by the client. Map<String, String[]> headers = new HashMap<String, String[]>(); Map<String, String> queries = new HashMap<String, String>(); // Expiration time of the request message. Set it to expire in 24 hours. 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); } } |
Algoritmo de assinatura na linguagem de programação C
Clique aqui para baixar o código de exemplo para calcular a assinatura na linguagem de programação C.
- A API para calcular a assinatura está contida no arquivo de cabeçalho sign.h.
- O código de exemplo para calcular a assinatura está contido no arquivo de cabeçalho main.c.
Tratamento de erros de incompatibilidade de assinatura
Durante uma chamada de API do OBS, se o seguinte erro for relatado,
Código de status: 403 Proibido
Código de erro: SignatureDoesNotMatch
Mensagem de erro: A assinatura da solicitação que calculamos não corresponde à assinatura que você forneceu. Verifique sua chave e método de assinatura.
Lide com o problema consultando Por que as assinaturas não combinam?