開發與維運

訪問 cdn、oss 跨域問題

作者:張醫博

背景:

經常遇到有跨域的問題,老生長談,卻又屢禁不止,談到跨域我們就瞭解下它是什麼?

一句話簡單說明

一個資源請求一個其它域名的資源時會發起一個跨域 HTTP 請求 (cross-origin HTTP request)。比如說,域名A(http://domaina.example) 的某 Web 應用通過標籤引入了域名 B(http://domainb.foo) 的某圖片資源(http://domainb.foo/image.jpg),域名 A 的 Web 應用就會導致瀏覽器發起一個跨域 HTTP 請求

http://www.123.com/index.html 調用 http://www.123.com/server.php (非跨域)

http://www.123.com/index.html 調用 http://www.456.com/server.php (主域名不同:123/456,跨域)

http://abc.123.com/index.html 調用 http://def.123.com/server.php (子域名不同:abc/def,跨域)

http://www.123.com:8080/index.html 調用 http://www.123.com:8081/server.php (端口不同:8080/8081,跨域)

http://www.123.com/index.html 調用 https://www.123.com/server.php (協議不同:http/https,跨域)

請注意:localhost和127.0.0.1雖然都指向本機,但也屬於跨域。

跨域請求標識

origin ,當瀏覽器識別出 client 發起的請求需要轉到另外一個域名上處理是,會在請求的 request header 中增加一個 origin 標識,如下我用 curl 測試了一個域名

curl -voa http://mo-im.oss-cn-beijing.aliyuncs.com/stu_avatar/010/personal.jpg -H "Origin:www.mobby.cn"
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0*   Trying 59.110.190.173...
* TCP_NODELAY set
* Connected to mo-im.oss-cn-beijing.aliyuncs.com (59.110.190.173) port 80 (#0)
> GET /stu_avatar/010/personal.jpg HTTP/1.1
> Host: mo-im.oss-cn-beijing.aliyuncs.com
> User-Agent: curl/7.54.0
> Accept: */*
> Origin:www.mo.cn
> 
< HTTP/1.1 200 OK
< Server: AliyunOSS
< Date: Sun, 09 Sep 2018 12:30:28 GMT
< Content-Type: image/jpeg
< Content-Length: 8407
< Connection: keep-alive
< x-oss-request-id: 
< Access-Control-Allow-Origin: www.mobby.cn
< Access-Control-Allow-Credentials: true
< Access-Control-Allow-Methods: GET, POST, HEAD
< Access-Control-Max-Age: 0
< Accept-Ranges: bytes

可以看到擋我發起 origin 的請求頭後,如果目標的網頁服務允許來源的域名訪問,就會在響應的 respond 頭上帶上跨域的響應頭。(以下 header 目標域名如果設置了才會有響應)

< Access-Control-Allow-Origin: www.mobby.cn (允許的跨域來源,可以寫 *,或者絕對域名) 
< Access-Control-Allow-Headers: *(允許跨域時攜帶哪些 header )
< Access-Control-Allow-Methods: GET, POST, HEAD (允許哪些跨域請求方法,origin 是默認支持的)

跨域設置分類 cdn-cdn 、cdn-oss

類型一:cdn-cdn

image.png

通過報錯可以看出來 發起跨區域請求的源頭 是 bo3.ai.com 加載了 www.ai.com 網站的資源,這兩個域名都在 阿里雲 cdn 加速。既然找到了請求目的 www.ai.com,那麼直接檢查下目的域名上是否新增了跨域頭。這種情況基本都是目的域名沒有加上允許的跨域頭導致。

類型二:cdn-oss

image.png

這個問題比較特殊,拆分兩部分說明。出現這種情況,通過截圖我們發現用戶有兩種請求,分別是 get 和 post 兩種,由於 get 好測試,我們先說 get

1、出現跨域錯誤,首先就要檢查原是否添加了跨域頭,於是我們使用 curl 測試一下,如果最簡單的 get 測試成功返回跨域頭,說明目的 域名設置了跨域響應,如果測試失敗說明原沒有添加跨域響應。通過如下圖很顯然看到了目的添加了跨域頭。

curl -voa http://mo-im.oss-cn-beijing.aliyuncs.com/stu_avatar/010/personal.jpg -H "Origin:www.mo.cn"
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0*   Trying 59.110.190.173...
* TCP_NODELAY set
* Connected to mo-im.oss-cn-beijing.aliyuncs.com () port 80 (#0)
> GET /stu_avatar/010/personal.jpg HTTP/1.1
> Host: mo-im.oss-cn-beijing.aliyuncs.com
> User-Agent: curl/7.54.0
> Accept: */*
> Origin:www.mo.cn
> 
< HTTP/1.1 200 OK
< Server: AliyunOSS
< Date: Sun, 09 Sep 2018 12:30:28 GMT
< Content-Type: image/jpeg
< Content-Length: 8407
< Connection: keep-alive
< x-oss-request-id: 5B951264980F8FDB749972B3
< Access-Control-Allow-Origin: www.mo.cn
< Access-Control-Allow-Credentials: true
< Access-Control-Allow-Methods: GET, POST, HEAD
< Access-Control-Max-Age: 0

2、通過 get 測試發現 oss 是加了跨域頭的,但是為什麼 post 請求就返回 405 呢?沒有任何跨域頭呢?用戶反饋為什麼手動 curl 測試也是失敗

curl -v -X POST -d '{"user":"xxx"}' http://mo-im.oss-cn-beijing.aliyuncs.com/stu_avatar/010/personal.jpg -H "Origin:www.mo.cn"
Note: Unnecessary use of -X or --request, POST is already inferred.
*   Trying 59.110.190.173...
* TCP_NODELAY set
* Connected to mo-im.oss-cn-beijing.aliyuncs.com (59.110.190.173) port 80 (#0)
> POST /stu_avatar/010/personal.jpg HTTP/1.1
> Host: mo-im.oss-cn-beijing.aliyuncs.com
> User-Agent: curl/7.54.0
> Accept: */*
> Origin:www.mo.cn
> Content-Length: 14
> Content-Type: application/x-www-form-urlencoded
> 
* upload completely sent off: 14 out of 14 bytes
< HTTP/1.1 405 Method Not Allowed
< Server: AliyunOSS
< Date: Sun, 09 Sep 2018 13:06:28 GMT
< Content-Type: application/xml
< Content-Length: 337
< Connection: keep-alive
< x-oss-request-id: 
< Allow: GET DELETE HEAD PUT POST OPTIONS
< 
<?xml version="1.0" encoding="UTF-8"?>
<Error>
  <Code>MethodNotAllowed</Code>
  <Message>The specified method is not allowed against this resource.</Message>
  <RequestId></RequestId>
  <HostId>mo-im.oss-cn-beijing.aliyuncs.com</HostId>
  <Method>POST</Method>
  <ResourceType>Object</ResourceType>
</Error>

2.1、 首先我們看 oss 對於 post 的要求

https://help.aliyun.com/document_detail/31988.html?spm=a2c4g.11186623.6.1008.5bb84b4e1JEoA4

2.2 、發現無論是 curl 測試還是代碼的請求,都出先一個共性的問題。

  • 請求的格式不是 rfc 標準規定的 content-type:multipart/form-data
  • 請求頭不是內存不是 rfc 規定的表單域提交
  • 既然不是表單域,那麼要求的 filename 肯定也不知最後一個域。

2.3 、在以上情況下既然是非法的提交那麼 oss 肯定返回了 405 ,所以不會出觸發跨域的響應頭,必要要正確的返回 200 狀態才可以,那麼我們就用一段 Java 代碼演示下跨域的操作以及正確的響應頭抓包信息。

JAVA 跨域請求源碼

package com.alibaba.edas.carshop.OSS;

import javax.activation.MimetypesFileTypeMap;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

import org.apache.commons.codec.binary.Base64;

import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;

public class OSSPostFile {
    // The local file path to upload.
    private String localFilePath = "C:\\T\\1.txt";
    // OSS domain, such as http://oss-cn-hangzhou.aliyuncs.com
    private String endpoint = "http://oss-cn-beijing.aliyuncs.com";
    // Access key Id. Please get it from https://ak-console.aliyun.com
    private String accessKeyId = "";
    private String accessKeySecret = "";
    // The existing bucket name
    private String bucketName = "您自己的bucket名稱";
    // The key name for the file to upload.
    private String key = "1.txt";

    public void PostObject() throws Exception {
        // append the 'bucketname.' prior to the domain, such as
        // http://bucket1.oss-cn-hangzhou.aliyuncs.com.
        String urlStr = endpoint.replace("http://", "http://" + bucketName + ".");

        // form fields
        Map<String, String> formFields = new LinkedHashMap<String, String>();

        // key
        formFields.put("key", this.key);
        // Content-Disposition
        formFields.put("Content-Disposition", "attachment;filename=" + localFilePath);
        // OSSAccessKeyId
        formFields.put("OSSAccessKeyId", accessKeyId);
        // policy
        String policy = "{\"expiration\": \"2120-01-01T12:00:00.000Z\",\"conditions\": [[\"content-length-range\", 0, 104857600000]]}";
        String encodePolicy = new String(Base64.encodeBase64(policy.getBytes()));
        formFields.put("policy", encodePolicy);
        // Signature
        String signaturecom = computeSignature(accessKeySecret, encodePolicy);
        formFields.put("Signature", signaturecom);

        String ret = formUpload(urlStr, formFields, localFilePath);

        System.out.println("Post Object [" + this.key + "] to bucket [" + bucketName + "]");
        System.out.println("post reponse:" + ret);
    }

    private static String computeSignature(String accessKeySecret, String encodePolicy)
            throws UnsupportedEncodingException, NoSuchAlgorithmException, InvalidKeyException {
        // convert to UTF-8
        byte[] key = accessKeySecret.getBytes("UTF-8");
        byte[] data = encodePolicy.getBytes("UTF-8");

        // hmac-sha1
        Mac mac = Mac.getInstance("HmacSHA1");
        mac.init(new SecretKeySpec(key, "HmacSHA1"));
        byte[] sha = mac.doFinal(data);

        // base64
        return new String(Base64.encodeBase64(sha));
    }

    private static String formUpload(String urlStr, Map<String, String> formFields, String localFile) throws Exception {
        String res = "";
        HttpURLConnection conn = null;
        String boundary = "9431149156168";

        try {
            URL url = new URL(urlStr);
            conn = (HttpURLConnection) url.openConnection();
            conn.setConnectTimeout(5000);
            conn.setReadTimeout(30000);
            conn.setDoOutput(true);
            conn.setDoInput(true);
            conn.setRequestMethod("POST");
            conn.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows; U; Windows NT 6.1; zh-CN; rv:1.9.2.6)");
            conn.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary);
            OutputStream out = new DataOutputStream(conn.getOutputStream());

            // text
            if (formFields != null) {
                StringBuffer strBuf = new StringBuffer();
                Iterator<Entry<String, String>> iter = formFields.entrySet().iterator();
                int i = 0;

                while (iter.hasNext()) {
                    Entry<String, String> entry = iter.next();
                    String inputName = entry.getKey();
                    String inputValue = entry.getValue();

                    if (inputValue == null) {
                        continue;
                    }

                    if (i == 0) {
                        strBuf.append("--").append(boundary).append("\r\n");
                        strBuf.append("Content-Disposition: form-data; name=\"" + inputName + "\"\r\n\r\n");
                        strBuf.append(inputValue);
                    } else {
                        strBuf.append("\r\n").append("--").append(boundary).append("\r\n");
                        strBuf.append("Content-Disposition: form-data; name=\"" + inputName + "\"\r\n\r\n");
                        strBuf.append(inputValue);
                    }

                    i++;
                }
                out.write(strBuf.toString().getBytes());
            }

            StringBuffer strBuf1 = new StringBuffer();
            String callback = "{\"callbackUrl\":\"http://47.93.116.168/Revice.ashx\",\"callbackBody\":\"{\\\"bucket\\\"=${bucket},\\\"size\\\"=${size}}\"}";

            byte[] textByte = callback.getBytes("UTF-8");
            strBuf1.append("\r\n").append("--").append(boundary).append("\r\n");

            String callbackstr = new String(Base64.encodeBase64(textByte));
            strBuf1.append("Content-Disposition: form-data; name=\"callback\"\r\n\r\n" + callbackstr + "\r\n\r\n");
            out.write(strBuf1.toString().getBytes());

            // file
            File file = new File(localFile);
            String filename = file.getName();
            String contentType = new MimetypesFileTypeMap().getContentType(file);
            if (contentType == null || contentType.equals("")) {
                contentType = "application/octet-stream";
            }

            StringBuffer strBuf = new StringBuffer();
            strBuf.append("\r\n").append("--").append(boundary).append("\r\n");
            strBuf.append("Content-Disposition: form-data; name=\"file\"; " + "filename=\"" + filename + "\"\r\n");
            strBuf.append("Content-Type: " + contentType + "\r\n\r\n");
            out.write(strBuf.toString().getBytes());

            DataInputStream in = new DataInputStream(new FileInputStream(file));
            int bytes = 0;
            byte[] bufferOut = new byte[1024];
            while ((bytes = in.read(bufferOut)) != -1) {
                out.write(bufferOut, 0, bytes);
            }
            in.close();

            byte[] endData = ("\r\n--" + boundary + "--\r\n").getBytes();
            out.write(endData);
            out.flush();
            out.close();

            // Gets the file data
            strBuf = new StringBuffer();
            BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
            String line = null;
            while ((line = reader.readLine()) != null) {
                strBuf.append(line).append("\n");
            }
            res = strBuf.toString();
            reader.close();
            reader = null;
        } catch (Exception e) {
            System.err.println("Send post request exception: " + e.getLocalizedMessage());
            throw e;
        } finally {
            if (conn != null) {
                conn.disconnect();
                conn = null;
            }
        }

        return res;
    }

}

經過代碼測試以及抓包驗證在 正確的 post 的前提下,oss 的跨域規則已經生效。

image.png

Leave a Reply

Your email address will not be published. Required fields are marked *