305 lines
15 KiB
Markdown
305 lines
15 KiB
Markdown
# 会话服务
|
||
|
||
<cite>
|
||
**本文档引用文件**
|
||
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go)
|
||
- [yggdrasil_service.go](file://internal/service/yggdrasil_service.go)
|
||
- [yggdrasil_service_test.go](file://internal/service/yggdrasil_service_test.go)
|
||
- [routes.go](file://internal/handler/routes.go)
|
||
- [profile.go](file://internal/model/profile.go)
|
||
- [profile_service.go](file://internal/service/profile_service.go)
|
||
- [redis.go](file://pkg/redis/redis.go)
|
||
</cite>
|
||
|
||
## 目录
|
||
1. [简介](#简介)
|
||
2. [API路由结构](#api路由结构)
|
||
3. [核心API功能详解](#核心api功能详解)
|
||
4. [会话数据结构](#会话数据结构)
|
||
5. [反作弊机制](#反作弊机制)
|
||
6. [与Minecraft客户端交互流程](#与minecraft客户端交互流程)
|
||
7. [错误处理与日志](#错误处理与日志)
|
||
8. [测试验证](#测试验证)
|
||
|
||
## 简介
|
||
本文档详细描述了Yggdrasil会话服务的核心功能,重点聚焦于`/sessionserver`路由组下的三个关键API:`GetProfileByUUID`、`JoinServer`和`HasJoinedServer`。这些API构成了Minecraft服务器会话验证系统的核心,负责处理玩家加入服务器的会话建立、验证和玩家档案查询。
|
||
|
||
系统通过Redis存储会话数据,利用`Join_`为前缀的键名和15分钟的TTL(生存时间)来管理会话生命周期。整个流程确保了只有经过身份验证的玩家才能加入服务器,同时通过IP地址和用户名的双重验证机制防止作弊行为。
|
||
|
||
**Section sources**
|
||
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L1-L100)
|
||
- [routes.go](file://internal/handler/routes.go#L87-L111)
|
||
|
||
## API路由结构
|
||
会话服务的API路由定义在`routes.go`文件中,位于`/yggdrasil/sessionserver`路径下。该路由组提供了三个核心端点:
|
||
|
||
- `GET /session/minecraft/profile/:uuid`:根据玩家UUID获取其公开档案信息。
|
||
- `POST /session/minecraft/join`:接收客户端的会话信息,建立服务器会话。
|
||
- `GET /session/minecraft/hasJoined`:验证某玩家是否已成功加入指定服务器。
|
||
|
||
```mermaid
|
||
graph TB
|
||
subgraph "Yggdrasil API"
|
||
A[/yggdrasil/sessionserver]
|
||
A --> B[GET /session/minecraft/profile/:uuid]
|
||
A --> C[POST /session/minecraft/join]
|
||
A --> D[GET /session/minecraft/hasJoined]
|
||
end
|
||
```
|
||
|
||
**Diagram sources **
|
||
- [routes.go](file://internal/handler/routes.go#L100-L105)
|
||
|
||
**Section sources**
|
||
- [routes.go](file://internal/handler/routes.go#L87-L105)
|
||
|
||
## 核心API功能详解
|
||
|
||
### JoinServer API
|
||
`JoinServer` API是建立服务器会话的核心。当Minecraft客户端成功登录后,会调用此API来“加入”一个特定的服务器。
|
||
|
||
**功能流程:**
|
||
1. **接收参数**:API接收`serverId`、`accessToken`和`selectedProfile`(玩家UUID)三个必需参数。
|
||
2. **输入验证**:对`serverId`进行格式检查(长度不超过100字符,不包含`<>\"'&`等危险字符),并验证客户端IP地址格式。
|
||
3. **令牌验证**:通过`accessToken`在数据库中查找对应的令牌记录,确保令牌有效。
|
||
4. **配置文件匹配**:将`selectedProfile`(客户端提供的UUID)与令牌中关联的`ProfileId`进行比对,确保玩家使用的是正确的配置文件。
|
||
5. **构建会话数据**:从数据库中获取该UUID对应的玩家档案,提取其用户名(`Name`)。
|
||
6. **存储会话**:将`accessToken`、`userName`、`selectedProfile`和客户端IP地址构建成`SessionData`结构体,序列化后存入Redis。存储的键名为`Join_` + `serverId`,TTL设置为15分钟。
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant Client as "Minecraft客户端"
|
||
participant Handler as "yggdrasil_handler"
|
||
participant Service as "yggdrasil_service"
|
||
participant Redis as "Redis"
|
||
participant DB as "数据库"
|
||
Client->>Handler : POST /join (serverId, accessToken, selectedProfile)
|
||
Handler->>Service : JoinServer(serverId, accessToken, selectedProfile, clientIP)
|
||
Service->>DB : 根据accessToken查找Token
|
||
DB-->>Service : Token信息
|
||
Service->>Service : 验证selectedProfile与Token匹配
|
||
Service->>DB : 根据UUID查找Profile
|
||
DB-->>Service : Profile (含用户名)
|
||
Service->>Service : 构建SessionData对象
|
||
Service->>Service : 序列化SessionData
|
||
Service->>Redis : Set(Join_serverId, marshaledData, 15min)
|
||
Redis-->>Service : 成功
|
||
Service-->>Handler : 成功
|
||
Handler-->>Client : HTTP 204 No Content
|
||
```
|
||
|
||
**Diagram sources **
|
||
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L449-L496)
|
||
- [yggdrasil_service.go](file://internal/service/yggdrasil_service.go#L81-L163)
|
||
|
||
**Section sources**
|
||
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L449-L496)
|
||
- [yggdrasil_service.go](file://internal/service/yggdrasil_service.go#L81-L163)
|
||
|
||
### HasJoinedServer API
|
||
`HasJoinedServer` API用于服务器端验证一个玩家是否已经通过了会话验证。
|
||
|
||
**功能流程:**
|
||
1. **接收参数**:API接收`serverId`和`username`两个必需参数,以及可选的`ip`参数。
|
||
2. **输入验证**:确保`serverId`和`username`不为空。
|
||
3. **获取会话数据**:使用`Join_` + `serverId`作为键名,从Redis中获取之前存储的会话数据。
|
||
4. **反序列化**:将获取到的JSON数据反序列化为`SessionData`结构体。
|
||
5. **验证匹配**:
|
||
- **用户名匹配**:比较会话数据中的`UserName`与请求中的`username`是否完全一致(区分大小写)。
|
||
- **IP地址匹配**:如果请求中提供了`ip`参数,则会比较会话数据中的`IP`与请求的`ip`是否一致。
|
||
6. **返回结果**:如果所有验证通过,则从数据库中获取该`username`对应的完整玩家档案(包括皮肤、披风等信息)并返回;否则返回验证失败。
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant Server as "Minecraft服务器"
|
||
participant Handler as "yggdrasil_handler"
|
||
participant Service as "yggdrasil_service"
|
||
participant Redis as "Redis"
|
||
participant DB as "数据库"
|
||
Server->>Handler : GET /hasJoined?serverId=...&username=...&ip=...
|
||
Handler->>Service : HasJoinedServer(serverId, username, ip)
|
||
Service->>Redis : Get(Join_serverId)
|
||
Redis-->>Service : marshaledData (或 nil)
|
||
alt 会话不存在
|
||
Service-->>Handler : 错误
|
||
Handler-->>Server : HTTP 204 No Content
|
||
else 会话存在
|
||
Service->>Service : 反序列化为SessionData
|
||
Service->>Service : 验证UserName == username
|
||
Service->>Service : 验证IP (如果提供)
|
||
alt 验证失败
|
||
Service-->>Handler : 错误
|
||
Handler-->>Server : HTTP 204 No Content
|
||
else 验证成功
|
||
Service->>DB : 根据username查找Profile
|
||
DB-->>Service : Profile信息
|
||
Service-->>Handler : Profile
|
||
Handler-->>Server : HTTP 200 + Profile JSON
|
||
end
|
||
end
|
||
```
|
||
|
||
**Diagram sources **
|
||
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L498-L552)
|
||
- [yggdrasil_service.go](file://internal/service/yggdrasil_service.go#L165-L201)
|
||
|
||
**Section sources**
|
||
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L498-L552)
|
||
- [yggdrasil_service.go](file://internal/service/yggdrasil_service.go#L165-L201)
|
||
|
||
### GetProfileByUUID API
|
||
`GetProfileByUUID` API提供了一种通过玩家UUID查询其公开档案信息的方式。
|
||
|
||
**功能流程:**
|
||
1. **接收参数**:从URL路径中获取`:uuid`参数。
|
||
2. **格式化UUID**:调用`utils.FormatUUID`函数,将可能存在的十六进制格式UUID转换为标准的带连字符格式。
|
||
3. **查询档案**:调用`service.GetProfileByUUID`方法,根据格式化后的UUID在数据库中查找对应的`Profile`记录。
|
||
4. **序列化响应**:将`Profile`模型转换为包含皮肤(Skin)和披风(Cape)URL的`ProfileResponse`结构体。
|
||
5. **返回结果**:将序列化后的JSON数据返回给客户端。
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant Client as "客户端"
|
||
participant Handler as "yggdrasil_handler"
|
||
participant Service as "profile_service"
|
||
participant DB as "数据库"
|
||
Client->>Handler : GET /profile/ : uuid
|
||
Handler->>Handler : FormatUUID(uuid)
|
||
Handler->>Service : GetProfileByUUID(uuid)
|
||
Service->>DB : FindProfileByUUID(uuid)
|
||
DB-->>Service : Profile
|
||
Service-->>Handler : Profile
|
||
Handler->>Handler : SerializeProfile(Profile)
|
||
Handler-->>Client : HTTP 200 + Profile JSON
|
||
```
|
||
|
||
**Diagram sources **
|
||
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L427-L447)
|
||
- [profile_service.go](file://internal/service/profile_service.go#L71-L81)
|
||
|
||
**Section sources**
|
||
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L427-L447)
|
||
- [profile_service.go](file://internal/service/profile_service.go#L71-L81)
|
||
|
||
## 会话数据结构
|
||
`SessionData`结构体定义了存储在Redis中的会话信息,是`JoinServer`和`HasJoinedServer`两个API之间通信的核心载体。
|
||
|
||
```go
|
||
type SessionData struct {
|
||
AccessToken string `json:"accessToken"`
|
||
UserName string `json:"userName"`
|
||
SelectedProfile string `json:"selectedProfile"`
|
||
IP string `json:"ip"`
|
||
}
|
||
```
|
||
|
||
- **AccessToken**:客户端的访问令牌,用于在`JoinServer`时验证身份。
|
||
- **UserName**:玩家的用户名(如`Steve`),在`HasJoinedServer`时用于比对。
|
||
- **SelectedProfile**:玩家的UUID,用于唯一标识玩家。
|
||
- **IP**:客户端的IP地址,用于反作弊验证。
|
||
|
||
该结构体在`JoinServer`时被创建并序列化存储,在`HasJoinedServer`时被反序列化读取并用于验证。
|
||
|
||
```mermaid
|
||
classDiagram
|
||
class SessionData {
|
||
+string accessToken
|
||
+string userName
|
||
+string selectedProfile
|
||
+string ip
|
||
}
|
||
```
|
||
|
||
**Diagram sources **
|
||
- [yggdrasil_service.go](file://internal/service/yggdrasil_service.go#L25-L30)
|
||
|
||
**Section sources**
|
||
- [yggdrasil_service.go](file://internal/service/yggdrasil_service.go#L25-L30)
|
||
|
||
## 反作弊机制
|
||
系统通过`HasJoinedServer` API实现了有效的反作弊机制,主要依赖于以下两个层面的验证:
|
||
|
||
1. **令牌绑定验证**:在`JoinServer`阶段,系统强制要求`selectedProfile`(UUID)必须与`accessToken`所关联的配置文件ID完全匹配。这确保了玩家不能使用他人的令牌来冒充身份。
|
||
|
||
2. **IP地址与时间戳验证**:
|
||
- **IP地址验证**:`HasJoinedServer` API可以选择性地接收一个`ip`参数。如果提供了该参数,系统会将其与`JoinServer`时记录的IP地址进行比对。如果两者不一致,则验证失败。这可以有效防止玩家在一台机器上登录后,将令牌分享给另一台机器上的其他玩家使用。
|
||
- **时间戳验证**:通过将Redis中会话数据的TTL设置为15分钟,系统实现了会话的自动过期。这意味着即使令牌和IP验证通过,该会话也只在有限时间内有效,增加了作弊的难度和成本。
|
||
|
||
**Section sources**
|
||
- [yggdrasil_service.go](file://internal/service/yggdrasil_service.go#L195-L198)
|
||
|
||
## 与Minecraft客户端交互流程
|
||
以下是Minecraft客户端与本会话服务交互的完整流程示例:
|
||
|
||
1. **客户端登录**:玩家在Minecraft启动器中输入邮箱和密码,启动器调用`/authserver/authenticate` API进行身份验证,并获取`accessToken`和`availableProfiles`。
|
||
2. **选择配置文件**:启动器列出可用的配置文件,玩家选择一个(如`Steve`)。
|
||
3. **加入服务器**:
|
||
- 玩家在游戏内选择一个服务器并点击“加入”。
|
||
- 启动器调用`/sessionserver/session/minecraft/join` API,携带`serverId`(服务器的哈希值)、`accessToken`和`selectedProfile`(`Steve`的UUID)。
|
||
- 服务端验证信息无误后,将包含`accessToken`、`userName`(`Steve`)、`selectedProfile`和客户端IP的`SessionData`存入Redis,键名为`Join_` + `serverId`。
|
||
4. **服务器验证**:
|
||
- 游戏客户端连接到Minecraft服务器。
|
||
- 服务器向本会话服务的`/sessionserver/session/minecraft/hasJoined` API发起请求,携带`serverId`、`username`(`Steve`)和客户端IP。
|
||
- 服务端查找Redis中对应的会话数据,并验证`userName`和`ip`是否匹配。
|
||
- 如果验证通过,服务端返回`Steve`的完整档案信息(包括皮肤URL),服务器允许玩家加入游戏;否则,连接被拒绝。
|
||
|
||
```mermaid
|
||
flowchart TD
|
||
A[Minecraft客户端] --> |1. authenticate| B[/authserver/authenticate]
|
||
B --> C{获取 accessToken 和 UUID}
|
||
C --> D[选择配置文件]
|
||
D --> E[点击加入服务器]
|
||
E --> |3. join| F[/sessionserver/join]
|
||
F --> G[Redis: 存储会话]
|
||
E --> H[Minecraft服务器]
|
||
H --> |4. hasJoined| I[/sessionserver/hasJoined]
|
||
I --> J[Redis: 查找会话]
|
||
J --> K{验证通过?}
|
||
K --> |是| L[返回玩家档案]
|
||
L --> M[允许加入游戏]
|
||
K --> |否| N[拒绝连接]
|
||
```
|
||
|
||
**Diagram sources **
|
||
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L449-L552)
|
||
- [yggdrasil_service.go](file://internal/service/yggdrasil_service.go#L81-L201)
|
||
|
||
**Section sources**
|
||
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L449-L552)
|
||
- [yggdrasil_service.go](file://internal/service/yggdrasil_service.go#L81-L201)
|
||
|
||
## 错误处理与日志
|
||
系统在关键操作点都集成了详细的错误处理和日志记录:
|
||
|
||
- **输入验证**:对所有API的输入参数进行严格校验,如空值、格式错误等,并返回清晰的错误信息。
|
||
- **业务逻辑错误**:对于令牌无效、配置文件不匹配、用户名不匹配等情况,返回特定的错误码和消息。
|
||
- **系统错误**:对数据库查询失败、Redis操作失败、JSON序列化/反序列化失败等底层错误进行捕获和记录。
|
||
- **日志记录**:使用`zap`日志库,对关键操作(如“玩家成功加入服务器”、“会话验证失败”)进行结构化日志记录,便于问题追踪和审计。
|
||
|
||
**Section sources**
|
||
- [yggdrasil_handler.go](file://internal/handler/yggdrasil_handler.go#L459-L484)
|
||
- [yggdrasil_service.go](file://internal/service/yggdrasil_service.go#L103-L155)
|
||
|
||
## 测试验证
|
||
系统的功能通过单元测试得到了充分验证,确保了核心逻辑的正确性。
|
||
|
||
- **常量验证**:测试确认`SessionKeyPrefix`常量值为`"Join_"`,`SessionTTL`为15分钟。
|
||
- **输入验证**:对`JoinServer`和`HasJoinedServer`的输入参数(空值、格式)进行了全面的边界测试。
|
||
- **逻辑验证**:测试了`JoinServer`的会话键生成逻辑,确保`serverId`能正确拼接成`Join_serverId`的格式。
|
||
- **匹配逻辑**:验证了`HasJoinedServer`的用户名和IP地址匹配逻辑,确保大小写敏感和IP比对的正确性。
|
||
|
||
```mermaid
|
||
graph TD
|
||
A[测试用例] --> B[TestYggdrasilService_Constants]
|
||
A --> C[TestJoinServer_InputValidation]
|
||
A --> D[TestHasJoinedServer_InputValidation]
|
||
A --> E[TestJoinServer_SessionKey]
|
||
A --> F[TestHasJoinedServer_UsernameMatching]
|
||
A --> G[TestHasJoinedServer_IPMatching]
|
||
```
|
||
|
||
**Diagram sources **
|
||
- [yggdrasil_service_test.go](file://internal/service/yggdrasil_service_test.go#L10-L350)
|
||
|
||
**Section sources**
|
||
- [yggdrasil_service_test.go](file://internal/service/yggdrasil_service_test.go#L10-L350) |