feat: add configurable transport-independent CORS policy
This commit is contained in:
17
examples/cors/README.md
Normal file
17
examples/cors/README.md
Normal 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
10
examples/cors/config.yaml
Normal 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
76
examples/cors/main.go
Normal 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user