refactor: unify auth api and preserve legacy auth flows

This commit is contained in:
waner 2026-03-30 16:03:52 +08:00
parent b9f561cd1f
commit ba919b4aff
28 changed files with 1185 additions and 874 deletions

View File

@ -0,0 +1,97 @@
package com.cisd.tms.modules.auth.controller;
import com.cisd.tms.common.api.ApiResponse;
import com.cisd.tms.modules.auth.dto.UkeyBindRequest;
import com.cisd.tms.modules.auth.security.RequireAuthLevel;
import com.cisd.tms.modules.auth.security.RequireRole;
import com.cisd.tms.modules.auth.service.AuthAdminService;
import com.cisd.tms.modules.mk.dto.UKeySignDTO;
import com.cisd.tms.modules.mk.dto.UKeySignResult;
import com.cisd.tms.security.internal.InternalApiAuthInterceptor;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.PathVariable;
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;
@RestController
@RequestMapping("/api/v1/auth")
@Tag(name = "认证管理", description = "角色启用、密码重置与 UKey 绑定接口")
public class AuthAdminController {
private final AuthAdminService authAdminService;
public AuthAdminController(AuthAdminService authAdminService) {
this.authAdminService = authAdminService;
}
@PostMapping("/roles/{roleCode}/enable")
@Operation(summary = "启用角色", description = "仅允许 KEY_ADMIN FULL 会话启用目标角色。")
@RequireRole("KEY_ADMIN")
@RequireAuthLevel("FULL")
public ApiResponse<Void> enableRole(@PathVariable("roleCode") String roleCode, HttpServletRequest request) {
authAdminService.enableRole(
(String) request.getAttribute(InternalApiAuthInterceptor.ATTR_ROLE_CODE),
(String) request.getAttribute(InternalApiAuthInterceptor.ATTR_AUTH_LEVEL),
roleCode
);
return ApiResponse.success();
}
@PostMapping("/roles/{roleCode}/reset-password")
@Operation(summary = "重置角色密码", description = "仅允许 KEY_ADMIN FULL 会话重置目标角色密码。")
@RequireRole("KEY_ADMIN")
@RequireAuthLevel("FULL")
public ApiResponse<Void> resetPassword(@PathVariable("roleCode") String roleCode, HttpServletRequest request) {
authAdminService.resetPassword(
(String) request.getAttribute(InternalApiAuthInterceptor.ATTR_ROLE_CODE),
(String) request.getAttribute(InternalApiAuthInterceptor.ATTR_AUTH_LEVEL),
roleCode
);
return ApiResponse.success();
}
@PostMapping("/roles/{roleCode}/ukeys/bind")
@Operation(summary = "绑定角色 UKey", description = "仅允许 KEY_ADMIN FULL 会话登记目标角色的 UKey 绑定信息。")
@RequireRole("KEY_ADMIN")
@RequireAuthLevel("FULL")
public ApiResponse<Void> bindUkey(
@PathVariable("roleCode") String roleCode,
@Valid @RequestBody UkeyBindRequest request,
HttpServletRequest httpRequest
) {
authAdminService.bindIssuedUkey(
(String) httpRequest.getAttribute(InternalApiAuthInterceptor.ATTR_ROLE_CODE),
(String) httpRequest.getAttribute(InternalApiAuthInterceptor.ATTR_AUTH_LEVEL),
roleCode,
request.getSlotNo(),
request.getUkeySerial(),
request.getPubKey(),
request.getUid(),
request.getRid(),
request.getIssuerSignature()
);
return ApiResponse.success();
}
@PostMapping("/roles/{roleCode}/ukeys/issue-sign")
@Operation(summary = "生成 UKey 发行签名", description = "按旧绑定流程为目标角色 UKey 材料生成发行签名。")
@RequireRole("KEY_ADMIN")
@RequireAuthLevel("FULL")
public ApiResponse<UKeySignResult> issueUkeyBindingSign(
@PathVariable("roleCode") String roleCode,
@RequestBody UKeySignDTO request,
HttpServletRequest httpRequest
) {
return ApiResponse.success(authAdminService.issueUkeyBindingSign(
(String) httpRequest.getAttribute(InternalApiAuthInterceptor.ATTR_ROLE_CODE),
(String) httpRequest.getAttribute(InternalApiAuthInterceptor.ATTR_AUTH_LEVEL),
roleCode,
request
));
}
}

View File

@ -1,17 +1,15 @@
package com.cisd.tms.modules.auth.controller;
import com.cisd.tms.common.api.ApiResponse;
import com.cisd.tms.modules.auth.dto.CaptchaResponse;
import com.cisd.tms.modules.auth.dto.ChangePasswordRequest;
import com.cisd.tms.modules.auth.dto.CurrentUserResponse;
import com.cisd.tms.modules.auth.dto.LoginRequest;
import com.cisd.tms.modules.auth.dto.LoginResponse;
import com.cisd.tms.modules.auth.dto.UkeyBindRequest;
import com.cisd.tms.modules.auth.security.RequireAuthLevel;
import com.cisd.tms.modules.auth.security.RequireRole;
import com.cisd.tms.modules.auth.service.AuthAdminService;
import com.cisd.tms.modules.auth.dto.PasswordLoginRequest;
import com.cisd.tms.modules.auth.dto.UkeyLoginRandomRequest;
import com.cisd.tms.modules.auth.dto.UkeyLoginRandomResponse;
import com.cisd.tms.modules.auth.dto.UkeyLoginRequest;
import com.cisd.tms.modules.auth.service.AuthService;
import com.cisd.tms.modules.mk.dto.UKeySignDTO;
import com.cisd.tms.modules.mk.dto.UKeySignResult;
import com.cisd.tms.security.internal.InternalApiAuthInterceptor;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
@ -20,46 +18,61 @@ import jakarta.validation.Valid;
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.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/v1/auth")
@Tag(name = "认证鉴权", description = "内部用户登录与当前用户信息接口")
@Tag(name = "认证鉴权", description = "管理端认证与当前会话接口")
public class AuthController {
private final AuthService authService;
private final AuthAdminService authAdminService;
public AuthController(AuthService authService, AuthAdminService authAdminService) {
public AuthController(AuthService authService) {
this.authService = authService;
this.authAdminService = authAdminService;
}
@PostMapping("/login")
@Operation(summary = "内部用户登录", description = "校验角色口令,可选校验 UKey 序列号,返回访问令牌和认证等级。")
public ApiResponse<LoginResponse> login(@Valid @RequestBody LoginRequest request) {
return ApiResponse.success(authService.login(request));
@PostMapping("/password-login")
@Operation(summary = "口令登录", description = "按旧系统受限登录流程签发 LIMITED 会话。")
public ApiResponse<LoginResponse> passwordLogin(@Valid @RequestBody PasswordLoginRequest request) {
return ApiResponse.success(authService.passwordLogin(request));
}
@PostMapping("/ukey-login/randoms")
@Operation(summary = "申请 UKey 登录随机数", description = "按角色要求下发 UKey 登录签名随机数。")
public ApiResponse<UkeyLoginRandomResponse> issueUkeyLoginRandoms(@Valid @RequestBody UkeyLoginRandomRequest request) {
return ApiResponse.success(authService.issueUkeyLoginRandoms(request));
}
@PostMapping("/ukey-login")
@Operation(summary = "UKey 登录", description = "按旧系统标准 UKey 校验顺序签发 FULL 会话。")
public ApiResponse<LoginResponse> ukeyLogin(@Valid @RequestBody UkeyLoginRequest request) {
return ApiResponse.success(authService.ukeyLogin(request));
}
@PostMapping("/captcha")
@Operation(summary = "生成验证码", description = "为口令登录流程生成一次性验证码。")
public ApiResponse<CaptchaResponse> issueCaptcha() {
return ApiResponse.success(authService.issueCaptcha());
}
@GetMapping("/me")
@Operation(summary = "查询当前用户", description = "根据内部鉴权链路透传的当前角色标识返回当前登录用户信息。")
@Operation(summary = "查询当前用户", description = "根据内部鉴权上下文返回当前会话信息。")
public ApiResponse<CurrentUserResponse> me(HttpServletRequest request) {
String user = (String) request.getAttribute(com.cisd.tms.security.internal.InternalApiAuthInterceptor.ATTR_ROLE_CODE);
String sessionToken = (String) request.getAttribute(com.cisd.tms.security.internal.InternalApiAuthInterceptor.ATTR_SESSION_TOKEN);
String user = (String) request.getAttribute(InternalApiAuthInterceptor.ATTR_ROLE_CODE);
String sessionToken = (String) request.getAttribute(InternalApiAuthInterceptor.ATTR_SESSION_TOKEN);
return ApiResponse.success(authService.me(user, sessionToken));
}
@PostMapping("/logout")
@Operation(summary = "退出当前会话", description = "使当前内部管理端会话失效。")
@Operation(summary = "退出当前会话", description = "使当前会话失效。")
public ApiResponse<Void> logout(HttpServletRequest request) {
authService.logout((String) request.getAttribute(InternalApiAuthInterceptor.ATTR_SESSION_TOKEN));
return ApiResponse.success();
}
@PostMapping("/change-password")
@Operation(summary = "修改当前角色口令", description = "基于当前登录会话校验并更新角色口令。")
@Operation(summary = "修改当前角色口令", description = "基于当前会话校验并更新当前角色口令。")
public ApiResponse<Void> changePassword(@Valid @RequestBody ChangePasswordRequest request, HttpServletRequest httpRequest) {
authService.changePassword(
(String) httpRequest.getAttribute(InternalApiAuthInterceptor.ATTR_SESSION_TOKEN),
@ -68,70 +81,4 @@ public class AuthController {
);
return ApiResponse.success();
}
@PostMapping("/roles/{roleCode}/enable")
@Operation(summary = "启用角色", description = "仅允许密钥管理员 FULL 会话启用目标角色。")
@RequireRole("KEY_ADMIN")
@RequireAuthLevel("FULL")
public ApiResponse<Void> enableRole(@PathVariable("roleCode") String roleCode, HttpServletRequest request) {
authAdminService.enableRole(
(String) request.getAttribute(InternalApiAuthInterceptor.ATTR_ROLE_CODE),
(String) request.getAttribute(InternalApiAuthInterceptor.ATTR_AUTH_LEVEL),
roleCode
);
return ApiResponse.success();
}
@PostMapping("/roles/{roleCode}/reset-password")
@Operation(summary = "重置角色密码", description = "仅允许密钥管理员 FULL 会话重置目标角色密码。")
@RequireRole("KEY_ADMIN")
@RequireAuthLevel("FULL")
public ApiResponse<Void> resetPassword(@PathVariable("roleCode") String roleCode, HttpServletRequest request) {
authAdminService.resetPassword(
(String) request.getAttribute(InternalApiAuthInterceptor.ATTR_ROLE_CODE),
(String) request.getAttribute(InternalApiAuthInterceptor.ATTR_AUTH_LEVEL),
roleCode
);
return ApiResponse.success();
}
@PostMapping("/roles/{roleCode}/ukeys/bind")
@Operation(summary = "绑定角色 UKey", description = "仅允许密钥管理员 FULL 会话绑定或覆盖目标角色槽位上的 UKey。")
@RequireRole("KEY_ADMIN")
@RequireAuthLevel("FULL")
public ApiResponse<Void> bindUkey(
@PathVariable("roleCode") String roleCode,
@Valid @RequestBody UkeyBindRequest request,
HttpServletRequest httpRequest
) {
authAdminService.bindIssuedUkey(
(String) httpRequest.getAttribute(InternalApiAuthInterceptor.ATTR_ROLE_CODE),
(String) httpRequest.getAttribute(InternalApiAuthInterceptor.ATTR_AUTH_LEVEL),
roleCode,
request.getSlotNo(),
request.getUkeySerial(),
request.getUkeyPubkey(),
request.getUid(),
request.getRid(),
request.getIssuerSign()
);
return ApiResponse.success();
}
@PostMapping("/roles/{roleCode}/ukeys/issue-sign")
@Operation(summary = "生成绑定用 UKey 发行签名", description = "按旧管理端绑定流程为目标角色的 UKey 信息出具发行签名。")
@RequireRole("KEY_ADMIN")
@RequireAuthLevel("FULL")
public ApiResponse<UKeySignResult> issueUkeyBindingSign(
@PathVariable("roleCode") String roleCode,
@RequestBody UKeySignDTO request,
HttpServletRequest httpRequest
) {
return ApiResponse.success(authAdminService.issueUkeyBindingSign(
(String) httpRequest.getAttribute(InternalApiAuthInterceptor.ATTR_ROLE_CODE),
(String) httpRequest.getAttribute(InternalApiAuthInterceptor.ATTR_AUTH_LEVEL),
roleCode,
request
));
}
}

View File

@ -1,50 +0,0 @@
package com.cisd.tms.modules.auth.controller;
import com.cisd.tms.common.api.ApiResponse;
import com.cisd.tms.modules.auth.dto.CompatPasswordLoginRequest;
import com.cisd.tms.modules.auth.dto.CompatUkeyLoginRequest;
import com.cisd.tms.modules.auth.dto.LoginResponse;
import com.cisd.tms.modules.auth.dto.UkeyLoginRandomResponse;
import com.cisd.tms.modules.auth.service.CompatAuthService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
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;
@RestController
@RequestMapping("/api/v1/auth")
@Tag(name = "认证兼容接口", description = "兼容旧管理端的口令登录、UKey 登录与随机数接口")
public class CompatAuthController {
private final CompatAuthService compatAuthService;
public CompatAuthController(CompatAuthService compatAuthService) {
this.compatAuthService = compatAuthService;
}
@GetMapping("/ukey-login/randoms")
@Operation(summary = "生成 UKey 登录随机数", description = "兼容旧登录流程,为指定角色生成一组待签名随机数。")
public ApiResponse<UkeyLoginRandomResponse> issueUkeyLoginRandoms(
@RequestParam("role") String role,
@RequestParam(value = "count", required = false) Integer count
) {
return ApiResponse.success(compatAuthService.issueUkeyLoginRandoms(role, count));
}
@PostMapping("/password-login")
@Operation(summary = "兼容口令登录", description = "兼容旧管理端的角色口令登录语义,底层复用当前会话体系。")
public ApiResponse<LoginResponse> passwordLogin(@Valid @RequestBody CompatPasswordLoginRequest request) {
return ApiResponse.success(compatAuthService.passwordLogin(request));
}
@PostMapping("/ukey-login")
@Operation(summary = "兼容 UKey 登录", description = "兼容旧管理端的 UKey 登录语义,校验发行签名、随机数和登录签名。")
public ApiResponse<LoginResponse> ukeyLogin(@Valid @RequestBody CompatUkeyLoginRequest request) {
return ApiResponse.success(compatAuthService.ukeyLogin(request));
}
}

View File

@ -0,0 +1,29 @@
package com.cisd.tms.modules.auth.dto;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "验证码响应")
public class CaptchaResponse {
@Schema(description = "验证码标识", example = "captcha-001")
private String captchaId;
@Schema(description = "Base64 编码的验证码图片", example = "iVBORw0KGgoAAAANSUhEUg...")
private String imageBase64;
public String getCaptchaId() {
return captchaId;
}
public void setCaptchaId(String captchaId) {
this.captchaId = captchaId;
}
public String getImageBase64() {
return imageBase64;
}
public void setImageBase64(String imageBase64) {
this.imageBase64 = imageBase64;
}
}

View File

@ -1,32 +0,0 @@
package com.cisd.tms.modules.auth.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
@Schema(description = "兼容旧管理端的口令登录请求")
public class CompatPasswordLoginRequest {
@NotBlank(message = "role is required")
@Schema(description = "角色标识,兼容旧角色名称", example = "auditadmin")
private String role;
@NotBlank(message = "password is required")
@Schema(description = "登录口令", example = "12345678")
private String password;
public String getRole() {
return role;
}
public void setRole(String role) {
this.role = role;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}

View File

@ -1,132 +0,0 @@
package com.cisd.tms.modules.auth.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import java.util.List;
@Schema(description = "兼容旧管理端的 UKey 登录请求")
public class CompatUkeyLoginRequest {
@NotBlank(message = "role is required")
@Schema(description = "角色标识,兼容旧角色名称", example = "keyadmin")
private String role;
@NotBlank(message = "password is required")
@Schema(description = "登录口令", example = "12345678")
private String password;
@Valid
@NotEmpty(message = "authInfo is required")
@Schema(description = "UKey 登录认证材料")
private List<LoginAuthInfo> authInfo;
public String getRole() {
return role;
}
public void setRole(String role) {
this.role = role;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public List<LoginAuthInfo> getAuthInfo() {
return authInfo;
}
public void setAuthInfo(List<LoginAuthInfo> authInfo) {
this.authInfo = authInfo;
}
@Schema(description = "单个 UKey 登录认证项")
public static class LoginAuthInfo {
@NotBlank(message = "pubKey is required")
private String pubKey;
@NotBlank(message = "uid is required")
private String uid;
@NotBlank(message = "rid is required")
private String rid;
@NotBlank(message = "ra is required")
private String ra;
@NotBlank(message = "rb is required")
private String rb;
@NotBlank(message = "issueSign is required")
private String issueSign;
@NotBlank(message = "loginSignData is required")
private String loginSignData;
@NotBlank(message = "loginSign is required")
private String loginSign;
public String getPubKey() {
return pubKey;
}
public void setPubKey(String pubKey) {
this.pubKey = pubKey;
}
public String getUid() {
return uid;
}
public void setUid(String uid) {
this.uid = uid;
}
public String getRid() {
return rid;
}
public void setRid(String rid) {
this.rid = rid;
}
public String getRa() {
return ra;
}
public void setRa(String ra) {
this.ra = ra;
}
public String getRb() {
return rb;
}
public void setRb(String rb) {
this.rb = rb;
}
public String getIssueSign() {
return issueSign;
}
public void setIssueSign(String issueSign) {
this.issueSign = issueSign;
}
public String getLoginSignData() {
return loginSignData;
}
public void setLoginSignData(String loginSignData) {
this.loginSignData = loginSignData;
}
public String getLoginSign() {
return loginSign;
}
public void setLoginSign(String loginSign) {
this.loginSign = loginSign;
}
}
}

View File

@ -0,0 +1,59 @@
package com.cisd.tms.modules.auth.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
@Schema(description = "口令登录请求")
public class PasswordLoginRequest {
/**
* 统一使用主角色编码不再暴露旧系统影子角色名
*/
@NotBlank(message = "roleCode is required")
@Schema(description = "角色编码", example = "AUDIT_ADMIN")
private String roleCode;
@NotBlank(message = "password is required")
@Schema(description = "登录口令", example = "12345678")
private String password;
@NotBlank(message = "captchaCode is required")
@Schema(description = "图形验证码", example = "ABCD")
private String captchaCode;
@NotBlank(message = "captchaId is required")
@Schema(description = "验证码标识", example = "captcha-001")
private String captchaId;
public String getRoleCode() {
return roleCode;
}
public void setRoleCode(String roleCode) {
this.roleCode = roleCode;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getCaptchaCode() {
return captchaCode;
}
public void setCaptchaCode(String captchaCode) {
this.captchaCode = captchaCode;
}
public String getCaptchaId() {
return captchaId;
}
public void setCaptchaId(String captchaId) {
this.captchaId = captchaId;
}
}

View File

@ -15,9 +15,9 @@ public class UkeyBindRequest {
@Schema(description = "UKey 序列号", example = "UK-001")
private String ukeySerial;
@NotBlank(message = "ukeyPubkey is required")
@NotBlank(message = "pubKey is required")
@Schema(description = "UKey 公钥")
private String ukeyPubkey;
private String pubKey;
@NotBlank(message = "uid is required")
@Schema(description = "UKey 认证信息 uid", example = "1")
@ -27,9 +27,9 @@ public class UkeyBindRequest {
@Schema(description = "UKey 认证信息 rid", example = "RID-001")
private String rid;
@NotBlank(message = "issuerSign is required")
@NotBlank(message = "issuerSignature is required")
@Schema(description = "认证公钥签名值")
private String issuerSign;
private String issuerSignature;
public Integer getSlotNo() {
return slotNo;
@ -47,20 +47,20 @@ public class UkeyBindRequest {
this.ukeySerial = ukeySerial;
}
public String getUkeyPubkey() {
return ukeyPubkey;
public String getPubKey() {
return pubKey;
}
public void setUkeyPubkey(String ukeyPubkey) {
this.ukeyPubkey = ukeyPubkey;
public void setPubKey(String pubKey) {
this.pubKey = pubKey;
}
public String getIssuerSign() {
return issuerSign;
public String getIssuerSignature() {
return issuerSignature;
}
public void setIssuerSign(String issuerSign) {
this.issuerSign = issuerSign;
public void setIssuerSignature(String issuerSignature) {
this.issuerSignature = issuerSignature;
}
public String getUid() {

View File

@ -0,0 +1,88 @@
package com.cisd.tms.modules.auth.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
@Schema(description = "单个 UKey 登录证明")
public class UkeyLoginProof {
@NotBlank(message = "pubKey is required")
private String pubKey;
@NotBlank(message = "uid is required")
private String uid;
@NotBlank(message = "rid is required")
private String rid;
/**
* 对应旧系统下发的 rb命名改为更明确的服务端随机数
*/
@NotBlank(message = "serverRandom is required")
private String serverRandom;
@NotBlank(message = "issueSignature is required")
private String issueSignature;
@NotBlank(message = "loginPayload is required")
private String loginPayload;
@NotBlank(message = "loginSignature is required")
private String loginSignature;
public String getPubKey() {
return pubKey;
}
public void setPubKey(String pubKey) {
this.pubKey = pubKey;
}
public String getUid() {
return uid;
}
public void setUid(String uid) {
this.uid = uid;
}
public String getRid() {
return rid;
}
public void setRid(String rid) {
this.rid = rid;
}
public String getServerRandom() {
return serverRandom;
}
public void setServerRandom(String serverRandom) {
this.serverRandom = serverRandom;
}
public String getIssueSignature() {
return issueSignature;
}
public void setIssueSignature(String issueSignature) {
this.issueSignature = issueSignature;
}
public String getLoginPayload() {
return loginPayload;
}
public void setLoginPayload(String loginPayload) {
this.loginPayload = loginPayload;
}
public String getLoginSignature() {
return loginSignature;
}
public void setLoginSignature(String loginSignature) {
this.loginSignature = loginSignature;
}
}

View File

@ -0,0 +1,20 @@
package com.cisd.tms.modules.auth.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
@Schema(description = "申请 UKey 登录随机数")
public class UkeyLoginRandomRequest {
@NotBlank(message = "roleCode is required")
@Schema(description = "角色编码", example = "SUPER_ADMIN")
private String roleCode;
public String getRoleCode() {
return roleCode;
}
public void setRoleCode(String roleCode) {
this.roleCode = roleCode;
}
}

View File

@ -0,0 +1,48 @@
package com.cisd.tms.modules.auth.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import java.util.List;
@Schema(description = "UKey 登录请求")
public class UkeyLoginRequest {
@NotBlank(message = "roleCode is required")
@Schema(description = "角色编码", example = "KEY_ADMIN")
private String roleCode;
@NotBlank(message = "password is required")
@Schema(description = "角色口令", example = "12345678")
private String password;
@Valid
@NotEmpty(message = "ukeyProofs is required")
@Schema(description = "UKey 登录证明列表")
private List<UkeyLoginProof> ukeyProofs;
public String getRoleCode() {
return roleCode;
}
public void setRoleCode(String roleCode) {
this.roleCode = roleCode;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public List<UkeyLoginProof> getUkeyProofs() {
return ukeyProofs;
}
public void setUkeyProofs(List<UkeyLoginProof> ukeyProofs) {
this.ukeyProofs = ukeyProofs;
}
}

View File

@ -7,8 +7,21 @@ import java.time.LocalDateTime;
@TableName("tms_auth_session")
public class AuthSessionEntity extends BaseEntity {
/**
* 当前会话令牌
*/
private String sessionToken;
/**
* 当前会话所属角色编码
*/
private String roleCode;
/**
* 当前会话的认证方式区分口令登录和 UKey 登录
*/
private String authMethod;
/**
* 当前会话的认证等级和具体接口权限直接关联
*/
private String authLevel;
private LocalDateTime issuedAt;
private LocalDateTime lastActiveAt;
@ -31,6 +44,14 @@ public class AuthSessionEntity extends BaseEntity {
this.roleCode = roleCode;
}
public String getAuthMethod() {
return authMethod;
}
public void setAuthMethod(String authMethod) {
this.authMethod = authMethod;
}
public String getAuthLevel() {
return authLevel;
}

View File

@ -7,8 +7,22 @@ import java.time.LocalDateTime;
@TableName("tms_role_ukey_binding")
public class RoleUkeyBindingEntity extends BaseEntity {
/**
* 绑定所属角色
*/
private String roleCode;
/**
* 角色下的 UKey 槽位 1 开始和旧系统卡位模型保持一致
*/
private Integer slotNo;
/**
* 旧系统 auth 文件中的 uid登录时要参与认证匹配
*/
private String uid;
/**
* 旧系统 auth 文件中的 rid登录时要参与角色槽位校验
*/
private String rid;
private String ukeySerial;
private String ukeyPubkey;
private String issuerSign;
@ -32,6 +46,22 @@ public class RoleUkeyBindingEntity extends BaseEntity {
this.slotNo = slotNo;
}
public String getUid() {
return uid;
}
public void setUid(String uid) {
this.uid = uid;
}
public String getRid() {
return rid;
}
public void setRid(String rid) {
this.rid = rid;
}
public String getUkeySerial() {
return ukeySerial;
}

View File

@ -0,0 +1,11 @@
package com.cisd.tms.modules.auth.enums;
/**
* 认证方式
* PASSWORD 对应旧项目的受限口令登录
* UKEY 对应旧项目的完整 UKey 登录
*/
public enum AuthMethod {
PASSWORD,
UKEY
}

View File

@ -144,6 +144,8 @@ public class AuthAdminService {
Integer slotNo,
String ukeySerial,
String ukeyPubkey,
String uid,
String rid,
String issuerSign
) {
requireKeyAdminFull(operatorRoleCode, operatorAuthLevel);
@ -160,12 +162,12 @@ public class AuthAdminService {
binding.setRoleCode(targetRoleCode);
binding.setSlotNo(slotNo);
binding.setStatus("ACTIVE");
applyBinding(binding, ukeySerial, ukeyPubkey, issuerSign);
applyBinding(binding, ukeySerial, ukeyPubkey, uid, rid, issuerSign);
roleUkeyBindingRepository.save(binding);
return;
}
applyBinding(binding, ukeySerial, ukeyPubkey, issuerSign);
applyBinding(binding, ukeySerial, ukeyPubkey, uid, rid, issuerSign);
roleUkeyBindingRepository.update(binding);
}
@ -183,7 +185,7 @@ public class AuthAdminService {
requireKeyAdminFull(operatorRoleCode, operatorAuthLevel);
resolveRoleCode(targetRoleCode);
verifyIssuerSignature(targetRoleCode, ukeyPubkey, uid, rid, issuerSign);
bindUkey(operatorRoleCode, operatorAuthLevel, targetRoleCode, slotNo, ukeySerial, ukeyPubkey, issuerSign);
bindUkey(operatorRoleCode, operatorAuthLevel, targetRoleCode, slotNo, ukeySerial, ukeyPubkey, uid, rid, issuerSign);
}
public UKeySignResult issueUkeyBindingSign(
@ -205,13 +207,25 @@ public class AuthAdminService {
dto.setExtra(request.getExtra());
String signValue = lmkService.signIk(toIssuePayload(dto, authKeyPair));
String extra = "";
// 旧系统要求超级管理员发卡时返回主密钥分量其他角色则透传调用方附带信息
String extra = RoleCode.SUPER_ADMIN == targetRole
? lmkService.getComponent(Integer.parseInt(request.getUid()))
: request.getExtra();
return UKeySignResult.builder().sign(signValue).component(extra).build();
}
private void applyBinding(RoleUkeyBindingEntity binding, String ukeySerial, String ukeyPubkey, String issuerSign) {
private void applyBinding(
RoleUkeyBindingEntity binding,
String ukeySerial,
String ukeyPubkey,
String uid,
String rid,
String issuerSign
) {
binding.setUkeySerial(ukeySerial);
binding.setUkeyPubkey(ukeyPubkey);
binding.setUid(uid);
binding.setRid(rid);
binding.setIssuerSign(issuerSign);
binding.setStatus("ACTIVE");
binding.setBoundAt(now());

View File

@ -0,0 +1,24 @@
package com.cisd.tms.modules.auth.service;
import com.cisd.tms.modules.auth.enums.AuthLevel;
import com.cisd.tms.modules.auth.enums.AuthMethod;
import com.cisd.tms.modules.auth.enums.RoleCode;
import org.springframework.stereotype.Service;
@Service
public class AuthPolicyService {
/**
* 统一根据认证方式推导本次登录会话的认证等级
*/
public AuthLevel resolveAuthLevel(AuthMethod authMethod) {
return authMethod == AuthMethod.UKEY ? AuthLevel.FULL : AuthLevel.LIMITED;
}
/**
* 统一读取角色要求的 UKey 数量避免控制器和服务层散落硬编码
*/
public int requiredUkeyCount(RoleCode roleCode) {
return roleCode.getRequiredUkeyCount();
}
}

View File

@ -2,28 +2,45 @@ package com.cisd.tms.modules.auth.service;
import com.cisd.tms.common.enums.ErrorCode;
import com.cisd.tms.common.exception.BizException;
import com.cisd.tms.modules.auth.dto.CaptchaResponse;
import com.cisd.tms.modules.auth.dto.CurrentUserResponse;
import com.cisd.tms.modules.auth.dto.LoginRequest;
import com.cisd.tms.modules.auth.dto.LoginResponse;
import com.cisd.tms.modules.auth.dto.PasswordLoginRequest;
import com.cisd.tms.modules.auth.dto.UkeyLoginProof;
import com.cisd.tms.modules.auth.dto.UkeyLoginRandomRequest;
import com.cisd.tms.modules.auth.dto.UkeyLoginRandomResponse;
import com.cisd.tms.modules.auth.dto.UkeyLoginRequest;
import com.cisd.tms.modules.auth.entity.AuthSessionEntity;
import com.cisd.tms.modules.auth.entity.AuthUserEntity;
import com.cisd.tms.modules.auth.entity.RoleAccountEntity;
import com.cisd.tms.modules.auth.entity.RoleUkeyBindingEntity;
import com.cisd.tms.modules.auth.enums.AuthLevel;
import com.cisd.tms.modules.auth.enums.AuthMethod;
import com.cisd.tms.modules.auth.enums.RoleAccountStatus;
import com.cisd.tms.modules.auth.enums.RoleCode;
import com.cisd.tms.modules.auth.repository.AuthUserRepository;
import com.cisd.tms.modules.auth.repository.AuthSessionRepository;
import com.cisd.tms.modules.auth.repository.RoleAccountRepository;
import com.cisd.tms.modules.auth.repository.RoleUkeyBindingRepository;
import com.cisd.tms.modules.mk.dto.UKeySignDTO;
import com.cisd.tms.modules.mk.dto.UKeySignEntity;
import com.cisd.tms.modules.mk.enums.MasterKeyStatus;
import com.cisd.tms.modules.mk.service.LmkService;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.Base64;
import java.time.Clock;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@ -38,6 +55,11 @@ public class AuthService {
private final AuthSessionRepository authSessionRepository;
private final RoleUkeyBindingRepository roleUkeyBindingRepository;
private final PasswordHasher passwordHasher;
private final LmkService lmkService;
private final UkeyLoginRandomService ukeyLoginRandomService;
private final CompatUkeyVerifier compatUkeyVerifier;
private final CaptchaService captchaService;
private final ObjectMapper objectMapper;
private final Clock clock;
private final Supplier<String> tokenSupplier;
@ -55,6 +77,11 @@ public class AuthService {
authSessionRepository,
roleUkeyBindingRepository,
passwordHasher,
null,
null,
null,
null,
new ObjectMapper(),
Clock.systemUTC(),
() -> "tms-" + UUID.randomUUID()
);
@ -68,12 +95,47 @@ public class AuthService {
PasswordHasher passwordHasher,
Clock clock,
Supplier<String> tokenSupplier
) {
this(
roleAccountRepository,
authUserRepository,
authSessionRepository,
roleUkeyBindingRepository,
passwordHasher,
null,
null,
null,
null,
new ObjectMapper(),
clock,
tokenSupplier
);
}
AuthService(
RoleAccountRepository roleAccountRepository,
AuthUserRepository authUserRepository,
AuthSessionRepository authSessionRepository,
RoleUkeyBindingRepository roleUkeyBindingRepository,
PasswordHasher passwordHasher,
LmkService lmkService,
UkeyLoginRandomService ukeyLoginRandomService,
CompatUkeyVerifier compatUkeyVerifier,
CaptchaService captchaService,
ObjectMapper objectMapper,
Clock clock,
Supplier<String> tokenSupplier
) {
this.roleAccountRepository = roleAccountRepository;
this.authUserRepository = authUserRepository;
this.authSessionRepository = authSessionRepository;
this.roleUkeyBindingRepository = roleUkeyBindingRepository;
this.passwordHasher = passwordHasher;
this.lmkService = lmkService;
this.ukeyLoginRandomService = ukeyLoginRandomService;
this.compatUkeyVerifier = compatUkeyVerifier;
this.captchaService = captchaService;
this.objectMapper = objectMapper;
this.clock = clock;
this.tokenSupplier = tokenSupplier;
}
@ -88,8 +150,9 @@ public class AuthService {
}
resetFailureState(roleAccount);
AuthMethod authMethod = resolveAuthMethod(request.getUkeySerials());
AuthLevel authLevel = resolveAuthLevel(roleAccount, request.getUkeySerials());
AuthSessionEntity session = buildSession(roleAccount.getRoleCode(), authLevel);
AuthSessionEntity session = buildSession(roleAccount.getRoleCode(), authMethod, authLevel);
authSessionRepository.save(session);
LoginResponse response = new LoginResponse();
@ -101,6 +164,71 @@ public class AuthService {
return response;
}
/**
* 标准化口令登录入口当前先复用统一登录实现并签发 LIMITED 会话
*/
public LoginResponse passwordLogin(PasswordLoginRequest request) {
ensureMasterKeyReady();
requireCaptchaService().verify(request.getCaptchaId(), request.getCaptchaCode());
LoginRequest loginRequest = new LoginRequest();
loginRequest.setRoleCode(request.getRoleCode());
loginRequest.setPassword(request.getPassword());
return login(loginRequest);
}
/**
* 标准化 UKey 随机数申请入口当前先按角色要求生成占位随机数后续再接旧系统完整校验链路
*/
public UkeyLoginRandomResponse issueUkeyLoginRandoms(UkeyLoginRandomRequest request) {
RoleCode roleCode = RoleCode.valueOf(request.getRoleCode());
UkeyLoginRandomResponse response = new UkeyLoginRandomResponse();
response.setRoleCode(roleCode.getCode());
response.setRandoms(requireUkeyLoginRandomService().issue(roleCode.getCode(), roleCode.getRequiredUkeyCount()));
return response;
}
/**
* 标准化 UKey 登录入口当前先按证明数量映射到完整登录
*/
public LoginResponse ukeyLogin(UkeyLoginRequest request) {
ensureMasterKeyReady();
RoleCode roleCode = RoleCode.valueOf(request.getRoleCode());
List<RoleUkeyBindingEntity> activeBindings = roleUkeyBindingRepository.findActiveByRoleCode(roleCode.getCode());
validateUkeyCount(roleCode, activeBindings, request.getUkeyProofs());
Map<String, RoleUkeyBindingEntity> bindingsByPubKey = activeBindings.stream()
.collect(Collectors.toMap(RoleUkeyBindingEntity::getUkeyPubkey, item -> item, (left, right) -> left, java.util.LinkedHashMap::new));
Set<String> requestPubKeys = request.getUkeyProofs().stream()
.map(UkeyLoginProof::getPubKey)
.collect(Collectors.toSet());
if (requestPubKeys.size() != request.getUkeyProofs().size() || !bindingsByPubKey.keySet().containsAll(requestPubKeys)) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "ukey auth info does not match bound role");
}
requireUkeyLoginRandomService().assertIssued(
roleCode.getCode(),
request.getUkeyProofs().stream().map(UkeyLoginProof::getServerRandom).toList()
);
String authKeyPair = requireLmkService().exportIkPublicKeyHex();
List<String> matchedSerials = new java.util.ArrayList<>();
for (UkeyLoginProof proof : request.getUkeyProofs()) {
RoleUkeyBindingEntity binding = bindingsByPubKey.get(proof.getPubKey());
requireCompatUkeyVerifier().verifyIssuedBinding(buildIssuePayload(request.getRoleCode(), proof, authKeyPair), proof.getIssueSignature());
requireCompatUkeyVerifier().verifyLoginSignature(proof.getPubKey(), proof.getLoginPayload(), proof.getLoginSignature());
matchedSerials.add(binding.getUkeySerial());
}
LoginRequest loginRequest = new LoginRequest();
loginRequest.setRoleCode(request.getRoleCode());
loginRequest.setPassword(request.getPassword());
loginRequest.setUkeySerials(matchedSerials);
return login(loginRequest);
}
/**
* 标准化验证码生成入口当前先返回可用于联调的占位图像内容
*/
public CaptchaResponse issueCaptcha() {
return requireCaptchaService().issueCaptcha();
}
public CurrentUserResponse me(String username) {
return me(username, null);
}
@ -198,6 +326,18 @@ public class AuthService {
roleAccountRepository.update(roleAccount);
}
private AuthMethod resolveAuthMethod(List<String> ukeySerials) {
return (ukeySerials == null || ukeySerials.isEmpty()) ? AuthMethod.PASSWORD : AuthMethod.UKEY;
}
private void ensureMasterKeyReady() {
LmkService service = requireLmkService();
MasterKeyStatus.StatusDetail status = service.getMasterKeyStatus();
if (status == null || status.getCode() == MasterKeyStatus.ABNORMAL.getCode()) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "master key is not ready");
}
}
private AuthLevel resolveAuthLevel(RoleAccountEntity roleAccount, List<String> ukeySerials) {
if (ukeySerials == null || ukeySerials.isEmpty()) {
return AuthLevel.LIMITED;
@ -222,10 +362,46 @@ public class AuthService {
return AuthLevel.FULL;
}
private AuthSessionEntity buildSession(String roleCode, AuthLevel authLevel) {
private void validateUkeyCount(
RoleCode roleCode,
List<RoleUkeyBindingEntity> activeBindings,
List<UkeyLoginProof> proofs
) {
if (proofs == null || proofs.size() != roleCode.getRequiredUkeyCount()) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "ukey count does not satisfy role requirement");
}
if (activeBindings.size() != roleCode.getRequiredUkeyCount()) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "ukey count does not satisfy role requirement");
}
}
private String buildIssuePayload(String roleCode, UkeyLoginProof proof, String authKeyPair) {
UKeySignDTO dto = new UKeySignDTO();
dto.setPubKey(proof.getPubKey());
dto.setRole(toLegacyRole(RoleCode.valueOf(roleCode)));
dto.setUid(proof.getUid());
dto.setRid(proof.getRid());
try {
return objectMapper.writeValueAsString(UKeySignEntity.getInstance(dto, authKeyPair));
} catch (JsonProcessingException ex) {
throw new IllegalStateException("serialize ukey issue payload failed", ex);
}
}
private String toLegacyRole(RoleCode roleCode) {
return switch (roleCode) {
case SUPER_ADMIN -> "superadmin";
case KEY_ADMIN -> "keyadmin";
case AUDIT_ADMIN -> "auditadmin";
case OPS_ADMIN -> "configadmin";
};
}
private AuthSessionEntity buildSession(String roleCode, AuthMethod authMethod, AuthLevel authLevel) {
LocalDateTime issuedAt = now();
AuthSessionEntity entity = new AuthSessionEntity();
entity.setRoleCode(roleCode);
entity.setAuthMethod(authMethod.name());
entity.setAuthLevel(authLevel.name());
entity.setSessionToken(tokenSupplier.get());
entity.setIssuedAt(issuedAt);
@ -246,4 +422,32 @@ public class AuthService {
}
return session;
}
private LmkService requireLmkService() {
if (lmkService == null) {
throw new IllegalStateException("lmkService is required for this operation");
}
return lmkService;
}
private UkeyLoginRandomService requireUkeyLoginRandomService() {
if (ukeyLoginRandomService == null) {
throw new IllegalStateException("ukeyLoginRandomService is required for this operation");
}
return ukeyLoginRandomService;
}
private CompatUkeyVerifier requireCompatUkeyVerifier() {
if (compatUkeyVerifier == null) {
throw new IllegalStateException("compatUkeyVerifier is required for this operation");
}
return compatUkeyVerifier;
}
private CaptchaService requireCaptchaService() {
if (captchaService == null) {
throw new IllegalStateException("captchaService is required for this operation");
}
return captchaService;
}
}

View File

@ -0,0 +1,10 @@
package com.cisd.tms.modules.auth.service;
import com.cisd.tms.modules.auth.dto.CaptchaResponse;
public interface CaptchaService {
CaptchaResponse issueCaptcha();
void verify(String captchaId, String captchaCode);
}

View File

@ -1,158 +0,0 @@
package com.cisd.tms.modules.auth.service;
import com.cisd.tms.common.enums.ErrorCode;
import com.cisd.tms.common.exception.BizException;
import com.cisd.tms.modules.auth.dto.CompatPasswordLoginRequest;
import com.cisd.tms.modules.auth.dto.CompatUkeyLoginRequest;
import com.cisd.tms.modules.auth.dto.LoginRequest;
import com.cisd.tms.modules.auth.dto.LoginResponse;
import com.cisd.tms.modules.auth.dto.UkeyLoginRandomResponse;
import com.cisd.tms.modules.auth.entity.RoleUkeyBindingEntity;
import com.cisd.tms.modules.auth.enums.RoleCode;
import com.cisd.tms.modules.auth.repository.RoleUkeyBindingRepository;
import com.cisd.tms.modules.mk.dto.UKeySignDTO;
import com.cisd.tms.modules.mk.dto.UKeySignEntity;
import com.cisd.tms.modules.mk.enums.MasterKeyStatus;
import com.cisd.tms.modules.mk.service.LmkService;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import org.springframework.stereotype.Service;
@Service
public class CompatAuthService {
private final AuthService authService;
private final RoleUkeyBindingRepository roleUkeyBindingRepository;
private final LmkService lmkService;
private final UkeyLoginRandomService ukeyLoginRandomService;
private final CompatUkeyVerifier compatUkeyVerifier;
private final ObjectMapper objectMapper;
public CompatAuthService(
AuthService authService,
RoleUkeyBindingRepository roleUkeyBindingRepository,
LmkService lmkService,
UkeyLoginRandomService ukeyLoginRandomService,
CompatUkeyVerifier compatUkeyVerifier,
ObjectMapper objectMapper
) {
this.authService = authService;
this.roleUkeyBindingRepository = roleUkeyBindingRepository;
this.lmkService = lmkService;
this.ukeyLoginRandomService = ukeyLoginRandomService;
this.compatUkeyVerifier = compatUkeyVerifier;
this.objectMapper = objectMapper;
}
public UkeyLoginRandomResponse issueUkeyLoginRandoms(String legacyRole, Integer count) {
RoleCode roleCode = resolveRoleCode(legacyRole);
int resolvedCount = count == null ? roleCode.getRequiredUkeyCount() : count;
UkeyLoginRandomResponse response = new UkeyLoginRandomResponse();
response.setRoleCode(roleCode.getCode());
response.setRandoms(ukeyLoginRandomService.issue(roleCode.getCode(), resolvedCount));
return response;
}
public LoginResponse passwordLogin(CompatPasswordLoginRequest request) {
ensureMasterKeyReady();
RoleCode roleCode = resolveRoleCode(request.getRole());
LoginRequest loginRequest = new LoginRequest();
loginRequest.setRoleCode(roleCode.getCode());
loginRequest.setPassword(request.getPassword());
return authService.login(loginRequest);
}
public LoginResponse ukeyLogin(CompatUkeyLoginRequest request) {
ensureMasterKeyReady();
RoleCode roleCode = resolveRoleCode(request.getRole());
List<RoleUkeyBindingEntity> activeBindings = roleUkeyBindingRepository.findActiveByRoleCode(roleCode.getCode());
validateUkeyCount(roleCode, activeBindings, request.getAuthInfo());
Map<String, RoleUkeyBindingEntity> bindingsByPubKey = activeBindings.stream()
.collect(Collectors.toMap(RoleUkeyBindingEntity::getUkeyPubkey, item -> item, (left, right) -> left, LinkedHashMap::new));
Set<String> requestPubKeys = request.getAuthInfo().stream()
.map(CompatUkeyLoginRequest.LoginAuthInfo::getPubKey)
.collect(Collectors.toSet());
if (requestPubKeys.size() != request.getAuthInfo().size() || !bindingsByPubKey.keySet().containsAll(requestPubKeys)) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "ukey auth info does not match bound role");
}
ukeyLoginRandomService.assertIssued(
roleCode.getCode(),
request.getAuthInfo().stream().map(CompatUkeyLoginRequest.LoginAuthInfo::getRb).toList()
);
String authKeyPair = lmkService.exportIkPublicKeyHex();
List<String> matchedSerials = new ArrayList<>();
for (CompatUkeyLoginRequest.LoginAuthInfo loginAuthInfo : request.getAuthInfo()) {
RoleUkeyBindingEntity binding = bindingsByPubKey.get(loginAuthInfo.getPubKey());
compatUkeyVerifier.verifyIssuedBinding(buildIssuePayload(request.getRole(), loginAuthInfo, authKeyPair), loginAuthInfo.getIssueSign());
compatUkeyVerifier.verifyLoginSignature(loginAuthInfo.getPubKey(), loginAuthInfo.getLoginSignData(), loginAuthInfo.getLoginSign());
matchedSerials.add(binding.getUkeySerial());
}
LoginRequest loginRequest = new LoginRequest();
loginRequest.setRoleCode(roleCode.getCode());
loginRequest.setPassword(request.getPassword());
loginRequest.setUkeySerials(matchedSerials);
return authService.login(loginRequest);
}
private void ensureMasterKeyReady() {
MasterKeyStatus.StatusDetail status = lmkService.getMasterKeyStatus();
if (status == null || status.getCode() == MasterKeyStatus.ABNORMAL.getCode()) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "master key is not ready");
}
}
private void validateUkeyCount(
RoleCode roleCode,
List<RoleUkeyBindingEntity> activeBindings,
List<CompatUkeyLoginRequest.LoginAuthInfo> authInfos
) {
if (authInfos == null || authInfos.size() != roleCode.getRequiredUkeyCount()) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "ukey count does not satisfy role requirement");
}
if (activeBindings.size() != roleCode.getRequiredUkeyCount()) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "ukey count does not satisfy role requirement");
}
}
private String buildIssuePayload(String legacyRole, CompatUkeyLoginRequest.LoginAuthInfo loginAuthInfo, String authKeyPair) {
UKeySignDTO uKeySignDTO = new UKeySignDTO();
uKeySignDTO.setPubKey(loginAuthInfo.getPubKey());
uKeySignDTO.setRole(normalizeLegacyRole(legacyRole));
uKeySignDTO.setUid(loginAuthInfo.getUid());
uKeySignDTO.setRid(loginAuthInfo.getRid());
try {
return objectMapper.writeValueAsString(UKeySignEntity.getInstance(uKeySignDTO, authKeyPair));
} catch (JsonProcessingException ex) {
throw new IllegalStateException("serialize ukey issue payload failed", ex);
}
}
private RoleCode resolveRoleCode(String role) {
return switch (normalizeLegacyRole(role)) {
case "superadmin" -> RoleCode.SUPER_ADMIN;
case "keyadmin" -> RoleCode.KEY_ADMIN;
case "auditadmin" -> RoleCode.AUDIT_ADMIN;
case "configadmin", "opsadmin" -> RoleCode.OPS_ADMIN;
case "super_admin" -> RoleCode.SUPER_ADMIN;
case "key_admin" -> RoleCode.KEY_ADMIN;
case "audit_admin" -> RoleCode.AUDIT_ADMIN;
case "ops_admin" -> RoleCode.OPS_ADMIN;
default -> throw new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "invalid role");
};
}
private String normalizeLegacyRole(String role) {
return role == null ? "" : role.trim().toLowerCase(Locale.ROOT);
}
}

View File

@ -0,0 +1,16 @@
ALTER TABLE tms_auth_session
ADD COLUMN auth_method VARCHAR(32) NULL AFTER role_code;
UPDATE tms_auth_session
SET auth_method = CASE
WHEN auth_level = 'FULL' THEN 'UKEY'
ELSE 'PASSWORD'
END
WHERE auth_method IS NULL;
ALTER TABLE tms_auth_session
MODIFY COLUMN auth_method VARCHAR(32) NOT NULL;
ALTER TABLE tms_role_ukey_binding
ADD COLUMN uid VARCHAR(64) NULL AFTER slot_no,
ADD COLUMN rid VARCHAR(64) NULL AFTER uid;

View File

@ -8,6 +8,7 @@
<id property="id" column="id"/>
<result property="sessionToken" column="session_token"/>
<result property="roleCode" column="role_code"/>
<result property="authMethod" column="auth_method"/>
<result property="authLevel" column="auth_level"/>
<result property="issuedAt" column="issued_at"/>
<result property="lastActiveAt" column="last_active_at"/>
@ -21,6 +22,7 @@
SELECT id,
session_token,
role_code,
auth_method,
auth_level,
issued_at,
last_active_at,

View File

@ -8,6 +8,8 @@
<id property="id" column="id"/>
<result property="roleCode" column="role_code"/>
<result property="slotNo" column="slot_no"/>
<result property="uid" column="uid"/>
<result property="rid" column="rid"/>
<result property="ukeySerial" column="ukey_serial"/>
<result property="ukeyPubkey" column="ukey_pubkey"/>
<result property="issuerSign" column="issuer_sign"/>
@ -22,6 +24,8 @@
SELECT id,
role_code,
slot_no,
uid,
rid,
ukey_serial,
ukey_pubkey,
issuer_sign,
@ -41,6 +45,8 @@
SELECT id,
role_code,
slot_no,
uid,
rid,
ukey_serial,
ukey_pubkey,
issuer_sign,

View File

@ -1,8 +1,10 @@
package com.cisd.tms.modules.auth.controller;
import com.cisd.tms.common.exception.GlobalExceptionHandler;
import com.cisd.tms.modules.auth.dto.CaptchaResponse;
import com.cisd.tms.modules.auth.dto.CurrentUserResponse;
import com.cisd.tms.modules.auth.dto.LoginResponse;
import com.cisd.tms.modules.auth.dto.UkeyLoginRandomResponse;
import com.cisd.tms.modules.auth.service.AuthAdminService;
import com.cisd.tms.modules.auth.service.AuthService;
import com.cisd.tms.modules.mk.dto.UKeySignResult;
@ -14,6 +16,7 @@ import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import static org.hamcrest.Matchers.containsString;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
@ -22,27 +25,195 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
class AuthControllerTest {
@Test
void shouldLoginWithRoleBasedPayload() throws Exception {
void shouldLoginWithPasswordPayload() throws Exception {
AuthService authService = Mockito.mock(AuthService.class);
AuthAdminService authAdminService = Mockito.mock(AuthAdminService.class);
LoginResponse response = new LoginResponse();
response.setRoleCode("AUDIT_ADMIN");
response.setAuthLevel("LIMITED");
response.setToken("token-password-001");
Mockito.when(authService.passwordLogin(ArgumentMatchers.any())).thenReturn(response);
MockMvc mockMvc = MockMvcBuilders
.standaloneSetup(new AuthController(authService), new AuthAdminController(authAdminService))
.setControllerAdvice(new GlobalExceptionHandler())
.build();
mockMvc.perform(post("/api/v1/auth/password-login")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"roleCode": "AUDIT_ADMIN",
"password": "12345678",
"captchaCode": "ABCD",
"captchaId": "captcha-001"
}
"""))
.andExpect(status().isOk())
.andExpect(content().string(containsString("\"token\":\"token-password-001\"")))
.andExpect(content().string(containsString("\"authLevel\":\"LIMITED\"")));
}
@Test
void shouldIssueUkeyLoginRandoms() throws Exception {
AuthService authService = Mockito.mock(AuthService.class);
AuthAdminService authAdminService = Mockito.mock(AuthAdminService.class);
UkeyLoginRandomResponse response = new UkeyLoginRandomResponse();
response.setRoleCode("SUPER_ADMIN");
response.setRandoms(java.util.List.of("RB-1", "RB-2", "RB-3"));
Mockito.when(authService.issueUkeyLoginRandoms(ArgumentMatchers.any())).thenReturn(response);
MockMvc mockMvc = MockMvcBuilders
.standaloneSetup(new AuthController(authService), new AuthAdminController(authAdminService))
.setControllerAdvice(new GlobalExceptionHandler())
.build();
mockMvc.perform(post("/api/v1/auth/ukey-login/randoms")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"roleCode": "SUPER_ADMIN"
}
"""))
.andExpect(status().isOk())
.andExpect(content().string(containsString("\"roleCode\":\"SUPER_ADMIN\"")))
.andExpect(content().string(containsString("\"RB-1\"")));
}
@Test
void shouldLoginWithUkeyPayload() throws Exception {
AuthService authService = Mockito.mock(AuthService.class);
AuthAdminService authAdminService = Mockito.mock(AuthAdminService.class);
LoginResponse response = new LoginResponse();
response.setRoleCode("KEY_ADMIN");
response.setAuthLevel("FULL");
response.setToken("token-001");
response.setExpiresAt("2026-03-23T03:10Z");
response.setNeedChangePassword(false);
Mockito.when(authService.login(ArgumentMatchers.any())).thenReturn(response);
response.setToken("token-ukey-001");
Mockito.when(authService.ukeyLogin(ArgumentMatchers.any())).thenReturn(response);
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new AuthController(authService, authAdminService))
MockMvc mockMvc = MockMvcBuilders
.standaloneSetup(new AuthController(authService), new AuthAdminController(authAdminService))
.setControllerAdvice(new GlobalExceptionHandler())
.build();
mockMvc.perform(post("/api/v1/auth/login")
mockMvc.perform(post("/api/v1/auth/ukey-login")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"roleCode\":\"KEY_ADMIN\",\"password\":\"12345678\",\"ukeySerials\":[\"UK-1\",\"UK-2\"]}"))
.content("""
{
"roleCode": "KEY_ADMIN",
"password": "12345678",
"ukeyProofs": [
{
"pubKey": "PUB-1",
"uid": "4",
"rid": "4",
"serverRandom": "RB-1",
"issueSignature": "ISSUE-1",
"loginPayload": "LOGIN-DATA-1",
"loginSignature": "LOGIN-SIGN-1"
},
{
"pubKey": "PUB-2",
"uid": "5",
"rid": "5",
"serverRandom": "RB-2",
"issueSignature": "ISSUE-2",
"loginPayload": "LOGIN-DATA-2",
"loginSignature": "LOGIN-SIGN-2"
}
]
}
"""))
.andExpect(status().isOk())
.andExpect(content().string(org.hamcrest.Matchers.containsString("\"roleCode\":\"KEY_ADMIN\"")))
.andExpect(content().string(org.hamcrest.Matchers.containsString("\"authLevel\":\"FULL\"")));
.andExpect(content().string(containsString("\"token\":\"token-ukey-001\"")))
.andExpect(content().string(containsString("\"authLevel\":\"FULL\"")));
}
@Test
void shouldIssueCaptchaPayload() throws Exception {
AuthService authService = Mockito.mock(AuthService.class);
AuthAdminService authAdminService = Mockito.mock(AuthAdminService.class);
CaptchaResponse response = new CaptchaResponse();
response.setCaptchaId("captcha-001");
response.setImageBase64("IMAGE-BASE64");
Mockito.when(authService.issueCaptcha()).thenReturn(response);
MockMvc mockMvc = MockMvcBuilders
.standaloneSetup(new AuthController(authService), new AuthAdminController(authAdminService))
.setControllerAdvice(new GlobalExceptionHandler())
.build();
mockMvc.perform(post("/api/v1/auth/captcha"))
.andExpect(status().isOk())
.andExpect(content().string(containsString("\"captchaId\":\"captcha-001\"")))
.andExpect(content().string(containsString("\"imageBase64\":\"IMAGE-BASE64\"")));
}
@Test
void shouldReadCurrentRoleFromRequestContextForMeEndpoint() throws Exception {
AuthService authService = Mockito.mock(AuthService.class);
AuthAdminService authAdminService = Mockito.mock(AuthAdminService.class);
CurrentUserResponse response = new CurrentUserResponse();
response.setUsername("AUDIT_ADMIN");
response.setDisplayName("审计管理员");
response.setRole("AUDIT_ADMIN");
response.setAuthLevel("LIMITED");
response.setNeedChangePassword(false);
Mockito.when(authService.me("AUDIT_ADMIN", "token-me-001")).thenReturn(response);
MockMvc mockMvc = MockMvcBuilders
.standaloneSetup(new AuthController(authService), new AuthAdminController(authAdminService))
.setControllerAdvice(new GlobalExceptionHandler())
.build();
mockMvc.perform(get("/api/v1/auth/me")
.requestAttr(InternalApiAuthInterceptor.ATTR_SESSION_TOKEN, "token-me-001")
.requestAttr(InternalApiAuthInterceptor.ATTR_ROLE_CODE, "AUDIT_ADMIN"))
.andExpect(status().isOk())
.andExpect(content().string(containsString("\"role\":\"AUDIT_ADMIN\"")))
.andExpect(content().string(containsString("\"authLevel\":\"LIMITED\"")));
}
@Test
void shouldLogoutThroughCurrentSessionEndpoint() throws Exception {
AuthService authService = Mockito.mock(AuthService.class);
AuthAdminService authAdminService = Mockito.mock(AuthAdminService.class);
MockMvc mockMvc = MockMvcBuilders
.standaloneSetup(new AuthController(authService), new AuthAdminController(authAdminService))
.setControllerAdvice(new GlobalExceptionHandler())
.build();
mockMvc.perform(post("/api/v1/auth/logout")
.requestAttr(InternalApiAuthInterceptor.ATTR_SESSION_TOKEN, "token-logout-001"))
.andExpect(status().isOk())
.andExpect(content().string(containsString("\"success\":true")));
Mockito.verify(authService).logout("token-logout-001");
}
@Test
void shouldChangePasswordThroughCurrentSessionEndpoint() throws Exception {
AuthService authService = Mockito.mock(AuthService.class);
AuthAdminService authAdminService = Mockito.mock(AuthAdminService.class);
MockMvc mockMvc = MockMvcBuilders
.standaloneSetup(new AuthController(authService), new AuthAdminController(authAdminService))
.setControllerAdvice(new GlobalExceptionHandler())
.build();
mockMvc.perform(post("/api/v1/auth/change-password")
.requestAttr(InternalApiAuthInterceptor.ATTR_SESSION_TOKEN, "token-change-001")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"currentPassword": "12345678",
"newPassword": "87654321"
}
"""))
.andExpect(status().isOk())
.andExpect(content().string(containsString("\"success\":true")));
Mockito.verify(authService).changePassword("token-change-001", "12345678", "87654321");
}
@Test
@ -50,7 +221,8 @@ class AuthControllerTest {
AuthService authService = Mockito.mock(AuthService.class);
AuthAdminService authAdminService = Mockito.mock(AuthAdminService.class);
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new AuthController(authService, authAdminService))
MockMvc mockMvc = MockMvcBuilders
.standaloneSetup(new AuthController(authService), new AuthAdminController(authAdminService))
.setControllerAdvice(new GlobalExceptionHandler())
.build();
@ -58,7 +230,7 @@ class AuthControllerTest {
.requestAttr(InternalApiAuthInterceptor.ATTR_ROLE_CODE, "KEY_ADMIN")
.requestAttr(InternalApiAuthInterceptor.ATTR_AUTH_LEVEL, "FULL"))
.andExpect(status().isOk())
.andExpect(content().string(org.hamcrest.Matchers.containsString("\"success\":true")));
.andExpect(content().string(containsString("\"success\":true")));
Mockito.verify(authAdminService).enableRole("KEY_ADMIN", "FULL", "AUDIT_ADMIN");
}
@ -68,7 +240,8 @@ class AuthControllerTest {
AuthService authService = Mockito.mock(AuthService.class);
AuthAdminService authAdminService = Mockito.mock(AuthAdminService.class);
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new AuthController(authService, authAdminService))
MockMvc mockMvc = MockMvcBuilders
.standaloneSetup(new AuthController(authService), new AuthAdminController(authAdminService))
.setControllerAdvice(new GlobalExceptionHandler())
.build();
@ -80,14 +253,14 @@ class AuthControllerTest {
{
"slotNo": 1,
"ukeySerial": "UK-NEW",
"ukeyPubkey": "PUB-NEW",
"pubKey": "PUB-NEW",
"uid": "1",
"rid": "RID-001",
"issuerSign": "SIG-NEW"
"issuerSignature": "SIG-NEW"
}
"""))
.andExpect(status().isOk())
.andExpect(content().string(org.hamcrest.Matchers.containsString("\"success\":true")));
.andExpect(content().string(containsString("\"success\":true")));
Mockito.verify(authAdminService).bindIssuedUkey("KEY_ADMIN", "FULL", "SUPER_ADMIN", 1, "UK-NEW", "PUB-NEW", "1", "RID-001", "SIG-NEW");
}
@ -104,7 +277,8 @@ class AuthControllerTest {
ArgumentMatchers.any()
)).thenReturn(response);
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new AuthController(authService, authAdminService))
MockMvc mockMvc = MockMvcBuilders
.standaloneSetup(new AuthController(authService), new AuthAdminController(authAdminService))
.setControllerAdvice(new GlobalExceptionHandler())
.build();
@ -122,72 +296,7 @@ class AuthControllerTest {
}
"""))
.andExpect(status().isOk())
.andExpect(content().string(org.hamcrest.Matchers.containsString("\"sign\":\"ISSUE-SIGN-001\"")))
.andExpect(content().string(org.hamcrest.Matchers.containsString("\"extra\":\"COMP-001\"")));
}
@Test
void shouldReadCurrentRoleFromRequestContextForMeEndpoint() throws Exception {
AuthService authService = Mockito.mock(AuthService.class);
AuthAdminService authAdminService = Mockito.mock(AuthAdminService.class);
CurrentUserResponse response = new CurrentUserResponse();
response.setUsername("AUDIT_ADMIN");
response.setDisplayName("审计管理员");
response.setRole("AUDIT_ADMIN");
response.setAuthLevel("LIMITED");
response.setNeedChangePassword(false);
Mockito.when(authService.me("AUDIT_ADMIN", "token-me-001")).thenReturn(response);
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new AuthController(authService, authAdminService))
.setControllerAdvice(new GlobalExceptionHandler())
.build();
mockMvc.perform(get("/api/v1/auth/me")
.requestAttr(InternalApiAuthInterceptor.ATTR_SESSION_TOKEN, "token-me-001")
.requestAttr(InternalApiAuthInterceptor.ATTR_ROLE_CODE, "AUDIT_ADMIN"))
.andExpect(status().isOk())
.andExpect(content().string(org.hamcrest.Matchers.containsString("\"role\":\"AUDIT_ADMIN\"")))
.andExpect(content().string(org.hamcrest.Matchers.containsString("\"authLevel\":\"LIMITED\"")));
}
@Test
void shouldLogoutThroughCurrentSessionEndpoint() throws Exception {
AuthService authService = Mockito.mock(AuthService.class);
AuthAdminService authAdminService = Mockito.mock(AuthAdminService.class);
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new AuthController(authService, authAdminService))
.setControllerAdvice(new GlobalExceptionHandler())
.build();
mockMvc.perform(post("/api/v1/auth/logout")
.requestAttr(InternalApiAuthInterceptor.ATTR_SESSION_TOKEN, "token-logout-001"))
.andExpect(status().isOk())
.andExpect(content().string(org.hamcrest.Matchers.containsString("\"success\":true")));
Mockito.verify(authService).logout("token-logout-001");
}
@Test
void shouldChangePasswordThroughCurrentSessionEndpoint() throws Exception {
AuthService authService = Mockito.mock(AuthService.class);
AuthAdminService authAdminService = Mockito.mock(AuthAdminService.class);
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new AuthController(authService, authAdminService))
.setControllerAdvice(new GlobalExceptionHandler())
.build();
mockMvc.perform(post("/api/v1/auth/change-password")
.requestAttr(InternalApiAuthInterceptor.ATTR_SESSION_TOKEN, "token-change-001")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"currentPassword": "12345678",
"newPassword": "87654321"
}
"""))
.andExpect(status().isOk())
.andExpect(content().string(org.hamcrest.Matchers.containsString("\"success\":true")));
Mockito.verify(authService).changePassword("token-change-001", "12345678", "87654321");
.andExpect(content().string(containsString("\"sign\":\"ISSUE-SIGN-001\"")))
.andExpect(content().string(containsString("\"component\":\"COMP-001\"")));
}
}

View File

@ -1,114 +0,0 @@
package com.cisd.tms.modules.auth.controller;
import com.cisd.tms.common.exception.GlobalExceptionHandler;
import com.cisd.tms.modules.auth.dto.LoginResponse;
import com.cisd.tms.modules.auth.dto.UkeyLoginRandomResponse;
import com.cisd.tms.modules.auth.service.CompatAuthService;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentMatchers;
import org.mockito.Mockito;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import static org.hamcrest.Matchers.containsString;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
class CompatAuthControllerTest {
@Test
void shouldIssueUkeyLoginRandoms() throws Exception {
CompatAuthService compatAuthService = Mockito.mock(CompatAuthService.class);
UkeyLoginRandomResponse response = new UkeyLoginRandomResponse();
response.setRoleCode("SUPER_ADMIN");
response.setRandoms(java.util.List.of("RB-1", "RB-2", "RB-3"));
Mockito.when(compatAuthService.issueUkeyLoginRandoms("superadmin", null)).thenReturn(response);
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new CompatAuthController(compatAuthService))
.setControllerAdvice(new GlobalExceptionHandler())
.build();
mockMvc.perform(get("/api/v1/auth/ukey-login/randoms")
.param("role", "superadmin"))
.andExpect(status().isOk())
.andExpect(content().string(containsString("\"roleCode\":\"SUPER_ADMIN\"")))
.andExpect(content().string(containsString("\"RB-1\"")));
}
@Test
void shouldLoginWithCompatiblePasswordPayload() throws Exception {
CompatAuthService compatAuthService = Mockito.mock(CompatAuthService.class);
LoginResponse response = new LoginResponse();
response.setRoleCode("AUDIT_ADMIN");
response.setAuthLevel("LIMITED");
response.setToken("token-password-001");
Mockito.when(compatAuthService.passwordLogin(ArgumentMatchers.any())).thenReturn(response);
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new CompatAuthController(compatAuthService))
.setControllerAdvice(new GlobalExceptionHandler())
.build();
mockMvc.perform(post("/api/v1/auth/password-login")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"role": "auditadmin",
"password": "12345678"
}
"""))
.andExpect(status().isOk())
.andExpect(content().string(containsString("\"token\":\"token-password-001\"")))
.andExpect(content().string(containsString("\"authLevel\":\"LIMITED\"")));
}
@Test
void shouldLoginWithCompatibleUkeyPayload() throws Exception {
CompatAuthService compatAuthService = Mockito.mock(CompatAuthService.class);
LoginResponse response = new LoginResponse();
response.setRoleCode("KEY_ADMIN");
response.setAuthLevel("FULL");
response.setToken("token-ukey-001");
Mockito.when(compatAuthService.ukeyLogin(ArgumentMatchers.any())).thenReturn(response);
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new CompatAuthController(compatAuthService))
.setControllerAdvice(new GlobalExceptionHandler())
.build();
mockMvc.perform(post("/api/v1/auth/ukey-login")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"role": "keyadmin",
"password": "12345678",
"authInfo": [
{
"pubKey": "PUB-1",
"uid": "1",
"rid": "RID-1",
"ra": "RA-1",
"rb": "RB-1",
"issueSign": "ISSUE-1",
"loginSignData": "LOGIN-DATA-1",
"loginSign": "LOGIN-SIGN-1"
},
{
"pubKey": "PUB-2",
"uid": "2",
"rid": "RID-2",
"ra": "RA-2",
"rb": "RB-2",
"issueSign": "ISSUE-2",
"loginSignData": "LOGIN-DATA-2",
"loginSign": "LOGIN-SIGN-2"
}
]
}
"""))
.andExpect(status().isOk())
.andExpect(content().string(containsString("\"token\":\"token-ukey-001\"")))
.andExpect(content().string(containsString("\"authLevel\":\"FULL\"")));
}
}

View File

@ -126,6 +126,8 @@ class AuthAdminServiceTest {
RoleUkeyBindingEntity updated = bindings.findActiveByRoleCodeAndSlotNo(RoleCode.SUPER_ADMIN.getCode(), 1).orElseThrow();
Assertions.assertEquals("UK-NEW", updated.getUkeySerial());
Assertions.assertEquals("PUB-NEW", updated.getUkeyPubkey());
Assertions.assertEquals("1", updated.getUid());
Assertions.assertEquals("RID-001", updated.getRid());
Assertions.assertEquals("SIG-NEW", updated.getIssuerSign());
Assertions.assertEquals(LocalDateTime.of(2026, 3, 23, 3, 0), updated.getBoundAt());
org.mockito.Mockito.verify(lmkService).verifyIk(

View File

@ -21,6 +21,29 @@ class AuthDomainModelTest {
Assertions.assertEquals("FULL", com.cisd.tms.modules.auth.enums.AuthLevel.FULL.name());
}
@Test
void shouldExposePasswordAndUkeyAuthMethodsOnly() {
Assertions.assertEquals(2, com.cisd.tms.modules.auth.enums.AuthMethod.values().length);
Assertions.assertEquals("PASSWORD", com.cisd.tms.modules.auth.enums.AuthMethod.PASSWORD.name());
Assertions.assertEquals("UKEY", com.cisd.tms.modules.auth.enums.AuthMethod.UKEY.name());
}
@Test
void shouldResolveExpectedAuthLevelByAuthMethod() {
Assertions.assertEquals(
com.cisd.tms.modules.auth.enums.AuthLevel.LIMITED,
new com.cisd.tms.modules.auth.service.AuthPolicyService().resolveAuthLevel(
com.cisd.tms.modules.auth.enums.AuthMethod.PASSWORD
)
);
Assertions.assertEquals(
com.cisd.tms.modules.auth.enums.AuthLevel.FULL,
new com.cisd.tms.modules.auth.service.AuthPolicyService().resolveAuthLevel(
com.cisd.tms.modules.auth.enums.AuthMethod.UKEY
)
);
}
@Test
void shouldExposeExpectedRoleAccountStatuses() {
Assertions.assertEquals(3, com.cisd.tms.modules.auth.enums.RoleAccountStatus.values().length);

View File

@ -1,19 +1,28 @@
package com.cisd.tms.modules.auth.service;
import com.cisd.tms.common.exception.BizException;
import com.cisd.tms.modules.auth.dto.CaptchaResponse;
import com.cisd.tms.modules.auth.dto.LoginRequest;
import com.cisd.tms.modules.auth.dto.LoginResponse;
import com.cisd.tms.modules.auth.dto.PasswordLoginRequest;
import com.cisd.tms.modules.auth.dto.UkeyLoginProof;
import com.cisd.tms.modules.auth.dto.UkeyLoginRandomRequest;
import com.cisd.tms.modules.auth.dto.UkeyLoginRandomResponse;
import com.cisd.tms.modules.auth.dto.UkeyLoginRequest;
import com.cisd.tms.modules.auth.entity.AuthSessionEntity;
import com.cisd.tms.modules.auth.entity.AuthUserEntity;
import com.cisd.tms.modules.auth.entity.RoleAccountEntity;
import com.cisd.tms.modules.auth.entity.RoleUkeyBindingEntity;
import com.cisd.tms.modules.auth.enums.AuthLevel;
import com.cisd.tms.modules.auth.enums.AuthMethod;
import com.cisd.tms.modules.auth.enums.RoleAccountStatus;
import com.cisd.tms.modules.auth.enums.RoleCode;
import com.cisd.tms.modules.auth.repository.AuthUserRepository;
import com.cisd.tms.modules.auth.repository.AuthSessionRepository;
import com.cisd.tms.modules.auth.repository.RoleAccountRepository;
import com.cisd.tms.modules.auth.repository.RoleUkeyBindingRepository;
import com.cisd.tms.modules.mk.enums.MasterKeyStatus;
import com.cisd.tms.modules.mk.service.LmkService;
import java.time.Clock;
import java.time.Instant;
import java.time.LocalDateTime;
@ -24,6 +33,7 @@ import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
@ -62,6 +72,7 @@ class AuthServiceTest {
Assertions.assertEquals(1, sessions.count());
AuthSessionEntity session = sessions.findBySessionToken("token-limited-001").orElseThrow();
Assertions.assertEquals(AuthLevel.LIMITED.name(), session.getAuthLevel());
Assertions.assertEquals(AuthMethod.PASSWORD.name(), session.getAuthMethod());
Assertions.assertEquals(LocalDateTime.of(2026, 3, 23, 2, 0), session.getIssuedAt());
Assertions.assertEquals(LocalDateTime.of(2026, 3, 23, 2, 10), session.getExpiresAt());
}
@ -95,6 +106,8 @@ class AuthServiceTest {
Assertions.assertEquals(AuthLevel.FULL.name(), response.getAuthLevel());
Assertions.assertEquals("token-full-001", response.getToken());
Assertions.assertFalse(response.getNeedChangePassword());
AuthSessionEntity session = sessions.findBySessionToken("token-full-001").orElseThrow();
Assertions.assertEquals(AuthMethod.UKEY.name(), session.getAuthMethod());
}
@Test
@ -254,6 +267,167 @@ class AuthServiceTest {
Assertions.assertEquals(LocalDateTime.of(2026, 3, 23, 2, 0), session.getExpiresAt());
}
@Test
void shouldIssueRoleScopedRandomsForUkeyLogin() {
UkeyLoginRandomService randomService = org.mockito.Mockito.mock(UkeyLoginRandomService.class);
org.mockito.Mockito.when(randomService.issue(RoleCode.SUPER_ADMIN.getCode(), 3)).thenReturn(List.of("RB-1", "RB-2", "RB-3"));
AuthService service = new AuthService(
new InMemoryRoleAccountRepository(),
new InMemoryLegacyAuthUserRepository(),
new InMemoryAuthSessionRepository(),
new InMemoryRoleUkeyBindingRepository(),
new FakePasswordHasher(),
org.mockito.Mockito.mock(LmkService.class),
randomService,
org.mockito.Mockito.mock(CompatUkeyVerifier.class),
new InMemoryCaptchaService(),
new ObjectMapper(),
FIXED_CLOCK,
() -> "unused"
);
UkeyLoginRandomRequest request = new UkeyLoginRandomRequest();
request.setRoleCode(RoleCode.SUPER_ADMIN.getCode());
UkeyLoginRandomResponse response = service.issueUkeyLoginRandoms(request);
Assertions.assertEquals(RoleCode.SUPER_ADMIN.getCode(), response.getRoleCode());
Assertions.assertEquals(List.of("RB-1", "RB-2", "RB-3"), response.getRandoms());
}
@Test
void shouldRejectPasswordLoginWhenMasterKeyIsNotReady() {
InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository();
roleAccounts.save(activeRole(RoleCode.AUDIT_ADMIN, "HASH:12345678:SALT-A", "SALT-A", 0, false));
LmkService lmkService = org.mockito.Mockito.mock(LmkService.class);
org.mockito.Mockito.when(lmkService.getMasterKeyStatus()).thenReturn(MasterKeyStatus.ABNORMAL.getDetail());
InMemoryCaptchaService captchaService = new InMemoryCaptchaService();
String captchaId = captchaService.issue("ABCD").getCaptchaId();
AuthService service = new AuthService(
roleAccounts,
new InMemoryLegacyAuthUserRepository(),
new InMemoryAuthSessionRepository(),
new InMemoryRoleUkeyBindingRepository(),
new FakePasswordHasher(),
lmkService,
org.mockito.Mockito.mock(UkeyLoginRandomService.class),
org.mockito.Mockito.mock(CompatUkeyVerifier.class),
captchaService,
new ObjectMapper(),
FIXED_CLOCK,
() -> "unused"
);
PasswordLoginRequest request = new PasswordLoginRequest();
request.setRoleCode(RoleCode.AUDIT_ADMIN.getCode());
request.setPassword("12345678");
request.setCaptchaId(captchaId);
request.setCaptchaCode("ABCD");
BizException exception = Assertions.assertThrows(BizException.class, () -> service.passwordLogin(request));
Assertions.assertEquals("master key is not ready", exception.getMessage());
}
@Test
void shouldIssueCaptchaAndAcceptPasswordLoginWithValidCaptcha() {
InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository();
roleAccounts.save(activeRole(RoleCode.OPS_ADMIN, "HASH:12345678:SALT-O", "SALT-O", 0, false));
LmkService lmkService = org.mockito.Mockito.mock(LmkService.class);
org.mockito.Mockito.when(lmkService.getMasterKeyStatus()).thenReturn(MasterKeyStatus.NORMAL.getDetail("MAC-001"));
InMemoryCaptchaService captchaService = new InMemoryCaptchaService();
AuthService service = new AuthService(
roleAccounts,
new InMemoryLegacyAuthUserRepository(),
new InMemoryAuthSessionRepository(),
new InMemoryRoleUkeyBindingRepository(),
new FakePasswordHasher(),
lmkService,
org.mockito.Mockito.mock(UkeyLoginRandomService.class),
org.mockito.Mockito.mock(CompatUkeyVerifier.class),
captchaService,
new ObjectMapper(),
FIXED_CLOCK,
() -> "token-password-002"
);
CaptchaResponse captcha = service.issueCaptcha();
PasswordLoginRequest request = new PasswordLoginRequest();
request.setRoleCode(RoleCode.OPS_ADMIN.getCode());
request.setPassword("12345678");
request.setCaptchaId(captcha.getCaptchaId());
request.setCaptchaCode("ABCD");
LoginResponse response = service.passwordLogin(request);
Assertions.assertEquals("token-password-002", response.getToken());
Assertions.assertEquals(AuthLevel.LIMITED.name(), response.getAuthLevel());
}
@Test
void shouldVerifyIssuedUkeysRandomsAndLoginSignsBeforeCreatingFullSession() {
InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository();
InMemoryAuthSessionRepository sessions = new InMemoryAuthSessionRepository();
InMemoryRoleUkeyBindingRepository ukeyBindings = new InMemoryRoleUkeyBindingRepository();
roleAccounts.save(activeRole(RoleCode.KEY_ADMIN, "HASH:12345678:SALT-K", "SALT-K", 0, false));
ukeyBindings.save(activeBinding(RoleCode.KEY_ADMIN, 1, "UK-1", "PUB-1", "4", "4"));
ukeyBindings.save(activeBinding(RoleCode.KEY_ADMIN, 2, "UK-2", "PUB-2", "5", "5"));
LmkService lmkService = org.mockito.Mockito.mock(LmkService.class);
UkeyLoginRandomService randomService = org.mockito.Mockito.mock(UkeyLoginRandomService.class);
CompatUkeyVerifier verifier = org.mockito.Mockito.mock(CompatUkeyVerifier.class);
org.mockito.Mockito.when(lmkService.getMasterKeyStatus()).thenReturn(MasterKeyStatus.NORMAL.getDetail("MAC-001"));
org.mockito.Mockito.when(lmkService.exportIkPublicKeyHex()).thenReturn("IK-PUB-001");
org.mockito.Mockito.when(randomService.issue(RoleCode.KEY_ADMIN.getCode(), 2)).thenReturn(List.of("RB-1", "RB-2"));
AuthService service = new AuthService(
roleAccounts,
new InMemoryLegacyAuthUserRepository(),
sessions,
ukeyBindings,
new FakePasswordHasher(),
lmkService,
randomService,
verifier,
new InMemoryCaptchaService(),
new ObjectMapper(),
FIXED_CLOCK,
() -> "token-full-ukey-002"
);
UkeyLoginRandomRequest randomRequest = new UkeyLoginRandomRequest();
randomRequest.setRoleCode(RoleCode.KEY_ADMIN.getCode());
service.issueUkeyLoginRandoms(randomRequest);
UkeyLoginRequest request = new UkeyLoginRequest();
request.setRoleCode(RoleCode.KEY_ADMIN.getCode());
request.setPassword("12345678");
request.setUkeyProofs(List.of(
proof("PUB-1", "4", "4", "RB-1", "ISSUE-1", "LOGIN-DATA-1", "LOGIN-SIGN-1"),
proof("PUB-2", "5", "5", "RB-2", "ISSUE-2", "LOGIN-DATA-2", "LOGIN-SIGN-2")
));
LoginResponse response = service.ukeyLogin(request);
Assertions.assertEquals("token-full-ukey-002", response.getToken());
Assertions.assertEquals(AuthLevel.FULL.name(), response.getAuthLevel());
org.mockito.Mockito.verify(randomService).assertIssued(RoleCode.KEY_ADMIN.getCode(), List.of("RB-1", "RB-2"));
org.mockito.Mockito.verify(verifier).verifyIssuedBinding(
"{\"pubKey\":\"PUB-1\",\"authKeyPair\":\"IK-PUB-001\",\"role\":\"keyadmin\",\"uid\":\"4\",\"rid\":\"4\"}",
"ISSUE-1"
);
org.mockito.Mockito.verify(verifier).verifyIssuedBinding(
"{\"pubKey\":\"PUB-2\",\"authKeyPair\":\"IK-PUB-001\",\"role\":\"keyadmin\",\"uid\":\"5\",\"rid\":\"5\"}",
"ISSUE-2"
);
org.mockito.Mockito.verify(verifier).verifyLoginSignature("PUB-1", "LOGIN-DATA-1", "LOGIN-SIGN-1");
org.mockito.Mockito.verify(verifier).verifyLoginSignature("PUB-2", "LOGIN-DATA-2", "LOGIN-SIGN-2");
}
private static RoleAccountEntity activeRole(
RoleCode roleCode,
String passwordHash,
@ -297,6 +471,21 @@ class AuthServiceTest {
return entity;
}
private static RoleUkeyBindingEntity activeBinding(
RoleCode roleCode,
int slotNo,
String serial,
String pubKey,
String uid,
String rid
) {
RoleUkeyBindingEntity entity = activeBinding(roleCode, slotNo, serial);
entity.setUkeyPubkey(pubKey);
entity.setUid(uid);
entity.setRid(rid);
return entity;
}
private static class FakePasswordHasher implements PasswordHasher {
@Override
@ -403,4 +592,50 @@ class AuthServiceTest {
entity.setExpiresAt(LocalDateTime.of(2026, 3, 23, 2, 8));
return entity;
}
private static UkeyLoginProof proof(
String pubKey,
String uid,
String rid,
String serverRandom,
String issueSignature,
String loginPayload,
String loginSignature
) {
UkeyLoginProof proof = new UkeyLoginProof();
proof.setPubKey(pubKey);
proof.setUid(uid);
proof.setRid(rid);
proof.setServerRandom(serverRandom);
proof.setIssueSignature(issueSignature);
proof.setLoginPayload(loginPayload);
proof.setLoginSignature(loginSignature);
return proof;
}
private static class InMemoryCaptchaService implements CaptchaService {
private final Map<String, String> store = new ConcurrentHashMap<>();
@Override
public CaptchaResponse issueCaptcha() {
return issue("ABCD");
}
private CaptchaResponse issue(String code) {
String captchaId = "captcha-" + store.size();
store.put(captchaId, code);
CaptchaResponse response = new CaptchaResponse();
response.setCaptchaId(captchaId);
response.setImageBase64("mock-image");
return response;
}
@Override
public void verify(String captchaId, String captchaCode) {
String expected = store.remove(captchaId);
if (expected == null || !expected.equalsIgnoreCase(captchaCode)) {
throw new BizException(com.cisd.tms.common.enums.ErrorCode.UNAUTHORIZED.getCode(), "captcha verification failed");
}
}
}
}

View File

@ -1,198 +0,0 @@
package com.cisd.tms.modules.auth.service;
import com.cisd.tms.common.exception.BizException;
import com.cisd.tms.modules.auth.dto.CompatPasswordLoginRequest;
import com.cisd.tms.modules.auth.dto.CompatUkeyLoginRequest;
import com.cisd.tms.modules.auth.dto.LoginResponse;
import com.cisd.tms.modules.auth.dto.UkeyLoginRandomResponse;
import com.cisd.tms.modules.auth.entity.RoleUkeyBindingEntity;
import com.cisd.tms.modules.auth.enums.RoleCode;
import com.cisd.tms.modules.auth.repository.RoleUkeyBindingRepository;
import com.cisd.tms.modules.mk.enums.MasterKeyStatus;
import com.cisd.tms.modules.mk.service.LmkService;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.List;
import java.util.Optional;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.ArgumentMatchers;
import org.mockito.Mockito;
class CompatAuthServiceTest {
@Test
void shouldIssueRoleScopedRandomsForUkeyLogin() {
AuthService authService = Mockito.mock(AuthService.class);
RoleUkeyBindingRepository bindings = Mockito.mock(RoleUkeyBindingRepository.class);
LmkService lmkService = Mockito.mock(LmkService.class);
UkeyLoginRandomService randomService = Mockito.mock(UkeyLoginRandomService.class);
CompatUkeyVerifier verifier = Mockito.mock(CompatUkeyVerifier.class);
Mockito.when(randomService.issue(RoleCode.SUPER_ADMIN.getCode(), 3)).thenReturn(List.of("RB-1", "RB-2", "RB-3"));
CompatAuthService service = new CompatAuthService(authService, bindings, lmkService, randomService, verifier, new ObjectMapper());
UkeyLoginRandomResponse response = service.issueUkeyLoginRandoms("superadmin", null);
Assertions.assertEquals(RoleCode.SUPER_ADMIN.getCode(), response.getRoleCode());
Assertions.assertEquals(List.of("RB-1", "RB-2", "RB-3"), response.getRandoms());
}
@Test
void shouldDelegateCompatiblePasswordLoginIntoCurrentRoleLoginFlow() {
AuthService authService = Mockito.mock(AuthService.class);
RoleUkeyBindingRepository bindings = Mockito.mock(RoleUkeyBindingRepository.class);
LmkService lmkService = Mockito.mock(LmkService.class);
UkeyLoginRandomService randomService = Mockito.mock(UkeyLoginRandomService.class);
CompatUkeyVerifier verifier = Mockito.mock(CompatUkeyVerifier.class);
Mockito.when(lmkService.getMasterKeyStatus()).thenReturn(MasterKeyStatus.NORMAL.getDetail("MAC-001"));
LoginResponse loginResponse = new LoginResponse();
loginResponse.setRoleCode(RoleCode.AUDIT_ADMIN.getCode());
loginResponse.setAuthLevel("LIMITED");
loginResponse.setToken("token-password-001");
Mockito.when(authService.login(ArgumentMatchers.any())).thenReturn(loginResponse);
CompatAuthService service = new CompatAuthService(authService, bindings, lmkService, randomService, verifier, new ObjectMapper());
CompatPasswordLoginRequest request = new CompatPasswordLoginRequest();
request.setRole("auditadmin");
request.setPassword("12345678");
LoginResponse response = service.passwordLogin(request);
Assertions.assertEquals("token-password-001", response.getToken());
ArgumentCaptor<com.cisd.tms.modules.auth.dto.LoginRequest> captor =
ArgumentCaptor.forClass(com.cisd.tms.modules.auth.dto.LoginRequest.class);
Mockito.verify(authService).login(captor.capture());
Assertions.assertEquals(RoleCode.AUDIT_ADMIN.getCode(), captor.getValue().getRoleCode());
Assertions.assertEquals("12345678", captor.getValue().getPassword());
Assertions.assertNull(captor.getValue().getUkeySerials());
}
@Test
void shouldRejectCompatiblePasswordLoginWhenMasterKeyIsNotReady() {
AuthService authService = Mockito.mock(AuthService.class);
RoleUkeyBindingRepository bindings = Mockito.mock(RoleUkeyBindingRepository.class);
LmkService lmkService = Mockito.mock(LmkService.class);
UkeyLoginRandomService randomService = Mockito.mock(UkeyLoginRandomService.class);
CompatUkeyVerifier verifier = Mockito.mock(CompatUkeyVerifier.class);
Mockito.when(lmkService.getMasterKeyStatus()).thenReturn(MasterKeyStatus.ABNORMAL.getDetail());
CompatAuthService service = new CompatAuthService(authService, bindings, lmkService, randomService, verifier, new ObjectMapper());
CompatPasswordLoginRequest request = new CompatPasswordLoginRequest();
request.setRole("auditadmin");
request.setPassword("12345678");
BizException exception = Assertions.assertThrows(BizException.class, () -> service.passwordLogin(request));
Assertions.assertEquals("master key is not ready", exception.getMessage());
Mockito.verifyNoInteractions(authService);
}
@Test
void shouldVerifyIssuedUkeysRandomsAndLoginSignsBeforeCreatingFullSession() {
AuthService authService = Mockito.mock(AuthService.class);
RoleUkeyBindingRepository bindings = Mockito.mock(RoleUkeyBindingRepository.class);
LmkService lmkService = Mockito.mock(LmkService.class);
UkeyLoginRandomService randomService = Mockito.mock(UkeyLoginRandomService.class);
CompatUkeyVerifier verifier = Mockito.mock(CompatUkeyVerifier.class);
Mockito.when(lmkService.getMasterKeyStatus()).thenReturn(MasterKeyStatus.NORMAL.getDetail("MAC-001"));
Mockito.when(lmkService.exportIkPublicKeyHex()).thenReturn("IK-PUB-001");
Mockito.when(bindings.findActiveByRoleCode(RoleCode.KEY_ADMIN.getCode())).thenReturn(List.of(
binding(1, "UK-1", "PUB-1"),
binding(2, "UK-2", "PUB-2")
));
LoginResponse loginResponse = new LoginResponse();
loginResponse.setRoleCode(RoleCode.KEY_ADMIN.getCode());
loginResponse.setAuthLevel("FULL");
loginResponse.setToken("token-full-001");
Mockito.when(authService.login(ArgumentMatchers.any())).thenReturn(loginResponse);
CompatAuthService service = new CompatAuthService(authService, bindings, lmkService, randomService, verifier, new ObjectMapper());
CompatUkeyLoginRequest request = new CompatUkeyLoginRequest();
request.setRole("keyadmin");
request.setPassword("12345678");
request.setAuthInfo(List.of(
authInfo("PUB-1", "1", "RID-1", "RA-1", "RB-1", "ISSUE-1", "LOGIN-DATA-1", "LOGIN-SIGN-1"),
authInfo("PUB-2", "2", "RID-2", "RA-2", "RB-2", "ISSUE-2", "LOGIN-DATA-2", "LOGIN-SIGN-2")
));
LoginResponse response = service.ukeyLogin(request);
Assertions.assertEquals("token-full-001", response.getToken());
Mockito.verify(randomService).assertIssued(RoleCode.KEY_ADMIN.getCode(), List.of("RB-1", "RB-2"));
Mockito.verify(verifier).verifyIssuedBinding(
"{\"pubKey\":\"PUB-1\",\"authKeyPair\":\"IK-PUB-001\",\"role\":\"keyadmin\",\"uid\":\"1\",\"rid\":\"RID-1\"}",
"ISSUE-1"
);
Mockito.verify(verifier).verifyIssuedBinding(
"{\"pubKey\":\"PUB-2\",\"authKeyPair\":\"IK-PUB-001\",\"role\":\"keyadmin\",\"uid\":\"2\",\"rid\":\"RID-2\"}",
"ISSUE-2"
);
Mockito.verify(verifier).verifyLoginSignature("PUB-1", "LOGIN-DATA-1", "LOGIN-SIGN-1");
Mockito.verify(verifier).verifyLoginSignature("PUB-2", "LOGIN-DATA-2", "LOGIN-SIGN-2");
ArgumentCaptor<com.cisd.tms.modules.auth.dto.LoginRequest> captor =
ArgumentCaptor.forClass(com.cisd.tms.modules.auth.dto.LoginRequest.class);
Mockito.verify(authService).login(captor.capture());
Assertions.assertEquals(RoleCode.KEY_ADMIN.getCode(), captor.getValue().getRoleCode());
Assertions.assertEquals("12345678", captor.getValue().getPassword());
Assertions.assertEquals(List.of("UK-1", "UK-2"), captor.getValue().getUkeySerials());
}
@Test
void shouldRejectUkeyLoginWhenRequestPubkeyIsNotBoundToRole() {
AuthService authService = Mockito.mock(AuthService.class);
RoleUkeyBindingRepository bindings = Mockito.mock(RoleUkeyBindingRepository.class);
LmkService lmkService = Mockito.mock(LmkService.class);
UkeyLoginRandomService randomService = Mockito.mock(UkeyLoginRandomService.class);
CompatUkeyVerifier verifier = Mockito.mock(CompatUkeyVerifier.class);
Mockito.when(lmkService.getMasterKeyStatus()).thenReturn(MasterKeyStatus.NORMAL.getDetail("MAC-001"));
Mockito.when(bindings.findActiveByRoleCode(RoleCode.AUDIT_ADMIN.getCode())).thenReturn(List.of(
binding(1, "UK-1", "PUB-BOUND")
));
CompatAuthService service = new CompatAuthService(authService, bindings, lmkService, randomService, verifier, new ObjectMapper());
CompatUkeyLoginRequest request = new CompatUkeyLoginRequest();
request.setRole("auditadmin");
request.setPassword("12345678");
request.setAuthInfo(List.of(authInfo("PUB-OTHER", "1", "RID-1", "RA-1", "RB-1", "ISSUE-1", "LOGIN-DATA-1", "LOGIN-SIGN-1")));
BizException exception = Assertions.assertThrows(BizException.class, () -> service.ukeyLogin(request));
Assertions.assertEquals("ukey auth info does not match bound role", exception.getMessage());
Mockito.verifyNoInteractions(authService);
}
private static RoleUkeyBindingEntity binding(int slotNo, String serial, String pubkey) {
RoleUkeyBindingEntity entity = new RoleUkeyBindingEntity();
entity.setId((long) slotNo);
entity.setRoleCode(RoleCode.KEY_ADMIN.getCode());
entity.setSlotNo(slotNo);
entity.setUkeySerial(serial);
entity.setUkeyPubkey(pubkey);
entity.setStatus("ACTIVE");
return entity;
}
private static CompatUkeyLoginRequest.LoginAuthInfo authInfo(
String pubKey,
String uid,
String rid,
String ra,
String rb,
String issueSign,
String loginSignData,
String loginSign
) {
CompatUkeyLoginRequest.LoginAuthInfo info = new CompatUkeyLoginRequest.LoginAuthInfo();
info.setPubKey(pubKey);
info.setUid(uid);
info.setRid(rid);
info.setRa(ra);
info.setRb(rb);
info.setIssueSign(issueSign);
info.setLoginSignData(loginSignData);
info.setLoginSign(loginSign);
return info;
}
}