前面的主题都专注於扩充加强 Ktor 及实作底层基础设施功能,最後我们来看在 Multi-Project 架构下,要如何建立一个子专案。那麽要建立什麽子专案呢? 我认为除了功能开发之外,後续的监控及维运也是非常重要的,所以我参考 Spring Boot Actuator 的概念,建立一个专门监控、管理系统的子专案 Ops
(DevOps 的 Operations 缩写)。
由於我不可能独自实作像 Spring Boot Actuator 这麽完整的功能,这也不是我的目标,所以我目前只有实作最基本的 health check endpoint。我把重点放在协助小规模的新创开发团队,在没有完善的公司制度、人力资源及基础设施服务的情况下,後端工程师在开发 API 的时候能兼顾资安与团队合作效率。
通常一个网站除了开发前台功能给外部使用者,公司也需要有内部的後台管理系统,用来建立初始资料、新增网站内容、或是启用/停用某个功能。然而小规模的新创开发团队可能没有时间为「内部使用」的管理系统拆分成独立的微服务,更没有时间去做 Web 管理後台,所以在开发维运初期,後端工程师通常使用最简单的 API Key 验证方式保护内部 API,再使用 Postman 工具进行操作,此时就会遇到内部 API 权限控管与工作分配的两难。
如果後端工程师把 API Key 交给他人,委托处理这些日常琐事,就会有 API Key 外流的风险。反之,如果 API Key 由後端自己保管,那麽工作时就可能常常被杂事打断,影响工作效率。当然以上问题可以透过严谨的制度及网路管理解决部分问题,但如果系统在开发初期就能快速为内部 API 加上 RBAC 机制,就能对公司每位员工做更细致的权限控管,放心分配工作,也能提早为未来开发 Web 後台管理系统奠下基础。
对於小公司来说,因为人数少、时间少、资源少,所以容易基於人与人之间的信任而简化制度流程,防火墙也只能挡掉公司外的人,所以内部 API 权限控管是比较容易忽略的。而且就算有严谨的制度流程,从资安的角度来看,OWASP 在 2019 年列出十大 API 安全风险 包含这一项 Broken Function Level Authorization
,所以系统本来就应该有 RBAC 机制保护所有 API,建立最後一道防线。
架构设计上,前後台的使用者帐号、角色权限、登入验证机制…等应该要分开,所以本专案的 Multi-Project 模组化开发方式非常适合营运初期的小规模开发团队,把後台拆分为独立的子专案就好,不必拆分为微服务,增加开发及维运的成本。
Club 前台子专案的使用者从 App 端登入时,系统要透过 http header clientVersion
检查是否有新版本,甚至要求必须先升级才能继续使用,所以我在後台子专案开发 App 版本管理 API ,并且交由 App Team 自行去记录 App 的版本资讯,达到权限控管及分工合作的目标。
※ 实际上 OpsTeam 也可以使用这些 API,所以我把 OpsTeam 的角色也加到 AppTeam 的 PrincipalAuth 物件里面
fun Routing.opsAppRelease() {
val appReleaseService by inject<AppReleaseService>()
route("${OpsConst.urlRootPath}/app/releases") {
authorize(OpsAuth.AppTeam) {
// 新增 API
post<CreateAppReleaseForm, Unit>(OpsOpenApi.CreateAppRelease) { dto ->
appReleaseService.create(dto)
call.respond(CodeResponseDTO.OK)
}
// 修改 API
put<UpdateAppReleaseForm, Unit>(OpsOpenApi.UpdateAppRelease) { dto ->
appReleaseService.update(dto)
call.respond(CodeResponseDTO.OK)
}
// 查询 API
dynamicQuery<AppReleaseDTO>(OpsOpenApi.FindAppReleases) { dynamicQuery ->
call.respond(dynamicQuery.queryDB<AppReleaseDTO>())
}
// 验证版本 API
getWithLocation<CheckAppReleaseLocation, CheckAppReleaseResponse>(OpsOpenApi.CheckAppRelease) { location ->
val result = appReleaseService.check(AppVersion(location.appId, location.verName))
call.respond(DataResponseDTO(CheckAppReleaseResponse(result)))
}
}
}
}
// 实际上 OpsTeam 也可以使用这些 API,所以我把 OpsTeam 的角色也加到 AppTeam 的 PrincipalAuth 物件里面
val AppTeam = PrincipalAuth.User(
userAuthProviderName, allAuthSchemes, setOf(UserSource),
mapOf(OpsUserType.User.value to setOf(OpsUserRole.OpsTeam.value, OpsUserRole.AppTeam.value))
)
@OptIn(KtorExperimentalLocationsAPI::class)
@io.ktor.locations.Location("/check")
data class CheckAppReleaseLocation(val appId: String, val verName: String) : Location()
data class CheckAppReleaseResponse(val result: ClientVersionCheckResult)
之前写的文章大多是描述子专案如何在 Multi-Project 架构下,实作某个特定功能。这麽多篇文章看下来,读者可能会有见树不见林的感觉,所以我在此以後台 Ops 子专案为例,列出从头到尾建立一个子专案需要实作那些项目。
先在 settings.gradle.kts 增加 projects:ops
rootProject.name = "fanpoll"
include("app", "infra", "projects:ops", "projects:club")
然後 ops 子专案的 build.gradle.kts 引入 app.subproject-conventions plugin 即可
plugins {
id("app.subproject-conventions")
}
更多细节可参考 [Day 4] 使用 Gradle Multi-Project Builds X Shadow Plugin X Docker Compose 建置、打包、部署
下图是 ops 子专案的档案目录结构,我把子专案实作 Multi-Project 架构下的各个功能的程序码都放在独立的 OpsXXX.kt 档案,一目了然。另一方面,目前 ops 子专案的功能相对较少且独立,再加上 Kotlin 可以把多个类别放在同一个档案,所以我依功能划分档案,集中放置在features package。
话说我们能依自己的想法规划档案目录结构,这都是因为 Ktor 不基於 annotation 及 DI,程序执行流程都是自己控制的函式呼叫。不像 Spring 依赖 @ComponentScan
, @EntityScan
annotation 或是 Play Framework 预设寻找 controllers, models …等资料夹,需要遵循 Web 框架规范的方式编排档案。
在 Ktor 设定档 application.conf 加入 ops module,当 Server 启动时就会依序执行各个 module 对应的 main function
更多细节可参考 [Day 3] 以 Ktor Module 实作模组化开发
ktor {
application {
modules = [
fanpoll.infra.ApplicationKt.main,
fanpoll.ops.OpsProjectKt.opsMain,
fanpoll.club.ClubProjectKt.clubMain
]
}
}
fun Application.opsMain() {
// 初始化 ops 子专案…
}
初始化 ops 专案需要先建立 Project 物件,必须传入以下物件
List<PrincipalSourceAuthConfig>
List<UserType>
ProjectOpenApi
List<NotificationType>?
底层 infra module 再根据 Project 物件执行相对应的功能
更多细节可参考 [Day 3] 以 Ktor Module 实作模组化开发
fun Application.opsMain() {
logger.info { "load ${OpsConst.projectId} project..." }
val projectManager = get<ProjectManager>()
val projectConfig = ProjectManager.loadConfig<OpsConfig>(OpsConst.projectId)
projectManager.register(
Project(
OpsConst.projectId,
projectConfig.auth.principalSourceAuthConfigs,
OpsUserType.values().map { it.value },
OpsOpenApi.Instance,
OpsNotification.AllTypes
)
)
// 以下省略
}
class Project(
override val id: String,
val principalSourceAuthConfigs: List<PrincipalSourceAuthConfig>,
val userTypes: List<UserType>,
val projectOpenApi: ProjectOpenApi,
val notificationTypes: List<NotificationType>? = null
) : IdentifiableObject<String>()
初始化 ops 子专案包括以下项目
fun Application.opsMain() {
// 以上省略
authentication {
service(OpsAuth.serviceAuthProviderName, projectConfig.auth.getServiceAuthConfigs())
session(
OpsAuth.userAuthProviderName,
UserSessionAuthValidator(projectConfig.auth.getUserAuthConfigs(), get()).configureFunction
)
}
val availableLangs = get<AvailableLangs>()
val responseMessagesProvider = get<ResponseMessagesProvider>()
responseMessagesProvider.merge(
HoconMessagesProvider(availableLangs, "i18n/response/${OpsConst.projectId}", "response_")
)
val i18nNotificationProjectMessages = get<I18nNotificationProjectMessages>()
i18nNotificationProjectMessages.addProvider(
OpsConst.projectId,
I18nNotificationMessagesProvider(
PropertiesMessagesProvider(
availableLangs,
"i18n/notification/${OpsConst.projectId}",
"notification_"
)
)
)
koin {
modules(
module(createdAtStart = true) {
single { projectConfig }
single { OpsUserService() }
single { OpsLoginService(get()) }
}
)
}
// 以下省略
}
每个 Web 框架都有自己定义 route 的方式,例如 Spring MVC 使用 @RequestMapping
annotation,Play Framework 使用自定义的 route 语法撰写 route 在 conf/routes 设定档。至於 Ktor 则是让开发者使用 DSL 语法撰写 route function,所以我们可以使用巢状阶层的结构来编排 route,而且可以依功能放置在独立的 XXXRoute.kt 档案。
fun Application.opsMain() {
// 以上省略
routing {
ops()
}
}
// 集中在 OpsRoutes.kt 档案里定义每个功能的 route
fun Routing.ops() {
opsUser()
opsLogin()
opsMonitor()
opsDataReport()
opsAppRelease()
}
如果要架设一个HTTPs File Server, 个人使用, 所以不会有certificate, ...
前言 JavaScript 运算子大概可以分为几大类:算数运算子、逻辑运算子、赋值运算子、比较运算子...
如何卸载 Mac 软件? 当许多用户购买新的 Macbook/iMac 时,他们会在 Mac 上下载...
在开始学习机器学习之前,我们得先准备好环境,我们使用python来当我们的程序语言,稍微介绍一下py...
Composite:组合模式,当需要组合两个类的时候,比如画图:需要画直綫和点,那麽如果分别调用画直...