diff --git a/web-front/src/utils/playgroundApi.js b/web-front/src/utils/playgroundApi.js index 566f87f..ddb4a21 100644 --- a/web-front/src/utils/playgroundApi.js +++ b/web-front/src/utils/playgroundApi.js @@ -5,6 +5,15 @@ const axiosInstance = axios.create({ withCredentials: true // 重要:允许跨域请求携带cookie }); +// 请求拦截器:将存储的Token添加到Authorization请求头 +axiosInstance.interceptors.request.use(config => { + const token = localStorage.getItem('playground_token'); + if (token) { + config.headers['Authorization'] = `Bearer ${token}`; + } + return config; +}); + /** * 演练场API服务 */ @@ -30,7 +39,12 @@ export const playgroundApi = { async login(password) { try { const response = await axiosInstance.post('/v2/playground/login', { password }); - return response.data; + const data = response.data; + // 登录成功时从响应中提取并保存Token + if ((data.code === 200 || data.success) && data.data?.token) { + localStorage.setItem('playground_token', data.data.token); + } + return data; } catch (error) { throw new Error(error.response?.data?.error || error.message || '登录失败'); } diff --git a/web-front/src/views/Playground.vue b/web-front/src/views/Playground.vue index 4f35206..42b79f2 100644 --- a/web-front/src/views/Playground.vue +++ b/web-front/src/views/Playground.vue @@ -1146,10 +1146,11 @@ function parseById(shareLinkInfo, http, logger) { const isAuthed = res.data.authed || res.data.public; authed.value = isAuthed; - // 如果后端session已失效,清除localStorage + // 如果后端认证已失效,清除localStorage中的认证信息 if (!isAuthed && savedAuth === 'true') { localStorage.removeItem('playground_authed'); localStorage.removeItem('playground_auth_time'); + localStorage.removeItem('playground_token'); } return isAuthed; diff --git a/web-service/src/main/java/cn/qaiu/lz/web/config/PlaygroundConfig.java b/web-service/src/main/java/cn/qaiu/lz/web/config/PlaygroundConfig.java index 1f7c041..2049bc5 100644 --- a/web-service/src/main/java/cn/qaiu/lz/web/config/PlaygroundConfig.java +++ b/web-service/src/main/java/cn/qaiu/lz/web/config/PlaygroundConfig.java @@ -4,6 +4,10 @@ import io.vertx.core.json.JsonObject; import lombok.Data; import lombok.extern.slf4j.Slf4j; +import java.security.SecureRandom; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + /** * JS演练场配置 * @@ -12,11 +16,21 @@ import lombok.extern.slf4j.Slf4j; @Data @Slf4j public class PlaygroundConfig { - + + /** Token有效期:24小时 */ + private static final long TOKEN_EXPIRY_MS = 24 * 60 * 60 * 1000L; + + private static final SecureRandom SECURE_RANDOM = new SecureRandom(); + /** * 单例实例 */ private static PlaygroundConfig instance; + + /** + * 已颁发的认证Token及其创建时间 + */ + private final Map validTokens = new ConcurrentHashMap<>(); /** * 是否启用演练场 @@ -41,6 +55,39 @@ public class PlaygroundConfig { */ private PlaygroundConfig() { } + + /** + * 生成并存储一个新的认证Token,同时清理过期Token + */ + public String generateToken() { + // 清理过期Token,防止Map无限增长 + long now = System.currentTimeMillis(); + validTokens.entrySet().removeIf(e -> now - e.getValue() > TOKEN_EXPIRY_MS); + // 使用SecureRandom生成32字节的密码学安全Token + byte[] bytes = new byte[32]; + SECURE_RANDOM.nextBytes(bytes); + StringBuilder sb = new StringBuilder(64); + for (byte b : bytes) { + sb.append(String.format("%02x", b)); + } + String token = sb.toString(); + validTokens.put(token, now); + return token; + } + + /** + * 校验Token是否合法且未过期 + */ + public boolean validateToken(String token) { + if (token == null || token.isEmpty()) return false; + Long createdAt = validTokens.get(token); + if (createdAt == null) return false; + if (System.currentTimeMillis() - createdAt > TOKEN_EXPIRY_MS) { + validTokens.remove(token); + return false; + } + return true; + } /** * 获取单例实例 diff --git a/web-service/src/main/java/cn/qaiu/lz/web/controller/PlaygroundApi.java b/web-service/src/main/java/cn/qaiu/lz/web/controller/PlaygroundApi.java index bf9f794..4972d86 100644 --- a/web-service/src/main/java/cn/qaiu/lz/web/controller/PlaygroundApi.java +++ b/web-service/src/main/java/cn/qaiu/lz/web/controller/PlaygroundApi.java @@ -21,7 +21,6 @@ import io.vertx.core.http.HttpServerRequest; import io.vertx.core.http.HttpServerResponse; import io.vertx.core.json.JsonObject; import io.vertx.ext.web.RoutingContext; -import io.vertx.ext.web.Session; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; @@ -47,7 +46,6 @@ public class PlaygroundApi { private static final int MAX_PARSER_COUNT = 100; private static final int MAX_CODE_LENGTH = 128 * 1024; // 128KB 代码长度限制 - private static final String SESSION_AUTH_KEY = "playgroundAuthed"; private final DbService dbService = AsyncServiceUtil.getAsyncServiceInstance(DbService.class); /** @@ -74,14 +72,14 @@ public class PlaygroundApi { return true; } - // 否则检查Session中的认证状态 - Session session = ctx.session(); - if (session == null) { - return false; + // 检查 Authorization: Bearer 请求头 + String authHeader = ctx.request().getHeader("Authorization"); + if (authHeader != null && authHeader.startsWith("Bearer ") && authHeader.length() > 7) { + String token = authHeader.substring(7).trim(); + return config.validateToken(token); } - Boolean authed = session.get(SESSION_AUTH_KEY); - return authed != null && authed; + return false; } /** @@ -116,12 +114,8 @@ public class PlaygroundApi { try { PlaygroundConfig config = PlaygroundConfig.getInstance(); - // 如果是公开模式,直接成功 + // 如果是公开模式,直接成功(不需要token) if (config.isPublic()) { - Session session = ctx.session(); - if (session != null) { - session.put(SESSION_AUTH_KEY, true); - } promise.complete(JsonResult.success("公开模式,无需密码").toJsonObject()); return promise.future(); } @@ -137,11 +131,9 @@ public class PlaygroundApi { // 验证密码 if (config.getPassword().equals(password)) { - Session session = ctx.session(); - if (session != null) { - session.put(SESSION_AUTH_KEY, true); - } - promise.complete(JsonResult.success("登录成功").toJsonObject()); + String token = config.generateToken(); + JsonObject tokenData = new JsonObject().put("token", token); + promise.complete(JsonResult.data(tokenData).toJsonObject()); } else { promise.complete(JsonResult.error("密码错误").toJsonObject()); }