From 937d95ae74a40f956db52d609626f13b57e9a1ab Mon Sep 17 00:00:00 2001 From: OpenCode Date: Mon, 27 Jul 2026 20:40:56 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20SaySS=E4=BB=A3=E7=90=86=E6=A8=A1?= =?UTF-8?q?=E5=9D=97=E5=90=8E=E7=AB=AF=E9=9B=86=E6=88=90=20-=20=E7=99=BB?= =?UTF-8?q?=E5=BD=95token=E5=AD=98=E5=BA=93/=E7=99=BD=E5=90=8D=E5=8D=95?= =?UTF-8?q?=E5=A2=9E=E5=88=A0=E5=90=8C=E6=AD=A5SaySS/=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E8=80=85=E7=94=A8=E6=88=B7ID?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AgentInfoEntity 新增 sayssAuthData 字段存储 SaySS auth_data token - AgentWhitelistEntity 新增 sayssTrustedIpId、addedByUserId 字段,addedByName 虚拟字段 - SayssService 实现 AES-CBC 加密中间件调用,支持 loginAndSave/checkTokenValid/addTrustedIp/deleteTrustedIp - AdminAgentInfoController 新增 loginSayss、checkToken 接口 - AdminAgentWhitelistController 删除时先从 SaySS 删除成功后再删本地,分页关联查出 agentName 和 addedByName - AppAgentInfoController addWhitelist 先调 SaySS 添加成功后保存本地,记录当前 App 用户 ID - application.yml 新增 sayss 配置 --- .../admin/AdminAgentInfoController.java | 26 ++ .../admin/AdminAgentWhitelistController.java | 40 +++ .../app/AppAgentInfoController.java | 17 +- .../modules/agent/entity/AgentInfoEntity.java | 3 + .../agent/entity/AgentWhitelistEntity.java | 9 + .../modules/agent/service/SayssService.java | 261 ++++++++++++++++++ src/main/resources/application.yml | 9 +- 7 files changed, 363 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/cool/modules/agent/service/SayssService.java diff --git a/src/main/java/com/cool/modules/agent/controller/admin/AdminAgentInfoController.java b/src/main/java/com/cool/modules/agent/controller/admin/AdminAgentInfoController.java index 9df1281..a0e275e 100644 --- a/src/main/java/com/cool/modules/agent/controller/admin/AdminAgentInfoController.java +++ b/src/main/java/com/cool/modules/agent/controller/admin/AdminAgentInfoController.java @@ -3,19 +3,45 @@ package com.cool.modules.agent.controller.admin; import cn.hutool.json.JSONObject; import com.cool.core.annotation.CoolRestController; import com.cool.core.base.BaseController; +import com.cool.core.request.R; import com.cool.modules.agent.entity.AgentInfoEntity; import com.cool.modules.agent.service.AgentInfoService; +import com.cool.modules.agent.service.SayssService; +import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestAttribute; @RequiredArgsConstructor @Tag(name = "代理管理", description = "代理信息管理") @CoolRestController(api = {"add", "delete", "update", "page", "info"}) public class AdminAgentInfoController extends BaseController { + private final SayssService sayssService; + @Override protected void init(HttpServletRequest request, JSONObject requestParams) { setPageOption(createOp()); } + + @Operation(summary = "登录SaySS") + @PostMapping("/loginSayss") + public R loginSayss(@RequestAttribute JSONObject requestParams) { + Long id = requestParams.getLong("id"); + String authData = sayssService.loginAndSave(id); + if (authData == null) { + return R.error("登录SaySS失败,请检查用户名密码"); + } + return R.ok(); + } + + @Operation(summary = "验证SaySS Token") + @PostMapping("/checkToken") + public R checkToken(@RequestAttribute JSONObject requestParams) { + Long id = requestParams.getLong("id"); + boolean valid = sayssService.checkTokenValid(id); + return R.ok(new JSONObject().set("valid", valid)); + } } diff --git a/src/main/java/com/cool/modules/agent/controller/admin/AdminAgentWhitelistController.java b/src/main/java/com/cool/modules/agent/controller/admin/AdminAgentWhitelistController.java index 37f6bab..61f1be6 100644 --- a/src/main/java/com/cool/modules/agent/controller/admin/AdminAgentWhitelistController.java +++ b/src/main/java/com/cool/modules/agent/controller/admin/AdminAgentWhitelistController.java @@ -1,15 +1,25 @@ package com.cool.modules.agent.controller.admin; +import cn.hutool.core.convert.Convert; import cn.hutool.json.JSONObject; import com.cool.core.annotation.CoolRestController; import com.cool.core.base.BaseController; +import com.cool.core.request.R; import com.cool.modules.agent.entity.AgentInfoEntity; import com.cool.modules.agent.entity.AgentWhitelistEntity; import com.cool.modules.agent.service.AgentInfoService; import com.cool.modules.agent.service.AgentWhitelistService; +import com.cool.modules.agent.service.SayssService; +import com.cool.modules.user.entity.UserInfoEntity; +import com.cool.modules.user.service.UserInfoService; +import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.servlet.http.HttpServletRequest; +import java.util.Map; import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestAttribute; @RequiredArgsConstructor @Tag(name = "白名单管理", description = "代理白名单管理") @@ -17,6 +27,8 @@ import lombok.RequiredArgsConstructor; public class AdminAgentWhitelistController extends BaseController { private final AgentInfoService agentInfoService; + private final UserInfoService userInfoService; + private final SayssService sayssService; @Override protected void init(HttpServletRequest request, JSONObject requestParams) { @@ -26,6 +38,34 @@ public class AdminAgentWhitelistController extends BaseController params, + @RequestAttribute() JSONObject requestParams) { + Long[] ids = Convert.toLongArray(getIds(params)); + for (Long id : ids) { + AgentWhitelistEntity entity = service.getById(id); + if (entity == null) continue; + boolean sayssDeleted; + if (entity.getSayssTrustedIpId() != null) { + sayssDeleted = sayssService.deleteTrustedIp(entity.getAgentInfoId(), entity.getSayssTrustedIpId()); + } else { + sayssDeleted = sayssService.deleteTrustedIpByIp(entity.getAgentInfoId(), entity.getIpAddress()); + } + if (!sayssDeleted) { + return R.error("从SaySS删除白名单失败,IP: " + entity.getIpAddress() + ",请检查代理账号是否正确"); + } + } + service.delete(requestParams, ids); + return R.ok(); + } } diff --git a/src/main/java/com/cool/modules/agent/controller/app/AppAgentInfoController.java b/src/main/java/com/cool/modules/agent/controller/app/AppAgentInfoController.java index 3f3c8da..9d3ca65 100644 --- a/src/main/java/com/cool/modules/agent/controller/app/AppAgentInfoController.java +++ b/src/main/java/com/cool/modules/agent/controller/app/AppAgentInfoController.java @@ -3,20 +3,24 @@ package com.cool.modules.agent.controller.app; import cn.hutool.json.JSONObject; import com.cool.core.annotation.CoolRestController; import com.cool.core.request.R; +import com.cool.core.util.CoolSecurityUtil; import com.cool.modules.agent.entity.AgentWhitelistEntity; import com.cool.modules.agent.service.AgentInfoService; import com.cool.modules.agent.service.AgentWhitelistService; +import com.cool.modules.agent.service.SayssService; import com.mybatisflex.core.query.QueryWrapper; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestAttribute; import static com.cool.modules.agent.entity.table.AgentWhitelistEntityTableDef.AGENT_WHITELIST_ENTITY; +@Slf4j @RequiredArgsConstructor @Tag(name = "代理", description = "App端代理") @CoolRestController(api = {"list"}) @@ -24,6 +28,7 @@ public class AppAgentInfoController { private final AgentInfoService agentInfoService; private final AgentWhitelistService agentWhitelistService; + private final SayssService sayssService; @Operation(summary = "代理列表") @GetMapping("/list") @@ -58,13 +63,23 @@ public class AppAgentInfoController { @Operation(summary = "添加白名单IP") @PostMapping("/addWhitelist") - public R addWhitelist(@RequestAttribute JSONObject requestParams) { + public R addWhitelist(@RequestAttribute JSONObject requestParams, HttpServletRequest request) { Long agentInfoId = requestParams.getLong("agentInfoId"); String ipAddress = requestParams.getStr("ipAddress"); + Long sayssTrustedIpId = sayssService.addTrustedIp(agentInfoId, ipAddress); + if (sayssTrustedIpId == null) { + log.warn("SaySS添加白名单失败,agentInfoId={}, ip={}", agentInfoId, ipAddress); + return R.error("添加白名单到SaySS失败,请检查代理账号是否正确"); + } + + Long addedByUserId = CoolSecurityUtil.getCurrentUserId(); + AgentWhitelistEntity entity = new AgentWhitelistEntity(); entity.setAgentInfoId(agentInfoId); entity.setIpAddress(ipAddress); + entity.setAddedByUserId(addedByUserId); + entity.setSayssTrustedIpId(sayssTrustedIpId); agentWhitelistService.save(entity); return R.ok(entity); diff --git a/src/main/java/com/cool/modules/agent/entity/AgentInfoEntity.java b/src/main/java/com/cool/modules/agent/entity/AgentInfoEntity.java index 78e9b98..edb4b9c 100644 --- a/src/main/java/com/cool/modules/agent/entity/AgentInfoEntity.java +++ b/src/main/java/com/cool/modules/agent/entity/AgentInfoEntity.java @@ -28,4 +28,7 @@ public class AgentInfoEntity extends TenantEntity { @ColumnDefine(comment = "Token状态 0:失效 1:生效", defaultValue = "0") private Integer tokenStatus; + + @ColumnDefine(comment = "SaySS认证Token(auth_data)") + private String sayssAuthData; } diff --git a/src/main/java/com/cool/modules/agent/entity/AgentWhitelistEntity.java b/src/main/java/com/cool/modules/agent/entity/AgentWhitelistEntity.java index 772d6a2..4023d71 100644 --- a/src/main/java/com/cool/modules/agent/entity/AgentWhitelistEntity.java +++ b/src/main/java/com/cool/modules/agent/entity/AgentWhitelistEntity.java @@ -17,6 +17,15 @@ public class AgentWhitelistEntity extends TenantEntity { @ColumnDefine(comment = "IP地址", notNull = true) private String ipAddress; + @ColumnDefine(comment = "添加者用户ID") + private Long addedByUserId; + + @ColumnDefine(comment = "SaySS白名单记录ID") + private Long sayssTrustedIpId; + @com.mybatisflex.annotation.Column(ignore = true) private String agentName; + + @com.mybatisflex.annotation.Column(ignore = true) + private String addedByName; } diff --git a/src/main/java/com/cool/modules/agent/service/SayssService.java b/src/main/java/com/cool/modules/agent/service/SayssService.java new file mode 100644 index 0000000..df1c480 --- /dev/null +++ b/src/main/java/com/cool/modules/agent/service/SayssService.java @@ -0,0 +1,261 @@ +package com.cool.modules.agent.service; + +import cn.hutool.core.codec.Base64; +import cn.hutool.core.util.RandomUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.Mode; +import cn.hutool.crypto.Padding; +import cn.hutool.crypto.symmetric.AES; +import cn.hutool.http.HttpRequest; +import cn.hutool.http.HttpResponse; +import cn.hutool.json.JSONArray; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import com.cool.modules.agent.entity.AgentInfoEntity; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +@ConfigurationProperties(prefix = "sayss") +@Data +@RequiredArgsConstructor +public class SayssService { + + private String middlewareUrl; + private String middlewareKey; + private String middlewarePath; + private boolean enabled; + + private final AgentInfoService agentInfoService; + + private String encryptPath(String path, String iv) { + AES aes = new AES(Mode.CBC, Padding.PKCS5Padding, middlewareKey.getBytes(), iv.getBytes()); + return aes.encryptBase64(path); + } + + private String[] buildEncryptedUrlWithIv(String path) { + String iv = RandomUtil.randomString("0123456789abcdef", 16); + String encrypted = encryptPath(path, iv); + String encoded = Base64.encode(encrypted); + return new String[]{middlewareUrl + middlewarePath + "/" + encoded, iv}; + } + + private String login(String email, String password) { + if (!enabled) return null; + try { + String[] urlAndIv = buildEncryptedUrlWithIv("/passport/auth/login"); + JSONObject body = new JSONObject(); + body.set("email", email); + body.set("password", password); + HttpResponse resp = HttpRequest.post(urlAndIv[0]) + .header("Content-Type", "application/json") + .header("Accept", "application/json, text/plain, */*") + .header("X-IV", urlAndIv[1]) + .body(body.toString()) + .timeout(10000).execute(); + JSONObject json = JSONUtil.parseObj(resp.body()); + if ("success".equals(json.getStr("status")) && json.get("data") != null) { + return json.getJSONObject("data").getStr("auth_data"); + } + log.error("SaySS登录失败: {}", json.getStr("message")); + } catch (Exception e) { + log.error("SaySS登录异常: {}", e.getMessage()); + } + return null; + } + + public String loginAndSave(Long agentInfoId) { + if (!enabled) return null; + AgentInfoEntity agent = (AgentInfoEntity) agentInfoService.info(agentInfoId); + if (agent == null || StrUtil.isBlank(agent.getUsername()) || StrUtil.isBlank(agent.getPassword())) { + return null; + } + String authData = login(agent.getUsername(), agent.getPassword()); + if (authData != null) { + agent.setSayssAuthData(authData); + agent.setTokenStatus(1); + agentInfoService.update(agent); + } + return authData; + } + + public boolean checkTokenValid(Long agentInfoId) { + if (!enabled) return false; + AgentInfoEntity agent = (AgentInfoEntity) agentInfoService.info(agentInfoId); + if (agent == null || StrUtil.isBlank(agent.getSayssAuthData())) { + return false; + } + try { + String[] urlAndIv = buildEncryptedUrlWithIv("/user/info"); + HttpResponse resp = HttpRequest.get(urlAndIv[0]) + .header("Accept", "application/json, text/plain, */*") + .header("X-IV", urlAndIv[1]) + .header("Authorization", agent.getSayssAuthData()) + .timeout(10000).execute(); + JSONObject json = JSONUtil.parseObj(resp.body()); + if ("未登录或登陆已过期".equals(json.getStr("message"))) { + agent.setSayssAuthData(null); + agent.setTokenStatus(0); + agentInfoService.update(agent); + return false; + } + boolean valid = "success".equals(json.getStr("status")); + if (!valid) { + agent.setTokenStatus(0); + agentInfoService.update(agent); + } + return valid; + } catch (Exception e) { + log.error("SaySS验证Token异常: {}", e.getMessage()); + return false; + } + } + + private JSONObject sayssGet(String authData, String path) { + if (!enabled) return new JSONObject().set("status", "error").set("message", "SaySS未启用"); + try { + String[] urlAndIv = buildEncryptedUrlWithIv(path); + HttpResponse resp = HttpRequest.get(urlAndIv[0]) + .header("Accept", "application/json, text/plain, */*") + .header("X-IV", urlAndIv[1]) + .header("Authorization", authData) + .timeout(10000).execute(); + return JSONUtil.parseObj(resp.body()); + } catch (Exception e) { + log.error("SaySS GET请求失败: {}", e.getMessage()); + return new JSONObject().set("status", "error").set("message", e.getMessage()); + } + } + + private JSONObject sayssPost(String authData, String path, Object body) { + if (!enabled) return new JSONObject().set("status", "error").set("message", "SaySS未启用"); + try { + String[] urlAndIv = buildEncryptedUrlWithIv(path); + HttpResponse resp = HttpRequest.post(urlAndIv[0]) + .header("Content-Type", "application/json") + .header("Accept", "application/json, text/plain, */*") + .header("X-IV", urlAndIv[1]) + .header("Authorization", authData) + .body(body instanceof String ? (String) body : body.toString()) + .timeout(10000).execute(); + return JSONUtil.parseObj(resp.body()); + } catch (Exception e) { + log.error("SaySS POST请求失败: {}", e.getMessage()); + return new JSONObject().set("status", "error").set("message", e.getMessage()); + } + } + + private JSONObject sayssDelete(String authData, String path) { + if (!enabled) return new JSONObject().set("status", "error").set("message", "SaySS未启用"); + try { + String[] urlAndIv = buildEncryptedUrlWithIv(path); + HttpResponse resp = HttpRequest.delete(urlAndIv[0]) + .header("Accept", "application/json, text/plain, */*") + .header("X-IV", urlAndIv[1]) + .header("Authorization", authData) + .timeout(10000).execute(); + return JSONUtil.parseObj(resp.body()); + } catch (Exception e) { + log.error("SaySS DELETE请求失败: {}", e.getMessage()); + return new JSONObject().set("status", "error").set("message", e.getMessage()); + } + } + + private String getAuthData(Long agentInfoId) { + AgentInfoEntity agent = (AgentInfoEntity) agentInfoService.info(agentInfoId); + if (agent == null || StrUtil.isBlank(agent.getUsername()) || StrUtil.isBlank(agent.getPassword())) { + return null; + } + if (StrUtil.isNotBlank(agent.getSayssAuthData())) { + return agent.getSayssAuthData(); + } + String authData = login(agent.getUsername(), agent.getPassword()); + if (authData != null) { + agent.setSayssAuthData(authData); + agent.setTokenStatus(1); + agentInfoService.update(agent); + } + return authData; + } + + public Long addTrustedIp(Long agentInfoId, String ip) { + if (!enabled) { + log.warn("SaySS未启用,跳过添加白名单到SaySS"); + return null; + } + String authData = getAuthData(agentInfoId); + if (authData == null) { + log.error("无法获取SaySS认证信息,agentInfoId={}", agentInfoId); + return null; + } + JSONObject body = new JSONObject(); + body.set("ip", ip); + body.set("ip_type", "home"); + JSONObject result = sayssPost(authData, "/user/subscribe-guard/trusted-ips", body); + if ("success".equals(result.getStr("status")) && result.get("data") != null) { + Object data = result.get("data"); + if (data instanceof JSONObject) { + return ((JSONObject) data).getLong("id"); + } + } + log.error("SaySS添加白名单失败: {}", result.getStr("message")); + return null; + } + + public boolean deleteTrustedIp(Long agentInfoId, Long sayssTrustedIpId) { + if (!enabled) { + log.warn("SaySS未启用,跳过从SaySS删除白名单"); + return true; + } + String authData = getAuthData(agentInfoId); + if (authData == null) { + log.error("无法获取SaySS认证信息,agentInfoId={}", agentInfoId); + return false; + } + if (sayssTrustedIpId == null) { + log.warn("SaySS白名单记录ID为空,无法从SaySS删除"); + return false; + } + JSONObject result = sayssDelete(authData, "/user/subscribe-guard/trusted-ips/" + sayssTrustedIpId); + if ("success".equals(result.getStr("status"))) { + return true; + } + log.error("SaySS删除白名单失败: {}", result.getStr("message")); + return false; + } + + public boolean deleteTrustedIpByIp(Long agentInfoId, String ip) { + if (!enabled) { + log.warn("SaySS未启用,跳过从SaySS删除白名单"); + return true; + } + Long trustedIpId = findTrustedIpId(agentInfoId, ip); + if (trustedIpId == null) { + log.warn("在SaySS中未找到IP: {},可能已被删除", ip); + return true; + } + return deleteTrustedIp(agentInfoId, trustedIpId); + } + + public Long findTrustedIpId(Long agentInfoId, String ip) { + if (!enabled) return null; + String authData = getAuthData(agentInfoId); + if (authData == null) return null; + JSONObject result = sayssGet(authData, "/user/subscribe-guard/trusted-ips"); + if ("success".equals(result.getStr("status")) && result.get("data") != null) { + Object data = result.get("data"); + JSONArray list = data instanceof JSONArray ? (JSONArray) data : new JSONArray().put(data); + for (int i = 0; i < list.size(); i++) { + JSONObject item = list.getJSONObject(i); + if (ip.equals(item.getStr("ip_address"))) { + return item.getLong("id"); + } + } + } + return null; + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 9a16fe1..f62c5f3 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -173,4 +173,11 @@ qinglong: admin-username: wol admin-password: "123456" admin-token: - token: \ No newline at end of file + token: + +# SaySS配置 +sayss: + enabled: true + middleware-url: https://cluster.epayudt.com + middleware-key: 96a6ad9f5e7bf48b + middleware-path: /newsay \ No newline at end of file