Skip to content

认证授权规范

核心原则

认证授权必须使用 JWT Token,遵循最小权限原则,确保敏感操作需要权限验证。

允许的做法

JWT 认证流程

登录接口

go
// ✅ 正确 - 登录请求
type LoginRequest struct {
    Username string `json:"username" validate:"required"`
    Password string `json:"password" validate:"required"`
}

type LoginResponse struct {
    Token     string `json:"token"`
    ExpiresAt int64  `json:"expiresAt"`
}

// Handler 层
func (h *AuthHandler) Login(c echo.Context) error {
    var req LoginRequest
    if err := c.Bind(&req); err != nil {
        return c.JSON(http.StatusBadRequest, ErrorResponse{
            Success:      false,
            ErrorCode:    ErrInvalidParams,
            ErrorMessage: "参数错误",
        })
    }

    token, expiresAt, err := h.authService.Login(
        c.Request().Context(),
        req.Username,
        req.Password,
    )
    if err != nil {
        return c.JSON(http.StatusUnauthorized, ErrorResponse{
            Success:      false,
            ErrorCode:    ErrInvalidCredential,
            ErrorMessage: "用户名或密码错误",
        })
    }

    return c.JSON(http.StatusOK, Response{
        Success: true,
        Data: LoginResponse{
            Token:     token,
            ExpiresAt: expiresAt,
        },
    })
}

JWT Token 生成

go
// ✅ 正确 - Service 层生成 Token
import (
    "time"
    "github.com/golang-jwt/jwt/v5"
)

type Claims struct {
    UserID int64  `json:"user_id"`
    Role   string `json:"role"`
    jwt.RegisteredClaims
}

func (s *AuthService) GenerateToken(userID int64, role string) (string, int64, error) {
    expiresAt := time.Now().Add(24 * time.Hour).Unix()

    claims := &Claims{
        UserID: userID,
        Role:   role,
        RegisteredClaims: jwt.RegisteredClaims{
            ExpiresAt: jwt.NewNumericDate(time.Unix(expiresAt, 0)),
            IssuedAt:  jwt.NewNumericDate(time.Now()),
        },
    }

    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    tokenString, err := token.SignedString([]byte(s.config.JWTSecret))
    if err != nil {
        return "", 0, err
    }

    return tokenString, expiresAt, nil
}

认证中间件

go
// ✅ 正确 - JWT 认证中间件
import (
    echojwt "github.com/labstack/echo-jwt/v4"
)

func (r *Router) authMiddleware() echo.MiddlewareFunc {
    config := echojwt.Config{
        SigningKey: []byte(r.app.Config().JWTSecret),
        TokenLookup: "header:Authorization:Bearer ",
    }
    return echojwt.WithConfig(config)
}

// 路由应用中间件
func (r *Router) RegisterRoutes() {
    api := r.echo.Group("/api/v1")

    // 公开接口
    api.POST("/auth/login", r.authHandler.Login)

    // 受保护接口
    protected := api.Group("")
    protected.Use(r.authMiddleware())
    protected.GET("/resource", r.resourceHandler.Get)
}

权限检查

go
// ✅ 正确 - 权限检查中间件
func (r *Router) adminOnly() echo.MiddlewareFunc {
    return func(next echo.HandlerFunc) echo.HandlerFunc {
        return func(c echo.Context) error {
            user := c.Get("user").(*jwt.Token)
            claims := user.Claims.(*Claims)

            if claims.Role != "admin" {
                return c.JSON(http.StatusForbidden, ErrorResponse{
                    Success:      false,
                    ErrorCode:    ErrPermissionDenied,
                    ErrorMessage: "权限不足",
                })
            }

            return next(c)
        }
    }
}

// 应用到需要管理员权限的路由
admin := protected.Group("/admin")
admin.Use(r.adminOnly())
admin.DELETE("/resource/:id", r.resourceHandler.Delete)

从 Token 获取用户信息

go
// ✅ 正确 - Handler 层获取当前用户
func (h *Handler) GetCurrentUser(c echo.Context) error {
    user := c.Get("user").(*jwt.Token)
    claims := user.Claims.(*Claims)
    userID := claims.UserID

    data, err := h.service.GetByID(c.Request().Context(), userID)
    if err != nil {
        return c.JSON(http.StatusNotFound, ErrorResponse{
            Success:      false,
            ErrorCode:    ErrResourceNotFound,
            ErrorMessage: "用户不存在",
        })
    }

    return c.JSON(http.StatusOK, Response{
        Success: true,
        Data:    data,
    })
}

前端实现

TypeScript 实现

typescript
// ✅ 正确 - 登录服务
import { request } from '@umijs/max';

export const authService = {
  login: async (username: string, password: string) => {
    return request<API.Response<{ token: string; expiresAt: number }>>(
      '/api/v1/auth/login',
      {
        method: 'POST',
        data: { username, password },
      }
    );
  },
};

// Token 存储
export const setToken = (token: string) => {
  localStorage.setItem('token', token);
};

export const getToken = (): string | null => {
  return localStorage.getItem('token');
};

export const removeToken = () => {
  localStorage.removeItem('token');
};

请求拦截器

typescript
// ✅ 正确 - UmiJS 请求配置
import { RequestConfig } from '@umijs/max';
import { getToken, removeToken } from './auth';
import { ErrorCode } from './errorCode';

export const request: RequestConfig = {
  requestInterceptors: [
    (url, options) => {
      const token = getToken();
      if (token) {
        return {
          url,
          options: {
            ...options,
            headers: {
              ...options.headers,
              Authorization: `Bearer ${token}`,
            },
          },
        };
      }
      return { url, options };
    },
  ],

  responseInterceptors: [
    async (response) => {
      const data = await response.clone().json();

      // Token 过期处理
      if (!data.success && data.errorCode === ErrorCode.TOKEN_EXPIRED) {
        removeToken();
        window.location.href = '/login';
      }

      return response;
    },
  ],
};

安全要求

密码处理

go
// ✅ 正确 - 使用 bcrypt 加密密码
import "golang.org/x/crypto/bcrypt"

func (s *AuthService) HashPassword(password string) (string, error) {
    hashedPassword, err := bcrypt.GenerateFromPassword(
        []byte(password),
        bcrypt.DefaultCost,
    )
    if err != nil {
        return "", err
    }
    return string(hashedPassword), nil
}

func (s *AuthService) CheckPassword(hashedPassword, password string) bool {
    err := bcrypt.CompareHashAndPassword(
        []byte(hashedPassword),
        []byte(password),
    )
    return err == nil
}

敏感字段保护

go
// ✅ 正确 - 分离绑定和业务结构体
type CreateRequest struct {
    Name     string `json:"name" validate:"required"`
    Email    string `json:"email" validate:"required,email"`
    Password string `json:"password" validate:"required,min=8"`
    // 不包含 Role、IsAdmin 等敏感字段
}

func (h *Handler) Create(c echo.Context) error {
    var req CreateRequest
    if err := c.Bind(&req); err != nil {
        return c.JSON(http.StatusBadRequest, ErrorResponse{
            Success:      false,
            ErrorCode:    ErrInvalidParams,
            ErrorMessage: "参数错误",
        })
    }

    // Service 层内部设置敏感字段
    user, err := h.service.Create(c.Request().Context(), &req)
    // ...
}

// Service 层
func (s *Service) Create(ctx context.Context, req *CreateRequest) (*User, error) {
    user := &User{
        Name:     req.Name,
        Email:    req.Email,
        Password: hashedPassword,
        Role:     "user",      // 内部设置,不从外部接收
        IsAdmin:  false,       // 内部设置,不从外部接收
    }
    // ...
}

禁止的做法

  • 禁止 在响应中返回密码
  • 禁止 在 URL 中传递 Token
  • 避免 Token 过期时间过长
go
// ❌ 错误 - 响应包含密码
type UserResponse struct {
    ID       int64  `json:"id"`
    Name     string `json:"name"`
    Password string `json:"password"`  // 禁止
}

// ✅ 正确 - 使用 json:"-" 排除密码
type User struct {
    ID       int64  `json:"id"`
    Name     string `json:"name"`
    Password string `json:"-"`  // 不序列化到 JSON
}

// ❌ 错误 - URL 传递 Token
GET /api/v1/resource?token=xxx

// ✅ 正确 - Header 传递 Token
Authorization: Bearer xxx

相关文档