This commit is contained in:
liulu 2024-12-10 17:21:17 +08:00
parent 4febd0520d
commit 63fce073dd
15 changed files with 490 additions and 44 deletions

View File

@ -49,6 +49,10 @@
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
<dependency>
<groupId>net.java.dev.jna</groupId>
<artifactId>jna</artifactId>

View File

@ -29,4 +29,7 @@ public interface SpDeviceMapper extends BaseMapper<Device> {
};
}

View File

@ -1,6 +1,7 @@
package com.sunyard.chsm.model.dto;
import com.sunyard.chsm.enums.DeviceTmkStatus;
import com.sunyard.chsm.sdf.adapter.SdfApiAdapter;
import lombok.Data;
/**
@ -17,5 +18,7 @@ public class DeviceCheckRes {
private boolean hasError = false;
private String message;
private SdfApiAdapter sdfApiAdapter;
}

View File

@ -1,22 +0,0 @@
package com.sunyard.chsm.sdf.context;
import lombok.Data;
/**
* @author liulu
* @since 2024/11/5
*/
@Data
public class DeviceContext {
private String serviceIp;
private Integer servicePort;
private String manufacturer;
private String manufacturerModel;
private String accessCredentials;
private String jnaLibName;
private Integer weight;
}

View File

@ -167,13 +167,14 @@ public class TmkService {
}
Optional.ofNullable(hs).ifPresent(sdfApi::closeSession);
Optional.ofNullable(hd).ifPresent(sdfApi::closeDevice);
res.setSdfApiAdapter(sdfApi);
log.debug("==========> end check device : {}", res);
return res;
}
public void checkSoftDeviceTmk() {
if (!isTmkInit() || !enableSoftDevice()) {
if (!isTmkInit() || !isEnableSoftDevice()) {
return;
}
byte[] softTmk = getSoftDeviceEncTmk();
@ -203,7 +204,7 @@ public class TmkService {
if (Objects.nonNull(device)) {
return device;
}
if (enableSoftDevice()) {
if (isEnableSoftDevice()) {
device = new Device();
device.setManufacturerModel(BouncyCastleProvider.PROVIDER_NAME);
device.setEncKeyIdx(1);
@ -219,11 +220,16 @@ public class TmkService {
return null;
}
private boolean isTmkInit() {
public boolean isTmkInit() {
ParamConf conf = paramConfMapper.selectByKey(ParamConfKeyConstant.TMK_INIT);
return conf != null && String.valueOf(true).equals(conf.getValue());
}
public boolean isEnableSoftDevice() {
ParamConf conf = paramConfMapper.selectByKey(ParamConfKeyConstant.ENABLE_SOFT_DEVICE);
return conf != null && String.valueOf(true).equals(conf.getValue());
}
private void updateTmkInit(boolean value) {
ParamConf conf = paramConfMapper.selectByKey(ParamConfKeyConstant.TMK_INIT);
if (conf == null) {
@ -238,15 +244,10 @@ public class TmkService {
}
}
private boolean enableSoftDevice() {
ParamConf conf = paramConfMapper.selectByKey(ParamConfKeyConstant.ENABLE_SOFT_DEVICE);
return conf != null && String.valueOf(true).equals(conf.getValue());
}
private byte[] getSoftDeviceEncTmk() {
boolean tmkInit = isTmkInit();
Assert.isTrue(tmkInit, "主密钥未初始化");
boolean enabled = enableSoftDevice();
boolean enabled = isEnableSoftDevice();
Assert.isTrue(enabled, "未启用软设备");
ParamConf conf = paramConfMapper.selectByKey(ParamConfKeyConstant.SOFT_ENC_TMK);
if (conf == null || ObjectUtils.isEmpty(conf.getValue())) {

View File

@ -54,10 +54,6 @@
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
</dependencies>

View File

@ -1,6 +1,7 @@
package com.sunyard.chsm;
import lombok.extern.slf4j.Slf4j;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@ -10,14 +11,11 @@ import org.springframework.boot.autoconfigure.SpringBootApplication;
*/
@Slf4j
@SpringBootApplication
@MapperScan({"com.sunyard.chsm.**.mapper"})
public class WebServerApp {
public static void main(String[] args) {
SpringApplication.run(WebServerApp.class, args);
log.info("---------------------WebServerApp 启动完成-------------------");
}

View File

@ -69,9 +69,9 @@ public class AppTokenFilter extends OncePerRequestFilter {
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.getOutputStream().write(JsonUtils.toJsonBytes(R.error("token格式错误")));
} catch (Exception ex) {
log.error("未知异常: {}", requestURI, ex);
log.error("系统异常: {}", requestURI, ex);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.getOutputStream().write(JsonUtils.toJsonBytes(R.error("发生异常")));
response.getOutputStream().write(JsonUtils.toJsonBytes(R.error("系统异常")));
}
}

View File

@ -34,7 +34,7 @@ import static com.sunyard.chsm.constant.SecurityConstant.ATTRIBUTE_APP_USER;
@Component
public class AuthHandler implements HandlerInterceptor, InitializingBean {
Cache<String, Map<String, List<Long>>> cache = null;
private Cache<String, Map<String, List<Long>>> cache = null;
/**
* token 过期时间, 分钟
@ -45,7 +45,6 @@ public class AuthHandler implements HandlerInterceptor, InitializingBean {
@Resource
private CryptoServiceApiMapper cryptoServiceApiMapper;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (!(handler instanceof HandlerMethod)) {
@ -58,6 +57,7 @@ public class AuthHandler implements HandlerInterceptor, InitializingBean {
HandlerMethod handlerMethod = (HandlerMethod) handler;
AuthCode authCode = handlerMethod.getMethodAnnotation(AuthCode.class);
if (authCode == null || ObjectUtils.isEmpty(authCode.value())) {
request.setAttribute("used_service_ids", user.getServiceIds());
return true;
}
@ -73,6 +73,7 @@ public class AuthHandler implements HandlerInterceptor, InitializingBean {
response.getOutputStream().write(JsonUtils.toJsonBytes(R.error("无权访问")));
return false;
}
request.setAttribute("used_service_ids", codeServiceMap.get(code));
return true;
}
@ -80,6 +81,7 @@ public class AuthHandler implements HandlerInterceptor, InitializingBean {
public void afterPropertiesSet() throws Exception {
cache = Caffeine.newBuilder()
.expireAfterWrite(Duration.ofMinutes(tokenExpireTime))
.maximumSize(400L)
.build();
}
}

View File

@ -0,0 +1,38 @@
package com.sunyard.chsm.pool;
import com.sunyard.chsm.sdf.adapter.SdfApiAdapter;
import lombok.Data;
import org.apache.commons.pool2.impl.GenericObjectPool;
/**
* @author liulu
* @since 2024/11/5
*/
@Data
public class DeviceContext {
private String ip;
private Integer port;
private String manufacturer;
private String model;
private Integer encKeyIdx;
private String accessCredentials;
private String jnaLibName;
private String deviceSerial;
private String pubKey;
private String encTmk;
private Integer weight;
private SdfApiAdapter sdfApiAdapter;
private GenericObjectPool<TMKContext> pool;
public String getUnionId() {
return String.join("-", deviceSerial, pubKey);
}
public String getIpPort() {
return String.join("-", ip, String.valueOf(port));
}
}

View File

@ -0,0 +1,209 @@
package com.sunyard.chsm.pool;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.sunyard.chsm.auth.AppUser;
import com.sunyard.chsm.constant.SecurityConstant;
import com.sunyard.chsm.enums.DeviceTmkStatus;
import com.sunyard.chsm.mapper.CryptoServiceDeviceGroupMapper;
import com.sunyard.chsm.mapper.SpDeviceMapper;
import com.sunyard.chsm.model.dto.DeviceCheckRes;
import com.sunyard.chsm.model.entity.CryptoServiceDeviceGroup;
import com.sunyard.chsm.model.entity.Device;
import com.sunyard.chsm.service.TmkService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.pool2.impl.GenericObjectPool;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import java.time.Duration;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
/**
* @author liulu
* @since 2024/12/10
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class DeviceManager implements ApplicationRunner {
private static final Map<Long, AtomicInteger> ROUND_MAP = new HashMap<>();
private Map<Long, List<DeviceContext>> deviceMap = new HashMap<>();
private boolean enableSoftDevice = false;
private final TmkService tmkService;
private final SpDeviceMapper spDeviceMapper;
private final CryptoServiceDeviceGroupMapper cryptoServiceDeviceGroupMapper;
private TMKContext chooseOne() {
RequestAttributes attributes = RequestContextHolder.currentRequestAttributes();
AppUser user = (AppUser) attributes.getAttribute(SecurityConstant.ATTRIBUTE_APP_USER, RequestAttributes.SCOPE_REQUEST);
Assert.notNull(user, "登录用户不能为空");
//noinspection unchecked
List<Long> serviceIds = (List<Long>) attributes.getAttribute("used_service_ids", RequestAttributes.SCOPE_REQUEST);
Assert.isTrue(!CollectionUtils.isEmpty(serviceIds), "应用: " + user.getName() + "没有可用服务");
AtomicInteger atomicInteger = ROUND_MAP.computeIfAbsent(user.getAppId(), k -> new AtomicInteger(1));
if (atomicInteger.get() > Integer.MAX_VALUE - 10000) {
atomicInteger.set(1);
}
List<DeviceContext> contexts = new ArrayList<>();
for (Long serviceId : serviceIds) {
Optional.ofNullable(deviceMap.get(serviceId))
.ifPresent(contexts::addAll);
}
DeviceContext device = getNextDevice(contexts, atomicInteger.getAndIncrement());
try {
return device.getPool().borrowObject();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static DeviceContext getNextDevice(List<DeviceContext> devices, int totalCalls) {
if (devices == null || devices.isEmpty()) {
return null;
}
int totalWeight = 0;
for (DeviceContext device : devices) {
totalWeight += device.getWeight();
}
int index = totalCalls % totalWeight;
for (DeviceContext device : devices) {
if (index < device.getWeight()) {
return device;
}
index -= device.getWeight();
}
return null; // 理论上不会到这里
}
private void syncDevice() {
log.debug(">>>>>>>>>>>>>>> start sync device <<<<<<<<<<<<<<<");
enableSoftDevice = tmkService.isEnableSoftDevice();
List<Device> devices = spDeviceMapper.selectList(
new LambdaQueryWrapper<Device>()
.eq(Device::getTmkStatus, DeviceTmkStatus.finished)
.gt(Device::getGroupId, 0)
);
if (CollectionUtils.isEmpty(devices)) {
log.info("no device for sync ...");
deviceMap.clear();
return;
}
Map<Long, List<Device>> groupDeviceMap = devices.stream().collect(Collectors.groupingBy(Device::getGroupId));
List<CryptoServiceDeviceGroup> serviceDeviceGroups = cryptoServiceDeviceGroupMapper
.selectList(new LambdaQueryWrapper<CryptoServiceDeviceGroup>()
.in(CryptoServiceDeviceGroup::getDeviceGroupId, groupDeviceMap.keySet()));
if (CollectionUtils.isEmpty(serviceDeviceGroups)) {
deviceMap.clear();
return;
}
Map<Long, List<Device>> waitSyncMap = serviceDeviceGroups.stream()
.collect(Collectors.toMap(CryptoServiceDeviceGroup::getServiceId,
it -> groupDeviceMap.get(it.getDeviceGroupId())));
for (Map.Entry<Long, List<Device>> entry : waitSyncMap.entrySet()) {
deviceMap.compute(entry.getKey(), (k, old) -> {
if (CollectionUtils.isEmpty(old)) {
return entry.getValue().stream().map(this::mapToContext).filter(Objects::nonNull).collect(Collectors.toList());
}
List<String> newSerials = entry.getValue().stream()
.map(Device::getDeviceSerial)
.collect(Collectors.toList());
List<String> oldSerials = old.stream()
.map(DeviceContext::getDeviceSerial)
.collect(Collectors.toList());
List<DeviceContext> noChanged = old.stream()
.filter(it -> newSerials.contains(it.getDeviceSerial()))
.collect(Collectors.toList());
List<Device> waitSync = entry.getValue().stream()
.filter(it -> !oldSerials.contains(it.getDeviceSerial()))
.collect(Collectors.toList());
noChanged.addAll(waitSync.stream().map(this::mapToContext).filter(Objects::nonNull).collect(Collectors.toList()));
return noChanged;
});
}
}
private DeviceContext mapToContext(Device device) {
try {
Assert.hasText(device.getEncTmk(), "TMK 状态异常");
DeviceCheckRes checkRes = tmkService.checkDevice(device);
if (!Objects.equals(checkRes.getDeviceSerial(), device.getDeviceSerial())
|| !Objects.equals(checkRes.getPubKey(), device.getPubKey())) {
return null;
}
DeviceContext context = new DeviceContext();
context.setIp(device.getServiceIp());
context.setPort(device.getServicePort());
context.setModel(device.getManufacturerModel());
context.setEncKeyIdx(device.getEncKeyIdx());
context.setAccessCredentials(device.getAccessCredentials());
context.setDeviceSerial(device.getDeviceSerial());
context.setPubKey(device.getPubKey());
context.setEncTmk(device.getEncTmk());
context.setWeight(device.getWeight());
context.setSdfApiAdapter(checkRes.getSdfApiAdapter());
GenericObjectPoolConfig<TMKContext> config = new GenericObjectPoolConfig<>();
config.setMaxWait(Duration.ofSeconds(5));
config.setJmxEnabled(false);
config.setMinIdle(2);
config.setTimeBetweenEvictionRuns(Duration.ofMinutes(1));
config.setTestWhileIdle(true);
TMKContextFactory tenantTMKContextFactory = new TMKContextFactory(checkRes.getSdfApiAdapter(), context);
GenericObjectPool<TMKContext> pool = new GenericObjectPool<>(tenantTMKContextFactory, config);
context.setPool(pool);
return context;
} catch (Exception ex) {
log.warn("device conn error: {}", device, ex);
return null;
}
}
@Override
public void run(ApplicationArguments args) throws Exception {
Executors.newSingleThreadScheduledExecutor()
.scheduleWithFixedDelay(this::syncDevice, 0L, 5L, TimeUnit.MINUTES);
}
}

View File

@ -0,0 +1,92 @@
package com.sunyard.chsm.pool;
import com.sunyard.chsm.enums.AlgMode;
import com.sunyard.chsm.enums.KeyAlg;
import com.sunyard.chsm.enums.Padding;
import com.sunyard.chsm.sdf.SdfApiService;
import com.sunyard.chsm.sdf.model.EccKey;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.Resource;
/**
* @author liulu
* @since 2024/12/10
*/
@Slf4j
public class LoadBalancedSdfApiService implements SdfApiService {
@Resource
private DeviceManager deviceManager;
@Override
public byte[] generateRandom(int len) {
return new byte[0];
}
@Override
public byte[] genSymKey(KeyAlg alg, Integer keyLen) {
return new byte[0];
}
@Override
public byte[] genSymKey(KeyAlg alg) {
return new byte[0];
}
@Override
public byte[] symEncrypt(KeyAlg alg, AlgMode mode, Padding padding, byte[] key, byte[] data) {
return new byte[0];
}
@Override
public byte[] symEncrypt(KeyAlg alg, byte[] key, byte[] data) {
return new byte[0];
}
@Override
public byte[] symDecrypt(KeyAlg alg, AlgMode mode, Padding padding, byte[] key, byte[] data) {
return new byte[0];
}
@Override
public byte[] symDecrypt(KeyAlg alg, byte[] key, byte[] data) {
return new byte[0];
}
@Override
public EccKey genKeyPairEcc() {
return null;
}
@Override
public byte[] calculateMAC(byte[] symKey, byte[] pucIv, byte[] pucData) {
return new byte[0];
}
@Override
public byte[] hmac(byte[] key, byte[] srcData) {
return new byte[0];
}
@Override
public byte[] hash(byte[] pucData) {
return new byte[0];
}
@Override
public byte[] encryptByTMK(byte[] data) {
return new byte[0];
}
@Override
public byte[] decryptByTMK(byte[] data) {
return new byte[0];
}
}

View File

@ -0,0 +1,21 @@
package com.sunyard.chsm.pool;
import lombok.Data;
/**
* @author liulu
* @since 2024/12/10
*/
@Data
public class TMKContext {
private String encTmk;
private String deviceHandle;
private String sessionHandle;
private String keyHandle;
}

View File

@ -0,0 +1,101 @@
package com.sunyard.chsm.pool;
import com.sunyard.chsm.sdf.adapter.SdfApiAdapter;
import com.sunyard.chsm.sdf.context.AlgId;
import com.sunyard.chsm.sdf.model.EccCipher;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.pool2.BasePooledObjectFactory;
import org.apache.commons.pool2.DestroyMode;
import org.apache.commons.pool2.PooledObject;
import org.apache.commons.pool2.impl.DefaultPooledObject;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import java.util.Arrays;
/**
* @author liulu
* @since 2024/12/10
*/
@Slf4j
public class TMKContextFactory extends BasePooledObjectFactory<TMKContext> {
private static final byte[] checkData = "1234567812345678".getBytes();
private final SdfApiAdapter sdfApiAdapter;
private final DeviceContext context;
private String deviceHandle;
public TMKContextFactory(SdfApiAdapter sdfApiAdapter, DeviceContext context) {
this.sdfApiAdapter = sdfApiAdapter;
this.context = context;
this.deviceHandle = sdfApiAdapter.openDevice();
}
@Override
public TMKContext create() throws Exception {
TMKContext keyHandle = new TMKContext();
String hs;
try {
hs = sdfApiAdapter.openSession(deviceHandle);
} catch (Exception ex) {
// 再次尝试,
this.deviceHandle = sdfApiAdapter.openDevice();
hs = sdfApiAdapter.openSession(deviceHandle);
}
keyHandle.setSessionHandle(hs);
keyHandle.setEncTmk(context.getEncTmk());
sdfApiAdapter.getPrivateKeyAccessRight(hs, context.getEncKeyIdx(), context.getAccessCredentials().getBytes());
String hk = sdfApiAdapter.importKeyWithISKECC(hs, context.getEncKeyIdx(), EccCipher.fromHex(context.getEncTmk()));
keyHandle.setKeyHandle(hk);
log.info("create key handle with isk for {}", context.getIpPort());
return keyHandle;
}
@Override
public PooledObject<TMKContext> wrap(TMKContext tmkContext) {
return new DefaultPooledObject<>(tmkContext);
}
@Override
public boolean validateObject(PooledObject<TMKContext> p) {
log.info("开始检查设备{}, session 是否有效", context.getIpPort());
TMKContext handle = p.getObject();
if (handle == null) {
return false;
}
try {
byte[] encData = sdfApiAdapter.symEncrypt(handle.getSessionHandle(),
handle.getKeyHandle(), AlgId.SGD_SM4_ECB, new byte[0], checkData);
byte[] decData = sdfApiAdapter.symDecrypt(handle.getSessionHandle(),
handle.getKeyHandle(), AlgId.SGD_SM4_ECB, new byte[0], encData);
Assert.isTrue(Arrays.equals(checkData, decData), "密码机加解密异常");
return true;
} catch (Exception e) {
log.warn("设备{}, validateObject error", context.getIpPort());
return false;
}
}
@Override
public void destroyObject(PooledObject<TMKContext> p, DestroyMode destroyMode) throws Exception {
TMKContext handle = p.getObject();
if (handle == null) {
return;
}
try {
String sessionHandle = handle.getSessionHandle();
if (StringUtils.hasText(handle.getKeyHandle())) {
sdfApiAdapter.destroyKey(sessionHandle, handle.getKeyHandle());
}
sdfApiAdapter.closeSession(sessionHandle);
} catch (Exception ex) {
//
log.warn("device: {}, {} destroyObject error ", context.getIpPort(), handle.getSessionHandle(), ex);
}
}
}

View File

@ -31,7 +31,7 @@ spring:
date-format: yyyy-MM-dd HH:mm:ss
mybatis-plus:
mapper-locations: classpath*:mapper/**/*Mapper.xml
mapper-locations: classpath:mapper/**/*Mapper.xml
# 原生配置
configuration:
log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl