Skip to content

Echo 框架规范

核心原则

Echo 框架必须遵循 RESTful 规范,使用统一的中间件和响应格式,确保 API 的一致性。

允许的做法

路由定义

  • 使用 RESTful 风格路由
  • 使用 分组管理路由
go
// ✅ 正确 - 路由注册
func Register(e *echo.Echo, app *app.App) {
    // API 分组
    api := e.Group("/api/v1")

    // 用户路由
    userHandler := handlers.NewUserHandler(app)
    api.POST("/users", userHandler.Create)
    api.GET("/users", userHandler.List)
    api.GET("/users/:id", userHandler.GetByID)
    api.PATCH("/users/:id", userHandler.Update)
    api.DELETE("/users/:id", userHandler.Delete)

    // 需要认证的路由
    auth := api.Group("", middleware.JWT())
    auth.POST("/products", productHandler.Create)
}

参数绑定

  • 使用 c.Bind() 绑定请求参数
  • 使用 c.Validate() 校验参数
  • 使用 专门的绑定结构体(DTO)
go
// ✅ 正确 - 参数绑定
type CreateUserRequest struct {
    Name     string `json:"name" validate:"required"`
    Email    string `json:"email" validate:"required,email"`
    Password string `json:"password" validate:"required,min=6"`
}

func (h *UserHandler) Create(c echo.Context) error {
    // 绑定参数
    req := new(CreateUserRequest)
    if err := c.Bind(req); err != nil {
        return c.JSON(400, response.Fail(1000, "参数格式错误"))
    }

    // 校验参数
    if err := c.Validate(req); err != nil {
        return c.JSON(400, response.Fail(1001, err.Error()))
    }

    // 转换为 Service 请求
    serviceReq := &services.CreateUserRequest{
        Name:     req.Name,
        Email:    req.Email,
        Password: req.Password,
    }

    // 调用 Service
    user, err := h.userService.Create(c.Request().Context(), serviceReq)
    if err != nil {
        return c.JSON(500, response.Fail(500, "创建失败"))
    }

    return c.JSON(201, response.Success(user))
}

获取参数

go
// ✅ 正确 - 获取不同类型参数
func (h *UserHandler) GetByID(c echo.Context) error {
    // 路径参数
    id := c.Param("id")

    // 查询参数
    page := c.QueryParam("page")
    pageSize := c.QueryParam("pageSize")

    // 请求头
    token := c.Request().Header.Get("Authorization")

    // Context
    ctx := c.Request().Context()
}

统一响应

  • 使用 response.Success/Fail/SuccessPaged
go
// ✅ 正确 - 统一响应
// 成功响应
return c.JSON(200, response.Success(data))

// 分页响应
return c.JSON(200, response.SuccessPaged(users, total, page, pageSize))

// 失败响应
return c.JSON(400, response.Fail(1000, "参数错误"))
return c.JSON(404, response.Fail(1002, "资源不存在"))
return c.JSON(500, response.Fail(500, "服务器错误"))

中间件使用

go
// ✅ 正确 - 中间件
// 全局中间件
e.Use(middleware.Logger())
e.Use(middleware.Recover())
e.Use(middleware.CORS())

// 路由组中间件
auth := api.Group("", middleware.JWT())

// 自定义中间件
func CustomMiddleware() echo.MiddlewareFunc {
    return func(next echo.HandlerFunc) echo.HandlerFunc {
        return func(c echo.Context) error {
            // 前置逻辑
            err := next(c)
            // 后置逻辑
            return err
        }
    }
}

禁止的做法

  • 禁止 直接返回未包装的数据
  • 禁止 在 Handler 中直接操作 GORM
  • 禁止 忽略错误
go
// ❌ 错误
func GetUser(c echo.Context) error {
    return c.JSON(200, user)  // 未使用统一响应体
}

// ❌ 错误
func CreateUser(c echo.Context) error {
    db.Create(&user)  // Handler 直接操作数据库
}

特殊场景

文件上传

go
// ✅ 正确 - 文件上传
func (h *FileHandler) Upload(c echo.Context) error {
    file, err := c.FormFile("file")
    if err != nil {
        return c.JSON(400, response.Fail(1000, "文件获取失败"))
    }

    // 文件大小限制
    if file.Size > 10*1024*1024 {
        return c.JSON(400, response.Fail(3001, "文件过大"))
    }

    // 保存文件
    src, err := file.Open()
    if err != nil {
        return c.JSON(500, response.Fail(500, "文件打开失败"))
    }
    defer src.Close()

    // 处理逻辑...
    return c.JSON(200, response.Success(map[string]string{
        "url": fileURL,
    }))
}

分页查询

go
// ✅ 正确 - 分页查询
func (h *UserHandler) List(c echo.Context) error {
    page, _ := strconv.Atoi(c.QueryParam("page"))
    pageSize, _ := strconv.Atoi(c.QueryParam("pageSize"))

    if page < 1 {
        page = 1
    }
    if pageSize < 1 || pageSize > 100 {
        pageSize = 20
    }

    users, total, err := h.userService.List(c.Request().Context(), page, pageSize)
    if err != nil {
        return c.JSON(500, response.Fail(500, "查询失败"))
    }

    return c.JSON(200, response.SuccessPaged(users, total, page, pageSize))
}

Echo 参数绑定安全实践

绑定机制说明

Echo 的参数绑定自动从多个来源提取数据并映射到结构体:

数据来源结构标签绑定顺序
URL 路径参数param:"id"第 1 阶段
查询参数query:"page"第 2 阶段(仅 GET/DELETE)
请求体json:"name" / form:"email"第 3 阶段

重要提示:后阶段的数据会覆盖前阶段的同名字段。

安全风险:参数注入

错误示例

go
// models/user.go - 业务模型
type User struct {
    ID       uint   `gorm:"primaryKey" json:"id"`
    Name     string `json:"name"`
    Email    string `json:"email"`
    Password string `json:"password"`
    IsAdmin  bool   `json:"is_admin"`  // ❌ 敏感字段暴露
}

// handlers/user.go
func (h *UserHandler) Create(c echo.Context) error {
    user := new(models.User)
    c.Bind(user)  // ❌ 直接绑定业务模型

    // 攻击者可提交: {"name":"张三","is_admin":true}
    // 导致普通用户注册为管理员
    h.db.Create(user)
    return c.JSON(200, user)
}

安全解决方案:DTO 分离

正确示例

go
// handlers/user.go - Handler 绑定结构体(DTO)
type CreateUserRequest struct {
    Name     string `json:"name" validate:"required"`
    Email    string `json:"email" validate:"required,email"`
    Password string `json:"password" validate:"required,min=6"`
    // ✅ 不包含 IsAdmin 等敏感字段
}

func (h *UserHandler) Create(c echo.Context) error {
    // 1. 绑定 Handler DTO
    req := new(CreateUserRequest)
    if err := c.Bind(req); err != nil {
        return c.JSON(400, response.Fail(1000, "参数格式错误"))
    }

    // 2. 校验参数
    if err := c.Validate(req); err != nil {
        return c.JSON(400, response.Fail(1001, err.Error()))
    }

    // 3. 显式转换到 Service 请求
    serviceReq := &services.CreateUserRequest{
        Name:     req.Name,
        Email:    req.Email,
        Password: req.Password,
        // IsAdmin 由系统内部赋值,不受外部请求影响
    }

    // 4. 调用 Service
    user, err := h.userService.Create(c.Request().Context(), serviceReq)
    if err != nil {
        return c.JSON(500, response.Fail(5000, "创建失败"))
    }

    return c.JSON(201, response.Success(user))
}
go
// services/user.go - Service 层
func (s *UserService) Create(ctx context.Context, req *CreateUserRequest) (*models.User, error) {
    hashedPassword, _ := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)

    user := &models.User{
        Name:     req.Name,
        Email:    req.Email,
        Password: string(hashedPassword),
        IsAdmin:  false,   // ✅ 系统内部赋值,不受外部控制
        IsActive: true,    // ✅ 系统默认值
    }

    if err := s.db.WithContext(ctx).Create(user).Error; err != nil {
        return nil, err
    }

    return user, nil
}

安全最佳实践总结

  • 使用独立的 Handler 绑定结构体(DTO),不直接绑定业务模型
  • 敏感字段不添加绑定标签(如 IsAdminRoleStatus
  • Handler 中显式转换 DTO 到 Service 请求结构体
  • 敏感字段由系统内部赋值,不从外部请求获取
  • 禁止在业务模型上直接添加 json / form 绑定标签
  • 禁止直接将 c.Bind() 结果用于数据库操作

详见 DTO 分离模式

相关文档