koa-cors 源码及基本原理解析

发布时间 2023-04-30 12:38:03作者: TangTaue

cors: 跨域资源共享(Cross-Origin Resource Sharing)是一种机制,用来允许不同源服务器上的指定资源可以被特定的Web应用访问。

在koa项目中使用cors中间件:

eg:

 1 var koa = require('koa');
 2 var route = require('koa-route');
 3 var cors = require('koa-cors');
 4 var app = koa();
 5 
 6 app.use(cors());
 7 
 8 app.use(route.get('/', function() {
 9   this.body = { msg: 'Hello World!' };
10 }));
11 
12 app.listen(3000);

koa-cors 源码解析:

  1 'use strict';
  2 
  3 const vary = require('vary');
  4 
  5 /**
  6  * CORS middleware
  7  *
  8  * @param {Object} [options]
  9  *  - {String|Function(ctx)} origin `Access-Control-Allow-Origin`, default is request Origin header
 10  *  - {String|Array} allowMethods `Access-Control-Allow-Methods`, default is 'GET,HEAD,PUT,POST,DELETE,PATCH'
 11  *  - {String|Array} exposeHeaders `Access-Control-Expose-Headers`
 12  *  - {String|Array} allowHeaders `Access-Control-Allow-Headers`
 13  *  - {String|Number} maxAge `Access-Control-Max-Age` in seconds
 14  *  - {Boolean|Function(ctx)} credentials `Access-Control-Allow-Credentials`
 15  *  - {Boolean} keepHeadersOnError Add set headers to `err.header` if an error is thrown
 16  *  - {Boolean} secureContext `Cross-Origin-Opener-Policy` & `Cross-Origin-Embedder-Policy` headers.', default is false
 17  *    @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer/Planned_changes
 18  *  - {Boolean} privateNetworkAccess handle `Access-Control-Request-Private-Network` request by return `Access-Control-Allow-Private-Network`, default to false
 19  *    @see https://wicg.github.io/private-network-access/
 20  * @return {Function} cors middleware
 21  * @public
 22  */
 23 module.exports = function (options) {
 24     // 这是默认配置 
 25     const defaults = {
 26         // Access-Control-Allow-Methods 必须字段,表示服务器支持的跨域HTTP方法、返回的是所有支持方法
 27         allowMethods: 'GET,HEAD,PUT,POST,DELETE,PATCH',
 28         secureContext: false,
 29     };
 30 
 31     // 覆盖默认配置 合并options
 32     options = {
 33         ...defaults,
 34         ...options,
 35     };
 36 
 37     // 组装options
 38     // CORS很多响应头value都是允许多个逗号分隔的字符串
 39     if (Array.isArray(options.exposeHeaders)) {
 40         options.exposeHeaders = options.exposeHeaders.join(',');
 41     }
 42 
 43     if (Array.isArray(options.allowMethods)) {
 44         options.allowMethods = options.allowMethods.join(',');
 45     }
 46 
 47     if (Array.isArray(options.allowHeaders)) {
 48         options.allowHeaders = options.allowHeaders.join(',');
 49     }
 50     // 设计预检请求的有效期
 51     if (options.maxAge) {
 52         options.maxAge = String(options.maxAge);
 53     }
 54 
 55     // keepHeadersOnError 默认设置为true
 56     options.keepHeadersOnError = options.keepHeadersOnError === undefined || !!options.keepHeadersOnError;
 57 
 58     // 返回中间件函数
 59     return async function cors(ctx, next) {
 60         // If the Origin header is not present terminate this set of steps.
 61         // The request is outside the scope of this specification.
 62 
 63         const requestOrigin = ctx.get('Origin');
 64 
 65         // Always set Vary header
 66         // https://github.com/rs/cors/issues/10
 67         ctx.vary('Origin');
 68 
 69         // 获取配置参数中的 origin,默认取 requestOrigin
 70         let origin;
 71         if (typeof options.origin === 'function') {
 72             origin = await options.origin(ctx);
 73             if (!origin) return await next();
 74         } else {
 75             origin = options.origin || requestOrigin;
 76         }
 77 
 78         // 处理不同场景下的credentials
 79         // Access-Control-Allow-Credentials:可选、布尔值、表示是否允许发送Cookie
 80         // Cookie默认不包含在CORS请求中,设为true表示服务器许可、
 81         // 如果不需要直接删除该字段即可
 82         let credentials;
 83         if (typeof options.credentials === 'function') {
 84             credentials = await options.credentials(ctx);
 85         } else {
 86             credentials = !!options.credentials;
 87         }
 88 
 89         if (credentials && origin === '*') {
 90             origin = requestOrigin;
 91         }
 92 
 93         const headersSet = {};
 94 
 95         function set(key, value) {
 96             ctx.set(key, value);
 97             headersSet[key] = value;
 98         }
 99 
100         // 非预检请求 简单请求
101         if (ctx.method !== 'OPTIONS') {
102             // 考虑配置 Access-Control-Allow-Origin,Access-Control-Allow-Credentials,Access-Control-Expose-Headers 三个响应头
103             // !  配置响应头
104             // CORS 之 简单请求、浏览器直接发出CORS请求,并且在浏览器请求头添加Origin字段
105             // 简单请求:
106             // 1、请求方法:HEAD、GET、POST
107             // 2、HTTP头信息字段限制,Accept、Accept-Language、Content-Language、Content-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain
108             // 设置接收来自origin域名的请求
109             set('Access-Control-Allow-Origin', origin);
110 
111             if (credentials === true) {
112                 // 设置允许客户端发送Cookies
113                 set('Access-Control-Allow-Credentials', 'true');
114             }
115 
116             // CORS请求,XHR对象的getResponseHeader方法只能获取6个基本字段
117             // Cache-Control Content-Language Content-Type Expires Last-Modified Pragma
118             // 因此如果你需要获取其它字段、则需要在Access-Control-Expose-Headers指定 
119             if (options.exposeHeaders) {
120                 set('Access-Control-Expose-Headers', options.exposeHeaders);
121             }
122 
123             if (options.secureContext) {
124                 set('Cross-Origin-Opener-Policy', 'same-origin');
125                 set('Cross-Origin-Embedder-Policy', 'require-corp');
126             }
127 
128             if (!options.keepHeadersOnError) {
129                 return await next();
130             }
131             // 异常情况处理
132             try {
133                 return await next();
134             } catch (err) {
135                 const errHeadersSet = err.headers || {};
136                 const varyWithOrigin = vary.append(errHeadersSet.vary || errHeadersSet.Vary || '', 'Origin');
137                 delete errHeadersSet.Vary;
138 
139                 err.headers = {
140                     ...errHeadersSet,
141                     ...headersSet,
142                     ...{
143                         vary: varyWithOrigin
144                     },
145                 };
146                 throw err;
147             }
148         } else {
149             // 预检请求:非简单请求即为预检请求、比如PUT、DELETE方法或者Content-Type: application/json。
150             // 如果是一个非简单请求的CORS请求,在正式通信之前,会增加一次HTTP查询请求,称为预检请求
151             // 预检请求使用的HTTP方法是OPTION,所以你知道上面为什么用OPTION作为判断了
152 
153             // 预检请求目的:浏览器发起请求,询问服务器,当前网页是否在服务器允许的origin名单内
154             // 以及想知道允许使用哪些HTTP Method、头信息字段等
155             // 只有得到正确答复、才会发出正式的XHR请求
156 
157             // 预检请求一般包括的字段有
158             // Origin: http://api.bob.com 必须会带上
159             // Access-Control-Request-Method: PUT // 必须,这里PUT指的是浏览器预检请求后的XHR请求方法
160             // Access-Control-Request-Headers: X-Custom-Header // 可选
161 
162             // 服务器接收预检请求后,会检测Origin、Access-Control-Allow-Methods、以及Access-Control-Request-Headers      
163             // 如果请求头不存在Access-Control-Request-Method头、或者解析失败、流程直接终止
164 
165             if (!ctx.get('Access-Control-Request-Method')) {
166                 // this not preflight request, ignore it
167                 return await next();
168             }
169 
170             // 检测正确,允许跨源请求,作出响应
171             // 设置返回请求域白名单
172             ctx.set('Access-Control-Allow-Origin', origin);
173 
174             // 设置允许浏览器CORS请求发送Cookies
175             if (credentials === true) {
176                 ctx.set('Access-Control-Allow-Credentials', 'true');
177             }
178 
179             if (options.maxAge) {
180                 ctx.set('Access-Control-Max-Age', options.maxAge);
181             }
182             // 是否是私有网络访问 只能 访问内网 或 局域网
183             if (options.privateNetworkAccess && ctx.get('Access-Control-Request-Private-Network')) {
184                 ctx.set('Access-Control-Allow-Private-Network', 'true');
185             }
186 
187             // 设置服务器支持的所有跨域方法
188             if (options.allowMethods) {
189                 ctx.set('Access-Control-Allow-Methods', options.allowMethods);
190             }
191 
192             if (options.secureContext) {
193                 set('Cross-Origin-Opener-Policy', 'same-origin');
194                 set('Cross-Origin-Embedder-Policy', 'require-corp');
195             }
196 
197             // 设置服务器支持的所有头信息字段
198             let allowHeaders = options.allowHeaders;
199             if (!allowHeaders) {
200                 // 如果没有自定义、从请求中获取
201                 allowHeaders = ctx.get('Access-Control-Request-Headers');
202             }
203             // 自定义拓展请求头
204             if (allowHeaders) {
205                 ctx.set('Access-Control-Allow-Headers', allowHeaders);
206             }
207             // 204 服务器处理请求、但是没有返回内容
208             ctx.status = 204;
209         }
210     };
211 };