修改jwk,增强功能:读取密钥时依旧以旧行为(读取classpath)为主,增加警告;增加读取失败时自动创建随机密钥并持久化。

This commit is contained in:
氵帆
2025-11-11 16:51:11 +08:00
parent 6ae7bdc8e0
commit 3149d9c154
6 changed files with 130 additions and 57 deletions

1
.gitignore vendored
View File

@@ -30,3 +30,4 @@ hs_err_pid*
certificates
/.vs
/docker/volumes
/docker/wvp/config/jwk.json

View File

@@ -1,6 +1,6 @@
可以在当前目录下:
使用`docker compose up -d`直接运行。
使用`docker compose up -d -build -force-recreate`强制重新构建所有服务的镜像并删除旧容器重新运行
使用`docker compose up -d --build --force-recreate`强制重新构建所有服务的镜像并删除旧容器重新运行
`.env`用来配置环境变量,在这里配好之后,其它的配置会自动联动的。

View File

@@ -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:

View File

@@ -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
<><C8B7><EFBFBD>ļ<EFBFBD><C4BC><EFBFBD>һ<EFBFBD><D2BB>
RUN mv wvp-pro-*.jar wvp.jar

View File

@@ -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

View File

@@ -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<JsonWebKey> 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 JSONjose4j的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<String, Object> extra) {
try {
/*