Servant中多级权限用户组的API权限系统实现方案问询
优雅实现Servant层级化权限系统的方案
很棒的问题!在Servant里处理这种管理员→超级用户→普通用户的层级化权限需求,完全不用重复写多个AuthProtect组合子。我在实际项目里用过一种基于类型级权限等级+自定义认证组合子的方案,既符合你要的超集逻辑,又能大幅减少重复代码。
核心思路
我们的目标是:
- 用一个通用的认证组合子,通过指定"最低权限等级"来控制接口访问
- 利用Haskell的类型系统(DataKinds、类型族)在编译期就确保权限逻辑的正确性
- 天然支持"高权限角色自动拥有低权限接口访问权"的超集特性
步骤1:定义权限等级与用户类型
首先用ADT定义层级化的角色,注意要导出Ord实例——因为我们要通过角色的顺序来判断权限高低:
{-# LANGUAGE DataKinds #-} {-# LANGUAGE TypeOperators #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE AllowAmbiguousTypes #-} {-# LANGUAGE ScopedTypeVariables #-} data UserRole = RegularUser | SuperUser | Admin deriving (Eq, Ord, Show, Enum, Bounded) -- 把角色提升到类型级别,方便在API类型里使用 type RegularUser = 'RegularUser type SuperUser = 'SuperUser type Admin = 'Admin -- 带角色信息的用户类型 data User = User { userId :: UUID , userRole :: UserRole -- 其他字段:用户名、邮箱等 }
这里Ord实例的默认行为正好符合我们的需求:Admin > SuperUser > RegularUser,完美对应超集关系。
步骤2:自定义通用权限认证组合子
我们定义一个AuthWithMinRole组合子,它接受一个类型级的最低权限要求作为参数。然后实现Servant的HasServer实例,在其中完成"验证用户权限是否达标"的逻辑:
-- 自定义认证组合子:要求用户权限不低于指定的minRole data AuthWithMinRole (minRole :: UserRole) -- 类型族:把类型级角色映射为枚举数值,方便比较 type family FromEnumRole (r :: UserRole) :: Nat where FromEnumRole 'RegularUser = 0 FromEnumRole 'SuperUser = 1 FromEnumRole 'Admin = 2 -- 辅助函数:从类型级角色获取对应的Term级值 getMinRole :: forall r. KnownNat (FromEnumRole r) => UserRole getMinRole = toEnum (fromIntegral (natVal (Proxy :: Proxy (FromEnumRole r)))) -- 实现HasServer实例,整合权限检查逻辑 instance (HasServer api context) => HasServer (AuthWithMinRole minRole :> api) context where type ServerT (AuthWithMinRole minRole :> api) m = User -> ServerT api m route _ ctx server = route (Proxy :: Proxy api) ctx $ hoistServerWithContext (Proxy :: Proxy api) ctx (return . checkPermission) server where checkPermission :: User -> m User checkPermission user = do let userRoleLevel = fromEnum (userRole user) minRequiredLevel = fromEnum (getMinRole @minRole) if userRoleLevel >= minRequiredLevel then return user else throwError err403 { errBody = "权限不足:需要至少" <> pack (show (getMinRole @minRole)) <> "权限" }
这个组合子的好处是:一次定义,终身复用——不管你有多少个角色等级,只需在API里指定对应的最低权限即可。
步骤3:定义层级化的API
现在我们可以轻松构建符合超集要求的API了:
-- 普通用户可访问的接口 type RegularAPI = AuthWithMinRole RegularUser :> "profile" :> Get '[JSON] User :<|> AuthWithMinRole RegularUser :> "posts" :> Get '[JSON] [Post] -- 超级用户可访问的接口(包含普通用户的所有接口) type SuperAPI = RegularAPI :<|> AuthWithMinRole SuperUser :> "users" :> Get '[JSON] [User] :<|> AuthWithMinRole SuperUser :> "posts" :> Capture "postId" UUID :> Delete '[JSON] NoContent -- 管理员可访问的接口(包含超级用户的所有接口) type AdminAPI = SuperAPI :<|> AuthWithMinRole Admin :> "settings" :> ReqBody '[JSON] AppSettings :> Post '[JSON] Bool :<|> AuthWithMinRole Admin :> "roles" :> Capture "userId" UUID :> ReqBody '[JSON] UserRole :> Put '[JSON] User -- 完整API入口 type FullAPI = RegularAPI :<|> SuperAPI :<|> AdminAPI
看到没?管理员的API直接继承了超级用户的所有接口,超级用户又继承了普通用户的——完全符合你要的超集逻辑,而且没有任何重复代码。
额外优化:避免重复挂载认证组合子
如果某个API组里所有接口的权限要求相同,还可以用Servant的(:>)把认证组合子提取到组的最外层:
-- 优化后的超级用户API:不用给每个接口单独加AuthWithMinRole type SuperAPI = AuthWithMinRole SuperUser :> ( "users" :> Get '[JSON] [User] :<|> "posts" :> Capture "postId" UUID :> Delete '[JSON] NoContent :<|> RegularAPI -- 直接包含普通用户接口,自动继承权限检查 )
这里要注意:普通用户接口已经挂载了AuthWithMinRole RegularUser,超级用户访问这些接口时,权限检查会自动通过(因为SuperUser > RegularUser),所以直接包含即可。
为什么这比多个AuthProtect好?
- 无重复代码:不用为每个角色写单独的
AuthProtect和对应的认证逻辑,一个AuthWithMinRole搞定所有 - 类型安全:编译期就能检查权限等级是否合法,避免运行时错误
- 扩展性强:如果以后要加新的角色(比如
Moderator),只需在UserRole里加一个构造器,更新FromEnumRole类型族即可,无需修改认证逻辑 - 天然支持超集:利用
Ord实例的顺序,高权限角色自动拥有低权限接口的访问权
内容的提问来源于stack exchange,提问作者nnnmmm
相关产品推荐
相关产品推荐

