
# URL中携带签名
#### 功能介绍
OBS服务支持用户构造一个特定操作的URL，这个URL的Query参数中会包含用户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.cn-north-4.myhuaweicloud.com
```
URL中使用临时AK，SK和securitytoken下载对象消息格式如下：
```
GET /ObjectKey?AccessKeyId=AccessKeyID&Expires=ExpiresValue&Signature=signature&x-obs-security-token=securitytoken HTTP/1.1
Host: bucketname.obs.cn-north-4.myhuaweicloud.com
```
#### URL中的Query参数说明
如果选择URL中携带签名访问OBS，那么URL中必须携带如[表1]所示的Query参数：
 表1URL中的Query参数说明 
| **参数名称**             | **参数类型** | **是否必选** | **描述**                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
|:---|:---|:---|:---|
| AccessKeyId          | String   | 必选       | **参数解释：** URL签发者的访问密钥ID（AK），OBS根据AK确定签发者的身份，并认为URL就是签发者在访问。如何获取AK请参见：[管理访问密钥](https://support.huaweicloud.com/usermanual-ca/ca_01_0003.html) **约束限制：** 不涉及 **取值范围：** 不涉及 **默认取值：** 无                                                                                                                                                                                                                                       |
| Expires              | String   | 必选       | **参数解释：** 签名失效的时间，此处以UNIX时间戳的形式来计量（以1970年1月1日零时为起点往后数秒），超过该时间后，URL签名失效。 **约束限制：** 不涉及 **取值范围：** 当前时间\<Expires\<当前时间+20年。单位：秒。 **默认取值：** 无                                                                                                                                                                                                                                                                                     |
| Signature            | String   | 必选       | **参数解释：** 根据用户SK、Expires等参数计算出的签名信息。 OBS支持3种计算URL签名的方式： - [使用SDK计算签名]  - [使用可视化签名工具计算签名]  - [手动编码计算签名]   |
| x-obs-security-token | String   | 可选       | **参数解释：** 使用临时AK/SK鉴权时，临时AK/SK和securitytoken必须同时使用，请求头中需要添加"x-obs-security-token"字段。 **约束限制：** 不涉及 **取值范围：** 获取临时AK/SK和securitytoken参考[获取临时AK/SK和securitytoken](https://support.huaweicloud.com/api-iam/iam_04_0002.html)。 **默认取值：** 无                                                                                                                                                                                  |
   
 #### SDK签名实现
表2OBS SDK签名实现 
| **SDK**    | **签名实现源文件**                                                                                                                                                                              |
|:---|:---|
| Java       | [AbstractClient.java](https://github.com/huaweicloud/huaweicloud-sdk-java-obs/blob/5af1b31a130417b28bfab1579e5d7211e3f84dae/app/src/main/java/com/obs/services/AbstractClient.java#L219) |
| Python     | [client.py](https://github.com/huaweicloud/huaweicloud-sdk-python-obs/blob/master/src/obs/client.py#L1059)                                                                               |
| Go         | [auth.go](https://github.com/huaweicloud/huaweicloud-sdk-go-obs/blob/master/obs/auth.go#L32)                                                                                             |
| C          | [request.c](https://github.com/huaweicloud/huaweicloud-sdk-c-obs/blob/5df228a0c83187ae95a80cca759baacb9325583c/source/eSDK_OBS_API/eSDK_OBS_API_C++/src/request.c#L819)                  |
| Node.Js    | [utils.js](https://github.com/huaweicloud/huaweicloud-sdk-nodejs-obs/blob/master/lib/utils.js#L1936)                                                                                     |
| Browser.Js | [utils.js](https://github.com/huaweicloud/huaweicloud-sdk-browserjs-obs/blob/master/src/utils.js#L2033)                                                                                  |
| PHP        | [SendRequestTrait.php](https://github.com/huaweicloud/huaweicloud-sdk-php-obs/blob/51272ef0c4415607f0f68460ae7706be330b8a82/Obs/Internal/SendRequestTrait.php#L59)                       |
| .NET       | [ObsClient.temporary.cs](https://github.com/huaweicloud/huaweicloud-sdk-dotnet-obs/blob/master/ObsClient.temporary.cs#L30)                                                               |
   
 #### 使用可视化签名工具计算签名
OBS提供可视化签名计算工具，帮助您高效计算Header中携带的签名，工具地址：[可视化签名计算工具](https://obs-community.obs.cn-north-1.myhuaweicloud.com/sign/query_signature.html)，如何使用工具详见[签名计算工具](https://support.huaweicloud.com/api-obs/obs_04_0171.html)。
 #### 手动编码计算签名
签名计算规则如下：
```
Signature = URL-Encode( Base64( HMAC-SHA1( UTF-8-Encoding-Of( Your_SK ), UTF-8-Encoding-Of( StringToSign ) ) ) )
```
签名的计算过程如下：
图1计算URL中携带的签名   
![](https://support.huaweicloud.com/api-obs/figure/zh-cn_image_0000002031657245.png "点击放大")
1. 构造请求字符串(StringToSign)。请求字符串(StringToSign)按照如下规则进行构造，各个参数的含义如[表3]所示，更多StringToSign构造示例参见[StringToSign构造示例]。
   
   ```
   StringToSign = 
        HTTP-Verb + "\n" +   
        Content-MD5 + "\n" +   
        Content-Type + "\n" +   
        Expires + "\n" +   
        CanonicalizedHeaders + "\n" +    
        CanonicalizedResource
   ```
    表3构造StringToSign所需参数说明 
   | **参数名称**              | **参数类型** | **是否必选** | **描述**                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
   |:---|:---|:---|:---|
   | HTTP-Verb             | String   | 必选       | **参数解释** **：** HTTP方法（也称为操作或动词），对REST接口而言，即为http请求操作的VERB，如："PUT"，"GET"，"DELETE"等字符串，您可根据要调用的API选择。 **约束限制：** 不涉及 **取值范围：** - GET：请求服务器返回指定资源，如获取桶列表、下载对象等。  - PUT：请求服务器更新指定资源，如创建桶、上传对象等。  - POST：请求服务器新增资源或执行特殊操作，如初始化上传段任务、合并段等。  - DELETE：请求服务器删除指定资源，如删除对象等。  - HEAD：请求服务器返回指定资源的概要，如获取对象元数据等。  - OPTIONS（暂不支持工具生成签名）：请求服务器检查是否具有某个资源的操作权限，需要桶配置CORS。   **默认取值：** 无，此参数必选。                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
   | Content-MD5           | String   | 可选       | **参数解释** **：** 消息体（body体）的MD5摘要字符串。Content-MD5是按照RFC 1864标准计算出的消息体（body体）的128bit MD5值经过base64编码后得到的字符串。填写该值能防止body体被篡改，保护数据的完整性和一致性。 **约束限制：** 如果想要在浏览器中使用携带签名的URL访问OBS，则计算签名时不要使用只能携带在头域部分的"Content-MD5"、"Content-Type"、"CanonicalizedHeaders"来计算签名。否则浏览器不能携带这些参数，请求发送到服务端之后，会提示签名错误。 **取值范围：** 长度为22的字符串。 **默认取值：** 不设置默认为空。                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
   | Content-Type          | String   | 可选       | **参数解释** **：** Content-Type用于指定Object的文件类型，例如： text/plain。文件类型决定浏览器将以什么形式、什么编码读取文件。 **约束限制：** 不涉及 **取值范围：** Content-type的常见取值参见[常见的Content-Type类型](https://support.huaweicloud.com/usermanual-obs/obs_03_0324.html)。 **默认取值：** 当请求中不带该头域时，该参数按照空字符串处理，例如[表4]。如果没有指定，则根据文件的扩展名自动生成Content-Type。如果没有扩展名，则默认为application/octet-stream。                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
   | Expires               | String   | 必选       | **参数解释：** 签名失效的时间，此处以UNIX时间戳的形式来计量（以1970年1月1日零时为起点往后数秒），超过该时间后，URL签名失效。 **约束限制：** 不涉及 **取值范围：** 当前时间\<Expires\<当前时间+20年。单位：秒。 **默认取值：** 无                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
   | CanonicalizedHeaders  | String   | 必选       | **参数解释** **：** OBS服务自定义的附加头域，即以"x-obs-"作为前缀的头域，如"x-obs-date，x-obs-acl，x-obs-meta-\*"等。每个头域包含头域名称和头域值两部分，如"x-obs-storage-class:STANDARD"中"x-obs-storage-class"是头域名称，"STANDARD"是头域值。 上传对象和上传段支持携带x-obs-content-sha256头域，该头域值为请求消息体（body体）的SHA256值转十六进制值，服务端会对携带此头域的请求计算其消息体的sha256值做校验（性能会有部分下降，在安全上推荐该算法）。 **约束限制：** 1. 头域名称要转为小写，但头域值要区分大小写，如"x-obs-storage-class:STANDARD"。需要添加多个头域时，所有头域要按照头域名称的字典序从小到大进行排序。[使用工具计算签名](https://support.huaweicloud.com/api-obs/obs_04_0171.html)时，无需关注头域名称的大小写转换和字典排序，工具会自动完成。  2. 头域名称不允许含有非ASCII码或不可识别字符，头域值也不建议使用非ASCII码或不可识别字符。如果一定要使用非ASCII码或不可识别字符，需要客户端自行做编解码处理，可以采用URL编码或者Base64编码，服务端不会做解码处理。  3. 头域中不能含有无意义空格或Tab键。例如：x-obs-meta-name: name（name前带有一个无意义空格），需要转换为：x-obs-meta-name:name  4. 如果头域名称重复，则需要将头域值进行合并，多个头域值以逗号分隔。如：x-obs-meta-name:name1和x-obs-meta-name:name2，合并成x-obs-meta-name:name1,name2。  5. 每一个头域字段都需要另起新行，以换行符"\\n"结尾。   **取值范围：** 调用API时，从API支持的头域中选取。 **默认取值：** 无                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |
   | CanonicalizedResource | String   | 必选       | **参数解释** **：** HTTP请求所指定的OBS资源，结构如下： ``` CanonicalizedResource = "/" + 桶名 +  "/" + 对象名 + "?" + 子资源 ``` 以获取对象（GetObject）接口为例，假设桶名为bucket-test，对象名为object-test，对象版本号为xxx，获取时需要重写Content-Type为text/plain，那么CanonicalizedResource为： ``` /bucket-test/object-test?response-content-type=text/plain&versionId=xxx ``` - 桶名： 如果桶未绑定自定义域名，则直接使用原始桶名。 如果桶绑定了自定义域名，则桶名由自定义域名表示，如"/obs.ccc.com/object"，其中"obs.ccc.com"为桶绑定的自定义域名。 如果API操作不指定桶，例如列举账号下所有桶操作，则桶名字段和对象名字段都不写。如"/"。   - 对象名： 要操作对象的对象名。需要遵循对象的命名规则。   - 子资源（多个子资源需按照字典序从小到大排列，并使用"\&"拼接）： 资源标识：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, restore, storageClass, storagePolicy, storageinfo, tagging, torrent, truncate, uploadId, uploads, versionId, versioning, versions, website,x-obs-security-token, object-lock, retention，symlink 用于指定响应头域的子资源：response-cache-control, response-content-disposition, response-content-encoding, response-content-language, response-content-type, response-expires 图片处理相关资源标识： x-image-process, x-image-save-bucket, x-image-save-object    **约束限制：** 子资源通常是唯一的，不建议包含多个相同关键字的子资源（例如，key=value1\&key=value2），如果存在这种情况，OBS服务端签名时只会计算第一个子资源且也只有第一个子资源的值会对实际业务产生作用。 **取值范围：** 不涉及。 **默认取值：** 不写默认为"/"。 |
      
   ![](https://support.huaweicloud.com/api-obs/public_sys-resources/note_3.0-zh-cn.png)
   如果想要在浏览器中使用URL中携带签名生成的预定义URL，则计算签名时不要使用只能携带在头域部分的"Content-MD5"、"Content-Type"、"CanonicalizedHeaders"来计算签名。否则浏览器不能携带这些参数，请求发送到服务端之后，会提示签名错误。
   
   
2. 对第一步的结果进行UTF-8编码。
3. 使用SK对第二步的结果进行HMAC-SHA1签名计算。
4. 对第三步的结果进行Base64编码。
5. 对第四步的结果进行URL编码，得到签名。
#### StringToSign构造示例
 表4下载对象在URL中携带签名的请求及StringToSign 
| 请求消息头                                                                                                                                                                                                                                         | StringToSign                                                                                                                                                                                                                      |
|:---|:---|
| GET /objectkey?AccessKeyId=MFyfvK41ba2giqM7Uio6PznpdUKGpownRZlmVmHc\&Expires=1532779451\&Signature=0Akylf43Bm3mD1bh2rM3dmVp1Bo%3D HTTP/1.1 Host: examplebucket.obs.cn-north-4.myhuaweicloud.com | GET \\n \\n \\n 1532779451\\n /examplebucket/objectkey |
   
表5在URL中使用临时AK/SK和securitytoken下载对象请求及StringToSign 
| 请求消息头                                                                                                                                                                                                                                                                                 | StringToSign                                                                                                                                                                                                                                                                       |
|:---|:---|
| GET /objectkey?AccessKeyId=MFyfvK41ba2giqM7Uio6PznpdUKGpownRZlmVmHc\&Expires=1532779451\&Signature=0Akylf43Bm3mD1bh2rM3dmVp1Bo%3D\&x-obs-security-token=YwkaRTbdY8g7q.... HTTP/1.1 Host: examplebucket.obs.cn-north-4.myhuaweicloud.com | GET \\n \\n \\n 1532779451\\n /examplebucket/objectkey?x-obs-security-token=YwkaRTbdY8g7q.... |
   
#### 签名代码示例
以下是计算URL中携带签名的示例代码：
- [Java中签名的计算方法]
  ```
  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",  "orchestration", 
              "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;
      }
      // 将字符串进行UTF-8编码
      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实例，并通过getInstance方法指定使用HMAC-SHA1算法
          Mac mac = Mac.getInstance("HmacSHA1");
          // 使用访问密钥SK初始化Mac对象
          mac.init(signingKey);
          return Base64.getEncoder().encodeToString(mac.doFinal(input.getBytes(DEFAULT_ENCODING)));
      }
    
      // 构造StringToSign
      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(); 
              } 
          } 
          // 构造StringToSign，拼接HTTP-Verb、Content-MD5、Content-Type、Expires
          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);
          // 构造StringToSign，拼接CanonicalizedHeaders
          for (Map.Entry<String, String> entry : canonicalizedHeaders.entrySet()) {
              stringToSign.append(entry.getKey()).append(":").append(entry.getValue()).append(SIGN_SEP);
          }
          // 构造StringToSign，拼接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);
          }
          // 403报错的情况下，可以打印stringtosign定位问题
          // 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"); 
           } 
           // 构造StringToSign 
           String stringToSign = this.stringToSign(httpMethod, headers, queries, bucketName, objectName, expires); 
    
           // 计算签名
           return this.encodeUrlString(this.hmacSha1(stringToSign)); 
       } 
       public String querySignature(String httpMethod, Map<String, String[]> headers, Map<String, String> queries, long expires) throws Exception {
      // 构造StringToSign
      String stringToSign = this.stringToSign(httpMethod, headers, queries, "", "", expires);
   
      // 计算签名
      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(); 
          if (bucketName == null || bucketName.isEmpty()) { 
              URL.append("https://").append(endpoint).append("/").append(this.encodeObjectName(objectName)).append("?");
          } else {
              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 listBucketSignatureUrl(SignDemo demo, Map<String, String[]> headers, Map<String, String> queries, String endpoint)
          throws Exception {
          // 请求消息参数Expires，设置24小时后失效
          long expires = (System.currentTimeMillis() + 86400000L) / 1000;
          // 列举桶请求，没有桶名对象名
          String signature = demo.querySignature("GET", headers, queries, expires);
          System.out.println(signature);
          String URL = demo.getURL(endpoint, queries, "", "", signature, expires);
          System.out.println(URL);
      }
   
      public static void signatureUrl(SignDemo demo, Map<String, String[]> headers, Map<String, String> queries, String endpoint)
          throws Exception {
          String bucketName = "bucket-test";
           String objectName = "hello.jpg"; 
          // 请求消息参数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);
      }
      public static void main(String[] args) throws Exception {
          SignDemo demo = new SignDemo();
          /* 认证用的ak和sk硬编码到代码中或者明文存储都有很大的安全风险，建议在配置文件或者环境变量中密文存放，使用时解密，确保安全；
          本示例以ak和sk保存在环境变量中为例，运行本示例前请先在本地环境中设置环境变量HUAWEICLOUD_SDK_AK和HUAWEICLOUD_SDK_SK。*/
  demo.ak = System.getenv("HUAWEICLOUD_SDK_AK");
  demo.sk = System.getenv("HUAWEICLOUD_SDK_SK");
          String endpoint = "<your-endpoint>";
          // 如果直接使用URL在浏览器地址栏中访问，无法带上头域，此处headers加入头域会导致签名不匹配，使用headers需要客户端处理
          Map<String, String[]> headers = new HashMap<String, String[]>();
          Map<String, String> queries = new HashMap<String, String>();
   
          // 列举桶
          listBucketSignatureUrl(demo, headers, queries, endpoint);
          // 非列举桶
          signatureUrl(demo, headers, queries, endpoint); 
      }
  }
  ```
- [C语言中签名的计算方法]
  请单击[此处](https://obs-community.obs.cn-north-1.myhuaweicloud.com/sign/signature_c.zip)，下载C语言签名计算代码样例，其中：
  1. 计算签名的接口包含在sign.h头文件中。
  
  2. 计算签名的示例代码在main.c文件中。
   
#### 使用URL携带签名方式为浏览器生成预定义的URL
计算出签名，然后将Host作为URL的前缀，可以生成预定义的URL，如下面的示例所示，在浏览器中直接输入该地址则可以下载examplebucket桶中的objectkey对象。这个链接的有效期是1532779451(对应时间为：2018-07-28 12:04:11)。
```
http(s)://examplebucket.obs.cn-north-4.myhuaweicloud.com/objectkey?AccessKeyId=AccessKeyID&Expires=1532779451&Signature=0Akylf43Bm3mD1bh2rM3dmVp1Bo%3D
```
在Linux环境上使用curl命令访问注意\&字符需要\\转义，如下命令将对象objectkey下载到output文件中：
```
curl  http(s)://examplebucket.obs.cn-north-4.myhuaweicloud.com/objectkey?AccessKeyId=AccessKeyID\&Expires=1532779451\&Signature=0Akylf43Bm3mD1bh2rM3dmVp1Bo%3D  -X GET -o output
```
#### 签名不匹配报错处理
如果调用OBS API报如下错误：
状态码：403 Forbidden
错误码：SignatureDoesNotMatch
错误信息：The request signature we calculated does not match the signature you provided. Check your key and signing method.
请参考以下案例进行排查处理：[签名不匹配（SignatureDoesNotMatch）如何处理](https://support.huaweicloud.com/obs_faq/obs_faq_0173.html)
