生产环境必看!跨域支持多个指定域名(拒绝使用 *,安全 + 可用双达标)

12次阅读
没有评论

Web 开发里,CORS 跨域配置是前后端联调的高频环节,但 90% 的开发者都踩过同一个坑:

为了省事直接用 Access-Control-Allow-Origin: *,结果无法携带 Cookie / Token,生产环境还存在严重的安全风险。

而只配置单个域名,又满足不了多前端、多域名访问的需求(比如管理后台、H5、小程序端同时请求一个后端)。

这篇博文就教你:*如何正确配置跨域,支持多个指定域名,不使用通配符 ,兼顾安全与可用性,覆盖 Java/Go/Python/PHP/Node.js 全主流语言。


一、核心前提:为什么不能用 *,又要支持多域名?

1. 通配符 * 的致命问题

  1. 无法携带凭证:开启 withCredentials=true(携带 Cookie、Authorization 头)时,浏览器强制禁止 Origin: *,直接报错;
  2. 安全风险极高:任何域名都能调用你的接口,容易被非法站点盗用、攻击。

2. 多域名需求场景

  • 前端域名:https://www.xxx.com
  • 管理后台域名:https://admin.xxx.com
  • 测试环境域名:https://test.xxx.com
  • 本地开发域名:http://localhost:3000

这些场景都需要白名单式允许跨域,而不是一刀切开放。

3. 正确实现原理(重点)

  1. 后端维护一个允许的域名白名单
  2. 获取请求头里的 Origin(当前请求域名);
  3. 判断 Origin 是否在白名单中;
  4. 动态返回对应域名,而不是固定 * 或单个域名。

标准响应头示例:

plaintext

Access-Control-Allow-Origin: https://www.xxx.com

二、全语言实现:支持多域名跨域(无 * 可直接复制)

所有方案均为生产可用版,支持白名单、携带 Cookie、处理 OPTIONS 预检请求。

1. Java Spring Boot(推荐全局配置)

java

运行

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class CorsConfig implements WebMvcConfigurer {

    // 允许的跨域域名白名单
    private static final String[] ALLOW_ORIGINS = {
            "http://localhost:3000",
            "https://www.xxx.com",
            "https://admin.xxx.com",
            "https://test.xxx.com"
    };

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins(ALLOW_ORIGINS) // 直接传入白名单数组
                .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
                .allowCredentials(true) // 允许携带Cookie
                .maxAge(3600)
                .allowedHeaders("*");
    }
}

2. Go Gin 框架(动态判断 Origin)

go

运行

package main

import (
	"github.com/gin-gonic/gin"
	"net/http"
	"strings"
)

// 域名白名单
var allowOrigins = map[string]bool{
	"http://localhost:3000":   true,
	"https://www.xxx.com":     true,
	"https://admin.xxx.com":   true,
}

func CorsMiddleware() gin.HandlerFunc {
	return func(c *gin.Context) {
		origin := c.Request.Header.Get("Origin")
		
		// 动态判断是否在白名单
		if allowOrigins[origin] {
			c.Header("Access-Control-Allow-Origin", origin)
		}

		c.Header("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS")
		c.Header("Access-Control-Allow-Headers", "Content-Type,Authorization")
		c.Header("Access-Control-Allow-Credentials", "true")

		// 处理预检请求
		if c.Request.Method == http.MethodOptions {
			c.AbortWithStatus(http.StatusNoContent)
			return
		}
		c.Next()
	}
}

func main() {
	r := gin.Default()
	r.Use(CorsMiddleware())
	r.Run(":8080")
}

3. Python Flask

python

运行

from flask import Flask, request

app = Flask(__name__)

# 白名单
ALLOW_ORIGINS = {
    "http://localhost:3000",
    "https://www.xxx.com",
    "https://admin.xxx.com"
}

@app.after_request
def handle_cors(response):
    origin = request.headers.get("Origin", "")
    # 匹配白名单
    if origin in ALLOW_ORIGINS:
        response.headers["Access-Control-Allow-Origin"] = origin
        response.headers["Access-Control-Allow-Credentials"] = "true"
        response.headers["Access-Control-Allow-Methods"] = "GET,POST,PUT,DELETE,OPTIONS"
        response.headers["Access-Control-Allow-Headers"] = "Content-Type,Authorization"
    return response

if __name__ == '__main__':
    app.run(port=8080)

4. PHP (原生 & Laravel)

原生 PHP

php

运行

<?php
$allowOrigins = [
    "http://localhost:3000",
    "https://www.xxx.com",
    "https://admin.xxx.com"
];

$origin = $_SERVER['HTTP_ORIGIN'] ?? '';

// 动态允许
if (in_array($origin, $allowOrigins)) {
    header("Access-Control-Allow-Origin: $origin");
    header("Access-Control-Allow-Credentials: true");
    header("Access-Control-Allow-Methods: GET,POST,PUT,DELETE,OPTIONS");
    header("Access-Control-Allow-Headers: Content-Type,Authorization");
}

// 处理预检
if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') {
    http_response_code(200);
    exit;
}

echo json_encode(["code" => 200]);
?>

Laravel 中间件

php

运行

namespace App\Http\Middleware;
use Closure;

class CorsMiddleware
{
    protected $allowOrigins = [
        "http://localhost:3000",
        "https://www.xxx.com",
    ];

    public function handle($request, Closure $next)
    {
        $origin = $request->header('Origin');
        $response = $next($request);

        if (in_array($origin, $this->allowOrigins)) {
            $response->header('Access-Control-Allow-Origin', $origin);
            $response->header('Access-Control-Allow-Credentials', 'true');
            $response->header('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE,OPTIONS');
        }
        return $response;
    }
}

5. Node.js Express

javascript

运行

const express = require('express');
const app = express();

// 白名单
const allowOrigins = [
    'http://localhost:3000',
    'https://www.xxx.com',
    'https://admin.xxx.com'
];

// 跨域中间件
app.use((req, res, next) => {
    const origin = req.headers.origin;
    if (allowOrigins.includes(origin)) {
        res.setHeader('Access-Control-Allow-Origin', origin);
        res.setHeader('Access-Control-Allow-Credentials', 'true');
        res.setHeader('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE,OPTIONS');
        res.setHeader('Access-Control-Allow-Headers', 'Content-Type,Authorization');
    }
    if (req.method === 'OPTIONS') {
        return res.sendStatus(200);
    }
    next();
});

app.listen(8080);

三、生产环境必知的 3 个关键规则

  1. 必须动态返回 Origin,不能写死浏览器一次只认一个域名,多域名必须用白名单 + 动态匹配。
  2. ** 允许携带 Cookie 时,绝对不能用 ***只要开启 Access-Control-Allow-Credentials: true* 直接失效。
  3. OPTIONS 预检请求必须放行自定义请求头(如 Token)、POST/JSON 请求都会触发预检,后端必须处理。

四、最佳实践总结

  1. 开发环境:可以用框架代理,无需配置 CORS;
  2. 测试 / 生产环境:一律使用域名白名单,禁止使用 *
  3. 多端共用后端:统一维护一份跨域白名单,新增前端只需要加域名;
  4. 安全优先:只给信任的域名开跨域权限。

结尾

学会这种动态白名单跨域方案,你就彻底告别:

  • The 'Access-Control-Allow-Origin' header contains multiple values
  • Credentials flag is 'true', but 'Access-Control-Allow-Origin' is '*'

所有语言一套逻辑,生产环境安全、稳定、无坑可用。

正文完
可以使用微信扫码关注公众号(ID:xzluomor)
post-qrcode
 0
评论(没有评论)
验证码