Skip to content

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 字段

字段类型说明
subString主体标识(通常为用户 ID)
rolesArray<String>角色列表
expInt64过期时间(Unix 时间戳)
extraMap<String, String>自定义扩展字段

路径放行

jwtAuthexcept 参数接受路径前缀列表,命中前缀的请求直接放行,不做 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 参数

参数类型说明
methodStringHTTP 方法,"*" 匹配所有
pathString路径前缀(精确匹配或前缀)
rolesArray<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 方案。

基于 Apache-2.0 许可证发布