fix:角色授权修改

This commit is contained in:
waner
2026-04-25 14:56:32 +08:00
parent 779052142b
commit 7bc364f367
17 changed files with 321 additions and 225 deletions
@@ -96,7 +96,8 @@ public class WebMvcConfig implements WebMvcConfigurer {
"/api/v1/auth/password-login",
"/api/v1/auth/ukey-login",
"/api/v1/auth/ukey-login/randoms",
"/api/v1/auth/captcha"
"/api/v1/auth/captcha",
"/api/v1/auth/super-admin/ukeys/issue-sign"
);
registry.addInterceptor(internalApiReplayInterceptor)
@@ -1,8 +1,9 @@
package com.cisd.tms.modules.auth.controller;
import com.cisd.tms.common.api.ApiResponse;
import com.cisd.tms.common.enums.ErrorCode;
import com.cisd.tms.common.exception.BizException;
import com.cisd.tms.modules.auth.dto.AdminChangePasswordRequest;
import com.cisd.tms.modules.auth.dto.UkeyBindRequest;
import com.cisd.tms.modules.auth.enums.AuthLevel;
import com.cisd.tms.modules.auth.enums.RoleCode;
import com.cisd.tms.modules.auth.security.RequireInternalAuth;
@@ -24,14 +25,14 @@ import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/v1/auth")
@RequiredArgsConstructor
@ReplayProtected
@Tag(name = "认证管理", description = "角色启用、密码重置与 UKey 绑定接口")
public class AuthAdminController {
private final AuthAdminService authAdminService;
@PostMapping("/roles/{roleCode}/enable")
@Operation(summary = "启用角色", description = "仅允许 KEY_ADMIN FULL 会话启用目标角色。")
@Operation(summary = "启用角色", description = "仅允许 SUPER_ADMIN FULL 会话启用目标角色。")
@ReplayProtected
@RequireInternalAuth(role = RoleCode.SUPER_ADMIN, authLevel = AuthLevel.FULL)
@AuditedOperation(module = ModuleCode.AUTH, action = ActionType.ENABLE, summary = "启用角色")
public ApiResponse<Void> enableRole(@PathVariable("roleCode") String roleCode, HttpServletRequest request) {
@@ -44,7 +45,8 @@ public class AuthAdminController {
}
@PostMapping("/roles/{roleCode}/reset-password")
@Operation(summary = "重置角色密码", description = "仅允许 KEY_ADMIN FULL 会话重置目标角色密码。")
@Operation(summary = "重置角色密码", description = "仅允许 SUPER_ADMIN FULL 会话重置目标角色密码。")
@ReplayProtected
@RequireInternalAuth(role = RoleCode.SUPER_ADMIN, authLevel = AuthLevel.FULL)
@AuditedOperation(module = ModuleCode.AUTH, action = ActionType.RESET, summary = "重置角色密码")
public ApiResponse<Void> resetPassword(@PathVariable("roleCode") String roleCode, HttpServletRequest request) {
@@ -58,6 +60,7 @@ public class AuthAdminController {
@PostMapping("/roles/{roleCode}/full-accounts/{uid}/change-password")
@Operation(summary = "管理员修改 FULL 账户口令", description = "管理员为目标角色指定 UKey 席位账号设置新口令。")
@ReplayProtected
// @RequireInternalAuth(role = RoleCode.SUPER_ADMIN, authLevel = AuthLevel.FULL)
@AuditedOperation(module = ModuleCode.AUTH, action = ActionType.UPDATE, summary = "管理员修改 FULL 账户口令")
public ApiResponse<Void> changeFullAccountPassword(
@@ -79,6 +82,7 @@ public class AuthAdminController {
@PostMapping("/roles/{roleCode}/limited-accounts/{username}/change-password")
@Operation(summary = "管理员修改 LIMITED 账户口令", description = "管理员为目标角色指定独立用户账号设置新口令。")
@ReplayProtected
// @RequireInternalAuth(role = RoleCode.SUPER_ADMIN, authLevel = AuthLevel.FULL)
@AuditedOperation(module = ModuleCode.AUTH, action = ActionType.UPDATE, summary = "管理员修改 LIMITED 账户口令")
public ApiResponse<Void> changeLimitedAccountPassword(
@@ -98,35 +102,31 @@ public class AuthAdminController {
return ApiResponse.success();
}
@PostMapping("/roles/{roleCode}/ukeys/bind")
@Operation(summary = "绑定角色 UKey", description = "仅允许 KEY_ADMIN FULL 会话登记目标角色的 UKey 绑定信息。")
@RequireInternalAuth(role = RoleCode.SUPER_ADMIN, authLevel = AuthLevel.FULL)
@AuditedOperation(module = ModuleCode.AUTH, action = ActionType.BIND, summary = "绑定角色 UKey")
public ApiResponse<Void> bindUkey(
@PathVariable("roleCode") String roleCode,
@Valid @RequestBody UkeyBindRequest request,
HttpServletRequest httpRequest
@PostMapping("/super-admin/ukeys/issue-sign")
@Operation(summary = "签发并绑定超级管理员 UKey", description = "系统初始化后登记超级管理员 UKey,不要求已有登录态。")
public ApiResponse<UKeySignResult> issueSuperAdminUkeyBindingSign(
@Valid @RequestBody UKeySignDTO request
) {
authAdminService.bindIssuedUkey(
(String) httpRequest.getAttribute(InternalApiAuthInterceptor.ATTR_ROLE_CODE),
(String) httpRequest.getAttribute(InternalApiAuthInterceptor.ATTR_AUTH_LEVEL),
roleCode,
request.getUid(),
request.getUkeySerial(),
request.getPubKey(),
request.getIssuerSignature()
);
return ApiResponse.success();
return ApiResponse.success(authAdminService.issueUkeyBindingSign(
null,
null,
RoleCode.SUPER_ADMIN.getCode(),
request
));
}
@PostMapping("/roles/{roleCode}/ukeys/issue-sign")
@Operation(summary = "生成 UKey 发行签名", description = "按旧绑定流程为目标角色 UKey 材料生成发行签名。")
// @RequireInternalAuth(role = RoleCode.SUPER_ADMIN, authLevel = AuthLevel.LIMITED)
@Operation(summary = "签发并绑定其他管理员 UKey", description = "仅允许 SUPER_ADMIN FULL 会话登记其他管理员的 UKey。")
@ReplayProtected
@RequireInternalAuth(role = RoleCode.SUPER_ADMIN, authLevel = AuthLevel.FULL)
public ApiResponse<UKeySignResult> issueUkeyBindingSign(
@PathVariable("roleCode") String roleCode,
@RequestBody UKeySignDTO request,
@Valid @RequestBody UKeySignDTO request,
HttpServletRequest httpRequest
) {
if (RoleCode.SUPER_ADMIN.getCode().equals(roleCode)) {
throw new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "use super-admin ukey binding endpoint");
}
return ApiResponse.success(authAdminService.issueUkeyBindingSign(
(String) httpRequest.getAttribute(InternalApiAuthInterceptor.ATTR_ROLE_CODE),
(String) httpRequest.getAttribute(InternalApiAuthInterceptor.ATTR_AUTH_LEVEL),
@@ -1,57 +0,0 @@
package com.cisd.tms.modules.auth.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.NotBlank;
@Schema(description = "UKey 绑定请求")
public class UkeyBindRequest {
@NotNull(message = "uid is required")
@Schema(description = "角色内固定席位编号", example = "1")
private Integer uid;
@NotBlank(message = "ukeySerial is required")
@Schema(description = "UKey 序列号", example = "UK-001")
private String ukeySerial;
@NotBlank(message = "pubKey is required")
@Schema(description = "UKey 公钥")
private String pubKey;
@NotBlank(message = "issuerSignature is required")
@Schema(description = "认证公钥签名值")
private String issuerSignature;
public Integer getUid() {
return uid;
}
public void setUid(Integer uid) {
this.uid = uid;
}
public String getUkeySerial() {
return ukeySerial;
}
public void setUkeySerial(String ukeySerial) {
this.ukeySerial = ukeySerial;
}
public String getPubKey() {
return pubKey;
}
public void setPubKey(String pubKey) {
this.pubKey = pubKey;
}
public String getIssuerSignature() {
return issuerSignature;
}
public void setIssuerSignature(String issuerSignature) {
this.issuerSignature = issuerSignature;
}
}
@@ -9,7 +9,7 @@ import org.apache.ibatis.annotations.Param;
@Mapper
public interface RoleUkeyBindingMapper extends BaseMapperX<RoleUkeyBindingEntity> {
RoleUkeyBindingEntity selectActiveByRoleCodeAndUid(@Param("roleCode") String roleCode, @Param("uid") Integer uid);
List<RoleUkeyBindingEntity> selectActiveByRoleCodeAndUid(@Param("roleCode") String roleCode, @Param("uid") Integer uid);
List<RoleUkeyBindingEntity> selectActiveByRoleCode(@Param("roleCode") String roleCode);
}
@@ -2,11 +2,10 @@ package com.cisd.tms.modules.auth.repository;
import com.cisd.tms.modules.auth.entity.RoleUkeyBindingEntity;
import java.util.List;
import java.util.Optional;
public interface RoleUkeyBindingRepository {
Optional<RoleUkeyBindingEntity> findActiveByRoleCodeAndUid(String roleCode, Integer uid);
List<RoleUkeyBindingEntity> findActiveByRoleCodeAndUid(String roleCode, Integer uid);
List<RoleUkeyBindingEntity> findActiveByRoleCode(String roleCode);
@@ -4,7 +4,6 @@ import com.cisd.tms.modules.auth.entity.RoleUkeyBindingEntity;
import com.cisd.tms.modules.auth.mapper.RoleUkeyBindingMapper;
import com.cisd.tms.modules.auth.repository.RoleUkeyBindingRepository;
import java.util.List;
import java.util.Optional;
import org.springframework.stereotype.Repository;
@Repository
@@ -17,8 +16,8 @@ public class RoleUkeyBindingRepositoryImpl implements RoleUkeyBindingRepository
}
@Override
public Optional<RoleUkeyBindingEntity> findActiveByRoleCodeAndUid(String roleCode, Integer uid) {
return Optional.ofNullable(roleUkeyBindingMapper.selectActiveByRoleCodeAndUid(roleCode, uid));
public List<RoleUkeyBindingEntity> findActiveByRoleCodeAndUid(String roleCode, Integer uid) {
return roleUkeyBindingMapper.selectActiveByRoleCodeAndUid(roleCode, uid);
}
@Override
@@ -27,26 +27,6 @@ public interface AuthAdminService {
String newPassword
);
void bindUkey(
String operatorRoleCode,
String operatorAuthLevel,
String targetRoleCode,
Integer uid,
String ukeySerial,
String ukeyPubkey,
String issuerSign
);
void bindIssuedUkey(
String operatorRoleCode,
String operatorAuthLevel,
String targetRoleCode,
Integer uid,
String ukeySerial,
String ukeyPubkey,
String issuerSign
);
UKeySignResult issueUkeyBindingSign(
String operatorRoleCode,
String operatorAuthLevel,
@@ -174,10 +174,7 @@ public class AuthAdminServiceImpl implements AuthAdminService {
authUserAccountRepository.update(account);
}
@Override
public void bindUkey(
String operatorRoleCode,
String operatorAuthLevel,
private void bindUkey(
String targetRoleCode,
Integer uid,
String ukeySerial,
@@ -185,15 +182,11 @@ public class AuthAdminServiceImpl implements AuthAdminService {
String issuerSign
) {
RoleCode targetRole = resolveRoleCode(targetRoleCode);
if (uid == null || uid < 1 || uid > targetRole.getRequiredUkeyCount()) {
throw new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "uid exceeds role ukey requirement");
}
validateRoleUid(targetRole, uid);
authFullAccountRepository.findByRoleCodeAndUid(targetRoleCode, uid)
.orElseThrow(() -> new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "target role full account not found"));
RoleUkeyBindingEntity binding = roleUkeyBindingRepository
.findActiveByRoleCodeAndUid(targetRoleCode, uid)
.orElseGet(RoleUkeyBindingEntity::new);
RoleUkeyBindingEntity binding = findExistingActiveBinding(targetRoleCode, uid, ukeySerial, ukeyPubkey);
if (binding.getId() == null) {
binding.setId((long) Math.abs(Objects.hash(targetRoleCode, uid, ukeySerial, TraceIdUtil.newTraceId())));
binding.setRoleCode(targetRoleCode);
@@ -208,19 +201,17 @@ public class AuthAdminServiceImpl implements AuthAdminService {
roleUkeyBindingRepository.update(binding);
}
@Override
public void bindIssuedUkey(
String operatorRoleCode,
String operatorAuthLevel,
String targetRoleCode,
private RoleUkeyBindingEntity findExistingActiveBinding(
String roleCode,
Integer uid,
String ukeySerial,
String ukeyPubkey,
String issuerSign
String ukeyPubkey
) {
resolveRoleCode(targetRoleCode);
verifyIssuerSignature(targetRoleCode, ukeyPubkey, uid, issuerSign);
bindUkey(operatorRoleCode, operatorAuthLevel, targetRoleCode, uid, ukeySerial, ukeyPubkey, issuerSign);
return roleUkeyBindingRepository.findActiveByRoleCodeAndUid(roleCode, uid).stream()
.filter(binding -> Objects.equals(binding.getUkeySerial(), ukeySerial)
|| Objects.equals(binding.getUkeyPubkey(), ukeyPubkey))
.findFirst()
.orElseGet(RoleUkeyBindingEntity::new);
}
@Override
@@ -231,18 +222,28 @@ public class AuthAdminServiceImpl implements AuthAdminService {
UKeySignDTO request
) {
RoleCode targetRole = resolveRoleCode(targetRoleCode);
validateRoleUid(targetRole, request.getUid());
String authKeyPair = lmkService.exportIkPublicKeyHex();
UKeySignDTO dto = new UKeySignDTO();
dto.setPubKey(request.getPubKey());
dto.setRole(targetRole.getCode());
dto.setUid(request.getUid());
String signValue = lmkService.signIk(toIssuePayload(dto, authKeyPair));
String signValue = lmkService.signIk(toIssuePayload(dto, authKeyPair, targetRole.getCode()));
MasterKeyBackupPacket backupPacket = null;
if (request.getUkeySerial() == null || request.getUkeySerial().isBlank()) {
throw new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "ukeySerial is required");
}
if (RoleCode.SUPER_ADMIN == targetRole) {
backupPacket = lmkService.buildBackupPacket(request.getUid());
}
bindUkey(
targetRole.getCode(),
request.getUid(),
request.getUkeySerial(),
request.getPubKey(),
signValue
);
return UKeySignResult.builder()
.sign(signValue)
.backupPacket(backupPacket)
@@ -295,6 +296,12 @@ public class AuthAdminServiceImpl implements AuthAdminService {
.orElseThrow(() -> new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "target role account not found"));
}
private void validateRoleUid(RoleCode targetRole, Integer uid) {
if (uid == null || uid < 1 || uid > targetRole.getRequiredUkeyCount()) {
throw new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "uid exceeds role ukey requirement");
}
}
private RoleCode resolveRoleCode(String roleCode) {
return List.of(RoleCode.values()).stream()
.filter(item -> item.getCode().equals(roleCode))
@@ -306,24 +313,9 @@ public class AuthAdminServiceImpl implements AuthAdminService {
return LocalDateTime.ofInstant(clock.instant(), ZoneOffset.UTC);
}
private void verifyIssuerSignature(String targetRoleCode, String ukeyPubkey, Integer uid, String issuerSign) {
private String toIssuePayload(UKeySignDTO dto, String authKeyPair, String roleCode) {
try {
UKeySignDTO dto = new UKeySignDTO();
dto.setPubKey(ukeyPubkey);
dto.setRole(resolveRoleCode(targetRoleCode).getCode());
dto.setUid(uid);
String authKeyPair = lmkService.exportIkPublicKeyHex();
lmkService.verifyIk(toIssuePayload(dto, authKeyPair), issuerSign);
} catch (BizException ex) {
throw ex;
} catch (RuntimeException ex) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "ukey issuer signature verification failed");
}
}
private String toIssuePayload(UKeySignDTO dto, String authKeyPair) {
try {
return objectMapper.writeValueAsString(UKeySignEntity.getInstance(dto, authKeyPair));
return objectMapper.writeValueAsString(UKeySignEntity.getInstance(dto, authKeyPair, roleCode));
} catch (JsonProcessingException ex) {
throw new IllegalStateException("serialize ukey issue payload failed", ex);
}
@@ -152,14 +152,8 @@ public class AuthServiceImpl implements AuthService {
RoleCode roleCode = RoleCode.valueOf(request.getRoleCode());
List<RoleUkeyBindingEntity> activeBindings = roleUkeyBindingRepository.findActiveByRoleCode(roleCode.getCode());
validateUkeyCount(roleCode, activeBindings, request.getLoginFactors());
Map<Integer, RoleUkeyBindingEntity> bindingsByUid = activeBindings.stream()
.collect(Collectors.toMap(RoleUkeyBindingEntity::getUid, item -> item, (left, right) -> left, java.util.LinkedHashMap::new));
Set<Integer> requestUids = request.getLoginFactors().stream()
.map(UkeyLoginProof::getUid)
.collect(Collectors.toSet());
if (requestUids.size() != request.getLoginFactors().size() || !bindingsByUid.keySet().containsAll(requestUids)) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "ukey auth info does not match bound role");
}
Map<Integer, List<RoleUkeyBindingEntity>> bindingsByUid = activeBindings.stream()
.collect(Collectors.groupingBy(RoleUkeyBindingEntity::getUid));
ukeyLoginRandomService.assertIssued(
roleCode.getCode(),
request.getLoginFactors().stream().map(UkeyLoginProof::getServerRandom).toList()
@@ -167,10 +161,10 @@ public class AuthServiceImpl implements AuthService {
String authKeyPair = lmkService.exportIkPublicKeyHex();
List<String> matchedSerials = new ArrayList<>();
for (UkeyLoginProof proof : request.getLoginFactors()) {
RoleUkeyBindingEntity binding = bindingsByUid.get(proof.getUid());
if (binding == null || !binding.getUkeyPubkey().equals(proof.getPubKey())) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "ukey auth info does not match bound role");
}
RoleUkeyBindingEntity binding = bindingsByUid.getOrDefault(proof.getUid(), List.of()).stream()
.filter(item -> item.getUkeyPubkey().equals(proof.getPubKey()))
.findFirst()
.orElseThrow(() -> new BizException(ErrorCode.UNAUTHORIZED.getCode(), "ukey auth info does not match bound role"));
compatUkeyVerifier.verifyIssuedBinding(buildIssuePayload(request.getRoleCode(), proof, authKeyPair), proof.getIssueSignature());
compatUkeyVerifier.verifyLoginSignature(proof.getPubKey(), proof.getLoginPayload(), proof.getLoginSignature());
matchedSerials.add(binding.getUkeySerial());
@@ -503,16 +497,20 @@ public class AuthServiceImpl implements AuthService {
}
List<RoleUkeyBindingEntity> bindings = roleUkeyBindingRepository.findActiveByRoleCode(roleAccount.getRoleCode());
if (bindings.size() != requiredUkeyCount) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "ukey count does not satisfy role requirement");
}
Set<String> boundSerials = bindings.stream()
.map(RoleUkeyBindingEntity::getUkeySerial)
.collect(Collectors.toSet());
if (!boundSerials.containsAll(uniqueSerials)) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "ukey verification failed");
}
long matchedUidCount = bindings.stream()
.filter(binding -> uniqueSerials.contains(binding.getUkeySerial()))
.map(RoleUkeyBindingEntity::getUid)
.distinct()
.count();
if (matchedUidCount != requiredUkeyCount) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "ukey count does not satisfy role requirement");
}
return authPolicyService.resolveAuthLevel(AuthMethod.UKEY);
}
@@ -524,7 +522,17 @@ public class AuthServiceImpl implements AuthService {
if (proofs == null || proofs.size() != authPolicyService.requiredUkeyCount(roleCode)) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "ukey count does not satisfy role requirement");
}
if (activeBindings.size() != authPolicyService.requiredUkeyCount(roleCode)) {
Set<Integer> requestUids = proofs.stream()
.map(UkeyLoginProof::getUid)
.collect(Collectors.toSet());
int requiredCount = authPolicyService.requiredUkeyCount(roleCode);
if (requestUids.size() != requiredCount) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "ukey count does not satisfy role requirement");
}
Set<Integer> activeUids = activeBindings.stream()
.map(RoleUkeyBindingEntity::getUid)
.collect(Collectors.toSet());
if (!activeUids.containsAll(requestUids)) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "ukey count does not satisfy role requirement");
}
}
@@ -532,10 +540,9 @@ public class AuthServiceImpl implements AuthService {
private String buildIssuePayload(String roleCode, UkeyLoginProof proof, String authKeyPair) {
UKeySignDTO dto = new UKeySignDTO();
dto.setPubKey(proof.getPubKey());
dto.setRole(RoleCode.valueOf(roleCode).getCode());
dto.setUid(proof.getUid());
try {
return objectMapper.writeValueAsString(UKeySignEntity.getInstance(dto, authKeyPair));
return objectMapper.writeValueAsString(UKeySignEntity.getInstance(dto, authKeyPair, RoleCode.valueOf(roleCode).getCode()));
} catch (JsonProcessingException ex) {
throw new IllegalStateException("serialize ukey issue payload failed", ex);
}
@@ -1,5 +1,7 @@
package com.cisd.tms.modules.mk.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
@@ -10,16 +12,19 @@ public class UKeySignDTO {
/**
* UKey 公钥
*/
@NotBlank(message = "pubKey is required")
private String pubKey;
/**
* 签名的角色
* UKey 序列号。发行签名接口会直接完成绑定,因此必传。
*/
private String role;
@NotBlank(message = "ukeySerial is required")
private String ukeySerial;
/**
* 角色内固定席位编号。
*/
@NotNull(message = "uid is required")
private Integer uid;
}
@@ -27,11 +27,11 @@ public class UKeySignEntity {
*/
private Integer uid;
public static UKeySignEntity getInstance(UKeySignDTO uKeySignDTO,String authKeyPair){
public static UKeySignEntity getInstance(UKeySignDTO uKeySignDTO, String authKeyPair, String role) {
return UKeySignEntity.builder()
.pubKey(uKeySignDTO.getPubKey())
.authKeyPair(authKeyPair)
.role(uKeySignDTO.getRole())
.role(role)
.uid(uKeySignDTO.getUid())
.build();
}
@@ -262,7 +262,8 @@ CREATE TABLE IF NOT EXISTS tms_role_ukey_binding (
unbound_at DATETIME(3) NULL,
create_time DATETIME(3) NOT NULL,
update_time DATETIME(3) NOT NULL,
UNIQUE KEY uk_tms_role_ukey_binding_role_uid (role_code, uid),
UNIQUE KEY uk_tms_role_ukey_binding_role_uid_serial (role_code, uid, ukey_serial),
KEY idx_tms_role_ukey_binding_role_uid_status (role_code, uid, status),
KEY idx_tms_role_ukey_binding_role_status (role_code, status)
);
@@ -34,7 +34,7 @@
WHERE role_code = #{roleCode}
AND uid = #{uid}
AND status = 'ACTIVE'
LIMIT 1
ORDER BY bound_at DESC, id DESC
</select>
<select id="selectActiveByRoleCode" parameterType="string" resultMap="RoleUkeyBindingResultMap">