Java实现AWS S3 V4 Authorization自定义验证

打印 上一主题 下一主题

主题 896|帖子 896|积分 2688

前言

最近在开发文件存储服务,需要符合s3的协议标准,可以直接接入aws-sdk,本文针对sdk发出请求的鉴权信息进行重新组合再签名验证有效性,sdk版本如下
  1.         <dependency>
  2.             <groupId>software.amazon.awssdk</groupId>
  3.             <artifactId>s3</artifactId>
  4.             <version>2.20.45</version>
  5.         </dependency>
复制代码
算法解析

首先对V4版本签名算法的数据结构及签名流程进行拆解分析,以请求头签名为示例讲解
signature = doSign(waitSignString)
签名示例

请求头签名
  1. AWS4-HMAC-SHA256 Credential=admin/20230530/us-east-1/s3/aws4_request, SignedHeaders=amz-sdk-invocation-id;amz-sdk-request;host;x-amz-content-sha256;x-amz-date, Signature=6f50628a101b46264c7783937be0366762683e0d319830b1844643e40b3b0ed
复制代码
Url签名
  1. http://localhost:8001/s3/kkk/test.docx?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20230531T024715Z&X-Amz-SignedHeaders=host&X-Amz-Expires=300&X-Amz-Credential=admin%2F20230531%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Signature=038e2ea71073761aa0370215621599649e9228177c332a0a79f784b1a6d9ee39
复制代码
数据结构

waitSignString = doHex(【第一部分】+【第二部分】+【第三部分】+【第四部分】),每部分使用\n换行符连接,第四部分不要加上换行符
第一部分

Algorithm – 用于创建规范请求的哈希的算法,对于 SHA-256,算法是 AWS4-HMAC-SHA256,则这部分的内容固定为
  1. "AWS4-HMAC-SHA256" + "\n"
复制代码
第二部分

RequestDateTime – 在凭证范围内使用的日期和时间,这个时间为请求发出的时间,直接从请求头获取x-amz-date即可,这部分内容为
  1. request.getHeader("x-amz-date") + "\n"
复制代码
第三部分

CredentialScope – 凭证范围,这会将生成的签名限制在指定的区域和服务范围内,该字符串采用以下格式:YYYYMMDD/region/service/aws4_request
这部分由4个内容信息拼接组成

  • 请求时间的YYYYMMDD格式
  • 存储区域
  • 存储服务
  • 请求头
这些信息我们都可以从请求头的Authorization凭证提取出Credential部分进行拆分重新组合
  1.         String[] parts = authorization.trim().split("\\,");
  2.         String credential = parts[0].split("\\=")[1];
  3.         String[] credentials = credential.split("\\/");
  4.         String accessKey = credentials[0];
  5.         if (!accessKeyId.equals(accessKey)) {
  6.             return false;
  7.         }
  8.         String date = credentials[1];
  9.         String region = credentials[2];
  10.         String service = credentials[3];
  11.         String aws4Request = credentials[4];
复制代码
这部分内容为
  1. date + "/" + region + "/" + service + "/" + aws4Request + "\n"
复制代码
第四部分

HashedCanonicalRequest – 规范请求的哈希
这部分内容为
  1. doHex(canonicalRequest)
复制代码
canonicalRequest具体拆解又可以6小部分组成,每部分使用\n换行符连接,最后不要加上换行符
  1. <HTTPMethod>\n
  2. <CanonicalURI>\n
  3. <CanonicalQueryString>\n
  4. CanonicalHeaders>\n
  5. <SignedHeaders>\n
  6. <HashedPayload>
复制代码

  • HTTPMethod
    代表请求的HTTP方法,例如GET,POST,DELETE,PUT等,直接从request获取即可
    这部分内容为
    1. String HTTPMethod = request.getMethod() + "\n"
    复制代码
  • CanonicalURI
    代表请求的路由部分,例如完成请求为http://localhost:8001/s3/aaaa/ccc.txt,则该部分为/s3/aaaa/ccc.txt
    需要进行encode操作,我这里直接获取则省略了这部分
    这部分内容为
    1. String CanonicalURI = request.getRequestURI().split("\\?")[0] + "\n";
    复制代码
  • CanonicalQueryString
    代表请求参数的拼接成字符串key1=value1&key2=value2这种形式,拼接的key需要按照字母排序
    value需要进行encode操作,我这里直接获取则省略了这部分
    1.         String queryString = ConvertOp.convert2String(request.getQueryString());
    2.         if(!StringUtil.isEmpty(queryString)){
    3.             Map<String, String> queryStringMap =  parseQueryParams(queryString);
    4.             List<String> keyList = new ArrayList<>(queryStringMap.keySet());
    5.             Collections.sort(keyList);
    6.             StringBuilder queryStringBuilder = new StringBuilder("");
    7.             for (String key:keyList) {
    8.                 queryStringBuilder.append(key).append("=").append(queryStringMap.get(key)).append("&");
    9.             }
    10.             queryStringBuilder.deleteCharAt(queryStringBuilder.lastIndexOf("&"));
    11.         }
    12.     public static Map<String, String> parseQueryParams(String queryString) {
    13.         Map<String, String> queryParams = new HashMap<>();
    14.         try {
    15.             if (queryString != null && !queryString.isEmpty()) {
    16.                 String[] queryParamsArray = queryString.split("\\&");
    17.                 for (String param : queryParamsArray) {
    18.                     String[] keyValue = param.split("\\=");
    19.                     if (keyValue.length == 1) {
    20.                         String key = keyValue[0];
    21.                         String value = "";
    22.                         queryParams.put(key, value);
    23.                     }
    24.                     else if (keyValue.length == 2) {
    25.                         String key = keyValue[0];
    26.                         String value = keyValue[1];
    27.                         queryParams.put(key, value);
    28.                     }
    29.                 }
    30.             }
    31.         } catch (Exception e) {
    32.             e.printStackTrace();
    33.         }
    34.         return queryParams;
    35.     }
    复制代码
    这部分内容为
    1. String CanonicalQueryString = queryStringBuilder.toString() + "\n"
    复制代码
  • CanonicalHeaders
    代表请求头拼接成字符串key:value的形式,每个head部分使用\n换行符连接,拼接的key需要按照字母排序
    签名的请求头从Authorization解析获取
    1.         String signedHeader = parts[1].split("\\=")[1];
    2.         String[] signedHeaders = signedHeader.split("\\;");
    复制代码
    1.         String headString = "";
    2.         for (String name : signedHeaders) {
    3.             headString += name + ":" + request.getHeader(name) + "\n";
    4.         }
    复制代码
    这部分内容为
    1. String CanonicalHeaders = headString + "\n"
    复制代码
  • SignedHeaders
    代表请求头的key部分,使用;隔开
    这部分内容为从Authorization解析中获取
    这部分内容为
    1. String SignedHeaders = signedHeader + "\n"
    复制代码
  • HashedPayload
    代表请求body部分的签名,直接从requet的head提取x-amz-content-sha256内容
    这部分内容为
    1. String HashedPayload = Stringrequest.getHeader("x-amz-content-sha256")
    复制代码
doHex

本部分只是一个字符串转16进制的一个操作
  1.     private String doHex(String data) {
  2.         MessageDigest messageDigest;
  3.         try {
  4.             messageDigest = MessageDigest.getInstance("SHA-256");
  5.             messageDigest.update(data.getBytes("UTF-8"));
  6.             byte[] digest = messageDigest.digest();
  7.             return String.format("%064x", new java.math.BigInteger(1, digest));
  8.         } catch (NoSuchAlgorithmException | UnsupportedEncodingException e) {
  9.             e.printStackTrace();
  10.         }
  11.         return null;
  12.     }
复制代码
签名流程

doSign 的流程为doBytesToHex(doHmacSHA256(signatureKey,waitSignString ))
doBytesToHex为byte转16进制操作
  1.     private String doBytesToHex(byte[] bytes) {
  2.         char[] hexChars = new char[bytes.length * 2];
  3.         for (int j = 0; j < bytes.length; j++) {
  4.             int v = bytes[j] & 0xFF;
  5.             hexChars[j * 2] = hexArray[v >>> 4];
  6.             hexChars[j * 2 + 1] = hexArray[v & 0x0F];
  7.         }
  8.         return new String(hexChars).toLowerCase();
  9.     }
复制代码
doHmacSHA256为签名算法
  1.     private byte[] doHmacSHA256(byte[] key, String data) throws Exception {
  2.         String algorithm = "HmacSHA256";
  3.         Mac mac = Mac.getInstance(algorithm);
  4.         mac.init(new SecretKeySpec(key, algorithm));
  5.         return mac.doFinal(data.getBytes("UTF8"));
  6.     }
复制代码
signatureKey签名密钥由secretAccessKey,请求时间,存储区域,存储服务,请求头这5个要素进行叠加签名生成
  1.         byte[] kSecret = ("AWS4" + secretAccessKey).getBytes("UTF8");
  2.         byte[] kDate = doHmacSHA256(kSecret, date);
  3.         byte[] kRegion = doHmacSHA256(kDate, region);
  4.         byte[] kService = doHmacSHA256(kRegion, service);
  5.         byte[] signatureKey = doHmacSHA256(kService, aws4Request);
复制代码
将最终生成的再签名与Authorization中解析出的Signature进行比较,一致则鉴权成功
调试位置

调试过程中需要验证每部分的签名是否拼接编码正确,我们需要和sdk生成的内容进行比对找出问题
调试software.amazon.awssdk.auth.signer.internal包下AbstractAws4Signer类的doSign类,获取stringToSign与你待签名字符串比对差异,源码如下
  1.     protected Builder doSign(SdkHttpFullRequest request, Aws4SignerRequestParams requestParams, T signingParams, ContentChecksum contentChecksum) {
  2.         Builder mutableRequest = request.toBuilder();
  3.         AwsCredentials sanitizedCredentials = this.sanitizeCredentials(signingParams.awsCredentials());
  4.         if (sanitizedCredentials instanceof AwsSessionCredentials) {
  5.             this.addSessionCredentials(mutableRequest, (AwsSessionCredentials)sanitizedCredentials);
  6.         }
  7.         this.addHostHeader(mutableRequest);
  8.         this.addDateHeader(mutableRequest, requestParams.getFormattedRequestSigningDateTime());
  9.         mutableRequest.firstMatchingHeader("x-amz-content-sha256").filter((h) -> {
  10.             return h.equals("required");
  11.         }).ifPresent((h) -> {
  12.             mutableRequest.putHeader("x-amz-content-sha256", contentChecksum.contentHash());
  13.         });
  14.         this.putChecksumHeader(signingParams.checksumParams(), contentChecksum.contentFlexibleChecksum(), mutableRequest, contentChecksum.contentHash());
  15.         AbstractAws4Signer.CanonicalRequest canonicalRequest = this.createCanonicalRequest(request, mutableRequest, contentChecksum.contentHash(), signingParams.doubleUrlEncode(), signingParams.normalizePath());
  16.         String canonicalRequestString = canonicalRequest.string();
  17.         String stringToSign = this.createStringToSign(canonicalRequestString, requestParams);
  18.         byte[] signingKey = this.deriveSigningKey(sanitizedCredentials, requestParams);
  19.         byte[] signature = this.computeSignature(stringToSign, signingKey);
  20.         mutableRequest.putHeader("Authorization", this.buildAuthorizationHeader(signature, sanitizedCredentials, requestParams, canonicalRequest));
  21.         this.processRequestPayload(mutableRequest, signature, signingKey, requestParams, signingParams, contentChecksum.contentFlexibleChecksum());
  22.         return mutableRequest;
  23.     }
复制代码
代码示例

通过拦截器进行验证的过程,完整代码如下,兼容了普通请求的头部验证和文件下载url的签名验证
  1. @Componentpublic class S3Intecept implements HandlerInterceptor {    @Autowired    private SystemConfig systemConfig;    @Override    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {        boolean flag = false;        String authorization = request.getHeader("Authorization");        if(!StringUtil.isEmpty(authorization)){            flag = validAuthorizationHead(request, systemConfig.getUsername(), systemConfig.getPassword());        }else{            authorization = request.getParameter("X-Amz-Credential");            if(!StringUtil.isEmpty(authorization)){                flag = validAuthorizationUrl(request, systemConfig.getUsername(), systemConfig.getPassword());            }        }        if(!flag){            response.setStatus(HttpStatus.UNAUTHORIZED.value());        }        return flag;    }    public boolean validAuthorizationHead(HttpServletRequest request, String accessKeyId, String secretAccessKey) throws Exception {        String authorization = request.getHeader("Authorization");        String requestDate = request.getHeader("x-amz-date");        String contentHash = request.getHeader("x-amz-content-sha256");        String httpMethod = request.getMethod();        String uri = request.getRequestURI().split("\\?")[0];        String queryString = ConvertOp.convert2String(request.getQueryString());        //示例        //AWS4-HMAC-SHA256 Credential=admin/20230530/us-east-1/s3/aws4_request, SignedHeaders=amz-sdk-invocation-id;amz-sdk-request;host;x-amz-content-sha256;x-amz-date, Signature=6f50628a101b46264c7783937be0366762683e0d319830b1844643e40b3b0ed        ///region authorization拆分        String[] parts = authorization.trim().split("\\,");        //第一部分-凭证范围        String credential = parts[0].split("\\=")[1];        String[] credentials = credential.split("\\/");        String accessKey = credentials[0];        if (!accessKeyId.equals(accessKey)) {            return false;        }        String date = credentials[1];        String region = credentials[2];        String service = credentials[3];        String aws4Request = credentials[4];        //第二部分-签名头中包含哪些字段        String signedHeader = parts[1].split("\\=")[1];
  2.         String[] signedHeaders = signedHeader.split("\\;");        //第三部分-生成的签名        String signature = parts[2].split("\\=")[1];        ///endregion        ///region 待签名字符串        String stringToSign = "";        //签名由4部分组成        //1-Algorithm – 用于创建规范请求的哈希的算法。对于 SHA-256,算法是 AWS4-HMAC-SHA256。        stringToSign += "AWS4-HMAC-SHA256" + "\n";        //2-RequestDateTime – 在凭证范围内使用的日期和时间。        stringToSign += requestDate + "\n";        //3-CredentialScope – 凭证范围。这会将生成的签名限制在指定的区域和服务范围内。该字符串采用以下格式:YYYYMMDD/region/service/aws4_request        stringToSign += date + "/" + region + "/" + service + "/" + aws4Request + "\n";        //4-HashedCanonicalRequest – 规范请求的哈希。        //\n        //\n        //\n        //\n        //\n        //        String hashedCanonicalRequest = "";        //4.1-HTTP Method        hashedCanonicalRequest += httpMethod + "\n";        //4.2-Canonical URI        hashedCanonicalRequest += uri + "\n";        //4.3-Canonical Query String        if(!StringUtil.isEmpty(queryString)){            Map queryStringMap =  parseQueryParams(queryString);            List keyList = new ArrayList(queryStringMap.keySet());            Collections.sort(keyList);            StringBuilder queryStringBuilder = new StringBuilder("");            for (String key:keyList) {                queryStringBuilder.append(key).append("=").append(queryStringMap.get(key)).append("&");            }            queryStringBuilder.deleteCharAt(queryStringBuilder.lastIndexOf("&"));            hashedCanonicalRequest += queryStringBuilder.toString() + "\n";        }else{            hashedCanonicalRequest += queryString + "\n";        }        //4.4-Canonical Headers        for (String name : signedHeaders) {            hashedCanonicalRequest += name + ":" + request.getHeader(name) + "\n";        }        hashedCanonicalRequest += "\n";        //4.5-Signed Headers        hashedCanonicalRequest += signedHeader + "\n";        //4.6-Hashed Payload        hashedCanonicalRequest += contentHash;        stringToSign += doHex(hashedCanonicalRequest);        ///endregion        ///region 重新生成签名        //计算签名的key        byte[] kSecret = ("AWS4" + secretAccessKey).getBytes("UTF8");
  3.         byte[] kDate = doHmacSHA256(kSecret, date);
  4.         byte[] kRegion = doHmacSHA256(kDate, region);
  5.         byte[] kService = doHmacSHA256(kRegion, service);
  6.         byte[] signatureKey = doHmacSHA256(kService, aws4Request);        //计算签名        byte[] authSignature = doHmacSHA256(signatureKey, stringToSign);        //对签名编码处理        String strHexSignature = doBytesToHex(authSignature);        ///endregion        if (signature.equals(strHexSignature)) {            return true;        }        return false;    }    public boolean validAuthorizationUrl(HttpServletRequest request, String accessKeyId, String secretAccessKey) throws Exception {        String requestDate = request.getParameter("X-Amz-Date");        String contentHash = "UNSIGNED-PAYLOAD";        String httpMethod = request.getMethod();        String uri = request.getRequestURI().split("\\?")[0];        String queryString = ConvertOp.convert2String(request.getQueryString());        //示例        //"http://localhost:8001/s3/kkk/%E6%B1%9F%E5%AE%81%E8%B4%A2%E6%94%BF%E5%B1%80%E9%A1%B9%E7%9B%AE%E5%AF%B9%E6%8E%A5%E6%96%87%E6%A1%A3.docx?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20230531T024715Z&X-Amz-SignedHeaders=host&X-Amz-Expires=300&X-Amz-Credential=admin%2F20230531%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Signature=038e2ea71073761aa0370215621599649e9228177c332a0a79f784b1a6d9ee39        ///region 参数准备        //第一部分-凭证范围        String credential =request.getParameter("X-Amz-Credential");        String[] credentials = credential.split("\\/");        String accessKey = credentials[0];        if (!accessKeyId.equals(accessKey)) {            return false;        }        String date = credentials[1];        String region = credentials[2];        String service = credentials[3];        String aws4Request = credentials[4];        //第二部分-签名头中包含哪些字段        String signedHeader = request.getParameter("X-Amz-SignedHeaders");        String[] signedHeaders = signedHeader.split("\\;");        //第三部分-生成的签名        String signature = request.getParameter("X-Amz-Signature");        ///endregion        ///region 验证expire        String expires = request.getParameter("X-Amz-Expires");        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmss'Z'");        LocalDateTime startDate = LocalDateTime.parse(requestDate,formatter);        ZoneId zoneId = ZoneId.systemDefault();        ZonedDateTime localDateTime = startDate.atZone(ZoneId.of("UTC")).withZoneSameInstant(zoneId);        startDate = localDateTime.toLocalDateTime();        LocalDateTime endDate = startDate.plusSeconds(ConvertOp.convert2Int(expires));        if(endDate.isBefore(LocalDateTime.now())){            return false;        }        ///endregion        ///region 待签名字符串        String stringToSign = "";        //签名由4部分组成        //1-Algorithm – 用于创建规范请求的哈希的算法。对于 SHA-256,算法是 AWS4-HMAC-SHA256。        stringToSign += "AWS4-HMAC-SHA256" + "\n";        //2-RequestDateTime – 在凭证范围内使用的日期和时间。        stringToSign += requestDate + "\n";        //3-CredentialScope – 凭证范围。这会将生成的签名限制在指定的区域和服务范围内。该字符串采用以下格式:YYYYMMDD/region/service/aws4_request        stringToSign += date + "/" + region + "/" + service + "/" + aws4Request + "\n";        //4-HashedCanonicalRequest – 规范请求的哈希。        //\n        //\n        //\n        //\n        //\n        //        String hashedCanonicalRequest = "";        //4.1-HTTP Method        hashedCanonicalRequest += httpMethod + "\n";        //4.2-Canonical URI        hashedCanonicalRequest += uri + "\n";        //4.3-Canonical Query String        if(!StringUtil.isEmpty(queryString)){            Map queryStringMap =  parseQueryParams(queryString);            List keyList = new ArrayList(queryStringMap.keySet());            Collections.sort(keyList);            StringBuilder queryStringBuilder = new StringBuilder("");            for (String key:keyList) {                if(!key.equals("X-Amz-Signature")){                    queryStringBuilder.append(key).append("=").append(queryStringMap.get(key)).append("&");                }            }            queryStringBuilder.deleteCharAt(queryStringBuilder.lastIndexOf("&"));            hashedCanonicalRequest += queryStringBuilder.toString() + "\n";        }else{            hashedCanonicalRequest += queryString + "\n";        }        //4.4-Canonical Headers        for (String name : signedHeaders) {            hashedCanonicalRequest += name + ":" + request.getHeader(name) + "\n";        }        hashedCanonicalRequest += "\n";        //4.5-Signed Headers        hashedCanonicalRequest += signedHeader + "\n";        //4.6-Hashed Payload        hashedCanonicalRequest += contentHash;        stringToSign += doHex(hashedCanonicalRequest);        ///endregion        ///region 重新生成签名        //计算签名的key        byte[] kSecret = ("AWS4" + secretAccessKey).getBytes("UTF8");
  7.         byte[] kDate = doHmacSHA256(kSecret, date);
  8.         byte[] kRegion = doHmacSHA256(kDate, region);
  9.         byte[] kService = doHmacSHA256(kRegion, service);
  10.         byte[] signatureKey = doHmacSHA256(kService, aws4Request);        //计算签名        byte[] authSignature = doHmacSHA256(signatureKey, stringToSign);        //对签名编码处理        String strHexSignature = doBytesToHex(authSignature);        ///endregion        if (signature.equals(strHexSignature)) {            return true;        }        return false;    }    private String doHex(String data) {
  11.         MessageDigest messageDigest;
  12.         try {
  13.             messageDigest = MessageDigest.getInstance("SHA-256");
  14.             messageDigest.update(data.getBytes("UTF-8"));
  15.             byte[] digest = messageDigest.digest();
  16.             return String.format("%064x", new java.math.BigInteger(1, digest));
  17.         } catch (NoSuchAlgorithmException | UnsupportedEncodingException e) {
  18.             e.printStackTrace();
  19.         }
  20.         return null;
  21.     }    private byte[] doHmacSHA256(byte[] key, String data) throws Exception {
  22.         String algorithm = "HmacSHA256";
  23.         Mac mac = Mac.getInstance(algorithm);
  24.         mac.init(new SecretKeySpec(key, algorithm));
  25.         return mac.doFinal(data.getBytes("UTF8"));
  26.     }    final protected static char[] hexArray = "0123456789ABCDEF".toCharArray();    private String doBytesToHex(byte[] bytes) {
  27.         char[] hexChars = new char[bytes.length * 2];
  28.         for (int j = 0; j < bytes.length; j++) {
  29.             int v = bytes[j] & 0xFF;
  30.             hexChars[j * 2] = hexArray[v >>> 4];
  31.             hexChars[j * 2 + 1] = hexArray[v & 0x0F];
  32.         }
  33.         return new String(hexChars).toLowerCase();
  34.     }    public static Map parseQueryParams(String queryString) {        Map queryParams = new HashMap();        try {            if (queryString != null && !queryString.isEmpty()) {                String[] queryParamsArray = queryString.split("\\&");                for (String param : queryParamsArray) {                    String[] keyValue = param.split("\\=");                    if (keyValue.length == 1) {                        String key = keyValue[0];                        String value = "";                        queryParams.put(key, value);                    }                    else if (keyValue.length == 2) {                        String key = keyValue[0];                        String value = keyValue[1];                        queryParams.put(key, value);                    }                }            }        } catch (Exception e) {            e.printStackTrace();        }        return queryParams;    }}
复制代码
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

玛卡巴卡的卡巴卡玛

金牌会员
这个人很懒什么都没写!
快速回复 返回顶部 返回列表