Web 平台開發日記 - 第二章:認證與權限系統實戰
核心內容: JWT 認證、Casbin RBAC 權限控制、前後端集成\
技術棧: Go + Gin + JWT + Casbin + Vue 3 + Pinia
📋 目錄
- 目標
- 系統架構設計
- JWT 認證實現
- Casbin RBAC 實現
- 響應碼統一處理
- 前端集成
- 測試驗證
- 項目實踐
🎯 目標
- [x] JWT Token 生成與驗證
- [x] 登錄/登出/Token刷新 API
- [x] JWT 中間件
- [x] Casbin RBAC 中間件
- [x] 用户服務層(User Service)
- [x] 用户管理 API
- [x] 前端登錄集成
- [x] HTTP 攔截器響應碼統一處理
- 完整的認證授權系統 - 支持登錄、登出、Token 刷新
- RBAC 權限控制 - 基於 Casbin 的角色訪問控制
- 前後端集成 - 統一的響應格式和錯誤處理
🏗️ 系統架構設計
認證授權架構圖
┌─────────────────────────────────────────────────────────────┐
│ 客户端層 │
│ HTTP 攔截器 │
│ ┌─────────────┴─────────────┐ │
│ │ • 自動添加 Token │ │
│ │ • Token 過期自動刷新 │ │
│ │ • 統一響應碼處理 │ │
│ │ • 錯誤統一提示 │ │
│ └─────────────┬─────────────┘ │
└───────────────────────┼───────────────────────────────────────┘
│ HTTP/JSON
▼
┌─────────────────────────────────────────────────────────────┐
│ 後端 API 層 │
│ ┌────────────┬────────────┬────────────┬────────────┐ │
│ │ 登錄接口 │ 登出接口 │ 刷新接口 │ 用户接口 │ │
│ │ /api/login │ /api/logout│/api/refresh│/api/user/* │ │
│ └─────┬──────┴─────┬──────┴─────┬──────┴─────┬──────┘ │
└────────┼────────────┼────────────┼────────────┼──────────────┘
│ │ │ │
└────────────┴────────────┴────────────┘
▼
┌─────────────────────────────────────────────────────────────┐
│ 中間件層 │
│ ┌──────────────────────┐ ┌──────────────────────┐ │
│ │ JWT 中間件 │ │ Casbin RBAC 中間件 │ │
│ │ • 驗證 Token 有效性 │ │ • 檢查用户角色 │ │
│ │ • 解析用户信息 │ │ • 驗證資源權限 │ │
│ │ • 注入上下文 │ │ • 動態權限加載 │ │
│ └──────────┬───────────┘ └──────────┬───────────┘ │
└─────────────┼──────────────────────────┼─────────────────────┘
│ │
└──────────┬───────────────┘
▼
┌─────────────────────────────────────────────────────────────┐
│ 服務層 (Service) │
│ UserService: GetUser, UpdateUser, AssignRoles, ... │
└─────────────────────┬───────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────┐
│ 數據訪問層 (GORM) │
│ User | Role | UserRole | Permission | Casbin Policy │
└─────────────────────┬───────────────────────────────────────┘
▼
┌────────────────┐
│ MySQL 數據庫 │
└────────────────┘
認證流程
用户 → 提交登錄 → 後端驗證 → 生成 JWT Token → 存儲 Session
↓
返回 Token + User
↓
前端存儲(Cookie + LocalStorage)
↓
訪問受保護資源 → JWT中間件驗證 → Casbin權限驗證 → 業務處理
權限控制模型
Casbin RBAC (Role-Based Access Control) 模型:
Subject (主體): user:1, user:2, ...
↓
Role (角色): role:admin, role:user
↓
Object (資源): /api/users, /api/roles, ...
↓
Action (操作): GET, POST, PUT, DELETE
示例:
p, role:admin, /api/users, GET # 管理員可以查看用户
g, user:1, role:admin # 用户1是管理員
🔐 JWT 認證實現
JWT Token 結構
// server/utils/jwt.go
type JWTClaims struct {
UserID uint `json:"userId"`
Username string `json:"username"`
RoleIDs []uint `json:"roleIds"`
jwt.RegisteredClaims
}
Token 組成:
Header.Payload.Signature
JWT 生成
func GenerateToken(userID uint, username string, roleIDs []uint) (string, error) {
expiresTime := time.Now().Add(time.Duration(global.Cfg.JWT.ExpiresTime) * time.Second)
claims := &JWTClaims{
UserID: userID,
Username: username,
RoleIDs: roleIDs,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(expiresTime),
IssuedAt: jwt.NewNumericDate(time.Now()),
Issuer: "enterprise-web-platform",
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(global.Cfg.JWT.SigningKey))
}
關鍵參數:
ExpiresAt: Token 過期時間(默認 7 天)SigningKey: 密鑰(從配置文件讀取)SigningMethod: HS256 算法
登錄接口實現
// server/api/v1/auth/login.go
func Login(c *gin.Context) {
var req LoginRequest
if err := c.ShouldBindJSON(&req); err != nil {
respondError(c, http.StatusBadRequest, "Invalid request")
return
}
// 驗證用户憑證
user, err := authenticateUser(req.Username, req.Password)
if err != nil {
respondError(c, http.StatusUnauthorized, err.Error())
return
}
// 生成 JWT Token
token, expiresAt, err := generateUserToken(&user)
if err != nil {
respondError(c, http.StatusInternalServerError, "Failed to generate token")
return
}
// 存儲 Session
storeSession(user.ID, user.Username, token)
// 返回響應
respondLoginSuccess(c, token, expiresAt, &user)
}
認證流程:
- 驗證用户名密碼(bcrypt)
- 生成 JWT Token
- 存儲 Session 到 Redis
- 返回 Token 和用户信息
Token 刷新機制
// server/api/v1/auth/refresh.go
func RefreshToken(c *gin.Context) {
userID, _ := middleware.GetUserID(c)
username, _ := middleware.GetUsername(c)
roleIDs, _ := middleware.GetRoleIDs(c)
// 檢查是否在刷新窗口期內
claims, _ := c.Get("claims")
jwtClaims := claims.(*utils.JWTClaims)
if !isTokenEligibleForRefresh(jwtClaims) {
respondError(c, http.StatusBadRequest, "Token not eligible for refresh")
return
}
// 生成新 Token
newToken, err := utils.GenerateToken(userID, username, roleIDs)
if err != nil {
respondError(c, http.StatusInternalServerError, "Failed to generate token")
return
}
expiresAt := time.Now().Add(time.Duration(global.Cfg.JWT.ExpiresTime) * time.Second).Unix()
c.JSON(http.StatusOK, gin.H{
"code": 200,
"data": RefreshTokenResponse{Token: newToken, ExpiresAt: expiresAt},
})
}
刷新時間窗口:
Token 創建 可刷新窗口 過期
│ │ │
│◄──── 6 天 ───────────►│◄──── 1 天 ─────►│
│ │ │
🛡️ Casbin RBAC 實現
Casbin 模型定義
# server/config/rbac_model.conf
[request_definition]
r = sub, obj, act
[policy_definition]
p = sub, obj, act
[role_definition]
g = _, _
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act
初始化權限策略
// server/initialize/data.go
func initializeCasbinPolicies(adminUserID uint) {
// 定義管理員權限
adminPolicies := [][]string{
{"role:admin", "/api/users", "GET"},
{"role:admin", "/api/users", "POST"},
{"role:admin", "/api/user/:id", "PUT"},
{"role:admin", "/api/user/:id", "DELETE"},
}
// 添加策略
for _, policy := range adminPolicies {
global.Enforcer.AddPolicy(policy)
}
// 關聯用户到角色
adminSubject := fmt.Sprintf("user:%d", adminUserID)
global.Enforcer.AddGroupingPolicy(adminSubject, "role:admin")
global.Enforcer.SavePolicy()
}
Casbin 中間件
// server/middleware/casbin.go
func CasbinRBAC() gin.HandlerFunc {
return func(c *gin.Context) {
// 獲取用户 ID
userID, exists := GetUserID(c)
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"code": 401, "message": "Unauthorized"})
c.Abort()
return
}
// 構建主體標識
subject := fmt.Sprintf("user:%d", userID)
object := c.Request.URL.Path
action := c.Request.Method
// 檢查權限
allowed, err := global.Enforcer.Enforce(subject, object, action)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "Permission check failed"})
c.Abort()
return
}
if !allowed {
c.JSON(http.StatusForbidden, gin.H{"code": 403, "message": "Permission denied"})
c.Abort()
return
}
c.Next()
}
}
權限檢查流程:
1. 輸入: (user:1, /api/users, GET)
2. 查詢: user:1 → role:admin
3. 匹配: role:admin + /api/users + GET → allow
4. 結果: ✅ 放行
UserService 使用 Casbin
// server/service/user_service.go
func (s *UserService) AssignRoles(userID uint, roleIDs []uint) error {
// 查詢角色
var roles []model.Role
global.DB.Where("id IN ?", roleIDs).Find(&roles)
// 更新用户角色
var user model.User
global.DB.Preload("Roles").First(&user, userID)
global.DB.Model(&user).Association("Roles").Replace(roles)
// 同步 Casbin 策略
subject := fmt.Sprintf("user:%d", userID)
global.Enforcer.DeleteRolesForUser(subject)
for _, role := range roles {
roleSubject := fmt.Sprintf("role:%s", role.Name)
global.Enforcer.AddGroupingPolicy(subject, roleSubject)
}
global.Enforcer.SavePolicy()
return nil
}
📡 響應碼統一處理
HTTP 標準狀態碼規範
遵循 RFC 7231 規範:
- 2xx 成功: 200 OK, 201 Created, 202 Accepted, 204 No Content
- 4xx 客户端錯誤: 400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found
- 5xx 服務器錯誤: 500 Internal Server Error, 503 Service Unavailable
前端響應碼工具
// web/src/utils/http/response-code.ts
// 判斷成功響應 (2xx)
export function isSuccessCode(code: number): boolean {
return code >= 200 && code < 300;
}
export const ResponseCode = {
SUCCESS: 200,
CREATED: 201,
UNAUTHORIZED: 401,
FORBIDDEN: 403,
INVALID_CREDENTIALS: 40001,
ACCOUNT_DISABLED: 40002,
TOKEN_EXPIRED: 40005,
INTERNAL_ERROR: 500,
} as const;
HTTP 攔截器增強
// web/src/utils/http/index.ts
private httpInterceptorsResponse(): void {
instance.interceptors.response.use(
(response) => {
const res = response.data;
// 統一處理業務響應碼
if (res && "code" in res) {
if (!isSuccessCode(res.code)) {
this.handleBusinessError(res.code, res.message);
return Promise.reject(new Error(res.message));
}
}
return response.data;
},
(error) => {
if (error.response) {
this.handleHttpError(error.response.status);
}
return Promise.reject(error);
}
);
}
private handleBusinessError(code: number, msg?: string): void {
switch (code) {
case ResponseCode.TOKEN_EXPIRED:
message("登錄已過期,請重新登錄");
useUserStoreHook().logOutLocal();
break;
case ResponseCode.ACCOUNT_DISABLED:
message("賬號已被禁用,請聯繫管理員");
break;
case ResponseCode.FORBIDDEN:
message("您沒有權限執行此操作");
break;
default:
message(msg || "操作失敗");
}
}
// web/src/views/login/index.vue
loginByUsername(data)
.then(res => {
// 攔截器已處理錯誤,這裏只會收到成功響應
return initRouter().then(() => {
router.push(getTopMenu(true).path);
message("登錄成功", { type: "success" });
});
})
.catch(err => {
console.error("Login error:", err);
});
🖥️ 前端集成
Vue Store 集成
// web/src/store/modules/user.ts
export const useUserStore = defineStore("pure-user", {
actions: {
async loginByUsername(data) {
return new Promise((resolve, reject) => {
getLogin(data)
.then(response => {
if (response?.data) {
const { token, expiresAt, user } = response.data;
const tokenData = {
accessToken: token,
expires: new Date(expiresAt * 1000),
refreshToken: token,
id: user.id,
username: user.username,
roles: user.roles,
// ...
};
setToken(tokenData);
resolve(response);
}
})
.catch(reject);
});
},
}
});
Token 存儲
// web/src/utils/auth.ts
export function setToken(data: DataInfo<Date>) {
const { accessToken, refreshToken, expires } = data;
// 1. 存儲到 Cookie
Cookies.set(TokenKey, JSON.stringify({ accessToken, expires, refreshToken }), {
expires: (expires - Date.now()) / 86400000
});
// 2. 存儲用户信息到 LocalStorage
useUserStoreHook().SET_USERNAME(data.username);
useUserStoreHook().SET_ROLES(data.roles);
storageLocal().setItem(userKey, {
id: data.id,
username: data.username,
roles: data.roles,
// ...
});
}
HTTP 請求攔截器
// web/src/utils/http/index.ts
private httpInterceptorsRequest(): void {
instance.interceptors.request.use(async (config) => {
const whiteList = ["/refresh-token", "/login"];
if (whiteList.some(url => config.url.endsWith(url))) {
return config;
}
const data = getToken();
if (data) {
const expired = parseInt(data.expires) - Date.now() <= 0;
if (expired) {
// Token 過期,觸發刷新
await useUserStoreHook().handRefreshToken({
refreshToken: data.refreshToken
});
}
config.headers["Authorization"] = formatToken(data.accessToken);
}
return config;
});
}
🧪 測試驗證
登錄測試
# 正常登錄
curl -X POST http://localhost:8888/api/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"admin123"}'
# 預期響應: 200 + Token + User
JWT 中間件測試
# 有效 Token 訪問
curl http://localhost:8888/api/user/info \
-H "Authorization: Bearer <token>"
# 預期響應: 200 + 用户信息
Casbin 權限測試
# 管理員訪問用户列表
curl http://localhost:8888/api/users \
-H "Authorization: Bearer <admin_token>"
# 預期: 200 成功
# 普通用户訪問
curl http://localhost:8888/api/users \
-H "Authorization: Bearer <user_token>"
# 預期: 403 Permission denied
前端集成測試
# 1. 啓動開發環境
./start_dev.sh
# 2. 訪問前端
http://localhost:8848
# 3. 測試登錄
用户名: admin
密碼: admin123
# 驗證:
✅ 登錄成功後自動跳轉
✅ Cookie 中有 authorized-token
✅ LocalStorage 中有 user-info
✅ Token 快過期時自動刷新
🎯 項目實踐
1. 安全實踐
密碼存儲:
// 使用 bcrypt 哈希
hashedPassword, _ := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
Token 簽名:
// 使用強密鑰(至少32字符)
SigningKey: "your-super-secret-key-with-at-least-32-characters"
Session 管理:
// 設置 TTL
ttl := time.Duration(global.Cfg.JWT.ExpiresTime) * time.Second
global.Redis.Set(ctx, sessionKey, data, ttl)
2. 中間件順序
// 正確的順序
router.Use(middleware.Logger()) // 1. 日誌
router.Use(middleware.Recovery()) // 2. 恢復
router.Use(middleware.CORS()) // 3. 跨域
router.Use(middleware.JWT()) // 4. 認證
router.Use(middleware.CasbinRBAC()) // 5. 授權
3. 路由配置
// 公開路由(無需認證)
publicGroup := router.Group("/api")
{
publicGroup.POST("/login", auth.Login)
}
// 需要認證的路由
privateGroup := router.Group("/api")
privateGroup.Use(middleware.JWT())
{
privateGroup.POST("/logout", auth.Logout)
privateGroup.GET("/user/info", user.GetUserInfo)
}
// 需要認證+授權的路由
adminGroup := router.Group("/api")
adminGroup.Use(middleware.JWT())
adminGroup.Use(middleware.CasbinRBAC())
{
adminGroup.GET("/users", user.ListUsers)
adminGroup.POST("/users", user.CreateUser)
}
📚 相關文檔
技術文檔
- JWT 官方文檔 - JSON Web Token 標準
- Casbin 官方文檔 - 權限管理框架
- Gin 官方文檔 - Go Web 框架
- Vue 3 官方文檔 - 漸進式 JavaScript 框架
- Pinia 官方文檔 - Vue 狀態管理庫
- bcrypt 文檔 - 密碼哈希算法