背景:
经常遇到有跨域的问题,老生长谈,却又屡禁不止,谈到跨域我们就了解下它是什么?
一句话简单说明
一个资源请求一个其它域名的资源时会发起一个跨域 HTTP 请求 (cross-origin HTTP request)。比如说,域名A(http://domaina.example) 的某 Web 应用通过<img>标签引入了域名 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 的要求
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 的跨域规则已经生效。