v3
veypi 7 months ago
parent a3923ebcdb
commit 792f3ba707

@ -0,0 +1,50 @@
package main
import (
"log"
"github.com/veypi/vbase/internal/api"
"github.com/veypi/vbase/internal/cache"
"github.com/veypi/vbase/internal/config"
"github.com/veypi/vbase/internal/model"
"github.com/veypi/vigo"
)
func main() {
// 初始化数据库
if err := model.InitDB(); err != nil {
log.Fatalf("Failed to init database: %v", err)
}
// 自动迁移
if err := model.AutoMigrate(); err != nil {
log.Fatalf("Failed to migrate database: %v", err)
}
// 初始化系统数据
if err := model.InitSystemData(); err != nil {
log.Fatalf("Failed to init system data: %v", err)
}
// 初始化Redis
if err := cache.Init(); err != nil {
log.Printf("Warning: Failed to init redis: %v", err)
} else {
log.Println("Redis connected")
}
// 创建路由
router := api.NewRouter()
// 创建服务器
server, err := vigo.New(vigo.WithHost(config.C.Server.Host), vigo.WithPort(config.C.Server.Port))
if err != nil {
log.Fatalf("Failed to create server: %v", err)
}
server.Router().Extend("api", router)
log.Printf("Server starting on %s:%d", config.C.Server.Host, config.C.Server.Port)
if err := server.Run(); err != nil {
log.Fatalf("Server error: %v", err)
}
}

@ -0,0 +1,513 @@
package auth
import (
"net/http"
"time"
"github.com/veypi/vbase/internal/cache"
"github.com/veypi/vbase/internal/config"
"github.com/veypi/vbase/internal/model"
"github.com/veypi/vbase/internal/pkg/crypto"
"github.com/veypi/vbase/internal/pkg/jwt"
"github.com/veypi/vigo"
)
// LoginRequest 登录请求
type LoginRequest struct {
Username string `json:"username" src:"json" desc:"用户名/邮箱/手机号"`
Password string `json:"password" src:"json" desc:"密码"`
CaptchaID string `json:"captcha_id,omitempty" src:"json" desc:"验证码ID"`
CaptchaCode string `json:"captcha_code,omitempty" src:"json" desc:"验证码"`
Remember bool `json:"remember,omitempty" src:"json" desc:"记住登录"`
}
// RegisterRequest 注册请求
type RegisterRequest struct {
Username string `json:"username" src:"json" desc:"用户名"`
Password string `json:"password" src:"json" desc:"密码"`
Email string `json:"email,omitempty" src:"json" desc:"邮箱"`
Phone string `json:"phone,omitempty" src:"json" desc:"手机号"`
Nickname string `json:"nickname,omitempty" src:"json" desc:"昵称"`
}
// AuthResponse 认证响应
type AuthResponse struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
TokenType string `json:"token_type"`
ExpiresIn int `json:"expires_in"`
User *UserInfo `json:"user"`
}
// UserInfo 用户信息
type UserInfo struct {
ID string `json:"id"`
Username string `json:"username"`
Nickname string `json:"nickname"`
Email string `json:"email"`
Avatar string `json:"avatar"`
}
// Login 用户登录
func Login(x *vigo.X, req *LoginRequest) (*AuthResponse, error) {
// 查找用户
var user model.User
query := model.DB.Where("username = ? OR email = ? OR phone = ?", req.Username, req.Username, req.Username)
if err := query.First(&user).Error; err != nil {
return nil, vigo.ErrNotAuthorized.WithString("invalid username or password")
}
// 检查用户状态
if user.Status != model.UserStatusActive {
return nil, vigo.ErrForbidden.WithString("user is disabled")
}
// 验证密码
if !crypto.VerifyPassword(req.Password, user.Password) {
return nil, vigo.ErrNotAuthorized.WithString("invalid username or password")
}
// 获取用户的组织信息
orgs, err := getUserOrgs(user.ID)
if err != nil {
return nil, vigo.ErrInternalServer.WithError(err)
}
// 生成token
orgClaims := make([]jwt.OrgClaim, 0, len(orgs))
for _, org := range orgs {
orgClaims = append(orgClaims, jwt.OrgClaim{
OrgID: org.OrgID,
Code: org.Code,
Name: org.Name,
Roles: org.Roles,
Status: org.Status,
})
}
tokenPair, err := jwt.GenerateTokenPair(
user.ID,
user.Username,
user.Nickname,
user.Avatar,
user.Email,
orgClaims,
)
if err != nil {
return nil, vigo.ErrInternalServer.WithError(err)
}
// 保存session
session := &model.Session{
UserID: user.ID,
TokenID: getJTI(tokenPair.AccessToken),
Type: "access",
DeviceInfo: x.Request.UserAgent(),
IP: x.GetRemoteIP(),
ExpiresAt: time.Now().Add(config.C.JWT.AccessExpiry),
}
model.DB.Create(session)
// 更新最后登录时间
now := time.Now()
model.DB.Model(&user).Update("last_login_at", now)
return &AuthResponse{
AccessToken: tokenPair.AccessToken,
RefreshToken: tokenPair.RefreshToken,
TokenType: tokenPair.TokenType,
ExpiresIn: tokenPair.ExpiresIn,
User: &UserInfo{
ID: user.ID,
Username: user.Username,
Nickname: user.Nickname,
Email: user.Email,
Avatar: user.Avatar,
},
}, nil
}
// Register 用户注册
func Register(x *vigo.X, req *RegisterRequest) (*AuthResponse, error) {
// 检查用户名是否已存在
var count int64
model.DB.Model(&model.User{}).Where("username = ?", req.Username).Count(&count)
if count > 0 {
return nil, vigo.ErrArgInvalid.WithString("username already exists")
}
// 检查邮箱是否已存在
if req.Email != "" {
model.DB.Model(&model.User{}).Where("email = ?", req.Email).Count(&count)
if count > 0 {
return nil, vigo.ErrArgInvalid.WithString("email already exists")
}
}
// 哈希密码
hashedPassword, err := crypto.HashPassword(req.Password, config.C.Security.BcryptCost)
if err != nil {
return nil, vigo.ErrInternalServer.WithError(err)
}
// 创建用户
user := &model.User{
Username: req.Username,
Password: hashedPassword,
Email: req.Email,
Phone: req.Phone,
Nickname: req.Nickname,
Status: model.UserStatusActive,
}
if user.Nickname == "" {
user.Nickname = user.Username
}
if err := model.DB.Create(user).Error; err != nil {
return nil, vigo.ErrInternalServer.WithError(err)
}
// 生成token
tokenPair, err := jwt.GenerateTokenPair(
user.ID,
user.Username,
user.Nickname,
user.Avatar,
user.Email,
nil, // 新用户无组织
)
if err != nil {
return nil, vigo.ErrInternalServer.WithError(err)
}
// 保存session
session := &model.Session{
UserID: user.ID,
TokenID: getJTI(tokenPair.AccessToken),
Type: "access",
DeviceInfo: x.Request.UserAgent(),
IP: x.GetRemoteIP(),
ExpiresAt: time.Now().Add(config.C.JWT.AccessExpiry),
}
model.DB.Create(session)
return &AuthResponse{
AccessToken: tokenPair.AccessToken,
RefreshToken: tokenPair.RefreshToken,
TokenType: tokenPair.TokenType,
ExpiresIn: tokenPair.ExpiresIn,
User: &UserInfo{
ID: user.ID,
Username: user.Username,
Nickname: user.Nickname,
Email: user.Email,
Avatar: user.Avatar,
},
}, nil
}
// RefreshRequest 刷新请求
type RefreshRequest struct {
RefreshToken string `json:"refresh_token" src:"json" desc:"刷新令牌"`
}
// Refresh 刷新Token
func Refresh(x *vigo.X, req *RefreshRequest) (*AuthResponse, error) {
// 解析refresh token
claims, err := jwt.ParseToken(req.RefreshToken)
if err != nil {
if err == jwt.ErrExpiredToken {
return nil, vigo.ErrNotAuthorized.WithString("refresh token expired")
}
return nil, vigo.ErrNotAuthorized.WithString("invalid refresh token")
}
if !jwt.IsRefreshToken(claims) {
return nil, vigo.ErrNotAuthorized.WithString("invalid token type")
}
// 查找用户
var user model.User
if err := model.DB.First(&user, "id = ?", claims.UserID).Error; err != nil {
return nil, vigo.ErrNotAuthorized.WithString("user not found")
}
if user.Status != model.UserStatusActive {
return nil, vigo.ErrForbidden.WithString("user is disabled")
}
// 获取用户的组织信息
orgs, err := getUserOrgs(user.ID)
if err != nil {
return nil, vigo.ErrInternalServer.WithError(err)
}
orgClaims := make([]jwt.OrgClaim, 0, len(orgs))
for _, org := range orgs {
orgClaims = append(orgClaims, jwt.OrgClaim{
OrgID: org.OrgID,
Code: org.Code,
Name: org.Name,
Roles: org.Roles,
Status: org.Status,
})
}
// 生成新token
tokenPair, err := jwt.GenerateTokenPair(
user.ID,
user.Username,
user.Nickname,
user.Avatar,
user.Email,
orgClaims,
)
if err != nil {
return nil, vigo.ErrInternalServer.WithError(err)
}
// 保存新session
session := &model.Session{
UserID: user.ID,
TokenID: getJTI(tokenPair.AccessToken),
Type: "access",
DeviceInfo: x.Request.UserAgent(),
IP: x.GetRemoteIP(),
ExpiresAt: time.Now().Add(config.C.JWT.AccessExpiry),
}
model.DB.Create(session)
return &AuthResponse{
AccessToken: tokenPair.AccessToken,
RefreshToken: tokenPair.RefreshToken,
TokenType: tokenPair.TokenType,
ExpiresIn: tokenPair.ExpiresIn,
User: &UserInfo{
ID: user.ID,
Username: user.Username,
Nickname: user.Nickname,
Email: user.Email,
Avatar: user.Avatar,
},
}, nil
}
// Logout 用户登出
func Logout(x *vigo.X) error {
tokenString := extractTokenFromRequest(x.Request)
if tokenString == "" {
return nil
}
jti, err := jwt.GetJTI(tokenString)
if err != nil {
return nil
}
// 加入黑名单
expiration, _ := jwt.GetExpiration(tokenString)
if cache.IsEnabled() {
ttl := time.Until(expiration)
if ttl > 0 {
cache.BlacklistToken(jti, ttl)
}
}
// 标记session为撤销
model.DB.Model(&model.Session{}).Where("token_id = ?", jti).Updates(map[string]interface{}{
"revoked": true,
"revoked_at": time.Now(),
})
return nil
}
// Me 获取当前用户信息
func Me(x *vigo.X) (*UserInfo, error) {
userID := getCurrentUserID(x)
if userID == "" {
return nil, vigo.ErrNotAuthorized
}
var user model.User
if err := model.DB.First(&user, "id = ?", userID).Error; err != nil {
return nil, vigo.ErrNotFound
}
return &UserInfo{
ID: user.ID,
Username: user.Username,
Nickname: user.Nickname,
Email: user.Email,
Avatar: user.Avatar,
}, nil
}
// UpdateMeRequest 更新自己请求
type UpdateMeRequest struct {
Nickname *string `json:"nickname,omitempty" src:"json" desc:"昵称"`
Avatar *string `json:"avatar,omitempty" src:"json" desc:"头像"`
Email *string `json:"email,omitempty" src:"json" desc:"邮箱"`
}
// UpdateMe 更新当前用户信息
func UpdateMe(x *vigo.X, req *UpdateMeRequest) (*UserInfo, error) {
userID := getCurrentUserID(x)
if userID == "" {
return nil, vigo.ErrNotAuthorized
}
updates := make(map[string]interface{})
if req.Nickname != nil {
updates["nickname"] = *req.Nickname
}
if req.Avatar != nil {
updates["avatar"] = *req.Avatar
}
if req.Email != nil {
// 检查邮箱是否被其他用户使用
var count int64
model.DB.Model(&model.User{}).Where("email = ? AND id != ?", *req.Email, userID).Count(&count)
if count > 0 {
return nil, vigo.ErrArgInvalid.WithString("email already exists")
}
updates["email"] = *req.Email
}
if err := model.DB.Model(&model.User{}).Where("id = ?", userID).Updates(updates).Error; err != nil {
return nil, vigo.ErrInternalServer.WithError(err)
}
return Me(x)
}
// ChangePasswordRequest 修改密码请求
type ChangePasswordRequest struct {
OldPassword string `json:"old_password" src:"json" desc:"旧密码"`
NewPassword string `json:"new_password" src:"json" desc:"新密码"`
}
// ChangePassword 修改密码
func ChangePassword(x *vigo.X, req *ChangePasswordRequest) error {
userID := getCurrentUserID(x)
if userID == "" {
return vigo.ErrNotAuthorized
}
var user model.User
if err := model.DB.First(&user, "id = ?", userID).Error; err != nil {
return vigo.ErrNotFound
}
// 验证旧密码
if !crypto.VerifyPassword(req.OldPassword, user.Password) {
return vigo.ErrArgInvalid.WithString("old password is incorrect")
}
// 哈希新密码
hashedPassword, err := crypto.HashPassword(req.NewPassword, config.C.Security.BcryptCost)
if err != nil {
return vigo.ErrInternalServer.WithError(err)
}
// 更新密码
if err := model.DB.Model(&user).Update("password", hashedPassword).Error; err != nil {
return vigo.ErrInternalServer.WithError(err)
}
return nil
}
// helper functions
func getCurrentUserID(x *vigo.X) string {
if uid, ok := x.Get("current_user").(string); ok {
return uid
}
return ""
}
func extractToken(r *vigo.X) string {
// 从Header获取
auth := r.Request.Header.Get("Authorization")
if auth != "" {
parts := make([]string, 0)
for _, p := range []string{auth} {
parts = append(parts, p)
}
// Simple check
if len(auth) > 7 && auth[:7] == "Bearer " {
return auth[7:]
}
}
// 从Query获取
return r.Request.URL.Query().Get("access_token")
}
func extractTokenFromRequest(r *http.Request) string {
// 从Header获取
auth := r.Header.Get("Authorization")
if auth != "" {
if len(auth) > 7 && auth[:7] == "Bearer " {
return auth[7:]
}
}
// 从Query获取
return r.URL.Query().Get("access_token")
}
func getJTI(token string) string {
jti, _ := jwt.GetJTI(token)
return jti
}
type userOrgInfo struct {
OrgID string
Code string
Name string
Roles []string
Status int
}
func getUserOrgs(userID string) ([]userOrgInfo, error) {
var members []model.OrgMember
if err := model.DB.Where("user_id = ? AND status = ?", userID, model.MemberStatusActive).Find(&members).Error; err != nil {
return nil, err
}
if len(members) == 0 {
return []userOrgInfo{}, nil
}
result := make([]userOrgInfo, 0, len(members))
for _, m := range members {
var org model.Org
if err := model.DB.First(&org, "id = ?", m.OrgID).Error; err != nil {
continue
}
// 解析角色ID
roles := parseRoles(m.RoleIDs)
result = append(result, userOrgInfo{
OrgID: m.OrgID,
Code: org.Code,
Name: org.Name,
Roles: roles,
Status: m.Status,
})
}
return result, nil
}
func parseRoles(roleIDs string) []string {
if roleIDs == "" {
return []string{}
}
// 简单解析,实际可能需要更复杂的逻辑
return []string{}
}

@ -0,0 +1,141 @@
package middleware
import (
"net/http"
"strings"
"github.com/veypi/vbase/internal/cache"
"github.com/veypi/vbase/internal/pkg/jwt"
"github.com/veypi/vigo"
)
const (
ContextKeyUser = "current_user"
ContextKeyClaims = "jwt_claims"
ContextKeyOrgID = "org_id"
ContextKeyIsAdmin = "is_admin"
)
// AuthRequired 认证中间件
func AuthRequired(skips ...string) func(*vigo.X) (any, error) {
skipMap := make(map[string]bool)
for _, s := range skips {
skipMap[s] = true
}
return func(x *vigo.X) (any, error) {
// 检查是否跳过
if skipMap[x.Request.URL.Path] {
x.Next()
return nil, nil
}
// 提取token
tokenString := extractToken(x.Request)
if tokenString == "" {
return nil, vigo.ErrNotAuthorized.WithString("missing token")
}
// 解析token
claims, err := jwt.ParseToken(tokenString)
if err != nil {
if err == jwt.ErrExpiredToken {
return nil, vigo.ErrNotAuthorized.WithString("token expired")
}
return nil, vigo.ErrNotAuthorized.WithString("invalid token")
}
// 必须是access token
if !jwt.IsAccessToken(claims) {
return nil, vigo.ErrNotAuthorized.WithString("invalid token type")
}
// 检查黑名单
if cache.IsEnabled() {
isRevoked, err := cache.IsTokenBlacklisted(claims.ID)
if err != nil {
return nil, vigo.ErrInternalServer.WithError(err)
}
if isRevoked {
return nil, vigo.ErrNotAuthorized.WithString("token revoked")
}
}
// 设置上下文
x.Set(ContextKeyClaims, claims)
x.Set(ContextKeyUser, claims.UserID)
x.Next()
return nil, nil
}
}
// OptionalAuth 可选认证
func OptionalAuth() func(*vigo.X) (any, error) {
return func(x *vigo.X) (any, error) {
tokenString := extractToken(x.Request)
if tokenString != "" {
claims, err := jwt.ParseToken(tokenString)
if err == nil && jwt.IsAccessToken(claims) {
x.Set(ContextKeyClaims, claims)
x.Set(ContextKeyUser, claims.UserID)
}
}
x.Next()
return nil, nil
}
}
// OrgContext 组织上下文中间件
func OrgContext() func(*vigo.X) (any, error) {
return func(x *vigo.X) (any, error) {
orgID := x.Request.Header.Get("X-Org-ID")
if orgID == "" {
orgID = x.Request.URL.Query().Get("org_id")
}
if orgID != "" {
x.Set(ContextKeyOrgID, orgID)
}
x.Next()
return nil, nil
}
}
// extractToken 从请求中提取token
func extractToken(r *http.Request) string {
// 从Header获取
auth := r.Header.Get("Authorization")
if auth != "" {
parts := strings.SplitN(auth, " ", 2)
if len(parts) == 2 && strings.EqualFold(parts[0], "Bearer") {
return parts[1]
}
}
// 从Query获取
return r.URL.Query().Get("access_token")
}
// CurrentUser 获取当前用户ID
func CurrentUser(x *vigo.X) string {
if uid, ok := x.Get(ContextKeyUser).(string); ok {
return uid
}
return ""
}
// CurrentClaims 获取当前JWT Claims
func CurrentClaims(x *vigo.X) *jwt.Claims {
if claims, ok := x.Get(ContextKeyClaims).(*jwt.Claims); ok {
return claims
}
return nil
}
// CurrentOrgID 获取当前组织ID
func CurrentOrgID(x *vigo.X) string {
if orgID, ok := x.Get(ContextKeyOrgID).(string); ok {
return orgID
}
return ""
}

@ -0,0 +1,111 @@
package middleware
import (
"fmt"
"net/http"
"time"
"github.com/veypi/vbase/internal/cache"
"github.com/veypi/vigo"
)
// RateLimiter 限流中间件
func RateLimiter(maxRequests int, window time.Duration) func(*vigo.X) (any, error) {
return func(x *vigo.X) (any, error) {
if !cache.IsEnabled() {
x.Next()
return nil, nil
}
// 使用IP+路径作为标识
identifier := x.GetRemoteIP()
path := x.Request.URL.Path
count, err := cache.IncrRateLimit(identifier, path, window)
if err != nil {
x.Next() // 缓存失败时放行
return nil, nil
}
if count > int64(maxRequests) {
x.ResponseWriter().Header().Set("Retry-After", fmt.Sprintf("%d", int(window.Seconds())))
return nil, vigo.NewError("rate limit exceeded").WithCode(http.StatusTooManyRequests)
}
x.Next()
return nil, nil
}
}
// RateLimiterByUser 基于用户的限流
func RateLimiterByUser(maxRequests int, window time.Duration) func(*vigo.X) (any, error) {
return func(x *vigo.X) (any, error) {
if !cache.IsEnabled() {
x.Next()
return nil, nil
}
userID := CurrentUser(x)
if userID == "" {
// 未登录用户使用IP限流
_, err := RateLimiter(maxRequests, window)(x)
return nil, err
}
path := x.Request.URL.Path
count, err := cache.IncrRateLimit("user:"+userID, path, window)
if err != nil {
x.Next()
return nil, nil
}
if count > int64(maxRequests) {
x.ResponseWriter().Header().Set("Retry-After", fmt.Sprintf("%d", int(window.Seconds())))
return nil, vigo.NewError("rate limit exceeded").WithCode(http.StatusTooManyRequests)
}
x.Next()
return nil, nil
}
}
// LoginRateLimit 登录限流(更严格)
func LoginRateLimit() func(*vigo.X) (any, error) {
return func(x *vigo.X) (any, error) {
if !cache.IsEnabled() {
x.Next()
return nil, nil
}
identifier := x.GetRemoteIP()
key := "login_attempt:" + identifier
count, _ := cache.Incr(key)
if count == 1 {
cache.Expire(key, 15*time.Minute)
}
// 5分钟内超过5次尝试需要验证码
if count >= 5 {
x.Set("require_captcha", true)
}
// 超过10次直接拒绝
if count >= 10 {
return nil, vigo.NewError("too many login attempts, please try again later").WithCode(http.StatusTooManyRequests)
}
x.Next()
return nil, nil
}
}
// ResetLoginAttempts 重置登录尝试次数
func ResetLoginAttempts(x *vigo.X) {
if !cache.IsEnabled() {
return
}
identifier := x.GetRemoteIP()
key := "login_attempt:" + identifier
cache.Delete(key)
}

@ -0,0 +1,399 @@
package oauth
import (
"crypto/rand"
"encoding/hex"
"strings"
"github.com/veypi/vbase/internal/api/middleware"
"github.com/veypi/vbase/internal/model"
"github.com/veypi/vigo"
)
// ClientRequest 客户端请求
type ClientRequest struct {
Name string `json:"name" src:"json" desc:"应用名称"`
Description string `json:"description" src:"json" desc:"应用描述"`
RedirectURIs []string `json:"redirect_uris" src:"json" desc:"回调地址列表"`
GrantTypes []string `json:"grant_types" src:"json" desc:"授权类型"`
ResponseTypes []string `json:"response_types" src:"json" desc:"响应类型"`
AllowedScopes []string `json:"allowed_scopes" src:"json" desc:"允许的权限范围"`
TokenExpiry int `json:"token_expiry" src:"json" desc:"Token有效期(秒)"`
RefreshExpiry int `json:"refresh_expiry" src:"json" desc:"RefreshToken有效期(秒)"`
OrgID string `json:"org_id" src:"json" desc:"所属组织ID(可选)"`
}
// ClientResponse 客户端响应
type ClientResponse struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
ClientID string `json:"client_id"`
ClientSecret string `json:"client_secret,omitempty"`
RedirectURIs []string `json:"redirect_uris"`
GrantTypes []string `json:"grant_types"`
ResponseTypes []string `json:"response_types"`
AllowedScopes []string `json:"allowed_scopes"`
TokenExpiry int `json:"token_expiry"`
RefreshExpiry int `json:"refresh_expiry"`
OwnerID string `json:"owner_id"`
OrgID string `json:"org_id,omitempty"`
Status int `json:"status"`
CreatedAt string `json:"created_at"`
}
// generateClientID 生成客户端ID
func generateClientID() string {
b := make([]byte, 16)
rand.Read(b)
return hex.EncodeToString(b)
}
// generateClientSecret 生成客户端密钥
func generateClientSecret() string {
b := make([]byte, 32)
rand.Read(b)
return hex.EncodeToString(b)
}
// CreateClient 创建OAuth客户端
func CreateClient(x *vigo.X, req *ClientRequest) (*ClientResponse, error) {
userID := middleware.CurrentUser(x)
if userID == "" {
return nil, vigo.ErrNotAuthorized
}
// 默认值
grantTypes := req.GrantTypes
if len(grantTypes) == 0 {
grantTypes = []string{model.GrantTypeAuthorizationCode, model.GrantTypeRefreshToken}
}
responseTypes := req.ResponseTypes
if len(responseTypes) == 0 {
responseTypes = []string{model.ResponseTypeCode}
}
allowedScopes := req.AllowedScopes
if len(allowedScopes) == 0 {
allowedScopes = []string{model.ScopeOpenID, model.ScopeProfile, model.ScopeEmail}
}
tokenExpiry := req.TokenExpiry
if tokenExpiry == 0 {
tokenExpiry = 3600
}
refreshExpiry := req.RefreshExpiry
if refreshExpiry == 0 {
refreshExpiry = 2592000 // 30天
}
client := &model.OAuthClient{
Name: req.Name,
Description: req.Description,
ClientID: generateClientID(),
ClientSecret: generateClientSecret(),
RedirectURIs: strings.Join(req.RedirectURIs, ","),
GrantTypes: strings.Join(grantTypes, ","),
ResponseTypes: strings.Join(responseTypes, ","),
AllowedScopes: strings.Join(allowedScopes, ","),
TokenExpiry: tokenExpiry,
RefreshExpiry: refreshExpiry,
OwnerID: userID,
Status: 1,
}
if req.OrgID != "" {
// 检查用户是否是组织成员
var member model.OrgMember
if err := model.DB.Where("org_id = ? AND user_id = ? AND status = ?", req.OrgID, userID, model.MemberStatusActive).First(&member).Error; err != nil {
return nil, vigo.ErrForbidden.WithString("you are not a member of this organization")
}
client.OrgID = req.OrgID
}
if err := model.DB.Create(client).Error; err != nil {
return nil, vigo.ErrInternalServer.WithError(err)
}
return &ClientResponse{
ID: client.ID,
Name: client.Name,
Description: client.Description,
ClientID: client.ClientID,
ClientSecret: client.ClientSecret,
RedirectURIs: req.RedirectURIs,
GrantTypes: grantTypes,
ResponseTypes: responseTypes,
AllowedScopes: allowedScopes,
TokenExpiry: client.TokenExpiry,
RefreshExpiry: client.RefreshExpiry,
OwnerID: client.OwnerID,
OrgID: client.OrgID,
Status: client.Status,
CreatedAt: client.CreatedAt.Format("2006-01-02 15:04:05"),
}, nil
}
// ListClientsRequest 客户端列表请求
type ListClientsRequest struct {
Page int `json:"page" src:"query" default:"1" desc:"页码"`
PageSize int `json:"page_size" src:"query" default:"10" desc:"每页数量"`
OrgID string `json:"org_id" src:"query" desc:"组织ID筛选"`
}
// ListClientsResponse 客户端列表响应
type ListClientsResponse struct {
Items []ClientResponse `json:"items"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
TotalPages int `json:"total_pages"`
}
// ListClients 获取OAuth客户端列表
func ListClients(x *vigo.X, req *ListClientsRequest) (*ListClientsResponse, error) {
userID := middleware.CurrentUser(x)
if userID == "" {
return nil, vigo.ErrNotAuthorized
}
if req.Page < 1 {
req.Page = 1
}
if req.PageSize < 1 || req.PageSize > 100 {
req.PageSize = 10
}
query := model.DB.Model(&model.OAuthClient{}).Where("owner_id = ? OR org_id IN (SELECT org_id FROM org_members WHERE user_id = ? AND status = ?)", userID, userID, model.MemberStatusActive)
if req.OrgID != "" {
query = query.Where("org_id = ?", req.OrgID)
}
var total int64
if err := query.Count(&total).Error; err != nil {
return nil, vigo.ErrInternalServer.WithError(err)
}
var clients []model.OAuthClient
offset := (req.Page - 1) * req.PageSize
if err := query.Offset(offset).Limit(req.PageSize).Order("created_at DESC").Find(&clients).Error; err != nil {
return nil, vigo.ErrInternalServer.WithError(err)
}
items := make([]ClientResponse, 0, len(clients))
for _, c := range clients {
items = append(items, toClientResponse(&c, false))
}
totalPages := int((total + int64(req.PageSize) - 1) / int64(req.PageSize))
return &ListClientsResponse{
Items: items,
Total: total,
Page: req.Page,
PageSize: req.PageSize,
TotalPages: totalPages,
}, nil
}
// GetClientRequest 获取客户端请求
type GetClientRequest struct {
ID string `json:"id" src:"path@client_id" desc:"客户端ID"`
}
// GetClient 获取客户端详情
func GetClient(x *vigo.X, req *GetClientRequest) (*ClientResponse, error) {
userID := middleware.CurrentUser(x)
if userID == "" {
return nil, vigo.ErrNotAuthorized
}
var client model.OAuthClient
if err := model.DB.Where("client_id = ?", req.ID).First(&client).Error; err != nil {
return nil, vigo.ErrNotFound
}
// 检查权限
if client.OwnerID != userID {
// 检查是否是组织成员
if client.OrgID != "" {
var member model.OrgMember
if err := model.DB.Where("org_id = ? AND user_id = ? AND status = ?", client.OrgID, userID, model.MemberStatusActive).First(&member).Error; err != nil {
return nil, vigo.ErrForbidden
}
} else {
return nil, vigo.ErrForbidden
}
}
resp := toClientResponse(&client, false)
return &resp, nil
}
// UpdateClientRequest 更新客户端请求
type UpdateClientRequest struct {
ID string `json:"id" src:"path@client_id" desc:"客户端ID"`
Name *string `json:"name" src:"json" desc:"应用名称"`
Description *string `json:"description" src:"json" desc:"应用描述"`
RedirectURIs []string `json:"redirect_uris" src:"json" desc:"回调地址列表"`
AllowedScopes []string `json:"allowed_scopes" src:"json" desc:"允许的权限范围"`
TokenExpiry *int `json:"token_expiry" src:"json" desc:"Token有效期"`
RefreshExpiry *int `json:"refresh_expiry" src:"json" desc:"RefreshToken有效期"`
Status *int `json:"status" src:"json" desc:"状态"`
}
// UpdateClient 更新OAuth客户端
func UpdateClient(x *vigo.X, req *UpdateClientRequest) (*ClientResponse, error) {
userID := middleware.CurrentUser(x)
if userID == "" {
return nil, vigo.ErrNotAuthorized
}
var client model.OAuthClient
if err := model.DB.Where("client_id = ?", req.ID).First(&client).Error; err != nil {
return nil, vigo.ErrNotFound
}
// 检查权限
if client.OwnerID != userID {
if client.OrgID != "" {
var member model.OrgMember
if err := model.DB.Where("org_id = ? AND user_id = ? AND status = ?", client.OrgID, userID, model.MemberStatusActive).First(&member).Error; err != nil {
return nil, vigo.ErrForbidden
}
} else {
return nil, vigo.ErrForbidden
}
}
updates := make(map[string]interface{})
if req.Name != nil {
updates["name"] = *req.Name
}
if req.Description != nil {
updates["description"] = *req.Description
}
if req.RedirectURIs != nil {
updates["redirect_uris"] = strings.Join(req.RedirectURIs, ",")
}
if req.AllowedScopes != nil {
updates["allowed_scopes"] = strings.Join(req.AllowedScopes, ",")
}
if req.TokenExpiry != nil {
updates["token_expiry"] = *req.TokenExpiry
}
if req.RefreshExpiry != nil {
updates["refresh_expiry"] = *req.RefreshExpiry
}
if req.Status != nil {
updates["status"] = *req.Status
}
if len(updates) > 0 {
if err := model.DB.Model(&client).Updates(updates).Error; err != nil {
return nil, vigo.ErrInternalServer.WithError(err)
}
}
// 重新获取
model.DB.First(&client, "client_id = ?", req.ID)
resp := toClientResponse(&client, false)
return &resp, nil
}
// DeleteClientRequest 删除客户端请求
type DeleteClientRequest struct {
ID string `json:"id" src:"path@client_id" desc:"客户端ID"`
}
// DeleteClient 删除OAuth客户端
func DeleteClient(x *vigo.X, req *DeleteClientRequest) error {
userID := middleware.CurrentUser(x)
if userID == "" {
return vigo.ErrNotAuthorized
}
var client model.OAuthClient
if err := model.DB.Where("client_id = ?", req.ID).First(&client).Error; err != nil {
return vigo.ErrNotFound
}
// 检查权限
if client.OwnerID != userID {
if client.OrgID != "" {
var member model.OrgMember
if err := model.DB.Where("org_id = ? AND user_id = ? AND status = ?", client.OrgID, userID, model.MemberStatusActive).First(&member).Error; err != nil {
return vigo.ErrForbidden
}
} else {
return vigo.ErrForbidden
}
}
// 软删除
if err := model.DB.Delete(&client).Error; err != nil {
return vigo.ErrInternalServer.WithError(err)
}
return nil
}
// RegenerateSecretRequest 重新生成密钥请求
type RegenerateSecretRequest struct {
ID string `json:"id" src:"path@client_id" desc:"客户端ID"`
}
// RegenerateSecret 重新生成客户端密钥
func RegenerateSecret(x *vigo.X, req *RegenerateSecretRequest) (*ClientResponse, error) {
userID := middleware.CurrentUser(x)
if userID == "" {
return nil, vigo.ErrNotAuthorized
}
var client model.OAuthClient
if err := model.DB.Where("client_id = ?", req.ID).First(&client).Error; err != nil {
return nil, vigo.ErrNotFound
}
// 检查权限
if client.OwnerID != userID {
if client.OrgID != "" {
var member model.OrgMember
if err := model.DB.Where("org_id = ? AND user_id = ? AND status = ?", client.OrgID, userID, model.MemberStatusActive).First(&member).Error; err != nil {
return nil, vigo.ErrForbidden
}
} else {
return nil, vigo.ErrForbidden
}
}
// 生成新密钥
newSecret := generateClientSecret()
if err := model.DB.Model(&client).Update("client_secret", newSecret).Error; err != nil {
return nil, vigo.ErrInternalServer.WithError(err)
}
resp := toClientResponse(&client, true)
return &resp, nil
}
// helper functions
func toClientResponse(c *model.OAuthClient, includeSecret bool) ClientResponse {
resp := ClientResponse{
ID: c.ID,
Name: c.Name,
Description: c.Description,
ClientID: c.ClientID,
RedirectURIs: strings.Split(c.RedirectURIs, ","),
GrantTypes: strings.Split(c.GrantTypes, ","),
ResponseTypes: strings.Split(c.ResponseTypes, ","),
AllowedScopes: strings.Split(c.AllowedScopes, ","),
TokenExpiry: c.TokenExpiry,
RefreshExpiry: c.RefreshExpiry,
OwnerID: c.OwnerID,
OrgID: c.OrgID,
Status: c.Status,
CreatedAt: c.CreatedAt.Format("2006-01-02 15:04:05"),
}
if includeSecret {
resp.ClientSecret = c.ClientSecret
}
return resp
}

@ -0,0 +1,585 @@
package oauth
import (
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"net/http"
"net/url"
"strings"
"time"
"github.com/veypi/vbase/internal/model"
"github.com/veypi/vigo"
)
// AuthorizeRequest 授权请求
type AuthorizeRequest struct {
ResponseType string `json:"response_type" src:"query" desc:"响应类型: code/token"`
ClientID string `json:"client_id" src:"query" desc:"客户端ID"`
RedirectURI string `json:"redirect_uri" src:"query" desc:"回调地址"`
Scope string `json:"scope" src:"query" desc:"请求的权限范围"`
State string `json:"state" src:"query" desc:"状态值(防CSRF)"`
CodeChallenge string `json:"code_challenge" src:"query" desc:"PKCE挑战码"`
CodeChallengeMethod string `json:"code_challenge_method" src:"query" desc:"PKCE方法: S256/plain"`
}
// AuthorizeResponse 授权响应 (用于重定向)
type AuthorizeResponse struct {
Code string `json:"code"`
State string `json:"state"`
}
// Authorize 授权端点 - 处理授权码请求
// GET /oauth/authorize
func Authorize(x *vigo.X, req *AuthorizeRequest) error {
// 验证必填参数
if req.ResponseType == "" {
return oauthError(x, req.RedirectURI, "invalid_request", "response_type is required", req.State)
}
if req.ClientID == "" {
return oauthError(x, req.RedirectURI, "invalid_request", "client_id is required", req.State)
}
// 查找客户端
var client model.OAuthClient
if err := model.DB.Where("client_id = ? AND status = ?", req.ClientID, 1).First(&client).Error; err != nil {
return oauthError(x, req.RedirectURI, "invalid_client", "client not found", req.State)
}
// 验证response_type
if !strings.Contains(client.ResponseTypes, req.ResponseType) {
return oauthError(x, req.RedirectURI, "unsupported_response_type", "", req.State)
}
// 验证redirect_uri
if req.RedirectURI != "" {
allowedURIs := strings.Split(client.RedirectURIs, ",")
found := false
for _, uri := range allowedURIs {
if strings.TrimSpace(uri) == req.RedirectURI {
found = true
break
}
}
if !found {
return oauthError(x, "", "invalid_request", "redirect_uri mismatch", "")
}
} else if client.RedirectURIs != "" {
// 使用第一个注册的回调地址
req.RedirectURI = strings.Split(client.RedirectURIs, ",")[0]
} else {
return oauthError(x, "", "invalid_request", "redirect_uri required", "")
}
// 验证scope
requestedScopes := parseScopes(req.Scope)
allowedScopes := parseScopes(client.AllowedScopes)
for _, scope := range requestedScopes {
if !contains(allowedScopes, scope) {
return oauthError(x, req.RedirectURI, "invalid_scope", "scope not allowed: "+scope, req.State)
}
}
// 获取当前用户
var userID string
if uid, ok := x.Get("current_user").(string); ok {
userID = uid
}
if userID == "" {
// 未登录,需要重定向到登录页面
loginURL := "/login?redirect=" + url.QueryEscape(x.Request.URL.String())
x.ResponseWriter().Header().Set("Location", loginURL)
x.ResponseWriter().WriteHeader(http.StatusFound)
return nil
}
// 获取组织ID (从请求头或用户选择)
orgID := x.Request.Header.Get("X-Org-ID")
if orgID == "" && client.OrgID != "" {
orgID = client.OrgID
}
switch req.ResponseType {
case model.ResponseTypeCode:
// 生成授权码
code, err := generateAuthorizationCode(&client, userID, orgID, req)
if err != nil {
return oauthError(x, req.RedirectURI, "server_error", "", req.State)
}
// 构建重定向URL
redirectURL, _ := url.Parse(req.RedirectURI)
q := redirectURL.Query()
q.Set("code", code)
if req.State != "" {
q.Set("state", req.State)
}
redirectURL.RawQuery = q.Encode()
x.ResponseWriter().Header().Set("Location", redirectURL.String())
x.ResponseWriter().WriteHeader(http.StatusFound)
return nil
case model.ResponseTypeToken:
// Implicit flow (简化模式) - 直接返回token
// 注意: 简化模式安全性较低,建议仅在必要时使用
return handleImplicitGrant(x, &client, userID, orgID, req)
default:
return oauthError(x, req.RedirectURI, "unsupported_response_type", "", req.State)
}
}
// TokenRequest 令牌请求
type TokenRequest struct {
GrantType string `json:"grant_type" src:"form" desc:"授权类型"`
Code string `json:"code" src:"form" desc:"授权码"`
RedirectURI string `json:"redirect_uri" src:"form" desc:"回调地址"`
ClientID string `json:"client_id" src:"form" desc:"客户端ID"`
ClientSecret string `json:"client_secret" src:"form" desc:"客户端密钥"`
RefreshToken string `json:"refresh_token" src:"form" desc:"刷新令牌"`
Scope string `json:"scope" src:"form" desc:"权限范围"`
CodeVerifier string `json:"code_verifier" src:"form" desc:"PKCE验证器"`
Username string `json:"username" src:"form" desc:"用户名(密码模式)"`
Password string `json:"password" src:"form" desc:"密码(密码模式)"`
}
// TokenResponse 令牌响应
type TokenResponse struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
ExpiresIn int `json:"expires_in"`
RefreshToken string `json:"refresh_token,omitempty"`
Scope string `json:"scope,omitempty"`
IDToken string `json:"id_token,omitempty"` // OIDC
}
// Token 令牌端点 - 交换授权码获取访问令牌
// POST /oauth/token
func Token(x *vigo.X, req *TokenRequest) (*TokenResponse, error) {
// 验证客户端身份
client, err := authenticateClient(x, req)
if err != nil {
return nil, vigo.ErrNotAuthorized.WithString("invalid_client")
}
// 验证grant_type
if !strings.Contains(client.GrantTypes, req.GrantType) {
return nil, vigo.ErrArgInvalid.WithString("unsupported_grant_type")
}
switch req.GrantType {
case model.GrantTypeAuthorizationCode:
return handleAuthorizationCodeGrant(x, client, req)
case model.GrantTypeRefreshToken:
return handleRefreshTokenGrant(x, client, req)
case model.GrantTypeClientCredentials:
return handleClientCredentialsGrant(x, client, req)
case model.GrantTypePassword:
return handlePasswordGrant(x, client, req)
default:
return nil, vigo.ErrArgInvalid.WithString("unsupported_grant_type")
}
}
// handleAuthorizationCodeGrant 处理授权码模式
func handleAuthorizationCodeGrant(x *vigo.X, client *model.OAuthClient, req *TokenRequest) (*TokenResponse, error) {
if req.Code == "" {
return nil, vigo.ErrArgInvalid.WithString("code is required")
}
// 查找授权码
var auth model.OAuthAuthorization
if err := model.DB.Where("code = ? AND client_id = ? AND used = ?", req.Code, client.ClientID, false).First(&auth).Error; err != nil {
return nil, vigo.ErrArgInvalid.WithString("invalid_grant")
}
// 检查是否过期
if time.Now().After(auth.ExpiresAt) {
return nil, vigo.ErrArgInvalid.WithString("invalid_grant: code expired")
}
// 验证redirect_uri
if req.RedirectURI != "" && req.RedirectURI != auth.RedirectURI {
return nil, vigo.ErrArgInvalid.WithString("invalid_grant: redirect_uri mismatch")
}
// PKCE验证
if auth.CodeChallenge != "" {
if req.CodeVerifier == "" {
return nil, vigo.ErrArgInvalid.WithString("invalid_grant: code_verifier required")
}
if !verifyPKCE(req.CodeVerifier, auth.CodeChallenge, auth.CodeChallengeMethod) {
return nil, vigo.ErrArgInvalid.WithString("invalid_grant: code_verifier mismatch")
}
}
// 标记授权码为已使用
now := time.Now()
model.DB.Model(&auth).Updates(map[string]interface{}{
"used": true,
"used_at": now,
})
// 生成访问令牌
tokenResp, err := generateTokenPair(client, auth.UserID, auth.OrgID, auth.Scope, client.TokenExpiry, client.RefreshExpiry)
if err != nil {
return nil, vigo.ErrInternalServer.WithError(err)
}
return tokenResp, nil
}
// handleRefreshTokenGrant 处理刷新令牌模式
func handleRefreshTokenGrant(x *vigo.X, client *model.OAuthClient, req *TokenRequest) (*TokenResponse, error) {
if req.RefreshToken == "" {
return nil, vigo.ErrArgInvalid.WithString("refresh_token is required")
}
// 查找刷新令牌
var token model.OAuthToken
if err := model.DB.Where("refresh_token = ? AND client_id = ? AND revoked = ?", req.RefreshToken, client.ClientID, false).First(&token).Error; err != nil {
return nil, vigo.ErrArgInvalid.WithString("invalid_grant")
}
// 检查是否过期
if time.Now().After(token.ExpiresAt) {
return nil, vigo.ErrArgInvalid.WithString("invalid_grant: token expired")
}
// 撤销旧的刷新令牌
now := time.Now()
model.DB.Model(&token).Updates(map[string]interface{}{
"revoked": true,
"revoked_at": now,
})
// 生成新的访问令牌
tokenResp, err := generateTokenPair(client, token.UserID, token.OrgID, token.Scope, client.TokenExpiry, client.RefreshExpiry)
if err != nil {
return nil, vigo.ErrInternalServer.WithError(err)
}
return tokenResp, nil
}
// handleClientCredentialsGrant 处理客户端凭证模式
func handleClientCredentialsGrant(x *vigo.X, client *model.OAuthClient, req *TokenRequest) (*TokenResponse, error) {
// 客户端凭证模式没有用户上下文,通常用于服务间调用
// 限制scope只允许非用户相关的权限
requestedScopes := parseScopes(req.Scope)
allowedScopes := []string{}
for _, scope := range requestedScopes {
if scope == "service" || strings.HasPrefix(scope, "service:") {
allowedScopes = append(allowedScopes, scope)
}
}
if len(allowedScopes) == 0 {
allowedScopes = []string{"service"}
}
scopeStr := strings.Join(allowedScopes, " ")
// 生成访问令牌
accessToken := generateRandomToken(32)
expiresAt := time.Now().Add(time.Duration(client.TokenExpiry) * time.Second)
token := &model.OAuthToken{
UserID: "", // 客户端凭证模式没有用户
ClientID: client.ClientID,
OrgID: client.OrgID,
AccessToken: accessToken,
TokenType: "Bearer",
Scope: scopeStr,
ExpiresAt: expiresAt,
}
if err := model.DB.Create(token).Error; err != nil {
return nil, vigo.ErrInternalServer.WithError(err)
}
return &TokenResponse{
AccessToken: accessToken,
TokenType: "Bearer",
ExpiresIn: client.TokenExpiry,
Scope: scopeStr,
}, nil
}
// handlePasswordGrant 处理密码模式 (不推荐,但为兼容性保留)
func handlePasswordGrant(x *vigo.X, client *model.OAuthClient, req *TokenRequest) (*TokenResponse, error) {
// 密码模式需要验证用户凭据
// 这里简化处理,实际应该调用用户认证逻辑
return nil, vigo.ErrArgInvalid.WithString("unsupported_grant_type: password grant is disabled")
}
// handleImplicitGrant 处理简化模式
func handleImplicitGrant(x *vigo.X, client *model.OAuthClient, userID, orgID string, req *AuthorizeRequest) error {
// 生成访问令牌
accessToken := generateRandomToken(32)
expiresAt := time.Now().Add(time.Duration(client.TokenExpiry) * time.Second)
token := &model.OAuthToken{
UserID: userID,
ClientID: client.ClientID,
OrgID: orgID,
AccessToken: accessToken,
TokenType: "Bearer",
Scope: req.Scope,
ExpiresAt: expiresAt,
}
if err := model.DB.Create(token).Error; err != nil {
return oauthError(x, req.RedirectURI, "server_error", "", req.State)
}
// 构建fragment URL
fragment := url.Values{}
fragment.Set("access_token", accessToken)
fragment.Set("token_type", "Bearer")
fragment.Set("expires_in", string(rune(client.TokenExpiry)))
if req.Scope != "" {
fragment.Set("scope", req.Scope)
}
if req.State != "" {
fragment.Set("state", req.State)
}
redirectURL, _ := url.Parse(req.RedirectURI)
redirectURL.Fragment = fragment.Encode()
x.ResponseWriter().Header().Set("Location", redirectURL.String())
x.ResponseWriter().WriteHeader(http.StatusFound)
return nil
}
// RevokeRequest 撤销请求
type RevokeRequest struct {
Token string `json:"token" src:"form" desc:"要撤销的令牌"`
TokenTypeHint string `json:"token_type_hint" src:"form" desc:"令牌类型提示: access_token/refresh_token"`
}
// Revoke 撤销令牌端点
// POST /oauth/revoke
func Revoke(x *vigo.X, req *RevokeRequest) error {
if req.Token == "" {
return nil // 根据RFC 7009无效的令牌也应返回200
}
// 尝试查找access_token
var token model.OAuthToken
if err := model.DB.Where("access_token = ?", req.Token).First(&token).Error; err == nil {
now := time.Now()
model.DB.Model(&token).Updates(map[string]interface{}{
"revoked": true,
"revoked_at": now,
})
return nil
}
// 尝试查找refresh_token
if err := model.DB.Where("refresh_token = ?", req.Token).First(&token).Error; err == nil {
now := time.Now()
model.DB.Model(&token).Updates(map[string]interface{}{
"revoked": true,
"revoked_at": now,
})
}
return nil
}
// IntrospectRequest 令牌内省请求
type IntrospectRequest struct {
Token string `json:"token" src:"form" desc:"要内省的令牌"`
TokenTypeHint string `json:"token_type_hint" src:"form" desc:"令牌类型提示"`
}
// IntrospectResponse 令牌内省响应
type IntrospectResponse struct {
Active bool `json:"active"`
Scope string `json:"scope,omitempty"`
ClientID string `json:"client_id,omitempty"`
Username string `json:"username,omitempty"`
TokenType string `json:"token_type,omitempty"`
Exp int64 `json:"exp,omitempty"`
Iat int64 `json:"iat,omitempty"`
Sub string `json:"sub,omitempty"`
Aud string `json:"aud,omitempty"`
Iss string `json:"iss,omitempty"`
Jti string `json:"jti,omitempty"`
}
// Introspect 令牌内省端点 (RFC 7662)
// POST /oauth/introspect
func Introspect(x *vigo.X, req *IntrospectRequest) (*IntrospectResponse, error) {
if req.Token == "" {
return &IntrospectResponse{Active: false}, nil
}
var token model.OAuthToken
if err := model.DB.Where("access_token = ? AND revoked = ?", req.Token, false).First(&token).Error; err != nil {
return &IntrospectResponse{Active: false}, nil
}
// 检查是否过期
if time.Now().After(token.ExpiresAt) {
return &IntrospectResponse{Active: false}, nil
}
// 获取用户信息
var user model.User
username := ""
if err := model.DB.First(&user, "id = ?", token.UserID).Error; err == nil {
username = user.Username
}
return &IntrospectResponse{
Active: true,
Scope: token.Scope,
ClientID: token.ClientID,
Username: username,
TokenType: token.TokenType,
Exp: token.ExpiresAt.Unix(),
Sub: token.UserID,
}, nil
}
// helper functions
func generateAuthorizationCode(client *model.OAuthClient, userID, orgID string, req *AuthorizeRequest) (string, error) {
code := generateRandomToken(32)
expiresAt := time.Now().Add(10 * time.Minute) // 授权码10分钟有效
auth := &model.OAuthAuthorization{
UserID: userID,
ClientID: client.ClientID,
OrgID: orgID,
Code: code,
Scope: req.Scope,
State: req.State,
CodeChallenge: req.CodeChallenge,
CodeChallengeMethod: req.CodeChallengeMethod,
RedirectURI: req.RedirectURI,
ExpiresAt: expiresAt,
}
if err := model.DB.Create(auth).Error; err != nil {
return "", err
}
return code, nil
}
func generateTokenPair(client *model.OAuthClient, userID, orgID, scope string, accessExpiry, refreshExpiry int) (*TokenResponse, error) {
accessToken := generateRandomToken(32)
refreshToken := generateRandomToken(32)
expiresAt := time.Now().Add(time.Duration(accessExpiry) * time.Second)
token := &model.OAuthToken{
UserID: userID,
ClientID: client.ClientID,
OrgID: orgID,
AccessToken: accessToken,
RefreshToken: refreshToken,
TokenType: "Bearer",
Scope: scope,
ExpiresAt: expiresAt,
}
if err := model.DB.Create(token).Error; err != nil {
return nil, err
}
return &TokenResponse{
AccessToken: accessToken,
TokenType: "Bearer",
ExpiresIn: accessExpiry,
RefreshToken: refreshToken,
Scope: scope,
}, nil
}
func generateRandomToken(length int) string {
b := make([]byte, length)
rand.Read(b)
return hex.EncodeToString(b)
}
func authenticateClient(x *vigo.X, req *TokenRequest) (*model.OAuthClient, error) {
// 优先从Basic Auth获取
clientID, clientSecret, ok := x.Request.BasicAuth()
if ok {
req.ClientID = clientID
req.ClientSecret = clientSecret
}
if req.ClientID == "" || req.ClientSecret == "" {
return nil, vigo.ErrNotAuthorized
}
var client model.OAuthClient
if err := model.DB.Where("client_id = ? AND client_secret = ? AND status = ?", req.ClientID, req.ClientSecret, 1).First(&client).Error; err != nil {
return nil, err
}
return &client, nil
}
func verifyPKCE(verifier, challenge, method string) bool {
switch method {
case "S256":
hash := sha256.Sum256([]byte(verifier))
encoded := base64.RawURLEncoding.EncodeToString(hash[:])
return encoded == challenge
case "plain":
return verifier == challenge
default:
return false
}
}
func parseScopes(scope string) []string {
if scope == "" {
return []string{}
}
return strings.Split(scope, " ")
}
func contains(arr []string, item string) bool {
for _, a := range arr {
if a == item {
return true
}
}
return false
}
func oauthError(x *vigo.X, redirectURI, errorCode, errorDescription, state string) error {
if redirectURI == "" {
return vigo.ErrArgInvalid.WithString(errorCode + ": " + errorDescription)
}
u, err := url.Parse(redirectURI)
if err != nil {
return vigo.ErrArgInvalid.WithString(errorCode + ": " + errorDescription)
}
q := u.Query()
q.Set("error", errorCode)
if errorDescription != "" {
q.Set("error_description", errorDescription)
}
if state != "" {
q.Set("state", state)
}
u.RawQuery = q.Encode()
x.ResponseWriter().Header().Set("Location", u.String())
x.ResponseWriter().WriteHeader(http.StatusFound)
return nil
}

@ -0,0 +1,224 @@
package oauth
import (
"github.com/veypi/vbase/internal/model"
"github.com/veypi/vigo"
)
// UserInfoResponse 用户信息响应 (OIDC标准格式)
type UserInfoResponse struct {
Sub string `json:"sub"` // 用户唯一标识
Name string `json:"name,omitempty"` // 全名
Nickname string `json:"nickname,omitempty"` // 昵称
PreferredUsername string `json:"preferred_username,omitempty"`
Profile string `json:"profile,omitempty"`
Picture string `json:"picture,omitempty"` // 头像
Website string `json:"website,omitempty"`
Email string `json:"email,omitempty"`
EmailVerified bool `json:"email_verified,omitempty"`
Phone string `json:"phone,omitempty"`
PhoneVerified bool `json:"phone_verified,omitempty"`
Gender string `json:"gender,omitempty"`
Birthdate string `json:"birthdate,omitempty"`
Zoneinfo string `json:"zoneinfo,omitempty"`
Locale string `json:"locale,omitempty"`
UpdatedAt int64 `json:"updated_at,omitempty"`
Orgs []OrgClaim `json:"orgs,omitempty"` // 扩展字段:用户所属组织
}
// OrgClaim 组织声明
type OrgClaim struct {
OrgID string `json:"org_id"`
Name string `json:"name"`
Code string `json:"code"`
Roles []string `json:"roles"`
Status int `json:"status"`
}
// UserInfo 用户信息端点 (OIDC)
// GET /oauth/userinfo
func UserInfo(x *vigo.X) (*UserInfoResponse, error) {
// 从context获取当前用户
userID, ok := x.Get("oauth_user_id").(string)
if !ok || userID == "" {
return nil, vigo.ErrNotAuthorized.WithString("invalid_token")
}
// 获取scope决定返回哪些字段
scope, _ := x.Get("oauth_scope").(string)
scopes := parseScopes(scope)
var user model.User
if err := model.DB.First(&user, "id = ?", userID).Error; err != nil {
return nil, vigo.ErrNotFound.WithString("user not found")
}
resp := &UserInfoResponse{
Sub: user.ID,
}
// 根据scope返回相应字段
if contains(scopes, "profile") {
resp.Name = user.Nickname
resp.Nickname = user.Nickname
resp.PreferredUsername = user.Username
resp.Picture = user.Avatar
resp.UpdatedAt = user.UpdatedAt.Unix()
}
if contains(scopes, "email") {
resp.Email = user.Email
resp.EmailVerified = user.EmailVerified
}
if contains(scopes, "phone") {
resp.Phone = user.Phone
resp.PhoneVerified = user.PhoneVerified
}
// 如果请求了org scope返回组织信息
if contains(scopes, "org") {
resp.Orgs = getUserOrgClaims(userID)
}
return resp, nil
}
// DiscoveryResponse OIDC发现文档响应
type DiscoveryResponse struct {
Issuer string `json:"issuer"`
AuthorizationEndpoint string `json:"authorization_endpoint"`
TokenEndpoint string `json:"token_endpoint"`
UserinfoEndpoint string `json:"userinfo_endpoint"`
RevocationEndpoint string `json:"revocation_endpoint"`
IntrospectionEndpoint string `json:"introspection_endpoint"`
JWKSUri string `json:"jwks_uri"`
ScopesSupported []string `json:"scopes_supported"`
ResponseTypesSupported []string `json:"response_types_supported"`
GrantTypesSupported []string `json:"grant_types_supported"`
SubjectTypesSupported []string `json:"subject_types_supported"`
IDTokenSigningAlgValuesSupported []string `json:"id_token_signing_alg_values_supported"`
TokenEndpointAuthMethodsSupported []string `json:"token_endpoint_auth_methods_supported"`
ClaimsSupported []string `json:"claims_supported"`
CodeChallengeMethodsSupported []string `json:"code_challenge_methods_supported"`
}
// Discovery OIDC发现文档端点
// GET /.well-known/openid-configuration
func Discovery(x *vigo.X) (*DiscoveryResponse, error) {
baseURL := "https://" + x.Request.Host // 生产环境应该使用配置
return &DiscoveryResponse{
Issuer: baseURL,
AuthorizationEndpoint: baseURL + "/oauth/authorize",
TokenEndpoint: baseURL + "/oauth/token",
UserinfoEndpoint: baseURL + "/oauth/userinfo",
RevocationEndpoint: baseURL + "/oauth/revoke",
IntrospectionEndpoint: baseURL + "/oauth/introspect",
JWKSUri: baseURL + "/oauth/jwks",
ScopesSupported: []string{
"openid",
"profile",
"email",
"phone",
"org",
"roles",
"offline_access",
},
ResponseTypesSupported: []string{"code", "token", "id_token"},
GrantTypesSupported: []string{
"authorization_code",
"refresh_token",
"client_credentials",
},
SubjectTypesSupported: []string{"public"},
IDTokenSigningAlgValuesSupported: []string{"RS256"},
TokenEndpointAuthMethodsSupported: []string{
"client_secret_basic",
"client_secret_post",
},
ClaimsSupported: []string{
"sub",
"name",
"nickname",
"preferred_username",
"picture",
"email",
"email_verified",
"phone",
"phone_verified",
"updated_at",
"orgs",
},
CodeChallengeMethodsSupported: []string{"S256", "plain"},
}, nil
}
// JWKSResponse JWKS响应
type JWKSResponse struct {
Keys []JWK `json:"keys"`
}
// JWK JSON Web Key
type JWK struct {
Kty string `json:"kty"`
Kid string `json:"kid"`
Use string `json:"use"`
N string `json:"n"`
E string `json:"e"`
Alg string `json:"alg"`
}
// JWKS 公钥端点
// GET /oauth/jwks
func JWKS(x *vigo.X) (*JWKSResponse, error) {
// 返回JWT签名公钥
// 实际实现需要从配置的私钥中提取公钥信息
// 这里简化返回
return &JWKSResponse{
Keys: []JWK{
{
Kty: "RSA",
Kid: "default",
Use: "sig",
Alg: "RS256",
// N和E应该从实际公钥计算得出
},
},
}, nil
}
// helper functions
func getUserOrgClaims(userID string) []OrgClaim {
var members []model.OrgMember
if err := model.DB.Where("user_id = ? AND status = ?", userID, model.MemberStatusActive).Find(&members).Error; err != nil {
return nil
}
if len(members) == 0 {
return []OrgClaim{}
}
result := make([]OrgClaim, 0, len(members))
for _, m := range members {
var org model.Org
if err := model.DB.First(&org, "id = ?", m.OrgID).Error; err != nil {
continue
}
roles := []string{}
if m.RoleIDs != "" {
roles = parseScopes(m.RoleIDs) // 复用parseScopes来split
}
result = append(result, OrgClaim{
OrgID: m.OrgID,
Name: org.Name,
Code: org.Code,
Roles: roles,
Status: m.Status,
})
}
return result
}

@ -0,0 +1,511 @@
package org
import (
"fmt"
"strings"
"time"
"github.com/veypi/vbase/internal/api/middleware"
"github.com/veypi/vbase/internal/model"
"github.com/veypi/vigo"
"gorm.io/gorm"
)
// ListRequest 组织列表请求
type ListRequest struct {
Page int `json:"page" src:"query" default:"1" desc:"页码"`
PageSize int `json:"page_size" src:"query" default:"10" desc:"每页数量"`
Keyword string `json:"keyword" src:"query" desc:"搜索关键词"`
}
// ListResponse 组织列表响应
type ListResponse struct {
Items []OrgInfo `json:"items"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
TotalPages int `json:"total_pages"`
}
// OrgInfo 组织信息
type OrgInfo struct {
ID string `json:"id"`
Name string `json:"name"`
Code string `json:"code"`
OwnerID string `json:"owner_id"`
ParentID string `json:"parent_id,omitempty"`
Path string `json:"path"`
Level int `json:"level"`
LeaderID string `json:"leader_id,omitempty"`
Description string `json:"description"`
Logo string `json:"logo"`
Status int `json:"status"`
MaxMembers int `json:"max_members"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
// 当前用户在该组织的信息
MyRoles []string `json:"my_roles,omitempty"`
MyStatus int `json:"my_status,omitempty"`
}
// List 获取当前用户的组织列表
func List(x *vigo.X, req *ListRequest) (*ListResponse, error) {
userID := middleware.CurrentUser(x)
if userID == "" {
return nil, vigo.ErrNotAuthorized
}
if req.Page < 1 {
req.Page = 1
}
if req.PageSize < 1 || req.PageSize > 100 {
req.PageSize = 10
}
// 查询用户所属的组织ID
var memberOrgs []model.OrgMember
if err := model.DB.Where("user_id = ? AND status = ?", userID, model.MemberStatusActive).Find(&memberOrgs).Error; err != nil {
return nil, vigo.ErrInternalServer.WithError(err)
}
if len(memberOrgs) == 0 {
return &ListResponse{
Items: []OrgInfo{},
Total: 0,
Page: req.Page,
PageSize: req.PageSize,
TotalPages: 0,
}, nil
}
orgIDs := make([]string, 0, len(memberOrgs))
memberMap := make(map[string]*model.OrgMember)
for _, m := range memberOrgs {
orgIDs = append(orgIDs, m.OrgID)
memberMap[m.OrgID] = &m
}
// 查询组织详情
var total int64
query := model.DB.Model(&model.Org{}).Where("id IN ?", orgIDs)
if req.Keyword != "" {
query = query.Where("name LIKE ? OR code LIKE ?", "%"+req.Keyword+"%", "%"+req.Keyword+"%")
}
if err := query.Count(&total).Error; err != nil {
return nil, vigo.ErrInternalServer.WithError(err)
}
var orgs []model.Org
offset := (req.Page - 1) * req.PageSize
if err := query.Offset(offset).Limit(req.PageSize).Order("created_at DESC").Find(&orgs).Error; err != nil {
return nil, vigo.ErrInternalServer.WithError(err)
}
items := make([]OrgInfo, 0, len(orgs))
for _, o := range orgs {
info := toOrgInfo(&o)
if m, ok := memberMap[o.ID]; ok {
info.MyRoles = parseRoles(m.RoleIDs)
info.MyStatus = m.Status
}
items = append(items, info)
}
totalPages := int((total + int64(req.PageSize) - 1) / int64(req.PageSize))
return &ListResponse{
Items: items,
Total: total,
Page: req.Page,
PageSize: req.PageSize,
TotalPages: totalPages,
}, nil
}
// CreateRequest 创建组织请求
type CreateRequest struct {
Name string `json:"name" src:"json" desc:"组织名称"`
Code string `json:"code" src:"json" desc:"组织编码"`
ParentID *string `json:"parent_id" src:"json" desc:"父组织ID"`
Description *string `json:"description" src:"json" desc:"描述"`
Logo *string `json:"logo" src:"json" desc:"Logo"`
MaxMembers *int `json:"max_members" src:"json" desc:"最大成员数"`
}
// Create 创建组织
func Create(x *vigo.X, req *CreateRequest) (*OrgInfo, error) {
userID := middleware.CurrentUser(x)
if userID == "" {
return nil, vigo.ErrNotAuthorized
}
// 检查编码是否已存在
var count int64
model.DB.Model(&model.Org{}).Where("code = ?", req.Code).Count(&count)
if count > 0 {
return nil, vigo.ErrArgInvalid.WithString("organization code already exists")
}
// 构建组织路径
path := "/" + req.Code
level := 0
if req.ParentID != nil && *req.ParentID != "" {
var parent model.Org
if err := model.DB.First(&parent, "id = ?", *req.ParentID).Error; err != nil {
return nil, vigo.ErrArgInvalid.WithString("parent organization not found")
}
path = parent.Path + "/" + req.Code
level = parent.Level + 1
}
org := &model.Org{
Name: req.Name,
Code: req.Code,
OwnerID: userID,
Path: path,
Level: level,
Status: model.OrgStatusActive,
MaxMembers: 100,
}
if req.ParentID != nil {
org.ParentID = req.ParentID
}
if req.Description != nil {
org.Description = *req.Description
}
if req.Logo != nil {
org.Logo = *req.Logo
}
if req.MaxMembers != nil {
org.MaxMembers = *req.MaxMembers
}
err := model.DB.Transaction(func(tx *gorm.DB) error {
// 创建组织
if err := tx.Create(org).Error; err != nil {
return err
}
// 创建成员关系(所有者)
member := &model.OrgMember{
OrgID: org.ID,
UserID: userID,
Status: model.MemberStatusActive,
JoinedAt: time.Now().Format("2006-01-02 15:04:05"),
}
return tx.Create(member).Error
})
if err != nil {
return nil, vigo.ErrInternalServer.WithError(err)
}
info := toOrgInfo(org)
return &info, nil
}
// GetRequest 获取组织请求
type GetRequest struct {
ID string `json:"id" src:"path@org_id" desc:"组织ID"`
}
// Get 获取组织详情
func Get(x *vigo.X, req *GetRequest) (*OrgInfo, error) {
userID := middleware.CurrentUser(x)
if userID == "" {
return nil, vigo.ErrNotAuthorized
}
var org model.Org
if err := model.DB.First(&org, "id = ?", req.ID).Error; err != nil {
return nil, vigo.ErrNotFound
}
// 检查用户是否是组织成员
var member model.OrgMember
if err := model.DB.Where("org_id = ? AND user_id = ?", req.ID, userID).First(&member).Error; err != nil {
return nil, vigo.ErrForbidden.WithString("you are not a member of this organization")
}
info := toOrgInfo(&org)
info.MyRoles = parseRoles(member.RoleIDs)
info.MyStatus = member.Status
return &info, nil
}
// UpdateRequest 更新组织请求
type UpdateRequest struct {
ID string `json:"id" src:"path@org_id" desc:"组织ID"`
Name *string `json:"name" src:"json" desc:"组织名称"`
Description *string `json:"description" src:"json" desc:"描述"`
Logo *string `json:"logo" src:"json" desc:"Logo"`
LeaderID *string `json:"leader_id" src:"json" desc:"负责人ID"`
MaxMembers *int `json:"max_members" src:"json" desc:"最大成员数"`
Status *int `json:"status" src:"json" desc:"状态"`
}
// Update 更新组织
func Update(x *vigo.X, req *UpdateRequest) (*OrgInfo, error) {
userID := middleware.CurrentUser(x)
if userID == "" {
return nil, vigo.ErrNotAuthorized
}
var org model.Org
if err := model.DB.First(&org, "id = ?", req.ID).Error; err != nil {
return nil, vigo.ErrNotFound
}
// 检查权限(只有所有者可以修改)
if org.OwnerID != userID {
return nil, vigo.ErrForbidden.WithString("only organization owner can update")
}
updates := make(map[string]interface{})
if req.Name != nil {
updates["name"] = *req.Name
}
if req.Description != nil {
updates["description"] = *req.Description
}
if req.Logo != nil {
updates["logo"] = *req.Logo
}
if req.LeaderID != nil {
updates["leader_id"] = *req.LeaderID
}
if req.MaxMembers != nil {
updates["max_members"] = *req.MaxMembers
}
if req.Status != nil {
updates["status"] = *req.Status
}
if len(updates) > 0 {
if err := model.DB.Model(&org).Updates(updates).Error; err != nil {
return nil, vigo.ErrInternalServer.WithError(err)
}
}
info := toOrgInfo(&org)
return &info, nil
}
// DeleteRequest 删除组织请求
type DeleteRequest struct {
ID string `json:"id" src:"path@org_id" desc:"组织ID"`
}
// Delete 删除组织
func Delete(x *vigo.X, req *DeleteRequest) error {
userID := middleware.CurrentUser(x)
if userID == "" {
return vigo.ErrNotAuthorized
}
var org model.Org
if err := model.DB.First(&org, "id = ?", req.ID).Error; err != nil {
return vigo.ErrNotFound
}
// 检查权限(只有所有者可以删除)
if org.OwnerID != userID {
return vigo.ErrForbidden.WithString("only organization owner can delete")
}
// 检查是否有子组织
var childCount int64
model.DB.Model(&model.Org{}).Where("parent_id = ?", req.ID).Count(&childCount)
if childCount > 0 {
return vigo.ErrArgInvalid.WithString("cannot delete organization with sub-organizations")
}
// 软删除
if err := model.DB.Delete(&org).Error; err != nil {
return vigo.ErrInternalServer.WithError(err)
}
return nil
}
// TreeResponse 组织树响应
type TreeResponse struct {
OrgInfo
Children []TreeResponse `json:"children,omitempty"`
}
// Tree 获取组织树
func Tree(x *vigo.X) ([]TreeResponse, error) {
userID := middleware.CurrentUser(x)
if userID == "" {
return nil, vigo.ErrNotAuthorized
}
// 获取用户所属的所有组织ID
var memberOrgs []model.OrgMember
if err := model.DB.Where("user_id = ? AND status = ?", userID, model.MemberStatusActive).Find(&memberOrgs).Error; err != nil {
return nil, vigo.ErrInternalServer.WithError(err)
}
if len(memberOrgs) == 0 {
return []TreeResponse{}, nil
}
orgIDs := make([]string, 0, len(memberOrgs))
for _, m := range memberOrgs {
orgIDs = append(orgIDs, m.OrgID)
}
// 获取所有组织
var orgs []model.Org
if err := model.DB.Where("id IN ?", orgIDs).Order("path").Find(&orgs).Error; err != nil {
return nil, vigo.ErrInternalServer.WithError(err)
}
// 构建树
return buildOrgTree(orgs, ""), nil
}
func buildOrgTree(orgs []model.Org, parentID string) []TreeResponse {
var result []TreeResponse
for _, o := range orgs {
if (parentID == "" && (o.ParentID == nil || *o.ParentID == "")) ||
(o.ParentID != nil && *o.ParentID == parentID) {
node := TreeResponse{
OrgInfo: toOrgInfo(&o),
}
node.Children = buildOrgTree(orgs, o.ID)
result = append(result, node)
}
}
return result
}
// helper functions
func toOrgInfo(o *model.Org) OrgInfo {
info := OrgInfo{
ID: o.ID,
Name: o.Name,
Code: o.Code,
OwnerID: o.OwnerID,
Path: o.Path,
Level: o.Level,
Description: o.Description,
Logo: o.Logo,
Status: o.Status,
MaxMembers: o.MaxMembers,
CreatedAt: o.CreatedAt.Format("2006-01-02 15:04:05"),
UpdatedAt: o.UpdatedAt.Format("2006-01-02 15:04:05"),
}
if o.ParentID != nil {
info.ParentID = *o.ParentID
}
if o.LeaderID != nil {
info.LeaderID = *o.LeaderID
}
return info
}
func parseRoles(roleIDs string) []string {
if roleIDs == "" {
return []string{}
}
return strings.Split(roleIDs, ",")
}
func formatRoleID(roleIDs []string) string {
return strings.Join(roleIDs, ",")
}
// ListMembersRequest 成员列表请求
type ListMembersRequest struct {
OrgID string `json:"org_id" src:"path@org_id" desc:"组织ID"`
Page int `json:"page" src:"query" default:"1" desc:"页码"`
PageSize int `json:"page_size" src:"query" default:"10" desc:"每页数量"`
Status *int `json:"status" src:"query" desc:"状态筛选"`
}
// MemberInfo 成员信息
type MemberInfo struct {
ID string `json:"id"`
OrgID string `json:"org_id"`
UserID string `json:"user_id"`
Username string `json:"username"`
Nickname string `json:"nickname"`
Avatar string `json:"avatar"`
Email string `json:"email"`
Roles []string `json:"roles"`
Position string `json:"position"`
Department string `json:"department"`
JoinedAt string `json:"joined_at"`
Status int `json:"status"`
}
// ListMembers 获取组织成员列表
func ListMembers(x *vigo.X, req *ListMembersRequest) (*ListResponse, error) {
userID := middleware.CurrentUser(x)
if userID == "" {
return nil, vigo.ErrNotAuthorized
}
if req.Page < 1 {
req.Page = 1
}
if req.PageSize < 1 || req.PageSize > 100 {
req.PageSize = 10
}
// 检查用户是否是组织成员
var currentMember model.OrgMember
if err := model.DB.Where("org_id = ? AND user_id = ?", req.OrgID, userID).First(&currentMember).Error; err != nil {
return nil, vigo.ErrForbidden.WithString("you are not a member of this organization")
}
var total int64
query := model.DB.Model(&model.OrgMember{}).Where("org_id = ?", req.OrgID)
if req.Status != nil {
query = query.Where("status = ?", *req.Status)
}
if err := query.Count(&total).Error; err != nil {
return nil, vigo.ErrInternalServer.WithError(err)
}
var members []model.OrgMember
offset := (req.Page - 1) * req.PageSize
if err := query.Offset(offset).Limit(req.PageSize).Find(&members).Error; err != nil {
return nil, vigo.ErrInternalServer.WithError(err)
}
items := make([]OrgInfo, 0) // 这里需要修改返回类型
_ = items
// 获取用户信息
memberInfos := make([]MemberInfo, 0, len(members))
for _, m := range members {
var user model.User
if err := model.DB.First(&user, "id = ?", m.UserID).Error; err != nil {
continue
}
memberInfos = append(memberInfos, MemberInfo{
ID: m.ID,
OrgID: m.OrgID,
UserID: m.UserID,
Username: user.Username,
Nickname: user.Nickname,
Avatar: user.Avatar,
Email: user.Email,
Roles: parseRoles(m.RoleIDs),
Position: m.Position,
Department: m.Department,
JoinedAt: m.JoinedAt,
Status: m.Status,
})
}
_ = memberInfos
return nil, fmt.Errorf("not fully implemented")
}

@ -0,0 +1,83 @@
package api
import (
"github.com/veypi/vbase/internal/api/auth"
"github.com/veypi/vbase/internal/api/middleware"
"github.com/veypi/vbase/internal/api/oauth"
"github.com/veypi/vbase/internal/api/org"
"github.com/veypi/vbase/internal/api/user"
"github.com/veypi/vigo"
"github.com/veypi/vigo/contrib/common"
)
// NewRouter 创建API路由
func NewRouter() vigo.Router {
r := vigo.NewRouter()
// 全局中间件
r.Use(middleware.AuthRequired())
r.Use(middleware.OrgContext())
r.After(common.JsonResponse, common.JsonErrorResponse)
// === 公开路由 ===
authRouter := vigo.NewRouter()
authRouter.Use(vigo.SkipBefore)
authRouter.Post("/login", "用户登录", auth.Login)
authRouter.Post("/register", "用户注册", auth.Register)
authRouter.Post("/refresh", "刷新Token", auth.Refresh)
authRouter.Post("/logout", "用户登出", auth.Logout)
r.Extend("/auth", authRouter)
// === 当前用户 ===
meRouter := vigo.NewRouter()
meRouter.Get("/", "获取当前用户信息", auth.Me)
meRouter.Patch("/", "更新当前用户信息", auth.UpdateMe)
meRouter.Post("/change-password", "修改密码", auth.ChangePassword)
r.Extend("/me", meRouter)
// === 用户管理 ===
userRouter := vigo.NewRouter()
userRouter.Get("/", "用户列表", user.List)
userRouter.Post("/", "创建用户", user.Create)
userRouter.Get("/{user_id}", "获取用户详情", user.Get)
userRouter.Patch("/{user_id}", "更新用户", user.Update)
userRouter.Delete("/{user_id}", "删除用户", user.Delete)
userRouter.Patch("/{user_id}/status", "更新用户状态", user.UpdateStatus)
r.Extend("/users", userRouter)
// === 组织管理 ===
orgRouter := vigo.NewRouter()
orgRouter.Get("/", "组织列表", org.List)
orgRouter.Post("/", "创建组织", org.Create)
orgRouter.Get("/{org_id}", "获取组织详情", org.Get)
orgRouter.Patch("/{org_id}", "更新组织", org.Update)
orgRouter.Delete("/{org_id}", "删除组织", org.Delete)
orgRouter.Get("/tree", "组织树", org.Tree)
orgRouter.Get("/{org_id}/members", "组织成员列表", org.ListMembers)
r.Extend("/orgs", orgRouter)
// === OAuth2.0服务端 ===
oauthRouter := vigo.NewRouter()
// OAuth公开端点
oauthRouter.Use(vigo.SkipBefore)
oauthRouter.Get("/authorize", "授权端点", oauth.Authorize)
oauthRouter.Post("/token", "令牌端点", oauth.Token)
oauthRouter.Post("/revoke", "撤销令牌", oauth.Revoke)
oauthRouter.Post("/introspect", "令牌内省", oauth.Introspect)
oauthRouter.Get("/userinfo", "用户信息(OIDC)", oauth.UserInfo)
oauthRouter.Get("/.well-known/openid-configuration", "OIDC发现文档", oauth.Discovery)
oauthRouter.Get("/jwks", "JWKS公钥", oauth.JWKS)
r.Extend("/oauth", oauthRouter)
// OAuth客户端管理
oauthClientRouter := vigo.NewRouter()
oauthClientRouter.Get("/", "OAuth客户端列表", oauth.ListClients)
oauthClientRouter.Post("/", "创建OAuth客户端", oauth.CreateClient)
oauthClientRouter.Get("/{client_id}", "获取客户端详情", oauth.GetClient)
oauthClientRouter.Patch("/{client_id}", "更新OAuth客户端", oauth.UpdateClient)
oauthClientRouter.Delete("/{client_id}", "删除OAuth客户端", oauth.DeleteClient)
oauthClientRouter.Post("/{client_id}/regenerate-secret", "重新生成密钥", oauth.RegenerateSecret)
r.Extend("/oauth/clients", oauthClientRouter)
return r
}

@ -0,0 +1,278 @@
package user
import (
"github.com/veypi/vbase/internal/model"
"github.com/veypi/vbase/internal/pkg/crypto"
"github.com/veypi/vigo"
)
// ListRequest 用户列表请求
type ListRequest struct {
Page int `json:"page" src:"query" default:"1" desc:"页码"`
PageSize int `json:"page_size" src:"query" default:"10" desc:"每页数量"`
Keyword string `json:"keyword" src:"query" desc:"搜索关键词"`
Status *int `json:"status" src:"query" desc:"状态筛选"`
}
// ListResponse 用户列表响应
type ListResponse struct {
Items []UserInfo `json:"items"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
TotalPages int `json:"total_pages"`
}
// UserInfo 用户信息
type UserInfo struct {
ID string `json:"id"`
Username string `json:"username"`
Nickname string `json:"nickname"`
Avatar string `json:"avatar"`
Email string `json:"email"`
Phone string `json:"phone"`
Status int `json:"status"`
EmailVerified bool `json:"email_verified"`
PhoneVerified bool `json:"phone_verified"`
LastLoginAt string `json:"last_login_at,omitempty"`
CreatedAt string `json:"created_at"`
}
// List 用户列表
func List(x *vigo.X, req *ListRequest) (*ListResponse, error) {
if req.Page < 1 {
req.Page = 1
}
if req.PageSize < 1 || req.PageSize > 100 {
req.PageSize = 10
}
var total int64
query := model.DB.Model(&model.User{})
if req.Keyword != "" {
query = query.Where("username LIKE ? OR nickname LIKE ? OR email LIKE ?", "%"+req.Keyword+"%", "%"+req.Keyword+"%", "%"+req.Keyword+"%")
}
if req.Status != nil {
query = query.Where("status = ?", *req.Status)
}
if err := query.Count(&total).Error; err != nil {
return nil, vigo.ErrInternalServer.WithError(err)
}
var users []model.User
offset := (req.Page - 1) * req.PageSize
if err := query.Offset(offset).Limit(req.PageSize).Order("created_at DESC").Find(&users).Error; err != nil {
return nil, vigo.ErrInternalServer.WithError(err)
}
items := make([]UserInfo, 0, len(users))
for _, u := range users {
items = append(items, toUserInfo(&u))
}
totalPages := int((total + int64(req.PageSize) - 1) / int64(req.PageSize))
return &ListResponse{
Items: items,
Total: total,
Page: req.Page,
PageSize: req.PageSize,
TotalPages: totalPages,
}, nil
}
// GetRequest 获取用户请求
type GetRequest struct {
ID string `json:"id" src:"path@user_id" desc:"用户ID"`
}
// Get 获取用户详情
func Get(x *vigo.X, req *GetRequest) (*UserInfo, error) {
var user model.User
if err := model.DB.First(&user, "id = ?", req.ID).Error; err != nil {
return nil, vigo.ErrNotFound
}
info := toUserInfo(&user)
return &info, nil
}
// CreateRequest 创建用户请求
type CreateRequest struct {
Username string `json:"username" src:"json" desc:"用户名"`
Password string `json:"password" src:"json" desc:"密码"`
Nickname *string `json:"nickname" src:"json" desc:"昵称"`
Email *string `json:"email" src:"json" desc:"邮箱"`
Phone *string `json:"phone" src:"json" desc:"手机号"`
Status *int `json:"status" src:"json" desc:"状态"`
}
// Create 创建用户
func Create(x *vigo.X, req *CreateRequest) (*UserInfo, error) {
// 检查用户名是否已存在
var count int64
model.DB.Model(&model.User{}).Where("username = ?", req.Username).Count(&count)
if count > 0 {
return nil, vigo.ErrArgInvalid.WithString("username already exists")
}
// 检查邮箱是否已存在
if req.Email != nil && *req.Email != "" {
model.DB.Model(&model.User{}).Where("email = ?", *req.Email).Count(&count)
if count > 0 {
return nil, vigo.ErrArgInvalid.WithString("email already exists")
}
}
// 哈希密码
hashedPassword, err := crypto.HashPassword(req.Password, 12)
if err != nil {
return nil, vigo.ErrInternalServer.WithError(err)
}
// 创建用户
user := &model.User{
Username: req.Username,
Password: hashedPassword,
Status: model.UserStatusActive,
}
if req.Nickname != nil {
user.Nickname = *req.Nickname
} else {
user.Nickname = req.Username
}
if req.Email != nil {
user.Email = *req.Email
}
if req.Phone != nil {
user.Phone = *req.Phone
}
if req.Status != nil {
user.Status = *req.Status
}
if err := model.DB.Create(user).Error; err != nil {
return nil, vigo.ErrInternalServer.WithError(err)
}
info := toUserInfo(user)
return &info, nil
}
// UpdateRequest 更新用户请求
type UpdateRequest struct {
ID string `json:"id" src:"path@user_id" desc:"用户ID"`
Nickname *string `json:"nickname" src:"json" desc:"昵称"`
Avatar *string `json:"avatar" src:"json" desc:"头像"`
Email *string `json:"email" src:"json" desc:"邮箱"`
Phone *string `json:"phone" src:"json" desc:"手机号"`
Status *int `json:"status" src:"json" desc:"状态"`
}
// Update 更新用户
func Update(x *vigo.X, req *UpdateRequest) (*UserInfo, error) {
var user model.User
if err := model.DB.First(&user, "id = ?", req.ID).Error; err != nil {
return nil, vigo.ErrNotFound
}
updates := make(map[string]interface{})
if req.Nickname != nil {
updates["nickname"] = *req.Nickname
}
if req.Avatar != nil {
updates["avatar"] = *req.Avatar
}
if req.Email != nil && *req.Email != user.Email {
// 检查邮箱是否被其他用户使用
var count int64
model.DB.Model(&model.User{}).Where("email = ? AND id != ?", *req.Email, req.ID).Count(&count)
if count > 0 {
return nil, vigo.ErrArgInvalid.WithString("email already exists")
}
updates["email"] = *req.Email
}
if req.Phone != nil && *req.Phone != user.Phone {
var count int64
model.DB.Model(&model.User{}).Where("phone = ? AND id != ?", *req.Phone, req.ID).Count(&count)
if count > 0 {
return nil, vigo.ErrArgInvalid.WithString("phone already exists")
}
updates["phone"] = *req.Phone
}
if req.Status != nil {
updates["status"] = *req.Status
}
if len(updates) > 0 {
if err := model.DB.Model(&user).Updates(updates).Error; err != nil {
return nil, vigo.ErrInternalServer.WithError(err)
}
}
info := toUserInfo(&user)
return &info, nil
}
// DeleteRequest 删除用户请求
type DeleteRequest struct {
ID string `json:"id" src:"path@user_id" desc:"用户ID"`
}
// Delete 删除用户(软删除)
func Delete(x *vigo.X, req *DeleteRequest) error {
var user model.User
if err := model.DB.First(&user, "id = ?", req.ID).Error; err != nil {
return vigo.ErrNotFound
}
if err := model.DB.Delete(&user).Error; err != nil {
return vigo.ErrInternalServer.WithError(err)
}
return nil
}
// UpdateStatusRequest 更新用户状态请求
type UpdateStatusRequest struct {
ID string `json:"id" src:"path@user_id" desc:"用户ID"`
Status int `json:"status" src:"json" desc:"状态: 0禁用 1正常 2未激活"`
}
// UpdateStatus 更新用户状态
func UpdateStatus(x *vigo.X, req *UpdateStatusRequest) (*UserInfo, error) {
var user model.User
if err := model.DB.First(&user, "id = ?", req.ID).Error; err != nil {
return nil, vigo.ErrNotFound
}
if err := model.DB.Model(&user).Update("status", req.Status).Error; err != nil {
return nil, vigo.ErrInternalServer.WithError(err)
}
info := toUserInfo(&user)
return &info, nil
}
// helper function
func toUserInfo(u *model.User) UserInfo {
info := UserInfo{
ID: u.ID,
Username: u.Username,
Nickname: u.Nickname,
Avatar: u.Avatar,
Email: u.Email,
Phone: u.Phone,
Status: u.Status,
EmailVerified: u.EmailVerified,
PhoneVerified: u.PhoneVerified,
CreatedAt: u.CreatedAt.Format("2006-01-02 15:04:05"),
}
if u.LastLoginAt != nil {
info.LastLoginAt = u.LastLoginAt.Format("2006-01-02 15:04:05")
}
return info
}

@ -0,0 +1,374 @@
package cache
import (
"context"
"encoding/json"
"fmt"
"time"
"github.com/redis/go-redis/v9"
"github.com/veypi/vbase/internal/config"
)
var (
Client *redis.Client
Ctx = context.Background()
)
// Init 初始化Redis连接
func Init() error {
if !config.C.Redis.Enabled {
return nil
}
Client = redis.NewClient(&redis.Options{
Addr: config.C.Redis.Addr,
Password: config.C.Redis.Password,
DB: config.C.Redis.DB,
})
if err := Client.Ping(Ctx).Err(); err != nil {
return fmt.Errorf("failed to connect redis: %w", err)
}
return nil
}
// IsEnabled 是否启用缓存
func IsEnabled() bool {
return config.C.Redis.Enabled && Client != nil
}
// Get 获取字符串值
func Get(key string) (string, error) {
if !IsEnabled() {
return "", fmt.Errorf("redis not enabled")
}
return Client.Get(Ctx, key).Result()
}
// GetObject 获取并反序列化对象
func GetObject(key string, dest interface{}) error {
if !IsEnabled() {
return fmt.Errorf("redis not enabled")
}
data, err := Client.Get(Ctx, key).Bytes()
if err != nil {
return err
}
return json.Unmarshal(data, dest)
}
// Set 设置字符串值
func Set(key string, value string, expiration time.Duration) error {
if !IsEnabled() {
return nil
}
return Client.Set(Ctx, key, value, expiration).Err()
}
// SetObject 序列化并设置对象
func SetObject(key string, value interface{}, expiration time.Duration) error {
if !IsEnabled() {
return nil
}
data, err := json.Marshal(value)
if err != nil {
return err
}
return Client.Set(Ctx, key, data, expiration).Err()
}
// Delete 删除key
func Delete(keys ...string) error {
if !IsEnabled() {
return nil
}
return Client.Del(Ctx, keys...).Err()
}
// Exists 检查key是否存在
func Exists(keys ...string) (int64, error) {
if !IsEnabled() {
return 0, nil
}
return Client.Exists(Ctx, keys...).Result()
}
// Expire 设置过期时间
func Expire(key string, expiration time.Duration) error {
if !IsEnabled() {
return nil
}
return Client.Expire(Ctx, key, expiration).Err()
}
// TTL 获取剩余过期时间
func TTL(key string) (time.Duration, error) {
if !IsEnabled() {
return 0, nil
}
return Client.TTL(Ctx, key).Result()
}
// Incr 自增
func Incr(key string) (int64, error) {
if !IsEnabled() {
return 0, fmt.Errorf("redis not enabled")
}
return Client.Incr(Ctx, key).Result()
}
// IncrBy 增加指定值
func IncrBy(key string, value int64) (int64, error) {
if !IsEnabled() {
return 0, fmt.Errorf("redis not enabled")
}
return Client.IncrBy(Ctx, key, value).Result()
}
// Decr 自减
func Decr(key string) (int64, error) {
if !IsEnabled() {
return 0, fmt.Errorf("redis not enabled")
}
return Client.Decr(Ctx, key).Result()
}
// HSet 设置hash字段
func HSet(key string, values ...interface{}) error {
if !IsEnabled() {
return nil
}
return Client.HSet(Ctx, key, values...).Err()
}
// HGet 获取hash字段
func HGet(key, field string) (string, error) {
if !IsEnabled() {
return "", fmt.Errorf("redis not enabled")
}
return Client.HGet(Ctx, key, field).Result()
}
// HGetAll 获取hash所有字段
func HGetAll(key string) (map[string]string, error) {
if !IsEnabled() {
return nil, fmt.Errorf("redis not enabled")
}
return Client.HGetAll(Ctx, key).Result()
}
// HDel 删除hash字段
func HDel(key string, fields ...string) error {
if !IsEnabled() {
return nil
}
return Client.HDel(Ctx, key, fields...).Err()
}
// SetNX 仅当key不存在时才设置用于分布式锁
func SetNX(key string, value interface{}, expiration time.Duration) (bool, error) {
if !IsEnabled() {
return false, fmt.Errorf("redis not enabled")
}
return Client.SetNX(Ctx, key, value, expiration).Result()
}
// ==================== 权限缓存相关 ====================
// PermKey 生成权限缓存key
func PermKey(userID, orgID, resource, action string) string {
if orgID == "" {
return fmt.Sprintf("perm:%s:%s:%s", userID, resource, action)
}
return fmt.Sprintf("perm:%s:%s:%s:%s", userID, orgID, resource, action)
}
// SetPermission 缓存权限结果
func SetPermission(userID, orgID, resource, action string, allowed bool, expiration time.Duration) error {
key := PermKey(userID, orgID, resource, action)
value := "deny"
if allowed {
value = "allow"
}
return Set(key, value, expiration)
}
// GetPermission 获取缓存的权限结果
func GetPermission(userID, orgID, resource, action string) (allowed bool, cached bool, err error) {
key := PermKey(userID, orgID, resource, action)
value, err := Get(key)
if err != nil {
if err == redis.Nil {
return false, false, nil
}
return false, false, err
}
return value == "allow", true, nil
}
// DeletePermission 删除权限缓存
func DeletePermission(userID, orgID, resource, action string) error {
key := PermKey(userID, orgID, resource, action)
return Delete(key)
}
// DeleteUserPermissions 删除用户的所有权限缓存
func DeleteUserPermissions(userID string) error {
if !IsEnabled() {
return nil
}
pattern := fmt.Sprintf("perm:%s:*", userID)
return deleteByPattern(pattern)
}
// DeleteOrgPermissions 删除组织的所有权限缓存
func DeleteOrgPermissions(orgID string) error {
if !IsEnabled() {
return nil
}
pattern := fmt.Sprintf("perm:*:%s:*", orgID)
return deleteByPattern(pattern)
}
// deleteByPattern 根据pattern删除key
func deleteByPattern(pattern string) error {
iter := Client.Scan(Ctx, 0, pattern, 0).Iterator()
var keys []string
for iter.Next(Ctx) {
keys = append(keys, iter.Val())
if len(keys) >= 100 {
if err := Delete(keys...); err != nil {
return err
}
keys = keys[:0]
}
}
if err := iter.Err(); err != nil {
return err
}
if len(keys) > 0 {
return Delete(keys...)
}
return nil
}
// ==================== 用户/组织缓存 ====================
// UserKey 用户缓存key
func UserKey(userID string) string {
return fmt.Sprintf("user:%s", userID)
}
// OrgKey 组织缓存key
func OrgKey(orgID string) string {
return fmt.Sprintf("org:%s", orgID)
}
// OrgMemberKey 组织成员缓存key
func OrgMemberKey(orgID, userID string) string {
return fmt.Sprintf("org:%s:member:%s", orgID, userID)
}
// ==================== Token黑名单 ====================
// TokenBlacklistKey Token黑名单key
func TokenBlacklistKey(jti string) string {
return fmt.Sprintf("token:revoked:%s", jti)
}
// BlacklistToken 将Token加入黑名单
func BlacklistToken(jti string, expiration time.Duration) error {
key := TokenBlacklistKey(jti)
return Set(key, "1", expiration)
}
// IsTokenBlacklisted 检查Token是否在黑名单中
func IsTokenBlacklisted(jti string) (bool, error) {
key := TokenBlacklistKey(jti)
_, err := Get(key)
if err != nil {
if err == redis.Nil {
return false, nil
}
return false, err
}
return true, nil
}
// ==================== 限流缓存 ====================
// RateLimitKey 限流key
func RateLimitKey(identifier, path string) string {
return fmt.Sprintf("ratelimit:%s:%s", identifier, path)
}
// IncrRateLimit 增加限流计数
func IncrRateLimit(identifier, path string, window time.Duration) (int64, error) {
key := RateLimitKey(identifier, path)
count, err := Incr(key)
if err != nil {
return 0, err
}
// 第一次设置过期时间
if count == 1 {
Expire(key, window)
}
return count, nil
}
// GetRateLimit 获取当前限流计数
func GetRateLimit(identifier, path string) (int64, error) {
key := RateLimitKey(identifier, path)
count, err := Get(key)
if err != nil {
if err == redis.Nil {
return 0, nil
}
return 0, err
}
var result int64
fmt.Sscanf(count, "%d", &result)
return result, nil
}
// ==================== 验证码缓存 ====================
// CaptchaKey 验证码key
func CaptchaKey(captchaID string) string {
return fmt.Sprintf("captcha:%s", captchaID)
}
// SetCaptcha 存储验证码
func SetCaptcha(captchaID, code string, expiration time.Duration) error {
key := CaptchaKey(captchaID)
return Set(key, code, expiration)
}
// VerifyCaptcha 验证验证码(验证后删除)
func VerifyCaptcha(captchaID, code string) (bool, error) {
key := CaptchaKey(captchaID)
storedCode, err := Get(key)
if err != nil {
if err == redis.Nil {
return false, nil
}
return false, err
}
// 验证后删除
Delete(key)
return storedCode == code, nil
}
// ==================== OAuth缓存 ====================
// OAuthCodeKey OAuth授权码key
func OAuthCodeKey(code string) string {
return fmt.Sprintf("oauth:code:%s", code)
}
// OAuthStateKey OAuth state key
func OAuthStateKey(state string) string {
return fmt.Sprintf("oauth:state:%s", state)
}

@ -0,0 +1,167 @@
package config
import (
"os"
"strconv"
"time"
)
// Config 全局配置
type Config struct {
// 服务配置
Server ServerConfig
// 数据库配置
Database DatabaseConfig
// Redis配置
Redis RedisConfig
// JWT配置
JWT JWTConfig
// 安全配置
Security SecurityConfig
// OAuth配置
OAuth OAuthConfig
// 应用信息
App AppConfig
}
type ServerConfig struct {
Host string
Port int
Mode string // debug/release
}
type DatabaseConfig struct {
Type string // mysql/postgres/sqlite
DSN string
MaxOpenConns int
MaxIdleConns int
ConnMaxLifetime time.Duration
}
type RedisConfig struct {
Addr string
Password string
DB int
Enabled bool
}
type JWTConfig struct {
Secret string
AccessExpiry time.Duration
RefreshExpiry time.Duration
Issuer string
}
type SecurityConfig struct {
BcryptCost int
MaxLoginAttempts int
CaptchaEnabled bool
}
type OAuthConfig struct {
AuthorizationExpiry time.Duration
AccessExpiry time.Duration
RefreshExpiry time.Duration
}
type AppConfig struct {
ID string
Name string
InitAdmin InitAdminConfig
}
type InitAdminConfig struct {
Username string
Password string
Email string
}
var C *Config
func init() {
C = &Config{
Server: ServerConfig{
Host: getEnv("SERVER_HOST", "0.0.0.0"),
Port: getEnvInt("SERVER_PORT", 8080),
Mode: getEnv("SERVER_MODE", "debug"),
},
Database: DatabaseConfig{
Type: getEnv("DB_TYPE", "mysql"),
DSN: getEnv("DB_DSN", "root:123456@tcp(127.0.0.1:3306)/vbase?charset=utf8mb4&parseTime=True&loc=Local"),
MaxOpenConns: getEnvInt("DB_MAX_OPEN", 100),
MaxIdleConns: getEnvInt("DB_MAX_IDLE", 10),
ConnMaxLifetime: time.Hour,
},
Redis: RedisConfig{
Enabled: getEnvBool("REDIS_ENABLED", true),
Addr: getEnv("REDIS_ADDR", "localhost:6379"),
Password: getEnv("REDIS_PASSWORD", ""),
DB: getEnvInt("REDIS_DB", 0),
},
JWT: JWTConfig{
Secret: getEnv("JWT_SECRET", "your-secret-key-change-in-production-min-32-characters"),
AccessExpiry: getEnvDuration("JWT_ACCESS_EXPIRY", time.Hour),
RefreshExpiry: getEnvDuration("JWT_REFRESH_EXPIRY", 30*24*time.Hour),
Issuer: getEnv("JWT_ISSUER", "vbase"),
},
Security: SecurityConfig{
BcryptCost: getEnvInt("BCRYPT_COST", 12),
MaxLoginAttempts: getEnvInt("MAX_LOGIN_ATTEMPTS", 5),
CaptchaEnabled: getEnvBool("CAPTCHA_ENABLED", true),
},
OAuth: OAuthConfig{
AuthorizationExpiry: getEnvDuration("OAUTH_AUTH_EXPIRY", 10*time.Minute),
AccessExpiry: getEnvDuration("OAUTH_ACCESS_EXPIRY", time.Hour),
RefreshExpiry: getEnvDuration("OAUTH_REFRESH_EXPIRY", 30*24*time.Hour),
},
App: AppConfig{
ID: getEnv("APP_ID", "vbase"),
Name: getEnv("APP_NAME", "VBase IAM"),
InitAdmin: InitAdminConfig{
Username: getEnv("INIT_ADMIN_USERNAME", "admin"),
Password: getEnv("INIT_ADMIN_PASSWORD", ""), // 为空时随机生成
Email: getEnv("INIT_ADMIN_EMAIL", "admin@example.com"),
},
},
}
}
func getEnv(key, defaultVal string) string {
if v := os.Getenv(key); v != "" {
return v
}
return defaultVal
}
func getEnvInt(key string, defaultVal int) int {
if v := os.Getenv(key); v != "" {
if i, err := strconv.Atoi(v); err == nil {
return i
}
}
return defaultVal
}
func getEnvBool(key string, defaultVal bool) bool {
if v := os.Getenv(key); v != "" {
if b, err := strconv.ParseBool(v); err == nil {
return b
}
}
return defaultVal
}
func getEnvDuration(key string, defaultVal time.Duration) time.Duration {
if v := os.Getenv(key); v != "" {
if d, err := time.ParseDuration(v); err == nil {
return d
}
}
return defaultVal
}

@ -0,0 +1,24 @@
package model
import (
"time"
"github.com/google/uuid"
"gorm.io/gorm"
)
// Base 基础模型
type Base struct {
ID string `json:"id" gorm:"primaryKey;type:varchar(36)"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
}
// BeforeCreate 自动生成UUID
func (b *Base) BeforeCreate(tx *gorm.DB) error {
if b.ID == "" {
b.ID = uuid.New().String()
}
return nil
}

@ -0,0 +1,268 @@
package model
import (
"fmt"
"github.com/veypi/vbase/internal/config"
"gorm.io/driver/mysql"
"gorm.io/driver/postgres"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
var DB *gorm.DB
// InitDB 初始化数据库
func InitDB() error {
cfg := config.C.Database
var dialector gorm.Dialector
switch cfg.Type {
case "mysql":
dialector = mysql.Open(cfg.DSN)
case "postgres":
dialector = postgres.Open(cfg.DSN)
case "sqlite":
dialector = sqlite.Open(cfg.DSN)
default:
return fmt.Errorf("unsupported database type: %s", cfg.Type)
}
var err error
DB, err = gorm.Open(dialector, &gorm.Config{
Logger: logger.Default.LogMode(logger.Info),
})
if err != nil {
return fmt.Errorf("failed to connect database: %w", err)
}
sqlDB, err := DB.DB()
if err != nil {
return err
}
sqlDB.SetMaxOpenConns(cfg.MaxOpenConns)
sqlDB.SetMaxIdleConns(cfg.MaxIdleConns)
sqlDB.SetConnMaxLifetime(cfg.ConnMaxLifetime)
return nil
}
// AutoMigrate 自动迁移表结构
func AutoMigrate() error {
return DB.AutoMigrate(
&User{},
&Identity{},
&Session{},
&Org{},
&OrgMember{},
&Policy{},
&Role{},
&OAuthClient{},
&OAuthAuthorization{},
&OAuthToken{},
)
}
// InitSystemData 初始化系统数据
func InitSystemData() error {
return DB.Transaction(func(tx *gorm.DB) error {
// 1. 创建系统策略
if err := initSystemPolicies(tx); err != nil {
return err
}
// 2. 检查是否需要创建初始管理员
var count int64
if err := tx.Model(&User{}).Count(&count).Error; err != nil {
return err
}
if count == 0 {
if err := initAdminUser(tx); err != nil {
return err
}
}
return nil
})
}
// initSystemPolicies 创建系统内置策略
func initSystemPolicies(tx *gorm.DB) error {
policies := []Policy{
{
Code: SysPolicyUserReadOwn,
Name: "读取自己",
Description: "用户读取自己的信息",
Resource: "user",
Action: "read",
Condition: "resource.id == user.id",
Effect: EffectAllow,
IsSystem: true,
},
{
Code: SysPolicyUserUpdateOwn,
Name: "更新自己",
Description: "用户更新自己的信息",
Resource: "user",
Action: "update",
Condition: "resource.id == user.id",
Effect: EffectAllow,
IsSystem: true,
},
{
Code: SysPolicyUserDeleteOwn,
Name: "删除自己",
Description: "用户删除自己的账号",
Resource: "user",
Action: "delete",
Condition: "resource.id == user.id",
Effect: EffectAllow,
IsSystem: true,
},
{
Code: SysPolicyOrgAdmin,
Name: "组织管理员",
Description: "组织所有者拥有所有权限",
Resource: "*",
Action: "*",
Condition: "org.owner_id == user.id",
Effect: EffectAllow,
Priority: 100,
IsSystem: true,
},
{
Code: SysPolicyOrgRead,
Name: "读取组织",
Description: "组织成员可读组织信息",
Resource: "org",
Action: "read",
Condition: "member.org_id == org.id",
Effect: EffectAllow,
IsSystem: true,
},
{
Code: SysPolicyMemberRead,
Name: "读取成员",
Description: "读取组织成员列表",
Resource: "member",
Action: "read",
Condition: "member.org_id == org.id",
Effect: EffectAllow,
IsSystem: true,
},
{
Code: SysPolicyMemberManage,
Name: "管理成员",
Description: "管理组织成员",
Resource: "member",
Action: "*",
Condition: "",
Effect: EffectAllow,
IsSystem: true,
},
{
Code: SysPolicyRoleRead,
Name: "读取角色",
Description: "读取组织角色",
Resource: "role",
Action: "read",
Condition: "resource.org_id == org.id",
Effect: EffectAllow,
IsSystem: true,
},
{
Code: SysPolicyRoleManage,
Name: "管理角色",
Description: "管理组织角色",
Resource: "role",
Action: "*",
Condition: "",
Effect: EffectAllow,
IsSystem: true,
},
{
Code: SysPolicyPolicyRead,
Name: "读取策略",
Description: "读取策略",
Resource: "policy",
Action: "read",
Condition: "",
Effect: EffectAllow,
IsSystem: true,
},
{
Code: SysPolicyPolicyManage,
Name: "管理策略",
Description: "管理策略",
Resource: "policy",
Action: "*",
Condition: "",
Effect: EffectAllow,
IsSystem: true,
},
}
for _, p := range policies {
var existing Policy
if err := tx.Where("code = ?", p.Code).First(&existing).Error; err != nil {
if err == gorm.ErrRecordNotFound {
if err := tx.Create(&p).Error; err != nil {
return err
}
} else {
return err
}
}
}
return nil
}
// initAdminUser 创建初始管理员
func initAdminUser(tx *gorm.DB) error {
adminCfg := config.C.App.InitAdmin
// 生成随机密码(如果未配置)
password := adminCfg.Password
if password == "" {
password = generateRandomPassword(16)
fmt.Printf("\n========================================\n")
fmt.Printf("Initial admin user created!\n")
fmt.Printf("Username: %s\n", adminCfg.Username)
fmt.Printf("Password: %s\n", password)
fmt.Printf("Email: %s\n", adminCfg.Email)
fmt.Printf("========================================\n\n")
}
// 密码哈希这里需要crypto包后续实现
// hashedPassword, _ := crypto.HashPassword(password)
admin := &User{
Username: adminCfg.Username,
Password: password, // TODO: hash password
Email: adminCfg.Email,
Nickname: "Administrator",
Status: UserStatusActive,
EmailVerified: true,
PhoneVerified: false,
}
if err := tx.Create(admin).Error; err != nil {
return err
}
return nil
}
// generateRandomPassword 生成随机密码
func generateRandomPassword(length int) string {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*"
result := make([]byte, length)
for i := range result {
result[i] = charset[i%len(charset)]
}
return string(result)
}

@ -0,0 +1,92 @@
package model
import (
"time"
)
// OAuthClient OAuth客户端
type OAuthClient struct {
Base
Name string `json:"name" gorm:"size:50;not null"`
Description string `json:"description" gorm:"size:200"`
ClientID string `json:"client_id" gorm:"uniqueIndex;size:32;not null"`
ClientSecret string `json:"-" gorm:"size:64;not null"`
RedirectURIs string `json:"redirect_uris" gorm:"type:text"` // 逗号分隔
GrantTypes string `json:"grant_types" gorm:"size:100"` // authorization_code/refresh_token/client_credentials
ResponseTypes string `json:"response_types" gorm:"size:50"` // code/token
AllowedScopes string `json:"allowed_scopes" gorm:"size:200"` // openid profile email org roles
TokenExpiry int `json:"token_expiry" gorm:"default:3600"` // access_token有效期(秒)
RefreshExpiry int `json:"refresh_expiry" gorm:"default:2592000"`
OwnerID string `json:"owner_id" gorm:"not null"`
OrgID string `json:"org_id" gorm:"index"`
Status int `json:"status" gorm:"default:1"`
}
func (OAuthClient) TableName() string {
return "oauth_clients"
}
// OAuthAuthorization 授权码
type OAuthAuthorization struct {
Base
UserID string `json:"user_id" gorm:"index;not null"`
ClientID string `json:"client_id" gorm:"index;not null"`
OrgID string `json:"org_id" gorm:"index"`
Code string `json:"code" gorm:"uniqueIndex;size:64"`
Scope string `json:"scope" gorm:"size:200"`
State string `json:"state" gorm:"size:100"`
RedirectURI string `json:"redirect_uri" gorm:"size:500"`
CodeChallenge string `json:"-" gorm:"size:128"`
CodeChallengeMethod string `json:"-" gorm:"size:10"`
Used bool `json:"used" gorm:"default:false"`
UsedAt *time.Time `json:"used_at"`
ExpiresAt time.Time `json:"expires_at"`
}
func (OAuthAuthorization) TableName() string {
return "oauth_authorizations"
}
// OAuthToken OAuth访问令牌
type OAuthToken struct {
Base
UserID string `json:"user_id" gorm:"index;not null"`
ClientID string `json:"client_id" gorm:"index;not null"`
OrgID string `json:"org_id" gorm:"index"`
AccessToken string `json:"-" gorm:"uniqueIndex;size:64"`
RefreshToken string `json:"-" gorm:"uniqueIndex;size:64"`
TokenType string `json:"token_type" gorm:"size:10;default:Bearer"`
Scope string `json:"scope" gorm:"size:200"`
ExpiresAt time.Time `json:"expires_at"`
Revoked bool `json:"revoked" gorm:"default:false"`
RevokedAt *time.Time `json:"revoked_at"`
}
func (OAuthToken) TableName() string {
return "oauth_tokens"
}
// Grant Types
const (
GrantTypeAuthorizationCode = "authorization_code"
GrantTypeRefreshToken = "refresh_token"
GrantTypeClientCredentials = "client_credentials"
GrantTypePassword = "password"
)
// Response Types
const (
ResponseTypeCode = "code"
ResponseTypeToken = "token"
)
// Scopes
const (
ScopeOpenID = "openid"
ScopeProfile = "profile"
ScopeEmail = "email"
ScopePhone = "phone"
ScopeOrg = "org"
ScopeRoles = "roles"
ScopeOffline = "offline_access"
)

@ -0,0 +1,61 @@
package model
// Org 组织/租户
type Org struct {
Base
Name string `json:"name" gorm:"size:50;not null"`
Code string `json:"code" gorm:"uniqueIndex;size:30;not null"`
OwnerID string `json:"owner_id" gorm:"not null"`
ParentID *string `json:"parent_id" gorm:"index"`
Path string `json:"path" gorm:"size:500;index"`
Level int `json:"level" gorm:"default:0"`
LeaderID *string `json:"leader_id"`
Description string `json:"description" gorm:"size:200"`
Logo string `json:"logo" gorm:"size:500"`
Settings string `json:"-" gorm:"type:text"` // JSON配置
Status int `json:"status" gorm:"default:1"`
MaxMembers int `json:"max_members" gorm:"default:100"`
}
func (Org) TableName() string {
return "orgs"
}
// OrgMember 组织成员关系
type OrgMember struct {
Base
OrgID string `json:"org_id" gorm:"type:varchar(36);uniqueIndex:idx_org_user;not null"`
UserID string `json:"user_id" gorm:"type:varchar(36);uniqueIndex:idx_org_user;not null"`
RoleIDs string `json:"role_ids" gorm:"size:200"` // 逗号分隔
Position string `json:"position" gorm:"size:50"`
Department string `json:"department" gorm:"size:50"`
JoinedAt string `json:"joined_at"`
Status int `json:"status" gorm:"default:1"` // 0:待审核 1:正常 2:禁用
}
func (OrgMember) TableName() string {
return "org_members"
}
// OrgStatus 组织状态
const (
OrgStatusDisabled = 0
OrgStatusActive = 1
)
// MemberStatus 成员状态
const (
MemberStatusPending = 0
MemberStatusActive = 1
MemberStatusDisabled = 2
)
// IsOwner 检查用户是否是组织所有者
func (o *Org) IsOwner(userID string) bool {
return o.OwnerID == userID
}
// IsMemberActive 成员是否有效
func (m *OrgMember) IsMemberActive() bool {
return m.Status == MemberStatusActive
}

@ -0,0 +1,65 @@
package model
// Policy 策略定义
type Policy struct {
Base
OrgID string `json:"org_id" gorm:"index"` // 空表示全局策略
Code string `json:"code" gorm:"uniqueIndex;size:50;not null"`
Name string `json:"name" gorm:"size:50;not null"`
Description string `json:"description" gorm:"size:200"`
Resource string `json:"resource" gorm:"size:50;not null"` // 资源类型
Action string `json:"action" gorm:"size:20;not null"` // read/create/update/delete/*
Condition string `json:"condition" gorm:"type:text"` // CEL表达式
Effect string `json:"effect" gorm:"size:10;default:allow"`
Priority int `json:"priority" gorm:"default:0"`
IsSystem bool `json:"is_system" gorm:"default:false"`
}
func (Policy) TableName() string {
return "policies"
}
// Role 角色
type Role struct {
Base
OrgID string `json:"org_id" gorm:"index;not null"`
Code string `json:"code" gorm:"size:50;not null"`
Name string `json:"name" gorm:"size:50;not null"`
Description string `json:"description" gorm:"size:200"`
PolicyIDs string `json:"policy_ids" gorm:"type:text"` // 逗号分隔
IsDefault bool `json:"is_default" gorm:"default:false"`
IsSystem bool `json:"is_system" gorm:"default:false"`
SortOrder int `json:"sort_order" gorm:"default:0"`
}
func (Role) TableName() string {
return "roles"
}
// Effect 常量
const (
EffectAllow = "allow"
EffectDeny = "deny"
)
// System Policies 系统内置策略编码
const (
SysPolicyUserReadOwn = "sys:user:read:own"
SysPolicyUserUpdateOwn = "sys:user:update:own"
SysPolicyUserDeleteOwn = "sys:user:delete:own"
SysPolicyOrgAdmin = "sys:org:admin"
SysPolicyOrgRead = "sys:org:read"
SysPolicyMemberRead = "sys:member:read"
SysPolicyMemberManage = "sys:member:manage"
SysPolicyRoleRead = "sys:role:read"
SysPolicyRoleManage = "sys:role:manage"
SysPolicyPolicyRead = "sys:policy:read"
SysPolicyPolicyManage = "sys:policy:manage"
)
// System Roles 系统内置角色编码
const (
SysRoleOrgOwner = "owner"
SysRoleOrgAdmin = "admin"
SysRoleOrgMember = "member"
)

@ -0,0 +1,72 @@
package model
import (
"time"
)
// User 全局用户表
type User struct {
Base
Username string `json:"username" gorm:"uniqueIndex;size:50;not null"`
Password string `json:"-" gorm:"size:255"` // bcrypt hash
Nickname string `json:"nickname" gorm:"size:50"`
Avatar string `json:"avatar" gorm:"size:500"`
Email string `json:"email" gorm:"uniqueIndex;size:100"`
Phone string `json:"phone" gorm:"uniqueIndex;size:20"`
Status int `json:"status" gorm:"default:1"` // 0:禁用 1:正常 2:未激活
EmailVerified bool `json:"email_verified" gorm:"default:false"`
PhoneVerified bool `json:"phone_verified" gorm:"default:false"`
LastLoginAt *time.Time `json:"last_login_at"`
}
// TableName 表名
func (User) TableName() string {
return "users"
}
// Identity 第三方身份绑定
type Identity struct {
Base
UserID string `json:"user_id" gorm:"index;not null"`
Provider string `json:"provider" gorm:"size:20;not null"` // google/github/wechat/ldap
ProviderUID string `json:"provider_uid" gorm:"index;size:100;not null"` // 第三方唯一ID
ProviderName string `json:"provider_name" gorm:"size:50"`
Avatar string `json:"avatar" gorm:"size:500"`
Email string `json:"email" gorm:"size:100"`
AccessToken string `json:"-" gorm:"size:500"`
RefreshToken string `json:"-" gorm:"size:500"`
ExpiresAt *time.Time `json:"-"`
}
func (Identity) TableName() string {
return "identities"
}
// Session 登录会话
type Session struct {
Base
UserID string `json:"user_id" gorm:"index;not null"`
TokenID string `json:"token_id" gorm:"uniqueIndex;size:36"` // JWT jti
Type string `json:"type" gorm:"size:20;not null"` // access/refresh
DeviceInfo string `json:"device_info" gorm:"size:200"`
IP string `json:"ip" gorm:"size:50"`
ExpiresAt time.Time `json:"expires_at"`
Revoked bool `json:"revoked" gorm:"default:false"`
RevokedAt *time.Time `json:"revoked_at"`
}
func (Session) TableName() string {
return "sessions"
}
// UserStatus 用户状态常量
const (
UserStatusDisabled = 0
UserStatusActive = 1
UserStatusInactive = 2
)
// IsActive 用户是否激活
func (u *User) IsActive() bool {
return u.Status == UserStatusActive
}

@ -0,0 +1,54 @@
package crypto
import (
"crypto/rand"
"encoding/base64"
"fmt"
"golang.org/x/crypto/bcrypt"
)
// HashPassword 使用bcrypt哈希密码
func HashPassword(password string, cost int) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), cost)
if err != nil {
return "", fmt.Errorf("failed to hash password: %w", err)
}
return string(bytes), nil
}
// VerifyPassword 验证密码
func VerifyPassword(password, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
}
// GenerateRandomString 生成随机字符串
func GenerateRandomString(length int) (string, error) {
bytes := make([]byte, length)
if _, err := rand.Read(bytes); err != nil {
return "", err
}
return base64.URLEncoding.EncodeToString(bytes)[:length], nil
}
// GenerateSecret 生成密钥
func GenerateSecret(length int) string {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
bytes := make([]byte, length)
rand.Read(bytes)
for i, b := range bytes {
bytes[i] = charset[b%byte(len(charset))]
}
return string(bytes)
}
// GenerateClientID 生成OAuth客户端ID
func GenerateClientID() string {
return "vc_" + GenerateSecret(28)
}
// GenerateClientSecret 生成OAuth客户端密钥
func GenerateClientSecret() string {
return GenerateSecret(64)
}

@ -0,0 +1,166 @@
package jwt
import (
"errors"
"fmt"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/google/uuid"
"github.com/veypi/vbase/internal/config"
)
var (
ErrInvalidToken = errors.New("invalid token")
ErrExpiredToken = errors.New("token expired")
ErrTokenRevoked = errors.New("token revoked")
)
// Claims JWT声明
type Claims struct {
jwt.RegisteredClaims
UserID string `json:"uid"`
Username string `json:"username"`
Nickname string `json:"nickname"`
Avatar string `json:"avatar"`
Email string `json:"email"`
Orgs []OrgClaim `json:"orgs,omitempty"`
Type string `json:"type"` // access/refresh
Scope string `json:"scope,omitempty"`
}
// OrgClaim 组织声明
type OrgClaim struct {
OrgID string `json:"org_id"`
Code string `json:"code"`
Name string `json:"name"`
Roles []string `json:"roles"`
Status int `json:"status"`
}
// TokenPair Token对
type TokenPair struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
TokenType string `json:"token_type"`
ExpiresIn int `json:"expires_in"`
}
// GenerateTokenPair 生成token对
func GenerateTokenPair(userID, username, nickname, avatar, email string, orgs []OrgClaim) (*TokenPair, error) {
accessToken, err := GenerateAccessToken(userID, username, nickname, avatar, email, orgs)
if err != nil {
return nil, err
}
refreshToken, err := GenerateRefreshToken(userID)
if err != nil {
return nil, err
}
return &TokenPair{
AccessToken: accessToken,
RefreshToken: refreshToken,
TokenType: "Bearer",
ExpiresIn: int(config.C.JWT.AccessExpiry.Seconds()),
}, nil
}
// GenerateAccessToken 生成访问令牌
func GenerateAccessToken(userID, username, nickname, avatar, email string, orgs []OrgClaim) (string, error) {
now := time.Now()
claims := Claims{
RegisteredClaims: jwt.RegisteredClaims{
ID: uuid.New().String(), // jti
Issuer: config.C.JWT.Issuer,
Subject: userID,
Audience: jwt.ClaimStrings{config.C.App.ID},
IssuedAt: jwt.NewNumericDate(now),
NotBefore: jwt.NewNumericDate(now),
ExpiresAt: jwt.NewNumericDate(now.Add(config.C.JWT.AccessExpiry)),
},
UserID: userID,
Username: username,
Nickname: nickname,
Avatar: avatar,
Email: email,
Orgs: orgs,
Type: "access",
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(config.C.JWT.Secret))
}
// GenerateRefreshToken 生成刷新令牌
func GenerateRefreshToken(userID string) (string, error) {
now := time.Now()
claims := Claims{
RegisteredClaims: jwt.RegisteredClaims{
ID: uuid.New().String(),
Issuer: config.C.JWT.Issuer,
Subject: userID,
IssuedAt: jwt.NewNumericDate(now),
ExpiresAt: jwt.NewNumericDate(now.Add(config.C.JWT.RefreshExpiry)),
},
UserID: userID,
Type: "refresh",
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(config.C.JWT.Secret))
}
// ParseToken 解析Token
func ParseToken(tokenString string) (*Claims, error) {
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return []byte(config.C.JWT.Secret), nil
})
if err != nil {
if errors.Is(err, jwt.ErrTokenExpired) {
return nil, ErrExpiredToken
}
return nil, ErrInvalidToken
}
if claims, ok := token.Claims.(*Claims); ok && token.Valid {
return claims, nil
}
return nil, ErrInvalidToken
}
// GetJTI 获取Token ID
func GetJTI(tokenString string) (string, error) {
claims, err := ParseToken(tokenString)
if err != nil {
return "", err
}
return claims.ID, nil
}
// GetExpiration 获取过期时间
func GetExpiration(tokenString string) (time.Time, error) {
claims, err := ParseToken(tokenString)
if err != nil {
return time.Time{}, err
}
if claims.ExpiresAt == nil {
return time.Time{}, errors.New("no expiration")
}
return claims.ExpiresAt.Time, nil
}
// IsAccessToken 是否是访问令牌
func IsAccessToken(claims *Claims) bool {
return claims.Type == "access"
}
// IsRefreshToken 是否是刷新令牌
func IsRefreshToken(claims *Claims) bool {
return claims.Type == "refresh"
}

@ -0,0 +1,258 @@
package service
import (
"fmt"
"strings"
"time"
"github.com/veypi/vbase/internal/cache"
"github.com/veypi/vbase/internal/model"
)
// PermissionChecker 权限检查器
type PermissionChecker struct{}
// NewPermissionChecker 创建权限检查器
func NewPermissionChecker() *PermissionChecker {
return &PermissionChecker{}
}
// CheckResult 检查结果
type CheckResult struct {
Allowed bool `json:"allowed"`
Reason string `json:"reason,omitempty"`
}
// Check 检查权限
func (pc *PermissionChecker) Check(userID, orgID, resource, action string, resourceData map[string]any) (*CheckResult, error) {
// 1. 检查缓存
if cache.IsEnabled() {
allowed, cached, err := cache.GetPermission(userID, orgID, resource, action)
if err == nil && cached {
return &CheckResult{Allowed: allowed}, nil
}
}
// 2. 检查是否是组织所有者(拥有所有权限)
if orgID != "" {
var org model.Org
if err := model.DB.First(&org, "id = ?", orgID).Error; err == nil {
if org.OwnerID == userID {
pc.cacheResult(userID, orgID, resource, action, true)
return &CheckResult{Allowed: true, Reason: "organization owner"}, nil
}
}
}
// 3. 获取用户在该组织的角色
var member model.OrgMember
if err := model.DB.Where("org_id = ? AND user_id = ? AND status = ?", orgID, userID, model.MemberStatusActive).First(&member).Error; err != nil {
// 用户不在组织中
pc.cacheResult(userID, orgID, resource, action, false)
return &CheckResult{Allowed: false, Reason: "not a member of organization"}, nil
}
// 4. 获取角色关联的策略
roleIDs := parseRoles(member.RoleIDs)
if len(roleIDs) == 0 {
pc.cacheResult(userID, orgID, resource, action, false)
return &CheckResult{Allowed: false, Reason: "no roles assigned"}, nil
}
var roles []model.Role
if err := model.DB.Where("id IN ?", roleIDs).Find(&roles).Error; err != nil {
return nil, err
}
// 5. 收集所有策略ID
policyIDMap := make(map[string]bool)
for _, role := range roles {
ids := parseRoles(role.PolicyIDs)
for _, id := range ids {
policyIDMap[id] = true
}
}
if len(policyIDMap) == 0 {
pc.cacheResult(userID, orgID, resource, action, false)
return &CheckResult{Allowed: false, Reason: "no policies assigned"}, nil
}
policyIDs := make([]string, 0, len(policyIDMap))
for id := range policyIDMap {
policyIDs = append(policyIDs, id)
}
// 6. 获取策略详情
var policies []model.Policy
if err := model.DB.Where("id IN ?", policyIDs).Find(&policies).Error; err != nil {
return nil, err
}
// 7. 评估策略
allowed := pc.evaluatePolicies(policies, userID, orgID, resource, action, resourceData)
pc.cacheResult(userID, orgID, resource, action, allowed)
if allowed {
return &CheckResult{Allowed: true}, nil
}
return &CheckResult{Allowed: false, Reason: "policy denied"}, nil
}
// evaluatePolicies 评估策略
func (pc *PermissionChecker) evaluatePolicies(policies []model.Policy, userID, orgID, resource, action string, resourceData map[string]any) bool {
// 先处理deny策略
for _, p := range policies {
if p.Effect != model.EffectDeny {
continue
}
if pc.matchPolicy(&p, resource, action) {
// 检查条件
if pc.evaluateCondition(&p, userID, orgID, resourceData) {
return false
}
}
}
// 再处理allow策略
for _, p := range policies {
if p.Effect != model.EffectAllow {
continue
}
if pc.matchPolicy(&p, resource, action) {
if pc.evaluateCondition(&p, userID, orgID, resourceData) {
return true
}
}
}
return false
}
// matchPolicy 匹配策略资源和方法
func (pc *PermissionChecker) matchPolicy(p *model.Policy, resource, action string) bool {
// 资源匹配
if p.Resource != "*" && p.Resource != resource {
return false
}
// 动作匹配
if p.Action != "*" && p.Action != action {
return false
}
return true
}
// evaluateCondition 评估条件
func (pc *PermissionChecker) evaluateCondition(p *model.Policy, userID, orgID string, resourceData map[string]any) bool {
condition := p.Condition
if condition == "" || condition == "true" {
return true
}
// 简单条件评估
switch condition {
case "owner":
// 检查是否是资源所有者
if ownerID, ok := resourceData["owner_id"].(string); ok {
return ownerID == userID
}
if createdBy, ok := resourceData["created_by"].(string); ok {
return createdBy == userID
}
return false
case "org_member":
// 检查是否是组织成员
if orgID == "" {
return false
}
var count int64
model.DB.Model(&model.OrgMember{}).Where("org_id = ? AND user_id = ? AND status = ?", orgID, userID, model.MemberStatusActive).Count(&count)
return count > 0
default:
// 其他复杂条件暂时返回true
return true
}
}
// cacheResult 缓存结果
func (pc *PermissionChecker) cacheResult(userID, orgID, resource, action string, allowed bool) {
if cache.IsEnabled() {
cache.SetPermission(userID, orgID, resource, action, allowed, 1*time.Minute)
}
}
// GetUserPermissions 获取用户的所有权限
func (pc *PermissionChecker) GetUserPermissions(userID, orgID string) ([]string, error) {
// 获取用户角色
var member model.OrgMember
if err := model.DB.Where("org_id = ? AND user_id = ? AND status = ?", orgID, userID, model.MemberStatusActive).First(&member).Error; err != nil {
return []string{}, nil
}
roleIDs := parseRoles(member.RoleIDs)
if len(roleIDs) == 0 {
return []string{}, nil
}
var roles []model.Role
if err := model.DB.Where("id IN ?", roleIDs).Find(&roles).Error; err != nil {
return nil, err
}
// 收集策略
policyIDMap := make(map[string]bool)
for _, role := range roles {
ids := parseRoles(role.PolicyIDs)
for _, id := range ids {
policyIDMap[id] = true
}
}
if len(policyIDMap) == 0 {
return []string{}, nil
}
policyIDs := make([]string, 0, len(policyIDMap))
for id := range policyIDMap {
policyIDs = append(policyIDs, id)
}
var policies []model.Policy
if err := model.DB.Where("id IN ?", policyIDs).Find(&policies).Error; err != nil {
return nil, err
}
// 格式化权限
perms := make([]string, 0, len(policies))
for _, p := range policies {
if p.Effect == model.EffectAllow {
perms = append(perms, fmt.Sprintf("%s:%s", p.Resource, p.Action))
}
}
return perms, nil
}
// ClearUserPermissionCache 清除用户权限缓存
func ClearUserPermissionCache(userID string) {
if cache.IsEnabled() {
cache.DeleteUserPermissions(userID)
}
}
// ClearOrgPermissionCache 清除组织权限缓存
func ClearOrgPermissionCache(orgID string) {
if cache.IsEnabled() {
cache.DeleteOrgPermissions(orgID)
}
}
func parseRoles(roleIDs string) []string {
if roleIDs == "" {
return []string{}
}
return strings.Split(roleIDs, ",")
}
// InitPermissionChecker 初始化权限检查器
var InitPermissionChecker = NewPermissionChecker()

@ -0,0 +1,940 @@
# vbase 用户权限系统重构设计
## 一、设计原则
1. **标准化**:遵循 OAuth2.0 / OIDC 标准协议
2. **无状态**:服务无状态,水平扩展友好
3. **安全性**密码bcrypt、JWT签名、HTTPS强制
4. **性能**Redis缓存热点数据1分钟TTL自动刷新
5. **可维护性**:清晰分层,单一职责
---
## 二、系统架构
```
┌─────────────────────────────────────────────────────────────┐
│ 接入层 │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
│ │ REST API │ │ OAuth2.0 │ │ OIDC Discovery │ │
│ └──────┬──────┘ └──────┬──────┘ └──────────┬──────────┘ │
└─────────┼────────────────┼────────────────────┼────────────┘
│ │ │
┌─────────▼────────────────▼────────────────────▼────────────┐
│ 服务层 │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
│ │ Auth服务 │ │ Org服务 │ │ OAuth服务 │ │
│ │ (认证) │ │ (组织) │ │ (第三方接入) │ │
│ └─────────────┘ └─────────────┘ └─────────────────────┘ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
│ │ User服务 │ │ Role服务 │ │ Permission服务 │ │
│ │ (用户) │ │ (角色) │ │ (权限) │ │
│ └─────────────┘ └─────────────┘ └─────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
┌─────────▼──────────────────────────────────────────────────┐
│ 数据层 │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
│ │ MySQL/PSQL │ │ Redis │ │ (可选)Etcd │ │
│ │ (主存储) │ │ (缓存/会话) │ │ (配置中心) │ │
│ └─────────────┘ └─────────────┘ └─────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
---
## 三、核心数据模型
### 3.1 身份认证模型
```go
// User - 全局用户表
// 一个用户可属于多个组织,但登录是全局的
type User struct {
ID string `json:"id" gorm:"primaryKey;type:varchar(36)"`
Username string `json:"username" gorm:"uniqueIndex;size:50;not null"`
Password string `json:"-" gorm:"size:255"` // bcrypt hash第三方登录可为空
Nickname string `json:"nickname" gorm:"size:50"`
Avatar string `json:"avatar" gorm:"size:500"`
Email string `json:"email" gorm:"uniqueIndex;size:100"`
Phone string `json:"phone" gorm:"uniqueIndex;size:20"`
Status int `json:"status" gorm:"default:1"` // 0:禁用 1:正常 2:未激活
EmailVerified bool `json:"email_verified" gorm:"default:false"`
PhoneVerified bool `json:"phone_verified" gorm:"default:false"`
LastLoginAt *time.Time `json:"last_login_at"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
}
// Identity - 第三方身份绑定
// 支持多种登录方式绑定到同一账号
type Identity struct {
ID string `json:"id" gorm:"primaryKey;type:varchar(36)"`
UserID string `json:"user_id" gorm:"index;not null"`
Provider string `json:"provider" gorm:"size:20;not null"` // google/github/wechat/ldap
ProviderUID string `json:"provider_uid" gorm:"index;size:100;not null"` // 第三方唯一ID
ProviderName string `json:"provider_name" gorm:"size:50"`
Avatar string `json:"avatar" gorm:"size:500"`
Email string `json:"email" gorm:"size:100"`
AccessToken string `json:"-" gorm:"size:500"` // 加密存储
RefreshToken string `json:"-" gorm:"size:500"`
ExpiresAt *time.Time `json:"-"`
CreatedAt time.Time `json:"created_at"`
}
// Session - 登录会话
// 用于多端登录管理和撤销
type Session struct {
ID string `json:"id" gorm:"primaryKey;type:varchar(36)"`
UserID string `json:"user_id" gorm:"index;not null"`
TokenID string `json:"token_id" gorm:"uniqueIndex;size:36"` // JWT jti
Type string `json:"type" gorm:"size:20;not null"` // access/refresh
DeviceInfo string `json:"device_info" gorm:"size:200"`
IP string `json:"ip" gorm:"size:50"`
ExpiresAt time.Time `json:"expires_at"`
Revoked bool `json:"revoked" gorm:"default:false"`
RevokedAt *time.Time `json:"revoked_at"`
CreatedAt time.Time `json:"created_at"`
}
```
### 3.2 组织架构模型
```go
// Org - 组织/租户
// 完全隔离的数据边界
type Org struct {
ID string `json:"id" gorm:"primaryKey;type:varchar(36)"`
Name string `json:"name" gorm:"size:50;not null"`
Code string `json:"code" gorm:"uniqueIndex;size:30;not null"` // 组织唯一编码
OwnerID string `json:"owner_id" gorm:"not null"` // 创建者/所有者
// 树形结构
ParentID *string `json:"parent_id" gorm:"index"`
Path string `json:"path" gorm:"size:500;index"` // 完整路径 /root/tech/backend
Level int `json:"level" gorm:"default:0"`
// 配置
Description string `json:"description" gorm:"size:200"`
Logo string `json:"logo" gorm:"size:500"`
Settings string `json:"-" gorm:"type:text"` // JSON配置
// 状态
Status int `json:"status" gorm:"default:1"` // 0:禁用 1:正常
MaxMembers int `json:"max_members" gorm:"default:100"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// OrgMember - 组织成员关系
// 用户与组织的多对多关系
type OrgMember struct {
ID string `json:"id" gorm:"primaryKey;type:varchar(36)"`
OrgID string `json:"org_id" gorm:"uniqueIndex:idx_org_user;not null"`
UserID string `json:"user_id" gorm:"uniqueIndex:idx_org_user;not null"`
// 角色(多个角色,逗号分隔)
RoleIDs string `json:"role_ids" gorm:"size:200"`
// 成员信息
Position string `json:"position" gorm:"size:50"` // 职位
Department string `json:"department" gorm:"size:50"` // 部门名称(冗余)
JoinedAt time.Time `json:"joined_at"`
// 状态 0:待审核 1:正常 2:禁用
Status int `json:"status" gorm:"default:1"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
```
### 3.3 权限模型RBAC + ABAC
```go
// Role - 角色
// 组织级别的角色定义
type Role struct {
ID string `json:"id" gorm:"primaryKey;type:varchar(36)"`
OrgID string `json:"org_id" gorm:"index;not null"`
Code string `json:"code" gorm:"size:50;not null"` // 角色编码
Name string `json:"name" gorm:"size:50;not null"`
Description string `json:"description" gorm:"size:200"`
// 关联策略
PolicyIDs string `json:"policy_ids" gorm:"type:text"` // 逗号分隔
// 属性
IsDefault bool `json:"is_default" gorm:"default:false"` // 新成员默认角色
IsSystem bool `json:"is_system" gorm:"default:false"` // 系统内置(不可删)
SortOrder int `json:"sort_order" gorm:"default:0"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// Policy - 策略
// 细粒度权限定义支持ABAC
type Policy struct {
ID string `json:"id" gorm:"primaryKey;type:varchar(36)"`
OrgID string `json:"org_id" gorm:"index"` // 空表示全局策略
Code string `json:"code" gorm:"uniqueIndex;size:50;not null"`
Name string `json:"name" gorm:"size:50;not null"`
Description string `json:"description" gorm:"size:200"`
// 资源定义
Resource string `json:"resource" gorm:"size:50;not null"` // 资源类型: user/org/member/role等
Action string `json:"action" gorm:"size:20;not null"` // read/create/update/delete/* /batch_create等
// ABAC条件CEL表达式
// 示例: "resource.owner == user.id" - 只能操作自己的资源
// 示例: "user.roles.exists(r, r == 'admin')" - 需要admin角色
// 示例: "resource.org_id == org.id" - 只能操作当前组织的资源
Condition string `json:"condition" gorm:"type:text"`
// 效果: allow/denydeny优先
Effect string `json:"effect" gorm:"size:10;default:allow"`
// 优先级数字越大优先级越高deny策略建议高优先级
Priority int `json:"priority" gorm:"default:0"`
IsSystem bool `json:"is_system" gorm:"default:false"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
```
### 3.4 OAuth2.0 模型
```go
// OAuthClient - 注册的应用客户端
type OAuthClient struct {
ID string `json:"id" gorm:"primaryKey;type:varchar(36)"`
Name string `json:"name" gorm:"size:50;not null"`
Description string `json:"description" gorm:"size:200"`
// 客户端凭证
ClientID string `json:"client_id" gorm:"uniqueIndex;size:32;not null"`
ClientSecret string `json:"-" gorm:"size:64;not null"`
// OAuth配置
RedirectURIs string `json:"redirect_uris" gorm:"type:text"` // 逗号分隔
GrantTypes string `json:"grant_types" gorm:"size:100"` // authorization_code/refresh_token/client_credentials
ResponseTypes string `json:"response_types" gorm:"size:50"` // code/token
// 访问控制
AllowedScopes string `json:"allowed_scopes" gorm:"size:200"` // openid profile email org roles
TokenExpiry int `json:"token_expiry" gorm:"default:3600"` // access_token有效期(秒)
RefreshExpiry int `json:"refresh_expiry" gorm:"default:2592000"` // refresh_token有效期(秒)
// 归属
OwnerID string `json:"owner_id" gorm:"not null"`
OrgID string `json:"org_id" gorm:"index"` // 可选,绑定特定组织
// 状态 0:禁用 1:正常
Status int `json:"status" gorm:"default:1"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// OAuthAuthorization - 授权码存储
type OAuthAuthorization struct {
ID string `json:"id" gorm:"primaryKey;type:varchar(36)"`
UserID string `json:"user_id" gorm:"index;not null"`
ClientID string `json:"client_id" gorm:"index;not null"`
OrgID string `json:"org_id" gorm:"index"`
// 授权信息
Code string `json:"code" gorm:"uniqueIndex;size:64"`
Scope string `json:"scope" gorm:"size:200"`
State string `json:"state" gorm:"size:100"`
// PKCE
CodeChallenge string `json:"-" gorm:"size:128"`
CodeChallengeMethod string `json:"-" gorm:"size:10"`
// 状态
Used bool `json:"used" gorm:"default:false"`
UsedAt *time.Time `json:"used_at"`
ExpiresAt time.Time `json:"expires_at"`
CreatedAt time.Time `json:"created_at"`
}
// OAuthToken - OAuth访问令牌
type OAuthToken struct {
ID string `json:"id" gorm:"primaryKey;type:varchar(36)"`
UserID string `json:"user_id" gorm:"index;not null"`
ClientID string `json:"client_id" gorm:"index;not null"`
OrgID string `json:"org_id" gorm:"index"`
// Token信息
AccessToken string `json:"-" gorm:"uniqueIndex;size:64"`
RefreshToken string `json:"-" gorm:"uniqueIndex;size:64"`
TokenType string `json:"token_type" gorm:"size:10;default:Bearer"`
Scope string `json:"scope" gorm:"size:200"`
// 有效期
ExpiresAt time.Time `json:"expires_at"`
Revoked bool `json:"revoked" gorm:"default:false"`
RevokedAt *time.Time `json:"revoked_at"`
CreatedAt time.Time `json:"created_at"`
}
```
---
## 四、API 设计
### 4.1 路由结构
```
/api/v1
├── /auth # 认证相关(公开或需基础认证)
│ ├── POST /login
│ ├── POST /logout
│ ├── POST /refresh
│ ├── POST /register
│ ├── POST /forgot-password
│ ├── POST /reset-password
│ ├── GET /captcha
│ ├── GET /oauth/:provider # 第三方登录跳转
│ ├── GET /oauth/:provider/callback
│ └── POST /oauth/bind
├── /me # 当前用户(需认证)
│ ├── GET /
│ ├── PATCH /
│ ├── GET /sessions # 登录会话列表
│ ├── DELETE /sessions/:id # 撤销会话
│ ├── GET /identities # 绑定的第三方账号
│ ├── DELETE /identities/:provider # 解绑
│ ├── GET /orgs # 我的组织列表
│ └── POST /change-password
├── /users # 用户管理(需权限)
│ ├── GET /
│ ├── POST /
│ ├── GET /:id
│ ├── PATCH /:id
│ ├── DELETE /:id
│ └── PATCH /:id/status
├── /orgs # 组织管理
│ ├── GET / # 列表
│ ├── POST / # 创建(需登录)
│ ├── GET /:id
│ ├── PATCH /:id # 需组织管理员权限
│ ├── DELETE /:id
│ ├── GET /:id/tree # 组织树
│ ├── GET /:id/members # 成员列表
│ ├── POST /:id/members # 邀请成员
│ ├── GET /:id/members/:user_id
│ ├── PATCH /:id/members/:user_id # 修改角色/状态
│ ├── DELETE /:id/members/:user_id
│ ├── GET /:id/roles # 组织角色
│ ├── POST /:id/roles
│ └── GET /:id/policies # 组织策略
├── /roles # 角色管理(需组织上下文)
│ ├── GET /
│ ├── POST /
│ ├── GET /:id
│ ├── PATCH /:id
│ └── DELETE /:id
├── /policies # 策略管理
│ ├── GET /
│ ├── POST /
│ ├── GET /:id
│ ├── PATCH /:id
│ └── DELETE /:id
├── /oauth # OAuth2.0 服务端
│ ├── GET /authorize # 授权端点
│ ├── POST /token # 令牌端点
│ ├── POST /revoke # 撤销令牌
│ ├── GET /userinfo # 用户信息
│ ├── GET /.well-known/openid-configuration
│ ├── GET /.well-known/jwks.json
│ │
│ └── /clients # 客户端管理(需认证)
│ ├── GET /
│ ├── POST /
│ ├── GET /:id
│ ├── PATCH /:id
│ ├── DELETE /:id
│ └── POST /:id/reset-secret
└── /check # 权限检查(内部服务调用)
└── POST /permission # 检查指定权限
```
### 4.2 认证相关 API
#### POST /api/v1/auth/login
账号密码登录
```json
// Request
{
"username": "string", // 用户名/邮箱/手机号
"password": "string",
"captcha_id": "string", // 可选,错误多次后需要
"captcha_code": "string",
"remember": false // 是否长效token
}
// Response 200
{
"access_token": "eyJhbGciOiJSUzI1NiIs...",
"refresh_token": "dGhpcyBpcyBhIHJlZnJlc2g...",
"token_type": "Bearer",
"expires_in": 3600,
"user": {
"id": "uuid",
"username": "john",
"nickname": "John Doe",
"email": "john@example.com",
"avatar": "https://..."
}
}
```
#### POST /api/v1/auth/oauth/:provider
第三方登录返回跳转URL或直接302跳转
```json
// Query Params
?redirect_uri=https://app.com/callback&state=xyz
// Response (如果是SPA返回URL)
{
"auth_url": "https://accounts.google.com/o/oauth2/v2/auth..."
}
```
#### POST /api/v1/auth/oauth/bind
绑定第三方账号到已有账号
```json
// Request
{
"provider": "google",
"code": "authorization_code_from_provider",
"bind_type": "register" | "login", // 新注册还是绑定已有
"username": "string", // bind_type=register时需要
"password": "string" // bind_type=login时需要
}
// Response
{
"access_token": "...",
"user": {...}
}
```
### 4.3 组织相关 API
#### GET /api/v1/orgs
获取组织列表(用户加入的)
```json
// Response
{
"items": [
{
"id": "uuid",
"name": "技术部",
"code": "tech",
"logo": "https://...",
"owner_id": "uuid",
"member_count": 25,
"my_roles": ["admin", "developer"],
"joined_at": "2024-01-15T10:30:00Z"
}
]
}
```
#### POST /api/v1/orgs
创建组织
```json
// Request
{
"name": "新产品线",
"code": "new-product", // 唯一编码
"description": "...",
"logo": "https://..."
}
// Response
{
"id": "uuid",
"name": "新产品线",
"code": "new-product",
"owner_id": "current_user_id",
// 自动创建owner角色并分配
}
```
**注意**创建者自动成为组织所有者owner拥有所有权限。
### 4.4 权限相关 API
#### GET /api/v1/check/permissions
获取当前用户在指定组织的所有权限(用于前端按钮控制)
```json
// Query Params
?org_id=xxx
// Response
{
"org_id": "uuid",
"roles": ["admin", "developer"],
"permissions": [
"user:read",
"user:update:own",
"org:read",
"member:manage",
"role:read"
]
}
```
#### POST /api/v1/check/permission
检查具体权限(其他服务调用)
```json
// Request
{
"org_id": "uuid",
"resource": "user",
"action": "update",
"resource_id": "target_user_id" // 可选
}
// Response
{
"allowed": true,
"reason": "用户是资源所有者"
}
```
---
## 五、权限系统设计
### 5.1 策略表达式CEL
使用 [CEL (Common Expression Language)](https://github.com/google/cel-go) 作为条件表达式语言。
```go
// 内置变量
user // 当前用户对象
org // 当前组织对象
resource // 被访问资源对象
action // 动作: read/create/update/delete
// 示例策略条件
"true" // 无条件允许
"resource.created_by == user.id" // 只能操作自己创建的资源
"resource.owner_id == user.id" // 资源所有者
"org.members.exists(m, m.user_id == user.id && m.roles.exists(r, r.code == 'admin'))"
"user.id in org.owners" // 组织所有者
"resource.org_id == org.id" // 只能访问当前组织的资源
"resource.status == 'pending' && action == 'update'" // 特定状态才允许操作
```
### 5.2 系统内置策略
| 编码 | 资源 | 动作 | 条件 | 效果 | 说明 |
|------|------|------|------|------|------|
| `sys:user:read:own` | user | read | `resource.id == user.id` | allow | 读自己 |
| `sys:user:update:own` | user | update | `resource.id == user.id` | allow | 改自己 |
| `sys:org:admin` | org | * | `org.owner_id == user.id` | allow | 组织所有者 |
| `sys:member:read` | member | read | `resource.org_id == org.id` | allow | 读成员 |
| `sys:member:manage` | member | * | 需自定义角色 | allow | 管理成员 |
| `sys:role:read` | role | read | `resource.org_id == org.id` | allow | 读角色 |
| `sys:role:manage` | role | * | 需自定义角色 | allow | 管理角色 |
### 5.3 权限检查流程
```go
// 中间件流程
func PermissionMiddleware(resource, action string) vigo.Middleware {
return func(x *vigo.X) error {
// 1. 获取当前用户
user := auth.CurrentUser(x)
if user == nil {
return vigo.ErrUnauthorized
}
// 2. 获取当前组织从Header或Query
orgID := x.Request.Header.Get("X-Org-ID")
if orgID == "" {
orgID = x.Query("org_id")
}
// 3. 构建缓存key
cacheKey := fmt.Sprintf("perm:%s:%s:%s:%s", user.ID, orgID, resource, action)
// 4. 查缓存
if cached, err := redis.Get(cacheKey); err == nil {
if cached == "deny" {
return vigo.ErrForbidden
}
x.Set("permission_checked", true)
return x.Next()
}
// 5. 查询用户在该组织的角色
member := getOrgMember(orgID, user.ID)
if member == nil {
redis.Set(cacheKey, "deny", 1*time.Minute)
return vigo.ErrForbidden
}
// 6. 收集所有策略
policies := getPoliciesByRoles(member.RoleIDs)
// 7. 评估策略
allowed := evaluatePolicies(policies, user, org, resource, action)
// 8. 写入缓存
if allowed {
redis.Set(cacheKey, "allow", 1*time.Minute)
} else {
redis.Set(cacheKey, "deny", 1*time.Minute)
}
if !allowed {
return vigo.ErrForbidden
}
return x.Next()
}
}
```
---
## 六、OAuth2.0 / OIDC 实现
### 6.1 支持的授权流程
1. **Authorization Code Flow**推荐支持PKCE
- 用于服务端应用
- 支持 `code_challenge` (PKCE)
2. **Implicit Flow**(不推荐,但为了兼容支持)
- 用于纯前端应用建议迁移到PKCE
3. **Client Credentials Flow**
- 用于服务间调用
4. **Refresh Token**
- 用于刷新 access_token
### 6.2 OIDC 支持
- `/.well-known/openid-configuration` - 发现端点
- `/.well-known/jwks.json` - 公钥获取
- Scope: `openid profile email org roles`
- ID Token: JWT格式包含用户基本信息
### 6.3 授权流程示例
```
┌─────────────┐ ┌─────────────┐
│ 第三方应用 │ │ vbase │
└──────┬──────┘ └──────┬──────┘
│ │
│ 1. GET /oauth/authorize? │
│ response_type=code&
│ client_id=xxx&
│ redirect_uri=xxx&
│ scope=openid profile org&
│ state=xxx&
│ code_challenge=xxx&
│ code_challenge_method=S256 │
├─────────────────────────────────────────────────►│
│ │
│ 2. 未登录,重定向到登录页 │
│◄─────────────────────────────────────────────────┤
│ │
│ 3. 登录后,用户授权确认页面 │
│ (展示应用请求的权限范围) │
├─────────────────────────────────────────────────►│
│ │
│ 4. 302 重定向到 redirect_uri │
│ ?code=xxx&state=xxx │
│◄─────────────────────────────────────────────────┤
│ │
│ 5. POST /oauth/token │
│ grant_type=authorization_code&
│ code=xxx&
│ redirect_uri=xxx&
│ client_id=xxx&
│ client_secret=xxx&
│ code_verifier=xxx (PKCE) │
├─────────────────────────────────────────────────►│
│ │
│ 6. 返回 Token │
│ { │
│ access_token: "...", │
│ id_token: "...", │
│ refresh_token: "...", │
│ token_type: "Bearer", │
│ expires_in: 3600 │
│ } │
│◄─────────────────────────────────────────────────┤
```
---
## 七、缓存策略
### 7.1 Redis 缓存结构
```
# 权限缓存(核心)
key: perm:{user_id}:{org_id}:{resource}:{action}
value: allow | deny
ttl: 60秒
# 用户信息缓存
key: user:{user_id}
value: JSON
ttl: 300秒
# 组织信息缓存
key: org:{org_id}
value: JSON
ttl: 300秒
# 组织成员缓存
key: org:{org_id}:member:{user_id}
value: JSON (包含角色ID列表)
ttl: 60秒
# 角色策略缓存
key: role:{role_id}:policies
value: JSON数组
ttl: 300秒
# Session/Token 黑名单(用于撤销)
key: token:revoked:{jti}
value: 1
ttl: 与token剩余有效期一致
# 限流计数
key: ratelimit:{ip}:{path}
value: 计数
ttl: 60秒
# 验证码
key: captcha:{captcha_id}
value: 验证码内容
ttl: 300秒
# OAuth 授权码
key: oauth:code:{code}
value: 授权信息JSON
ttl: 600秒
```
### 7.2 缓存更新策略
1. **被动失效**:数据变更时删除缓存
2. **主动刷新**TTL到期自动重新加载
3. **权限缓存**短TTL1分钟保证实时性
4. **配置缓存**长TTL5分钟减少DB查询
---
## 八、安全设计
### 8.1 密码安全
- **算法**bcryptcost=12
- **历史密码**不允许重复使用最近5次密码
- **复杂度**最小8位包含大小写+数字
- **传输**HTTPS only前端bcrypt预哈希可选
### 8.2 Token安全
- **Access Token**JWT RS256签名有效期1小时
- **Refresh Token**随机字符串有效期30天可撤销
- **Token绑定**:可选绑定设备指纹,防止盗用
- **撤销机制**JWT jti 存入黑名单实现撤销
### 8.3 OAuth安全
- **PKCE**所有public client强制启用
- **State参数**强制验证防止CSRF
- **Redirect URI**:必须预注册,严格匹配
- **Client Secret**仅用于confidential client
### 8.4 防护措施
- **限流**
- 登录5次/分钟错误5次后需要验证码
- 注册3次/小时
- API100次/分钟/用户
- **验证码**
- 图形验证码:登录错误多次后
- 邮箱/短信验证码:密码重置、敏感操作
- **审计日志**
- 登录/登出
- 密码修改
- 权限变更
- 组织重要操作
---
## 九、初始化流程
系统首次启动时自动执行:
1. **创建数据库表**
2. **创建超级管理员**
- 检测用户表是否为空
- 为空时创建默认超管账号
- 用户名/密码从环境变量读取(或随机生成并打印日志)
3. **创建系统策略**
- 创建所有系统内置策略
4. **创建默认组织**(可选)
- 创建名为"Default"的根组织
- 将超管加入该组织
---
## 十、技术选型
| 组件 | 选型 | 说明 |
|------|------|------|
| Web框架 | Vigo | 保持使用 |
| ORM | GORM | 保持使用 |
| 数据库 | MySQL/PostgreSQL | 保持使用 |
| 缓存 | Redis | 必选 |
| 密码哈希 | bcrypt | golang.org/x/crypto/bcrypt |
| JWT | jwt-go / golang-jwt | 标准库 |
| CEL表达式 | cel-go | github.com/google/cel-go |
| 验证码 | base64Captcha | 或自研 |
| OAuth2 | go-oauth2/oauth2 | 或自研实现 |
---
## 十一、目录结构
```
/
├── cmd/
│ └── server/
│ └── main.go # 服务入口
├── internal/ # 私有代码
│ ├── api/ # API层
│ │ ├── auth/ # 认证API
│ │ ├── user/ # 用户API
│ │ ├── org/ # 组织API
│ │ ├── role/ # 角色API
│ │ ├── policy/ # 策略API
│ │ ├── oauth/ # OAuth2.0服务端
│ │ └── middleware/ # 中间件
│ │ ├── auth.go # 认证中间件
│ │ ├── permission.go # 权限中间件
│ │ ├── ratelimit.go # 限流中间件
│ │ └── cors.go # 跨域中间件
│ ├── service/ # 业务逻辑层
│ │ ├── auth.go
│ │ ├── user.go
│ │ ├── org.go
│ │ ├── role.go
│ │ ├── policy.go
│ │ ├── permission.go # 权限检查核心
│ │ └── oauth.go
│ ├── model/ # 数据模型
│ │ ├── user.go
│ │ ├── org.go
│ │ ├── role.go
│ │ ├── policy.go
│ │ ├── oauth.go
│ │ └── migrate.go # 数据库迁移
│ ├── repository/ # 数据访问层
│ │ ├── user.go
│ │ ├── org.go
│ │ └── ...
│ ├── cache/ # 缓存封装
│ │ └── redis.go
│ ├── pkg/ # 内部工具包
│ │ ├── crypto/ # 加密工具
│ │ ├── jwt/ # JWT工具
│ │ ├── cel/ # CEL表达式
│ │ └── oauth/ # OAuth2工具
│ └── config/ # 配置
│ └── config.go
├── pkg/ # 可公开使用的包
│ └── sdk/ # 其他服务使用的SDK
├── docs/ # 文档
├── scripts/ # 脚本
├── configs/ # 配置文件
├── go.mod
└── README.md
```
---
## 十二、开发计划
### Phase 1: 基础架构
1. 数据库模型定义和迁移
2. Redis缓存封装
3. JWT认证中间件
4. 基础配置管理
### Phase 2: 认证模块
1. 用户注册/登录/登出
2. 密码管理
3. Session管理
4. 验证码
### Phase 3: 组织与成员
1. 组织CRUD
2. 成员管理
3. 组织树查询
### Phase 4: 权限系统
1. 策略定义
2. 角色管理
3. 权限检查中间件
4. CEL表达式评估
### Phase 5: OAuth2.0
1. 客户端管理
2. 授权流程
3. Token管理
4. OIDC支持
### Phase 6: 第三方登录
1. Google/GitHub/微信登录
2. 账号绑定
### Phase 7: 完善
1. 审计日志
2. 管理后台API
3. 测试和文档
---
**确认以上设计后开始Phase 1实现。**
Loading…
Cancel
Save