JWT 认证
JWT(JSON Web Token)是 ACE Framework 推荐的无状态认证方案。内置的 jwtAuth 中间件基于 HS256 签名算法,只需几行代码即可为整个应用或特定路由组开启认证保护。
认证原理
快速开始
1. 配置密钥
在 config/application.toml 中声明 JWT 密钥与过期时间:
toml
[jwt]
secret = "your-super-secret-key-change-in-production"
expireSeconds = 3600安全提示
jwt.secret 不可提交到版本控制。生产环境请通过环境变量或密钥管理服务注入,并保证长度不低于 32 字节。
2. 挂载认证中间件
cangjie
import ace_framework.*
import ace_security.*
@Controller["/api"]
class ApiController {
// 整个 /api 前缀受 JWT 保护
}
// main.cj
let app = App()
let secret = config.get("jwt.secret")
// 全局挂载,对 /api/* 生效
app.use(jwtAuth(secret, except: ["/api/login", "/api/register"]))
app.use(router.routes())
listen(app, "0.0.0.0", 8080)颁发 Token
登录成功后调用 signJwt 生成 token:
cangjie
import ace_security.jwt.*
@Controller["/api"]
class AuthController {
@Post["/login"]
func login(@Body body: LoginDto): Response {
// 验证用户名密码(省略)
let user = userService.findByCredentials(body.username, body.password)
?? return Response.json(401, {error: "用户名或密码错误"})
let payload = JwtPayload(
sub: user.id,
roles: user.roles, // ["admin", "user"]
extra: {}
)
let secret = config.get("jwt.secret")
let expire = config.getInt("jwt.expireSeconds") ?? 3600
let token = signJwt(payload, secret, expire)
return Response.json({token: token, expiresIn: expire})
}
}读取当前用户
认证通过后,jwtAuth 将解析后的用户信息写入 ctx.state["principal"],使用 currentPrincipal 辅助函数读取:
cangjie
import ace_security.jwt.*
@Controller["/api"]
class ProfileController {
@Get["/me"]
func getProfile(ctx: Context): Response {
let principal = currentPrincipal(ctx) ?? return Response.json(401, {error: "未认证"})
// principal.sub — 用户 ID
// principal.roles — 角色列表 Array<String>
return Response.json({id: principal.sub, roles: principal.roles})
}
}Principal 字段
| 字段 | 类型 | 说明 |
|---|---|---|
sub | String | 主体标识(通常为用户 ID) |
roles | Array<String> | 角色列表 |
exp | Int64 | 过期时间(Unix 时间戳) |
extra | Map<String, String> | 自定义扩展字段 |
路径放行
jwtAuth 的 except 参数接受路径前缀列表,命中前缀的请求直接放行,不做 token 校验:
cangjie
app.use(jwtAuth(secret, except: [
"/api/login",
"/api/register",
"/health",
"/docs"
]))精细化放行
如需按 HTTP 方法放行(如仅 GET /api/products 公开),可在 jwtAuth 后面增加一个自定义中间件在 ctx.state 中补写 Principal,或使用路由级中间件代替全局挂载。
角色授权
在认证基础上,authorize 中间件提供基于角色的访问控制(RBAC):
cangjie
import ace_security.jwt.*
// 只允许 admin 角色访问 /admin/*
let adminPolicy = [RolePolicy("*", "/admin", ["admin"])]
router.use("/admin", authorize(adminPolicy))
// 精细到方法级别
let policies = [
RolePolicy("DELETE", "/api/users", ["admin"]),
RolePolicy("POST", "/api/posts", ["admin", "editor"])
]
app.use(authorize(policies))RolePolicy 参数
| 参数 | 类型 | 说明 |
|---|---|---|
method | String | HTTP 方法,"*" 匹配所有 |
path | String | 路径前缀(精确匹配或前缀) |
roles | Array<String> | 允许访问的角色(OR 关系) |
超出权限时返回 403 Forbidden,未认证时返回 401 Unauthorized。
完整示例
cangjie
// main.cj
import ace_web.*
import ace_router.*
import ace_http.*
import ace_security.jwt.*
import ace_framework.*
let config = AppConfig.load()
let secret = config.get("jwt.secret") ?? panic("jwt.secret not configured")
let app = App()
let router = Router()
// 登录接口 — 无需认证
router.post("/api/login") { ctx =>
let body = ctx.formValue("username")
// ... 验证逻辑 ...
let token = signJwt(JwtPayload(sub: "uid-001", roles: ["user"]), secret, 3600)
ctx.json({token: token})
}
// 受保护资源
router.get("/api/profile") { ctx =>
let p = currentPrincipal(ctx)!
ctx.json({sub: p.sub, roles: p.roles})
}
app.use(jwtAuth(secret, except: ["/api/login"]))
app.use(router.routes())
listen(app, "0.0.0.0", 8080)Token 刷新
ACE 内置 JWT 为无状态设计,不支持主动吊销。需要吊销能力时,请结合黑名单(Redis)或改用短期 token + 刷新 token 方案。