初始化天卡开放平台后端项目
包含完整功能模块: - 设备激活(直销激活码模式 + 代销免激活码模式) - 微信支付 + 天卡订单 - 天卡池管理 + 异步橘视厅SDK激活 - 第三方合作伙伴接口(代销模式) - 签名鉴权(appKey/appSecret + nonce防重放) - 天卡消耗(事务 + 幂等 + 并发控制) - 媒资接口(三级授权过滤) - 审计日志 - 定价策略(3层:门店 > 渠道 > 平台) - 分账(微信分账 + 分润规则) - Docker部署(含Redis容器)
This commit is contained in:
+138
@@ -0,0 +1,138 @@
|
||||
package io.as.modules.openapi.sign.impl;
|
||||
|
||||
import io.as.common.utils.RedisUtils;
|
||||
import io.as.modules.openapi.sign.ApiDeviceSignService;
|
||||
import io.as.modules.openapi.sign.DeviceSignCache;
|
||||
import io.as.modules.openapi.sign.DeviceSignResult;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
|
||||
/**
|
||||
* 设备 sign:HMAC-SHA256(deviceId:timestamp:orgCode, secret),Base64URL 输出;校验时验证签名并检查时间戳在有效期内
|
||||
* <p>对齐生产环境 as-partner-backend 的 ApiDeviceSignServiceImpl</p>
|
||||
*/
|
||||
@Service
|
||||
public class ApiDeviceSignServiceImpl implements ApiDeviceSignService {
|
||||
|
||||
private static final String HMAC_SHA256 = "HmacSHA256";
|
||||
/** sign 有效时长(毫秒):24 小时 */
|
||||
private static final long SIGN_VALID_MS = 24 * 60 * 60 * 1000;
|
||||
/** Redis key 前缀:openapi: 隔离于生产 api:jst: */
|
||||
private static final String REDIS_KEY_LATEST_SIGN_PREFIX = "openapi:device-sign:";
|
||||
|
||||
@Value("${renren.api.device-sign-secret:renren-device-sign-secret-change-in-prod}")
|
||||
private String secret;
|
||||
|
||||
@Autowired
|
||||
private RedisUtils redisUtils;
|
||||
|
||||
@Override
|
||||
public String generate(Long deviceId, long timestampMs, String orgCode) {
|
||||
String orgCodePart = orgCode == null ? "" : orgCode.trim();
|
||||
String payload = deviceId + ":" + timestampMs + ":" + orgCodePart;
|
||||
String hmac = hmacSha256Hex(payload, secret);
|
||||
String toEncode = payload + ":" + hmac;
|
||||
String sign = Base64.getUrlEncoder().withoutPadding().encodeToString(toEncode.getBytes(StandardCharsets.UTF_8));
|
||||
if (deviceId != null) {
|
||||
String key = REDIS_KEY_LATEST_SIGN_PREFIX + deviceId;
|
||||
DeviceSignCache cache = new DeviceSignCache(deviceId, orgCodePart, sign, timestampMs);
|
||||
redisUtils.set(key, cache, SIGN_VALID_MS / 1000);
|
||||
}
|
||||
return sign;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DeviceSignResult verify(String sign) {
|
||||
if (StringUtils.isBlank(sign)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
String decoded = new String(Base64.getUrlDecoder().decode(sign.trim()), StandardCharsets.UTF_8);
|
||||
int lastColon = decoded.lastIndexOf(':');
|
||||
if (lastColon <= 0) {
|
||||
return null;
|
||||
}
|
||||
String payload = decoded.substring(0, lastColon);
|
||||
String hmacReceived = decoded.substring(lastColon + 1);
|
||||
String hmacExpected = hmacSha256Hex(payload, secret);
|
||||
if (!hmacExpected.equalsIgnoreCase(hmacReceived)) {
|
||||
return null;
|
||||
}
|
||||
// payload: deviceId:timestamp:orgCode
|
||||
String[] parts = payload.split(":", 3);
|
||||
if (parts.length < 2) {
|
||||
return null;
|
||||
}
|
||||
Long deviceId = Long.parseLong(parts[0].trim());
|
||||
long timestamp = Long.parseLong(parts[1].trim());
|
||||
String orgCode = parts.length >= 3 ? parts[2].trim() : null;
|
||||
long now = System.currentTimeMillis();
|
||||
if (timestamp > now || (now - timestamp) > SIGN_VALID_MS) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (deviceId == null) {
|
||||
return null;
|
||||
}
|
||||
// 结合 Redis 校验:只允许当前设备最新 sign 生效(若 Redis 有记录且不同,则判为旧 sign)
|
||||
String key = REDIS_KEY_LATEST_SIGN_PREFIX + deviceId;
|
||||
DeviceSignCache cache = redisUtils.get(key, DeviceSignCache.class);
|
||||
if (cache != null && !sign.equals(cache.getSign())) {
|
||||
return null;
|
||||
}
|
||||
// Redis 没记录时,用签名中自带的 orgCode;有记录时以缓存中的为准
|
||||
if (cache != null && cache.getOrgCode() != null) {
|
||||
orgCode = cache.getOrgCode();
|
||||
}
|
||||
return new DeviceSignResult(deviceId, timestamp, orgCode);
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void refreshExpire(Long deviceId) {
|
||||
if (deviceId == null) {
|
||||
return;
|
||||
}
|
||||
String key = REDIS_KEY_LATEST_SIGN_PREFIX + deviceId;
|
||||
DeviceSignCache cache = redisUtils.get(key, DeviceSignCache.class);
|
||||
if (cache != null) {
|
||||
redisUtils.set(key, cache, SIGN_VALID_MS / 1000);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invalidate(Long deviceId) {
|
||||
if (deviceId == null) {
|
||||
return;
|
||||
}
|
||||
String key = REDIS_KEY_LATEST_SIGN_PREFIX + deviceId;
|
||||
// 哨兵缓存:verify 时 cache != null 且 sign != "REVOKED" 即拒绝旧 sign,
|
||||
// 直到重新 init 调 generate 覆盖此哨兵或 TTL 过期。
|
||||
DeviceSignCache revoked = new DeviceSignCache(deviceId, null, "REVOKED", System.currentTimeMillis());
|
||||
redisUtils.set(key, revoked, SIGN_VALID_MS / 1000);
|
||||
}
|
||||
|
||||
private static String hmacSha256Hex(String data, String secretKey) {
|
||||
try {
|
||||
Mac mac = Mac.getInstance(HMAC_SHA256);
|
||||
mac.init(new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), HMAC_SHA256));
|
||||
byte[] hmac = mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
|
||||
StringBuilder sb = new StringBuilder(hmac.length * 2);
|
||||
for (byte b : hmac) {
|
||||
sb.append(String.format("%02x", b));
|
||||
}
|
||||
return sb.toString();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("HMAC error", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user