如何在Java中生成AWS签名

19

当我从 REST 客户端调用 API 端点时,由于签名问题而出现错误。

请求:

主机: https://xxx.execute-api.ap-southeast-1.amazonaws.com/latest/api/name

授权: AWS4-HMAC-SHA256 Credential={AWSKEY}/20160314/ap-southeast-1/execute-api/aws4_request,SignedHeaders=host;range;x-amz-date,Signature={signature}

X-Amz-日期: 20160314T102915Z

响应:

{
"message": "The request signature we calculated does not match the signature you provided. Check your AWS Secret Access Key and signing method. Consult the service documentation for details. The Canonical String for this request should have been 'xxx' "
}

我从Java代码开始,按照AWS的参考文献生成签名。

    String secretKey = "{mysecretkey}";
    String dateStamp = "20160314";
    String regionName = "ap-southeast-1";
    String serviceName = "execute-api";

    byte[] signature = getSignatureKey(secretKey, dateStamp, regionName, serviceName);
    System.out.println("Signature : " + Hex.encodeHexString(signature));

    static byte[] HmacSHA256(String data, byte[] key) throws Exception  {
         String algorithm="HmacSHA256";
         Mac mac = Mac.getInstance(algorithm);
         mac.init(new SecretKeySpec(key, algorithm));
         return mac.doFinal(data.getBytes("UTF8"));
    }

    static byte[] getSignatureKey(String key, String dateStamp, String regionName, String serviceName) throws Exception  {
         byte[] kSecret = ("AWS4" + key).getBytes("UTF8");
         byte[] kDate    = HmacSHA256(dateStamp, kSecret);
         byte[] kRegion  = HmacSHA256(regionName, kDate);
         byte[] kService = HmacSHA256(serviceName, kRegion);
         byte[] kSigning = HmacSHA256("aws4_request", kService);
         return kSigning;
    }

请问我在生成签名时错了什么?

如何生成签名的参考资料: http://docs.aws.amazon.com/general/latest/gr/signature-v4-examples.html#signature-v4-examples-java


你找到解决方案了吗? - Ana
你是怎么解决这个问题的?我遇到了同样的问题。 - FrankTan
2
你好,能否请您也添加解决方案,以便了解您是如何解决这个问题的? - Chintamani
6个回答

21
你可以使用来自aws-java-sdk-core的类:https://github.com/aws/aws-sdk-java/tree/master/aws-java-sdk-core 更具体地说,是Request、Aws4Signer和其他一些类:
//Instantiate the request
Request<Void> request = new DefaultRequest<Void>("es"); //Request to ElasticSearch
request.setHttpMethod(HttpMethodName.GET);
request.setEndpoint(URI.create("http://..."));

//Sign it...
AWS4Signer signer = new AWS4Signer();
signer.setRegionName("...");
signer.setServiceName(request.getServiceName());
signer.sign(request, new AwsCredentialsFromSystem());

//Execute it and get the response...
Response<String> rsp = new AmazonHttpClient(new ClientConfiguration())
    .requestExecutionBuilder()
    .executionContext(new ExecutionContext(true))
    .request(request)
    .errorResponseHandler(new SimpleAwsErrorHandler())
    .execute(new SimpleResponseHandler<String>());

如果你想要更清晰的设计,可以使用装饰者模式来组合一些优雅的类并隐藏上述混乱。这里有一个例子:http://www.amihaiemil.com/2017/02/18/decorators-with-tunnels.html


令人惊讶的是,它与其他HTTP客户端不兼容。相同的标头集生成“我们计算的请求签名与您提供的签名不匹配”的错误。AmazonHttp客户端运行顺畅。 - Łukasz
3
AwsCredentialsFromSystem是什么?我在AWS Java API中找不到该类的参考。 - totsubo
AwsCredentialsFromSystem 是我的类,它实现了 AwsCredentials 接口(来自 SDK)。它只是一个从系统属性中读取所需凭证的类:https://github.com/opencharles/charles-rest/blob/5c4b99c575e73dec6c7afe120ce0c9e15a8b8b61/src/main/java/com/amihaiemil/charles/aws/requests/SignedRequest.java#L107 - amihaiemil
我正在尝试使用您的答案作为示例,但是在“执行并获取响应”部分遇到了问题。它告诉我“AmazonHttpClient(ClientConfiguration)方法未定义”。 我正在使用eclipse。在那之前,我能够使用快速修复选项自动导入所有内容。但我不知道如何使AmazonHttpClient工作。...而且它告诉我“SimpleAwsErrorHandler()未定义”。以下是我的导入语句:(请看下一个评论) - benino
import com.amazonaws.ClientConfiguration; import com.amazonaws.DefaultRequest; import com.amazonaws.Request; import com.amazonaws.Response; import com.amazonaws.auth.AWS4Signer; import com.amazonaws.auth.AWSCredentials; import com.amazonaws.auth.BasicAWSCredentials; import com.amazonaws.http.ExecutionContext; import com.amazonaws.http.HttpMethodName; - benino

3
从上面的代码示例中,看起来您没有创建一个规范请求并将其包含在签名字符串中,如http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html所述。
与其自己实现,不如考虑使用第三方库。 aws-v4-signer-java是一个轻量级、零依赖的库,可以轻松生成AWS V4签名。
String contentSha256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
HttpRequest request = new HttpRequest("GET", new URI("https://examplebucket.s3.amazonaws.com?max-keys=2&prefix=J"));
String signature = Signer.builder()
        .awsCredentials(new AwsCredentials(ACCESS_KEY, SECRET_KEY))
        .header("Host", "examplebucket.s3.amazonaws.com")
        .header("x-amz-date", "20130524T000000Z")
        .header("x-amz-content-sha256", contentSha256)
        .buildS3(request, contentSha256)
        .getSignature();

免责声明:我是这些库的作者。

嗨,卢卡斯,我很好奇你是否已经测试过这个库,用于为前端客户端提供签名以直接上传到S3的后端服务器。是否有必要在客户端计算内容的sha256,并将其作为签名计算的一部分提交给服务器? - Charney Kaye
@CharneyKaye,你找到答案了吗?我也在寻找它。 - princebillyGK
@lucasweb 除了硬编码标头对,我们是否有任何提供传递Map/List的方法?如果有的话,那么对我来说,通过方法参数动态接收所有标头并生成签名将会很有用。 - Deepak Prabhu
这个是否适用于SQS? - Shamil Puthukkot
我能否使用aws-v4-signer为s3表单提交请求生成签名(x-amz-signature)?我相信在这种情况下不需要任何标头,签名由策略和密钥组成。 - PathOfNeo

2
这是可以使用100%的Java库而不需要额外依赖来实现的,只需使用在此生成的查询参数即可:查询参数。原始答案可翻译为“最初的回答”。
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SignatureException;
import java.util.Formatter;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;

...

private static final String ACCESS_KEY = "...";
private static final String SECRET_KEY = "...";
private static final int expiresTime = 1 * 24 * 60 * 60;
private static final String HMAC_SHA1_ALGORITHM = "HmacSHA1";

public void sign(String protocol, String bucketName, String contentPath) throws Exception {
    Calendar cal = Calendar.getInstance();
    cal.add(Calendar.HOUR_OF_DAY, 24);

    String host = bucketName + ".s3-us-west-2.amazonaws.com";
    long expireTime = cal.getTimeInMillis() / 1000;

    String signString = "GET\n" +
        "\n" +
        "\n" +
        expireTime + "\n" +
        "/" + bucketName + contentPath;

    SecretKeySpec signingKey = new SecretKeySpec(SECRET_KEY.getBytes(), HMAC_SHA1_ALGORITHM);
    Mac mac = Mac.getInstance(HMAC_SHA1_ALGORITHM);
    mac.init(signingKey);
    String signature = URLEncoder.encode(new String(Base64.getEncoder().encode(mac.doFinal(signString.getBytes()))));

    System.out.println(signature);
    String fullPayload = "?AWSAccessKeyId=" + ACCESS_KEY +
        "&Expires=" + expireTime + 
        "&Signature=" + signature;

    System.out.println(protocol + "://" + host + contentPath + fullPayload);
}

...

谢谢,我正在尝试为Azure生成密钥,因此不想使用Amazon SDK。这个纯Java解决方案解决了我的问题。 - FuZZbaLL
你不需要在 signString 中添加 host 吗? - ansh sachdeva

1

签名过程漫长且容易出错,这里有一些提示


1
你可以查看AWS网站分享的代码示例。我使用了一些实用类和一些Java类。因此,您不必使用所有类和其他内容。我在下面留下了链接。

Amazon文档中的AWS Java示例


0

对我来说,在Java中,以下代码可用于生成签名请求,通过API网关发送到Web套接字客户端 -

Request<Void> request = new DefaultRequest<Void>("execute-api"); //Request to API gateway
request.setHttpMethod(HttpMethodName.POST);
request.setEndpoint(URI.create(url));
String bodyContnt= "test data";
InputStream targetStream = new ByteArrayInputStream(bodyContnt.getBytes());
request.setContent(targetStream);

//Sign it...
AWS4Signer signer = new AWS4Signer();
signer.setRegionName("ap-south-1");
signer.setServiceName(request.getServiceName());
signer.sign(request, new Creds());
signer.setOverrideDate(new Date()); // needed as current ts is required

//Execute it and get the response...
Response<String> rsp = new AmazonHttpClient(new ClientConfiguration())
.requestExecutionBuilder()
.executionContext(new ExecutionContext(true))
.request(request)
.errorResponseHandler(new SimpleAwsErrorHandler(true))
      .execute(new SimpleResponseHandler());

1
你应该解释一下你的代码如何改进了此问题之前的答案。 - moken
@moken - 我的评论也旨在使输入中的日期设置方面更清晰。没有它,调用将失败! - jswordfish

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接