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