Merge remote-tracking branch 'origin/V1.00' into V1.00

This commit is contained in:
waner 2026-03-27 13:42:40 +08:00
commit 8e6933f4be
10 changed files with 743 additions and 0 deletions

View File

@ -0,0 +1,98 @@
package com.cisd.tms.common.util;
import jakarta.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.List;
public final class IpWhitelistUtil {
private IpWhitelistUtil() {
}
public static String getClientIp(HttpServletRequest request) {
String ipAddress = request.getHeader("X-Forwarded-For");
if (isBlankOrUnknown(ipAddress)) {
ipAddress = request.getHeader("Proxy-Client-IP");
}
if (isBlankOrUnknown(ipAddress)) {
ipAddress = request.getHeader("WL-Proxy-Client-IP");
}
if (isBlankOrUnknown(ipAddress)) {
ipAddress = request.getRemoteAddr();
}
return ipAddress;
}
public static List<String> readWhitelist(String path) {
List<String> whitelist = new ArrayList<>();
try (BufferedReader reader = new BufferedReader(new FileReader(path))) {
String line;
while ((line = reader.readLine()) != null) {
if (!line.isBlank()) {
whitelist.add(line.trim());
}
}
} catch (IOException e) {
// ignore when whitelist file missing
}
return whitelist;
}
public static boolean isIpInWhitelist(String ipAddress, List<String> whitelist) {
for (String entry : whitelist) {
if (entry.contains("/")) {
if (isIpInCidr(ipAddress, entry)) {
return true;
}
String cidrAddress = entry.split("/")[0];
if (cidrAddress.equals(ipAddress)) {
return true;
}
} else if (entry.equals(ipAddress)) {
return true;
}
}
return false;
}
private static boolean isIpInCidr(String ipAddress, String cidr) {
try {
String[] parts = cidr.split("/");
if (parts.length != 2) {
return false;
}
InetAddress inetAddr = InetAddress.getByName(parts[0]);
int prefixLength = Integer.parseInt(parts[1]);
byte[] network = inetAddr.getAddress();
byte[] ip = InetAddress.getByName(ipAddress).getAddress();
if (network.length != ip.length) {
return false;
}
int fullBytes = prefixLength / 8;
int remainingBits = prefixLength % 8;
for (int i = 0; i < fullBytes; i++) {
if (network[i] != ip[i]) {
return false;
}
}
if (remainingBits > 0) {
int mask = (-1) << (8 - remainingBits);
if ((network[fullBytes] & mask) != (ip[fullBytes] & mask)) {
return false;
}
}
return true;
} catch (Exception e) {
return false;
}
}
private static boolean isBlankOrUnknown(String value) {
return value == null || value.isEmpty() || "unknown".equalsIgnoreCase(value);
}
}

View File

@ -0,0 +1,72 @@
package com.cisd.tms.common.util;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.PublicKey;
import java.security.Security;
import java.security.Signature;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
public final class Sm2SignatureUtil {
private static final String PROVIDER = "BC";
private static final String SIGNATURE_ALGO = "SM3withSM2";
static {
if (Security.getProvider(PROVIDER) == null) {
Security.addProvider(new BouncyCastleProvider());
}
}
private Sm2SignatureUtil() {
}
public static boolean verifyBase64Signature(String publicKey, String data, String signatureBase64) {
if (publicKey == null || publicKey.isBlank()) {
throw new IllegalArgumentException("public key is required");
}
if (data == null) {
throw new IllegalArgumentException("data is required");
}
if (signatureBase64 == null || signatureBase64.isBlank()) {
throw new IllegalArgumentException("signature is required");
}
try {
PublicKey key = parsePublicKey(publicKey);
Signature signature = Signature.getInstance(SIGNATURE_ALGO, PROVIDER);
signature.initVerify(key);
signature.update(data.getBytes(StandardCharsets.UTF_8));
byte[] signatureBytes = Base64.getDecoder().decode(signatureBase64.trim());
return signature.verify(signatureBytes);
} catch (Exception e) {
throw new IllegalArgumentException("sm2 verify failed", e);
}
}
private static PublicKey parsePublicKey(String publicKey) throws Exception {
String normalized = stripPem(publicKey);
byte[] keyBytes = Base64.getDecoder().decode(normalized.getBytes(StandardCharsets.UTF_8));
X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("EC", PROVIDER);
return keyFactory.generatePublic(spec);
}
private static String stripPem(String key) {
String trimmed = key.trim();
if (!trimmed.contains("BEGIN")) {
return trimmed;
}
StringBuilder builder = new StringBuilder();
String[] lines = trimmed.split("\\R");
for (String line : lines) {
if (line.startsWith("-----")) {
continue;
}
builder.append(line.trim());
}
return builder.toString();
}
}

View File

@ -0,0 +1,52 @@
package com.cisd.tms.modules.mk.config;
import com.cisd.tms.modules.mk.dto.UserInfo;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ConfigurationProperties(prefix = "tms.ukey")
public class UKeyLoginProperties {
/**
* 认证信息白名单
*/
private List<UserInfo> authenticationInfoList = new ArrayList<>();
/**
* 角色密码映射
*/
private Map<String, String> rolePasswords = new HashMap<>();
/**
* IP 白名单文件路径
*/
private String whitelistPath;
public List<UserInfo> getAuthenticationInfoList() {
return authenticationInfoList;
}
public void setAuthenticationInfoList(List<UserInfo> authenticationInfoList) {
this.authenticationInfoList = authenticationInfoList;
}
public Map<String, String> getRolePasswords() {
return rolePasswords;
}
public void setRolePasswords(Map<String, String> rolePasswords) {
this.rolePasswords = rolePasswords;
}
public String getWhitelistPath() {
return whitelistPath;
}
public void setWhitelistPath(String whitelistPath) {
this.whitelistPath = whitelistPath;
}
}

View File

@ -0,0 +1,32 @@
package com.cisd.tms.modules.mk.controller;
import com.cisd.tms.common.api.ApiResponse;
import com.cisd.tms.modules.mk.dto.AuthInfo;
import com.cisd.tms.modules.mk.dto.LoginDTO;
import com.cisd.tms.modules.mk.service.UKeyLoginService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
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.RestController;
/**
* UKey 登录接口迁移自 manager.service
*/
@RestController
@RequestMapping("/api/v1")
@Tag(name = "UKey 登录", description = "UKey 登录校验与认证信息生成")
public class LoginController {
@Resource
private UKeyLoginService uKeyLoginService;
@PostMapping("/UKeyLogin2")
@Operation(summary = "UKey 登录", description = "校验 UKey 签名、随机数与角色密码,并返回认证信息。")
public ApiResponse<AuthInfo> uKeyLogin(@RequestBody LoginDTO loginDTO, HttpServletRequest request) {
return ApiResponse.success(uKeyLoginService.uKeyLogin(loginDTO, request));
}
}

View File

@ -0,0 +1,24 @@
package com.cisd.tms.modules.mk.dto;
import lombok.Data;
import java.security.SecureRandom;
@Data
public class AuthInfo extends SecretKey {
private String token;
public static AuthInfo getInstance(String token) {
AuthInfo authInfo = new AuthInfo();
authInfo.token = token;
authInfo.setKey(generateKey());
authInfo.setIv(generateKey());
return authInfo;
}
private static byte[] generateKey() {
byte[] key = new byte[16];
new SecureRandom().nextBytes(key);
return key;
}
}

View File

@ -0,0 +1,142 @@
package com.cisd.tms.modules.mk.dto;
import lombok.Data;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* 登录时传输的实体
*/
@Data
public class LoginDTO {
/**
* 选择的角色
*/
private String roleSelected;
/**
* 登录的角色
*/
private String role;
/**
* 认证信息
*/
private List<LoginAuthInfo> authInfo;
/**
* 密码
*/
private String password;
public Set<String> getRids() {
Set<String> roles = new HashSet<>();
if (authInfo == null) {
return roles;
}
for (LoginAuthInfo auth : authInfo) {
roles.add(auth.rid);
}
return roles;
}
public List<String> getRandom() {
List<String> randomList = new ArrayList<>();
if (authInfo == null) {
return randomList;
}
for (LoginAuthInfo loginAuthInfo : authInfo) {
randomList.add(loginAuthInfo.getRb());
}
return randomList;
}
public UserInfo getUserInfo() {
UserInfo userInfo = new UserInfo();
userInfo.setRole(this.role);
List<UserInfo.User> userList = new ArrayList<>();
if (authInfo != null) {
for (LoginAuthInfo loginAuthInfo : authInfo) {
UserInfo.User user = new UserInfo.User();
user.setUid(loginAuthInfo.getUid());
user.setRid(loginAuthInfo.getRid());
userList.add(user);
}
}
userInfo.setUser(userList);
return userInfo;
}
public List<UKeySignVerifyDTO> getUKeySignVerifyDTOList() {
List<UKeySignVerifyDTO> uKeySignVerifyDTOList = new ArrayList<>();
if (authInfo == null) {
return uKeySignVerifyDTOList;
}
for (LoginAuthInfo loginAuthInfo : authInfo) {
UKeySignVerifyDTO uKeySignVerifyDTO = new UKeySignVerifyDTO();
UKeySignDTO uKeySignDTO = new UKeySignDTO();
uKeySignDTO.setPubKey(loginAuthInfo.pubKey);
uKeySignDTO.setRole(this.role);
uKeySignDTO.setUid(loginAuthInfo.uid);
uKeySignDTO.setRid(loginAuthInfo.rid);
uKeySignVerifyDTO.setuKeySignDTO(uKeySignDTO);
uKeySignVerifyDTO.setSign(loginAuthInfo.issueSign);
uKeySignVerifyDTOList.add(uKeySignVerifyDTO);
}
return uKeySignVerifyDTOList;
}
public List<LoginSignDTO> getLoginSignDTOList() {
List<LoginSignDTO> loginSignDTOList = new ArrayList<>();
if (authInfo == null) {
return loginSignDTOList;
}
for (LoginAuthInfo loginAuthInfo : authInfo) {
LoginSignDTO loginSignDTO = new LoginSignDTO();
loginSignDTO.setPubKey(loginAuthInfo.pubKey);
loginSignDTO.setSignValue(loginAuthInfo.loginSign);
loginSignDTO.setLoginSignData(loginAuthInfo.loginSignData);
loginSignDTOList.add(loginSignDTO);
}
return loginSignDTOList;
}
@Data
public static class LoginAuthInfo {
/**
* U盾公钥
*/
private String pubKey;
/**
* U盾 uid
*/
private String uid;
/**
* U盾 rid
*/
private String rid;
/**
* 前端生成随机数
*/
private String ra;
/**
* 后端生成随机数
*/
private String rb;
/**
* 发行签名
*/
private String issueSign;
/**
* 登录签名的原始值
*/
private String loginSignData;
/**
* 登录签名
*/
private String loginSign;
}
}

View File

@ -0,0 +1,40 @@
package com.cisd.tms.modules.mk.dto;
public class LoginSignDTO {
/**
* U盾公钥
*/
private String pubKey;
/**
* 签名原始数据
*/
private String loginSignData;
/**
* 签名值
*/
private String signValue;
public String getPubKey() {
return pubKey;
}
public void setPubKey(String pubKey) {
this.pubKey = pubKey;
}
public String getLoginSignData() {
return loginSignData;
}
public void setLoginSignData(String loginSignData) {
this.loginSignData = loginSignData;
}
public String getSignValue() {
return signValue;
}
public void setSignValue(String signValue) {
this.signValue = signValue;
}
}

View File

@ -0,0 +1,18 @@
package com.cisd.tms.modules.mk.dto;
import lombok.Data;
/**
* 加密密钥及偏移量
*/
@Data
public class SecretKey {
/**
* 密钥
*/
private byte[] key;
/**
* 偏移量
*/
private byte[] iv;
}

View File

@ -0,0 +1,70 @@
package com.cisd.tms.modules.mk.dto;
import lombok.Data;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/**
* 角色信息
*/
@Data
public class UserInfo {
/**
* 角色名称
*/
private String role;
private Integer defpassflag;
private List<User> user;
private List<String> permissions;
@Data
public static class User {
/**
* uid
*/
private String uid;
/**
* rid
*/
private String rid;
public boolean isSameUser(User user) {
return this.uid.equals(user.uid) && this.rid.equals(user.rid);
}
}
/**
* 认证信息校验校验配置文件中的认证信息和传入的认证信息是否相同
*/
public boolean roleUserAuth(List<UserInfo> authenticationInfoList) {
List<User> userConfig = new ArrayList<>(getUserFromAuthList(authenticationInfoList));
for (User current : this.user) {
Iterator<User> iterable = userConfig.iterator();
while (iterable.hasNext()) {
if (iterable.next().isSameUser(current)) {
iterable.remove();
break;
}
}
}
return userConfig.isEmpty();
}
/**
* 获取配置信息当中相应角色应包含的认证信息
*/
public List<User> getUserFromAuthList(List<UserInfo> authenticationInfoList) {
for (UserInfo authenticationInfo : authenticationInfoList) {
if (authenticationInfo.getRole().equals(this.role)) {
this.permissions = authenticationInfo.getPermissions();
return authenticationInfo.getUser();
}
}
throw new IllegalArgumentException("role info not found: " + role);
}
}

View File

@ -0,0 +1,195 @@
package com.cisd.tms.modules.mk.service;
import com.cisd.tms.common.util.IpWhitelistUtil;
import com.cisd.tms.common.util.Sm2SignatureUtil;
import com.cisd.tms.modules.mk.config.UKeyLoginProperties;
import com.cisd.tms.modules.mk.dto.AuthInfo;
import com.cisd.tms.modules.mk.dto.LoginDTO;
import com.cisd.tms.modules.mk.dto.LoginSignDTO;
import com.cisd.tms.modules.mk.dto.UKeySignEntity;
import com.cisd.tms.modules.mk.dto.UKeySignVerifyDTO;
import com.cisd.tms.modules.mk.dto.UserInfo;
import com.cisd.tms.modules.mk.enums.MasterKeyStatus;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
import java.util.UUID;
@Service
public class UKeyLoginService {
private static final Logger log = LoggerFactory.getLogger(UKeyLoginService.class);
private final LmkService lmkService;
private final ObjectMapper objectMapper;
private final UKeyLoginProperties loginProperties;
private volatile List<String> randomList;
public UKeyLoginService(LmkService lmkService, ObjectMapper objectMapper, UKeyLoginProperties loginProperties) {
this.lmkService = lmkService;
this.objectMapper = objectMapper;
this.loginProperties = loginProperties;
}
public AuthInfo uKeyLogin(LoginDTO loginDTO, HttpServletRequest request) {
try {
log.info("UKey login payload: {}", objectMapper.writeValueAsString(loginDTO));
currentRoleCheck(loginDTO.getRole(), loginDTO.getRids());
uKeySignCheck(loginDTO.getUKeySignVerifyDTOList());
UserInfo userInfo = loginDTO.getUserInfo();
userInfoCheck(userInfo);
throwIfLmkNotGenerated();
randomCheck(loginDTO.getRandom());
loginSignCheck(loginDTO.getLoginSignDTOList());
whitelistCheck(request);
checkRolePassword(loginDTO.getRole(), loginDTO.getPassword());
AuthInfo authInfo = AuthInfo.getInstance(UUID.randomUUID().toString());
log.info("UKey login success, role={}", loginDTO.getRole());
return authInfo;
} catch (JsonProcessingException e) {
throw new IllegalStateException("serialize login payload failed", e);
} catch (RuntimeException e) {
log.error("UKey login failed: {}", e.getMessage(), e);
throw e;
}
}
private void currentRoleCheck(String role, java.util.Set<String> rids) {
boolean matched;
switch (role) {
case "superadmin" -> matched = rids.size() == 3
&& rids.contains("1")
&& rids.contains("2")
&& rids.contains("3");
case "keyadmin" -> matched = rids.size() == 2
&& rids.contains("4")
&& rids.contains("5");
case "auditadmin" -> matched = rids.size() == 1
&& rids.contains("6");
case "configadmin" -> matched = rids.size() == 1
&& rids.contains("7");
case "systemadmin" -> matched = rids.size() == 1
&& rids.contains("8");
default -> matched = false;
}
if (!matched) {
throw new IllegalArgumentException("selected role not matched with UKey rids");
}
}
private void uKeySignCheck(List<UKeySignVerifyDTO> uKeySignVerifyDTOList) {
String iPubKey = lmkService.exportIkPublicKeyHex();
boolean result = true;
for (UKeySignVerifyDTO uKeySignVerifyDTO : uKeySignVerifyDTOList) {
String payload;
try {
payload = objectMapper.writeValueAsString(UKeySignEntity.getInstance(
uKeySignVerifyDTO.getuKeySignDTO(), iPubKey));
} catch (JsonProcessingException e) {
throw new IllegalStateException("serialize ukey sign payload failed", e);
}
boolean verified = Sm2SignatureUtil.verifyBase64Signature(
uKeySignVerifyDTO.getuKeySignDTO().getPubKey(),
payload,
uKeySignVerifyDTO.getSign());
result = result && verified;
}
if (!result) {
throw new IllegalArgumentException("ukey issue sign verify failed");
}
}
private void userInfoCheck(UserInfo userInfo) {
List<UserInfo> config = loginProperties.getAuthenticationInfoList();
if (config == null || config.isEmpty()) {
throw new IllegalStateException("ukey authentication config missing");
}
boolean result = userInfo.roleUserAuth(config);
if (!result) {
throw new IllegalArgumentException("auth info incorrect");
}
}
private void throwIfLmkNotGenerated() {
int masterKeyCode = lmkService.getMasterKeyStatus().getCode();
if (MasterKeyStatus.ABNORMAL.getCode() == masterKeyCode) {
throw new IllegalStateException("master key not generated");
}
}
private void randomCheck(List<String> incomingRandoms) {
if (incomingRandoms == null || incomingRandoms.isEmpty()) {
throw new IllegalArgumentException("random number input error");
}
List<String> currentRandomList = this.randomList;
if (currentRandomList == null || currentRandomList.isEmpty()) {
throw new IllegalStateException("random list not initialized");
}
for (String r : incomingRandoms) {
if (!currentRandomList.contains(r)) {
throw new IllegalArgumentException("random number input error");
}
}
}
private void loginSignCheck(List<LoginSignDTO> loginSignDTOList) {
boolean result = true;
for (LoginSignDTO loginSignDTO : loginSignDTOList) {
boolean verified = Sm2SignatureUtil.verifyBase64Signature(
loginSignDTO.getPubKey(),
loginSignDTO.getLoginSignData(),
loginSignDTO.getSignValue());
result = result && verified;
}
if (!result) {
throw new IllegalArgumentException("login sign verify failed");
}
}
private void whitelistCheck(HttpServletRequest request) {
String clientIp = IpWhitelistUtil.getClientIp(request);
List<String> whitelist = IpWhitelistUtil.readWhitelist(resolveWhitelistPath());
if (!whitelist.isEmpty()) {
if (!IpWhitelistUtil.isIpInWhitelist(clientIp, whitelist)) {
throw new IllegalArgumentException("access denied");
}
}
}
private String resolveWhitelistPath() {
String configured = loginProperties.getWhitelistPath();
if (configured != null && !configured.isBlank()) {
return configured;
}
return System.getProperty("user.dir") + "/config/whitelist.txt";
}
private void checkRolePassword(String role, String password) {
Map<String, String> rolePasswords = loginProperties.getRolePasswords();
if (rolePasswords == null || rolePasswords.isEmpty()) {
throw new IllegalStateException("role password config missing");
}
String expected = rolePasswords.get(role);
if (expected == null) {
throw new IllegalArgumentException("role not found: " + role);
}
if (!expected.equals(password)) {
throw new IllegalArgumentException("password check failed");
}
}
public void setRandomList(List<String> randomList) {
this.randomList = randomList;
}
}