认证授权规范
核心原则
认证授权必须使用 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