프런트 엔드에 ak/sk를 노출하지 않고 알리바바 클라우드 oss를 직접 업로드하는 방식

수요 원인

이전에 글을 쓴 적이 있는데 프론트엔드에서 aws S3에 ak/sk를 노출하지 않고 aws S3에 직접 업로드하는 방식
알리바바 클라우드의 oss 업로드도 프로젝트에서 사용하기 때문에 알리바바 클라우드에도 ak/sk를 방지할 수 있는 솔루션이 있는지 연구했습니다. 프런트 엔드로 유출되는 것을 방지합니다.
여기에서도 이렇게 하는 이유를 요약합니다:
ak/sk가 사용자에게 알려지는 것을 방지하기 위한 기존의 업로드 방식으로 파일 유출 및 변조,
일반적으로 프런트 엔드 업로드 파일을 백엔드로 보낸 다음 백엔드에서 Alibaba Cloud oss로 업로드 참조 아키텍처:
用户 => 浏览器选文件 => 后端服务器 => oss
이 솔루션의 문제점:

  • 링크가 길고 업로드가 느린 이유는 여분의 중간 노드가 있고 시간이 두 배가 되기 때문입니다.
    링크가 길기 때문에 시간 초과 중단 오류가 발생할 확률이 더 높습니다.
  • 파일이 너무 크면 백엔드 서버에서 받을 수 없고 기본 구성을 수정해야 합니다.예를 들어 SpringBoot는 기본적으로 최대 1M을 업로드하지만 수정하면 추가 성능 문제가 발생할 수 있습니다.예를 들어 여러 사람이 동시에 큰 파일을 업로드하면 이 서비스는 다른 서비스에 응답하지 못할 수 있습니다. 사용자가 요청하면 심한 경우 눈사태가 발생할 수도 있습니다.
  • 불필요한 트래픽 과금이 있을 수 있습니다.일반적으로 클라우드 서버의 트래픽 유출은 과금됩니다.예를 들어 알리바바 클라우드는 80센트/GB여야 합니다.그리고 1G 파일을 업로드하면 서버는 1G 트래픽을 수신하고 oss를 업로드하고 1G 트래픽을 출력합니다
    . , 공용 네트워크 또는 교차 지역 전송과 관련된 경우 비용이 더 많이 듭니다.

해결책

Alibaba Cloud 문서를 확인하면 AWS와 유사한 솔루션을 제공합니다:
1. 백엔드는 OSS로 이동하여 aliyuncs.com 도메인 이름의 시간 제한 서명 URL을 생성합니다.
2. 프런트 엔드는 다음을 통해 OSS를 직접 업로드합니다. 이 URL
, ak/sk 여전히 백엔드에 저장되고 프론트엔드에 노출될 위험이 없으며 URL에는 시간 제한이 있으며 그 후에는 무효화됩니다.
공식 문서를 참조하십시오. Java는 서명된 URL을 사용하여 일시적으로 파일 업로드 또는 다운로드를 승인합니다.

그런데 이 공식 문서에는 Java 서명 url만 있고 Java로 업로드된 데모를 사용하는데, 검색을 해보니 자바스크립트 버전의 데모를 찾을 수가 없어서 직접 구현을 공부해야 했습니다. 다음은 구현 단계에 대한 간략한 설명입니다.

구현 단계

이 문서는 SpringBoot2.3.7.RELEASE를 기반으로 합니다
참고: 이 문서의 데모 코드는 github에 업로드되었습니다.질문이 있는 경우 여기를 클릭하여 이 코드를 다운로드 하고 로컬에서 실행 및 확인할 수 있습니다.

1. pom 종속성 가져오기, Aliyun sdk 가져오기 추가:

<dependency>
    <groupId>com.aliyun.oss</groupId>
    <artifactId>aliyun-sdk-oss</artifactId>
    <version>3.16.1</version>
</dependency>

2. 백엔드는 서명된 URL을 생성하기 위한 인터페이스를 추가합니다.


인터페이스 에는 업로드할 대상 파일의 상대 경로와 파일의 ContentType이라는 두 가지 매개 변수가 필요합니다.

package beinet.cn.frontstudy.oss;

import com.aliyun.oss.HttpMethod;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.internal.OSSHeaders;
import com.aliyun.oss.model.GeneratePresignedUrlRequest;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.joda.time.LocalDateTime;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;

@Slf4j
@RestController
public class OssController {
    
    
    private OSS client;

    // 下面4个参数,是上传到s3的必需配置
    /*
     注意:绝对不要把ak sk写在代码里,或写在配置里,泄露会导致oss数据泄露,被删除,被占用等不可预知的后果
     建议:
     1、安全性较低:加密后写入配置文件,代码里解密,参考: https://youbl.blog.csdn.net/article/details/122603550
     2、安全性较高:由运维在服务器上配置环境变量,程序中读取环境变量使用
    */
    private String accessKey = "I'm ak";
    private String secretKey = "I'm sk";
    private String region = "oss-cn-shenzhen";// 常用Region参考: https://help.aliyun.com/document_detail/140601.html
    private String endpoint = "https://" + region + ".aliyuncs.com";
    private String bucket = "my-bucket";

    @SneakyThrows
    public OssController() {
    
    
        this.client = new OSSClientBuilder().build(endpoint, accessKey, secretKey);
    }

    /**
     * 生成一个预签名的url,给前端js上传
     * 参考官网文档: https://help.aliyun.com/document_detail/32016.html
     *
     * @param ossFileName 上传到oss的文件相对路径
     * @param contentType 签名里会加入contentType进行计算,因此此参数必须
     * @return 签名后的url
     */
    @GetMapping("oss/sign")
    public String preUploadFile(@RequestParam String ossFileName, @RequestParam String contentType) {
    
    

        // 设置请求头。
        Map<String, String> headers = new HashMap<>();
        // 指定ContentType,注意:必须指定,这个header加入签名了,不指定时前端带Content-Type上传,会导致签名验证不通过
        headers.put(OSSHeaders.CONTENT_TYPE, contentType);

        // 生成签名URL。
        GeneratePresignedUrlRequest request = new GeneratePresignedUrlRequest(bucket, ossFileName, HttpMethod.PUT);
        // 设置过期时间1小时。
        request.setExpiration(LocalDateTime.now().plusHours(1).toDate());

        // 将请求头加入到request中。
        request.setHeaders(headers);
        return client.generatePresignedUrl(request).toString();
    }
}

3. 프런트엔드 업로드 코드

<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <title>阿里云OSS上传演示</title>
    <script type="text/javascript" src="/res/unpkg/vue.min.js"></script>
    <script type="text/javascript" src="/res/unpkg/axios.min.js"></script>
</head>
<body>
<hr>
<div id="divApp">
    <input type="file" ref="fileInput1" accept="*" @change="getFile">
</div>
<hr>
<script>
    var vueApp = new Vue({
     
     
        el: '#divApp',
        data: function () {
     
     
            return {
     
     
                title: '阿里云OSS-免ak/sk上传演示代码',
                ossSignUrl: '',
            }
        },
        methods: {
     
     
            getOssSignUrl: function (type) {
     
     
                // 因为rfc2616协议要求,语法有body必须有Content-Type的header,而oss又会对这个header进行签名计算,所以获取签名url时,要指定Content-Type
                let url = '/oss/sign?contentType=' + type + '&ossFileName=abc/signFile123.xxx';
                return axios.get(url).then(response => {
     
     
                    this.ossSignUrl = response.data;
                }).catch(error => this.ajaxError(error));
            },
            // 获取文件数据
            getFile: function (event) {
     
     
                let type = event.target.files[0].type;
                this.getOssSignUrl(type).then(() => {
     
     
                    this.uploadToSignUrl(event, type);
                });
            },
            uploadToSignUrl: function (evt, type) {
     
     
                // 通过fiddler抓包测试,body直接就是文件的内容,不能带有其它格式
                axios({
     
     
                    method: "PUT",
                    url: this.ossSignUrl,
                    data: evt.target.files[0],
                    transformRequest: [
                        function (data, headers) {
     
     
                            //delete headers.common['Content-Type'];
                            headers.put['Content-Type'] = type;
                            return data;
                        }
                    ],
                }).then(response => {
     
     
                    alert("上传成功" + response.data);
                }).catch(error => this.ajaxError(error));
            },
            ajaxError: function (error) {
     
     
                alert('未知错误' + error.message);
            },
        },
    });
</script>
</body>
</html>

그 과정에서 구덩이

프런트엔드에 업로드하는 aws 코드를 직접 재사용하면 충분할 줄 알았는데 문제를 찾아 수정하는데 반나절이 걸렸습니다.피트를 밟는 과정은 다음과 같이 구성됩니다.

첫 번째 단계에서 표준 multipart/form-data를 사용하여 업로드하는 데 오류가 있습니다.

GeneratePresignedUrlRequest request = new GeneratePresignedUrlRequest(bucket, ossFileName, HttpMethod.PUT);
// 设置过期时间1小时。
request.setExpiration(LocalDateTime.now().plusHours(1).toDate());
// 刚开始没有加这一步:将请求头加入到request中。
// request.setHeaders(headers);
return client.generatePresignedUrl(request).toString();

그런 다음 프런트 엔드는 삶과 죽음을 업로드할 수 없으며 오류를 계속 보고합니다. <Message>The request signature we calculated does not match the signature you provided. Check your key and signing method.</Message>
프런트 엔드 업로드 코드에 문제가 있습니다.

axios.put(url, evt.target.files[0]).then(response => {
    
    
    alert("上传成功" + response.data);
})

이렇게 변경한 후에도 여전히 서명 오류를 보고합니다.

let formFile = new FormData();
formFile.append("file", evt.target.files[0]);
axios.put(this.ossSignUrl, formFile);

자바스크립트 데모를 못찾아서 어디가 문제인지 모르겠어서 알리바바클라우드 문서만 계속 찾아볼 수 밖에...


나중에 Aliyun의 Java 데모: [https://help.aliyun.com/document_detail/32016.html#p-bpg-g75-6jc](https://help.aliyun.com/document_detail/32016.html# p-bpg-g75-6jc) 이 코드에 의해 생성된 서명된 URL은 정상적으로 업로드될 수 있습니다: ```java HttpPut put = new HttpPut(signedUrl.toString()); HttpEntity entity = new FileEntity( new File(pathName)); put.setEntity(엔티티); httpClient = HttpClients.createDefault(); 응답 = httpClient.execute(put); ```

그래서 이 컴퓨터에 Fiddler를 설치하여 자바스크립트 요청 패킷과 자바 요청 패킷을 비교하려고 했습니다. 차이점은 무엇입니까? 패킷을 캡처하는 프로세스가 복잡하고 httpClient에서 계속 오류를 보고합니다. 마지막으로 간단하게 설정
합니다 unable to find valid certification path to requested target
. 패킷 캡처를 정상적으로 완료하기 위해 httpClient에서 SSL 인증서 확인을 무시합니다.

fiddler가 패키지를 캡처하는 것으로 확인되었으며 javascript 서명 예외의 요청 본문은 다음과 같습니다.

PUT https://my-bucket.oss-cn-shenzhen.aliyuncs.com/abc/signFile123.xxx?Expires=1688011585&OSSAccessKeyId=xxx&Signature=xxx HTTP/1.1
Host: my-bucket.oss-cn-shenzhen.aliyuncs.com
Connection: keep-alive
Content-Length: 211
sec-ch-ua: "Not.A/Brand";v="8", "Chromium";v="114", "Google Chrome";v="114"
Accept: application/json, text/plain, */*
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryts74dVVwrCRtEbp1
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36
sec-ch-ua-platform: "Windows"
Origin: http://localhost:8801
Sec-Fetch-Site: cross-site
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: http://localhost:8801/
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8

------WebKitFormBoundaryts74dVVwrCRtEbp1
Content-Disposition: form-data; name="file"; filename="index.txt"
Content-Type: text/plain

文件内容
------WebKitFormBoundaryts74dVVwrCRtEbp1--

정상적으로 업로드 가능한 Java 패킷 캡처 요청 본문은 다음과 같습니다.

PUT https://my-bucket.oss-cn-shenzhen.aliyuncs.com/abc/signFile123.xxx?Expires=1688011757&OSSAccessKeyId=xxx&Signature=xxx HTTP/1.1
Content-Length: 28
Host: my-bucket.oss-cn-shenzhen.aliyuncs.com
Connection: Keep-Alive
User-Agent: Apache-HttpClient/4.5.14 (Java/11)
Accept-Encoding: gzip,deflate

文件内容

아 알고보니 알리바바 클라우드는 표준 multipart/form-data업로드 데이터 형식에 따라 파일을 받지 않고 본문을 직접 읽어서 완전한 파일 내용으로 하고 있습니다. 사양?

두 번째 단계에서 프런트 엔드는 더 많은 콘텐츠 유형을 업로드하여 서명 오류가 발생합니다.

Aliyun Niu, 파일을 직접 전송하도록 프런트 엔드 코드를 변경했습니다.

axios({
    
    
    method: "PUT",
    url: this.ossSignUrl,
    data: evt.target.files[0],
})

파일의 내용을 본문에 직접 쓰십시오. 이번에는 맞을 것입니다.
테스트 후에도 서명 오류가 계속 보고됩니다. 패킷 캡처를 살펴보세요.

PUT https://my-bucket.oss-cn-shenzhen.aliyuncs.com/abc/signFile123.xxx?Expires=1688019898&OSSAccessKeyId=xxx&Signature=xxx HTTP/1.1
Host: my-bucket.oss-cn-shenzhen.aliyuncs.com
Connection: keep-alive
Content-Length: 28
sec-ch-ua: "Not.A/Brand";v="8", "Chromium";v="114", "Google Chrome";v="114"
Accept: application/json, text/plain, */*
Content-Type: application/x-www-form-urlencoded
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36
sec-ch-ua-platform: "Windows"
Origin: http://localhost:8801
Sec-Fetch-Site: cross-site
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: http://localhost:8801/
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8

文件内容

본문이 더 이상 양식 데이터 형식이 아닌데 왜 오류가 발생합니까?

다행히 fiddler는 요청을 편집하고 요청을 재생하는 기능이 있으며 중복 헤더를 단계별로 삭제합니다.마지막으로 Content-Type을 삭제한 후 업로드에 성공합니다...

알겠습니다. 프런트 엔드 코드를 수정하고 이 Content-Type을 삭제하면 됩니다. 수정된 코드는 다음과 같습니다.

axios({
    
    
    method: "PUT",
    url: this.ossSignUrl,
    data: evt.target.files[0],
    transformRequest: [
        function (data, headers) {
    
    
            delete headers.common['Content-Type'];
            delete headers.put['Content-Type'];
            return data;
        }
    ],
})

여전히 오류를 보고하는 이유는 무엇입니까? 패킷 캡처를 보십시오. 요청에 여전히 Content-Type이 있습니까? ! ? axios를 확인한 후 공식
git 코드에 문제가 있습니다 . 몇 가지 다른 솔루션을 시도했지만 여전히 이 요청에서 Content-Type을 삭제할 수 없습니다.

문제를 해결하기 위해 세 번째 단계, 역 푸시

나중에 rfc 프로토콜에서 Header Content-Type을 제공해야 한다는 것이 생각나서 rfc2616 프로토콜을 확인해보니 다음과 같은 내용이 있습니다.

7.2.1 Type
   When an entity-body is included with a message, the data type of that
   body is determined via the header fields Content-Type and Content-
   Encoding. These define a two-layer, ordered encoding model:

       entity-body := Content-Encoding( Content-Type( data ) )

   Content-Type specifies the media type of the underlying data.
   Content-Encoding may be used to indicate any additional content
   codings applied to the data, usually for the purpose of data
   compression, that are a property of the requested resource. There is
   no default encoding.

   Any HTTP/1.1 message containing an entity-body SHOULD include a
   Content-Type header field defining the media type of that body. If
   and only if the media type is not given by a Content-Type field, the
   recipient MAY attempt to guess the media type via inspection of its
   content and/or the name extension(s) of the URI used to identify the
   resource. If the media type remains unknown, the recipient SHOULD
   treat it as type "application/octet-stream".

마지막 단락의 일반적인 생각은 본문이 있는 http 메시지가 있고, Content-Type을 포함해야 하며 대문자는 rfc가 이 헤더를 추가할 것을 강력히 권장한다는 것입니다. 추측을 하게 됩니다.

그래서 반대로 생각하면 서명할 때 이 헤더만 추가하면 되는데 왜 삭제해야 할까요?
그래서 코드를 수정하고 서명 URL 메서드에 Content-Type 매개변수를 추가하고 oss를 업로드할 때 동일한 매개변수를 사용합니다.

마지막으로 Alibaba Cloud에 대해 다시 이야기해 보겠습니다.

  • multipart/form-data업로드에 표준 업로드 형식을 사용하지 않는 이유는 무엇입니까 ?
    이것은 표준 파일 업로드 형식이며 최소한 호환되어야 합니다.
  • 데모 코드의 자바스크립트 버전을 제공하지 않는 이유는 무엇입니까?
    알리바바 클라우드는 서명 업로드 솔루션을 제공합니다 조금 깊이 생각해보면 웹이 보편화된 순간에 프론트엔드와 백엔드가 분리되어야 한다는 것을 알게 될 것입니다. 데모를 제공해야 합니다.
  • 서명 오류가 디버깅 기능을 제공합니까?
    예를 들어 url에 debug=true를 추가하고 반환 값에 서명 앞에 데이터를 추가하면 서명 알고리즘이 공개되며 반환 값은 키 키를 추가할 필요가 없습니다.

추천

출처blog.csdn.net/youbl/article/details/131454580