laravel9 + vue3 后端配置跨域资源共享(CORS)
在 Linux 服务器上搭建 PHP 语言的web,使用laravel 9框架+vue3前端框架。以前后端分离的方式,进行搭建配置。需要提前申请ssl证书。
目录:/var/web/www ├── exampleApi(php 后端 laravel 框架) ├── exampleHome(vue 打包后文件,前端前台) ├── exampleAdmin(vue 打包后文件,前端后台)
- 前端前台域名www.example.com指向/var/web/www/exampleHome
- 前端后台域名admin.example.com指向/var/web/www/exampleAdmin
- 后端数据域名api.example.com指向/var/web/www/exampleApi/public
Laravel 跨资源共享
创建允许跨域中间件
允许跨域,是通过设置响应头(Response Header)来实现,所以设置响应头中间件。
cd /var/web/www/exampleApiphp artisan make:middleware AccessControlAllowOrigin
vim app/Http/Middelware/AccessControlAllowOrigin.php
无认证的中间件
当前端不携带 cookie,也不携带验证证书,也不进行 HTTP 认证(在 header 中不携带 token)的时候,Access-Control-Allow-Credentials设置为false,此时Access-Control-Allow-Origin,可以设置为*(允许全部域名),也可以指定请求来源的域名,限制访问来源。
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class AccessControlAllowOrigin
{
public function handle(Request $request, Closure $next)
{
$response = $next($request);
$response->header('Access-Control-Allow-Origin', '*');
$response->header('Access-Control-Allow-Credentials', 'false');
return $response;
}
}
需要认证的中间件
当前端发送的请求中,身份认证信息(Credentials)可以是cookies、authorization headers(比如 token)或TLS client certificates的时候,Access-Control-Allow-Credentials,必须为true,此时Access-Control-Allow-Origin必须指定请求来源的域名。
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class AccessControlAllowOrigin
{
public function handle(Request $request, Closure $next)
{
$response = $next($request);
$origin = $request->header('Origin') ?? $request->server('HTTP_ORIGIN');;
$allowOrigin = [
'https://www.example.com',
'https://admin.example.com',
'https://api.example.com',
'http://localhost:3000',
'http://localhost:4173',
'http://localhost:5173',
];
if(!empty($origin))
{
if (in_array($origin, $allowOrigin))
{
$response->header('Access-Control-Allow-Origin',$origin);
$response->header('Access-Control-Allow-Credentials', 'true');
$response->header('Access-Control-Expose-Headers', 'Origin, X-Requested-With, Authorization');
}
else{
return response('Unauthorized.', 401);
}
}
return $response;
}
}
- $allowOrigin,是允许跨域的域名列表。
- Authorization,是后端插件 passport 认证 token 的方式。
预检请求中间件
Laravel 自带的默认处理OPTIONS方式,适合无身份认证的请求。配置文件:config/cors.php,插件在app/Http/Kernel.php中启用,\Illuminate\Http\Middleware\HandleCors::class。需要把注销掉,不启用。
protected $middleware = [
// \App\Http\Middleware\TrustHosts::class,
\App\Http\Middleware\TrustProxies::class,
//\Illuminate\Http\Middleware\HandleCors::class,
\App\Http\Middleware\PreventRequestsDuringMaintenance::class,
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
\App\Http\Middleware\TrimStrings::class,
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
];
Laravel 处理有身份认证的OPTIONS方式,有缺陷。如果请求的 URL 不存在相关的其它方式(如 GET 或 POST)的请求,则会返回404 NOT FOUND的错误。如果存在相同 URL 的请求,会返回一个状态码为 200 的成功响应,但没有任何额外内容。这个OPTIONS请求不会进到此api.php路由文件的生命周期内,至少该 GET 请求所在路由文件 api 所绑定的中间件是没有进入的。
查看源码,在文件vendor/laravel/framework/src/Illuminate/Routing/AbstractRouteCollection.php中
protected function getRouteForMethods($request, array $methods)
{
if ($request->method() === 'OPTIONS') {
return (new Route('OPTIONS', $request->path(), function () use ($methods) {
return new Response('', 200, ['Allow' => implode(',', $methods)]);
}))->bind($request);
}
$this->methodNotAllowed($methods, $request->method());
}
判断如果请求方式是 OPTIONS,则返回状态码为 200 响应,但是没有返回任何 header 信息,否则返回一个methodNotAllowed状态码为 405 的错误(即请求方式不允许的情况)。所以在系统处理 OPTIONS 请求的过程中添加相关header信息。还需要另外,再有个处理preflight request(预检请求)中间件。
cd /var/web/www/exampleApiphp artisan make:middleware AccessControlAllowOriginPreflight
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class AccessControlAllowOriginPreflight
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure(\Illuminate\Http\Request): (\Illuminate\Http\Response|\Illuminate\Http\RedirectResponse) $next
* @return \Illuminate\Http\Response|\Illuminate\Http\RedirectResponse
*/
public function handle(Request $request, Closure $next)
{
if(strtoupper($request->getMethod()) == 'OPTIONS')
{
$response = $next($request);
$origin = $request->header('Origin') ?? $request->server('HTTP_ORIGIN');
$allowOrigin = [
'https://www.example.com',
'https://admin.example.com',
'https://api.example.com',
'http://localhost:3000',
'http://localhost:4173',
'http://localhost:5173',
];
if(!empty($origin))
{
if (in_array($origin, $allowOrigin))
{
$response->header('Access-Control-Allow-Origin',$origin);
$response->header('Access-Control-Allow-Credentials', 'true');
$response->header('Access-Control-Allow-Methods','POST, OPTIONS, GET, HEAD, PUT, DELETE, PATCH');
$response->header('Access-Control-Allow-Headers','Origin, Expect, Accept, Accept-Language, Access-Control-Request-Method, Access-Control-Request-Headers, Content-Disposition, Content-Encoding, Content-Type, Content-Length, Content-Language, Content-Length, Content-MD5, Content-Range, Expires, Pragma, Last-Modified, ETag, cache-control, Connection, User-Agent, X-XSRF-TOKEN, XSRF-TOKEN, X-Requested-With, WWW-Authenticate, Authorization');
$response->header('Access-Control-Max-Age', '3600');
}
else{
return response('Unauthorized.', 401);
}
}
return $response;
}
return $next($request);
}
}
处理预检请求的响应头:
- Access-Control-Allow-Origin
- Access-Control-Allow-Credentials
- Access-Control-Allow-Methods
- Access-Control-Allow-Headers
- Access-Control-Max-Age
处理真实实际请求的响应头:
- Access-Control-Allow-Origin
- Access-Control-Allow-Credentials
- Access-Control-Expose-Headers
对于带身份认证信息的跨域资源请求(比如,携带 token),预检请求和真实请求,都需要设置Access-Control-Allow-Origin和Access-Control-Allow-Credentials。参考header 信息
注册中间件
我们只需要在$middlewareGroups中间件组下的api中添加即可。由于群组路由中间件是在路由匹配过程之后才进入,因此 OPTIONS 请求尚未通过此处中间件的handle函数,就已经返回了。因此我们添加的中间件,需要添加到$middleware数组中,不能添加到$middlewareGroups中间件组下的api中。打开文件app/Http/Kernel.php修改。
<?php
namespace App\Http;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
class Kernel extends HttpKernel
{
/**
* 全局中间件。每一次请求,这里面的每个中间件都会执行。对所有的请求要做一些处理的时候,就适合定义在该属性内。
* 注释掉原来默认带的 HandleCors 中间件,使用自己创建的 options 中间件
*/
protected $middleware = [
// \App\Http\Middleware\TrustHosts::class,
\App\Http\Middleware\TrustProxies::class,
// Illuminate\Http\Middleware\HandleCors::class,
\App\Http\Middleware\PreventRequestsDuringMaintenance::class,
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
\App\Http\Middleware\TrimStrings::class,
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
\App\Http\Middleware\AccessControlAllowOriginPreflight::class,
];
/**
* 中间件组。针对不同的路由文件,启用的不同组件。
* 比如,路由文件 routes/api.php 只启用了其数组的中间件,需要通过其中的验证才能继续执行。否则被拒绝。
*/
protected $middlewareGroups = [
'web' => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\Session\Middleware\AuthenticateSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
'api' => [
'throttle:api',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
\App\Http\Middleware\AccessControlAllowOrigin::class,
],
];
/**
* 路由中间件。在定义路由时候引用,才会被启用,发挥验证的效果。有些个别的请求,我们需要执行特别的中间件时,就适合定义在这属性里面。
* 比如,在 routes/web.php 中 Route::get('/user','UserController@index')->middleware('auth');
*/
protected $routeMiddleware = [
'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
];
}
Laravel9 默认具备的跨域资源共享插件有:
第一个是Laravel-cors,其配置文件config/cors.php,在Kernel.php中的引用,是Illuminate\Http\Middleware\HandleCors::class。若使用此插件,还需要自己另外再配置其他内容,若不使用直接注释掉即可。
第二个是Sanctum,其配置文件config/sanctum.php,在app/Models/User.php中的引用,是use Laravel\Sanctum\HasApiTokens;。
路由访问规则
在路由配置routes/api.php中添加路由规则。下面是测试举例,根基自己的实际情况来写访问规则即可。
<?php
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
Route::get('/', function () {
return 'ok';
});
| 中间件 | 作用 |
|---|---|
| Authenticate | 用户身份验证。可修改 redirectTo 方法,返回未经身份验证的用户应该重定向到的路径。 |
| EncryptCookies | 对 Cookie 进行加解密处理与验证。可通过$except 数组属性设置不做加密处理的 cookie。 |
| RedirectIfAuthenticated | 当请求页是注册、登录、忘记密码时,检测用户是否已经登录,如果已经登录,那么就重定向到首页,如果没有就打开相应界面。可以在 handle 方法中定制重定向到的路径。 |
| TrimStrings | 对请求参数内容进行前后空白字符清理。可通过$except 数组属性设置不做处理的参数。 |
| TrustHosts | 在 Illuminate 请求对象中配置了受信任主机的白名单 |
| TrustProxies | 配置可信代理。可通过$proxies 属性设置可信代理列表,$headers 属性设置用来检测代理的 HTTP 头字段。 |
| VerifyCsrfToken | 验证请求里的令牌是否与存储在会话中令牌匹配。可通过$except 数组属性设置不做 CSRF 验证的网址。 |
| PreventRequestsDuringMaintenance |
配置域名 nginx
- 前端前台域名www.example.com指向/var/web/www/exampleHome
- 前端后台域名admin.example.com指向/var/web/www/exampleAdmin
- 后端数据域名api.example.com指向/var/web/www/exampleApi/public
vim /usr/local/nginx/conf/nginx.conf
user www;
worker_processes 2;
error_log /var/log/nginx/error.log notice;
pid /usr/local/nginx/logs/nginx.pid;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
gzip on;
gzip_http_version 1.1;
gzip_vary on;
gzip_comp_level 3;
gzip_proxied any;
gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript image/jpeg image/gif image/png;
gzip_min_length 1;
gzip_buffers 16 8k;
gzip_disable "MSIE [1-6].(?!.*SV1)";
client_max_body_size 20M;
server {
listen 80;
server_name example.com;
rewrite ^(.*)$ https://www.example.com$1 permanent;
}
server {
listen 80;
server_name www.example.com;
rewrite ^(.*)$ https://www.example.com$1 permanent;
}
server {
listen 443 ssl;
server_name www.example.com;
root /var/web/www/exampleHome;
index index.html;
charset utf-8;
add_header X-Frame-Options "SAMEORIGIN";
add_header X-XSS-Protection "1; mode=block";
add_header X-Content-Type-Options "nosniff";
ssl_certificate "/usr/local/nginx/cert/exampleHome/www.example.com_bundle.crt";
ssl_certificate_key "/usr/local/nginx/cert/exampleHome/www.example.com.key";
ssl_session_cache shared:SSL:1m;
ssl_session_timeout 10m;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
location / {
try_files $uri $uri/ /index.html;
}
location = /favicon.ico { access_log off; log_not_found off; }
location = /robots.txt { access_log off; log_not_found off; }
error_page 404 /index.html;
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/local/nginx/html;
}
}
server {
listen 80;
server_name admin.example.com;
rewrite ^(.*)$ https://admin.example.com$1 permanent;
}
server {
listen 443 ssl;
server_name admin.example.com;
root /var/web/www/exampleAdmin;
index index.html;
charset utf-8;
add_header X-Frame-Options "SAMEORIGIN";
add_header X-XSS-Protection "1; mode=block";
add_header X-Content-Type-Options "nosniff";
ssl_certificate "/usr/local/nginx/cert/exampleAdmin/admin.example.com_bundle.crt";
ssl_certificate_key "/usr/local/nginx/cert/exampleAdmin/admin.example.com.key";
ssl_session_cache shared:SSL:1m;
ssl_session_timeout 10m;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
location / {
try_files $uri $uri/ /index.html;
}
location = /favicon.ico { access_log off; log_not_found off; }
location = /robots.txt { access_log off; log_not_found off; }
error_page 404 /index.html;
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/local/nginx/html;
}
}
server {
listen 80;
server_name api.example.com;
rewrite ^(.*)$ https://api.example.com$1 permanent;
}
server {
listen 443 ssl;
server_name api.example.com;
root /var/web/www/exampleApi/public;
index index.php index.html index.htm;
charset utf-8;
add_header X-Frame-Options "SAMEORIGIN";
add_header X-XSS-Protection "1; mode=block";
add_header X-Content-Type-Options "nosniff";
ssl_certificate "/usr/local/nginx/cert/exampleApi/api.example.com_bundle.crt";
ssl_certificate_key "/usr/local/nginx/cert/exampleApi/api.example.com.key";
ssl_session_cache shared:SSL:1m;
ssl_session_timeout 10m;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
location ~* \.html${
rewrite /(.*)\.html$ /$1 permanent;
}
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location = /favicon.ico { access_log off; log_not_found off; }
location = /robots.txt { access_log off; log_not_found off; }
error_page 404 /index.php;
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/local/nginx/html;
}
location ~ \.php(.*)$ {
include fastcgi.conf;
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
}
if (!-d $request_filename)
{
rewrite ^/(.+)/$ /$1 permanent;
}
if ($request_uri ~* index/?$)
{
rewrite ^/(.*)/index/?$ /$1 permanent;
}
if (!-e $request_filename)
{
rewrite ^/(.*)$ /index.php?/$1 last;
break;
}
location ~ /\.(?!well-known).* {
deny all;
}
}
}
然后重启 nginx
nginx -tsystemctl restart nginx
