补充鉴权中间件

This commit is contained in:
yumaojun03 2025-06-08 11:18:31 +08:00
parent 9b8b96856c
commit f1d8684ab7
9 changed files with 498 additions and 0 deletions

View File

@ -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"
)

View File

@ -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
}

View File

@ -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/apps/token/issuers"
// 鉴权中间件
_ "122.51.31.227/go-course/go18/devcloud/mcenter/permission"
) )

View File

@ -34,6 +34,7 @@ func GetAccessTokenFromHTTP(r *http.Request) string {
return tk return tk
} }
// 从上下文中 提取 用户身份信息
func GetTokenFromCtx(ctx context.Context) *Token { func GetTokenFromCtx(ctx context.Context) *Token {
if v := ctx.Value(CTX_TOKEN_KEY); v != nil { if v := ctx.Value(CTX_TOKEN_KEY); v != nil {
return v.(*Token) return v.(*Token)

View File

@ -2,6 +2,7 @@ package api
import ( import (
"122.51.31.227/go-course/go18/devcloud/mcenter/apps/user" "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"
"github.com/infraboard/mcube/v2/ioc/config/gorestful" "github.com/infraboard/mcube/v2/ioc/config/gorestful"
@ -29,9 +30,13 @@ func (h *UserRestfulApiHandler) Init() error {
tags := []string{"用户登录"} tags := []string{"用户登录"}
ws := gorestful.ObjectRouter(h) ws := gorestful.ObjectRouter(h)
// required_auth=true/false
ws.Route(ws.GET("").To(h.QueryUser). ws.Route(ws.GET("").To(h.QueryUser).
Doc("用户列表查询"). Doc("用户列表查询").
Metadata(restfulspec.KeyOpenAPITags, tags). Metadata(restfulspec.KeyOpenAPITags, tags).
// 这个开关怎么生效
// 中间件需求读取接口的描述信息,来决定是否需要认证
Metadata(permission.Auth(true)).
Param(restful.QueryParameter("page_size", "分页大小").DataType("integer")). Param(restful.QueryParameter("page_size", "分页大小").DataType("integer")).
Param(restful.QueryParameter("page_number", "页码").DataType("integer")). Param(restful.QueryParameter("page_number", "页码").DataType("integer")).
Writes(Set{}). Writes(Set{}).

View File

@ -8,6 +8,12 @@ import (
) )
func (h *UserRestfulApiHandler) QueryUser(r *restful.Request, w *restful.Response) { func (h *UserRestfulApiHandler) QueryUser(r *restful.Request, w *restful.Response) {
// 补充下 Token校验
// 作为一个开发者, 业务接口开发代码里面,需要补充认证
// 通过中间件 来 剥离开 用户认证逻辑:
// 站在一个库作者的角度 来设计一个 认证的使用方式, 能不能 通过开关来控制一个接口需不需要被保护(on/off)
// 这个开关应该加那里? 接口描述(接口的装饰信息)
// 获取用户通过API传入的参数 // 获取用户通过API传入的参数
req := user.NewQueryUserRequest() req := user.NewQueryUserRequest()
@ -36,5 +42,6 @@ func (h *UserRestfulApiHandler) QueryUser(r *restful.Request, w *restful.Respons
// 能不能直接通过JSON标签 这样方式来完成脱敏: must:"3,4" (181*****4777) // 能不能直接通过JSON标签 这样方式来完成脱敏: must:"3,4" (181*****4777)
// 不能每次都调用吧,因此这个脱敏逻辑 放到 Rsep函数内进行处理 // 不能每次都调用吧,因此这个脱敏逻辑 放到 Rsep函数内进行处理
//
response.Success(w, set) response.Success(w, set)
} }

View File

@ -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. 加载鉴权处理逻辑(中间件)

View File

@ -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
}

View File

@ -0,0 +1,76 @@
<mxfile host="65bd71144e">
<diagram id="h2EQwLDlSJcMUsYSktMs" name="第 1 页">
<mxGraphModel dx="892" dy="370" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="2" value="QuerUser" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="650" y="190" width="120" height="60" as="geometry"/>
</mxCell>
<mxCell id="3" value="" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="490" y="70" width="70" height="310" as="geometry"/>
</mxCell>
<mxCell id="5" value="Actor" style="shape=umlActor;verticalLabelPosition=bottom;verticalAlign=top;html=1;outlineConnect=0;" vertex="1" parent="1">
<mxGeometry x="710" y="70" width="30" height="60" as="geometry"/>
</mxCell>
<mxCell id="6" value="A" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
<mxGeometry x="740" y="85" width="60" height="30" as="geometry"/>
</mxCell>
<mxCell id="7" value="Actor" style="shape=umlActor;verticalLabelPosition=bottom;verticalAlign=top;html=1;outlineConnect=0;" vertex="1" parent="1">
<mxGeometry x="330" y="10" width="30" height="60" as="geometry"/>
</mxCell>
<mxCell id="8" value="B" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
<mxGeometry x="370" y="30" width="60" height="30" as="geometry"/>
</mxCell>
<mxCell id="9" value="Actor" style="shape=umlActor;verticalLabelPosition=bottom;verticalAlign=top;html=1;outlineConnect=0;" vertex="1" parent="1">
<mxGeometry x="80" y="115" width="30" height="60" as="geometry"/>
</mxCell>
<mxCell id="10" value="" style="ellipse;shape=cloud;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="120" y="175" width="120" height="80" as="geometry"/>
</mxCell>
<mxCell id="11" value="User" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
<mxGeometry x="65" y="70" width="60" height="30" as="geometry"/>
</mxCell>
<mxCell id="12" style="edgeStyle=none;html=1;exitX=1;exitY=0.3333333333333333;exitDx=0;exitDy=0;exitPerimeter=0;entryX=0.4;entryY=0.1;entryDx=0;entryDy=0;entryPerimeter=0;" edge="1" parent="1" source="9" target="10">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="21" style="edgeStyle=none;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="13" target="3">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="13" value="API&amp;nbsp;&lt;div&gt;Server&lt;/div&gt;" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="400" y="70" width="70" height="310" as="geometry"/>
</mxCell>
<mxCell id="14" style="edgeStyle=none;html=1;exitX=0.96;exitY=0.7;exitDx=0;exitDy=0;exitPerimeter=0;entryX=-0.002;entryY=0.525;entryDx=0;entryDy=0;entryPerimeter=0;" edge="1" parent="1" source="10" target="13">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="15" value="Request" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
<mxGeometry x="150" y="100" width="60" height="30" as="geometry"/>
</mxCell>
<mxCell id="16" style="edgeStyle=none;html=1;exitX=0;exitY=0.75;exitDx=0;exitDy=0;entryX=0.55;entryY=0.95;entryDx=0;entryDy=0;entryPerimeter=0;" edge="1" parent="1" source="13" target="10">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="17" value="Response" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
<mxGeometry x="250" y="290" width="60" height="30" as="geometry"/>
</mxCell>
<mxCell id="18" value="业务逻辑处理" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
<mxGeometry x="700" y="300" width="60" height="30" as="geometry"/>
</mxCell>
<mxCell id="19" value="" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="500" y="80" width="70" height="310" as="geometry"/>
</mxCell>
<mxCell id="20" value="中间件&lt;div&gt;...&lt;/div&gt;" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
<mxGeometry x="510" y="90" width="70" height="310" as="geometry"/>
</mxCell>
<mxCell id="22" style="edgeStyle=none;html=1;exitX=1;exitY=0.25;exitDx=0;exitDy=0;entryX=0.017;entryY=0.371;entryDx=0;entryDy=0;entryPerimeter=0;" edge="1" parent="1" source="20" target="2">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="23" style="edgeStyle=none;html=1;exitX=0;exitY=0.75;exitDx=0;exitDy=0;entryX=0.971;entryY=0.727;entryDx=0;entryDy=0;entryPerimeter=0;" edge="1" parent="1" source="2" target="20">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<mxCell id="24" value="Request, Response,&lt;div&gt;Next&lt;/div&gt;" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
<mxGeometry x="510" y="430" width="60" height="30" as="geometry"/>
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>