背景
在设计RBAC模型的管理后台时,当更新角色/用户等场景,需要进行安全策略校验
典型校验规则如:
- 是否允许创建多个超管
- 当前用户的权限
- 是否具备超管权限
- 当前用户的身份
- 是否允许更新比当前用户拥有角色、权限更大的用户
等等,需要依次校验完,才能继续往下执行更新逻辑
设计模式的选择
如何执行以上业务规则执行和逻辑判断,通常有多种设计模式来实现
- 责任链模式(Chain of Responsibility):
这是一个很自然的选择,因为每次权限校验操作(如身份验证、权限检验、角色权限范围校验等)都可以视为责任链中的一个环节。可以创建不同的校验处理器,每个处理器对应一个校验规则,然后组成一个链条依次执行。如果某个环节校验失败,则校验过程终止,返回错误。
- 策略模式(Strategy):
如果校验规则在不同场景下经常更换或者相同规则需要不同实现,策略模式会适用。可以定义一个策略接口,以及多个实现了该接口的策略类,每个类封装了一个具体的校验规则。然后在上下文中根据需要选择并应用合适的策略。
- 工厂模式(Factory):
使用工厂模式可以根据不同请求或者上下文情况创建合适的校验对象。例如,可能需要不同类型的校验器来处理不同类型的用户或角色。工厂可以根据请求的具体内容决定实例化哪个校验器。
- 装饰者模式(Decorator):
如果权限校验是增强原有功能的话,装饰者模式可能适用。可以构建核心校验逻辑,然后根据需要层层添加额外的校验规则,每一层都能增加一些新的校验逻辑。
- 状态模式(State):
如果权限验证过程彼此关联并依赖于预先的验证结果,则状态模式可以用来表示权限校验的不同状态和过渡。每一个状态都包含了特定的行为(即权限校验规则),且状态之间可以相互转换。
- 观察者模式(Observer):
如果需要在权限校验规则变更时通知到其他相关组件(例如,用户界面需要更新或审计记录需要写入),观察者模式可用于实现这样的事件驱动逻辑。
设计模式的选择需要基于具体的业务需求和实现的复杂性。例如,在执行权限校验时,如果校验步骤是固定的、一致的,并且需要按特定顺序执行,责任链模式会比较合适。如果各个角色或操作可能需要不同的校验规则组合,可能需要策略模式灵活处理不同的规则选择。如果校验逻辑很复杂,依赖于多个状态和过渡,可能需要状态模式来管理状态转换的逻辑。有时候,一个复杂的系统可能需要组合使用多种设计模式来满足需求。
场景和选择
责任链设计模式实现
使用责任链模式实现基于角色的权限校验系统,需要定义一个Handler
接口和具体的校验功能,例如身份验证、角色权限检查等等。
首先,定义Handler
接口和基础处理器BaseHandler
:
go
package main
import (
"fmt"
)
// 请求上下文,包含所需的校验信息,可根据实际场景扩展
type RequestContext struct {
UserID string
Role string
Action string // 用于校验该用户是否有权限执行某个操作
}
// Handler 接口定义了处理请求的 Handle 方法和设置下个处理器的 SetNext 方法
type Handler interface {
Handle(request *RequestContext) bool
SetNext(handler Handler) Handler
}
// BaseHandler 结构提供了 SetNext 方法,其他所有具体的处理器都可以嵌入此结构体
type BaseHandler struct {
next Handler
}
// SetNext 设置链上的下一个处理者
func (h *BaseHandler) SetNext(handler Handler) Handler {
h.next = handler
return handler
}
// HandleNext 调用链上的下一个处理者
func (h *BaseHandler) HandleNext(request *RequestContext) bool {
if h.next != nil {
return h.next.Handle(request)
}
// 如果没有下一个处理者,则返回 true,表示默认通过
return true
}
接下来创建具体的处理器对象,例如AuthenticationHandler
处理用户身份验证,AuthorizationHandler
处理权限检验,以及RolePermissionHandler
处理角色权限范围校验:
go
// AuthenticationHandler 负责身份验证
type AuthenticationHandler struct {
BaseHandler
}
func (h *AuthenticationHandler) Handle(request *RequestContext) bool {
// 这里实现具体的身份验证逻辑
if request.UserID == "user123" { // 假设"user123"是验证通过的例子
fmt.Println("身份验证成功,用户:", request.UserID)
return h.HandleNext(request)
}
fmt.Println("身份验证失败,用户:", request.UserID)
return false
}
// AuthorizationHandler 负责权限检验
type AuthorizationHandler struct {
BaseHandler
}
func (h *AuthorizationHandler) Handle(request *RequestContext) bool {
// 这里实现具体的权限检验逻辑
authorizedActions := map[string][]string{
"admin": []string{"create", "update", "delete"},
"user": []string{"read"},
}
// 确定用户角色是否有权限执行特定操作
if actions, ok := authorizedActions[request.Role]; ok {
for _, action := range actions {
if action == request.Action {
fmt.Println("权限检验成功,角色:", request.Role, "操作:", request.Action)
return h.HandleNext(request)
}
}
}
fmt.Println("权限检验失败,角色:", request.Role, "操作:", request.Action)
return false
}
// RolePermissionHandler 负责角色权限范围校验
type RolePermissionHandler struct {
BaseHandler
}
func (h *RolePermissionHandler) Handle(request *RequestContext) bool {
// 校验角色权限范围,例如检查角色是否越权等
// 这里仅示例,不具体判断逻辑
// 假设传入的请求总是在权限范围内
fmt.Println("角色权限范围校验成功,角色:", request.Role)
return h.HandleNext(request)
}
最后,在main
函数中组合这些处理器以形成一个责任链,并执行权限校验:
go
func main() {
request := &RequestContext{
UserID: "user123",
Role: "admin",
Action: "create",
}
// 构建责任链
authHandler := &AuthenticationHandler{}
authzHandler := &AuthorizationHandler{}
rolePermissionHandler := &RolePermissionHandler{}
authHandler.SetNext(authzHandler).SetNext(rolePermissionHandler)
// 执行处理链
if authHandler.Handle(request) {
fmt.Println("权限校验通过,操作允许")
} else {
fmt.Println("权限校验不通过,操作拒绝")
}
}
// 输出结果:
// 身份验证成功,用户: user123
// 权限检验成功,角色: admin 操作: create
// 角色权限范围校验成功,角色: admin
// 权限校验通过,操作允许
以上代码片段中main
函数执行输出了各个责任链处理器的校验过程和结果。如果请求的用户ID、角色或行为不满足某个校验环节的条件,该环节会返回 false 并终止责任链的继续执行,此时权限校验不通过。
在真实业务场景中,身份验证、权限校验和角色权限范围校验通常涉及对数据库或其他服务的查询操作,需要根据实际需要在对应的处理器中正确实现这些逻辑。
使用责任链模式,可以轻松地添加、移除或重新排列责任链中的处理器,甚至在运行时动态地改变处理链的组合,这使得能够方便地扩展或调整权限校验逻辑。同时,责任链模式也使得每个校验处理器的职责单一,增强了代码的可读性和可维护性。
策略模式实现
策略模式允许在运行时选择最合适的算法或行为。在权限校验的场景中,各种校验逻辑可以被抽象为独立的策略,并且可以互换使用。这样,可以根据请求的不同场景来应用不同的校验策略,而不需要修改现有的代码。下面是如何使用策略模式来实现身份验证、权限校验和角色权限范围校验。
首先,定义一个Strategy
接口,以及它的实现者来代表不同的校验规则:
go
// Strategy 接口定义了权限校验的方法
type Strategy interface {
Execute(*RequestContext) bool
}
// AuthenticationStrategy 实现了身份验证策略
type AuthenticationStrategy struct{}
func (s *AuthenticationStrategy) Execute(ctx *RequestContext) bool {
// 实现具体的身份验证逻辑
if ctx.UserID == "user123" { // 示例:只有 user123 被视为验证通过
fmt.Println("身份验证成功,用户ID:", ctx.UserID)
return true
}
fmt.Println("身份验证失败,用户ID:", ctx.UserID)
return false
}
// AuthorizationStrategy 实现了权限校验策略
type AuthorizationStrategy struct{}
func (s *AuthorizationStrategy) Execute(ctx *RequestContext) bool {
// 实现具体的权限校验逻辑
authorizedActions := map[string][]string{
"admin": {"create", "update", "delete"},
"user": {"read"},
}
if actions, ok := authorizedActions[ctx.Role]; ok {
for _, action := range actions {
if action == ctx.Action {
fmt.Println("权限校验成功,角色:", ctx.Role, "操作:", ctx.Action)
return true
}
}
}
fmt.Println("权限校验失败,角色:", ctx.Role, "操作:", ctx.Action)
return false
}
// RolePermissionStrategy 实现了角色权限范围校验策略
type RolePermissionStrategy struct{}
func (s *RolePermissionStrategy) Execute(ctx *RequestContext) bool {
// 实现具体的角色权限范围校验逻辑
// 示例中不具体实现,此处总是校验成功
fmt.Println("角色权限范围校验成功,角色:", ctx.Role)
return true
}
接下来,定义一个Context
结构体用来管理某一具体场景下所要使用的策略:
go
type Context struct {
strategies []Strategy
}
func (c *Context) AddStrategy(strategy Strategy) {
c.strategies = append(c.strategies, strategy)
}
func (c *Context) Execute(ctx *RequestContext) bool {
for _, strategy := range c.strategies {
if !strategy.Execute(ctx) {
return false
}
}
return true
}
最后,在main
函数中,可以根据场景配置不同的策略:
go
func main() {
ctx := &RequestContext{
UserID: "user123",
Role: "admin",
Action: "create",
}
// 创建一个新的场景,并配置策略
scenario := &Context{}
scenario.AddStrategy(&AuthenticationStrategy{})
scenario.AddStrategy(&AuthorizationStrategy{})
scenario.AddStrategy(&RolePermissionStrategy{})
// 执行策略
if scenario.Execute(ctx) {
fmt.Println("所有校验规则通过,允许操作。")
} else {
fmt.Println("校验规则未通过,拒绝操作。")
}
}
当执行main
函数的时候,系统会通过所配置的策略来执行校验流程。如果所有的策略都返回true
,则权限校验通过;如果任何策略返回false
,则校验失败,整个计划不会继续执行。
策略模式在这里的好处是强调了校验逻辑的可插拔性和可复用性。可以方便地切换、添加或移除策略,而不影响上下文场景的其他部分。这种模式特别适用于当希望在不同环境下(比如不同的客户端请求,或者不同的用户权限级别)应用不同的验证规则,却不需要为每种环境编写唯一的验证逻辑。
策略模式的好处在于:
- 它满足开闭原则,可以添加新的策略而无需改动现有代码。
- 它提供了高度的灵活性,不同策略之间可以方便地相互切换。
- 每个策略都封装了自己的算法,保持代码的可维护性和清晰性。
在实际场景中,如果策略之间有共享的数据或者依赖关系,可能需要更复杂的上下文管理。此外,可能还需要一个策略工厂来根据不同条件产生合适的策略,尤其是在策略数量很多或者策略逻辑较为复杂的情况下。
总之,策略模式特别适合权限校验规则这种需要容易扩展和更换算法的场景,通过分离算法和上下文环境,可以使得添加新的验证规则或更改现有验证规则变得更加直接和容易。