Skip to content

认证、授权与安全

以 Better Auth、Arcjet 和基于角色的授权为例

边界划分

  • 认证:验证请求者身份,建立可信的会话上下文。
  • 授权:依据角色、资源归属或业务策略,决定该身份能否执行操作。
  • 运行时安全:在请求进入业务逻辑前处理限流、机器人流量和常见攻击。

Controller 只声明端点和输入;认证、授权与安全策略不应散落在每个 Controller 方法中。

Better Auth

Better Auth 是 TypeScript 认证与授权框架,可管理用户、账号、会话和验证记录。它可以通过 Prisma 或 Drizzle adapter 使用现有 PostgreSQL 数据库。

最少配置包括:高熵的 BETTER_AUTH_SECRET、明确的 BETTER_AUTH_URL、数据库 adapter 和所需登录方式。认证服务拥有其表结构和迁移;业务数据中的用户 ID 只作为关联键,不复制密码、会话或第三方账号凭据。

bash
pnpm dlx @better-auth/cli@latest secret

将命令生成的值写入 .env,不要自己编造短密钥:

md
BETTER_AUTH_SECRET=替换为命令生成的高熵密钥
BETTER_AUTH_URL=http://localhost:3000
ts
import { betterAuth } from 'better-auth'
import { prismaAdapter } from 'better-auth/adapters/prisma'

import { prisma } from './prisma'

export const auth = betterAuth({
  baseURL: process.env.BETTER_AUTH_URL,
  database: prismaAdapter(prisma, {
    provider: 'postgresql',
  }),
  emailAndPassword: {
    enabled: true,
  },
})

NestJS 需要将认证端点挂载到应用,并把经过验证的会话转换为请求上下文。使用 Better Auth 的 NestJS 集成时,应按集成文档关闭默认 body parser,再由 Auth Module 接管对应认证路由;否则请求体可能被重复消费。

ts
const app = await NestFactory.create(AppModule, {
  bodyParser: false,
})

官方 NestJS 集成使用全局 AuthGuard,再通过 @AllowAnonymous() 显式标记公开端点;也可以使用自定义 Guard,但必须统一处理会话解析、失败响应和当前用户注入。

RBAC

角色信息是授权输入,不是认证结果。将角色定义集中在领域模型中,并通过元数据声明端点所需角色:

ts
import { SetMetadata } from '@nestjs/common'

export const ROLES_KEY = 'roles'
export const Roles = (...roles: Array<'admin' | 'participant'>) => SetMetadata(ROLES_KEY, roles)
ts
@Post()
@Roles('admin')
create(@Body() input: CreateHackathonDto) {
  return this.hackathonsService.create(input)
}

RolesGuard 读取元数据和当前用户角色后做出决定。仅检查角色不足以覆盖所有场景:更新或删除资源时,还应在 Service 中验证资源归属、状态和业务约束。

课程的角色模型只允许 participantadmin,注册时固定为 participant,并且不能由注册请求传入。管理员需由受控的管理流程或数据库操作提升,不能暴露“修改自己角色”的公开接口。

Arcjet

Arcjet 可作为请求进入应用前的运行时安全层,提供限流、机器人检测和 Shield WAF 等规则。NestJS 集成可注册全局模块或 Guard;安全决策应在 Controller 和数据库访问之前执行。

配置原则:

  • ARCJET_KEY 等环境变量提供密钥,绝不写入源码。
  • 限流维度按端点和身份选择:匿名端点通常按 IP,已登录端点优先按用户 ID。
  • 首次上线使用 DRY_RUN 观察误拦截、允许规则和流量阈值,再切换为阻断模式。
  • 命中限流返回 429,命中机器人或 WAF 规则通常返回 403;记录决策原因,不记录敏感请求内容。

课程从全局 Shield 与固定窗口限流开始。ARCJET_MODE=dry-run 先只记录决策,切换为 live 后才执行阻断;示例阈值为每个 IP 60 秒 10 次请求。不同 Arcjet 规则的拒绝状态码可能不同,应以实际规则结果和 SDK 文档为准。

Arcjet 是纵深防御的一层,不能替代输入校验、认证、授权或数据库约束。

公开端点与最小权限

登录、注册、健康检查和公开读取端点必须显式列出;其余端点默认需要认证。每个写操作都应检查:请求者身份、角色、资源归属和资源当前状态。

参考

基于 MIT 许可发布