feat: add configurable transport-independent CORS policy

This commit is contained in:
2026-09-26 19:53:28 +08:00
parent 9fade3bedb
commit 800974cfff
33 changed files with 1645 additions and 23 deletions

17
examples/cors/README.md Normal file
View File

@@ -0,0 +1,17 @@
# CORS 与正常鉴权
在框架根目录运行:
```sh
go run ./examples/cors
WEB_ORIGIN=https://admin.example go run ./examples/cors
```
仅监听临时 localhost 端口,不依赖数据库或外部服务。配置由统一 LoadProject 加载,示例模拟浏览器 Origin/预检请求头,依次检查:
- OPTIONS 预检返回 204,无需登录。
- 实际 GET 未登录仍返回 401,前端可读取这条错误响应。
- 示例凭据通过后返回 200。
- 404 也保留允许来源的 CORS 响应头。
示例 Token 仅用于展示,项目使用自身鉴权实现。CORS 不验证 Token,也不保证其他客户端不能调用 API。完整规则与配置见 [cors](../../cors/README.md)。

10
examples/cors/config.yaml Normal file
View File

@@ -0,0 +1,10 @@
server:
cors:
enabled: true
allowedOrigins:
- "${WEB_ORIGIN:-https://web.example}"
allowedMethods: [GET, POST]
allowedHeaders: [Authorization, Content-Type]
exposedHeaders: [X-Request-ID]
allowCredentials: false
maxAge: 10m

76
examples/cors/main.go Normal file
View File

@@ -0,0 +1,76 @@
package main
import (
"errors"
"flag"
"fmt"
"io"
"net/http"
"net/http/httptest"
"os"
"time"
omni "git.yuntuo.hk/k2safe/go-omni"
"git.yuntuo.hk/k2safe/go-omni/config"
)
type Config struct {
Server config.Config `json:"server"`
}
func (c Config) Validate() error { return config.AtField("server", c.Server.Validate()) }
func run(path string) error {
cfg, err := config.LoadProject(path, Config{Server: config.Default()})
if err != nil {
return err
}
a := omni.New(omni.WithConfig(cfg.Server))
defer a.Close()
a.GET("/private", func(c *omni.Context) error {
if c.Header("Authorization") != "Bearer example-only" {
return omni.Unauthorized("LOGIN_REQUIRED", "login required")
}
return c.OK("authenticated")
})
server := httptest.NewServer(a)
defer server.Close()
client := &http.Client{Timeout: 3 * time.Second}
defer client.CloseIdleConnections()
for _, tc := range []struct {
method, path, auth string
status int
}{{"OPTIONS", "/private", "", 204}, {"GET", "/private", "", 401}, {"GET", "/private", "Bearer example-only", 200}, {"GET", "/missing", "", 404}} {
r, _ := http.NewRequest(tc.method, server.URL+tc.path, nil)
r.Header.Set("Origin", cfg.Server.CORS.AllowedOrigins[0])
if tc.method == "OPTIONS" {
r.Header.Set("Access-Control-Request-Method", "GET")
r.Header.Set("Access-Control-Request-Headers", "authorization")
}
if tc.auth != "" {
r.Header.Set("Authorization", tc.auth)
}
res, err := client.Do(r)
if err != nil {
return err
}
_, readErr := io.Copy(io.Discard, res.Body)
closeErr := res.Body.Close()
if err := errors.Join(readErr, closeErr); err != nil {
return err
}
if res.StatusCode != tc.status || res.Header.Get("Access-Control-Allow-Origin") == "" {
return fmt.Errorf("unexpected CORS response: %d", res.StatusCode)
}
fmt.Printf("%s %s -> %d, CORS allowed\n", tc.method, tc.path, res.StatusCode)
}
return nil
}
func main() {
path := flag.String("config", "examples/cors/config.yaml", "selected configuration file")
flag.Parse()
if err := run(*path); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}