-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 2f8789f
Showing
6 changed files
with
325 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
.idea | ||
composer.lock | ||
vendor |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
# ThinkCors | ||
|
||
ThinkPHP跨域扩展 | ||
|
||
## 安装 | ||
|
||
``` | ||
composer require topthink/think-cors | ||
``` | ||
|
||
## 配置 | ||
|
||
配置文件位于 `config/cors.php` | ||
|
||
``` | ||
[ | ||
'paths' => ['api/*'], | ||
... | ||
] | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
{ | ||
"name": "topthink/think-cors", | ||
"description": "The Cors Library For ThinkPHP", | ||
"license": "Apache-2.0", | ||
"authors": [ | ||
{ | ||
"name": "yunwuxin", | ||
"email": "[email protected]" | ||
} | ||
], | ||
"require": { | ||
"topthink/framework": "^6.0|^8.0" | ||
}, | ||
"autoload": { | ||
"psr-4": { | ||
"think\\cors\\": "src" | ||
} | ||
}, | ||
"extra": { | ||
"think": { | ||
"services": [ | ||
"think\\cors\\Service" | ||
], | ||
"config": { | ||
"cors": "src/config.php" | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,248 @@ | ||
<?php | ||
|
||
namespace think\cors; | ||
|
||
use Closure; | ||
use think\Config; | ||
use think\Request; | ||
use think\Response; | ||
|
||
class HandleCors | ||
{ | ||
/** @var string[] */ | ||
protected array $paths = []; | ||
/** @var string[] */ | ||
protected array $allowedOrigins = []; | ||
/** @var string[] */ | ||
protected array $allowedOriginsPatterns = []; | ||
/** @var string[] */ | ||
protected array $allowedMethods = []; | ||
/** @var string[] */ | ||
protected array $allowedHeaders = []; | ||
/** @var string[] */ | ||
private array $exposedHeaders = []; | ||
protected bool $supportsCredentials = false; | ||
protected ?int $maxAge = 0; | ||
|
||
protected bool $allowAllOrigins = false; | ||
protected bool $allowAllMethods = false; | ||
protected bool $allowAllHeaders = false; | ||
|
||
public function __construct(Config $config) | ||
{ | ||
$options = $config->get('cors', []); | ||
|
||
$this->paths = $options['paths'] ?? $this->paths; | ||
$this->allowedOrigins = $options['allowed_origins'] ?? $this->allowedOrigins; | ||
$this->allowedOriginsPatterns = $options['allowed_origins_patterns'] ?? $this->allowedOriginsPatterns; | ||
$this->allowedMethods = $options['allowed_methods'] ?? $this->allowedMethods; | ||
$this->allowedHeaders = $options['allowed_headers'] ?? $this->allowedHeaders; | ||
$this->supportsCredentials = $options['supports_credentials'] ?? $this->supportsCredentials; | ||
|
||
$maxAge = $this->maxAge; | ||
if (array_key_exists('max_age', $options)) { | ||
$maxAge = $options['max_age']; | ||
} | ||
$this->maxAge = $maxAge === null ? null : (int) $maxAge; | ||
|
||
$exposedHeaders = $options['exposed_headers'] ?? $this->exposedHeaders; | ||
$this->exposedHeaders = $exposedHeaders === false ? [] : $exposedHeaders; | ||
|
||
// Normalize case | ||
$this->allowedHeaders = array_map('strtolower', $this->allowedHeaders); | ||
$this->allowedMethods = array_map('strtoupper', $this->allowedMethods); | ||
|
||
// Normalize ['*'] to true | ||
$this->allowAllOrigins = in_array('*', $this->allowedOrigins); | ||
$this->allowAllHeaders = in_array('*', $this->allowedHeaders); | ||
$this->allowAllMethods = in_array('*', $this->allowedMethods); | ||
|
||
// Transform wildcard pattern | ||
if (!$this->allowAllOrigins) { | ||
foreach ($this->allowedOrigins as $origin) { | ||
if (strpos($origin, '*') !== false) { | ||
$this->allowedOriginsPatterns[] = $this->convertWildcardToPattern($origin); | ||
} | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* @param Request $request | ||
* @param Closure $next | ||
* @return Response | ||
*/ | ||
public function handle($request, Closure $next) | ||
{ | ||
if (!$this->hasMatchingPath($request)) { | ||
return $next($request); | ||
} | ||
|
||
if ($this->isPreflightRequest($request)) { | ||
return $this->handlePreflightRequest($request); | ||
} | ||
|
||
/** @var Response $response */ | ||
$response = $next($request); | ||
|
||
return $this->addPreflightRequestHeaders($response, $request); | ||
} | ||
|
||
protected function addPreflightRequestHeaders(Response $response, Request $request): Response | ||
{ | ||
$this->configureAllowedOrigin($response, $request); | ||
|
||
if ($response->getHeader('Access-Control-Allow-Origin')) { | ||
$this->configureAllowCredentials($response, $request); | ||
$this->configureAllowedMethods($response, $request); | ||
$this->configureAllowedHeaders($response, $request); | ||
$this->configureMaxAge($response, $request); | ||
} | ||
|
||
return $response; | ||
} | ||
|
||
protected function configureAllowedOrigin(Response $response, Request $request): void | ||
{ | ||
if ($this->allowAllOrigins === true && !$this->supportsCredentials) { | ||
$response->header(['Access-Control-Allow-Origin' => '*']); | ||
} elseif ($this->isSingleOriginAllowed()) { | ||
$response->header(['Access-Control-Allow-Origin' => array_values($this->allowedOrigins)[0]]); | ||
} else { | ||
if ($this->isCorsRequest($request) && $this->isOriginAllowed($request)) { | ||
$response->header(['Access-Control-Allow-Origin' => (string) $request->header('Origin')]); | ||
} | ||
} | ||
} | ||
|
||
protected function configureAllowCredentials(Response $response, Request $request): void | ||
{ | ||
if ($this->supportsCredentials) { | ||
$response->header(['Access-Control-Allow-Credentials' => 'true']); | ||
} | ||
} | ||
|
||
protected function configureAllowedMethods(Response $response, Request $request): void | ||
{ | ||
if ($this->allowAllMethods === true) { | ||
$allowMethods = strtoupper((string) $request->header('Access-Control-Request-Method')); | ||
} else { | ||
$allowMethods = implode(', ', $this->allowedMethods); | ||
} | ||
|
||
$response->header(['Access-Control-Allow-Methods' => $allowMethods]); | ||
} | ||
|
||
protected function configureAllowedHeaders(Response $response, Request $request): void | ||
{ | ||
if ($this->allowAllHeaders === true) { | ||
$allowHeaders = (string) $request->header('Access-Control-Request-Headers'); | ||
} else { | ||
$allowHeaders = implode(', ', $this->allowedHeaders); | ||
} | ||
$response->header(['Access-Control-Allow-Headers' => $allowHeaders]); | ||
} | ||
|
||
protected function configureMaxAge(Response $response, Request $request): void | ||
{ | ||
if ($this->maxAge !== null) { | ||
$response->header(['Access-Control-Max-Age' => (string) $this->maxAge]); | ||
} | ||
} | ||
|
||
protected function handlePreflightRequest(Request $request) | ||
{ | ||
$response = response('', 204); | ||
|
||
return $this->addPreflightRequestHeaders($response, $request); | ||
} | ||
|
||
protected function isCorsRequest(Request $request) | ||
{ | ||
return !!$request->header('Origin'); | ||
} | ||
|
||
protected function isPreflightRequest(Request $request) | ||
{ | ||
return $request->method() === 'OPTIONS' && $request->header('Access-Control-Request-Method'); | ||
} | ||
|
||
protected function isSingleOriginAllowed(): bool | ||
{ | ||
if ($this->allowAllOrigins === true || count($this->allowedOriginsPatterns) > 0) { | ||
return false; | ||
} | ||
|
||
return count($this->allowedOrigins) === 1; | ||
} | ||
|
||
protected function isOriginAllowed(Request $request): bool | ||
{ | ||
if ($this->allowAllOrigins === true) { | ||
return true; | ||
} | ||
|
||
$origin = (string) $request->header('Origin'); | ||
|
||
if (in_array($origin, $this->allowedOrigins)) { | ||
return true; | ||
} | ||
|
||
foreach ($this->allowedOriginsPatterns as $pattern) { | ||
if (preg_match($pattern, $origin)) { | ||
return true; | ||
} | ||
} | ||
|
||
return false; | ||
} | ||
|
||
protected function hasMatchingPath(Request $request) | ||
{ | ||
$url = $request->baseUrl(); | ||
$url = trim($url, '/'); | ||
if ($url === '') { | ||
$url = '/'; | ||
} | ||
|
||
$paths = $this->getPathsByHost($request->host(true)); | ||
|
||
foreach ($paths as $path) { | ||
if ($path !== '/') { | ||
$path = trim($path, '/'); | ||
} | ||
|
||
if ($path === $url) { | ||
return true; | ||
} | ||
|
||
$pattern = $this->convertWildcardToPattern($path); | ||
|
||
if (preg_match($pattern, $url) === 1) { | ||
return true; | ||
} | ||
} | ||
|
||
return false; | ||
} | ||
|
||
protected function getPathsByHost($host) | ||
{ | ||
$paths = $this->paths; | ||
|
||
if (isset($paths[$host])) { | ||
return $paths[$host]; | ||
} | ||
|
||
return array_filter($paths, function ($path) { | ||
return is_string($path); | ||
}); | ||
} | ||
|
||
protected function convertWildcardToPattern($pattern) | ||
{ | ||
$pattern = preg_quote($pattern, '#'); | ||
$pattern = str_replace('\*', '.*', $pattern); | ||
return '#^' . $pattern . '\z#u'; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
<?php | ||
|
||
namespace think\cors; | ||
|
||
class Service extends \think\Service | ||
{ | ||
public function boot(): void | ||
{ | ||
$this->app->event->listen('HttpRun', function () { | ||
$this->app->middleware->add(HandleCors::class); | ||
}); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
<?php | ||
|
||
return [ | ||
'paths' => [], | ||
'allowed_origins' => ['*'], | ||
'allowed_origins_patterns' => [], | ||
'allowed_methods' => ['*'], | ||
'allowed_headers' => ['*'], | ||
'exposed_headers' => [], | ||
'max_age' => 0, | ||
'supports_credentials' => false, | ||
]; |