You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Vapor前后端分离认证:会话持久化与中间件实现疑问

问题描述

我用Vapor开发带登录、注册页和需登录操作的Web前端,同时有一个独立部署的Vapor实例通过REST API处理认证(多端共用)。目前Web端的/login表单会POST请求到API的/auth/login接口,根据响应处理登录结果,但遇到认证持久化的问题:

  • 是否需要自定义中间件存储会话令牌,还是后续请求都通过API校验令牌?
  • 已在API端启用Bearer认证,登录成功后API返回会话令牌(用于Bearer授权)和vapor-session Cookie。我尝试把这个Cookie加到Web端登录后的重定向响应里,希望浏览器后续请求自动携带,但实际浏览器发送的vapor-session值不对,没成功。
  • 不确定这个Cookie方案是否合理,也不知道最佳实现方式。

附上部分代码:

API路由配置(不确定是否需要sessionAuthenticator)

auth//.grouped(UserDB.sessionAuthenticator())
    .grouped(UserAuthenticator())
    .grouped(UserDB.guardMiddleware())
    .group("")
        { authenticated in
            authenticated.get("me", use: getCurrentUser)

支持Bearer认证的UserAuthenticator(通过数据库查询会话令牌)

struct UserAuthenticator: BearerAuthenticator {
    
    typealias User = UserDB

    func authenticate(bearer: BearerAuthorization, for request: Request) -> EventLoopFuture<Void> {
        
        let tokenHash = SHA256.hash(bearer.token)
        
        return request.sessionTokens
            .find(tokenHash: tokenHash)
            .flatMap {
                if let sessionToken = $0 {
                    request.auth.login(sessionToken.user)
                }
                return request.eventLoop.makeSucceededVoidFuture()
            }
        
   }
    
}

目前Web端的认证路由没加中间件,是否需要添加并在中间件的authenticate方法里调用API校验?不清楚正确的实现路径。


解决方案

1. 直接放弃vapor-session跨实例共享方案

Vapor的vapor-session Cookie是实例绑定的:每个Vapor实例有独立的会话存储(默认内存,也可配置Redis等),不同实例的会话数据不互通。你把API端的vapor-session Cookie加到Web端响应里,Web端实例根本查不到对应会话数据,自然验证失败,这个方案从根上就不合理。

2. 正确的认证持久化方案:Web端存储Bearer令牌+自定义中间件自动校验

步骤一:Web端安全存储Bearer令牌

登录成功后,从API响应中提取Bearer令牌,将其存入浏览器的HttpOnly+Secure Cookie(比localStorage更安全,可防范XSS攻击)。示例代码:

// Web端处理/login POST请求的handler
func loginHandler(_ req: Request) async throws -> Response {
    let loginData = try req.content.decode(LoginRequest.self)
    // 调用认证API的登录接口
    let apiResponse = try await req.client.post("https://your-auth-api.com/auth/login") { apiReq in
        try apiReq.content.encode(loginData)
    }
    guard apiResponse.status == .ok else {
        // 登录失败,返回登录页
        return req.redirect(to: "/login")
    }
    let tokenResponse = try apiResponse.content.decode(TokenResponse.self)
    // 设置安全Cookie存储令牌
    let response = req.redirect(to: "/dashboard")
    response.cookies.set(
        Cookie(
            name: "auth_token",
            value: tokenResponse.bearerToken,
            expires: .distantFuture,
            httpOnly: true,
            secure: true,
            sameSite: .lax
        )
    )
    return response
}

步骤二:Web端自定义认证中间件

编写中间件自动完成以下操作:

  1. 跳过登录、注册等无需认证的路由
  2. 从Cookie中提取Bearer令牌
  3. 调用API的/me接口校验令牌有效性
  4. 校验通过后将用户信息存入请求上下文,供后续路由使用

示例代码:

struct WebAuthMiddleware: AsyncMiddleware {
    func respond(to request: Request, chainingTo next: AsyncResponder) async throws -> Response {
        // 定义无需认证的公开路由
        let publicPaths = ["/login", "/register", "/"]
        if publicPaths.contains(request.url.path) {
            return try await next.respond(to: request)
        }
        
        // 从Cookie中获取令牌
        guard let token = request.cookies["auth_token"]?.value else {
            // 无令牌,重定向到登录页
            return request.redirect(to: "/login")
        }
        
        // 调用API校验令牌有效性
        let apiResponse = try await request.client.get("https://your-auth-api.com/auth/me") { apiReq in
            apiReq.headers.bearerAuthorization = .init(token: token)
        }
        
        guard apiResponse.status == .ok else {
            // 令牌无效,清除Cookie并重定向登录
            let response = request.redirect(to: "/login")
            response.cookies.remove(name: "auth_token")
            return response
        }
        
        // 将用户信息存入请求上下文,供后续路由调用
        let user = try apiResponse.content.decode(UserDB.self)
        request.storage.set(UserDB.self, to: user)
        
        return try await next.respond(to: request)
    }
}

步骤三:Web端路由绑定中间件

将自定义中间件挂载到需要认证的路由组上:

let authenticatedRoutes = app.grouped(WebAuthMiddleware())
authenticatedRoutes.get("/dashboard") { req in
    // 从请求上下文获取用户信息
    let user = req.storage.get(UserDB.self)!
    return try await req.view.render("dashboard", ["user": user])
}

3. API端路由优化

API端的/me接口已经通过UserAuthenticator和guardMiddleware实现Bearer认证,不需要启用sessionAuthenticator(因为你用的是Bearer令牌而非Vapor原生会话),保持现有路由配置即可。


内容的提问来源于stack exchange,提问作者GP89

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.17 00:55:23