@h-ai/iam
v0.1.0-alpha.10
Published
Hai Framework identity and access management module.
Readme
@h-ai/iam
身份与访问管理模块,提供统一的 iam 对象实现认证、会话、授权与用户管理。
功能特性
| 功能 | 说明 | | -------------- | ------------------------------------------------------ | | 认证 | 密码、OTP(邮箱/短信验证码)、LDAP、API Key 多策略认证 | | 会话 | 有状态会话(随机访问令牌 + 缓存,滑动续期可选) | | 授权 | RBAC 角色与权限管理(DB + 缓存,通配符权限匹配) | | 用户管理 | 注册、查询、更新、密码重置、管理员重置密码 | | API Key | API Key 创建、吊销、验证,支持 scope 与过期时间 | | 前端客户端 | HTTP API 契约定义,支持独立前端使用 |
安装
pnpm add @h-ai/iam依赖
@h-ai/reldb— 数据库(用户/角色/权限持久化),需在 iam.init() 前初始化@h-ai/cache— 缓存(会话/OTP/重置令牌/权限缓存),需在 iam.init() 前初始化@h-ai/crypto— 密码哈希(内部使用,自动初始化)@h-ai/audit— 审计日志(RBAC 关键操作记录)
快速开始
import { cache } from '@h-ai/cache'
import { iam } from '@h-ai/iam'
import { reldb } from '@h-ai/reldb'
// 1. 初始化依赖
await reldb.init({ type: 'sqlite', database: './data.db' })
await cache.init({ type: 'memory' })
// 2. 初始化 IAM(自动使用已初始化的 reldb 和 cache 单例)
await iam.init({
session: { maxAge: 86400, sliding: true },
// OTP 回调(启用 OTP 登录时注入发送逻辑)
onOtpSendEmail: async (email, code) => {
await sendEmail(email, `验证码: ${code}`)
},
// 密码重置回调(启用密码重置时注入通知逻辑)
onPasswordResetRequest: async (user, token, expiresAt) => {
await sendEmail(user.email!, `重置链接: https://example.com/reset?token=${token}`)
},
})
// 2. 注册
const userResult = await iam.user.register({
username: 'admin',
email: '[email protected]',
password: 'Password123',
})
// 3. 登录
const loginResult = await iam.auth.login({
identifier: 'admin',
password: 'Password123',
})
if (loginResult.success) {
const { user, tokens } = loginResult.data
}
// 4. 验证令牌
const session = await iam.auth.verifyToken(loginResult.data.tokens.accessToken)
// 5. OTP 验证码登录
await iam.auth.sendOtp('[email protected]')
const otpResult = await iam.auth.loginWithOtp({
identifier: '[email protected]',
code: '123456',
})
// 6. 检查权限
const hasPermission = await iam.authz.checkPermission(
loginResult.data.user.id,
'user:read',
)
// 7. 关闭
await iam.close()设计说明
架构概览
IAM 是生命周期单例模块,通过 iam.init() / iam.close() 管理运行时状态。内部由 5 个子模块构成,按依赖顺序初始化:
iam.init()
├─ session (缓存会话管理,无 DB 依赖)
├─ authz (RBAC 角色/权限,DB + 缓存)
├─ authn (认证策略,依赖 session + authz)
├─ user (用户管理,依赖 session + authz + passwordStrategy)
└─ seed (种子数据,依赖 authz)所有子模块使用工厂 + 闭包模式创建(如 createSessionOperations(deps)),不使用 class。通过 getter 暴露给调用方(iam.auth / iam.user / iam.authz / iam.session / iam.apiKey)。
初始化前访问任何子模块方法,均返回 NOT_INITIALIZED 错误(基于 NotInitializedKit Proxy)。
数据存储分布
| 数据类型 | 存储位置 | 说明 |
| ------------------------------------------------- | ------------ | ----------------------------------------------------------------------------------------------------------------------------- |
| 用户、角色、权限、关联关系、API Key | DB(6 张表) | hai_iam_users、hai_iam_roles、hai_iam_permissions、hai_iam_role_permissions、hai_iam_user_roles、hai_iam_api_keys |
| 会话(accessToken → Session) | 缓存 | 有 TTL,滑动续期可选 |
| Token 映射(refreshToken → userId + accessToken) | 缓存 | 独立 key,refresh 后旧 token 立即失效 |
| OTP 验证码 | 缓存 | 有 TTL + 尝试次数限制 |
| 密码重置令牌 | 缓存 | 有 TTL + 最大验证次数限制 |
| API Key | DB | hai_iam_api_keys,明文不落库,仅存 hash;前缀用于候选检索 |
子模块:认证(authn)— iam.auth
关键对象
AuthnOperations— 认证操作接口,包含login/loginWithOtp/loginWithLdap/loginWithApiKey/logout/verifyToken/sendOtp/registerAndLoginAuthStrategy— 认证策略接口(type+authenticate+challenge?),4 种实现:密码、OTP、LDAP、API KeyAuthResult— 登录结果,含用户信息、TokenPair(accessToken + refreshToken)、角色/权限列表Credentials— 统一凭证联合类型({ type: 'password' | 'otp' | 'ldap' | 'apikey' } & 具体凭证)
关键流程
密码登录流程:
login(credentials)
→ 检查密码登录已启用
→ 按 identifier 查找用户(username / email / phone 三字段匹配)
→ 用户不存在时执行 dummy hash(防 timing attack,固定耗时)
→ 检查账户是否锁定(lockoutDuration 内 loginFailedCount >= maxLoginAttempts)
→ 密码哈希验证(@h-ai/crypto)
→ 验证失败:记录失败次数,达上限则锁定账户
→ 验证成功:重置失败计数
→ 检查密码是否过期(expirationDays > 0 时检查 passwordUpdatedAt)
→ 查询用户角色 + 权限
→ 创建会话(session.create → 生成 TokenPair)
→ 返回 AuthResult { user, tokens, roles, permissions, agreements? }Token 验证流程:
verifyToken(accessToken)
→ session.get(accessToken) → 缓存查询
→ 滑动续期模式下自动延长 TTL
→ 返回 Session 对象(含 userId、roles、permissions)使用方式
// 密码登录
const result = await iam.auth.login({ identifier: 'admin', password: 'Password123' })
// OTP 登录
await iam.auth.sendOtp('[email protected]')
const result = await iam.auth.loginWithOtp({ identifier: '[email protected]', code: '123456' })
// LDAP 登录(需在 init 时提供 ldapClientFactory)
const result = await iam.auth.loginWithLdap({ username: 'jdoe', password: 'pass' })
// API Key 登录(需启用 login.apikey: true)
const result = await iam.auth.loginWithApiKey({ key: 'hai_xxxx...' })
// 验证令牌
const session = await iam.auth.verifyToken(accessToken)
// 注册并登录(一站式)
const result = await iam.auth.registerAndLogin({ username: 'new', password: 'Pass123' })特别关注
- Timing attack 防护:用户不存在时执行 dummy password hash,保持响应时间一致
- 账户锁定:连续登录失败
maxLoginAttempts次后锁定lockoutDuration秒 - 认证策略可独立启用/禁用:通过
login.password/login.otp/login.ldap/login.apikey控制 - OTP 安全:rejection sampling 消除模偏差,常量时间比较防 timing attack,频率限制(
resendInterval)
子模块:会话(session)— iam.session
关键对象
SessionOperations— 会话操作接口:create/get/verifyToken/update/delete/deleteByUserId/refresh/revokeRefresh/patchUserSessionsSession— 会话实体,含 userId、roles、permissions、accessToken、过期时间等TokenPair— 令牌对:{ accessToken, refreshToken, expiresIn, tokenType: 'Bearer' }
关键流程
Token 刷新(Rotation 策略):
refresh(refreshToken)
→ 从缓存读取 refreshToken → { userId, accessToken } 映射
→ 获取旧 session(通过 accessToken)
→ 删除旧 refreshToken(缓存 key 立即失效 → 防重放攻击)
→ 删除旧 accessToken session
→ 创建全新 session(复用旧 session 的用户上下文)
→ 返回新的 TokenPair单设备登录:
create(options) [singleDevice=true]
→ 清除该用户所有已有令牌(遍历 user→tokens 映射)
→ 生成新的 accessToken + refreshToken
→ 存储 session 到缓存使用方式
// 创建会话(通常由 login 内部调用)
const session = await iam.session.create({
userId: 'user-id',
username: 'admin',
roles: ['admin'],
permissions: ['user:read', 'user:write'],
})
// 验证令牌
const session = await iam.session.verifyToken(accessToken)
// 刷新令牌
const newTokens = await iam.session.refresh(refreshToken)
// 强制下线
await iam.session.deleteByUserId(userId)特别关注
- 纯缓存存储:不落库,重启后所有会话失效(符合安全最佳实践)
- 滑动续期:
sliding: true时每次get()自动延长 TTL - Refresh Rotation:旧 refreshToken 单次使用后立即失效,检测到重放即说明 token 泄漏
- Token 强度:256-bit crypto random(
crypto.getRandomValues(new Uint8Array(32))),base64url 编码
子模块:授权(authz)— iam.authz
关键对象
AuthzOperations— 授权操作接口,覆盖角色 CRUD、权限 CRUD、角色-权限/用户-角色分配、权限检查Role— 角色实体:{ id, code, name, description, isSystem }Permission— 权限实体:{ id, code, name, type, resource, action },type 可为menu/api/buttonPermissionType— 权限类型枚举
关键流程
权限检查(checkPermission):
checkPermission(userId, permission)
→ RBAC 未启用 → 直接返回 true
→ 查询用户角色列表(DB)
→ 检查是否包含超管角色(缓存 superAdminRoleId)→ 是则直接返回 true
→ 逐角色查询权限代码列表(缓存优先,miss 时查 DB 并写入缓存)
→ 逐权限匹配:精确匹配 或 通配符匹配(`admin:*` 匹配 `admin:read`)
→ 返回 true / false角色/权限变更后的会话同步:
assignPermissionToRole(roleId, permId)
→ DB 写入关联关系
→ 清除角色权限缓存
→ 查询该角色下所有用户 → 逐用户重新解析权限 → 更新活跃 session
(best-effort:同步失败仅 log.error,不影响权限分配结果)使用方式
// 角色管理
const role = await iam.authz.createRole({ code: 'editor', name: '编辑' })
const roles = await iam.authz.getAllRoles({ page: 1, pageSize: 20 })
// 权限管理
const perm = await iam.authz.createPermission({
code: 'article:publish',
name: '发布文章',
type: 'api',
resource: 'article',
action: 'publish',
})
// 分配与检查
await iam.authz.assignRole(userId, role.data.id)
await iam.authz.assignPermissionToRole(role.data.id, perm.data.id)
const allowed = await iam.authz.checkPermission(userId, 'article:publish')
// 批量同步角色
await iam.authz.syncRoles(userId, [roleId1, roleId2])特别关注
- 通配符匹配:
admin:*匹配admin:read、admin:write:detail等(单层*匹配冒号后全部) - 超管角色:配置的
rbac.superAdminRole(默认super_admin)自动拥有所有权限 - 事务保护:
deleteRole/deletePermission使用 DB 事务,先清关联再删实体 - 会话同步延迟:权限变更后活跃 session 中的权限列表会被同步更新,但为 best-effort(大量用户场景可能有延迟)
- 种子数据:
seedDefaultData: true时自动创建 admin/user/guest 三个默认角色及基础权限,幂等执行
子模块:用户管理(user)— iam.user
关键对象
UserOperations— 用户操作接口:注册、查询、更新、删除、密码修改与重置、密码强度验证User— 用户实体(公开字段):{ id, username, email, phone, displayName, avatarUrl, enabled, roles? }StoredUser— 内部存储用户(含passwordHash、loginFailedCount、lockedUntil等敏感字段)RegisterOptions/RegisterResult— 注册输入与输出UpdateCurrentUserInput— 当前用户可修改的白名单字段(displayName/avatarUrl/phone/metadata)
关键流程
用户注册流程:
register(options)
→ 检查注册是否启用
→ 密码强度验证(Zod schema + 自定义规则)
→ 密码哈希(@h-ai/crypto)
→ DB 事务:创建用户 + 查询创建结果
→ 分配默认角色(rbac.defaultRole,事务外执行,失败仅 log)
→ 返回 RegisterResult { user, agreements? }密码重置流程:
requestPasswordReset(email)
→ 按邮箱查找用户(不存在时静默返回 ok → 防枚举攻击)
→ 生成 256-bit 随机令牌
→ 缓存存储 SHA-256(token) → userId 映射(有 TTL + maxAttempts)
→ 调用 onPasswordResetRequest 回调(业务层负责发送邮件/短信)
confirmPasswordReset(token, newPassword)
→ 缓存查询 SHA-256(token) → 校验有效期和尝试次数
→ 密码强度验证 → 哈希 → 更新 DB
→ 清除该用户所有活跃会话(强制重新登录)
→ 删除已使用的重置令牌使用方式
// 注册
const result = await iam.user.register({
username: 'alice',
email: '[email protected]',
password: 'Password123',
})
// 当前用户操作
const me = await iam.user.getCurrentUser(accessToken)
await iam.user.updateCurrentUser(accessToken, { displayName: '新名称' })
await iam.user.changeCurrentUserPassword(accessToken, 'oldPass', 'newPass')
// 管理操作
const users = await iam.user.listUsers({ page: 1, pageSize: 20, search: 'alice', include: ['roles'] })
await iam.user.updateUser(userId, { enabled: false }) // 禁用用户 → 自动清除会话
await iam.user.deleteUser(userId) // 删除用户 → 事务清除角色关联
// 密码重置
await iam.user.requestPasswordReset('[email protected]')
await iam.user.confirmPasswordReset(token, 'NewPassword456')
await iam.user.adminResetPassword(userId, 'TempPassword123')
// 密码强度验证(同步方法)
const valid = iam.user.validatePassword('weak')特别关注
toUser()字段脱敏:所有对外返回的用户对象均剥离passwordHash、loginFailedCount等敏感字段- 会话联动:
deleteUser/updateUser({ enabled: false })/changePassword/confirmPasswordReset自动清除受影响用户的所有活跃会话 - 防枚举攻击:
requestPasswordReset对不存在的邮箱返回ok(undefined)而非错误 - validatePassword 是同步方法:这是
UserOperations中唯一的同步方法,其余均为异步 - 搜索模糊匹配:
listUsers支持按用户名、邮箱、手机号、显示名称模糊搜索,LIKE 通配符已转义
子模块:API Key — iam.apiKey
关键对象
ApiKeyOperations— API Key 操作接口:createApiKey/listApiKeys/getApiKey/revokeApiKey/verifyApiKeyApiKey— API Key 实体(公开字段):{ id, userId, name, keyPrefix, enabled, expiresAt, scopes }CreateApiKeyResult— 创建结果:{ apiKey, rawKey }(明文密钥仅返回一次)
关键流程
API Key 验证:
verifyApiKey(rawKey)
→ 从 rawKey 提取前缀(prefix + 前 8 字符)
→ 按前缀从 DB 检索候选 API Key 列表(缩小范围)
→ 逐候选项进行 hash 验证(同步操作)
→ 验证通过 → 检查是否过期/禁用
→ 异步更新 lastUsedAt(不阻塞返回)
→ 返回 ApiKey 实体使用方式
// 创建 API Key
const result = await iam.apiKey.createApiKey(userId, {
name: 'CI/CD',
expirationDays: 90,
scopes: ['api:read'],
})
// result.data.rawKey → 明文密钥(仅此一次展示)
// 列出用户的 API Key
const keys = await iam.apiKey.listApiKeys(userId)
// 吊销
await iam.apiKey.revokeApiKey(keyId)特别关注
- 明文不落库:数据库仅存储 hash,创建时返回的
rawKey是唯一获取机会 - 前缀检索:使用
keyPrefix缩小候选集,避免全表扫描 - 数量限制:单用户最多
maxKeysPerUser(默认 10)个 API Key - 需显式启用:
login.apikey: true才会初始化 API Key 子功能
更多用法
详细 API 参数、错误码及集成模式请参考 Skill 模板(packages/cli/templates/skills/hai-iam/SKILL.md)。
前端 API 契约
前端通过 @h-ai/iam/api 导出的契约定义(iamEndpoints)与 @h-ai/api-client 配合调用:
import { api } from '@h-ai/api-client'
import { iamEndpoints } from '@h-ai/iam/api'
await api.init({ baseUrl: '/api/iam' })
// 登录
const result = await api.call(iamEndpoints.login, {
identifier: 'admin',
password: 'Password123',
})
// 获取当前用户
const user = await api.call(iamEndpoints.currentUser, {})
// 修改密码
await api.call(iamEndpoints.changePassword, {
oldPassword: 'Password123',
newPassword: 'NewPassword456',
})密码重置
// 请求重置(即使用户不存在也返回 ok,防止枚举)
await iam.user.requestPasswordReset('[email protected]')
// 确认重置(校验令牌有效期和尝试次数 → 更新密码 → 清除所有会话)
const result = await iam.user.confirmPasswordReset(token, 'NewPassword456')
// 管理员重置(无需旧密码)
await iam.user.adminResetPassword(userId, 'TempPassword123')错误处理
所有操作返回 HaiResult<T>,通过 HaiIamError 做分支判断:
import { HaiIamError, iam } from '@h-ai/iam'
const result = await iam.auth.login({ identifier: 'admin', password: 'wrong' })
if (!result.success) {
if (result.error.code === HaiIamError.INVALID_CREDENTIALS.code) {
// 用户名或密码错误
}
}测试
pnpm test许可证
Apache-2.0
