Compare commits

...

6 Commits

Author SHA1 Message Date
阿斌
ed84a4f718
Pre Merge pull request !36 from 阿斌/N/A 2025-11-22 12:58:19 +00:00
648540858
321c7e2600
Merge pull request #2019 from ShuiFan0/docker
修改jwk,增强功能:读取密钥时依旧以旧行为(读取classpath)为主,增加警告;增加读取失败时自动创建随机密钥并持久化。
2025-11-22 20:57:10 +08:00
648540858
288cb6eb84
Merge pull request #2036 from tkdg/master
在线状态判断,用错变量
2025-11-21 21:08:28 +08:00
tkdg
7b9c86c2a8
在线状态判断,用错变量
Device device = deviceService.getDeviceByDeviceId(deviceId);
Assert.notNull(deviceId, "设备不存在");

Device device = deviceService.getDeviceByDeviceId(deviceId);
Assert.notNull(device, "设备不存在");
2025-11-20 21:30:38 +08:00
氵帆
3149d9c154 修改jwk,增强功能:读取密钥时依旧以旧行为(读取classpath)为主,增加警告;增加读取失败时自动创建随机密钥并持久化。 2025-11-11 16:51:11 +08:00
阿斌
da98101aac
update src/main/resources/civilCode.csv.
行政规划错误。江苏南通海门市,修改为海门区,浙江杭州删除下城区、江干区,新增钱塘区,临平区

Signed-off-by: 阿斌 <38912748@qq.com>
2024-12-15 08:58:42 +00:00
8 changed files with 134 additions and 61 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
#确保文件名一致
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 jakarta.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 {
/*

View File

@ -89,7 +89,7 @@ public class PlayController {
Assert.notNull(channelId, "通道国标编号不可为NULL");
// 获取可用的zlm
Device device = deviceService.getDeviceByDeviceId(deviceId);
Assert.notNull(deviceId, "设备不存在");
Assert.notNull(device, "设备不存在");
DeviceChannel channel = deviceChannelService.getOne(deviceId, channelId);
Assert.notNull(channel, "通道不存在");

View File

@ -861,7 +861,7 @@
320623,如东县,3206
320681,启东市,3206
320682,如皋市,3206
320684,海门,3206
320684,海门,3206
320685,海安市,3206
3207,连云港市,32
320703,连云区,3207
@ -918,8 +918,6 @@
33,浙江省,
3301,杭州市,33
330102,上城区,3301
330103,下城区,3301
330104,江干区,3301
330105,拱墅区,3301
330106,西湖区,3301
330108,滨江区,3301
@ -927,6 +925,8 @@
330110,余杭区,3301
330111,富阳区,3301
330112,临安区,3301
330113,临平区,3301
330114,钱塘区,3301
330122,桐庐县,3301
330127,淳安县,3301
330182,建德市,3301

1 编号 名称 上级
861 320623 如东县 3206
862 320681 启东市 3206
863 320682 如皋市 3206
864 320684 海门市 海门区 3206
865 320685 海安市 3206
866 3207 连云港市 32
867 320703 连云区 3207
918 33 浙江省
919 3301 杭州市 33
920 330102 上城区 3301
330103 下城区 3301
330104 江干区 3301
921 330105 拱墅区 3301
922 330106 西湖区 3301
923 330108 滨江区 3301
925 330110 余杭区 3301
926 330111 富阳区 3301
927 330112 临安区 3301
928 330113 临平区 3301
929 330114 钱塘区 3301
930 330122 桐庐县 3301
931 330127 淳安县 3301
932 330182 建德市 3301