diff --git a/devcloud/mcenter/apps/endpoint/enum.go b/devcloud/mcenter/apps/endpoint/enum.go new file mode 100644 index 0000000..ea9f083 --- /dev/null +++ b/devcloud/mcenter/apps/endpoint/enum.go @@ -0,0 +1,19 @@ +package endpoint + +type ACCESS_MODE uint8 + +const ( + ACCESS_MODE_READ = iota + ACCESS_MODE_READ_WRITE +) + +const ( + META_REQUIRED_AUTH_KEY = "required_auth" + META_REQUIRED_CODE_KEY = "required_code" + META_REQUIRED_PERM_KEY = "required_perm" + META_REQUIRED_ROLE_KEY = "required_role" + META_REQUIRED_AUDIT_KEY = "required_audit" + META_REQUIRED_NAMESPACE_KEY = "required_namespace" + META_RESOURCE_KEY = "resource" + META_ACTION_KEY = "action" +) diff --git a/devcloud/mcenter/apps/endpoint/model.go b/devcloud/mcenter/apps/endpoint/model.go new file mode 100644 index 0000000..ed23be0 --- /dev/null +++ b/devcloud/mcenter/apps/endpoint/model.go @@ -0,0 +1,249 @@ +package endpoint + +import ( + "fmt" + + "github.com/emicklei/go-restful/v3" + "github.com/google/uuid" + "github.com/infraboard/mcube/v2/ioc/config/application" + "github.com/infraboard/mcube/v2/tools/pretty" + "github.com/infraboard/mcube/v2/types" + "github.com/infraboard/modules/iam/apps" +) + +func NewEndpoint() *Endpoint { + return &Endpoint{ + ResourceMeta: *apps.NewResourceMeta(), + } +} + +func IsEndpointExist(set *types.Set[*Endpoint], target *Endpoint) bool { + for _, item := range set.Items { + if item.Id == target.Id { + return true + } + } + return false +} + +// Endpoint Service's features +type Endpoint struct { + // 基础数据 + apps.ResourceMeta + // 路由条目信息 + RouteEntry `bson:",inline" validate:"required"` +} + +func (e *Endpoint) TableName() string { + return "endpoints" +} + +func (e *Endpoint) String() string { + return pretty.ToJSON(e) +} + +func (e *Endpoint) IsMatched(service, method, path string) bool { + if e.Service != service { + return false + } + if e.Method != method { + return false + } + if e.Path != path { + return false + } + return true +} + +func (u *Endpoint) SetRouteEntry(v RouteEntry) *Endpoint { + u.RouteEntry = v + return u +} + +func NewRouteEntry() *RouteEntry { + return &RouteEntry{ + RequiredRole: []string{}, + Extras: map[string]string{}, + } +} + +// Entry 路由条目, service-method-path +type RouteEntry struct { + // 该功能属于那个服务 + UUID string `json:"uuid" bson:"uuid" gorm:"column:uuid;type:varchar(100);uniqueIndex" optional:"true" description:"路由UUID"` + // 该功能属于那个服务 + Service string `json:"service" bson:"service" validate:"required,lte=64" gorm:"column:service;type:varchar(100);index" description:"服务名称"` + // 服务那个版本的功能 + Version string `json:"version" bson:"version" validate:"required,lte=64" gorm:"column:version;type:varchar(100)" optional:"true" description:"版本版本"` + // 资源名称 + Resource string `json:"resource" bson:"resource" gorm:"column:resource;type:varchar(100);index" description:"资源名称"` + // 资源操作 + Action string `json:"action" bson:"action" gorm:"column:action;type:varchar(100);index" description:"资源操作"` + // 读或者写 + AccessMode ACCESS_MODE `json:"access_mode" bson:"access_mode" gorm:"column:access_mode;type:tinyint(1);index" optional:"true" description:"读写权限"` + // 操作标签 + ActionLabel string `json:"action_label" gorm:"column:action_label;type:varchar(200);index" optional:"true" description:"资源标签"` + // 函数名称 + FunctionName string `json:"function_name" bson:"function_name" gorm:"column:function_name;type:varchar(100)" optional:"true" description:"函数名称"` + // HTTP path 用于自动生成http api + Path string `json:"path" bson:"path" gorm:"column:path;type:varchar(200);index" description:"接口的路径"` + // HTTP method 用于自动生成http api + Method string `json:"method" bson:"method" gorm:"column:method;type:varchar(100);index" description:"接口的方法"` + // 接口说明 + Description string `json:"description" bson:"description" gorm:"column:description;type:text" optional:"true" description:"接口说明"` + // 是否校验用户身份 (acccess_token 校验) + RequiredAuth bool `json:"required_auth" bson:"required_auth" gorm:"column:required_auth;type:tinyint(1)" optional:"true" description:"是否校验用户身份 (acccess_token 校验)"` + // 验证码校验(开启双因子认证需要) (code 校验) + RequiredCode bool `json:"required_code" bson:"required_code" gorm:"column:required_code;type:tinyint(1)" optional:"true" description:"验证码校验(开启双因子认证需要) (code 校验)"` + // 开启鉴权 + RequiredPerm bool `json:"required_perm" bson:"required_perm" gorm:"column:required_perm;type:tinyint(1)" optional:"true" description:"开启鉴权"` + // ACL模式下, 允许的通过的身份标识符, 比如角色, 用户类型之类 + RequiredRole []string `json:"required_role" bson:"required_role" gorm:"column:required_role;serializer:json;type:json" optional:"true" description:"ACL模式下, 允许的通过的身份标识符, 比如角色, 用户类型之类"` + // 是否开启操作审计, 开启后这次操作将被记录 + RequiredAudit bool `json:"required_audit" bson:"required_audit" gorm:"column:required_audit;type:tinyint(1)" optional:"true" description:"是否开启操作审计, 开启后这次操作将被记录"` + // 名称空间不能为空 + RequiredNamespace bool `json:"required_namespace" bson:"required_namespace" gorm:"column:required_namespace;type:tinyint(1)" optional:"true" description:"名称空间不能为空"` + // 扩展信息 + Extras map[string]string `json:"extras" bson:"extras" gorm:"column:extras;serializer:json;type:json" optional:"true" description:"扩展信息"` +} + +// service-method-path +func (e *RouteEntry) BuildUUID() *RouteEntry { + e.UUID = uuid.NewSHA1(uuid.Nil, fmt.Appendf(nil, "%s-%s-%s", e.Service, e.Method, e.Path)).String() + return e +} + +func GetRouteMeta[T any](m map[string]any, key string) T { + if v, ok := m[key]; ok { + return v.(T) + } + + var t T + return t +} + +// func GetRouteMetaString(m map[string]any, key string) string { +// if v, ok := m[key]; ok { +// return v.(string) +// } + +// var t string +// return t +// } + +func (e *RouteEntry) LoadMeta(meta map[string]any) { + e.Service = application.Get().AppName + e.Resource = GetRouteMeta[string](meta, META_RESOURCE_KEY) + e.Action = GetRouteMeta[string](meta, META_ACTION_KEY) + e.RequiredAuth = GetRouteMeta[bool](meta, META_REQUIRED_AUTH_KEY) + e.RequiredCode = GetRouteMeta[bool](meta, META_REQUIRED_CODE_KEY) + e.RequiredPerm = GetRouteMeta[bool](meta, META_REQUIRED_PERM_KEY) + e.RequiredRole = GetRouteMeta[[]string](meta, META_REQUIRED_ROLE_KEY) + e.RequiredAudit = GetRouteMeta[bool](meta, META_REQUIRED_AUDIT_KEY) + e.RequiredNamespace = GetRouteMeta[bool](meta, META_REQUIRED_NAMESPACE_KEY) +} + +// UniquePath todo +func (e *RouteEntry) HasRequiredRole() bool { + return len(e.RequiredRole) > 0 +} + +// UniquePath todo +func (e *RouteEntry) UniquePath() string { + return fmt.Sprintf("%s.%s", e.Method, e.Path) +} + +func (e *RouteEntry) IsRequireRole(target string) bool { + for i := range e.RequiredRole { + if e.RequiredRole[i] == "*" { + return true + } + + if e.RequiredRole[i] == target { + return true + } + } + + return false +} + +func (e *RouteEntry) SetRequiredAuth(v bool) *RouteEntry { + e.RequiredAuth = v + return e +} + +func (e *RouteEntry) AddRequiredRole(roles ...string) *RouteEntry { + e.RequiredRole = append(e.RequiredRole, roles...) + return e +} + +func (e *RouteEntry) SetRequiredPerm(v bool) *RouteEntry { + e.RequiredPerm = v + return e +} + +func (e *RouteEntry) SetLabel(value string) *RouteEntry { + e.ActionLabel = value + return e +} + +func (e *RouteEntry) SetExtensionFromMap(m map[string]string) *RouteEntry { + if e.Extras == nil { + e.Extras = map[string]string{} + } + + for k, v := range m { + e.Extras[k] = v + } + return e +} + +func (e *RouteEntry) SetRequiredCode(v bool) *RouteEntry { + e.RequiredCode = v + return e +} + +func NewEntryFromRestRequest(req *restful.Request) *RouteEntry { + entry := NewRouteEntry() + + // 请求拦截 + route := req.SelectedRoute() + if route == nil { + return nil + } + + entry.FunctionName = route.Operation() + entry.Method = route.Method() + entry.LoadMeta(route.Metadata()) + entry.Path = route.Path() + return entry +} + +func NewEntryFromRestRouteReader(route restful.RouteReader) *RouteEntry { + entry := NewRouteEntry() + entry.FunctionName = route.Operation() + entry.Method = route.Method() + entry.LoadMeta(route.Metadata()) + entry.Path = route.Path() + return entry +} + +func NewEntryFromRestRoute(route restful.Route) *RouteEntry { + entry := NewRouteEntry() + entry.FunctionName = route.Operation + entry.Method = route.Method + entry.LoadMeta(route.Metadata) + entry.Path = route.Path + return entry +} + +func NewEntryFromRestfulContainer(c *restful.Container) (entries []*RouteEntry) { + wss := c.RegisteredWebServices() + for i := range wss { + for _, route := range wss[i].Routes() { + es := NewEntryFromRestRoute(route) + entries = append(entries, es) + } + } + return entries +} diff --git a/devcloud/mcenter/apps/registry.go b/devcloud/mcenter/apps/registry.go index 315c7cc..061cb54 100644 --- a/devcloud/mcenter/apps/registry.go +++ b/devcloud/mcenter/apps/registry.go @@ -9,4 +9,6 @@ import ( // 颁发器 _ "122.51.31.227/go-course/go18/devcloud/mcenter/apps/token/issuers" + // 鉴权中间件 + _ "122.51.31.227/go-course/go18/devcloud/mcenter/permission" ) diff --git a/devcloud/mcenter/apps/token/model.go b/devcloud/mcenter/apps/token/model.go index b6330a6..1b08eeb 100644 --- a/devcloud/mcenter/apps/token/model.go +++ b/devcloud/mcenter/apps/token/model.go @@ -34,6 +34,7 @@ func GetAccessTokenFromHTTP(r *http.Request) string { return tk } +// 从上下文中 提取 用户身份信息 func GetTokenFromCtx(ctx context.Context) *Token { if v := ctx.Value(CTX_TOKEN_KEY); v != nil { return v.(*Token) diff --git a/devcloud/mcenter/apps/user/api/api.go b/devcloud/mcenter/apps/user/api/api.go index 5b4b35a..a2bac75 100644 --- a/devcloud/mcenter/apps/user/api/api.go +++ b/devcloud/mcenter/apps/user/api/api.go @@ -2,6 +2,7 @@ package api import ( "122.51.31.227/go-course/go18/devcloud/mcenter/apps/user" + "122.51.31.227/go-course/go18/devcloud/mcenter/permission" "github.com/infraboard/mcube/v2/ioc" "github.com/infraboard/mcube/v2/ioc/config/gorestful" @@ -29,9 +30,13 @@ func (h *UserRestfulApiHandler) Init() error { tags := []string{"用户登录"} ws := gorestful.ObjectRouter(h) + // required_auth=true/false ws.Route(ws.GET("").To(h.QueryUser). Doc("用户列表查询"). Metadata(restfulspec.KeyOpenAPITags, tags). + // 这个开关怎么生效 + // 中间件需求读取接口的描述信息,来决定是否需要认证 + Metadata(permission.Auth(true)). Param(restful.QueryParameter("page_size", "分页大小").DataType("integer")). Param(restful.QueryParameter("page_number", "页码").DataType("integer")). Writes(Set{}). diff --git a/devcloud/mcenter/apps/user/api/user.go b/devcloud/mcenter/apps/user/api/user.go index a8aa811..660cba4 100644 --- a/devcloud/mcenter/apps/user/api/user.go +++ b/devcloud/mcenter/apps/user/api/user.go @@ -8,6 +8,12 @@ import ( ) func (h *UserRestfulApiHandler) QueryUser(r *restful.Request, w *restful.Response) { + // 补充下 Token校验 + // 作为一个开发者, 业务接口开发代码里面,需要补充认证 + // 通过中间件 来 剥离开 用户认证逻辑: + // 站在一个库作者的角度 来设计一个 认证的使用方式, 能不能 通过开关来控制一个接口需不需要被保护(on/off) + // 这个开关应该加那里? 接口描述(接口的装饰信息) + // 获取用户通过API传入的参数 req := user.NewQueryUserRequest() @@ -36,5 +42,6 @@ func (h *UserRestfulApiHandler) QueryUser(r *restful.Request, w *restful.Respons // 能不能直接通过JSON标签 这样方式来完成脱敏: must:"3,4" (181*****4777) // 不能每次都调用吧,因此这个脱敏逻辑 放到 Rsep函数内进行处理 + // response.Success(w, set) } diff --git a/devcloud/mcenter/permission/README.md b/devcloud/mcenter/permission/README.md new file mode 100644 index 0000000..76e2704 --- /dev/null +++ b/devcloud/mcenter/permission/README.md @@ -0,0 +1,19 @@ +# 接口鉴权工具(中间件) + + +1. 路有装饰, 路有配置 +```go +// required_auth=true/false +ws.Route(ws.GET("").To(h.QueryUser). + Doc("用户列表查询"). + Metadata(restfulspec.KeyOpenAPITags, tags). + // 这个开关怎么生效 + // 中间件需求读取接口的描述信息,来决定是否需要认证 + Metadata(permission.Auth(true)). + Param(restful.QueryParameter("page_size", "分页大小").DataType("integer")). + Param(restful.QueryParameter("page_number", "页码").DataType("integer")). + Writes(Set{}). + Returns(200, "OK", Set{})) +``` + +2. 加载鉴权处理逻辑(中间件) \ No newline at end of file diff --git a/devcloud/mcenter/permission/checker.go b/devcloud/mcenter/permission/checker.go new file mode 100644 index 0000000..ed96a80 --- /dev/null +++ b/devcloud/mcenter/permission/checker.go @@ -0,0 +1,120 @@ +package permission + +import ( + "context" + + "122.51.31.227/go-course/go18/devcloud/mcenter/apps/endpoint" + "122.51.31.227/go-course/go18/devcloud/mcenter/apps/token" + "github.com/emicklei/go-restful/v3" + "github.com/infraboard/mcube/v2/exception" + "github.com/infraboard/mcube/v2/http/restful/response" + "github.com/infraboard/mcube/v2/ioc" + "github.com/infraboard/mcube/v2/ioc/config/gorestful" + "github.com/infraboard/mcube/v2/ioc/config/log" + "github.com/rs/zerolog" +) + +func init() { + ioc.Config().Registry(&Checker{}) +} + +func Auth(v bool) (string, bool) { + return endpoint.META_REQUIRED_AUTH_KEY, v +} + +func Permission(v bool) (string, bool) { + return endpoint.META_REQUIRED_PERM_KEY, v +} + +func Resource(v string) (string, string) { + return endpoint.META_RESOURCE_KEY, v +} + +func Action(v string) (string, string) { + return endpoint.META_ACTION_KEY, v +} + +// 这个中间件也是对象, 认证与鉴权 +// 通过路由装饰 来当中开关,控制怎么认证,是否开启认证,是否开启坚强,角色标记 +type Checker struct { + ioc.ObjectImpl + log *zerolog.Logger + + token token.Service + // policy policy.Service +} + +// 中间件对象名称 +func (c *Checker) Name() string { + return "permission_checker" +} + +// 对象初始化的优先级, 由于业务接口在Init函数里面 使用默认优先级0, 由大到小 +// 框架是 899 898 +// 框架的init函数调用完成,里面调用 这个对象的init函数, 实现了全局中间件 +func (c *Checker) Priority() int { + return gorestful.Priority() - 1 +} + +func (c *Checker) Init() error { + c.log = log.Sub(c.Name()) + c.token = token.GetService() + // c.policy = policy.GetService() + + // 注册认证中间件 + gorestful.RootRouter().Filter(c.Check) + return nil +} + +// 中间件的函数里面 +func (c *Checker) Check(r *restful.Request, w *restful.Response, next *restful.FilterChain) { + // 请求处理前, 对接口进行保护 + // 1. 知道用户当前访问的是哪个接口, 当前url 匹配到的路由是哪个 + // SelectedRoute, 它可以返回当前URL适配哪个路有, RouteReader + // 封装了一个函数 来获取Meta信息 NewEntryFromRestRouteReader + route := endpoint.NewEntryFromRestRouteReader(r.SelectedRoute()) + if route.RequiredAuth { + // 校验身份 + tk, err := c.CheckToken(r) + if err != nil { + response.Failed(w, err) + return + } + + // 校验权限 + if err := c.CheckPolicy(r, tk, route); err != nil { + response.Failed(w, err) + return + } + } + + // 请求处理 + next.ProcessFilter(r, w) + + // 请求处理后 +} + +func (c *Checker) CheckToken(r *restful.Request) (*token.Token, error) { + v := token.GetAccessTokenFromHTTP(r.Request) + if v == "" { + return nil, exception.NewUnauthorized("请先登录") + } + + tk, err := c.token.ValiateToken(r.Request.Context(), token.NewValiateTokenRequest(v)) + if err != nil { + return nil, err + } + + // 如果校验成功,需要把 用户的身份信息,放到请求的上下文中,方便后面的逻辑获取 + // context.WithValue 来往ctx 添加 value + // key: value, value token对象 + ctx := context.WithValue(r.Request.Context(), token.CTX_TOKEN_KEY, tk) + + // ctx 生成一个新的,继续往下传递 + r.Request = r.Request.WithContext(ctx) + return tk, nil +} + +func (c *Checker) CheckPolicy(r *restful.Request, tk *token.Token, route *endpoint.RouteEntry) error { + return nil +} diff --git a/devcloud/mcenter/permission/design.drawio b/devcloud/mcenter/permission/design.drawio new file mode 100644 index 0000000..e44d0ed --- /dev/null +++ b/devcloud/mcenter/permission/design.drawio @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file