feat: SaySS代理模块后端集成 - 登录token存库/白名单增删同步SaySS/添加者用户ID

- 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 配置
This commit is contained in:
OpenCode
2026-07-27 20:40:56 +08:00
parent bf7c79c87f
commit 937d95ae74
7 changed files with 363 additions and 2 deletions
@@ -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<AgentInfoService, AgentInfoEntity> {
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));
}
}
@@ -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<AgentWhitelistService, AgentWhitelistEntity> {
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<AgentWhitelist
if (agent != null) {
entity.setAgentName(agent.getName());
}
if (entity.getAddedByUserId() != null) {
UserInfoEntity user = (UserInfoEntity) userInfoService.info(entity.getAddedByUserId());
if (user != null) {
entity.setAddedByName(user.getNickName());
}
}
}));
}
@Operation(summary = "删除", description = "先从SaySS删除白名单,成功后再从本地删除")
@PostMapping("/delete")
protected R delete(HttpServletRequest request, @RequestBody Map<String, Object> 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();
}
}
@@ -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);
@@ -28,4 +28,7 @@ public class AgentInfoEntity extends TenantEntity<AgentInfoEntity> {
@ColumnDefine(comment = "Token状态 0:失效 1:生效", defaultValue = "0")
private Integer tokenStatus;
@ColumnDefine(comment = "SaySS认证Token(auth_data)")
private String sayssAuthData;
}
@@ -17,6 +17,15 @@ public class AgentWhitelistEntity extends TenantEntity<AgentWhitelistEntity> {
@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;
}
@@ -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;
}
}
+8 -1
View File
@@ -173,4 +173,11 @@ qinglong:
admin-username: wol
admin-password: "123456"
admin-token:
token:
token:
# SaySS配置
sayss:
enabled: true
middleware-url: https://cluster.epayudt.com
middleware-key: 96a6ad9f5e7bf48b
middleware-path: /newsay