Skip to content

Session

Session 是 Web 应用最常见的状态管理手段。ACE 提供开箱即用的 sessionMiddleware,基于 Cookie 存储 Session ID,实际数据保存在可插拔的 SessionStore 中。

注册中间件

在应用入口或 @Middleware 装饰的类中注册:

cangjie
import ace_web.*
import ace_web.session.*

let app = App()
app.use(sessionMiddleware(
    MemorySessionStore(),   // 存储后端
    "ace.sid",              // Cookie 名称
    1800_000,               // TTL:毫秒,此处 30 分钟
    defaultSidGenerator     // Session ID 生成器(可选,默认 UUID v4)
))

sessionMiddleware 参数说明:

参数类型说明
storeSessionStore存储后端,内置 MemorySessionStore
cookieNameString写入客户端的 Cookie 名,默认建议 ace.sid
ttlMsInt64会话有效期(毫秒),超时自动失效
sidGenerator() -> StringSession ID 生成函数,默认使用安全随机 UUID

辅助函数

读写 Session 无需直接操作 store,使用框架提供的辅助函数即可:

cangjie
import ace_web.session.*

// 存入键值
await sessionSet(ctx, "userId", "42")
await sessionSet(ctx, "role", "admin")

// 读取键值(返回 Option<String>)
let uid = await sessionGet(ctx, "userId") ?? "anonymous"

// 读取当前请求的 Session ID
let sid = sessionId(ctx)

// 销毁整个 Session(退出登录)
await sessionDestroy(ctx)
函数返回类型说明
sessionSet(ctx, key, value)Unit写入 Session 键值
sessionGet(ctx, key)Option<String>读取 Session 值,不存在返回 None
sessionId(ctx)String读取当前 Session ID
sessionDestroy(ctx)Unit清除整个 Session 并失效 Cookie

内置存储:MemorySessionStore

MemorySessionStore 将所有 Session 数据保存在进程内存中,适合开发调试。

cangjie
let store = MemorySessionStore()

生产环境不可用

MemorySessionStore 不持久化、不跨进程共享,重启即丢失。多实例部署或生产环境请替换为 Redis/数据库后端。

自定义 SessionStore

实现 SessionStore 接口即可接入任意存储:

cangjie
interface SessionStore {
    func get(sid: String): Future<Option<HashMap<String, String>>>
    func set(sid: String, data: HashMap<String, String>, ttlMs: Int64): Future<Unit>
    func remove(sid: String): Future<Unit>
    func touch(sid: String, ttlMs: Int64): Future<Unit>  // 刷新 TTL
}

以 Redis 为例(伪代码):

cangjie
class RedisSessionStore <: SessionStore {
    let client: RedisClient

    public func get(sid: String): Future<Option<HashMap<String, String>>> {
        let raw = await client.get("sess:\(sid)")
        return raw.map { parseJson($0) }
    }

    public func set(sid: String, data: HashMap<String, String>, ttlMs: Int64): Future<Unit> {
        await client.setex("sess:\(sid)", ttlMs / 1000, stringifyJson(data))
    }

    public func remove(sid: String): Future<Unit> {
        await client.del("sess:\(sid)")
    }

    public func touch(sid: String, ttlMs: Int64): Future<Unit> {
        await client.expire("sess:\(sid)", ttlMs / 1000)
    }
}

注册时替换存储后端:

cangjie
app.use(sessionMiddleware(RedisSessionStore(client), "ace.sid", 1800_000))

登录/鉴权示例

登录接口——写入 Session:

cangjie
@Controller["/auth"]
class AuthController {
    @Inject var userService: UserService

    @Post["/login"]
    func login(ctx: Context): Future<Unit> {
        let body   = ctx.formValue("username") ?? ""
        let passwd = ctx.formValue("password") ?? ""
        let user   = await userService.verify(body, passwd)
        match user {
            case Some(u) =>
                await sessionSet(ctx, "userId", u.id)
                await sessionSet(ctx, "role", u.role)
                ctx.json("""{"ok":true}""")
            case None =>
                ctx.status = 401
                ctx.json("""{"ok":false,"msg":"credentials invalid"}""")
        }
    }

    @Get["/profile"]
    func profile(ctx: Context): Future<Unit> {
        let uid = await sessionGet(ctx, "userId") ?? throw Unauthorized()
        let user = await userService.findById(uid)
        ctx.json(stringifyJson(user))
    }

    @Post["/logout"]
    func logout(ctx: Context): Future<Unit> {
        await sessionDestroy(ctx)
        ctx.json("""{"ok":true}""")
    }
}

多实例部署

水平扩展时,所有实例必须共享同一个 Session 存储,推荐:

  • Redis:最常见,支持 TTL、集群;实现 RedisSessionStore
  • 数据库:适合对审计有要求的场景;TTL 靠定时清理任务维护。

Cookie 安全

生产环境建议在 sessionMiddleware 配置中开启 secure: true(仅 HTTPS 传输)和 httpOnly: true(防 XSS 读取 Cookie)。

基于 Apache-2.0 许可证发布