加密
This commit is contained in:
parent
0f08fe882a
commit
4588888604
@ -57,6 +57,7 @@ public class FoundationProperties {
|
||||
private Integer agentId;
|
||||
private String secret;
|
||||
private String token;
|
||||
private String encodingAesKey;
|
||||
private String crmAgentId = "crm-assistant";
|
||||
private String callbackPath = "/v1/integrations/wecom/callback";
|
||||
|
||||
@ -70,6 +71,8 @@ public class FoundationProperties {
|
||||
public void setSecret(String secret) { this.secret = secret; }
|
||||
public String getToken() { return token; }
|
||||
public void setToken(String token) { this.token = token; }
|
||||
public String getEncodingAesKey() { return encodingAesKey; }
|
||||
public void setEncodingAesKey(String encodingAesKey) { this.encodingAesKey = encodingAesKey; }
|
||||
public String getCrmAgentId() { return crmAgentId; }
|
||||
public void setCrmAgentId(String crmAgentId) { this.crmAgentId = crmAgentId; }
|
||||
public String getCallbackPath() { return callbackPath; }
|
||||
|
||||
@ -7,12 +7,13 @@ import com.company.agentfoundation.security.AgentContext;
|
||||
import com.company.agentfoundation.service.ChatService;
|
||||
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.Arrays;
|
||||
import java.util.HexFormat;
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@ -21,35 +22,43 @@ public class WecomCallbackController {
|
||||
private final FoundationProperties properties;
|
||||
private final ChatService chatService;
|
||||
private final WecomClient wecomClient;
|
||||
private final WecomCryptoService wecomCryptoService;
|
||||
private final XmlMapper xmlMapper = new XmlMapper();
|
||||
|
||||
public WecomCallbackController(FoundationProperties properties, ChatService chatService, WecomClient wecomClient) {
|
||||
public WecomCallbackController(
|
||||
FoundationProperties properties,
|
||||
ChatService chatService,
|
||||
WecomClient wecomClient,
|
||||
WecomCryptoService wecomCryptoService
|
||||
) {
|
||||
this.properties = properties;
|
||||
this.chatService = chatService;
|
||||
this.wecomClient = wecomClient;
|
||||
this.wecomCryptoService = wecomCryptoService;
|
||||
}
|
||||
|
||||
@GetMapping("/callback")
|
||||
public String verify(@RequestParam("msg_signature") String signature,
|
||||
@RequestParam("timestamp") String timestamp,
|
||||
@RequestParam("nonce") String nonce,
|
||||
@RequestParam("echostr") String echostr) {
|
||||
if (!validSignature(signature, timestamp, nonce)) {
|
||||
public String verify(
|
||||
@RequestParam("msg_signature") String signature,
|
||||
@RequestParam("timestamp") String timestamp,
|
||||
@RequestParam("nonce") String nonce,
|
||||
@RequestParam("echostr") String echostr
|
||||
) {
|
||||
String payload = wecomCryptoService.isSafeModeEnabled() ? echostr : null;
|
||||
if (!wecomCryptoService.validateSignature(signature, timestamp, nonce, payload)) {
|
||||
throw new IllegalArgumentException("Invalid WeCom signature");
|
||||
}
|
||||
return echostr;
|
||||
return wecomCryptoService.isSafeModeEnabled() ? wecomCryptoService.decrypt(echostr) : echostr;
|
||||
}
|
||||
|
||||
@PostMapping(value = "/callback", consumes = {MediaType.TEXT_XML_VALUE, MediaType.APPLICATION_XML_VALUE})
|
||||
public String receive(@RequestParam("msg_signature") String signature,
|
||||
@RequestParam("timestamp") String timestamp,
|
||||
@RequestParam("nonce") String nonce,
|
||||
@RequestBody String body) throws Exception {
|
||||
if (!validSignature(signature, timestamp, nonce)) {
|
||||
throw new IllegalArgumentException("Invalid WeCom signature");
|
||||
}
|
||||
|
||||
WecomIncomingMessage msg = xmlMapper.readValue(body, WecomIncomingMessage.class);
|
||||
public String receive(
|
||||
@RequestParam("msg_signature") String signature,
|
||||
@RequestParam("timestamp") String timestamp,
|
||||
@RequestParam("nonce") String nonce,
|
||||
@RequestBody String body
|
||||
) throws Exception {
|
||||
WecomIncomingMessage msg = parseIncomingMessage(signature, timestamp, nonce, body);
|
||||
if (msg == null || msg.getMsgType() == null || !"text".equalsIgnoreCase(msg.getMsgType())) {
|
||||
return "success";
|
||||
}
|
||||
@ -62,36 +71,36 @@ public class WecomCallbackController {
|
||||
ChatRequest req = new ChatRequest(
|
||||
null,
|
||||
List.of(
|
||||
new ChatRequest.Message("system", "你是企业CRM智能助手。请给出简洁、可执行的客户跟进建议。"),
|
||||
new ChatRequest.Message("system", "You are an enterprise CRM assistant. Provide concise, actionable follow-up advice."),
|
||||
new ChatRequest.Message("user", userText)
|
||||
),
|
||||
0.3
|
||||
);
|
||||
|
||||
ChatResponse chat = chatService.chat(new AgentContext(properties.getWecom().getCrmAgentId()), req);
|
||||
String reply = (chat.content() == null || chat.content().isBlank()) ? "我已收到你的请求,但暂时无法生成建议。" : chat.content();
|
||||
String reply = (chat.content() == null || chat.content().isBlank())
|
||||
? "Request received, but I could not generate advice right now."
|
||||
: chat.content();
|
||||
wecomClient.sendText(msg.getFromUserName(), reply);
|
||||
|
||||
return "success";
|
||||
}
|
||||
|
||||
private boolean validSignature(String signature, String timestamp, String nonce) {
|
||||
String token = properties.getWecom().getToken();
|
||||
if (token == null || token.isBlank()) {
|
||||
return false;
|
||||
private WecomIncomingMessage parseIncomingMessage(String signature, String timestamp, String nonce, String body) throws Exception {
|
||||
if (wecomCryptoService.isSafeModeEnabled()) {
|
||||
WecomEncryptedEnvelope envelope = xmlMapper.readValue(body, WecomEncryptedEnvelope.class);
|
||||
if (envelope == null || envelope.getEncrypt() == null || envelope.getEncrypt().isBlank()) {
|
||||
throw new IllegalArgumentException("Missing WeCom encrypted payload");
|
||||
}
|
||||
if (!wecomCryptoService.validateSignature(signature, timestamp, nonce, envelope.getEncrypt())) {
|
||||
throw new IllegalArgumentException("Invalid WeCom signature");
|
||||
}
|
||||
String plainXml = wecomCryptoService.decrypt(envelope.getEncrypt());
|
||||
return xmlMapper.readValue(plainXml, WecomIncomingMessage.class);
|
||||
}
|
||||
|
||||
String[] arr = new String[]{token, timestamp, nonce};
|
||||
Arrays.sort(arr);
|
||||
String raw = String.join("", arr);
|
||||
|
||||
try {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-1");
|
||||
byte[] sha1 = digest.digest(raw.getBytes(StandardCharsets.UTF_8));
|
||||
String localSignature = HexFormat.of().formatHex(sha1);
|
||||
return signature.equalsIgnoreCase(localSignature);
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
if (!wecomCryptoService.validateSignature(signature, timestamp, nonce, null)) {
|
||||
throw new IllegalArgumentException("Invalid WeCom signature");
|
||||
}
|
||||
return xmlMapper.readValue(body, WecomIncomingMessage.class);
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,93 @@
|
||||
package com.company.agentfoundation.integration.wecom;
|
||||
|
||||
import com.company.agentfoundation.config.FoundationProperties;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
import java.util.HexFormat;
|
||||
|
||||
@Service
|
||||
public class WecomCryptoService {
|
||||
private final FoundationProperties properties;
|
||||
|
||||
public WecomCryptoService(FoundationProperties properties) {
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
public boolean isSafeModeEnabled() {
|
||||
String key = properties.getWecom().getEncodingAesKey();
|
||||
return key != null && !key.isBlank();
|
||||
}
|
||||
|
||||
public boolean validateSignature(String signature, String timestamp, String nonce, String payload) {
|
||||
String token = properties.getWecom().getToken();
|
||||
if (token == null || token.isBlank()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String[] values = payload == null
|
||||
? new String[]{token, timestamp, nonce}
|
||||
: new String[]{token, timestamp, nonce, payload};
|
||||
Arrays.sort(values);
|
||||
String raw = String.join("", values);
|
||||
|
||||
try {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-1");
|
||||
String local = HexFormat.of().formatHex(digest.digest(raw.getBytes(StandardCharsets.UTF_8)));
|
||||
return local.equalsIgnoreCase(signature);
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public String decrypt(String cipherText) {
|
||||
try {
|
||||
byte[] aesKey = getAesKey();
|
||||
Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
|
||||
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(aesKey, "AES"), new IvParameterSpec(Arrays.copyOfRange(aesKey, 0, 16)));
|
||||
byte[] plain = cipher.doFinal(Base64.getDecoder().decode(cipherText));
|
||||
byte[] unpadded = removePadding(plain);
|
||||
|
||||
ByteBuffer buffer = ByteBuffer.wrap(unpadded, 16, unpadded.length - 16);
|
||||
int xmlLength = buffer.getInt();
|
||||
byte[] xmlBytes = new byte[xmlLength];
|
||||
buffer.get(xmlBytes);
|
||||
byte[] receiveIdBytes = new byte[buffer.remaining()];
|
||||
buffer.get(receiveIdBytes);
|
||||
|
||||
String receiveId = new String(receiveIdBytes, StandardCharsets.UTF_8);
|
||||
String expected = properties.getWecom().getCorpId();
|
||||
if (expected != null && !expected.isBlank() && !expected.equals(receiveId)) {
|
||||
throw new IllegalArgumentException("WeCom receiveId mismatch");
|
||||
}
|
||||
return new String(xmlBytes, StandardCharsets.UTF_8);
|
||||
} catch (IllegalArgumentException ex) {
|
||||
throw ex;
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalArgumentException("Failed to decrypt WeCom payload", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] getAesKey() {
|
||||
String key = properties.getWecom().getEncodingAesKey();
|
||||
if (key == null || key.isBlank()) {
|
||||
throw new IllegalArgumentException("WECOM_ENCODING_AES_KEY is missing");
|
||||
}
|
||||
return Base64.getDecoder().decode(key + "=");
|
||||
}
|
||||
|
||||
private byte[] removePadding(byte[] plainText) {
|
||||
int pad = plainText[plainText.length - 1] & 0xFF;
|
||||
if (pad < 1 || pad > 32) {
|
||||
pad = 0;
|
||||
}
|
||||
return Arrays.copyOfRange(plainText, 0, plainText.length - pad);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,23 @@
|
||||
package com.company.agentfoundation.integration.wecom;
|
||||
|
||||
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
|
||||
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
|
||||
|
||||
@JacksonXmlRootElement(localName = "xml")
|
||||
public class WecomEncryptedEnvelope {
|
||||
@JacksonXmlProperty(localName = "ToUserName")
|
||||
private String toUserName;
|
||||
|
||||
@JacksonXmlProperty(localName = "AgentID")
|
||||
private String agentId;
|
||||
|
||||
@JacksonXmlProperty(localName = "Encrypt")
|
||||
private String encrypt;
|
||||
|
||||
public String getToUserName() { return toUserName; }
|
||||
public void setToUserName(String toUserName) { this.toUserName = toUserName; }
|
||||
public String getAgentId() { return agentId; }
|
||||
public void setAgentId(String agentId) { this.agentId = agentId; }
|
||||
public String getEncrypt() { return encrypt; }
|
||||
public void setEncrypt(String encrypt) { this.encrypt = encrypt; }
|
||||
}
|
||||
@ -32,5 +32,6 @@ foundation:
|
||||
agent-id: ${WECOM_AGENT_ID:1000002}
|
||||
secret: ${WECOM_SECRET:}
|
||||
token: ${WECOM_TOKEN:}
|
||||
encoding-aes-key: ${WECOM_ENCODING_AES_KEY:}
|
||||
crm-agent-id: crm-assistant
|
||||
callback-path: /v1/integrations/wecom/callback
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user