From 3149d9c154828c204db41bdbe367397921c7bc3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B0=B5=E5=B8=86?= <595835443@qq.com> Date: Tue, 11 Nov 2025 16:51:11 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9jwk=EF=BC=8C=E5=A2=9E?= =?UTF-8?q?=E5=BC=BA=E5=8A=9F=E8=83=BD=EF=BC=9A=E8=AF=BB=E5=8F=96=E5=AF=86?= =?UTF-8?q?=E9=92=A5=E6=97=B6=E4=BE=9D=E6=97=A7=E4=BB=A5=E6=97=A7=E8=A1=8C?= =?UTF-8?q?=E4=B8=BA=EF=BC=88=E8=AF=BB=E5=8F=96classpath=EF=BC=89=E4=B8=BA?= =?UTF-8?q?=E4=B8=BB=EF=BC=8C=E5=A2=9E=E5=8A=A0=E8=AD=A6=E5=91=8A=EF=BC=9B?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E8=AF=BB=E5=8F=96=E5=A4=B1=E8=B4=A5=E6=97=B6?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E5=88=9B=E5=BB=BA=E9=9A=8F=E6=9C=BA=E5=AF=86?= =?UTF-8?q?=E9=92=A5=E5=B9=B6=E6=8C=81=E4=B9=85=E5=8C=96=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + docker/README.md | 2 +- docker/docker-compose.yml | 1 + docker/wvp/Dockerfile | 3 +- docker/wvp/wvp/application-docker.yml | 1 + .../iot/vmp/conf/security/JwtUtils.java | 179 ++++++++++++------ 6 files changed, 130 insertions(+), 57 deletions(-) diff --git a/.gitignore b/.gitignore index c937a5c05..2b17a4b09 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,4 @@ hs_err_pid* certificates /.vs /docker/volumes +/docker/wvp/config/jwk.json diff --git a/docker/README.md b/docker/README.md index a7d0bcd3f..7dddd9ec3 100644 --- a/docker/README.md +++ b/docker/README.md @@ -1,6 +1,6 @@ 可以在当前目录下: 使用`docker compose up -d`直接运行。 -使用`docker compose up -d -build -force-recreate`强制重新构建所有服务的镜像并删除旧容器重新运行 +使用`docker compose up -d --build --force-recreate`强制重新构建所有服务的镜像并删除旧容器重新运行 `.env`用来配置环境变量,在这里配好之后,其它的配置会自动联动的。 diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 9758161c0..5119cbe44 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -96,6 +96,7 @@ services: - polaris-mysql - polaris-media volumes: + - ./wvp/config:/opt/wvp/config - ./wvp/wvp/:/opt/ylcx/wvp/ - ./logs/wvp:/opt/wvp/logs/ environment: diff --git a/docker/wvp/Dockerfile b/docker/wvp/Dockerfile index e66b4cda4..52e62d2fb 100644 --- a/docker/wvp/Dockerfile +++ b/docker/wvp/Dockerfile @@ -50,12 +50,13 @@ RUN java -version && javac -version RUN apt-get update && \ apt-get install -y maven && \ rm -rf /var/lib/apt/lists/* - + COPY . /build WORKDIR /build RUN ls && mvn clean package -Dmaven.test.skip=true WORKDIR /build/target +#ȷļһ RUN mv wvp-pro-*.jar wvp.jar diff --git a/docker/wvp/wvp/application-docker.yml b/docker/wvp/wvp/application-docker.yml index 04eeff8df..ada29f6f3 100644 --- a/docker/wvp/wvp/application-docker.yml +++ b/docker/wvp/wvp/application-docker.yml @@ -135,6 +135,7 @@ user-settings: # - http://127.0.0.1:8080 # - http://0.0.0.0:8080 # - ${NGINX_HOST} + jwkFile: classpath:xxxxxxxxxxx.json logging: config: classpath:logback-spring.xml diff --git a/src/main/java/com/genersoft/iot/vmp/conf/security/JwtUtils.java b/src/main/java/com/genersoft/iot/vmp/conf/security/JwtUtils.java index b833c748f..04bb19423 100644 --- a/src/main/java/com/genersoft/iot/vmp/conf/security/JwtUtils.java +++ b/src/main/java/com/genersoft/iot/vmp/conf/security/JwtUtils.java @@ -27,14 +27,18 @@ import org.springframework.stereotype.Component; import javax.annotation.Resource; import java.io.BufferedReader; import java.io.File; +import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.time.LocalDateTime; import java.time.ZoneOffset; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; @Slf4j @Component @@ -90,73 +94,138 @@ public class JwtUtils implements InitializingBean { } /** - * 创建密钥对 - * - * @throws JoseException JoseException + * 创建密钥对(修复所有bug+classpath警告+密钥持久化) */ private RsaJsonWebKey generateRsaJsonWebKey() throws JoseException { - RsaJsonWebKey rsaJsonWebKey = null; - try { - String jwkFile = userSetting.getJwkFile(); - InputStream inputStream = null; - if (jwkFile.startsWith("classpath:")){ - String filePath = jwkFile.substring("classpath:".length()); - ClassPathResource civilCodeFile = new ClassPathResource(filePath); - if (civilCodeFile.exists()) { - inputStream = civilCodeFile.getInputStream(); - } - }else { - File civilCodeFile = new File(userSetting.getCivilCodeFile()); - if (civilCodeFile.exists()) { - inputStream = Files.newInputStream(civilCodeFile.toPath()); - } + // 前置校验:避免空指针(防止userSetting未初始化或jwkFile未配置) + if (userSetting == null) { + log.error("[API AUTH] userSetting 未初始化!"); + return createDefaultRsaKey(); + } + String jwkFile = userSetting.getJwkFile(); + if (jwkFile == null || jwkFile.trim().isEmpty()) { + log.error("[API AUTH] JWK文件路径未配置!"); + return createDefaultRsaKey(); + } - } - if (inputStream == null ) { - log.warn("[API AUTH] 读取jwk.json失败,文件不存在,将使用新生成的随机RSA密钥对"); - // 生成一个RSA密钥对,该密钥对将用于JWT的签名和验证,包装在JWK中 - rsaJsonWebKey = RsaJwkGenerator.generateJwk(2048); - // 给JWK一个密钥ID - rsaJsonWebKey.setKeyId(keyId); - return rsaJsonWebKey; - } - BufferedReader inputStreamReader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)); - int index = -1; - String line; - StringBuilder content = new StringBuilder(); - while ((line = inputStreamReader.readLine()) != null) { - content.append(line); - index ++; - if (index == 0) { - continue; - } - } - inputStreamReader.close(); - inputStream.close(); + // 尝试读取JWK文件(自动处理classpath/本地文件,用try-with-resources自动关流,无泄露) + try (InputStream inputStream = getJwkInputStream(jwkFile); + BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) { - - String jwkJson = content.toString(); + // 读取JSON(不跳过任何行,修复原bug) + String jwkJson = reader.lines().collect(Collectors.joining()); JsonWebKeySet jsonWebKeySet = new JsonWebKeySet(jwkJson); List jsonWebKeys = jsonWebKeySet.getJsonWebKeys(); - if (!jsonWebKeys.isEmpty()) { - JsonWebKey jsonWebKey = jsonWebKeys.get(0); + + // 筛选:取第一个有效的RSA私钥(签名需要私钥,避免后续报错) + for (JsonWebKey jsonWebKey : jsonWebKeys) { if (jsonWebKey instanceof RsaJsonWebKey) { - rsaJsonWebKey = (RsaJsonWebKey) jsonWebKey; + RsaJsonWebKey rsaKey = (RsaJsonWebKey) jsonWebKey; + // 校验是否包含私钥 + if (rsaKey.getPrivateKey() != null) { + log.info("[API AUTH] 从JWK文件读取RSA密钥成功,keyId: {}", rsaKey.getKeyId()); + return rsaKey; + } } } - } catch (Exception ignore) {} - if (rsaJsonWebKey == null) { - log.warn("[API AUTH] 读取jwk.json失败,获取内容失败,将使用新生成的随机RSA密钥对"); - // 生成一个RSA密钥对,该密钥对将用于JWT的签名和验证,包装在JWK中 - rsaJsonWebKey = RsaJwkGenerator.generateJwk(2048); - // 给JWK一个密钥ID - rsaJsonWebKey.setKeyId(keyId); - }else { - log.info("[API AUTH] 读取jwk.json成功"); + log.error("[API AUTH] JWK文件中无有效RSA私钥(仅公钥无法签名JWT)"); + + } catch (IOException e) { + log.error("[API AUTH] 读取JWK文件失败(路径:{})", jwkFile, e); + } catch (Exception e) { + log.error("[API AUTH] 解析JWK文件失败(JSON格式错误或密钥无效)", e); } - return rsaJsonWebKey; + + // 所有失败场景:生成默认密钥并持久化(避免重启失效) + return createAndPersistDefaultRsaKey(jwkFile); } + /** + * 获取JWK文件输入流(支持classpath/本地文件,classpath读取加安全警告) + */ + private InputStream getJwkInputStream(String jwkFile) throws IOException { + if (jwkFile.startsWith("classpath:")) { + String filePath = jwkFile.substring("classpath:".length()); + ClassPathResource resource = new ClassPathResource(filePath); + if (resource.exists()) { + // 关键:classpath读取时打印安全警告,提醒用户确认密钥来源 + log.warn("[API AUTH] 从classpath读取内置JWK文件:{}!请确认该密钥是您自己签发的," + + "classpath内置密钥存在泄露风险,生产环境建议改用外部文件配置", filePath); + return resource.getInputStream(); + } + // throw new IOException("classpath下JWK文件不存在:" + filePath); + } + { + File file = determinePersistPath(jwkFile).toFile();// 外部配置与classpath失败场景下 + if (file.exists() && file.canRead()) { + log.debug("[API AUTH] 从本地文件读取JWK文件:{}", file.getAbsolutePath()); + return Files.newInputStream(file.toPath()); + } + throw new IOException("本地JWK文件不存在或无读取权限:" + file.getAbsolutePath()); + } + } + + /** + * 生成默认RSA密钥(单独抽取,修复之前漏写的问题) + */ + private RsaJsonWebKey createDefaultRsaKey() throws JoseException { + RsaJsonWebKey defaultKey = RsaJwkGenerator.generateJwk(4096); + defaultKey.setKeyId(keyId); + log.warn("[API AUTH] 使用默认生成的RSA密钥(未持久化,重启会失效),keyId: {}", defaultKey.getKeyId()); + return defaultKey; + } + + /** + * 生成默认RSA密钥并持久化到文件(修复原重复代码,避免重启失效) + */ + private RsaJsonWebKey createAndPersistDefaultRsaKey(String configJwkFile) throws JoseException { + // 1. 生成4096位RSA密钥(原2048位升级,更安全) + RsaJsonWebKey defaultKey = RsaJwkGenerator.generateJwk(4096); + defaultKey.setKeyId(keyId); // keyId配置 + + // 2. 确定持久化路径:优先用户配置的非classpath路径,否则用默认外部路径 + Path persistPath = determinePersistPath(configJwkFile); + if (persistPath == null) { + log.warn("[API AUTH] 生成默认RSA密钥(keyId: {}),但配置路径是classpath(只读)!" + + "服务重启后密钥会失效,请修改jwkFile为外部可写路径(如:/opt/config/jwk.json)", defaultKey.getKeyId()); + return defaultKey; + } + + // 3. 保存密钥到文件(标准JWK Set格式,下次启动可直接读取) + try { + // 自动创建父目录(比如./config不存在时会自动建) + Files.createDirectories(persistPath.getParent()); + // 构建标准JWK Set JSON(jose4j的toString()自带正确格式) + JsonWebKeySet jwkSet = new JsonWebKeySet(defaultKey); + String jwkJson = jwkSet.toJson(JsonWebKey.OutputControlLevel.INCLUDE_PRIVATE); + // 写入文件(覆盖已有文件,避免重复) + Files.writeString(persistPath, jwkJson, StandardCharsets.UTF_8); + log.info("[API AUTH] 生成默认RSA密钥(keyId: {})并持久化到:{}", + defaultKey.getKeyId(), persistPath.toAbsolutePath()); + } catch (IOException e) { + log.error("[API AUTH] 生成默认RSA密钥成功,但持久化失败(路径:{})!服务重启后密钥会失效", + persistPath.toAbsolutePath(), e); + } + + return defaultKey; + } + + /** + * 确定密钥持久化路径(兼容classpath只读场景) + */ + private Path determinePersistPath(String configJwkFile) { + // 若配置路径不是classpath,直接用用户配置的路径(外部可写) + if (!configJwkFile.startsWith("classpath:")) { + return Paths.get(configJwkFile); + } + // 若配置是classpath,保存到默认外部路径:./config/jwk.json(项目根目录下的config文件夹) + Path defaultPath = Paths.get("config", "jwk.json"); + log.warn("[API AUTH] 配置的jwkFile是classpath路径(只读),默认密钥将保存到外部路径:{}", + defaultPath.toAbsolutePath()); + return defaultPath; + } + + public static String createToken(String username, Long expirationTime, Map extra) { try { /*