Skip to content

Commit 0f891b0

Browse files
committed
feat(api): 重构白名单系统
- 简化白名单添加逻辑,仅需玩家名即可添加,UUID 自动补充 - 新增可配置的 API Token 认证机制,默认启用安全保护 - 支持批量操作(添加/删除)与来源统计 - 增强白名单状态跟踪,引入 uuid_pending 状态字段 - 完善同步系统,支持 UUID 补充后的自动文件更新 - 更新 API 文档,提供详尽使用示例和迁移指南 - 插件首次启动时自动生成管理员密码与 API Token - 提升兼容性,同时支持正版与离线模式
1 parent 339d68c commit 0f891b0

14 files changed

Lines changed: 1204 additions & 162 deletions

File tree

API.md

Lines changed: 558 additions & 77 deletions
Large diffs are not rendered by default.

README.md

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646

4747
## 安装方法
4848

49-
1. 下载最新版本的 `convenient-access-0.1.0.jar`
49+
1. 下载最新版本的 `convenient-access-0.5.0.jar`
5050
2. 将插件文件放入服务器的 `plugins` 目录
5151
3. 重启服务器或使用 `/reload` 命令
5252
4. 插件将自动:
@@ -294,7 +294,7 @@ curl -H "X-API-Key: your-api-key" \
294294

295295
## 开发信息
296296

297-
- **版本**: 0.1.0
297+
- **版本**: 0.5.0
298298
- **作者**: xaoxiao
299299
- **许可证**: MIT
300300
- **最低 Java 版本**: 17
@@ -340,7 +340,16 @@ curl -H "X-API-Key: your-api-key" \
340340

341341
## 更新日志
342342

343-
### v0.1.0 (2024-01-01)
343+
### v0.5.0 (2025-10-02) - WhitelistPlus设计集成
344+
- 🎯 **重大改进**:基于WhitelistPlus设计理念重构白名单系统
345+
- ✨ **简化API**:添加白名单现在只需玩家名,UUID可选
346+
- 🔄 **自动UUID补充**:玩家首次登录时自动补充UUID
347+
- 📊 **增强统计**:新增UUID待补充状态、来源分解等统计信息
348+
- 🔧 **批量操作**:支持批量添加和删除操作
349+
- 📁 **同步系统**:新增UUID更新同步任务类型
350+
- 🎮 **兼容性**:完美支持离线和正版服务器
351+
352+
### v0.1.0 (2024-01-01) - 初始版本
344353
- ✅ **完整的白名单管理系统**
345354
- 数据库设计和CRUD操作
346355
- 批量操作和高级查询

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
<groupId>com.xaoxiao</groupId>
88
<artifactId>convenient-access</artifactId>
9-
<version>0.1.0</version>
9+
<version>0.5.0</version>
1010
<packaging>jar</packaging>
1111

1212
<name>ConvenientAccess</name>

src/main/java/com/xaoxiao/convenientaccess/ConvenientAccessPlugin.java

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ public void onEnable() {
3535
// 初始化配置管理器
3636
configManager = new ConfigManager(this);
3737

38+
// 自动生成管理员密码和API Token(仅首次启动)
39+
initializeAuthConfig();
40+
3841
// 初始化现有组件
3942
cacheManager = new CacheManager(configManager);
4043
sparkIntegration = new SparkIntegration(this);
@@ -178,4 +181,74 @@ public void reload() {
178181
throw new RuntimeException("插件重载失败: " + e.getMessage());
179182
}
180183
}
184+
185+
/**
186+
* 初始化认证配置(自动生成密码和Token)
187+
*/
188+
private void initializeAuthConfig() {
189+
boolean configChanged = false;
190+
191+
// 检查并生成管理员密码
192+
String adminPassword = configManager.getAdminPassword();
193+
if (adminPassword == null || adminPassword.trim().isEmpty()) {
194+
// 生成12位随机密码
195+
String newPassword = generateRandomPassword(12);
196+
configManager.setAdminPassword(newPassword);
197+
configChanged = true;
198+
logger.info("已自动生成管理员密码: {}", newPassword);
199+
logger.warn("请妥善保管管理员密码,用于生成注册令牌等管理操作");
200+
} else {
201+
logger.info("使用配置文件中的管理员密码");
202+
}
203+
204+
// 检查并生成API Token
205+
String apiToken = configManager.getApiToken();
206+
if (apiToken == null || apiToken.trim().isEmpty()) {
207+
// 生成64位API Token(sk-开头)
208+
String newToken = generateApiToken();
209+
configManager.setApiToken(newToken);
210+
configChanged = true;
211+
logger.info("已自动生成API访问令牌: {}", newToken);
212+
logger.warn("请妥善保管API令牌,用于API访问认证");
213+
} else {
214+
logger.info("使用配置文件中的API令牌");
215+
}
216+
217+
if (configChanged) {
218+
logger.info("认证配置已更新并保存到配置文件");
219+
}
220+
}
221+
222+
/**
223+
* 生成随机密码
224+
*/
225+
private String generateRandomPassword(int length) {
226+
String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
227+
StringBuilder sb = new StringBuilder();
228+
java.util.Random random = new java.util.Random();
229+
230+
for (int i = 0; i < length; i++) {
231+
sb.append(chars.charAt(random.nextInt(chars.length())));
232+
}
233+
234+
return sb.toString();
235+
}
236+
237+
/**
238+
* 生成API Token(sk-开头的64位token)
239+
*/
240+
private String generateApiToken() {
241+
String prefix = configManager.getTokenPrefix();
242+
String tokenChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
243+
StringBuilder sb = new StringBuilder(prefix);
244+
java.util.Random random = new java.util.Random();
245+
246+
// 生成64位token(包含前缀)
247+
int tokenLength = 64 - prefix.length();
248+
for (int i = 0; i < tokenLength; i++) {
249+
sb.append(tokenChars.charAt(random.nextInt(tokenChars.length())));
250+
}
251+
252+
return sb.toString();
253+
}
181254
}

src/main/java/com/xaoxiao/convenientaccess/api/ApiRouter.java

Lines changed: 92 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
import org.slf4j.Logger;
66
import org.slf4j.LoggerFactory;
77

8+
import com.xaoxiao.convenientaccess.config.ConfigManager;
9+
810
import jakarta.servlet.ServletException;
911
import jakarta.servlet.http.HttpServlet;
1012
import jakarta.servlet.http.HttpServletRequest;
@@ -19,10 +21,80 @@ public class ApiRouter extends HttpServlet {
1921

2022
private final WhitelistApiController whitelistController;
2123
private final UserApiController userController;
24+
private final ConfigManager configManager;
2225

23-
public ApiRouter(WhitelistApiController whitelistController, UserApiController userController) {
26+
public ApiRouter(WhitelistApiController whitelistController, UserApiController userController, ConfigManager configManager) {
2427
this.whitelistController = whitelistController;
2528
this.userController = userController;
29+
this.configManager = configManager;
30+
}
31+
32+
/**
33+
* 验证API请求的认证
34+
*/
35+
private boolean isAuthenticated(HttpServletRequest request, String path) {
36+
// 如果认证被禁用,直接通过
37+
if (!configManager.isAuthEnabled()) {
38+
return true;
39+
}
40+
41+
// 公开的端点不需要认证
42+
if (isPublicEndpoint(path)) {
43+
return true;
44+
}
45+
46+
// 检查API Token
47+
String apiKey = request.getHeader("X-API-Key");
48+
if (apiKey == null) {
49+
apiKey = request.getHeader("Authorization");
50+
if (apiKey != null && apiKey.startsWith("Bearer ")) {
51+
apiKey = apiKey.substring(7);
52+
}
53+
}
54+
55+
if (apiKey != null) {
56+
String validToken = configManager.getApiToken();
57+
if (validToken != null && validToken.equals(apiKey)) {
58+
return true;
59+
}
60+
}
61+
62+
// 对于管理员端点,检查管理员密码
63+
if (isAdminEndpoint(path)) {
64+
String adminPassword = request.getHeader("X-Admin-Password");
65+
if (adminPassword != null) {
66+
String validPassword = configManager.getAdminPassword();
67+
return validPassword != null && validPassword.equals(adminPassword);
68+
}
69+
}
70+
71+
return false;
72+
}
73+
74+
/**
75+
* 判断是否为公开端点(不需要认证)
76+
*/
77+
private boolean isPublicEndpoint(String path) {
78+
return path.equals("/api/v1/register") ||
79+
path.equals("/api/v1/admin/generate-token");
80+
}
81+
82+
/**
83+
* 判断是否为管理员端点
84+
*/
85+
private boolean isAdminEndpoint(String path) {
86+
return path.startsWith("/api/v1/admin/");
87+
}
88+
89+
/**
90+
* 发送认证失败响应
91+
*/
92+
private void sendAuthFailedResponse(HttpServletResponse response) throws IOException {
93+
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
94+
response.setContentType("application/json");
95+
response.setCharacterEncoding("UTF-8");
96+
response.getWriter().write("{\"success\":false,\"error\":\"Unauthorized: Invalid API key or admin password\"}");
97+
response.getWriter().flush();
2698
}
2799

28100
@Override
@@ -51,6 +123,12 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response)
51123

52124
logger.debug("GET request to: {}", path);
53125

126+
// 检查认证
127+
if (!isAuthenticated(request, path)) {
128+
sendAuthFailedResponse(response);
129+
return;
130+
}
131+
54132
try {
55133
// 白名单相关路由
56134
if (path.startsWith("/api/v1/whitelist")) {
@@ -83,6 +161,12 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response)
83161

84162
logger.debug("POST request to: {}", path);
85163

164+
// 检查认证
165+
if (!isAuthenticated(request, path)) {
166+
sendAuthFailedResponse(response);
167+
return;
168+
}
169+
86170
try {
87171
// 白名单相关路由
88172
if (path.startsWith("/api/v1/whitelist")) {
@@ -124,6 +208,12 @@ protected void doDelete(HttpServletRequest request, HttpServletResponse response
124208

125209
logger.debug("DELETE request to: {}", path);
126210

211+
// 检查认证
212+
if (!isAuthenticated(request, path)) {
213+
sendAuthFailedResponse(response);
214+
return;
215+
}
216+
127217
try {
128218
// 白名单删除路由
129219
if (path.startsWith("/api/v1/whitelist/")) {
@@ -145,7 +235,7 @@ protected void doOptions(HttpServletRequest request, HttpServletResponse respons
145235
// 设置CORS头
146236
response.setHeader("Access-Control-Allow-Origin", "*");
147237
response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
148-
response.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Admin-Password");
238+
response.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, X-API-Key, X-Admin-Password");
149239
response.setHeader("Access-Control-Max-Age", "3600");
150240
response.setStatus(HttpServletResponse.SC_OK);
151241
}

src/main/java/com/xaoxiao/convenientaccess/api/WhitelistApiController.java

Lines changed: 30 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
package com.xaoxiao.convenientaccess.api;
22

3+
import java.io.IOException;
4+
import java.time.LocalDateTime;
5+
import java.time.format.DateTimeFormatter;
6+
import java.util.ArrayList;
7+
import java.util.List;
8+
import java.util.concurrent.CompletableFuture;
9+
10+
import org.slf4j.Logger;
11+
import org.slf4j.LoggerFactory;
12+
313
import com.google.gson.Gson;
414
import com.google.gson.GsonBuilder;
515
import com.google.gson.JsonArray;
@@ -10,23 +20,13 @@
1020
import com.google.gson.stream.JsonWriter;
1121
import com.xaoxiao.convenientaccess.sync.SyncTask;
1222
import com.xaoxiao.convenientaccess.sync.SyncTaskManager;
13-
import com.xaoxiao.convenientaccess.whitelist.BatchOperation;
1423
import com.xaoxiao.convenientaccess.utils.UuidUtils;
24+
import com.xaoxiao.convenientaccess.whitelist.BatchOperation;
1525
import com.xaoxiao.convenientaccess.whitelist.WhitelistEntry;
1626
import com.xaoxiao.convenientaccess.whitelist.WhitelistManager;
17-
import com.xaoxiao.convenientaccess.whitelist.WhitelistStats;
27+
1828
import jakarta.servlet.http.HttpServletRequest;
1929
import jakarta.servlet.http.HttpServletResponse;
20-
import org.slf4j.Logger;
21-
import org.slf4j.LoggerFactory;
22-
23-
import java.io.IOException;
24-
import java.time.LocalDateTime;
25-
import java.time.format.DateTimeFormatter;
26-
import java.util.ArrayList;
27-
import java.util.List;
28-
import java.util.Optional;
29-
import java.util.concurrent.CompletableFuture;
3030

3131
/**
3232
* 白名单API控制器
@@ -113,16 +113,15 @@ public void handleAddPlayer(HttpServletRequest request, HttpServletResponse resp
113113
String requestBody = readRequestBody(request);
114114
JsonObject json = JsonParser.parseString(requestBody).getAsJsonObject();
115115

116-
// 参数验证 - 要求name、added_by_name、added_by_uuid、source,uuid变为可选
117-
if (!json.has("name") || !json.has("added_by_name") || !json.has("added_by_uuid") || !json.has("source")) {
118-
sendJsonResponse(response, 400, ApiResponse.badRequest("缺少必需参数: name, added_by_name, added_by_uuid, source"));
116+
// 参数验证 - 只需要name、source,其他为可选
117+
if (!json.has("name") || !json.has("source")) {
118+
sendJsonResponse(response, 400, ApiResponse.badRequest("缺少必需参数: name, source"));
119119
return;
120120
}
121121

122122
String name = json.get("name").getAsString();
123-
String providedUuid = json.has("uuid") ? json.get("uuid").getAsString() : null;
124-
String addedByName = json.get("added_by_name").getAsString();
125-
String addedByUuid = json.get("added_by_uuid").getAsString();
123+
String addedByName = json.has("added_by_name") ? json.get("added_by_name").getAsString() : "API";
124+
String addedByUuid = json.has("added_by_uuid") ? json.get("added_by_uuid").getAsString() : "00000000-0000-0000-0000-000000000000";
126125
String sourceStr = json.get("source").getAsString();
127126

128127
// 处理时间戳 - 如果前端提供则使用,否则使用当前时间
@@ -145,15 +144,6 @@ public void handleAddPlayer(HttpServletRequest request, HttpServletResponse resp
145144
return;
146145
}
147146

148-
// 生成或验证UUID
149-
String uuid;
150-
try {
151-
uuid = UuidUtils.getOrGenerateUuid(name, providedUuid);
152-
} catch (IllegalArgumentException e) {
153-
sendJsonResponse(response, 400, ApiResponse.badRequest("玩家名不能为空"));
154-
return;
155-
}
156-
157147
WhitelistEntry.Source source;
158148
try {
159149
source = WhitelistEntry.Source.fromString(sourceStr);
@@ -162,26 +152,30 @@ public void handleAddPlayer(HttpServletRequest request, HttpServletResponse resp
162152
return;
163153
}
164154

165-
// 添加玩家
166-
whitelistManager.addPlayer(name, uuid, addedByName, addedByUuid, source, addedAt)
167-
.thenAccept(success -> {
155+
// 新逻辑:只使用玩家名添加到白名单,UUID留空等玩家登录时补充
156+
logger.info("添加玩家到白名单(仅用户名): {}", name);
157+
158+
CompletableFuture<Boolean> addFuture = whitelistManager.addPlayerByNameOnly(name, addedByName, addedByUuid, source, addedAt);
159+
160+
// 处理添加结果
161+
addFuture.thenAccept(success -> {
168162
if (success) {
169-
// 创建同步任务
170-
syncTaskManager.scheduleAddPlayer(uuid, name);
163+
// 创建同步任务(使用玩家名)
164+
syncTaskManager.scheduleAddPlayer(null, name);
171165

172166
JsonObject result = new JsonObject();
173-
result.addProperty("uuid", uuid);
174167
result.addProperty("name", name);
175168
result.addProperty("added", true);
176-
result.addProperty("uuid_generated", providedUuid == null || providedUuid.trim().isEmpty());
169+
result.addProperty("uuid_pending", true); // 表示UUID将在玩家登录时补充
170+
result.addProperty("message", "玩家已添加到白名单,UUID将在首次登录时自动补充");
177171

178172
sendJsonResponse(response, 201, ApiResponse.success(result, "玩家添加成功"));
179173
} else {
180-
sendJsonResponse(response, 400, ApiResponse.badRequest("添加玩家失败,可能已存在"));
174+
sendJsonResponse(response, 409, ApiResponse.error("玩家已在白名单中或添加失败"));
181175
}
182176
})
183177
.exceptionally(throwable -> {
184-
logger.error("添加玩家失败: {} ({})", name, uuid, throwable);
178+
logger.error("添加玩家失败: {}", name, throwable);
185179
sendJsonResponse(response, 500, ApiResponse.error("添加玩家失败"));
186180
return null;
187181
})

0 commit comments

Comments
 (0)