作者:張醫博
背景:
經常遇到有跨域的問題,老生長談,卻又屢禁不止,談到跨域我們就了解下它是什麼?
一句話簡單說明
一個資源請求一個其它域名的資源時會發起一個跨域 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

通過報錯可以看出來 發起跨區域請求的源頭 是 bo3.ai.com 加載了 www.ai.com 網站的資源,這兩個域名都在 阿裡雲 cdn 加速。既然找到了請求目的 www.ai.com,那麼直接檢查下目的域名上是否新增了跨域頭。這種情況基本都是目的域名沒有加上允許的跨域頭導緻。
類型二:cdn-oss
這個問題比較特殊,拆分兩部分說明。出現這種情況,通過截圖我們發現使用者有兩種請求,分别是 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 的跨域規則已經生效。