22 changed files with 794 additions and 3 deletions
@ -0,0 +1,67 @@ |
|||
package com.zdxt.enforcementcode.common.util.api; |
|||
|
|||
import cn.hutool.core.codec.Base64; |
|||
import cn.hutool.crypto.SecureUtil; |
|||
import cn.hutool.crypto.asymmetric.KeyType; |
|||
import cn.hutool.crypto.asymmetric.RSA; |
|||
|
|||
import java.nio.charset.StandardCharsets; |
|||
import java.security.KeyFactory; |
|||
import java.security.PrivateKey; |
|||
import java.security.PublicKey; |
|||
import java.security.Signature; |
|||
import java.security.spec.PKCS8EncodedKeySpec; |
|||
import java.security.spec.X509EncodedKeySpec; |
|||
import java.util.Map; |
|||
import java.util.TreeMap; |
|||
import java.util.stream.Collectors; |
|||
|
|||
public class ApiSignUtil { |
|||
|
|||
private static final String SIGN_ALGORITHM = "SHA256withRSA"; |
|||
|
|||
public static String generateRsaKeyPair() { |
|||
RSA rsa = SecureUtil.rsa(); |
|||
return "publicKey=" + rsa.getPublicKeyBase64() + "\nprivateKey=" + rsa.getPrivateKeyBase64(); |
|||
} |
|||
|
|||
public static String sign(String content, String privateKeyBase64) throws Exception { |
|||
byte[] keyBytes = Base64.decode(privateKeyBase64); |
|||
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes); |
|||
PrivateKey privateKey = KeyFactory.getInstance("RSA").generatePrivate(keySpec); |
|||
|
|||
Signature signature = Signature.getInstance(SIGN_ALGORITHM); |
|||
signature.initSign(privateKey); |
|||
signature.update(content.getBytes(StandardCharsets.UTF_8)); |
|||
return Base64.encode(signature.sign()); |
|||
} |
|||
|
|||
public static boolean verify(String content, String signBase64, String publicKeyBase64) throws Exception { |
|||
byte[] keyBytes = Base64.decode(publicKeyBase64); |
|||
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes); |
|||
PublicKey publicKey = KeyFactory.getInstance("RSA").generatePublic(keySpec); |
|||
|
|||
Signature signature = Signature.getInstance(SIGN_ALGORITHM); |
|||
signature.initVerify(publicKey); |
|||
signature.update(content.getBytes(StandardCharsets.UTF_8)); |
|||
return signature.verify(Base64.decode(signBase64)); |
|||
} |
|||
|
|||
public static String buildSignContent(Map<String, String> params) { |
|||
TreeMap<String, String> sorted = new TreeMap<>(params); |
|||
return sorted.entrySet().stream() |
|||
.filter(e -> e.getValue() != null && !e.getValue().isEmpty()) |
|||
.map(e -> e.getKey() + "=" + e.getValue()) |
|||
.collect(Collectors.joining("&")); |
|||
} |
|||
|
|||
public static String rsaEncrypt(String data, String publicKeyBase64) { |
|||
RSA rsa = SecureUtil.rsa(null, publicKeyBase64); |
|||
return rsa.encryptBase64(data, StandardCharsets.UTF_8, KeyType.PublicKey); |
|||
} |
|||
|
|||
public static String rsaDecrypt(String data, String privateKeyBase64) { |
|||
RSA rsa = SecureUtil.rsa(privateKeyBase64, null); |
|||
return rsa.decryptStr(data, KeyType.PrivateKey, StandardCharsets.UTF_8); |
|||
} |
|||
} |
|||
@ -0,0 +1,16 @@ |
|||
package com.zdxt.enforcementcode.config; |
|||
|
|||
import lombok.Data; |
|||
import org.springframework.boot.context.properties.ConfigurationProperties; |
|||
import org.springframework.stereotype.Component; |
|||
|
|||
@Data |
|||
@Component |
|||
@ConfigurationProperties(prefix = "api-platform") |
|||
public class ApiPlatformConfig { |
|||
|
|||
private boolean enabled = false; |
|||
|
|||
private long timestampExpireSeconds = 300; |
|||
|
|||
} |
|||
@ -0,0 +1,22 @@ |
|||
package com.zdxt.enforcementcode.config; |
|||
|
|||
|
|||
import com.zdxt.enforcementcode.handler.ApiSignInterceptor; |
|||
import lombok.RequiredArgsConstructor; |
|||
import org.springframework.context.annotation.Configuration; |
|||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry; |
|||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; |
|||
|
|||
@Configuration |
|||
@RequiredArgsConstructor |
|||
public class ApiPlatformWebConfig implements WebMvcConfigurer { |
|||
|
|||
private final ApiSignInterceptor apiSignInterceptor; |
|||
|
|||
@Override |
|||
public void addInterceptors(InterceptorRegistry registry) { |
|||
registry.addInterceptor(apiSignInterceptor) |
|||
.addPathPatterns("/external/api/**") |
|||
.order(10); |
|||
} |
|||
} |
|||
@ -0,0 +1,82 @@ |
|||
package com.zdxt.enforcementcode.controller.externalInterface; |
|||
|
|||
import cn.dev33.satoken.annotation.SaIgnore; |
|||
import cn.hutool.core.util.StrUtil; |
|||
import com.zdxt.enforcementcode.domain.EfcodeCpPlan; |
|||
import com.zdxt.enforcementcode.domain.SysApiPlatformConfigEntity; |
|||
import com.zdxt.enforcementcode.domain.bo.EfcodeCpPlanBo; |
|||
import com.zdxt.enforcementcode.domain.bo.externalInterface.LongGangCreateTaskBo; |
|||
import com.zdxt.enforcementcode.domain.vo.EfcodeCpPlanVo; |
|||
import com.zdxt.enforcementcode.domain.vo.externalInterface.LongGangCreateTaskVo; |
|||
import com.zdxt.enforcementcode.service.IEfcodeCpPlanService; |
|||
import com.zdxt.enforcementcode.service.IEfcodeCpTaskService; |
|||
import com.zdxt.enforcementcode.service.ISysApiPlatformConfigService; |
|||
import lombok.RequiredArgsConstructor; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.dromara.common.core.domain.R; |
|||
import org.dromara.common.core.utils.MapstructUtils; |
|||
import org.dromara.common.idempotent.annotation.RepeatSubmit; |
|||
import org.dromara.common.log.annotation.Log; |
|||
import org.dromara.common.log.enums.BusinessType; |
|||
import org.springframework.validation.annotation.Validated; |
|||
import org.springframework.web.bind.annotation.*; |
|||
|
|||
@Slf4j |
|||
@SaIgnore |
|||
@Validated |
|||
@RequiredArgsConstructor |
|||
@RestController |
|||
@RequestMapping("/external/api") |
|||
public class LongGangInterfaceController { |
|||
|
|||
private static final String HEADER_APP_KEY = "X-App-Key"; |
|||
|
|||
private final IEfcodeCpPlanService planService; |
|||
private final IEfcodeCpTaskService taskService; |
|||
private final ISysApiPlatformConfigService sysApiPlatformConfigService; |
|||
|
|||
@SaIgnore |
|||
@RepeatSubmit() |
|||
@Log(title = "外部接口-新增任务", businessType = BusinessType.INSERT) |
|||
@PostMapping("/createTask") |
|||
public R<LongGangCreateTaskVo> createTask( |
|||
@RequestHeader(value = HEADER_APP_KEY, required = false) String appKey, |
|||
@Validated @RequestBody LongGangCreateTaskBo bo) { |
|||
if (StrUtil.isBlank(appKey)) { |
|||
return R.fail("缺少请求头: " + HEADER_APP_KEY); |
|||
} |
|||
|
|||
SysApiPlatformConfigEntity consumer = sysApiPlatformConfigService.findByAppKey(appKey).orElse(null); |
|||
if (consumer == null) { |
|||
return R.fail("无效的 appKey"); |
|||
} |
|||
|
|||
String channelNo = consumer.getChannelNo(); |
|||
if (StrUtil.isBlank(channelNo)) { |
|||
return R.fail("渠道未配置 channelNo"); |
|||
} |
|||
|
|||
EfcodeCpPlanBo planBo = MapstructUtils.convert(bo, EfcodeCpPlanBo.class); |
|||
planBo.setChannelNo(channelNo); |
|||
planBo.setTaskId(""); |
|||
planBo.setReserveStatus(0L); |
|||
planBo.setSystemReserved(0L); |
|||
planBo.setManualReserved(0L); |
|||
planBo.setIsDelete(0L); |
|||
|
|||
planService.insertByBo(planBo); |
|||
|
|||
EfcodeCpPlanVo planVo = planService.queryById(planBo.getId()); |
|||
EfcodeCpPlan plan = MapstructUtils.convert(planVo, EfcodeCpPlan.class); |
|||
String taskId = taskService.forceGenerateTask(plan); |
|||
|
|||
LongGangCreateTaskVo vo = new LongGangCreateTaskVo(); |
|||
vo.setPlanId(planBo.getId()); |
|||
vo.setTaskId(taskId); |
|||
vo.setOutSerialNo(bo.getOutSerialNo()); |
|||
|
|||
log.info("[外部接口] 新增任务, appKey={}, channelNo={}, planId={}, taskId={}", |
|||
appKey, channelNo, planBo.getId(), taskId); |
|||
return R.ok(vo); |
|||
} |
|||
} |
|||
@ -0,0 +1,72 @@ |
|||
package com.zdxt.enforcementcode.domain; |
|||
|
|||
import com.baomidou.mybatisplus.annotation.TableId; |
|||
import com.baomidou.mybatisplus.annotation.TableName; |
|||
import lombok.Data; |
|||
import org.dromara.common.mybatis.core.domain.BaseEntity; |
|||
|
|||
import java.io.Serial; |
|||
|
|||
@Data |
|||
@TableName("sys_api_platform_config") |
|||
public class SysApiPlatformConfigEntity extends BaseEntity { |
|||
|
|||
@Serial |
|||
private static final long serialVersionUID = 1L; |
|||
|
|||
/** |
|||
* 主键ID |
|||
*/ |
|||
@TableId(value = "id") |
|||
private Long id; |
|||
|
|||
/** |
|||
* 渠道名称 |
|||
*/ |
|||
private String channelName; |
|||
|
|||
/** |
|||
* 渠道编号(0=市局,1=龙岗,2=龙华) |
|||
*/ |
|||
private String channelNo; |
|||
|
|||
/** |
|||
* 应用Key |
|||
*/ |
|||
private String appKey; |
|||
|
|||
/** |
|||
* 应用密钥(HMAC模式) |
|||
*/ |
|||
private String appSecret; |
|||
|
|||
/** |
|||
* 平台公钥(RSA) |
|||
*/ |
|||
private String platformPublicKey; |
|||
|
|||
/** |
|||
* 平台私钥(RSA) |
|||
*/ |
|||
private String platformPrivateKey; |
|||
|
|||
/** |
|||
* 调用方公钥(RSA) |
|||
*/ |
|||
private String callerPublicKey; |
|||
|
|||
/** |
|||
* 是否启用签名验证 |
|||
*/ |
|||
private Boolean signEnabled; |
|||
|
|||
/** |
|||
* 是否启用加解密 |
|||
*/ |
|||
private Boolean encryptEnabled; |
|||
|
|||
/** |
|||
* 状态(0正常 1停用) |
|||
*/ |
|||
private String status; |
|||
} |
|||
@ -0,0 +1,68 @@ |
|||
package com.zdxt.enforcementcode.domain.bo.externalInterface; |
|||
|
|||
import com.fasterxml.jackson.annotation.JsonFormat; |
|||
import lombok.Data; |
|||
|
|||
import jakarta.validation.constraints.NotBlank; |
|||
import jakarta.validation.constraints.NotNull; |
|||
import java.util.Date; |
|||
|
|||
@Data |
|||
public class LongGangCreateTaskBo { |
|||
|
|||
@NotNull(message = "计划类别不能为空") |
|||
private Long planType; |
|||
|
|||
@NotNull(message = "计划执行日期不能为空") |
|||
@JsonFormat(pattern = "yyyy-MM-dd") |
|||
private Date planDate; |
|||
|
|||
@NotNull(message = "执行时段不能为空") |
|||
private Long planTime; |
|||
|
|||
@NotBlank(message = "执法事项不能为空") |
|||
private String enforceContent; |
|||
|
|||
@NotBlank(message = "企业名称不能为空") |
|||
private String enterpriseName; |
|||
|
|||
@NotBlank(message = "企业信用代码不能为空") |
|||
private String enterpriseNumber; |
|||
|
|||
private String enterpriseId; |
|||
|
|||
private String contactPerson; |
|||
|
|||
private String contactPhone; |
|||
|
|||
@NotBlank(message = "执法人员姓名不能为空") |
|||
private String enforceUserNames; |
|||
|
|||
private String enforceUserIds; |
|||
|
|||
private String enforceUserCodes; |
|||
|
|||
private String enforceUserTels; |
|||
|
|||
private Long isPublic = 0L; |
|||
|
|||
private Long isUnion = 0L; |
|||
|
|||
@NotBlank(message = "执法单位ID不能为空") |
|||
private String bureauDeptId; |
|||
|
|||
@NotBlank(message = "执法单位名称不能为空") |
|||
private String bureauDeptName; |
|||
|
|||
private String organId; |
|||
|
|||
private String organName; |
|||
|
|||
private String deptId; |
|||
|
|||
private String deptName; |
|||
|
|||
private String remark; |
|||
|
|||
private String outSerialNo; |
|||
} |
|||
@ -0,0 +1,19 @@ |
|||
package com.zdxt.enforcementcode.domain.vo.externalInterface; |
|||
|
|||
import lombok.Data; |
|||
|
|||
import java.io.Serial; |
|||
import java.io.Serializable; |
|||
|
|||
@Data |
|||
public class LongGangCreateTaskVo implements Serializable { |
|||
|
|||
@Serial |
|||
private static final long serialVersionUID = 1L; |
|||
|
|||
private String planId; |
|||
|
|||
private String taskId; |
|||
|
|||
private String outSerialNo; |
|||
} |
|||
@ -0,0 +1,85 @@ |
|||
package com.zdxt.enforcementcode.handler; |
|||
|
|||
import cn.hutool.core.util.StrUtil; |
|||
import com.zdxt.enforcementcode.common.util.api.ApiSignUtil; |
|||
import com.zdxt.enforcementcode.config.ApiPlatformConfig; |
|||
import com.zdxt.enforcementcode.domain.SysApiPlatformConfigEntity; |
|||
import com.zdxt.enforcementcode.service.ISysApiPlatformConfigService; |
|||
import jakarta.servlet.*; |
|||
import jakarta.servlet.http.HttpServletRequest; |
|||
import jakarta.servlet.http.HttpServletResponse; |
|||
import lombok.RequiredArgsConstructor; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.dromara.common.core.constant.HttpStatus; |
|||
import org.dromara.common.core.domain.R; |
|||
import org.dromara.common.json.utils.JsonUtils; |
|||
import org.springframework.boot.web.servlet.FilterRegistrationBean; |
|||
import org.springframework.core.annotation.Order; |
|||
import org.springframework.stereotype.Component; |
|||
|
|||
import java.io.IOException; |
|||
|
|||
@Slf4j |
|||
@Component |
|||
@Order(FilterRegistrationBean.HIGHEST_PRECEDENCE + 10) |
|||
@RequiredArgsConstructor |
|||
public class ApiCryptoFilter implements Filter { |
|||
|
|||
private final ApiPlatformConfig apiPlatformConfig; |
|||
private final ISysApiPlatformConfigService sysApiPlatformConfigService; |
|||
|
|||
private static final String HEADER_ENCRYPTED = "X-Encrypted"; |
|||
private static final String HEADER_APP_KEY = "X-App-Key"; |
|||
|
|||
@Override |
|||
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) |
|||
throws IOException, ServletException { |
|||
|
|||
if (!apiPlatformConfig.isEnabled()) { |
|||
chain.doFilter(request, response); |
|||
return; |
|||
} |
|||
|
|||
HttpServletRequest httpRequest = (HttpServletRequest) request; |
|||
HttpServletResponse httpResponse = (HttpServletResponse) response; |
|||
|
|||
String appKey = httpRequest.getHeader(HEADER_APP_KEY); |
|||
String encryptedFlag = httpRequest.getHeader(HEADER_ENCRYPTED); |
|||
|
|||
if (StrUtil.isBlank(appKey) || !"true".equals(encryptedFlag)) { |
|||
chain.doFilter(request, response); |
|||
return; |
|||
} |
|||
|
|||
SysApiPlatformConfigEntity consumer = sysApiPlatformConfigService.findByAppKey(appKey).orElse(null); |
|||
|
|||
if (consumer == null || !consumer.getEncryptEnabled()) { |
|||
chain.doFilter(request, response); |
|||
return; |
|||
} |
|||
|
|||
ApiDecryptRequestWrapper decryptWrapper = new ApiDecryptRequestWrapper( |
|||
httpRequest, consumer.getPlatformPrivateKey()); |
|||
|
|||
ApiEncryptResponseWrapper encryptWrapper = new ApiEncryptResponseWrapper(httpResponse); |
|||
|
|||
try { |
|||
chain.doFilter(decryptWrapper, encryptWrapper); |
|||
} catch (Exception e) { |
|||
log.error("API加解密处理异常", e); |
|||
httpResponse.reset(); |
|||
httpResponse.setStatus(HttpStatus.SUCCESS); |
|||
httpResponse.setContentType("application/json;charset=UTF-8"); |
|||
httpResponse.getWriter().write(JsonUtils.toJsonString(R.fail("数据处理异常"))); |
|||
return; |
|||
} |
|||
|
|||
String responseBody = encryptWrapper.getContent(); |
|||
String encryptedResponse = ApiSignUtil.rsaEncrypt(responseBody, consumer.getCallerPublicKey()); |
|||
|
|||
httpResponse.reset(); |
|||
httpResponse.setContentType("application/json;charset=UTF-8"); |
|||
httpResponse.setHeader(HEADER_ENCRYPTED, "true"); |
|||
httpResponse.getWriter().write(encryptedResponse); |
|||
} |
|||
} |
|||
@ -0,0 +1,67 @@ |
|||
package com.zdxt.enforcementcode.handler; |
|||
|
|||
import cn.hutool.core.io.IoUtil; |
|||
import com.zdxt.enforcementcode.common.util.api.ApiSignUtil; |
|||
import jakarta.servlet.ReadListener; |
|||
import jakarta.servlet.ServletInputStream; |
|||
import jakarta.servlet.http.HttpServletRequest; |
|||
import jakarta.servlet.http.HttpServletRequestWrapper; |
|||
import org.springframework.http.MediaType; |
|||
|
|||
import java.io.*; |
|||
import java.nio.charset.StandardCharsets; |
|||
|
|||
public class ApiDecryptRequestWrapper extends HttpServletRequestWrapper { |
|||
|
|||
private final byte[] body; |
|||
|
|||
public ApiDecryptRequestWrapper(HttpServletRequest request, String platformPrivateKey) throws IOException { |
|||
super(request); |
|||
byte[] rawBytes = IoUtil.readBytes(request.getInputStream(), false); |
|||
String rawBody = new String(rawBytes, StandardCharsets.UTF_8); |
|||
|
|||
if (rawBody.startsWith("{") || rawBody.startsWith("[")) { |
|||
this.body = rawBytes; |
|||
} else { |
|||
String decrypted = ApiSignUtil.rsaDecrypt(rawBody, platformPrivateKey); |
|||
this.body = decrypted.getBytes(StandardCharsets.UTF_8); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public BufferedReader getReader() { |
|||
return new BufferedReader(new InputStreamReader(getInputStream())); |
|||
} |
|||
|
|||
@Override |
|||
public int getContentLength() { |
|||
return body.length; |
|||
} |
|||
|
|||
@Override |
|||
public long getContentLengthLong() { |
|||
return body.length; |
|||
} |
|||
|
|||
@Override |
|||
public String getContentType() { |
|||
return MediaType.APPLICATION_JSON_VALUE; |
|||
} |
|||
|
|||
@Override |
|||
public ServletInputStream getInputStream() { |
|||
final ByteArrayInputStream bais = new ByteArrayInputStream(body); |
|||
return new ServletInputStream() { |
|||
@Override |
|||
public int read() { return bais.read(); } |
|||
@Override |
|||
public int available() { return body.length; } |
|||
@Override |
|||
public boolean isFinished() { return false; } |
|||
@Override |
|||
public boolean isReady() { return true; } |
|||
@Override |
|||
public void setReadListener(ReadListener readListener) { } |
|||
}; |
|||
} |
|||
} |
|||
@ -0,0 +1,51 @@ |
|||
package com.zdxt.enforcementcode.handler; |
|||
|
|||
import jakarta.servlet.ServletOutputStream; |
|||
import jakarta.servlet.WriteListener; |
|||
import jakarta.servlet.http.HttpServletResponse; |
|||
import jakarta.servlet.http.HttpServletResponseWrapper; |
|||
|
|||
import java.io.*; |
|||
|
|||
public class ApiEncryptResponseWrapper extends HttpServletResponseWrapper { |
|||
|
|||
private final ByteArrayOutputStream byteArrayOutputStream; |
|||
private final ServletOutputStream servletOutputStream; |
|||
private final PrintWriter printWriter; |
|||
|
|||
public ApiEncryptResponseWrapper(HttpServletResponse response) throws IOException { |
|||
super(response); |
|||
this.byteArrayOutputStream = new ByteArrayOutputStream(); |
|||
this.servletOutputStream = new ServletOutputStream() { |
|||
@Override |
|||
public boolean isReady() { return true; } |
|||
@Override |
|||
public void setWriteListener(WriteListener writeListener) { } |
|||
@Override |
|||
public void write(int b) throws IOException { byteArrayOutputStream.write(b); } |
|||
@Override |
|||
public void write(byte[] b, int off, int len) throws IOException { byteArrayOutputStream.write(b, off, len); } |
|||
}; |
|||
this.printWriter = new PrintWriter(new OutputStreamWriter(byteArrayOutputStream)); |
|||
} |
|||
|
|||
@Override |
|||
public PrintWriter getWriter() { return printWriter; } |
|||
|
|||
@Override |
|||
public ServletOutputStream getOutputStream() { return servletOutputStream; } |
|||
|
|||
@Override |
|||
public void flushBuffer() throws IOException { |
|||
servletOutputStream.flush(); |
|||
printWriter.flush(); |
|||
} |
|||
|
|||
@Override |
|||
public void reset() { byteArrayOutputStream.reset(); } |
|||
|
|||
public String getContent() throws IOException { |
|||
flushBuffer(); |
|||
return byteArrayOutputStream.toString(); |
|||
} |
|||
} |
|||
@ -0,0 +1,116 @@ |
|||
package com.zdxt.enforcementcode.handler; |
|||
|
|||
import cn.hutool.core.util.StrUtil; |
|||
import com.zdxt.enforcementcode.common.util.api.ApiSignUtil; |
|||
import com.zdxt.enforcementcode.config.ApiPlatformConfig; |
|||
import com.zdxt.enforcementcode.domain.SysApiPlatformConfigEntity; |
|||
import com.zdxt.enforcementcode.service.ISysApiPlatformConfigService; |
|||
import jakarta.servlet.http.HttpServletRequest; |
|||
import jakarta.servlet.http.HttpServletResponse; |
|||
import lombok.RequiredArgsConstructor; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.dromara.common.core.constant.HttpStatus; |
|||
import org.dromara.common.core.domain.R; |
|||
import org.dromara.common.json.utils.JsonUtils; |
|||
import org.springframework.stereotype.Component; |
|||
import org.springframework.web.servlet.HandlerInterceptor; |
|||
|
|||
import java.io.IOException; |
|||
import java.util.HashMap; |
|||
import java.util.Map; |
|||
|
|||
@Slf4j |
|||
@Component |
|||
@RequiredArgsConstructor |
|||
public class ApiSignInterceptor implements HandlerInterceptor { |
|||
|
|||
private final ApiPlatformConfig apiPlatformConfig; |
|||
private final ISysApiPlatformConfigService sysApiPlatformConfigService; |
|||
|
|||
private static final String HEADER_APP_KEY = "X-App-Key"; |
|||
private static final String HEADER_TIMESTAMP = "X-Timestamp"; |
|||
private static final String HEADER_NONCE = "X-Nonce"; |
|||
private static final String HEADER_SIGN = "X-Sign"; |
|||
|
|||
@Override |
|||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { |
|||
if (!apiPlatformConfig.isEnabled()) { |
|||
return true; |
|||
} |
|||
|
|||
String appKey = request.getHeader(HEADER_APP_KEY); |
|||
String timestamp = request.getHeader(HEADER_TIMESTAMP); |
|||
String nonce = request.getHeader(HEADER_NONCE); |
|||
String sign = request.getHeader(HEADER_SIGN); |
|||
|
|||
if (StrUtil.hasBlank(appKey, timestamp, nonce, sign)) { |
|||
writeError(response, "缺少必要的签名参数: X-App-Key, X-Timestamp, X-Nonce, X-Sign"); |
|||
return false; |
|||
} |
|||
|
|||
SysApiPlatformConfigEntity consumer = sysApiPlatformConfigService.findByAppKey(appKey).orElse(null); |
|||
|
|||
if (consumer == null) { |
|||
writeError(response, "无效的 appKey"); |
|||
return false; |
|||
} |
|||
|
|||
if (!consumer.getSignEnabled()) { |
|||
return true; |
|||
} |
|||
|
|||
long ts; |
|||
try { |
|||
ts = Long.parseLong(timestamp); |
|||
} catch (NumberFormatException e) { |
|||
writeError(response, "timestamp 格式错误"); |
|||
return false; |
|||
} |
|||
|
|||
long now = System.currentTimeMillis() / 1000; |
|||
if (Math.abs(now - ts) > apiPlatformConfig.getTimestampExpireSeconds()) { |
|||
writeError(response, "请求已过期,timestamp 超时"); |
|||
return false; |
|||
} |
|||
|
|||
Map<String, String> signParams = new HashMap<>(); |
|||
signParams.put("appKey", appKey); |
|||
signParams.put("timestamp", timestamp); |
|||
signParams.put("nonce", nonce); |
|||
|
|||
String queryString = request.getQueryString(); |
|||
if (StrUtil.isNotBlank(queryString)) { |
|||
for (String param : queryString.split("&")) { |
|||
String[] kv = param.split("=", 2); |
|||
if (kv.length == 2) { |
|||
signParams.put(kv[0], kv[1]); |
|||
} |
|||
} |
|||
} |
|||
|
|||
String signContent = ApiSignUtil.buildSignContent(signParams); |
|||
log.debug("签名原文: {}", signContent); |
|||
|
|||
boolean verified = ApiSignUtil.verify(signContent, sign, consumer.getAppSecret()); |
|||
if (!verified) { |
|||
try { |
|||
verified = ApiSignUtil.verify(signContent, sign, consumer.getCallerPublicKey()); |
|||
} catch (Exception ignored) { |
|||
} |
|||
} |
|||
|
|||
if (!verified) { |
|||
writeError(response, "签名验证失败"); |
|||
return false; |
|||
} |
|||
|
|||
log.info("API签名验证通过, appKey={}, path={}", appKey, request.getRequestURI()); |
|||
return true; |
|||
} |
|||
|
|||
private void writeError(HttpServletResponse response, String message) throws IOException { |
|||
response.setStatus(HttpStatus.FORBIDDEN); |
|||
response.setContentType("application/json;charset=UTF-8"); |
|||
response.getWriter().write(JsonUtils.toJsonString(R.fail(message))); |
|||
} |
|||
} |
|||
@ -0,0 +1,8 @@ |
|||
package com.zdxt.enforcementcode.mapper; |
|||
|
|||
import com.zdxt.enforcementcode.domain.SysApiPlatformConfigEntity; |
|||
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus; |
|||
|
|||
public interface SysApiPlatformConfigMapper extends BaseMapperPlus<SysApiPlatformConfigEntity, Void> { |
|||
|
|||
} |
|||
@ -0,0 +1,12 @@ |
|||
package com.zdxt.enforcementcode.service; |
|||
|
|||
import com.zdxt.enforcementcode.domain.SysApiPlatformConfigEntity; |
|||
|
|||
import java.util.Optional; |
|||
|
|||
public interface ISysApiPlatformConfigService { |
|||
|
|||
void refreshCache(); |
|||
|
|||
Optional<SysApiPlatformConfigEntity> findByAppKey(String appKey); |
|||
} |
|||
@ -0,0 +1,48 @@ |
|||
package com.zdxt.enforcementcode.service.impl; |
|||
|
|||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
|||
import com.zdxt.enforcementcode.domain.SysApiPlatformConfigEntity; |
|||
import com.zdxt.enforcementcode.mapper.SysApiPlatformConfigMapper; |
|||
import com.zdxt.enforcementcode.service.ISysApiPlatformConfigService; |
|||
import lombok.RequiredArgsConstructor; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.springframework.stereotype.Service; |
|||
|
|||
import jakarta.annotation.PostConstruct; |
|||
import java.util.List; |
|||
import java.util.Map; |
|||
import java.util.Optional; |
|||
import java.util.concurrent.ConcurrentHashMap; |
|||
|
|||
@Slf4j |
|||
@Service |
|||
@RequiredArgsConstructor |
|||
public class SysApiPlatformConfigServiceImpl implements ISysApiPlatformConfigService{ |
|||
|
|||
private final SysApiPlatformConfigMapper sysApiPlatformConfigMapper; |
|||
|
|||
private final Map<String, SysApiPlatformConfigEntity> consumerCache = new ConcurrentHashMap<>(); |
|||
|
|||
@PostConstruct |
|||
public void init() { |
|||
refreshCache(); |
|||
} |
|||
|
|||
public void refreshCache() { |
|||
List<SysApiPlatformConfigEntity> list = sysApiPlatformConfigMapper.selectList( |
|||
new LambdaQueryWrapper<SysApiPlatformConfigEntity>() |
|||
.eq(SysApiPlatformConfigEntity::getStatus, "0") |
|||
); |
|||
Map<String, SysApiPlatformConfigEntity> newMap = new ConcurrentHashMap<>(); |
|||
for (SysApiPlatformConfigEntity entity : list) { |
|||
newMap.put(entity.getAppKey(), entity); |
|||
} |
|||
consumerCache.clear(); |
|||
consumerCache.putAll(newMap); |
|||
log.info("API接入配置缓存刷新, 共加载{}个渠道", newMap.size()); |
|||
} |
|||
|
|||
public Optional<SysApiPlatformConfigEntity> findByAppKey(String appKey) { |
|||
return Optional.ofNullable(consumerCache.get(appKey)); |
|||
} |
|||
} |
|||
Loading…
Reference in new issue