Skip to content

跨域处理(CORS)中间件

CORS 中间件负责两件事:

  • 处理预检请求
  • 给真实请求补充跨域响应头

它是否生效,不只看 useCors(...),还取决于终结点元数据。理解“全局策略”和“终结点策略”的关系,比记 API 名字更重要。

最小示例

cangjie
import soulsoft_web_http.*
import soulsoft_web_hosting.*
import soulsoft_web_routing.*
import soulsoft_web_cors.*
import soulsoft_extensions_injection.*

main(args: Array<String>): Int64 {
    let builder = WebHost.createBuilder(args)

    // 注册路由服务
    builder.services.addRouting()

    // 注册 CORS 服务,并声明默认策略和命名策略
    builder.services.addCors { options =>
        options.addDefaultPolicy { policy =>
            policy.withOrigins(["https://app.example.com"])
                .allowAnyMethod()
                .allowAnyHeader()
        }

        options.addPolicy("admin") { policy =>
            policy.withOrigins(["https://admin.example.com"])
                .allowAnyMethod()
                .allowAnyHeader()
                .allowCredentials()
        }
    }

    let app = builder.build()

    // 先执行路由匹配,再让 CORS 读取终结点元数据
    app.useRouting()
    app.useCors()

    // 未显式声明终结点策略,走全局默认策略
    app.mapGet("/profile") { ctx =>
        ctx.response.write("profile")
    }

    // 当前终结点改用命名策略 admin
    app.mapGet("/admin/users") { ctx =>
        ctx.response.write("users")
    }.requireCors("admin")

    app.run()
    return 0
}

这里有两个直接结论:

  • app.useRouting() 应放在 app.useCors() 之前。
  • 终结点即使不写 requireCors(),只要全局启用了 useCors(),仍会走全局策略。

常见挂载方式

cangjie
// 全局默认策略
app.useCors()

// 全局命名策略
app.useCors("admin")

// 全局内联策略
app.useCors { policy =>
    // 直接在中间件上声明一套全局策略
    policy.withOrigins(["https://api.example.com"])
        .allowAnyMethod()
        .allowAnyHeader()
}

// 终结点:使用当前策略
app.mapGet("/a") { ctx =>
    ctx.response.write("ok")
}.requireCors()

// 终结点:切换为命名策略
app.mapGet("/b") { ctx =>
    ctx.response.write("ok")
}.requireCors("admin")

// 终结点:直接使用内联策略
app.mapGet("/c") { ctx =>
    ctx.response.write("ok")
}.requireCors { policy =>
    // 该终结点单独定义自己的跨域规则
    policy.withOrigins(["https://upload.example.com"])
        .allowAnyMethod()
        .allowAnyHeader()
}

// 终结点:禁用 CORS
app.mapGet("/d") { ctx =>
    ctx.response.write("ok")
}.withMetadata([DisableCors()])

策略选择规则

同一个请求到来时,最终策略只看优先级,不看声明先后:

优先级写法结果
1DisableCors()直接禁用 CORS
2requireCors { ... }使用终结点内联策略
3requireCors("name")使用终结点命名策略
4useCors { ... }使用全局内联策略
5useCors("name")使用全局命名策略
6useCors()使用默认策略
7无可用策略请求继续执行,但不写 CORS 头

requireCors() 空参重载只表示“这个终结点参与 CORS 处理”,不会覆盖全局已选中的策略。

时序图

下面的时序图对应“请求带 Origin 头”的情况:

执行流程

执行时只要抓住四步:

  1. 请求没有 Origin 时,中间件直接放行。
  2. 请求带 Origin 时,先按上一节的优先级解析最终策略。
  3. 如果是预检请求,即 OPTIONS + Origin + Access-Control-Request-Method,中间件直接返回 204
  4. 如果是普通请求,先执行终结点,再把 CORS 响应头写回响应。

预检请求不会进入终结点;普通请求会进入终结点。

当策略允许当前来源时,响应中会按场景写入这些头:

  • Access-Control-Allow-Origin
  • Access-Control-Allow-Methods
  • Access-Control-Allow-Headers
  • Access-Control-Allow-Credentials
  • Access-Control-Max-Age
  • Access-Control-Expose-Headers
  • Vary: Origin

当来源不匹配或根本没有可用策略时,业务响应仍可能正常返回,只是不会带可用的 CORS 头,最终由浏览器决定是否拦截跨域访问。

一个推荐用法

推荐把“绝大多数接口都适用的策略”放到全局,把少量例外放到终结点:

cangjie
// 先完成路由匹配
app.useRouting()

// 给大多数接口应用全局默认策略
app.useCors()

// 普通接口直接继承全局策略
app.mapGet("/api/profile") { ctx =>
    ctx.response.write("profile")
}

// 管理接口改为使用命名策略
app.mapGet("/api/admin/users") { ctx =>
    ctx.response.write("users")
}.requireCors("admin")

// 内部接口显式禁用 CORS
app.mapGet("/internal/health") { ctx =>
    ctx.response.write("ok")
}.withMetadata([DisableCors()])

这个模式最稳定:

  • 全局策略负责兜底
  • 终结点策略只处理差异
  • 策略覆盖关系清晰,不容易混乱