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