美文网首页
AWS V4签名认证(Java实现)

AWS V4签名认证(Java实现)

作者: Acamy丶 | 来源:发表于2018-10-14 12:45 被阅读0次

    最近第三方新增的的HTTP API采用了亚马逊提供的签名算法,借此机会学习一下认证过程,并提供Java实现。该认证首先需要第三方提供的accessKey,secretKey等相关参数,然后根据官方提供的算法计算出认证字段并添加到请求的headers中,请示到达第三方服务器会对headers中的认证参数进行对比,如果一致才能请求成功,否则返回401码(未被授权)。亚马逊提供的这套认证算法最后需要往headers字段中添加Authorization标头, 具体可以查看如下示例。

    如果没有认证你发送的请求是这样的话:

    GET /?Param2=value2&Param1=value1 HTTP/1.1
    Host:example.amazonaws.com
    X-Amz-Date:20150830T123600Z
    

    添加认证后就应该是这样:

    GET /?Param2=value2&Param1=value1 HTTP/1.1
    Host:example.amazonaws.com
    X-Amz-Date:20150830T123600Z
    Authorization: AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=b97d918cfa904a5beff61c982a1b6f458b799221646efd99d3219ec94cdf2500
    

    Authorization标头包含如下信息:

    • 用于签名的算法 (AWS4-HMAC-SHA256)
    • 凭证范围(包含您的访问密钥 ID)
    • 已签名标头的列表
    • 计算签名。该签名基于您的请求信息,由您使用 AWS 秘密访问密钥生成。该签名用于向 AWS 确认您的身份。该部分也是最关键的部分。

    1. 认证流程

    创建规范请求->创建待签字符串->计算签名->将签名信息添加到请求,下面以官方提供的测试套件说明文档进行列举。

    首先需要知道:

    • 凭证范围:AKIDEXAMPLE/20150830/us-east-1/service/aws4_request
    • 私有密钥:wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY

    原始请求:

    GET /?Param2=value2&Param1=value1 HTTP/1.1
    Host:example.amazonaws.com
    X-Amz-Date:20150830T123600Z
    

    1.1 创建规范请求

    GET
    /
    Param1=value1&Param2=value2
    host:example.amazonaws.com
    x-amz-date:20150830T123600Z
    
    host;x-amz-date
    e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
    

    注意:

    • 参数按字母顺序(依据字符代码)进行排序。
    • 标头名称小写。
    • 在 x-amz-date 标头与已签名标头之间有一个换行符。
    • 负载的哈希是空字符串的哈希。

    1.2 创建待签字符串

    规范请求的哈希值返回以下值:
    816cd5b414d056048ba4f7c5386d6e0533120fb1fcfa93762cf0fc39e2cf19e0
    添加算法、请求日期、凭证范围和规范请求哈希以创建待签字符串:

    AWS4-HMAC-SHA256
    20150830T123600Z
    20150830/us-east-1/service/aws4_request
    816cd5b414d056048ba4f7c5386d6e0533120fb1fcfa93762cf0fc39e2cf19e0
    

    注意:

    • 第二行的日期与 x-amz-date 标头以及凭证范围的第一个元素匹配。
    • 最后一行为规范请求的十六进制编码哈希值。

    1.3 计算签名

    用签名密钥和待签名字符串创建签名
    AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=b97d918cfa904a5beff61c982a1b6f458b799221646efd99d3219ec94cdf2500

    1.4 将签名信息添加到请求

    GET /?Param2=value2&Param1=value1 HTTP/1.1
    Host:example.amazonaws.com
    X-Amz-Date:20150830T123600Z
    Authorization: AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=b97d918cfa904a5beff61c982a1b6f458b799221646efd99d3219ec94cdf2500
    

    上述的示例将用下面实现的算法来进行举例说明。

    2. Java实现

    2.1 工具类

    import java.io.UnsupportedEncodingException;
    import java.net.URLEncoder;
    import java.security.MessageDigest;
    import java.security.NoSuchAlgorithmException;
    import java.text.DateFormat;
    import java.text.SimpleDateFormat;
    import java.util.Date;
    import java.util.HashMap;
    import java.util.Map;
    import java.util.TimeZone;
    import java.util.TreeMap;
    import javax.crypto.Mac;
    import javax.crypto.spec.SecretKeySpec;
    
    /**
     * AWS V4 签名处理工具
     *
     * 参考链接:https://docs.aws.amazon.com/zh_cn/general/latest/gr/sigv4_signing.html
     */
    public class AWSV4Auth {
    
        private AWSV4Auth() {
        }
    
        public static class Builder {
    
            private String accessKeyID;
            private String secretAccessKey;
            private String regionName;
            private String serviceName;
            private String httpMethodName;
            private String canonicalURI;
            private TreeMap<String, String> queryParametes;
            private TreeMap<String, String> awsHeaders;
            private String payload;
            private boolean debug = false;
    
            public Builder(String accessKeyID, String secretAccessKey) {
                this.accessKeyID = accessKeyID;
                this.secretAccessKey = secretAccessKey;
            }
    
            public Builder regionName(String regionName) {
                this.regionName = regionName;
                return this;
            }
    
            public Builder serviceName(String serviceName) {
                this.serviceName = serviceName;
                return this;
            }
    
            public Builder httpMethodName(String httpMethodName) {
                this.httpMethodName = httpMethodName;
                return this;
            }
    
            public Builder canonicalURI(String canonicalURI) {
                this.canonicalURI = canonicalURI;
                return this;
            }
    
            public Builder queryParametes(TreeMap<String, String> queryParametes) {
                this.queryParametes = queryParametes;
                return this;
            }
    
            public Builder awsHeaders(TreeMap<String, String> awsHeaders) {
                this.awsHeaders = awsHeaders;
                return this;
            }
    
            public Builder payload(String payload) {
                this.payload = payload;
                return this;
            }
    
            public Builder debug() {
                this.debug = true;
                return this;
            }
    
            public AWSV4Auth build() {
                return new AWSV4Auth(this);
            }
        }
    
        private String accessKeyID;
        private String secretAccessKey;
        private String regionName;
        private String serviceName;
        private String httpMethodName;
        private String canonicalURI;
        private TreeMap<String, String> queryParametes;
        private TreeMap<String, String> awsHeaders;
        private String payload;
        private boolean debug = false;
    
        /* Other variables */
        private final String HMACAlgorithm = "AWS4-HMAC-SHA256";
        private final String aws4Request = "aws4_request";
        private String strSignedHeader;
        private String xAmzDate;
        private String currentDate;
    
        private AWSV4Auth(Builder builder) {
            accessKeyID = builder.accessKeyID;
            secretAccessKey = builder.secretAccessKey;
            regionName = builder.regionName;
            serviceName = builder.serviceName;
            httpMethodName = builder.httpMethodName;
            canonicalURI = builder.canonicalURI;
            queryParametes = builder.queryParametes;
            awsHeaders = builder.awsHeaders;
            payload = builder.payload;
            debug = builder.debug;
    
            /* Get current timestamp value.(UTC) */
            xAmzDate = getTimeStamp();
            currentDate = getDate();
        }
    
        /**
         * 任务 1:针对签名版本 4 创建规范请求
         *
         * @return
         */
        private String prepareCanonicalRequest() {
            StringBuilder canonicalURL = new StringBuilder("");
    
            /* Step 1.1 以HTTP方法(GET, PUT, POST, etc.)开头, 然后换行. */
            canonicalURL.append(httpMethodName).append("\n");
    
            /* Step 1.2 添加URI参数,换行. */
            canonicalURI = canonicalURI == null || canonicalURI.trim().isEmpty() ? "/" : canonicalURI;
            canonicalURL.append(canonicalURI).append("\n");
    
            /* Step 1.3 添加查询参数,换行. */
            StringBuilder queryString = new StringBuilder("");
            if (queryParametes != null && !queryParametes.isEmpty()) {
                for (Map.Entry<String, String> entrySet : queryParametes.entrySet()) {
                    String key = entrySet.getKey();
                    String value = entrySet.getValue();
                    queryString.append(key).append("=").append(encodeParameter(value)).append("&");
                }
    
                queryString.deleteCharAt(queryString.lastIndexOf("&"));
    
                queryString.append("\n");
            } else {
                queryString.append("\n");
            }
            canonicalURL.append(queryString);
    
            /* Step 1.4 添加headers, 每个header都需要换行. */
            StringBuilder signedHeaders = new StringBuilder("");
            if (awsHeaders != null && !awsHeaders.isEmpty()) {
                for (Map.Entry<String, String> entrySet : awsHeaders.entrySet()) {
                    String key = entrySet.getKey();
                    String value = entrySet.getValue();
                    signedHeaders.append(key).append(";");
                    canonicalURL.append(key).append(":").append(value).append("\n");
                }
                canonicalURL.append("\n");
            } else {
                canonicalURL.append("\n");
            }
    
            /* Step 1.5 添加签名的headers并换行. */
            strSignedHeader = signedHeaders.substring(0, signedHeaders.length() - 1); // 删掉最后的 ";"
            canonicalURL.append(strSignedHeader).append("\n");
    
            /* Step 1.6 对HTTP或HTTPS的body进行SHA256处理. */
            payload = payload == null ? "" : payload;
            canonicalURL.append(generateHex(payload));
    
            if (debug) {
                System.out.println("##Canonical Request:\n" + canonicalURL.toString());
            }
    
            return canonicalURL.toString();
        }
    
        /**
         * 任务 2:创建签名版本 4 的待签字符串
         *
         * @param canonicalURL
         * @return
         */
        private String prepareStringToSign(String canonicalURL) {
            String stringToSign = "";
    
            /* Step 2.1 以算法名称开头,并换行. */
            stringToSign = HMACAlgorithm + "\n";
    
            /* Step 2.2 添加日期,并换行. */
            stringToSign += xAmzDate + "\n";
    
            /* Step 2.3 添加认证范围,并换行. */
            stringToSign += currentDate + "/" + regionName + "/" + serviceName + "/" + aws4Request + "\n";
    
            /* Step 2.4 添加任务1返回的规范URL哈希处理结果,然后换行. */
            stringToSign += generateHex(canonicalURL);
    
            if (debug) {
                System.out.println("##String to sign:\n" + stringToSign);
            }
    
            return stringToSign;
        }
    
        /**
         * 任务 3:为 AWS Signature 版本 4 计算签名
         *
         * @param stringToSign
         * @return
         */
        private String calculateSignature(String stringToSign) {
            try {
                /* Step 3.1 生成签名的key */
                byte[] signatureKey = getSignatureKey(secretAccessKey, currentDate, regionName, serviceName);
    
                /* Step 3.2 计算签名. */
                byte[] signature = HmacSHA256(signatureKey, stringToSign);
    
                /* Step 3.2.1 对签名编码处理 */
                String strHexSignature = bytesToHex(signature);
                return strHexSignature;
            } catch (Exception ex) {
                ex.printStackTrace();
            }
            return null;
        }
    
        /**
         *任务 4:将签名信息添加到请求并返回headers
         *
         * @return
         */
        public Map<String, String> getHeaders() {
            awsHeaders.put("x-amz-date", xAmzDate);
    
            /* 执行任务 1: 创建aws v4签名的规范请求字符串. */
            String canonicalURL = prepareCanonicalRequest();
    
            /* 执行任务 2: 创建用来认证的字符串 4. */
            String stringToSign = prepareStringToSign(canonicalURL);
    
            /* 执行任务 3: 计算签名. */
            String signature = calculateSignature(stringToSign);
    
            if (signature != null) {
                Map<String, String> header = new HashMap<String, String>(0);
                header.put("x-amz-date", xAmzDate);
                header.put("Authorization", buildAuthorizationString(signature));
    
                if (debug) {
                    System.out.println("##Signature:\n" + signature);
                    System.out.println("##Header:");
                    for (Map.Entry<String, String> entrySet : header.entrySet()) {
                        System.out.println(entrySet.getKey() + " = " + entrySet.getValue());
                    }
                    System.out.println("================================");
                }
                return header;
            } else {
                if (debug) {
                    System.out.println("##Signature:\n" + signature);
                }
                return null;
            }
        }
    
        /**
         * 连接前几步处理的字符串生成Authorization header值.
         *
         * @param strSignature
         * @return
         */
        private String buildAuthorizationString(String strSignature) {
            return HMACAlgorithm + " "
                    + "Credential=" + accessKeyID + "/" + getDate() + "/" + regionName + "/" + serviceName + "/" + aws4Request + ", "
                    + "SignedHeaders=" + strSignedHeader + ", "
                    + "Signature=" + strSignature;
        }
    
        /**
         * 将字符串16进制化.
         *
         * @param data
         * @return
         */
        private String generateHex(String data) {
            MessageDigest messageDigest;
            try {
                messageDigest = MessageDigest.getInstance("SHA-256");
                messageDigest.update(data.getBytes("UTF-8"));
                byte[] digest = messageDigest.digest();
                return String.format("%064x", new java.math.BigInteger(1, digest));
            } catch (NoSuchAlgorithmException | UnsupportedEncodingException e) {
                e.printStackTrace();
            }
            return null;
        }
    
        /**
         * 以给定的key应用HmacSHA256算法处理数据.
         *
         * @param data
         * @param key
         * @return
         * @throws Exception
         * @reference:
         * http://docs.aws.amazon.com/general/latest/gr/signature-v4-examples.html#signature-v4-examples-java
         */
        private byte[] HmacSHA256(byte[] key, String data) throws Exception {
            String algorithm = "HmacSHA256";
            Mac mac = Mac.getInstance(algorithm);
            mac.init(new SecretKeySpec(key, algorithm));
            return mac.doFinal(data.getBytes("UTF8"));
        }
    
        /**
         * 生成AWS 签名
         *
         * @param key
         * @param date
         * @param regionName
         * @param serviceName
         * @return
         * @throws Exception
         * @reference
         * http://docs.aws.amazon.com/general/latest/gr/signature-v4-examples.html#signature-v4-examples-java
         */
        private byte[] getSignatureKey(String key, String date, String regionName, String serviceName) throws Exception {
            byte[] kSecret = ("AWS4" + key).getBytes("UTF8");
            byte[] kDate = HmacSHA256(kSecret, date);
            byte[] kRegion = HmacSHA256(kDate, regionName);
            byte[] kService = HmacSHA256(kRegion, serviceName);
            byte[] kSigning = HmacSHA256(kService, aws4Request);
            return kSigning;
        }
    
        final protected static char[] hexArray = "0123456789ABCDEF".toCharArray();
    
        /**
         * 将字节数组转换为16进制字符串
         *
         * @param bytes
         * @return
         */
        private String bytesToHex(byte[] bytes) {
            char[] hexChars = new char[bytes.length * 2];
            for (int j = 0; j < bytes.length; j++) {
                int v = bytes[j] & 0xFF;
                hexChars[j * 2] = hexArray[v >>> 4];
                hexChars[j * 2 + 1] = hexArray[v & 0x0F];
            }
            return new String(hexChars).toLowerCase();
        }
    
        /**
         * 获取yyyyMMdd'T'HHmmss'Z'格式的当前时间
         *
         * @return
         */
        private String getTimeStamp() {
            DateFormat dateFormat = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'");
            dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));//server timezone
            return dateFormat.format(new Date());
        }
    
        /**
         * 获取yyyyMMdd格式的当前日期
         *
         * @return
         */
        private String getDate() {
            DateFormat dateFormat = new SimpleDateFormat("yyyyMMdd");
            dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));//server timezone
            return dateFormat.format(new Date());
        }
    
        /**
         * UTF-8编码
         * @param param
         * @return
         */
        private String encodeParameter(String param){
            try {
                return URLEncoder.encode(param, "UTF-8");
            } catch (Exception e) {
                return URLEncoder.encode(param);
            }
        }
    }
    

    2.2 Example

    首先要有一点小改造,为了验证算法的准确信,必须保证参数的一致。因此可以对工具类中的getDate方法和getTimeStamp进行直接返回,实际中不必这么做。

        private String getTimeStamp() {
            DateFormat dateFormat = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'");
            dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));//server timezone
            //return dateFormat.format(new Date());
            return "20150830T123600Z";
        }
        private String getDate() {
            DateFormat dateFormat = new SimpleDateFormat("yyyyMMdd");
            dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));//server timezone
            //return dateFormat.format(new Date());
            return "20150830";
        }
    

    测试类:

    import com.example.aws4vauth.AWSV4Auth;
    import org.junit.Test;
    
    import java.util.Map;
    import java.util.TreeMap;
    
    public class AWSV4AuthTest {
        private static final String host = "example.amazonaws.com";
        private static final String accessKey = "AKIDEXAMPLE";
        private static final String secretKey = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY";
    
        private static final String region = "us-east-1";
        private static final String service = "service";
    
        public Map<String, String> auth(String uri, String method, TreeMap<String, String> params, String data){
            TreeMap<String, String> awsHeaders = new TreeMap<String, String>();
            awsHeaders.put("host", host);
    
            return new AWSV4Auth.Builder(accessKey, secretKey)
                    .regionName(region)
                    .serviceName(service)
                    .httpMethodName(method)
                    .canonicalURI(uri)
                    .queryParametes(params)
                    .awsHeaders(awsHeaders)
                    .payload(data)
                    .debug()
                    .build()
                    .getHeaders();
        }
    
        @Test
        public void Test(){
            String uri = "/";
            TreeMap<String, String> awsHeaders = new TreeMap<String, String>();
            awsHeaders.put("host", host);
    
            TreeMap<String, String> params = new TreeMap<String, String>();
            params.put("Param1","value1");
            params.put("Param2","value2");
            Map<String, String> header = auth(uri, "GET", params, null);
            for (Map.Entry<String, String> entrySet : header.entrySet()) {
                String key = entrySet.getKey();
                String value = entrySet.getValue();
            }
        }
    }
    

    输出:

    ##Canonical Request:
    GET
    /
    Param1=value1&Param2=value2
    host:example.amazonaws.com
    x-amz-date:20150830T123600Z
    
    host;x-amz-date
    e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
    ##String to sign:
    AWS4-HMAC-SHA256
    20150830T123600Z
    20150830/us-east-1/service/aws4_request
    816cd5b414d056048ba4f7c5386d6e0533120fb1fcfa93762cf0fc39e2cf19e0
    ##Signature:
    b97d918cfa904a5beff61c982a1b6f458b799221646efd99d3219ec94cdf2500
    ##Header:
    x-amz-date = 20150830T123600Z
    Authorization = AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=b97d918cfa904a5beff61c982a1b6f458b799221646efd99d3219ec94cdf2500
    ================================
    

    可以看到生成的签名与官方地完全一致,验证了算法实现的准确性。另也支持POST, DELETE, PATCH等请求,这里就不一一列举了,需要注意的如果有body话传入的paykload字符串应该为json格式。

    参考链接:
    https://docs.aws.amazon.com/zh_cn/general/latest/gr/sigv4_signing.html
    https://docs.aws.amazon.com/zh_cn/general/latest/gr/signature-v4-test-suite.html#signature-v4-test-suite-derived-creds

    相关文章

      网友评论

          本文标题:AWS V4签名认证(Java实现)

          本文链接:https://www.haomeiwen.com/subject/sudzaftx.html