• 首页
  • vue
  • TypeScript
  • JavaScript
  • scss
  • css3
  • html5
  • php
  • MySQL
  • redis
  • jQuery
  • 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/exampleApi
    php 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)可以是cookiesauthorization 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/exampleApi
    php 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';
    });
    
    
    app\Http\Middleware目录下中间件
    中间件作用
    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 -t
    
    systemctl restart nginx