diff --git a/app.example.toml b/app.example.toml index 8f4f9a0..0ef9cea 100644 --- a/app.example.toml +++ b/app.example.toml @@ -5,6 +5,8 @@ port="8080" driver="mysql" dsn="root:root@tcp(localhost:3306)/moredoc?charset=utf8mb4&loc=Local&parseTime=true" showSQL=true - prefix="mnt_" maxOpen=10 - maxIdle=10 \ No newline at end of file + maxIdle=10 +[jwt] + secret="moredoc" + expireDays=365 \ No newline at end of file diff --git a/biz/auth.go b/biz/auth.go deleted file mode 100644 index 5dd053d..0000000 --- a/biz/auth.go +++ /dev/null @@ -1,103 +0,0 @@ -package biz - -import ( - "context" - "moredoc/model" - "strings" - "time" - - "github.com/gin-gonic/gin" - grpc_auth "github.com/grpc-ecosystem/go-grpc-middleware/auth" - "go.uber.org/zap" - "google.golang.org/grpc" -) - -/* - -当前中间件的作用,主要是将用户的token信息解析存放到ctx中,方便后续的业务逻辑使用。 - -这里无论用户登录与否,都会执行该中间件,如果用户登录,则会将用户信息存放到ctx中,否则,ctx中不会有用户信息。 - -*/ - -type AuthService struct { - dbModel *model.DBModel - logger *zap.Logger -} - -func NewAuthService(dbModel *model.DBModel, logger *zap.Logger) (service *UserAPIService) { - return &UserAPIService{dbModel: dbModel, logger: logger.Named("AuthService")} -} - -type ContextKey string - -func (ck ContextKey) String() string { - return string(ck) -} - -const ( - CtxKeyUserClaims ContextKey = "user" -) - -// const ( -// messageInvalidToken = "您的登录令牌已过期,请重新登录" -// ) - -type ServiceAuthFuncOverride interface { - AuthFuncOverride(ctx context.Context, fullMethodName string) (context.Context, error) -} - -func (s *AuthService) AuthUnaryServerInterceptor() grpc.UnaryServerInterceptor { - return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { - newCtx, err := s.AuthGRPC(ctx, info) - if err != nil { - return nil, err - } - return handler(newCtx, req) - } -} - -// AuthGRPC 验证 gRPC 请求 -// 1. 从权限表中查询API,如果存在该API,则表示该API需要权限才能访问,如果不存在,则跳过 -// 2. 如果用户携带有token,则根据token判断是否有效,如果有效,则获取用户信息放到ctx,否则跳过 -func (s *AuthService) AuthGRPC(ctx context.Context, info *grpc.UnaryServerInfo) (context.Context, error) { - token, err := grpc_auth.AuthFromMD(ctx, "bearer") - if err != nil { - return ctx, nil - } - - claims, err := s.dbModel.CheckUserJWTToken(token) - // token存在,但是不正确或者已过期,这时需要返回错误,前端清除存储的错误登录信息 - if err != nil || claims == nil || claims.ExpiresAt < time.Now().Unix() || s.dbModel.IsInvalidToken(claims.UUID) { - // return ctx, status.Error(codes.Unauthenticated, messageInvalidToken) - return ctx, nil - } - - newCtx := context.WithValue(ctx, CtxKeyUserClaims, claims) - return newCtx, nil -} - -// AuthGin 验证 HTTP 请求 -func (s *AuthService) AuthGin() gin.HandlerFunc { - return func(ctx *gin.Context) { - auth := ctx.Request.Header.Get("authorization") - bearer := strings.Split(auth, " ") - - if auth == "" || len(bearer) != 2 { - ctx.Next() - return - } - - token := bearer[1] - claims, err := s.dbModel.CheckUserJWTToken(token) - if err != nil || claims == nil || claims.ExpiresAt < time.Now().Unix() || s.dbModel.IsInvalidToken(claims.UUID) { - // ctx.JSON(http.StatusUnauthorized, status.Error(codes.Unauthenticated, messageInvalidToken)) - // ctx.Abort() - ctx.Next() - return - } - - ctx.Set(CtxKeyUserClaims.String(), claims) - ctx.Next() - } -} diff --git a/biz/user.go b/biz/user.go index 59c7048..e61ccf6 100644 --- a/biz/user.go +++ b/biz/user.go @@ -5,6 +5,7 @@ import ( "time" pb "moredoc/api/v1" + "moredoc/middleware/auth" "moredoc/model" "moredoc/util" "moredoc/util/validate" @@ -23,10 +24,11 @@ type UserAPIService struct { pb.UnimplementedUserAPIServer dbModel *model.DBModel logger *zap.Logger + auth *auth.Auth } -func NewUserAPIService(dbModel *model.DBModel, logger *zap.Logger) (service *UserAPIService) { - return &UserAPIService{dbModel: dbModel, logger: logger.Named("UserAPIService")} +func NewUserAPIService(dbModel *model.DBModel, logger *zap.Logger, auth *auth.Auth) (service *UserAPIService) { + return &UserAPIService{dbModel: dbModel, logger: logger.Named("UserAPIService"), auth: auth} } func (s *UserAPIService) getValidFieldMap() map[string]string { @@ -34,7 +36,6 @@ func (s *UserAPIService) getValidFieldMap() map[string]string { } // Register 用户注册 -// TODO: 1. 如果系统启用了注册,判断是否需要管理员审核 func (s *UserAPIService) Register(ctx context.Context, req *pb.RegisterAndLoginRequest) (*emptypb.Empty, error) { err := validate.ValidateStruct(req, s.getValidFieldMap()) if err != nil { @@ -51,7 +52,7 @@ func (s *UserAPIService) Register(ctx context.Context, req *pb.RegisterAndLoginR return nil, status.Errorf(codes.InvalidArgument, "系统未开放注册") } - if !cfg.IsClose { + if cfg.IsClose { return nil, status.Errorf(codes.InvalidArgument, "网站已关闭,占时不允许注册") } @@ -83,7 +84,6 @@ func (s *UserAPIService) Register(ctx context.Context, req *pb.RegisterAndLoginR } // Login 用户登录 -// TODO: 1. 判断是否启用了验证码,如果启用了验证码,则需要进行验证码验证 func (s *UserAPIService) Login(ctx context.Context, req *pb.RegisterAndLoginRequest) (*pb.LoginReply, error) { errValidate := validate.ValidateStruct(req, s.getValidFieldMap()) @@ -105,7 +105,7 @@ func (s *UserAPIService) Login(ctx context.Context, req *pb.RegisterAndLoginRequ return nil, status.Errorf(codes.InvalidArgument, "用户名或密码错误") } - token, err := s.dbModel.CreateUserJWTToken(user.Id) + token, err := s.auth.CreateJWTToken(user.Id) if err != nil { return nil, status.Errorf(codes.Internal, err.Error()) } diff --git a/cmd/root.go b/cmd/root.go index b881c9a..4101ae3 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -105,9 +105,7 @@ func initConfig() { initLogger(cfg.Level) - if cfg.Database.Prefix == "" { - cfg.Database.Prefix = "nd_" - } + cfg.Database.Prefix = "mnt_" logger.Info("config", zap.Any("config", cfg)) } diff --git a/conf/config.go b/conf/config.go index f68e675..6fe7e92 100644 --- a/conf/config.go +++ b/conf/config.go @@ -5,6 +5,7 @@ type Config struct { Level string // Port int // listent port Database Database + JWT JWT } type Database struct { @@ -14,3 +15,8 @@ type Database struct { MaxOpen int Prefix string // table prefix, default is nd_ } + +type JWT struct { + Secret string + ExpireDays int64 +} diff --git a/middleware/auth/auth.go b/middleware/auth/auth.go new file mode 100644 index 0000000..f3b9726 --- /dev/null +++ b/middleware/auth/auth.go @@ -0,0 +1,123 @@ +package auth + +import ( + "context" + "moredoc/conf" + "strings" + "time" + + "github.com/gin-gonic/gin" + "github.com/gofrs/uuid" + "github.com/golang-jwt/jwt" + grpc_auth "github.com/grpc-ecosystem/go-grpc-middleware/auth" + "google.golang.org/grpc" +) + +type Auth struct { + jwt *conf.JWT +} + +type UserClaims struct { + UserId int64 + UUID string + jwt.StandardClaims +} + +func NewAuth(jwt *conf.JWT) *Auth { + return &Auth{ + jwt: jwt, + } +} + +type ContextKey string + +func (ck ContextKey) String() string { + return string(ck) +} + +const ( + CtxKeyUserClaims ContextKey = "user" + CtxKeyFullMethod ContextKey = "fullMethod" +) + +func (p *Auth) AuthUnaryServerInterceptor() grpc.UnaryServerInterceptor { + return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { + newCtx, err := p.AuthGRPC(ctx, info) + if err != nil { + return nil, err + } + return handler(newCtx, req) + } +} + +func (p *Auth) AuthGRPC(ctx context.Context, info *grpc.UnaryServerInfo) (context.Context, error) { + ctx = context.WithValue(ctx, CtxKeyFullMethod, info.FullMethod) + token, err := grpc_auth.AuthFromMD(ctx, "bearer") + if err != nil { + return ctx, nil + } + + claims, err := p.CheckJWTToken(token) + if err != nil || claims == nil || claims.ExpiresAt < time.Now().Unix() { + return ctx, nil + } + + newCtx := context.WithValue(ctx, CtxKeyUserClaims, claims) + return newCtx, nil +} + +func (p *Auth) AuthGin() gin.HandlerFunc { + return func(ctx *gin.Context) { + auth := ctx.Request.Header.Get("authorization") + bearer := strings.Split(auth, " ") + + if auth == "" || len(bearer) != 2 { + ctx.Next() + return + } + + token := bearer[1] + claims, err := p.CheckJWTToken(token) + if err != nil || claims == nil || claims.ExpiresAt < time.Now().Unix() { + ctx.Next() + return + } + + ctx.Set(CtxKeyUserClaims.String(), claims) + ctx.Next() + } +} + +// CreateUserJWTToken 生成用户JWT Token +func (p *Auth) CreateJWTToken(userId int64) (string, error) { + expireTime := time.Now().Add(time.Duration(p.jwt.ExpireDays) * 24 * time.Hour).Unix() + claims := UserClaims{ + UserId: userId, + UUID: uuid.Must(uuid.NewV4()).String(), + StandardClaims: jwt.StandardClaims{ + ExpiresAt: expireTime, + Issuer: "moredoc", + IssuedAt: time.Now().Unix(), + }, + } + token, err := jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString([]byte(p.jwt.Secret)) + return token, err +} + +// CheckUserJWTToken 验证用户JWT token +func (p *Auth) CheckJWTToken(token string) (*UserClaims, error) { + tokenClaims, err := jwt.ParseWithClaims(token, &UserClaims{}, func(token *jwt.Token) (interface{}, error) { + return []byte(p.jwt.Secret), nil + }) + + if err != nil { + return nil, err + } + + if tokenClaims != nil { + if claims, ok := tokenClaims.Claims.(*UserClaims); ok && tokenClaims.Valid { + return claims, nil + } + } + return nil, err +} diff --git a/model/config.go b/model/config.go index d0af92e..d0e0056 100644 --- a/model/config.go +++ b/model/config.go @@ -7,7 +7,6 @@ import ( "strings" "time" - "github.com/gofrs/uuid" jsoniter "github.com/json-iterator/go" "go.uber.org/zap" "gorm.io/gorm" @@ -224,49 +223,6 @@ func (m *DBModel) DeleteConfig(ids []interface{}) (err error) { return } -const ( - ConfigJWTDuration = "duration" - ConfigJWTSecret = "secret" -) - -type ConfigJWT struct { - Duration int `json:"duration"` // JWT有效期 - Secret string `json:"secret"` // JWT加密密钥 -} - -// GetConfigJWT 获取JWT配置 -func (m *DBModel) GetConfigOfJWT() (config ConfigJWT) { - var configs []Config - err := m.db.Where("category = ?", ConfigCategoryJWT).Find(&configs).Error - if err != nil && err != gorm.ErrRecordNotFound { - m.logger.Error("GetConfigJWT", zap.Error(err)) - } - - var data = make(map[string]interface{}) - - for _, cfg := range configs { - switch cfg.Name { - case "secret": - value := cfg.Value - if value == "" { - value = "moredoc" - } - data[cfg.Name] = value - case "duration": - value, _ := strconv.Atoi(cfg.Value) - if value <= 0 { - value = 3600 * 24 * 7 - } - data[cfg.Name] = value - } - } - - bytes, _ := json.Marshal(data) - json.Unmarshal(bytes, &config) - - return -} - const ( ConfigCaptchaLength = "length" ConfigCaptchaWidth = "width" @@ -427,10 +383,6 @@ func (m *DBModel) initConfig() (err error) { {Category: ConfigCategorySystem, Name: ConfigSystemTheme, Label: "网站主题", Value: "default", Placeholder: "请输入您网站的主题", InputType: "text", Sort: 9, Options: ""}, {Category: ConfigCategorySystem, Name: ConfigSystemCopyright, Label: "网站版权信息", Value: "", Placeholder: "请输入您网站的版权信息", InputType: "text", Sort: 10, Options: ""}, - // JWT 配置项 - {Category: ConfigCategoryJWT, Name: ConfigJWTDuration, Label: "Token有效期", Value: "365", Placeholder: "用户Token签名有效期,单位为天,默认365天", InputType: "number", Sort: 11, Options: ""}, - {Category: ConfigCategoryJWT, Name: ConfigJWTSecret, Label: "Token密钥", Value: uuid.Must(uuid.NewV4()).String(), Placeholder: "用户Token签名密钥,修改之后,之前所有的token签名都将失效,请慎重修改", InputType: "text", Sort: 12, Options: ""}, - // 验证码配置项 {Category: ConfigCategoryCaptcha, Name: ConfigCaptchaHeight, Label: "验证码高度", Value: "60", Placeholder: "请输入验证码高度,默认为60", InputType: "number", Sort: 13, Options: ""}, {Category: ConfigCategoryCaptcha, Name: ConfigCaptchaWidth, Label: "验证码宽度", Value: "240", Placeholder: "请输入验证码宽度,默认为240", InputType: "number", Sort: 14, Options: ""}, diff --git a/model/groupPermission.go b/model/groupPermission.go index 5ec5072..94bfb79 100644 --- a/model/groupPermission.go +++ b/model/groupPermission.go @@ -31,7 +31,6 @@ func (GroupPermission) TableName() string { } // CreateGroupPermission 创建GroupPermission -// TODO: 创建成功之后,注意相关表统计字段数值的增减 func (m *DBModel) CreateGroupPermission(groupPermission *GroupPermission) (err error) { err = m.db.Create(groupPermission).Error if err != nil { @@ -182,7 +181,6 @@ func (m *DBModel) GetGroupPermissionList(opt OptionGetGroupPermissionList) (grou } // DeleteGroupPermission 删除数据 -// TODO: 删除数据之后,存在 group_permission_id 的关联表,需要删除对应数据,同时相关表的统计数值,也要随着减少 func (m *DBModel) DeleteGroupPermission(ids []interface{}) (err error) { err = m.db.Where("id in (?)", ids).Delete(&GroupPermission{}).Error if err != nil { @@ -190,39 +188,3 @@ func (m *DBModel) DeleteGroupPermission(ids []interface{}) (err error) { } return } - -// CheckPermissionByUserId 根据用户ID,检查用户是否有权限 -func (m *DBModel) CheckPermissionByUserId(permissionIdentifier string, userId int64) (yes bool) { - var ( - userGroups []UserGroup - groupId []int64 - ) - - m.db.Where("user_id = ?", userId).Find(&userGroups) - for _, ug := range userGroups { - groupId = append(groupId, ug.GroupId) - } - - return m.CheckPermissionByGroupId(permissionIdentifier, groupId) -} - -// CheckPermissionByGroupId 根据用户所属用户组ID,检查用户是否有权限 -func (m *DBModel) CheckPermissionByGroupId(permissionIdentifier string, groupId []int64) (yes bool) { - if len(groupId) == 0 { - return - } - - permission, _ := m.GetPermissionByIdentifier(permissionIdentifier, "id") - if permission.Id == 0 { - return - } - - var groupPermission GroupPermission - err := m.db.Where("group_id in (?) and permission_id = ?", groupId, permission.Id).First(&groupPermission).Error - if err != nil { - m.logger.Error("CheckPermissionByGroupId", zap.Error(err)) - } - - // 如果有权限,返回true - return groupPermission.Id > 0 -} diff --git a/model/init.go b/model/init.go index 2b37c7b..4ed1f71 100644 --- a/model/init.go +++ b/model/init.go @@ -36,19 +36,21 @@ type DBModel struct { tableFieldsMap map[string]map[string]struct{} validToken sync.Map // map[tokenUUID]struct{} 有效的token uuid invalidToken sync.Map // map[tokenUUID]struct{} 存在,未过期但无效token,比如读者退出登录后的token + jwt conf.JWT } -func NewDBModel(cfg *conf.Database, lg *zap.Logger) (m *DBModel, err error) { +func NewDBModel(cfg *conf.Config, lg *zap.Logger) (m *DBModel, err error) { if lg == nil { err = errors.New("logger cant be nil") return } - tablePrefix = cfg.Prefix + tablePrefix = cfg.Database.Prefix m = &DBModel{ logger: lg.Named("model"), - tablePrefix: cfg.Prefix, + tablePrefix: cfg.Database.Prefix, + jwt: cfg.JWT, tableFields: make(map[string][]string), tableFieldsMap: make(map[string]map[string]struct{}), } @@ -59,21 +61,21 @@ func NewDBModel(cfg *conf.Database, lg *zap.Logger) (m *DBModel, err error) { ) sqlLogLevel := logger.Info - if !cfg.ShowSQL { + if !cfg.Database.ShowSQL { sqlLogLevel = logger.Silent } db, err = gorm.Open(mysql.New(mysql.Config{ - DSN: cfg.DSN, // DSN data source name - DefaultStringSize: 255, // string 类型字段的默认长度 - DisableDatetimePrecision: true, // 禁用 datetime 精度,MySQL 5.6 之前的数据库不支持 - DontSupportRenameIndex: true, // 重命名索引时采用删除并新建的方式,MySQL 5.7 之前的数据库和 MariaDB 不支持重命名索引 - DontSupportRenameColumn: true, // 用 `change` 重命名列,MySQL 8 之前的数据库和 MariaDB 不支持重命名列 - SkipInitializeWithVersion: false, // 根据当前 MySQL 版本自动配置 + DSN: cfg.Database.DSN, // DSN data source name + DefaultStringSize: 255, // string 类型字段的默认长度 + DisableDatetimePrecision: true, // 禁用 datetime 精度,MySQL 5.6 之前的数据库不支持 + DontSupportRenameIndex: true, // 重命名索引时采用删除并新建的方式,MySQL 5.7 之前的数据库和 MariaDB 不支持重命名索引 + DontSupportRenameColumn: true, // 用 `change` 重命名列,MySQL 8 之前的数据库和 MariaDB 不支持重命名列 + SkipInitializeWithVersion: false, // 根据当前 MySQL 版本自动配置 }), &gorm.Config{ NamingStrategy: schema.NamingStrategy{ - TablePrefix: cfg.Prefix, // 表名前缀,`User`表为`t_users` - SingularTable: true, // 使用单数表名,启用该选项后,`User` 表将是`user` + TablePrefix: cfg.Database.Prefix, // 表名前缀,`User`表为`t_users` + SingularTable: true, // 使用单数表名,启用该选项后,`User` 表将是`user` }, Logger: logger.Default.LogMode(sqlLogLevel), }) @@ -88,12 +90,12 @@ func NewDBModel(cfg *conf.Database, lg *zap.Logger) (m *DBModel, err error) { return } - if cfg.MaxIdle > 0 { - sqlDB.SetMaxIdleConns(cfg.MaxIdle) + if cfg.Database.MaxIdle > 0 { + sqlDB.SetMaxIdleConns(cfg.Database.MaxIdle) } - if cfg.MaxOpen > 0 { - sqlDB.SetMaxIdleConns(cfg.MaxOpen) + if cfg.Database.MaxOpen > 0 { + sqlDB.SetMaxIdleConns(cfg.Database.MaxOpen) } m.db = db @@ -143,6 +145,7 @@ func (m *DBModel) SyncDB() (err error) { &UserGroup{}, &Permission{}, &GroupPermission{}, + &Logout{}, } if err = m.db.AutoMigrate(tableModels...); err != nil { m.logger.Fatal("SyncDB", zap.Error(err)) @@ -190,10 +193,10 @@ func (m *DBModel) showTableColumn(tableName string) (columns []TableColumn, err // initialDatabase 初始化数据库相关数据 func (m *DBModel) initDatabase() (err error) { - if err = m.initPermission(); err != nil { - m.logger.Error("initialDatabase", zap.Error(err)) - return - } + // if err = m.initPermission(); err != nil { + // m.logger.Error("initialDatabase", zap.Error(err)) + // return + // } // 初始化用户组及其权限 if err = m.initGroupAndPermission(); err != nil { diff --git a/model/permission.go b/model/permission.go index 1eb8c95..4ab8781 100644 --- a/model/permission.go +++ b/model/permission.go @@ -11,10 +11,10 @@ import ( type Permission struct { Id int64 `form:"id" json:"id,omitempty" gorm:"primaryKey;autoIncrement;column:id;comment:;"` - Category string `form:"category" json:"category,omitempty" gorm:"column:category;type:varchar(64);size:64;index:category;comment:权限类别组;"` - Title string `form:"title" json:"title,omitempty" gorm:"column:title;type:varchar(255);size:255;comment:权限中文名称;"` + Method string `form:"method" json:"method,omitempty" gorm:"column:method;type:varchar(16);size:16;index:method_path,unique;comment:请求方法,grpc为空;"` + Path string `form:"path" json:"path,omitempty" gorm:"column:path;type:varchar(128);size:128;index:method_path,unique;comment:API路径;"` + Title string `form:"title" json:"title,omitempty" gorm:"column:title;type:varchar(255);size:255;comment:中文名称;"` Description string `form:"description" json:"description,omitempty" gorm:"column:description;type:varchar(255);size:255;comment:权限描述;"` - Identifier string `form:"identifier" json:"identifier,omitempty" gorm:"column:identifier;type:varchar(64);size:64;index:identifier,unique;comment:权限英文标识,如函数名称等;"` CreatedAt time.Time `form:"created_at" json:"created_at,omitempty" gorm:"column:created_at;type:datetime;comment:创建时间;"` UpdatedAt time.Time `form:"updated_at" json:"updated_at,omitempty" gorm:"column:updated_at;type:datetime;comment:更新时间;"` } @@ -34,161 +34,7 @@ func (Permission) TableName() string { // = 0; //} -// PermissionCategoryXXX 基本按照数据表来定义 -const ( - PermissionCategoryAttachment = "attachment" // 附件管理,包括上传等 - PermissionCategoryBanner = "banner" // 管理横幅 - PermissionCategoryCategory = "category" // 管理文档分类 - PermissionCategoryConfig = "config" // 管理系统配置:开启验证码,是否允许上传等 - PermissionCategoryDocument = "document" // 管理文档 - PermissionCategoryFriendlink = "friendlink" // 管理友情链接 - PermissionCategoryGroup = "group" // 管理用户组:创建、修改、删除、查看等 - PermissionCategoryGroupPermission = "groupPermission" // 权限设置 - PermissionCategoryUser = "user" // 管理用户:创建、修改、删除、查看、修改密码、禁用、变更分组等 -) - -const ( - // PermissionCategoryAttachment = "attachment" // 附件管理,包括上传等 - PermissionIdentifierAttachmentList = "attachmentList" // 查看附件列表 - PermissionIdentifierAttachmentDelete = "attachmentDelete" // 删除单个附件 - PermissionIdentifierAttachmentBatchDelete = "attachmentBatchDelete" // 批量删除附件 - PermissionIdentifierAttachmentDisable = "attachmentDisable" // 禁止附件,禁止后无法访问,用于控制非法附件 - - // PermissionCategoryBanner = "banner" // 管理横幅 - PermissionIdentifierBannerList = "bannerList" // 查看横幅列表 - PermissionIdentifierBannerCreate = "bannerCreate" // 创建横幅 - PermissionIdentifierBannerUpdate = "bannerUpdate" // 更新横幅 - PermissionIdentifierBannerDelete = "bannerDelete" // 删除横幅 - PermissionIdentifierBannerBatchDelete = "bannerBatchDelete" // 批量删除横幅 - - // PermissionCategoryCategory = "category" // 管理文档分类 - PermissionIdentifierCategoryList = "categoryList" // 查看文档分类列表 - PermissionIdentifierCategoryCreate = "categoryCreate" // 创建分类 - PermissionIdentifierCategoryUpdate = "categoryUpdate" // 更新分类 - PermissionIdentifierCategoryDelete = "categoryDelete" // 删除分类 - PermissionIdentifierCategoryBatchDelete = "categoryBatchDelete" // 批量删除分类 - - // PermissionCategoryConfig = "config" // 管理系统配置:开启验证码,是否允许上传等。不允许删除配置 - PermissionIdentifierConfigList = "configList" // 查看系统配置列表 - PermissionIdentifierConfigUpdate = "configUpdate" // 更新系统配置 - - // PermissionCategoryDocument = "document" // 管理文档 - PermissionIdentifierDocumentList = "documentList" // 查看文档列表 - PermissionIdentifierDocumentCreate = "documentCreate" // 创建文档 - PermissionIdentifierDocumentBatchCreate = "documentBatchCreate" // 批量创建文档 - PermissionIdentifierDocumentUpdate = "documentUpdate" // 更新文档 - PermissionIdentifierDocumentDelete = "documentDelete" // 删除文档 - PermissionIdentifierDocumentBatchDelete = "documentBatchDelete" // 批量删除文档 - - // PermissionCategoryFriendlink = "friendlink" // 管理友情链接 - PermissionIdentifierFriendlinkList = "friendlinkList" // 查看友情链接列表 - PermissionIdentifierFriendlinkCreate = "friendlinkCreate" // 创建友情链接 - PermissionIdentifierFriendlinkUpdate = "friendlinkUpdate" // 更新友情链接 - PermissionIdentifierFriendlinkDelete = "friendlinkDelete" // 删除友情链接 - PermissionIdentifierFriendlinkBatchDelete = "friendlinkBatchDelete" // 批量删除友情链接 - - // PermissionCategoryGroup = "group" // 管理用户组:创建、修改、删除、查看等 - PermissionIdentifierGroupList = "groupList" // 查看用户组列表 - PermissionIdentifierGroupCreate = "groupCreate" // 创建用户组 - PermissionIdentifierGroupUpdate = "groupUpdate" // 更新用户组 - PermissionIdentifierGroupDelete = "groupDelete" // 删除用户组 - PermissionIdentifierGroupBatchDelete = "groupBatchDelete" // 批量删除用户组 - - // PermissionCategoryGroupPermission = "groupPermission" // 权限设置 - PermissionIdentifierGroupPermissionList = "groupPermissionList" // 查看用户组权限列表 - PermissionIdentifierGroupPermissionUpdate = "groupPermissionUpdate" // 更新用户组权限 - - // PermissionCategoryUser = "user" // 管理用户:创建、修改、删除、查看、修改密码、禁用、变更分组等 - PermissionIdentifierUserCreate = "userCreate" // 创建用户 - PermissionIdentifierUserUpdate = "userUpdate" // 修改用户 - PermissionIdentifierUserDelete = "userDelete" // 删除用户 - PermissionIdentifierUserBatchDelete = "userBatchDelete" // 删除用户 - PermissionIdentifierUserList = "userList" // 查看用户 - PermissionIdentifierUserChangePassword = "userChangePassword" // 修改用户密码 - PermissionIdentifierUserDisable = "userDisable" // 禁用用户 - PermissionIdentifierUserChangeGroup = "userChangeGroup" // 变更用户分组 -) - -func (m *DBModel) initPermission() (err error) { - permissions := []Permission{ - {Id: 1, Category: PermissionCategoryAttachment, Identifier: PermissionIdentifierAttachmentList, Title: "附件管理"}, - {Id: 2, Category: PermissionCategoryAttachment, Identifier: PermissionIdentifierAttachmentDelete, Title: "删除附件"}, - {Id: 3, Category: PermissionCategoryAttachment, Identifier: PermissionIdentifierAttachmentBatchDelete, Title: "批量删除附件"}, - {Id: 4, Category: PermissionCategoryAttachment, Identifier: PermissionIdentifierAttachmentDisable, Title: "禁用附件"}, - {Id: 42, Category: PermissionCategoryAttachment, Identifier: PermissionIdentifierAttachmentDisable, Title: "禁用附件"}, - - {Id: 5, Category: PermissionCategoryBanner, Identifier: PermissionIdentifierBannerList, Title: "横幅管理"}, - {Id: 6, Category: PermissionCategoryBanner, Identifier: PermissionIdentifierBannerCreate, Title: "创建横幅"}, - {Id: 7, Category: PermissionCategoryBanner, Identifier: PermissionIdentifierBannerUpdate, Title: "更新横幅"}, - {Id: 8, Category: PermissionCategoryBanner, Identifier: PermissionIdentifierBannerDelete, Title: "删除横幅"}, - {Id: 9, Category: PermissionCategoryBanner, Identifier: PermissionIdentifierBannerBatchDelete, Title: "批量删除横幅"}, - - {Id: 10, Category: PermissionCategoryCategory, Identifier: PermissionIdentifierCategoryList, Title: "分类管理"}, - {Id: 11, Category: PermissionCategoryCategory, Identifier: PermissionIdentifierCategoryCreate, Title: "创建分类"}, - {Id: 12, Category: PermissionCategoryCategory, Identifier: PermissionIdentifierCategoryUpdate, Title: "更新分类"}, - {Id: 13, Category: PermissionCategoryCategory, Identifier: PermissionIdentifierCategoryDelete, Title: "删除分类"}, - {Id: 14, Category: PermissionCategoryCategory, Identifier: PermissionIdentifierCategoryBatchDelete, Title: "批量删除分类"}, - - {Id: 15, Category: PermissionCategoryConfig, Identifier: PermissionIdentifierConfigList, Title: "系统配置管理"}, - {Id: 16, Category: PermissionCategoryConfig, Identifier: PermissionIdentifierConfigUpdate, Title: "更新系统配置"}, - - {Id: 17, Category: PermissionCategoryDocument, Identifier: PermissionIdentifierDocumentList, Title: "文档管理"}, - {Id: 18, Category: PermissionCategoryDocument, Identifier: PermissionIdentifierDocumentCreate, Title: "创建文档"}, - {Id: 19, Category: PermissionCategoryDocument, Identifier: PermissionIdentifierDocumentBatchCreate, Title: "批量创建文档"}, - {Id: 20, Category: PermissionCategoryDocument, Identifier: PermissionIdentifierDocumentUpdate, Title: "更新文档"}, - {Id: 21, Category: PermissionCategoryDocument, Identifier: PermissionIdentifierDocumentDelete, Title: "删除文档"}, - {Id: 22, Category: PermissionCategoryDocument, Identifier: PermissionIdentifierDocumentBatchDelete, Title: "批量删除文档"}, - - {Id: 23, Category: PermissionCategoryGroup, Identifier: PermissionIdentifierGroupList, Title: "用户组管理"}, - {Id: 24, Category: PermissionCategoryGroup, Identifier: PermissionIdentifierGroupCreate, Title: "创建用户组"}, - {Id: 25, Category: PermissionCategoryGroup, Identifier: PermissionIdentifierGroupUpdate, Title: "更新用户组"}, - {Id: 26, Category: PermissionCategoryGroup, Identifier: PermissionIdentifierGroupDelete, Title: "删除用户组"}, - {Id: 27, Category: PermissionCategoryGroup, Identifier: PermissionIdentifierGroupBatchDelete, Title: "批量删除用户组"}, - - {Id: 28, Category: PermissionCategoryGroupPermission, Identifier: PermissionIdentifierGroupPermissionList, Title: "权限管理"}, - {Id: 29, Category: PermissionCategoryGroupPermission, Identifier: PermissionIdentifierGroupPermissionUpdate, Title: "设置权限"}, - - {Id: 30, Category: PermissionCategoryFriendlink, Identifier: PermissionIdentifierFriendlinkList, Title: "友链管理"}, - {Id: 31, Category: PermissionCategoryFriendlink, Identifier: PermissionIdentifierFriendlinkCreate, Title: "创建友链"}, - {Id: 32, Category: PermissionCategoryFriendlink, Identifier: PermissionIdentifierFriendlinkUpdate, Title: "更新友链"}, - {Id: 33, Category: PermissionCategoryFriendlink, Identifier: PermissionIdentifierFriendlinkDelete, Title: "删除友链"}, - {Id: 34, Category: PermissionCategoryFriendlink, Identifier: PermissionIdentifierFriendlinkBatchDelete, Title: "批量删除友链"}, - - {Id: 35, Category: PermissionCategoryUser, Identifier: PermissionIdentifierUserCreate, Title: "创建用户"}, - {Id: 36, Category: PermissionCategoryUser, Identifier: PermissionIdentifierUserUpdate, Title: "更新用户"}, - {Id: 37, Category: PermissionCategoryUser, Identifier: PermissionIdentifierUserDelete, Title: "删除用户"}, - {Id: 38, Category: PermissionCategoryUser, Identifier: PermissionIdentifierUserBatchDelete, Title: "批量删除用户"}, - {Id: 39, Category: PermissionCategoryUser, Identifier: PermissionIdentifierUserList, Title: "用户管理"}, - {Id: 40, Category: PermissionCategoryUser, Identifier: PermissionIdentifierUserChangePassword, Title: "修改用户密码"}, - {Id: 41, Category: PermissionCategoryUser, Identifier: PermissionIdentifierUserChangeGroup, Title: "变更用户分组"}, - } - - sess := m.db.Begin() - defer func() { - if err != nil { - sess.Rollback() - } else { - sess.Commit() - } - }() - - err = sess.Where("id > ?", 0).Delete(&Permission{}).Error - if err != nil { - m.logger.Error("delete permission error", zap.Error(err)) - return - } - - err = sess.Create(&permissions).Error - if err != nil { - m.logger.Error("create permission error", zap.Error(err)) - return - } - - return -} - // CreatePermission 创建Permission -// TODO: 创建成功之后,注意相关表统计字段数值的增减 func (m *DBModel) CreatePermission(permission *Permission) (err error) { err = m.db.Create(permission).Error if err != nil { @@ -227,8 +73,8 @@ func (m *DBModel) GetPermission(id interface{}, fields ...string) (permission Pe return } -// GetPermissionByIdentifier(identifier string, fields ...string) 根据唯一索引获取Permission -func (m *DBModel) GetPermissionByIdentifier(identifier string, fields ...string) (permission Permission, err error) { +// GetPermissionByMethodPath +func (m *DBModel) GetPermissionByMethodPath(method, path string, createIfNotExist bool, fields ...string) (permission Permission, err error) { db := m.db fields = m.FilterValidFields(Permission{}.TableName(), fields...) @@ -236,16 +82,89 @@ func (m *DBModel) GetPermissionByIdentifier(identifier string, fields ...string) db = db.Select(fields) } - db = db.Where("identifier = ?", identifier) + db = db.Where("path = ?", path) + if method != "" { + db = db.Where("method = ?", method) + } else { + db = db.Where("(method IS NULL or method = '')") + } err = db.First(&permission).Error if err != nil && err != gorm.ErrRecordNotFound { m.logger.Error("GetPermissionByIdentifier", zap.Error(err)) return } + + if permission.Id > 0 { + return + } + + if createIfNotExist { + permission.Method = method + permission.Path = path + err = m.CreatePermission(&permission) + if err != nil { + m.logger.Error("GetPermissionByIdentifier", zap.Error(err)) + return + } + } return } +// DeletePermission 删除数据 +func (m *DBModel) DeletePermission(ids []interface{}) (err error) { + err = m.db.Where("id in (?)", ids).Delete(&Permission{}).Error + if err != nil { + m.logger.Error("DeletePermission", zap.Error(err)) + } + return +} + +// CheckPermissionByUserId 根据用户ID,检查用户是否有权限 +func (m *DBModel) CheckPermissionByUserId(userId int64, method, path string) (yes bool) { + var ( + userGroups []UserGroup + groupId []int64 + ) + + // NOTE: ID为1的用户,拥有所有权限,可以理解为类似linux的root用户 + if userId == 1 { + return true + } + + if userId > 0 { + m.db.Where("user_id = ?", userId).Find(&userGroups) + for _, ug := range userGroups { + groupId = append(groupId, ug.GroupId) + } + } + + return m.CheckPermissionByGroupId(groupId, method, path) +} + +// CheckPermissionByGroupId 根据用户所属用户组ID,检查用户是否有权限 +func (m *DBModel) CheckPermissionByGroupId(groupId []int64, method, path string) (yes bool) { + fields := []string{"id", "type", "method", "path"} + permission, err := m.GetPermissionByMethodPath(method, path, true, fields...) + if err != nil { + m.logger.Error("CheckPermissionByGroupId", zap.Error(err)) + } + + if permission.Id == 0 { // 权限控制表里面不存在的记录,默认允许访问 + return true + } + + // 校验当前登录了的用户所属用户组,是否有权限 + var groupPermission GroupPermission + err = m.db.Where("group_id in (?) and permission_id = ?", groupId, permission.Id).First(&groupPermission).Error + if err != nil { + m.logger.Error("CheckPermissionByGroupId", zap.Error(err)) + } + + // 如果有权限,返回true + return groupPermission.Id > 0 +} + type OptionGetPermissionList struct { Page int Size int @@ -335,13 +254,3 @@ func (m *DBModel) GetPermissionList(opt OptionGetPermissionList) (permissionList } return } - -// DeletePermission 删除数据 -// TODO: 删除数据之后,存在 permission_id 的关联表,需要删除对应数据,同时相关表的统计数值,也要随着减少 -func (m *DBModel) DeletePermission(ids []interface{}) (err error) { - err = m.db.Where("id in (?)", ids).Delete(&Permission{}).Error - if err != nil { - m.logger.Error("DeletePermission", zap.Error(err)) - } - return -} diff --git a/model/user.go b/model/user.go index 4e0d432..e3d85b0 100644 --- a/model/user.go +++ b/model/user.go @@ -6,8 +6,6 @@ import ( "time" "github.com/alexandrevicenzi/unchained" - "github.com/gofrs/uuid" - "github.com/golang-jwt/jwt" "go.uber.org/zap" "gorm.io/gorm" ) @@ -266,48 +264,6 @@ func (m *DBModel) DeleteUser(ids []interface{}) (err error) { return } -type UserClaims struct { - UserId int64 - UUID string - jwt.StandardClaims -} - -// CreateUserJWTToken 生成用户JWT Token -func (m *DBModel) CreateUserJWTToken(userId int64) (string, error) { - jwtCfg := m.GetConfigOfJWT() - expireTime := time.Now().Add(time.Duration(jwtCfg.Duration) * 24 * time.Hour).Unix() - claims := UserClaims{ - UserId: userId, - UUID: uuid.Must(uuid.NewV4()).String(), - StandardClaims: jwt.StandardClaims{ - ExpiresAt: expireTime, - Issuer: "moredoc", - IssuedAt: time.Now().Unix(), - }, - } - token, err := jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString([]byte(jwtCfg.Secret)) - return token, err -} - -// CheckUserJWTToken 验证用户JWT token -func (m *DBModel) CheckUserJWTToken(token string) (*UserClaims, error) { - jwtCfg := m.GetConfigOfJWT() - tokenClaims, err := jwt.ParseWithClaims(token, &UserClaims{}, func(token *jwt.Token) (interface{}, error) { - return []byte(jwtCfg.Secret), nil - }) - - if err != nil { - return nil, err - } - - if tokenClaims != nil { - if claims, ok := tokenClaims.Claims.(*UserClaims); ok && tokenClaims.Valid { - return claims, nil - } - } - return nil, err -} - func (m *DBModel) initUser() (err error) { // 如果不存在任意用户,则初始化一个用户作为管理员 var existUser User diff --git a/service/serve.go b/service/serve.go index fe15c4f..e46efb2 100644 --- a/service/serve.go +++ b/service/serve.go @@ -9,6 +9,7 @@ import ( v1 "moredoc/api/v1" "moredoc/biz" "moredoc/conf" + "moredoc/middleware/auth" "moredoc/middleware/jsonpb" "moredoc/model" @@ -28,6 +29,7 @@ import ( // Run start server func Run(cfg *conf.Config, logger *zap.Logger) { + size := 100 * 1024 * 1024 // 100MB dialOpts := []grpc.DialOption{ grpc.WithInsecure(), @@ -35,9 +37,11 @@ func Run(cfg *conf.Config, logger *zap.Logger) { grpc.WithDefaultCallOptions(grpc.UseCompressor("gzip")), } + auth := auth.NewAuth(&cfg.JWT) + grpcServer := grpc.NewServer( grpc.UnaryInterceptor(grpc_middleware.ChainUnaryServer( - // jwtil.UnaryServerInterceptor(jwtil.TokenAuthInterceptor), + auth.AuthUnaryServerInterceptor(), grpc_recovery.UnaryServerInterceptor(), )), ) @@ -49,20 +53,20 @@ func Run(cfg *conf.Config, logger *zap.Logger) { ), ) - dbModel, err := model.NewDBModel(&cfg.Database, logger) + dbModel, err := model.NewDBModel(cfg, logger) if err != nil { logger.Fatal("NewDBModel", zap.Error(err)) return } + endpoint := fmt.Sprintf("localhost:%v", cfg.Port) + // ========================================================================= // 【start】 在这里,注册您的API服务模块 // ========================================================================= - endpoint := fmt.Sprintf("localhost:%v", cfg.Port) - // 用户API接口服务 - userAPIService := biz.NewUserAPIService(dbModel, logger) + userAPIService := biz.NewUserAPIService(dbModel, logger, auth) v1.RegisterUserAPIServer(grpcServer, userAPIService) err = v1.RegisterUserAPIHandlerFromEndpoint(context.Background(), gwmux, endpoint, dialOpts) if err != nil { diff --git a/service/syncdb.go b/service/syncdb.go index ed4388f..a5c9aef 100644 --- a/service/syncdb.go +++ b/service/syncdb.go @@ -10,7 +10,7 @@ import ( func SyncDB(cfg *conf.Config, logger *zap.Logger) { lg := logger.Named("syncdb") lg.Info("start syncdb") - dbModel, err := model.NewDBModel(&cfg.Database, logger) + dbModel, err := model.NewDBModel(cfg, logger) if err != nil { lg.Fatal("NewDBModel", zap.Error(err)) return