diff --git a/cmd/agent/clipboard_20260429_195256.png b/cmd/agent/clipboard_20260429_195256.png new file mode 100644 index 0000000..3b73c12 Binary files /dev/null and b/cmd/agent/clipboard_20260429_195256.png differ diff --git a/cmd/agent/main.go b/cmd/agent/main.go new file mode 100644 index 0000000..3da0f68 --- /dev/null +++ b/cmd/agent/main.go @@ -0,0 +1,105 @@ +package main + +import ( + "context" + "fmt" + "log" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + "u-desk/internal/agent/config" + agentmw "u-desk/internal/agent/middleware" + "u-desk/internal/agent/handler" + "u-desk/internal/filesystem" + + "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" +) + +func main() { + cfg, err := config.Load("configs/agent.yaml") + if err != nil { + log.Fatalf("[FATAL] 加载配置失败: %v", err) + } + + fsConfig := filesystem.DefaultConfig() + fsSvc, err := filesystem.NewFileSystemService(fsConfig) + if err != nil { + log.Fatalf("[FATAL] 初始化文件服务失败: %v", err) + } + + e := echo.New() + e.HideBanner = true + e.HidePort = true + + e.Use(middleware.Recover()) + e.Use(middleware.Logger()) + e.Use(middleware.CORSWithConfig(middleware.CORSConfig{ + AllowOrigins: cfg.CORS.AllowedOrigins, + AllowMethods: []string{echo.GET, echo.PUT, echo.POST, echo.DELETE, echo.PATCH, echo.OPTIONS}, + AllowHeaders: []string{echo.HeaderOrigin, echo.HeaderContentType, echo.HeaderAuthorization, echo.HeaderAccept}, + })) + if cfg.Auth.Token != "" { + e.Use(agentmw.Auth(cfg.Auth.Token)) + } + + h := handler.New(fsSvc, cfg) + + api := e.Group("/api/v1") + { + api.GET("/ping", h.Ping) + api.GET("/info", h.Info) + + // 文件操作 — 所有通过 ?path= 参数传递路径 + api.GET("/fs", h.ListOrStat) // ?path=xxx [&action=stat] + api.GET("/fs/read", h.ReadFile) // ?path=xxx + api.PUT("/fs/write", h.WriteFile) // ?path=xxx & body={content} + api.POST("/fs/create", h.Create) // ?path=xxx & body={type,name} + api.DELETE("/fs/delete", h.Delete) // ?path=xxx + api.PATCH("/fs/rename", h.Rename) // ?path=xxx & body={new_path} + api.POST("/fs/upload", h.Upload) // ?path=xxx & body={content} + api.GET("/fs/detect", h.DetectType) // ?path=xxx + + sys := api.Group("/system") + { + sys.GET("/common-paths", h.CommonPaths) + sys.GET("/drives", h.Drives) + } + + proxy := api.Group("/proxy") + { + proxy.GET("/localfs/*", h.FileServerProxy) + proxy.GET("/html-preview", h.HTMLPreviewProxy) + } + } + + addr := fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port) + go func() { + log.Printf("[INFO] u-fs-agent 启动于 %s", addr) + if err := e.Start(addr); err != nil && err != http.ErrServerClosed { + log.Fatalf("[FATAL] HTTP 服务器错误: %v", err) + } + }() + + go func() { + if _, err := filesystem.StartLocalFileServer(); err != nil { + log.Printf("[WARN] 文件服务器启动失败(媒体预览不可用): %v", err) + } + }() + + quit := make(chan os.Signal, 1) + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) + <-quit + log.Println("[INFO] 正在关闭...") + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + filesystem.ShutdownLocalFileServer() + e.Shutdown(ctx) + fsSvc.Close(ctx) + log.Println("[INFO] 已关闭") +} diff --git a/configs/agent.yaml b/configs/agent.yaml new file mode 100644 index 0000000..08d873a --- /dev/null +++ b/configs/agent.yaml @@ -0,0 +1,29 @@ +# u-fs-agent 配置文件 +# 部署到远端服务器后修改此文件 + +server: + port: 9876 # 监听端口 + host: "0.0.0.0" # 监听地址 + +auth: + token: "" # API Token(留空则不验证,生产环境必须设置) + # 生成随机 token: openssl rand -hex 32 + +cors: + allowed_origins: + - "*" # 开发模式允许所有来源 + # 生产环境建议限定: + # - "http://localhost:5173" + # - "http://localhost:5174" + +log: + level: "info" # debug / info / warn / error + format: "json" # json / text + +file_server: + port: 8073 # 内置文件服务器端口(用于媒体预览代理) + max_file_size: 524288000 # 最大文件大小 500MB + +security: + allow_symlinks: false # 是否允许符号链接 + check_system_paths: true # 检查系统关键目录 diff --git a/go.mod b/go.mod index 42c12cf..7349eaa 100644 --- a/go.mod +++ b/go.mod @@ -6,10 +6,12 @@ require ( github.com/chromedp/cdproto v0.0.0-20250724212937-08a3db8b4327 github.com/chromedp/chromedp v0.14.2 github.com/glebarez/sqlite v1.11.0 + github.com/labstack/echo/v4 v4.15.0 github.com/shirou/gopsutil/v3 v3.24.5 github.com/wailsapp/wails/v2 v2.12.0 github.com/yuin/goldmark v1.8.2 golang.org/x/sys v0.40.0 + gopkg.in/yaml.v3 v3.0.1 gorm.io/gorm v1.31.1 ) @@ -30,7 +32,6 @@ require ( github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect - github.com/labstack/echo/v4 v4.15.0 // indirect github.com/labstack/gommon v0.4.2 // indirect github.com/leaanthony/go-ansi-parser v1.6.1 // indirect github.com/leaanthony/gosod v1.0.4 // indirect @@ -59,6 +60,7 @@ require ( golang.org/x/exp v0.0.0-20260112195511-716be5621a96 // indirect golang.org/x/net v0.49.0 // indirect golang.org/x/text v0.33.0 // indirect + golang.org/x/time v0.14.0 // indirect modernc.org/libc v1.67.7 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect diff --git a/go.sum b/go.sum index 5ae06bd..9895d53 100644 --- a/go.sum +++ b/go.sum @@ -141,9 +141,13 @@ golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9sn golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg= diff --git a/internal/agent/config/config.go b/internal/agent/config/config.go new file mode 100644 index 0000000..6b07a2a --- /dev/null +++ b/internal/agent/config/config.go @@ -0,0 +1,108 @@ +package config + +import ( + "fmt" + "os" + "strings" + + "gopkg.in/yaml.v3" +) + +type Config struct { + Server ServerConfig `yaml:"server"` + Auth AuthConfig `yaml:"auth"` + CORS CORSConfig `yaml:"cors"` + Log LogConfig `yaml:"log"` + FileServer FileServerConfig `yaml:"file_server"` + Security SecurityConfig `yaml:"security"` +} + +type ServerConfig struct { + Port int `yaml:"port"` + Host string `yaml:"host"` +} + +type AuthConfig struct { + Token string `yaml:"token"` +} + +type CORSConfig struct { + AllowedOrigins []string `yaml:"allowed_origins"` +} + +type LogConfig struct { + Level string `yaml:"level"` + Format string `yaml:"format"` +} + +type FileServerConfig struct { + Port int `yaml:"port"` + MaxFileSize int64 `yaml:"max_file_size"` +} + +type SecurityConfig struct { + AllowSymlinks bool `yaml:"allow_symlinks"` + CheckSystemPaths bool `yaml:"check_system_paths"` +} + +// FileServerAddr 返回文件服务器的完整地址 +func (c *Config) FileServerAddr() string { + return fmt.Sprintf("http://localhost:%d", c.FileServer.Port) +} + +func Load(path string) (*Config, error) { + data, err := os.ReadFile(path) + if err != nil { + // 配置文件不存在时使用默认值 + if os.IsNotExist(err) { + return Default(), nil + } + return nil, err + } + + cfg := Default() + if err := yaml.Unmarshal(data, cfg); err != nil { + return nil, err + } + + // 清理 origins 中的空格并去重 + seen := make(map[string]bool, len(cfg.CORS.AllowedOrigins)) + uniques := cfg.CORS.AllowedOrigins[:0] + for _, origin := range cfg.CORS.AllowedOrigins { + o := strings.TrimSpace(origin) + if o != "" && !seen[o] { + seen[o] = true + uniques = append(uniques, o) + } + } + cfg.CORS.AllowedOrigins = uniques + + return cfg, nil +} + +func Default() *Config { + return &Config{ + Server: ServerConfig{ + Port: 9876, + Host: "0.0.0.0", + }, + Auth: AuthConfig{ + Token: "", + }, + CORS: CORSConfig{ + AllowedOrigins: []string{"*"}, + }, + Log: LogConfig{ + Level: "info", + Format: "json", + }, + FileServer: FileServerConfig{ + Port: 8073, + MaxFileSize: 500 * 1024 * 1024, + }, + Security: SecurityConfig{ + AllowSymlinks: false, + CheckSystemPaths: true, + }, + } +} diff --git a/internal/agent/handler/file_handler.go b/internal/agent/handler/file_handler.go new file mode 100644 index 0000000..b8dee1c --- /dev/null +++ b/internal/agent/handler/file_handler.go @@ -0,0 +1,176 @@ +package handler + +import ( + "net/http" + "path/filepath" + "strings" + + "u-desk/internal/agent/model" + "u-desk/internal/filesystem" + + "github.com/labstack/echo/v4" +) + +type writeFileReq struct { + Content string `json:"content"` +} + +type createReq struct { + Type string `json:"type"` // "file" or "dir" + Name string `json:"name"` +} + +type renameReq struct { + NewPath string `json:"new_path"` +} + +type uploadReq struct { + Content string `json:"content"` // base64 编码内容 +} + +// ListOrStat 列出目录或获取文件信息(?get=stat 时返回单文件信息) +func (h *Handler) ListOrStat(c echo.Context) error { + path := getPath(c) + action := c.QueryParam("get") + + if action == "stat" { + info, err := h.fsSvc.GetFileInfo(path) + if err != nil { + return c.JSON(http.StatusOK, model.NotFound(err.Error())) + } + return c.JSON(http.StatusOK, model.OK(info)) + } + + files, err := h.fsSvc.ListDir(path) + if err != nil { + return c.JSON(http.StatusOK, model.InternalError(err.Error())) + } + // 限制返回数量,避免大目录导致前端卡顿 + limit := c.QueryParam("limit") + if limit != "" { + n := 0 + for i, f := range files { + if n >= 500 { // 硬限制 500 条 + break + } + files[i] = f + n++ + } + files = files[:n] + } + return c.JSON(http.StatusOK, model.OK(files)) +} + +// ReadFile 读取文件文本内容 +func (h *Handler) ReadFile(c echo.Context) error { + path := getPath(c) + content, err := h.fsSvc.ReadFile(path) + if err != nil { + return c.JSON(http.StatusOK, model.InternalError(err.Error())) + } + return c.JSON(http.StatusOK, model.OK(map[string]string{ + "content": content, + })) +} + +// WriteFile 写入文件文本内容 +func (h *Handler) WriteFile(c echo.Context) error { + path := getPath(c) + var req writeFileReq + if err := c.Bind(&req); err != nil { + return c.JSON(http.StatusBadRequest, model.BadRequest("无效请求体")) + } + if err := h.fsSvc.WriteFile(path, req.Content); err != nil { + return c.JSON(http.StatusInternalServerError, model.InternalError(err.Error())) + } + return c.JSON(http.StatusOK, model.NoContent()) +} + +// Create 创建文件或目录 +func (h *Handler) Create(c echo.Context) error { + parentPath := getPath(c) + var req createReq + if err := c.Bind(&req); err != nil { + return c.JSON(http.StatusBadRequest, model.BadRequest("无效请求体")) + } + + req.Name = strings.TrimSpace(req.Name) + if req.Name == "" { + return c.JSON(http.StatusBadRequest, model.BadRequest("名称不能为空")) + } + + var result *filesystem.FileOperationResult + var err error + + fullPath := filepath.Join(parentPath, req.Name) + + switch req.Type { + case "dir": + result, err = h.fsSvc.CreateDir(fullPath) + default: + result, err = h.fsSvc.CreateFile(fullPath) + } + + if err != nil { + return c.JSON(http.StatusInternalServerError, model.InternalError(err.Error())) + } + return c.JSON(http.StatusCreated, model.OK(result)) +} + +// Delete 删除文件或目录 +func (h *Handler) Delete(c echo.Context) error { + path := getPath(c) + result, err := h.fsSvc.DeletePath(path) + if err != nil { + return c.JSON(http.StatusInternalServerError, model.InternalError(err.Error())) + } + return c.JSON(http.StatusOK, model.OK(result)) +} + +// Rename 重命名文件或目录 +func (h *Handler) Rename(c echo.Context) error { + oldPath := getPath(c) + var req renameReq + if err := c.Bind(&req); err != nil { + return c.JSON(http.StatusBadRequest, model.BadRequest("无效请求体")) + } + req.NewPath = strings.TrimSpace(req.NewPath) + if req.NewPath == "" { + return c.JSON(http.StatusBadRequest, model.BadRequest("新路径不能为空")) + } + cleanNew := filepath.Clean(req.NewPath) + if strings.Contains(cleanNew, "..") { + return c.JSON(http.StatusBadRequest, model.BadRequest("新路径不允许包含 ..")) + } + result, err := h.fsSvc.RenamePath(oldPath, cleanNew) + if err != nil { + return c.JSON(http.StatusInternalServerError, model.InternalError(err.Error())) + } + return c.JSON(http.StatusOK, model.OK(result)) +} + +// Upload 上传 Base64 编码的二进制文件 +func (h *Handler) Upload(c echo.Context) error { + path := getPath(c) + var req uploadReq + if err := c.Bind(&req); err != nil { + return c.JSON(http.StatusBadRequest, model.BadRequest("无效请求体")) + } + if req.Content == "" { + return c.JSON(http.StatusBadRequest, model.BadRequest("内容不能为空")) + } + if err := h.fsSvc.SaveBase64File(path, req.Content); err != nil { + return c.JSON(http.StatusInternalServerError, model.InternalError(err.Error())) + } + return c.JSON(http.StatusOK, model.NoContent()) +} + +// DetectType 通过文件内容检测类型 +func (h *Handler) DetectType(c echo.Context) error { + path := getPath(c) + info, err := h.fsSvc.DetectFileTypeByContent(path) + if err != nil { + return c.JSON(http.StatusOK, model.InternalError(err.Error())) + } + return c.JSON(http.StatusOK, model.OK(info)) +} diff --git a/internal/agent/handler/handler.go b/internal/agent/handler/handler.go new file mode 100644 index 0000000..21a6a35 --- /dev/null +++ b/internal/agent/handler/handler.go @@ -0,0 +1,37 @@ +package handler + +import ( + "net/http/httputil" + "net/url" + "path/filepath" + + "u-desk/internal/agent/config" + "u-desk/internal/filesystem" + + "github.com/labstack/echo/v4" +) + +type Handler struct { + fsSvc *filesystem.FileSystemService + cfg *config.Config + fileProxy *httputil.ReverseProxy +} + +func New(fsSvc *filesystem.FileSystemService, cfg *config.Config) *Handler { + fileTarget, _ := url.Parse(cfg.FileServerAddr() + "/localfs/") + return &Handler{ + fsSvc: fsSvc, + cfg: cfg, + fileProxy: httputil.NewSingleHostReverseProxy(fileTarget), + } +} + +// getPath 从 query 参数提取并规范化文件路径 +func getPath(c echo.Context) string { + raw := c.QueryParam("path") + if raw == "" { + return "" + } + // URL 已被 Echo 自动 decode,只需转换路径分隔符 + return filepath.FromSlash(raw) +} diff --git a/internal/agent/handler/server_handler.go b/internal/agent/handler/server_handler.go new file mode 100644 index 0000000..a9d0dbb --- /dev/null +++ b/internal/agent/handler/server_handler.go @@ -0,0 +1,64 @@ +package handler + +import ( + "fmt" + "io" + "net/http" + "net/url" + "path/filepath" + "strings" + + "github.com/labstack/echo/v4" +) + +// FileServerProxy 反向代理到内置文件服务器(用于媒体预览) +func (h *Handler) FileServerProxy(c echo.Context) error { + rawPath := c.Param("*") + if rawPath == "" { + return c.String(http.StatusBadRequest, "缺少文件路径") + } + + clean := filepath.Clean(rawPath) + if strings.Contains(clean, "..") { + return c.String(http.StatusForbidden, "路径不允许包含 ..") + } + + // 防止多重 /localfs/ 前缀(循环去除所有) + targetPath := filepath.ToSlash(clean) + for strings.HasPrefix(targetPath, "localfs/") || strings.HasPrefix(targetPath, "localfs\\") { + targetPath = strings.TrimPrefix(targetPath, "localfs/") + targetPath = strings.TrimPrefix(targetPath, "localfs\\") + } + c.Request().URL.Path = "/localfs/" + targetPath + h.fileProxy.ServeHTTP(c.Response(), c.Request()) + return nil +} + +// HTMLPreviewProxy 代理 HTML 预览请求(直连内部服务器,避免 ReverseProxy 路径拼接问题) +func (h *Handler) HTMLPreviewProxy(c echo.Context) error { + rawPath := c.QueryParam("path") + if rawPath == "" { + return c.String(http.StatusBadRequest, "缺少 path 参数") + } + clean := filepath.Clean(rawPath) + if strings.Contains(clean, "..") { + return c.String(http.StatusForbidden, "路径不允许包含 ..") + } + theme := c.QueryParam("theme") + + targetURL := fmt.Sprintf("http://localhost:8073/localfs/html-preview?path=%s&theme=%s", + url.QueryEscape(clean), url.QueryEscape(theme)) + + resp, err := http.Get(targetURL) + if err != nil { + return c.String(http.StatusBadGateway, "内部服务器不可用") + } + defer resp.Body.Close() + + for k, v := range resp.Header { + c.Response().Header()[k] = v + } + c.Response().WriteHeader(resp.StatusCode) + io.Copy(c.Response(), resp.Body) + return nil +} diff --git a/internal/agent/handler/system_handler.go b/internal/agent/handler/system_handler.go new file mode 100644 index 0000000..d11160f --- /dev/null +++ b/internal/agent/handler/system_handler.go @@ -0,0 +1,113 @@ +package handler + +import ( + "net/http" + "os" + "runtime" + "strings" + + "u-desk/internal/agent/model" + + "github.com/labstack/echo/v4" +) + +// Ping 健康检查 +func (h *Handler) Ping(c echo.Context) error { + return c.JSON(http.StatusOK, model.OK(map[string]string{ + "status": "ok", + })) +} + +// Info 返回 Agent 信息 +func (h *Handler) Info(c echo.Context) error { + hostname, _ := os.Hostname() + return c.JSON(http.StatusOK, model.OK(map[string]interface{}{ + "version": "0.1.0", + "os": runtime.GOOS, + "arch": runtime.GOARCH, + "hostname": hostname, + })) +} + +// CommonPaths 返回常用系统路径 +func (h *Handler) CommonPaths(c echo.Context) error { + paths := map[string]string{} + + home, _ := os.UserHomeDir() + if home != "" { + paths["home"] = home + paths["desktop"] = home + "/Desktop" + paths["documents"] = home + "/Documents" + paths["downloads"] = home + "/Downloads" + } + + // 根据平台添加盘符/根路径 + if runtime.GOOS == "windows" { + for _, drive := range "ABCDEFGHIJKLMNOPQRSTUVWXYZ" { + _, err := os.Stat(string(drive) + ":\\") + if err == nil { + paths["drive_"+strings.ToLower(string(drive))] = string(drive) + ":\\" + } + } + } else { + paths["root"] = "/" + _, err := os.Stat("/home") + if err == nil { + paths["users"] = "/home" + } + } + + return c.JSON(http.StatusOK, model.OK(paths)) +} + +// Drives 返回可用磁盘列表 +func (h *Handler) Drives(c echo.Context) error { + type DriveInfo struct { + Name string `json:"name"` + Path string `json:"path"` + FsType string `json:"fs_type,omitempty"` + Total uint64 `json:"total"` + Free uint64 `json:"free"` + } + + var drives []DriveInfo + + if runtime.GOOS == "windows" { + for _, drive := range "ABCDEFGHIJKLMNOPQRSTUVWXYZ" { + drivePath := string(drive) + ":\\" + if _, err := os.Stat(drivePath); err != nil { + continue + } + drives = append(drives, DriveInfo{ + Name: strings.ToLower(string(drive)), + Path: drivePath, + Total: 0, + Free: 0, + }) + } + } else { + parts, err := os.ReadDir("/") + if err == nil { + for _, p := range parts { + name := p.Name() + if len(name) == 2 && name[0] != '.' && name[1] != '.' && p.IsDir() { + // 可能是挂载点 + fullPath := "/" + name + if stat, err := os.Stat(fullPath); err == nil && stat.IsDir() { + drives = append(drives, DriveInfo{ + Name: name, + Path: fullPath, + }) + _ = stat + } + } + } + } + // 至少返回根目录 + if len(drives) == 0 { + drives = append(drives, DriveInfo{Name: "/", Path: "/"}) + } + } + + return c.JSON(http.StatusOK, model.OK(drives)) +} diff --git a/internal/agent/middleware/auth.go b/internal/agent/middleware/auth.go new file mode 100644 index 0000000..1528c61 --- /dev/null +++ b/internal/agent/middleware/auth.go @@ -0,0 +1,61 @@ +package middleware + +import ( + "crypto/subtle" + "net/http" + "strings" + "time" + + "github.com/labstack/echo/v4" +) + +const cookieName = "fs_token" + +func Auth(token string) echo.MiddlewareFunc { + if token == "" { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + return next(c) + } + } + } + + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + // 1. Authorization header(API 调用,首选) + auth := c.Request().Header.Get("Authorization") + if len(auth) >= 7 && strings.HasPrefix(auth, "Bearer ") && + subtle.ConstantTimeCompare([]byte(auth[7:]), []byte(token)) == 1 { + setAuthCookie(c, token) + return next(c) + } + // 2. Cookie(/