AWS V4簽名認(rèn)證(Java實(shí)現(xiàn))

最近第三方新增的的HTTP API采用了亞馬遜提供的簽名算法,借此機(jī)會(huì)學(xué)習(xí)一下認(rèn)證過(guò)程,并提供Java實(shí)現(xiàn)。該認(rèn)證首先需要第三方提供的accessKey,secretKey等相關(guān)參數(shù),然后根據(jù)官方提供的算法計(jì)算出認(rèn)證字段并添加到請(qǐng)求的headers中,請(qǐng)示到達(dá)第三方服務(wù)器會(huì)對(duì)headers中的認(rèn)證參數(shù)進(jìn)行對(duì)比,如果一致才能請(qǐng)求成功,否則返回401碼(未被授權(quán))。亞馬遜提供的這套認(rèn)證算法最后需要往headers字段中添加Authorization標(biāo)頭, 具體可以查看如下示例。

如果沒(méi)有認(rèn)證你發(fā)送的請(qǐng)求是這樣的話:

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

添加認(rèn)證后就應(yīng)該是這樣:

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標(biāo)頭包含如下信息:

  • 用于簽名的算法 (AWS4-HMAC-SHA256)
  • 憑證范圍(包含您的訪問(wèn)密鑰 ID)
  • 已簽名標(biāo)頭的列表
  • 計(jì)算簽名。該簽名基于您的請(qǐng)求信息,由您使用 AWS 秘密訪問(wèn)密鑰生成。該簽名用于向 AWS 確認(rèn)您的身份。該部分也是最關(guān)鍵的部分。

1. 認(rèn)證流程

創(chuàng)建規(guī)范請(qǐng)求->創(chuàng)建待簽字符串->計(jì)算簽名->將簽名信息添加到請(qǐng)求,下面以官方提供的測(cè)試套件說(shuō)明文檔進(jìn)行列舉。

首先需要知道:

  • 憑證范圍:AKIDEXAMPLE/20150830/us-east-1/service/aws4_request
  • 私有密鑰:wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY

原始請(qǐng)求:

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

1.1 創(chuàng)建規(guī)范請(qǐng)求

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

host;x-amz-date
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

注意:

  • 參數(shù)按字母順序(依據(jù)字符代碼)進(jìn)行排序。
  • 標(biāo)頭名稱小寫(xiě)。
  • 在 x-amz-date 標(biāo)頭與已簽名標(biāo)頭之間有一個(gè)換行符。
  • 負(fù)載的哈希是空字符串的哈希。

1.2 創(chuàng)建待簽字符串

規(guī)范請(qǐng)求的哈希值返回以下值:
816cd5b414d056048ba4f7c5386d6e0533120fb1fcfa93762cf0fc39e2cf19e0
添加算法、請(qǐng)求日期、憑證范圍和規(guī)范請(qǐng)求哈希以創(chuàng)建待簽字符串:

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

注意:

  • 第二行的日期與 x-amz-date 標(biāo)頭以及憑證范圍的第一個(gè)元素匹配。
  • 最后一行為規(guī)范請(qǐng)求的十六進(jìn)制編碼哈希值。

1.3 計(jì)算簽名

用簽名密鑰和待簽名字符串創(chuàng)建簽名
AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=b97d918cfa904a5beff61c982a1b6f458b799221646efd99d3219ec94cdf2500

1.4 將簽名信息添加到請(qǐng)求

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

上述的示例將用下面實(shí)現(xiàn)的算法來(lái)進(jìn)行舉例說(shuō)明。

2. Java實(shí)現(xiàn)

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();
    }

    /**
     * 任務(wù) 1:針對(duì)簽名版本 4 創(chuàng)建規(guī)范請(qǐng)求
     *
     * @return
     */
    private String prepareCanonicalRequest() {
        StringBuilder canonicalURL = new StringBuilder("");

        /* Step 1.1 以HTTP方法(GET, PUT, POST, etc.)開(kāi)頭, 然后換行. */
        canonicalURL.append(httpMethodName).append("\n");

        /* Step 1.2 添加URI參數(shù),換行. */
        canonicalURI = canonicalURI == null || canonicalURI.trim().isEmpty() ? "/" : canonicalURI;
        canonicalURL.append(canonicalURI).append("\n");

        /* Step 1.3 添加查詢參數(shù),換行. */
        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, 每個(gè)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 對(duì)HTTP或HTTPS的body進(jìn)行SHA256處理. */
        payload = payload == null ? "" : payload;
        canonicalURL.append(generateHex(payload));

        if (debug) {
            System.out.println("##Canonical Request:\n" + canonicalURL.toString());
        }

        return canonicalURL.toString();
    }

    /**
     * 任務(wù) 2:創(chuàng)建簽名版本 4 的待簽字符串
     *
     * @param canonicalURL
     * @return
     */
    private String prepareStringToSign(String canonicalURL) {
        String stringToSign = "";

        /* Step 2.1 以算法名稱開(kāi)頭,并換行. */
        stringToSign = HMACAlgorithm + "\n";

        /* Step 2.2 添加日期,并換行. */
        stringToSign += xAmzDate + "\n";

        /* Step 2.3 添加認(rèn)證范圍,并換行. */
        stringToSign += currentDate + "/" + regionName + "/" + serviceName + "/" + aws4Request + "\n";

        /* Step 2.4 添加任務(wù)1返回的規(guī)范URL哈希處理結(jié)果,然后換行. */
        stringToSign += generateHex(canonicalURL);

        if (debug) {
            System.out.println("##String to sign:\n" + stringToSign);
        }

        return stringToSign;
    }

    /**
     * 任務(wù) 3:為 AWS Signature 版本 4 計(jì)算簽名
     *
     * @param stringToSign
     * @return
     */
    private String calculateSignature(String stringToSign) {
        try {
            /* Step 3.1 生成簽名的key */
            byte[] signatureKey = getSignatureKey(secretAccessKey, currentDate, regionName, serviceName);

            /* Step 3.2 計(jì)算簽名. */
            byte[] signature = HmacSHA256(signatureKey, stringToSign);

            /* Step 3.2.1 對(duì)簽名編碼處理 */
            String strHexSignature = bytesToHex(signature);
            return strHexSignature;
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return null;
    }

    /**
     *任務(wù) 4:將簽名信息添加到請(qǐng)求并返回headers
     *
     * @return
     */
    public Map<String, String> getHeaders() {
        awsHeaders.put("x-amz-date", xAmzDate);

        /* 執(zhí)行任務(wù) 1: 創(chuàng)建aws v4簽名的規(guī)范請(qǐng)求字符串. */
        String canonicalURL = prepareCanonicalRequest();

        /* 執(zhí)行任務(wù) 2: 創(chuàng)建用來(lái)認(rèn)證的字符串 4. */
        String stringToSign = prepareStringToSign(canonicalURL);

        /* 執(zhí)行任務(wù) 3: 計(jì)算簽名. */
        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進(jìn)制化.
     *
     * @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應(yīng)用HmacSHA256算法處理數(shù)據(jù).
     *
     * @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();

    /**
     * 將字節(jié)數(shù)組轉(zhuǎn)換為16進(jìn)制字符串
     *
     * @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'格式的當(dāng)前時(shí)間
     *
     * @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格式的當(dāng)前日期
     *
     * @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

首先要有一點(diǎn)小改造,為了驗(yàn)證算法的準(zhǔn)確信,必須保證參數(shù)的一致。因此可以對(duì)工具類中的getDate方法和getTimeStamp進(jìn)行直接返回,實(shí)際中不必這么做。

    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";
    }

測(cè)試類:

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
================================

可以看到生成的簽名與官方地完全一致,驗(yàn)證了算法實(shí)現(xiàn)的準(zhǔn)確性。另也支持POST, DELETE, PATCH等請(qǐng)求,這里就不一一列舉了,需要注意的如果有body話傳入的paykload字符串應(yīng)該為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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Spring Cloud為開(kāi)發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見(jiàn)模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,696評(píng)論 19 139
  • 國(guó)家電網(wǎng)公司企業(yè)標(biāo)準(zhǔn)(Q/GDW)- 面向?qū)ο蟮挠秒娦畔?shù)據(jù)交換協(xié)議 - 報(bào)批稿:20170802 前言: 排版 ...
    庭說(shuō)閱讀 12,535評(píng)論 6 13
  • 害怕辜負(fù)所有長(zhǎng)輩對(duì)自己的愛(ài),擔(dān)心不能像他們盡自己所能愛(ài)自己一樣,有能力去愛(ài)他們
    糗囧囧閱讀 194評(píng)論 0 0
  • 寫(xiě)在前面 以前寫(xiě)Vue寫(xiě)慣了,心血來(lái)潮,寫(xiě)起了React。并根據(jù)Vue官網(wǎng)文檔的語(yǔ)法順序,寫(xiě)了對(duì)應(yīng)的React的語(yǔ)...
    嘉寶_Appian閱讀 1,263評(píng)論 1 0
  • 絕句 春 微雨初晨一夜風(fēng), 北歸南鳥(niǎo)傲云穹。 匆匆游者聞花笑, 遙望山深冰雪融。 絕句 春 鳥(niǎo)銜南國(guó)他鄉(xiāng)土, 霧送...
    秦川二狗子閱讀 395評(píng)論 0 0

友情鏈接更多精彩內(nèi)容