├── .gitignore ├── src ├── Service.php ├── config.php └── HandleCors.php ├── composer.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | composer.lock 3 | vendor 4 | -------------------------------------------------------------------------------- /src/Service.php: -------------------------------------------------------------------------------- 1 | app->event->listen('HttpRun', function () { 10 | $this->app->middleware->add(HandleCors::class); 11 | }); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/config.php: -------------------------------------------------------------------------------- 1 | ['*'], 5 | 'allowed_origins' => ['*'], 6 | 'allowed_origins_patterns' => [], 7 | 'allowed_methods' => ['*'], 8 | 'allowed_headers' => ['*'], 9 | 'exposed_headers' => [], 10 | 'max_age' => 0, 11 | 'supports_credentials' => false, 12 | ]; 13 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "topthink/think-cors", 3 | "description": "The Cors Library For ThinkPHP", 4 | "license": "Apache-2.0", 5 | "authors": [ 6 | { 7 | "name": "yunwuxin", 8 | "email": "448901948@qq.com" 9 | } 10 | ], 11 | "require": { 12 | "topthink/framework": "^6.0|^8.0" 13 | }, 14 | "autoload": { 15 | "psr-4": { 16 | "think\\cors\\": "src" 17 | } 18 | }, 19 | "extra": { 20 | "think": { 21 | "services": [ 22 | "think\\cors\\Service" 23 | ], 24 | "config": { 25 | "cors": "src/config.php" 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ThinkCors 2 | 3 | ThinkPHP跨域扩展 4 | 5 | ## 安装 6 | 7 | ``` 8 | composer require topthink/think-cors 9 | ``` 10 | 11 | ## 配置 12 | 13 | 配置文件位于 `config/cors.php` 14 | 15 | ``` 16 | [ 17 | 'paths' => ['api/*'], 18 | ... 19 | ] 20 | ``` 21 | 22 | ### paths 配置示例 23 | 24 | 允许 api 目录下的跨域请求,`*` 代表通配符。 25 | 26 | ```php 27 | [ 28 | 'paths' => ['api/*'] 29 | ] 30 | ``` 31 | 32 | 当项目有多个域名时,支持为不同域名配置不同的目录。 33 | 34 | ```php 35 | [ 36 | 'paths' => [ 37 | 'www.thinkphp.cn' => ['api/*'], 38 | 'doc.thinkphp.cn' => ['user/*', 'article/*'], 39 | ] 40 | ] 41 | ``` 42 | 43 | ### allowed_origins 配置示例 44 | 45 | 当配置中有 `*` 时,代表不限制来源域。 46 | 47 | ```php 48 | [ 49 | 'allowed_origins' => ['*'], 50 | ] 51 | ``` 52 | 53 | 当我们需要限制来源域时,可以这么写。 54 | 55 | ```php 56 | [ 57 | 'allowed_origins' => ['www.thinkphp.cn', 'm.thinkphp.cn'], 58 | ] 59 | ``` 60 | 61 | ### allowed_origins_patterns 配置示例 62 | 63 | 除了固定来源域,有时候我们还想要允许不固定但有规则的来源域,那么可以通过正则来实现。例如这里我们允许 `thinkphp.cn` 的所有二级域。 64 | 65 | ```php 66 | [ 67 | 'allowed_origins_patterns' => ['#.*\.thinkphp\.cn#'], 68 | ] 69 | ``` 70 | 71 | ### allowed_methods 配置示例 72 | 73 | 当配置中有 `*` 时,代表不限制来源请求方式。 74 | 75 | ```php 76 | [ 77 | 'allowed_methods' => ['*'], 78 | ] 79 | ``` 80 | 81 | 当然我们也可以限制只允许 `GET` 和 `POST` 的跨域请求。 82 | 83 | ```php 84 | [ 85 | 'allowed_methods' => ['GET', 'POST'], 86 | ] 87 | ``` 88 | 89 | ### allowed_headers 配置示例 90 | 91 | 当配置中有 `*` 时,代表不限制请求头。 92 | 93 | ```php 94 | [ 95 | 'allowed_headers' => ['*'], 96 | ] 97 | ``` 98 | 99 | 当然我们也可以只允许跨域请求只传递给我们部分请求头。 100 | 101 | ```php 102 | [ 103 | 'allowed_headers' => ['X-Custom-Header', 'Upgrade-Insecure-Requests'], 104 | ] 105 | ``` 106 | 107 | ### max_age 配置示例 108 | 109 | 跨域预检结果是有缓存的,如果值为 -1,表示禁用缓存,则每次请求前都需要使用 `OPTIONS` 预检请求。如果想减少 `OPTIONS` 预检请求,我们可以把缓存有效期设置长些。 110 | 列如这里,我们把有效期设置为 2 小时(7200 秒): 111 | 112 | ```php 113 | [ 114 | 'max_age' => 7200, 115 | ] 116 | ``` 117 | 118 | ### supports_credentials 配置示例 119 | 120 | `Credentials` 可以是 `cookies`、`authorization headers` 或 `TLS client certificates`。当接口需要这些信息时,开启该项配置后,相关请求将会携带 `Credentials` 信息(如果有的话)。 121 | 122 | ```php 123 | [ 124 | 'supports_credentials' => true, 125 | ] 126 | ``` 127 | 128 | -------------------------------------------------------------------------------- /src/HandleCors.php: -------------------------------------------------------------------------------- 1 | get('cors', []); 34 | 35 | $this->paths = $options['paths'] ?? $this->paths; 36 | $this->allowedOrigins = $options['allowed_origins'] ?? $this->allowedOrigins; 37 | $this->allowedOriginsPatterns = $options['allowed_origins_patterns'] ?? $this->allowedOriginsPatterns; 38 | $this->allowedMethods = $options['allowed_methods'] ?? $this->allowedMethods; 39 | $this->allowedHeaders = $options['allowed_headers'] ?? $this->allowedHeaders; 40 | $this->exposedHeaders = $options['exposed_headers'] ?? $this->exposedHeaders; 41 | $this->supportsCredentials = $options['supports_credentials'] ?? $this->supportsCredentials; 42 | 43 | $maxAge = $this->maxAge; 44 | if (array_key_exists('max_age', $options)) { 45 | $maxAge = $options['max_age']; 46 | } 47 | $this->maxAge = $maxAge === null ? null : (int) $maxAge; 48 | 49 | // Normalize case 50 | $this->allowedHeaders = array_map('strtolower', $this->allowedHeaders); 51 | $this->allowedMethods = array_map('strtoupper', $this->allowedMethods); 52 | 53 | // Normalize ['*'] to true 54 | $this->allowAllOrigins = in_array('*', $this->allowedOrigins); 55 | $this->allowAllHeaders = in_array('*', $this->allowedHeaders); 56 | $this->allowAllMethods = in_array('*', $this->allowedMethods); 57 | 58 | // Transform wildcard pattern 59 | if (!$this->allowAllOrigins) { 60 | foreach ($this->allowedOrigins as $origin) { 61 | if (strpos($origin, '*') !== false) { 62 | $this->allowedOriginsPatterns[] = $this->convertWildcardToPattern($origin); 63 | } 64 | } 65 | } 66 | } 67 | 68 | /** 69 | * @param Request $request 70 | * @param Closure $next 71 | * @return Response 72 | */ 73 | public function handle($request, Closure $next) 74 | { 75 | if (!$this->hasMatchingPath($request)) { 76 | return $next($request); 77 | } 78 | 79 | if ($this->isPreflightRequest($request)) { 80 | return $this->handlePreflightRequest($request); 81 | } 82 | 83 | /** @var Response $response */ 84 | $response = $next($request); 85 | 86 | return $this->addPreflightRequestHeaders($response, $request); 87 | } 88 | 89 | protected function addPreflightRequestHeaders(Response $response, Request $request): Response 90 | { 91 | $this->configureAllowedOrigin($response, $request); 92 | 93 | if ($response->getHeader('Access-Control-Allow-Origin')) { 94 | $this->configureAllowCredentials($response, $request); 95 | $this->configureAllowedMethods($response, $request); 96 | $this->configureAllowedHeaders($response, $request); 97 | $this->configureExposedHeaders($response, $request); 98 | $this->configureMaxAge($response, $request); 99 | } 100 | 101 | return $response; 102 | } 103 | 104 | protected function configureAllowedOrigin(Response $response, Request $request): void 105 | { 106 | if ($this->allowAllOrigins === true && !$this->supportsCredentials) { 107 | $response->header(['Access-Control-Allow-Origin' => '*']); 108 | } elseif ($this->isSingleOriginAllowed()) { 109 | $response->header(['Access-Control-Allow-Origin' => array_values($this->allowedOrigins)[0]]); 110 | } else { 111 | if ($this->isCorsRequest($request) && $this->isOriginAllowed($request)) { 112 | $response->header(['Access-Control-Allow-Origin' => (string) $request->header('Origin')]); 113 | } 114 | } 115 | } 116 | 117 | protected function configureAllowCredentials(Response $response, Request $request): void 118 | { 119 | if ($this->supportsCredentials) { 120 | $response->header(['Access-Control-Allow-Credentials' => 'true']); 121 | } 122 | } 123 | 124 | protected function configureAllowedMethods(Response $response, Request $request): void 125 | { 126 | if ($this->allowAllMethods === true) { 127 | $allowMethods = strtoupper((string) $request->header('Access-Control-Request-Method')); 128 | } else { 129 | $allowMethods = implode(', ', $this->allowedMethods); 130 | } 131 | 132 | $response->header(['Access-Control-Allow-Methods' => $allowMethods]); 133 | } 134 | 135 | protected function configureAllowedHeaders(Response $response, Request $request): void 136 | { 137 | if ($this->allowAllHeaders === true) { 138 | $allowHeaders = (string) $request->header('Access-Control-Request-Headers'); 139 | } else { 140 | $allowHeaders = implode(', ', $this->allowedHeaders); 141 | } 142 | $response->header(['Access-Control-Allow-Headers' => $allowHeaders]); 143 | } 144 | 145 | protected function configureExposedHeaders(Response $response, Request $request): void 146 | { 147 | if ($this->exposedHeaders) { 148 | $exposeHeaders = implode(', ', $this->exposedHeaders); 149 | $response->header(['Access-Control-Expose-Headers' => $exposeHeaders]); 150 | } 151 | } 152 | 153 | protected function configureMaxAge(Response $response, Request $request): void 154 | { 155 | if ($this->maxAge !== null) { 156 | $response->header(['Access-Control-Max-Age' => (string) $this->maxAge]); 157 | } 158 | } 159 | 160 | protected function handlePreflightRequest(Request $request) 161 | { 162 | $response = response('', 204); 163 | 164 | return $this->addPreflightRequestHeaders($response, $request); 165 | } 166 | 167 | protected function isCorsRequest(Request $request) 168 | { 169 | return !!$request->header('Origin'); 170 | } 171 | 172 | protected function isPreflightRequest(Request $request) 173 | { 174 | return $request->method() === 'OPTIONS' && $request->header('Access-Control-Request-Method'); 175 | } 176 | 177 | protected function isSingleOriginAllowed(): bool 178 | { 179 | if ($this->allowAllOrigins === true || count($this->allowedOriginsPatterns) > 0) { 180 | return false; 181 | } 182 | 183 | return count($this->allowedOrigins) === 1; 184 | } 185 | 186 | protected function isOriginAllowed(Request $request): bool 187 | { 188 | if ($this->allowAllOrigins === true) { 189 | return true; 190 | } 191 | 192 | $origin = (string) $request->header('Origin'); 193 | 194 | if (in_array($origin, $this->allowedOrigins)) { 195 | return true; 196 | } 197 | 198 | foreach ($this->allowedOriginsPatterns as $pattern) { 199 | if (preg_match($pattern, $origin)) { 200 | return true; 201 | } 202 | } 203 | 204 | return false; 205 | } 206 | 207 | protected function hasMatchingPath(Request $request) 208 | { 209 | $url = $request->pathInfo(); 210 | $url = trim($url, '/'); 211 | if ($url === '') { 212 | $url = '/'; 213 | } 214 | 215 | $paths = $this->getPathsByHost($request->host(true)); 216 | 217 | foreach ($paths as $path) { 218 | if ($path !== '/') { 219 | $path = trim($path, '/'); 220 | } 221 | 222 | if ($path === $url) { 223 | return true; 224 | } 225 | 226 | $pattern = $this->convertWildcardToPattern($path); 227 | 228 | if (preg_match($pattern, $url) === 1) { 229 | return true; 230 | } 231 | } 232 | 233 | return false; 234 | } 235 | 236 | protected function getPathsByHost($host) 237 | { 238 | $paths = $this->paths; 239 | 240 | if (isset($paths[$host])) { 241 | return $paths[$host]; 242 | } 243 | 244 | return array_filter($paths, function ($path) { 245 | return is_string($path); 246 | }); 247 | } 248 | 249 | protected function convertWildcardToPattern($pattern) 250 | { 251 | $pattern = preg_quote($pattern, '#'); 252 | $pattern = str_replace('\*', '.*', $pattern); 253 | return '#^' . $pattern . '\z#u'; 254 | } 255 | } 256 | --------------------------------------------------------------------------------