跨域处理(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()])策略选择规则
同一个请求到来时,最终策略只看优先级,不看声明先后:
| 优先级 | 写法 | 结果 |
|---|---|---|
| 1 | DisableCors() | 直接禁用 CORS |
| 2 | requireCors { ... } | 使用终结点内联策略 |
| 3 | requireCors("name") | 使用终结点命名策略 |
| 4 | useCors { ... } | 使用全局内联策略 |
| 5 | useCors("name") | 使用全局命名策略 |
| 6 | useCors() | 使用默认策略 |
| 7 | 无可用策略 | 请求继续执行,但不写 CORS 头 |
requireCors() 空参重载只表示“这个终结点参与 CORS 处理”,不会覆盖全局已选中的策略。
时序图
下面的时序图对应“请求带 Origin 头”的情况:
执行流程
执行时只要抓住四步:
- 请求没有
Origin时,中间件直接放行。 - 请求带
Origin时,先按上一节的优先级解析最终策略。 - 如果是预检请求,即
OPTIONS + Origin + Access-Control-Request-Method,中间件直接返回204。 - 如果是普通请求,先执行终结点,再把 CORS 响应头写回响应。
预检请求不会进入终结点;普通请求会进入终结点。
当策略允许当前来源时,响应中会按场景写入这些头:
Access-Control-Allow-OriginAccess-Control-Allow-MethodsAccess-Control-Allow-HeadersAccess-Control-Allow-CredentialsAccess-Control-Max-AgeAccess-Control-Expose-HeadersVary: 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()])这个模式最稳定:
- 全局策略负责兜底
- 终结点策略只处理差异
- 策略覆盖关系清晰,不容易混乱