You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.15 06:54:25