Autenticación de la firma en una URL
OBS permite a los usuarios construir una URL para una operación específica. La URL contiene información como la AK, la firma, el período de validez y los recursos del usuario. Cualquier usuario que obtenga la URL puede realizar la operación. Después de recibir la solicitud, OBS considera que la operación es realizada por el usuario que emite la URL. Por ejemplo, si se construye la URL de una solicitud de descarga de objeto lleva información de firma, el usuario que obtiene la URL puede descargar el objeto, pero la URL es válida solo dentro del tiempo de caducidad especificado por el parámetro de Expires. La URL que lleva la firma se utiliza para permitir que otros utilicen la URL preemitida para la autenticación de identidad cuando no se proporciona el SK, y realizar la operación predefinida.
El formato del mensaje que contiene la solicitud de firma en la URL es el siguiente:
GET /ObjectKey?AccessKeyId=AccessKeyID&Expires=ExpiresValue&Signature=signature HTTP/1.1 Host: bucketname.obs.region.myhuaweicloud.com
El formato del mensaje que contiene la AK/SK temporal y el token de seguridad en la URL para descargar objetos es el siguiente:
GET /ObjectKey?AccessKeyId=AccessKeyID&Expires=ExpiresValue&Signature=signature&x-obs-security-token=securitytoken HTTP/1.1 Host: bucketname.obs.region.myhuaweicloud.com
Tabla 1 describe los parámetros.
Parámetro |
Descripción |
Obligatorio |
---|---|---|
AccessKeyId |
Información de AK del emisor. OBS determina la identidad del emisor basándose en la AK y considera que el emisor accede a la URL. Tipo: string |
Sí |
Expires |
Indica cuándo caduca la URL autorizada temporalmente, en segundos. La hora debe estar en formato de hora universal coordinada (UTC) y después de las 00:00:00 del 1 de enero de 1970. Tipo: string |
Sí |
Signature |
La firma generada usando la SK y el tiempo de caducidad. Tipo: string |
Sí |
x-obs-security-token |
Durante la autenticación temporal, la AK/SK temporal y el token de seguridad deben usarse al mismo tiempo y el campo x-obs-security-token debe agregarse al encabezado de solicitud. Para obtener detalles sobre cómo obtener un par de AK/SK temporal y un token de seguridad, consulte Obtención de un par de AK/SK temporal y un token de seguridad. |
No |
El proceso de cómputo de firmas es el siguiente:
1. Construye el StringToSign.
2. Realice la codificación UTF-8 sobre el resultado obtenido del paso anterior.
3. Utilice la SK para realizar el cálculo de la firma HMAC-SHA1 en el resultado obtenido del paso 2.
4. Realice la codificación de Base64 sobre el resultado obtenido del paso 3.
5. Realice la codificación de URL en el resultado del paso 4 para obtener la firma.
El StringToSign se construye de acuerdo con las siguientes reglas. Tabla 2 describe los parámetros.
StringToSign = HTTP-Verb + "\n" + Content-MD5 + "\n" + Content-Type + "\n" + Expires + "\n" + CanonicalizedHeaders + CanonicalizedResource;
Parámetro |
Descripción |
---|---|
HTTP-Verb |
Indica un método de solicitud de HTTP compatible con la API de REST de OBS. El valor puede ser un verbo HTTP como PUT, GET o DELETE. |
Content-MD5 |
Resumen de MD5 con 128 bits codificado en base64 del mensaje de acuerdo con RFC 1864. Este parámetro puede estar vacío. |
Content-Type |
Especifica el tipo de mensaje, por ejemplo, text/plain. Si una solicitud no contiene este campo de encabezado, este parámetro se considera como una string vacía. |
Expires |
Tiempo de caducidad de la autorización temporal, es decir, el valor del parámetro Expires en el mensaje de solicitud: ExpiresValue. |
CanonicalizedHeaders |
Campo de encabezado de solicitud OBS en un encabezado de solicitud HTTP, que hace referencia a los campos de encabezado iniciados con x-obs-, por ejemplo, x-obs-date, x-obs-acl y x-obs-meta-*.
|
CanonicalizedResource |
Indica el recurso de OBS especificado por una solicitud de HTTP. Este parámetro se construye de la siguiente manera: <Nombre del bucket + Nombre del objeto> + [Subrecurso 1] + [Subrecurso 2] +...
NOTA:
|
La firma se genera como sigue basándose en el StringToSign y la SK. El algoritmo de código de autenticación de mensajes basado en hash (HMAC algorithm) se usa para generar la firma.
Signature = URL-Encode( Base64( HMAC-SHA1( YourSecretAccessKeyID, UTF-8-Encoding-Of( StringToSign ) ) ) )
El método para calcular la firma transportada en la URL es diferente del método para calcular la firma de autorización transportada en un encabezado.
- La firma en la URL debe codificarse usando la URL después de la codificación Base64.
- Expires en StringToSign corresponde a Date en la información de autorización.
Genere una instancia de URL predefinida para el navegador llevando la firma en la URL.
Encabezados de solicitud |
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 |
Encabezado de la solicitud |
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.... |
Regla de cálculo de la firma
Signature = URL-Encode( Base64( HMAC-SHA1( YourSecretAccessKeyID, UTF-8-Encoding-Of( StringToSign ) ) ) )
Calcule la firma y utilice el host como prefijo de la URL para generar una URL predefinida:
http(s)://examplebucket.obs.region.myhuaweicloud.com/objectkey?AccessKeyId=AccessKeyID&Expires=1532779451&Signature=0Akylf43Bm3mD1bh2rM3dmVp1Bo%3D
Si introduce la dirección en el navegador, entonces se puede descargar la objectkey en el bucket examplebucket. El período de validez de este enlace es 1532779451 (indicando el sáb Jul 28 20:04:11 CST 2018).
En el sistema operativo Linux, al ejecutar el comando curl, debe agregar una barra diagonal (\) para escapar del carácter (&). El siguiente comando puede descargar el objeto objectkey en el archivo output:
curl http(s)://examplebucket.obs.region.myhuaweicloud.com/objectkey?AccessKeyId=AccessKeyID\&Expires=1532779451\&Signature=0Akylf43Bm3mD1bh2rM3dmVp1Bo%3D -X GET -o output
Si desea utilizar la URL predefinida generada por la firma incluida en la URL del navegador, no utilice Content-MD5, Content-Type o CanonicalizedHeaders que solo se pueden incluir en el encabezado para calcular la firma. De lo contrario, el navegador no puede llevar estos parámetros. Después de enviar la solicitud al servidor, se muestra un mensaje que indica que la firma es incorrecta.
Algoritmo de firma en 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 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 |
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", "backtosource", "cors", "customdomain", "delete", "deletebucket", "directcoldaccess", "encryption", "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","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", "object-lock", "retention")); 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) 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.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"); } 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.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(); /* Hard-coded or plaintext AK and SK are risky. For security purposes, encrypt your AK and SK and store them in the configuration file or environment variables. In this example, the AK and SK are stored in environment variables for identity authentication. Before running the code in this example, configure environment variables HUAWEICLOUD_SDK_AK and HUAWEICLOUD_SDK_SK. */ demo.ak = System.getenv("HUAWEICLOUD_SDK_AK"); demo.sk = System.getenv("HUAWEICLOUD_SDK_SK"); 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 with a browser. If a header is added to headers, the signature does not match. To use 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. 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 firma en el lenguaje de programación C
Descargue el ejemplo de código para calcular la firma en el lenguaje de programación C.
- La API para calcular la firma está contenida en el archivo de encabezado sign.h.
- El ejemplo de código para calcular la firma está contenido en el archivo de encabezado main.c.
Manejo de errores de coincidencia de firmas
Durante una invocación a la API de OBS, si se informa del siguiente error,
Código de estado: 403 Forbidden
Código de error: SignatureDoesNotMatch
Mensaje de error: The request signature we calculated does not match the signature you provided.Check your key and signing method.
Maneje el problema según ¿Por qué no coinciden las firmas?