Estos contenidos se han traducido de forma automática para su comodidad, pero Huawei Cloud no garantiza la exactitud de estos. Para consultar los contenidos originales, acceda a la versión en inglés.
Actualización más reciente 2024-09-18 GMT+08:00

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.

Tabla 1 Parámetros de solicitud

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

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

Signature

La firma generada usando la SK y el tiempo de caducidad.

Tipo: string

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; 
Tabla 2 Parámetros requeridos para construir un StringToSign

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-*.

  1. Todos los caracteres de las palabras clave del campo de encabezado deben convertirse en letras minúsculas. Si una solicitud contiene varios campos de encabezado, estos campos deben organizarse por palabras clave en el orden alfabético desde a hasta z.
  2. Si varios campos de encabezado de una solicitud tienen el mismo prefijo, combine los campos de encabezado en uno. Por ejemplo, x-obs-meta-name:name1 y x-obs-meta-name:name2 deben reorganizarse en x-obs-meta-name:name1,name2. Utilice la coma para separar los valores.
  3. Las palabras clave en el campo de encabezado de solicitud no pueden contener caracteres no ASCII o irreconocibles, que tampoco son recomendables para los valores en el campo de encabezado de solicitud. Si los dos tipos de caracteres son necesarios, deben codificarse y decodificarse en el lado del cliente. La codificación URL o la codificación Base64 son aceptables, pero el servidor no realiza la decodificación.
  4. Eliminar espacios y tabulaciones sin sentido en un campo de encabezado. Por ejemplo, x-obs-meta-name: name (con un espacio sin sentido en el frente de name) debe cambiarse a x-obs-meta-name:name.
  5. Cada campo de encabezado ocupa una línea separada.

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] +...

  1. Nombre del bucket y nombre del objeto
    • Si el bucket está enlazado a un nombre de dominio de usuario, utilice el nombre de dominio de usuario como nombre de bucket, por ejemplo, /obs.ccc.com/object. obs.ccc.com es el nombre de dominio del usuario vinculado al bucket. Si no se especifica ningún nombre de objeto, se muestra el bucket completo, por ejemplo, /obs.ccc.com/.
    • Si no accede a OBS con un nombre de dominio de usuario, este campo tiene el formato de /bucket/object. Si no se especifica ningún nombre de objeto, por ejemplo, /bucket/, se muestra el bucket completo. Si no se especifica el nombre del bucket, el valor de este campo es /.
  2. Si existe un subrecurso (como ?acl y ?logging), se debe agregar el subrecurso.

    OBS soporta una variedad de sub-recursos, incluidos 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, object-lock, retention, y x-obs-security-token.

  3. Si hay varios subrecursos, ordenarlos en el orden alfabético de a a z y utilizar & para combinar los subrecursos.
NOTA:
  • Un subrecurso es único. No agregue subrecursos con la misma palabra clave (por ejemplo, key=value1&key=value2) en la misma URL de solicitud. En este caso, la firma se calcula solo en base al primer subrecurso, y solo el valor del primer subrecurso tiene efecto en el servicio real.
  • Usando la API GetObject como ejemplo, suponga que hay un bucket llamado bucket-test y un objeto llamado object-test en el bucket, y la versión del objeto es xxx. Al obtener el objeto, es necesario reescribir Content-Type a text/plain. Entonces, el CanonicalizedResource calculado por la firma es /bucket-test/object-test?response-content-type=text/plain&versionId=xxx.

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.

Tabla 3 Solicitud que tiene la firma incluida en la URL y el StringToSign

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

Tabla 4 Solicitud de descarga de objetos que tiene la AK/SK temporal y el token de seguridad incluido en la URL y el StringToSign

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.

  1. La API para calcular la firma está contenida en el archivo de encabezado sign.h.
  2. 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?