feat: migrate server to kotlinx-rpc and add api.md

This commit is contained in:
SOR
2026-10-11 11:18:58 +08:00
parent 03ee40f6bd
commit 1de76bc445
18 changed files with 634 additions and 6 deletions
+6 -1
View File
@@ -23,6 +23,7 @@ Ktor 3.x 后端项目,采用纯正、原生的 Ktor 方式(Idiomatic Ktor)
- Auth0 Java-JWT (`4.4.0`) + Ktor Auth JWT (`ktor-server-auth-jwt`) - Auth0 Java-JWT (`4.4.0`) + Ktor Auth JWT (`ktor-server-auth-jwt`)
- BCrypt 密码加密 (`at.favre.lib:bcrypt:0.10.2`,与 Spring Security BCrypt 100% 兼容) - BCrypt 密码加密 (`at.favre.lib:bcrypt:0.10.2`,与 Spring Security BCrypt 100% 兼容)
- **序列化与时间**:`kotlinx-serialization-json` + `kotlinx-datetime` - **序列化与时间**:`kotlinx-serialization-json` + `kotlinx-datetime`
- **RPC 框架**:JetBrains `kotlinx-rpc` (`0.10.4`,基于 WebSocket 的 kRPC 协议与 Flow 双向流通信)
- **实时通信**:Ktor WebSockets (`ktor-server-websockets`) - **实时通信**:Ktor WebSockets (`ktor-server-websockets`)
- **跨域与异常拦截**:Ktor CORS (`ktor-server-cors`) + StatusPages (`ktor-server-status-pages`) - **跨域与异常拦截**:Ktor CORS (`ktor-server-cors`) + StatusPages (`ktor-server-status-pages`)
@@ -44,7 +45,7 @@ Ktor 3.x 后端项目,采用纯正、原生的 Ktor 方式(Idiomatic Ktor)
导入项目自带的 SQL 架构与初始数据: 导入项目自带的 SQL 架构与初始数据:
```bash ```bash
mysql -u backend -p kotlin_springboot < src/main/resources/springboot_vue3.sql mysql -u backend -p kotlin_springboot < src/main/resources/ktor_rpc.sql
``` ```
> **默认测试账户**: > **默认测试账户**:
@@ -95,3 +96,7 @@ INFO Application - Responding at http://0.0.0.0:8080
| `GET` | `/api/test/wstoken` | 公开 | 签发 WebSocket 一次性握手 Token | | `GET` | `/api/test/wstoken` | 公开 | 签发 WebSocket 一次性握手 Token |
| `WS` | `/ws/test` | 公开 | 测试 WebSocket 管道 | | `WS` | `/ws/test` | 公开 | 测试 WebSocket 管道 |
| `WS` | `/ws/v1/example` | `?token=...` | 校验一次性 Token 后建立连接 | | `WS` | `/ws/v1/example` | `?token=...` | 校验一次性 Token 后建立连接 |
| `RPC` | `/rpc` | 契约代理 | kotlinx-rpc 远程过程调用端点 (kRPC) |
> 详细的接口定义、字段校验规则、RPC 客户端接入代码请查阅 [API 接口文档 (api.md)](./api.md)。
+1 -1
View File
@@ -44,7 +44,7 @@ A Ktor 3.x backend project rewritten in an idiomatic Ktor manner, fully aligned
Import the included database schema and sample data: Import the included database schema and sample data:
```bash ```bash
mysql -u backend -p kotlin_springboot < src/main/resources/springboot_vue3.sql mysql -u backend -p kotlin_springboot < src/main/resources/ktor_rpc.sql
``` ```
> **Default Test Account**: > **Default Test Account**:
+326
View File
@@ -0,0 +1,326 @@
# Ktor Server Demo 接口文档 (RESTful & kotlinx-rpc)
本文档详细描述了 **Ktor Server Demo** 后端服务的 **kotlinx-rpc 远程过程调用接口** 与 **RESTful / WebSocket 接口**
规范,旨在帮助前端开发者、Kotlin 多平台(KMP)客户端开发者以及联调人员快速集成与测试。
---
## 1. kotlinx-rpc 远程过程调用规范 (推荐)
服务端基于 JetBrains 官方 **kotlinx-rpc** (kRPC 协议) 提供强类型、跨平台、端到端的 RPC 通信支持,基于 WebSocket 传输并支持
JSON 序列化。
### 1.1 RPC 服务端点
- **RPC 挂载端点**: `ws://localhost:8080/rpc`
- **传输协议**: `kRPC` (基于 Ktor WebSockets)
- **序列化格式**: JSON (`kotlinx.rpc.krpc.serialization.json`)
### 1.2 客户端接入方式 (Kotlin / KMP)
在客户端的 `build.gradle.kts` 中引入依赖:
```kotlin
plugins {
id("org.jetbrains.kotlinx.rpc.plugin") version "0.10.4"
id("org.jetbrains.kotlin.plugin.serialization") version "2.4.0"
}
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-rpc-krpc-ktor-client:0.10.4")
implementation("org.jetbrains.kotlinx:kotlinx-rpc-krpc-serialization-json:0.10.4")
implementation("io.ktor:ktor-client-cio:3.6.0")
implementation("io.ktor:ktor-client-websockets:3.6.0")
}
```
客户端建立 RPC 连接与调用示例:
```kotlin
import io.ktor.client.*
import io.ktor.client.engine.cio.*
import io.ktor.http.*
import kotlinx.rpc.krpc.ktor.client.installKrpc
import kotlinx.rpc.krpc.ktor.client.rpc
import kotlinx.rpc.krpc.serialization.json.json
import kotlinx.rpc.withService
val httpClient = HttpClient(CIO) {
installKrpc {
serialization {
json()
}
}
}
// 建立连接到 RPC 端点
val rpcClient = httpClient.rpc {
url {
protocol = URLProtocol.WS
host = "localhost"
port = 8080
path("/rpc")
}
}
// 1. 获取认证服务代理
val authService = rpcClient.withService<AuthRpcService>()
val loginResult = authService.login(LoginVO(username = "test", password = "123456"))
// 2. 获取测试服务代理
val testService = rpcClient.withService<TestRpcService>()
println(testService.hello())
// 3. 获取实时流式服务代理 (支持 Flow 双向流)
val streamService = rpcClient.withService<StreamRpcService>()
val echoResult = streamService.echo("hello")
```
---
### 1.3 RPC 契约定义清单
#### 1.3.1 认证服务 (`AuthRpcService`)
定义在包 `io.github.sor2171.backend.rpc.AuthRpcService`:
```kotlin
@Rpc
interface AuthRpcService {
// 用户名/邮箱与密码登录
suspend fun login(loginVO: LoginVO): ApiResponse<AuthorizeVO?>
// 发送邮箱验证码 (支持 REGISTER 或 RESET)
suspend fun askCode(email: String, type: EmailType, clientIp: String = "127.0.0.1"): ApiResponse<String?>
// 邮箱与验证码注册新账号
suspend fun register(registerVO: EmailRegisterVO): ApiResponse<String?>
// 验证码重置密码
suspend fun resetPassword(resetVO: PasswordResetVO): ApiResponse<String?>
// 注销登录并拉黑 Token
suspend fun logout(token: String): ApiResponse<String?>
// 续签刷新 Token
suspend fun relogin(token: String): ApiResponse<ReloginVO?>
}
```
#### 1.3.2 辅助与测试服务 (`TestRpcService`)
定义在包 `io.github.sor2171.backend.rpc.TestRpcService`:
```kotlin
@Rpc
interface TestRpcService {
// 连通性测试,返回 "Hello Kotlin Ktor RPC!"
suspend fun hello(): String
// 签发一次性 WebSocket 握手 Token
suspend fun getWsToken(handlerName: String = "ExampleHandler"): ApiResponse<String>
}
```
#### 1.3.3 实时与双向流通信服务 (`StreamRpcService`)
定义在包 `io.github.sor2171.backend.rpc.StreamRpcService`,利用 Kotlin 协程 `Flow` 进行全双工流式传输:
```kotlin
@Rpc
interface StreamRpcService {
// 单次回显调用
suspend fun echo(message: String): String
// 响应式双向流传输 (Client Flow -> Server Flow)
fun echoStream(messages: Flow<String>): Flow<String>
}
```
---
## 2. 全局设计规范 (RESTful API)
### 2.1 服务基础信息
- **服务基础地址 (Base URL)**: `http://localhost:8080`
- **WebSocket 基础地址 (WS URL)**: `ws://localhost:8080`
- **字符编码**: `UTF-8`
- **数据格式**: 请求体与响应体默认采用 `application/json`;部分接口(如登录)额外兼容 `application/x-www-form-urlencoded`。
### 2.2 跨域策略 (CORS)
服务已配置全局跨域支持:
- 支持任意 Host 源请求 (`anyHost()`)
- 允许携带凭证 (`Access-Control-Allow-Credentials: true`)
- 允许的请求方法:`GET`, `POST`, `PUT`, `DELETE`, `PATCH`, `OPTIONS`
- 允许的请求头:`Content-Type`, `Authorization`
### 2.3 身份认证机制 (Authentication)
- 基于 **JWT (JSON Web Token)** 进行无状态认证(HMAC256 签名)。
- 对于受保护的接口,客户端须在请求头中携带:
```http
Authorization: Bearer <your_jwt_token>
```
- **Token 结构说明**:包含用户 ID (`id`)、用户名 (`username`)、角色权限 (`authorities`)、唯一标识 (`jti`) 及签发/过期时间戳。默认有效期为
168 小时(7 天)。
- **Token 黑名单机制**:用户调用注销登录(Logout)后,Token 的唯一标识(`jti`)将被置入 Redis 黑名单,直至该 Token 自然过期。黑名单中的
Token 即使签名有效也无法通过鉴权。
### 2.4 全局限流与防护 (Flow Limit)
- 服务端基于客户端 IP 实现全局访问频率保护:
- 默认限频规则: **3 秒内请求次数超过 1000 次** 将触发防护。
- 触发防护后封禁时长: **60 秒**。
- 被限流时的响应:HTTP 状态码 `403 Forbidden`,返回信息为 `"Too many requests"`。
- 发送邮件验证码接口具有独立的频控逻辑: **同一 IP 60 秒内仅允许请求一次**。
### 2.5 统一响应结构 (ApiResponse)
所有 RESTful 接口均遵循统一的包装结构(除连通性测试等纯文本接口外):
```json
{
"code": 200,
"data": null,
"message": "Success"
}
```
#### 字段说明
| 字段名 | 类型 | 说明 |
|:----------|:---------|:--------------------------------------------------------------------------|
| `code` | `Int` | 业务状态码,通常与 HTTP 状态码保持一致(如 200、400、401、403、404、500) |
| `data` | `Any?` | 业务数据对象,成功时返回对应实体,无数据或失败时通常为 `null` |
| `message` | `String` | 提示消息(如 `"Success"` 或具体错误原因) |
---
## 3. RESTful 接口列表概览
> **说明**:用户认证相关路由同时支持 `/api/v1/auth/...` 与兼容前缀 `/api/auth/...`。
| 分类 | 方法 | 路径 | 认证方式 | 说明 |
|:------------|:-------|:------------------------|:---------------|:---------------------------------------|
| **认证** | `POST` | `/api/v1/auth/login` | 公开 | 用户名/邮箱与密码登录,返回 JWT |
| **认证** | `GET` | `/api/v1/auth/ask-code` | 公开 | 发送邮箱验证码(注册/重置密码) |
| **认证** | `POST` | `/api/v1/auth/register` | 公开 | 邮箱验证码注册新账号 |
| **认证** | `POST` | `/api/v1/auth/reset` | 公开 | 邮箱验证码重置登录密码 |
| **认证** | `GET` | `/api/v1/auth/logout` | `Bearer Token` | 注销登录并拉黑当前 Token |
| **认证** | `GET` | `/api/v1/auth/relogin` | `Bearer Token` | 续签刷新 Token |
| **测试** | `GET` | `/api/test/hello` | 公开 | 服务连通性测试 |
| **测试** | `GET` | `/api/test/wstoken` | 公开 | 签发一次性 WebSocket 握手 Token |
| **WS 通信** | `WS` | `/ws/test` | 公开 | WebSocket 基础连通测试通道 |
| **WS 通信** | `WS` | `/ws/v1/example` | `?token=...` | 基于一次性 Token 鉴权的 WebSocket 管道 |
---
## 4. RESTful 详细接口规范
### 4.1 用户登录 (`POST /api/v1/auth/login`)
- **请求体 (JSON)**:
```json
{
"username": "test",
"password": "123456",
"remember": false
}
```
- **成功响应 (HTTP 200)**:
```json
{
"code": 200,
"data": {
"username": "test",
"role": "user",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"expire": "2026-10-18T10:00:00"
},
"message": "Success"
}
```
### 4.2 发送邮箱验证码 (`GET /api/v1/auth/ask-code`)
- **Query 参数**: `email`(邮箱地址),`type`(可选,`register` 或 `reset`)。
- **成功响应 (HTTP 200)**:
```json
{
"code": 200,
"data": null,
"message": "Success"
}
```
### 4.3 邮箱注册 (`POST /api/v1/auth/register`)
- **请求体 (JSON)**:
```json
{
"email": "user@example.com",
"code": "849201",
"username": "newuser",
"password": "Password123"
}
```
- **成功响应 (HTTP 200)**:
```json
{
"code": 200,
"data": null,
"message": "Success"
}
```
### 4.4 密码重置 (`POST /api/v1/auth/reset`)
- **请求体 (JSON)**:
```json
{
"email": "user@example.com",
"code": "849201",
"password": "NewPassword123"
}
```
### 4.5 退出登录 (`GET /api/v1/auth/logout`)
- **请求头**: `Authorization: Bearer <token>`
- **成功响应 (HTTP 200)**:
```json
{
"code": 200,
"data": null,
"message": "Success"
}
```
### 4.6 刷新续期 Token (`GET /api/v1/auth/relogin`)
- **请求头**: `Authorization: Bearer <token>`
- **成功响应 (HTTP 200)**:
```json
{
"code": 200,
"data": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"expire": "2026-10-18T10:00:00"
},
"message": "Success"
}
```
---
## 5. 附录:测试账号
在导入初始数据后,默认预置以下测试账号:
- **用户名**: `test`
- **登录密码**: `123456`
- **绑定邮箱**: `1234567890@gmail.com`
- **默认角色**: `user`
+6
View File
@@ -3,6 +3,7 @@
plugins { plugins {
alias(libs.plugins.kotlin.jvm) alias(libs.plugins.kotlin.jvm)
alias(libs.plugins.kotlin.serialization) alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.kotlinx.rpc)
alias(ktorLibs.plugins.ktor) alias(ktorLibs.plugins.ktor)
} }
@@ -61,6 +62,11 @@ dependencies {
// Logging // Logging
implementation(libs.logback.classic) implementation(libs.logback.classic)
// kotlinx-rpc
implementation("org.jetbrains.kotlinx:kotlinx-rpc-krpc-ktor-server:0.10.4")
implementation("org.jetbrains.kotlinx:kotlinx-rpc-krpc-serialization-json:0.10.4")
testImplementation("org.jetbrains.kotlinx:kotlinx-rpc-krpc-ktor-client:0.10.4")
testImplementation(kotlin("test")) testImplementation(kotlin("test"))
testImplementation(ktorLibs.server.testHost) testImplementation(ktorLibs.server.testHost)
} }
+3
View File
@@ -1,6 +1,7 @@
[versions] [versions]
kotlin = "2.4.0" kotlin = "2.4.0"
logback = "1.6.3" logback = "1.6.3"
kotlinx-rpc = "0.10.4"
[libraries] [libraries]
logback-classic = { module = "ch.qos.logback:logback-classic", version.ref = "logback" } logback-classic = { module = "ch.qos.logback:logback-classic", version.ref = "logback" }
@@ -9,3 +10,5 @@ logback-classic = { module = "ch.qos.logback:logback-classic", version.ref = "lo
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
kotlinx-rpc = { id = "org.jetbrains.kotlinx.rpc.plugin", version.ref = "kotlinx-rpc" }
@@ -6,6 +6,7 @@ import io.github.sor2171.backend.plugins.configureSecurity
import io.github.sor2171.backend.plugins.configureSerialization import io.github.sor2171.backend.plugins.configureSerialization
import io.github.sor2171.backend.plugins.configureStatusPages import io.github.sor2171.backend.plugins.configureStatusPages
import io.github.sor2171.backend.plugins.configureWebsockets import io.github.sor2171.backend.plugins.configureWebsockets
import io.github.sor2171.backend.plugins.configureRpc
import io.github.sor2171.backend.service.AuthorizationService import io.github.sor2171.backend.service.AuthorizationService
import io.github.sor2171.backend.service.MailService import io.github.sor2171.backend.service.MailService
import io.github.sor2171.backend.service.RabbitService import io.github.sor2171.backend.service.RabbitService
@@ -16,6 +17,7 @@ import io.github.sor2171.backend.database.DatabaseFactory
import io.github.sor2171.backend.routes.authRoutes import io.github.sor2171.backend.routes.authRoutes
import io.github.sor2171.backend.routes.testRoutes import io.github.sor2171.backend.routes.testRoutes
import io.github.sor2171.backend.routes.wsRoutes import io.github.sor2171.backend.routes.wsRoutes
import io.github.sor2171.backend.routes.rpcRoutes
import io.github.sor2171.security.JwtUtils import io.github.sor2171.security.JwtUtils
import io.ktor.server.application.* import io.ktor.server.application.*
import io.ktor.server.config.* import io.ktor.server.config.*
@@ -117,6 +119,7 @@ fun Application.module() {
configureSecurity(jwtUtils) configureSecurity(jwtUtils)
configureStatusPages() configureStatusPages()
configureWebsockets() configureWebsockets()
configureRpc()
// 9. Configure FlowLimit Plugin // 9. Configure FlowLimit Plugin
val flowCount = environment.config.tryGetString("flow-limit.count")?.toIntOrNull() ?: 1000 val flowCount = environment.config.tryGetString("flow-limit.count")?.toIntOrNull() ?: 1000
@@ -135,5 +138,6 @@ fun Application.module() {
authRoutes(authorizationService) authRoutes(authorizationService)
testRoutes(wsTokenService) testRoutes(wsTokenService)
wsRoutes(wsTokenService) wsRoutes(wsTokenService)
rpcRoutes(authorizationService, wsTokenService)
} }
} }
@@ -12,10 +12,10 @@ object DatabaseFactory : AutoCloseable {
fun init(config: ApplicationConfig) { fun init(config: ApplicationConfig) {
val url = config.tryGetString("datasource.url") val url = config.tryGetString("datasource.url")
?: "jdbc:mysql://localhost:3306/kotlin_springboot?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC" ?: "jdbc:mysql://localhost:3306/ktor_rpc?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC"
val driver = config.tryGetString("datasource.driver") val driver = config.tryGetString("datasource.driver")
?: "com.mysql.cj.jdbc.Driver" ?: "com.mysql.cj.jdbc.Driver"
val username = config.tryGetString("datasource.username") ?: "backend" val username = config.tryGetString("datasource.username") ?: "root"
val password = config.tryGetString("datasource.password") ?: "123456" val password = config.tryGetString("datasource.password") ?: "123456"
val maxPoolSize = config.tryGetString("datasource.max-pool-size")?.toIntOrNull() ?: 10 val maxPoolSize = config.tryGetString("datasource.max-pool-size")?.toIntOrNull() ?: 10
@@ -0,0 +1,8 @@
package io.github.sor2171.backend.plugins
import io.ktor.server.application.*
import kotlinx.rpc.krpc.ktor.server.Krpc
fun Application.configureRpc() {
install(Krpc)
}
@@ -0,0 +1,35 @@
package io.github.sor2171.backend.routes
import io.github.sor2171.backend.rpc.AuthRpcService
import io.github.sor2171.backend.rpc.AuthRpcServiceImpl
import io.github.sor2171.backend.rpc.StreamRpcService
import io.github.sor2171.backend.rpc.StreamRpcServiceImpl
import io.github.sor2171.backend.rpc.TestRpcService
import io.github.sor2171.backend.rpc.TestRpcServiceImpl
import io.github.sor2171.backend.service.AuthorizationService
import io.github.sor2171.backend.service.WsTokenService
import io.ktor.server.routing.*
import kotlinx.rpc.krpc.ktor.server.rpc
import kotlinx.rpc.krpc.serialization.json.json
fun Route.rpcRoutes(
authorizationService: AuthorizationService,
wsTokenService: WsTokenService
) {
rpc("/rpc") {
rpcConfig {
serialization {
json()
}
}
registerService<AuthRpcService> {
AuthRpcServiceImpl(authorizationService)
}
registerService<TestRpcService> {
TestRpcServiceImpl(wsTokenService)
}
registerService<StreamRpcService> {
StreamRpcServiceImpl()
}
}
}
@@ -0,0 +1,20 @@
package io.github.sor2171.backend.rpc
import io.github.sor2171.backend.entity.ApiResponse
import io.github.sor2171.backend.entity.enums.EmailType
import io.github.sor2171.backend.entity.vo.request.EmailRegisterVO
import io.github.sor2171.backend.entity.vo.request.LoginVO
import io.github.sor2171.backend.entity.vo.request.PasswordResetVO
import io.github.sor2171.backend.entity.vo.response.AuthorizeVO
import io.github.sor2171.backend.entity.vo.response.ReloginVO
import kotlinx.rpc.annotations.Rpc
@Rpc
interface AuthRpcService {
suspend fun login(loginVO: LoginVO): ApiResponse<AuthorizeVO?>
suspend fun askCode(email: String, type: EmailType, clientIp: String = "127.0.0.1"): ApiResponse<String?>
suspend fun register(registerVO: EmailRegisterVO): ApiResponse<String?>
suspend fun resetPassword(resetVO: PasswordResetVO): ApiResponse<String?>
suspend fun logout(token: String): ApiResponse<String?>
suspend fun relogin(token: String): ApiResponse<ReloginVO?>
}
@@ -0,0 +1,76 @@
package io.github.sor2171.backend.rpc
import io.github.sor2171.backend.entity.ApiResponse
import io.github.sor2171.backend.entity.enums.EmailType
import io.github.sor2171.backend.entity.vo.request.EmailRegisterVO
import io.github.sor2171.backend.entity.vo.request.LoginVO
import io.github.sor2171.backend.entity.vo.request.PasswordResetVO
import io.github.sor2171.backend.entity.vo.response.AuthorizeVO
import io.github.sor2171.backend.entity.vo.response.ReloginVO
import io.github.sor2171.backend.service.AuthorizationService
class AuthRpcServiceImpl(
private val authorizationService: AuthorizationService
) : AuthRpcService {
override suspend fun login(loginVO: LoginVO): ApiResponse<AuthorizeVO?> {
if (loginVO.username.isBlank() || loginVO.password.isBlank()) {
return ApiResponse.failure(400, null, "Username and password cannot be empty")
}
val result = authorizationService.login(loginVO.username, loginVO.password)
return if (result != null) {
ApiResponse.success(data = result)
} else {
ApiResponse.failure(401, null, "Invalid username or password")
}
}
override suspend fun askCode(email: String, type: EmailType, clientIp: String): ApiResponse<String?> {
if (email.isBlank()) {
return ApiResponse.failure(400, null, "Email cannot be blank")
}
val ip = clientIp.ifBlank { "127.0.0.1" }
val msg = authorizationService.askEmailVerifyCode(type, email, ip)
return if (msg.isBlank()) {
ApiResponse.success(data = null)
} else {
ApiResponse.failure(400, null, msg)
}
}
override suspend fun register(registerVO: EmailRegisterVO): ApiResponse<String?> {
val msg = authorizationService.registerEmailAccount(registerVO)
return if (msg.isBlank()) {
ApiResponse.success(data = null)
} else {
ApiResponse.failure(400, null, msg)
}
}
override suspend fun resetPassword(resetVO: PasswordResetVO): ApiResponse<String?> {
val msg = authorizationService.resetEmailAccountPassword(resetVO)
return if (msg == "Password reset successfully") {
ApiResponse.success(data = null, message = "Password reset successfully")
} else {
ApiResponse.failure(400, null, msg)
}
}
override suspend fun logout(token: String): ApiResponse<String?> {
val success = authorizationService.invalidateJwt(token)
return if (success) {
ApiResponse.success(data = null)
} else {
ApiResponse.failure(400, null, "Logout Failed")
}
}
override suspend fun relogin(token: String): ApiResponse<ReloginVO?> {
val vo = authorizationService.jwtTokenRelogin(token)
return if (vo != null) {
ApiResponse.success(data = vo)
} else {
ApiResponse.failure(401, null, "Please login with password.")
}
}
}
@@ -0,0 +1,10 @@
package io.github.sor2171.backend.rpc
import kotlinx.coroutines.flow.Flow
import kotlinx.rpc.annotations.Rpc
@Rpc
interface StreamRpcService {
suspend fun echo(message: String): String
fun echoStream(messages: Flow<String>): Flow<String>
}
@@ -0,0 +1,17 @@
package io.github.sor2171.backend.rpc
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
class StreamRpcServiceImpl : StreamRpcService {
override suspend fun echo(message: String): String {
return "Kotlin Ktor RPC received: $message"
}
override fun echoStream(messages: Flow<String>): Flow<String> {
return messages.map { message ->
"Kotlin Ktor RPC received: $message"
}
}
}
@@ -0,0 +1,10 @@
package io.github.sor2171.backend.rpc
import io.github.sor2171.backend.entity.ApiResponse
import kotlinx.rpc.annotations.Rpc
@Rpc
interface TestRpcService {
suspend fun hello(): String
suspend fun getWsToken(handlerName: String = "ExampleHandler"): ApiResponse<String>
}
@@ -0,0 +1,22 @@
package io.github.sor2171.backend.rpc
import io.github.sor2171.backend.entity.ApiResponse
import io.github.sor2171.backend.service.WsTokenService
class TestRpcServiceImpl(
private val wsTokenService: WsTokenService
) : TestRpcService {
override suspend fun hello(): String {
return "Hello Kotlin Ktor RPC!"
}
override suspend fun getWsToken(handlerName: String): ApiResponse<String> {
return try {
val token = wsTokenService.registerToken(handlerName)
ApiResponse.success(data = token)
} catch (e: Exception) {
ApiResponse.failure<String>(500, "", e.message)
}
}
}
+2 -2
View File
@@ -6,8 +6,8 @@ ktor:
- io.github.sor2171.backend.ApplicationKt.module - io.github.sor2171.backend.ApplicationKt.module
datasource: datasource:
url: "jdbc:mysql://localhost:3306/kotlin_springboot?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC" url: "jdbc:mysql://localhost:3306/ktor_rpc?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC"
username: "backend" username: "root"
password: "123456" password: "123456"
driver: "com.mysql.cj.jdbc.Driver" driver: "com.mysql.cj.jdbc.Driver"
max-pool-size: 10 max-pool-size: 10
+86
View File
@@ -0,0 +1,86 @@
package io.github.sor2171.backend
import io.github.sor2171.backend.entity.enums.EmailType
import io.github.sor2171.backend.entity.vo.request.LoginVO
import io.github.sor2171.backend.routes.rpcRoutes
import io.github.sor2171.backend.rpc.AuthRpcService
import io.github.sor2171.backend.rpc.StreamRpcService
import io.github.sor2171.backend.rpc.TestRpcService
import io.github.sor2171.backend.service.AuthorizationService
import io.github.sor2171.backend.service.WsTokenService
import io.ktor.http.*
import io.ktor.server.testing.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.toList
import kotlinx.rpc.krpc.ktor.client.installKrpc
import kotlinx.rpc.krpc.ktor.client.rpc
import kotlinx.rpc.krpc.ktor.server.Krpc
import kotlinx.rpc.krpc.serialization.json.json
import kotlinx.rpc.withService
import kotlin.test.Test
import kotlin.test.assertEquals
class RpcTest {
@Test
fun testRealRpcServices() = testApplication {
install(Krpc)
val appScope = CoroutineScope(Dispatchers.Default)
routing {
val redis = io.github.sor2171.backend.service.RedisService("redis://localhost:6379")
val wsTokenService = WsTokenService(redis)
val accountRepo = io.github.sor2171.backend.database.AccountRepository()
val jwt = io.github.sor2171.security.JwtUtils("dummy-key-for-test-348572394857293847529384", 24, redis)
val mail = io.github.sor2171.backend.service.MailService("", 465, "", "")
val rabbit = io.github.sor2171.backend.service.RabbitService("localhost", 5672, "/", "admin", "123456", "mail", mail, appScope)
val authService = AuthorizationService(accountRepo, jwt, redis, rabbit)
rpcRoutes(authService, wsTokenService)
}
val rpcClient = createClient {
installKrpc {
serialization {
json()
}
}
}.rpc {
url {
path("/rpc")
}
}
// 1. Test TestRpcService
val testService = rpcClient.withService<TestRpcService>()
val helloResult = testService.hello()
assertEquals("Hello Kotlin Ktor RPC!", helloResult)
// 2. Test StreamRpcService - single call & streaming Flow
val streamService = rpcClient.withService<StreamRpcService>()
val echoResult = streamService.echo("hello stream")
assertEquals("Kotlin Ktor RPC received: hello stream", echoResult)
val streamResponses = streamService.echoStream(flowOf("A", "B", "C")).toList()
assertEquals(
listOf(
"Kotlin Ktor RPC received: A",
"Kotlin Ktor RPC received: B",
"Kotlin Ktor RPC received: C"
),
streamResponses
)
// 3. Test AuthRpcService validation
val authRpcService = rpcClient.withService<AuthRpcService>()
val emptyLoginRes = authRpcService.login(LoginVO("", ""))
assertEquals(400, emptyLoginRes.code)
assertEquals("Username and password cannot be empty", emptyLoginRes.message)
val emptyCodeRes = authRpcService.askCode("", EmailType.REGISTER)
assertEquals(400, emptyCodeRes.code)
assertEquals("Email cannot be blank", emptyCodeRes.message)
}
}