如何在Vapor中复用认证路由逻辑以同时支持API与Web前端?
问题描述
我正在用Vapor搭建认证系统,已经实现了AuthenticationController作为RouteCollection,代码如下:
struct AuthenticationController: RouteCollection { func boot(routes: RoutesBuilder) throws { routes.group("auth") { auth in auth.post("register", use: register) auth.post("login", use: login) } routes.group("email-verification") { emailVerificationRoutes in emailVerificationRoutes.post("", use: sendEmailVerification) emailVerificationRoutes.get("", use: verifyEmail) } ... } private func register(_ req: Request) throws -> EventLoopFuture<HTTPStatus> { ... } private func login(_ req: Request) throws -> EventLoopFuture<LoginResponse> { ... } ... }
该路由集合挂载在/api路径下:
app.group("api") { api in try! api.register(collection: AuthenticationController()) }
这些API接口返回EventLoopFuture类型,无数据时返回HTTPStatus,需要返回数据时返回实现Content协议的JSON结构体。
现在希望重构认证逻辑,同时为Web前端提供HTML界面(比如/auth/login路径的表单页面、错误响应等),要求Web界面和/api/auth/login等API接口共用核心逻辑。考虑过用flatMap转换EventLoopFuture返回View,或者抽离逻辑到共享层、创建WebAuthenticationController,但没找到示例,询问合适的实现方式。
解决方案
核心思路是将业务逻辑与路由/响应处理解耦:把认证的核心逻辑抽离到独立的服务类,让API控制器和Web控制器各自处理响应格式,同时复用同一套业务逻辑。
步骤1:抽离核心认证逻辑到服务类
创建AuthenticationService,封装所有核心认证操作,返回业务层面的结果(而非直接返回HTTP响应或View),这样可以被不同的控制器复用。
// 自定义业务错误,用于在服务层传递错误信息 enum AuthenticationError: Error { case invalidCredentials case emailAlreadyRegistered case verificationTokenInvalidOrExpired // 其他业务错误... } struct AuthenticationService { func register(req: Request, userData: RegisterData) -> EventLoopFuture<User> { // 核心注册逻辑:检查邮箱是否已存在、创建用户、保存到数据库等 return User.query(on: req.db) .filter(\.$email == userData.email) .first() .flatMap { existingUser in guard existingUser == nil else { return req.eventLoop.makeFailedFuture(AuthenticationError.emailAlreadyRegistered) } let user = User(email: userData.email, passwordHash: try! Bcrypt.hash(userData.password)) return user.save(on: req.db).map { user } } } func login(req: Request, loginData: LoginData) -> EventLoopFuture<User> { // 核心登录逻辑:验证用户名密码、返回用户模型 return User.query(on: req.db) .filter(\.$email == loginData.email) .first() .flatMap { user in guard let user = user, try! Bcrypt.verify(loginData.password, created: user.passwordHash) else { return req.eventLoop.makeFailedFuture(AuthenticationError.invalidCredentials) } return req.eventLoop.makeSucceededFuture(user) } } // 邮箱验证等其他核心方法... } // 为Request扩展,方便获取服务实例(可选,也可以直接初始化) extension Request { var authService: AuthenticationService { .init() } }
步骤2:改造原有API控制器
让AuthenticationController调用服务类的方法,然后将业务结果转换成API需要的响应格式:
struct AuthenticationController: RouteCollection { func boot(routes: RoutesBuilder) throws { routes.group("auth") { auth in auth.post("register", use: register) auth.post("login", use: login) } routes.group("email-verification") { emailVerificationRoutes in emailVerificationRoutes.post("", use: sendEmailVerification) emailVerificationRoutes.get("", use: verifyEmail) } } private func register(_ req: Request) throws -> EventLoopFuture<HTTPStatus> { let registerData = try req.content.decode(RegisterData.self) return req.authService.register(req: req, userData: registerData) .map { _ in .created } // 注册成功返回201 .flatMapError { error in // 根据业务错误返回对应的HTTP状态码和错误信息 switch error { case AuthenticationError.emailAlreadyRegistered: return req.eventLoop.makeFailedFuture(Abort(.conflict, reason: "邮箱已被注册")) default: return req.eventLoop.makeFailedFuture(Abort(.internalServerError)) } } } private func login(_ req: Request) throws -> EventLoopFuture<LoginResponse> { let loginData = try req.content.decode(LoginData.self) return req.authService.login(req: req, loginData: loginData) .map { user in // 生成JWT或其他登录凭证,封装成LoginResponse LoginResponse(token: try! user.generateToken(), userID: user.id!) } .flatMapError { error in switch error { case AuthenticationError.invalidCredentials: return req.eventLoop.makeFailedFuture(Abort(.unauthorized, reason: "用户名或密码错误")) default: return req.eventLoop.makeFailedFuture(Abort(.internalServerError)) } } } // 其他API方法... }
步骤3:创建Web界面控制器
新建WebAuthenticationController,处理HTML页面的请求,调用同一个服务类,根据结果渲染对应的View:
struct WebAuthenticationController: RouteCollection { func boot(routes: RoutesBuilder) throws { routes.group("auth") { auth in // 显示登录表单 auth.get("login", use: showLoginForm) // 处理登录表单提交 auth.post("login", use: handleLoginSubmit) // 显示注册表单 auth.get("register", use: showRegisterForm) // 处理注册表单提交 auth.post("register", use: handleRegisterSubmit) // 邮箱验证相关路由... } } private func showLoginForm(_ req: Request) throws -> EventLoopFuture<View> { // 渲染登录表单页面,可传递错误信息(如果有的话) let context = LoginContext(errorMessage: req.query[String.self, at: "error"]) return req.view.render("auth/login", context) } private func handleLoginSubmit(_ req: Request) throws -> EventLoopFuture<Response> { let loginData = try req.content.decode(LoginData.self) return req.authService.login(req: req, loginData: loginData) .flatMap { user in // 登录成功,设置会话(如果用会话认证) req.session.authenticate(user) // 重定向到首页或用户中心 return req.eventLoop.makeSucceededFuture(req.redirect(to: "/dashboard")) } .flatMapError { error in // 登录失败,重定向回登录页面并携带错误信息 let errorMessage: String switch error { case AuthenticationError.invalidCredentials: errorMessage = "用户名或密码错误" default: errorMessage = "登录失败,请稍后重试" } return req.eventLoop.makeSucceededFuture(req.redirect(to: "/auth/login?error=\(errorMessage.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!)")) } } private func showRegisterForm(_ req: Request) throws -> EventLoopFuture<View> { let context = RegisterContext(errorMessage: req.query[String.self, at: "error"]) return req.view.render("auth/register", context) } private func handleRegisterSubmit(_ req: Request) throws -> EventLoopFuture<Response> { let registerData = try req.content.decode(RegisterData.self) return req.authService.register(req: req, userData: registerData) .flatMap { user in // 注册成功,自动登录或重定向到登录页 req.session.authenticate(user) return req.eventLoop.makeSucceededFuture(req.redirect(to: "/dashboard")) } .flatMapError { error in let errorMessage: String switch error { case AuthenticationError.emailAlreadyRegistered: errorMessage = "邮箱已被注册" default: errorMessage = "注册失败,请稍后重试" } return req.eventLoop.makeSucceededFuture(req.redirect(to: "/auth/register?error=\(errorMessage.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!)")) } } // 其他Web相关方法... } // 页面渲染所需的上下文结构体 struct LoginContext: Encodable { let errorMessage: String? } struct RegisterContext: Encodable { let errorMessage: String? }
步骤4:挂载路由
分别挂载API控制器和Web控制器:
// 挂载API路由 app.group("api") { api in try api.register(collection: AuthenticationController()) } // 挂载Web路由 try app.register(collection: WebAuthenticationController())
关键说明
- 解耦逻辑:服务层专注于业务逻辑,控制器专注于响应格式处理(API返回JSON,Web返回HTML/重定向),符合单一职责原则。
- 复用核心逻辑:API和Web控制器调用同一个服务类方法,避免代码重复。
- 错误处理:服务层抛出业务错误,控制器根据自身场景转换为对应的响应(API返回带错误信息的JSON,Web重定向并携带错误参数)。
- EventLoopFuture转换:在Web控制器中,通过
flatMap和flatMapError处理服务返回的EventLoopFuture,将结果转换为View或Response(重定向)。
内容的提问来源于stack exchange,提问作者GP89
相关产品推荐
相关产品推荐

