diff --git a/src/main/java/com/cisd/tms/modules/auth/controller/AuthAdminController.java b/src/main/java/com/cisd/tms/modules/auth/controller/AuthAdminController.java new file mode 100644 index 0000000..e4fdd71 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/auth/controller/AuthAdminController.java @@ -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 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 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 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 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 + )); + } +} diff --git a/src/main/java/com/cisd/tms/modules/auth/controller/AuthController.java b/src/main/java/com/cisd/tms/modules/auth/controller/AuthController.java index 95d1fdb..78628c5 100644 --- a/src/main/java/com/cisd/tms/modules/auth/controller/AuthController.java +++ b/src/main/java/com/cisd/tms/modules/auth/controller/AuthController.java @@ -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 login(@Valid @RequestBody LoginRequest request) { - return ApiResponse.success(authService.login(request)); + @PostMapping("/password-login") + @Operation(summary = "口令登录", description = "按旧系统受限登录流程签发 LIMITED 会话。") + public ApiResponse passwordLogin(@Valid @RequestBody PasswordLoginRequest request) { + return ApiResponse.success(authService.passwordLogin(request)); + } + + @PostMapping("/ukey-login/randoms") + @Operation(summary = "申请 UKey 登录随机数", description = "按角色要求下发 UKey 登录签名随机数。") + public ApiResponse issueUkeyLoginRandoms(@Valid @RequestBody UkeyLoginRandomRequest request) { + return ApiResponse.success(authService.issueUkeyLoginRandoms(request)); + } + + @PostMapping("/ukey-login") + @Operation(summary = "UKey 登录", description = "按旧系统标准 UKey 校验顺序签发 FULL 会话。") + public ApiResponse ukeyLogin(@Valid @RequestBody UkeyLoginRequest request) { + return ApiResponse.success(authService.ukeyLogin(request)); + } + + @PostMapping("/captcha") + @Operation(summary = "生成验证码", description = "为口令登录流程生成一次性验证码。") + public ApiResponse issueCaptcha() { + return ApiResponse.success(authService.issueCaptcha()); } @GetMapping("/me") - @Operation(summary = "查询当前用户", description = "根据内部鉴权链路透传的当前角色标识返回当前登录用户信息。") + @Operation(summary = "查询当前用户", description = "根据内部鉴权上下文返回当前会话信息。") public ApiResponse 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 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 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 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 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 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 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 - )); - } } diff --git a/src/main/java/com/cisd/tms/modules/auth/controller/CompatAuthController.java b/src/main/java/com/cisd/tms/modules/auth/controller/CompatAuthController.java deleted file mode 100644 index 5873cb2..0000000 --- a/src/main/java/com/cisd/tms/modules/auth/controller/CompatAuthController.java +++ /dev/null @@ -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 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 passwordLogin(@Valid @RequestBody CompatPasswordLoginRequest request) { - return ApiResponse.success(compatAuthService.passwordLogin(request)); - } - - @PostMapping("/ukey-login") - @Operation(summary = "兼容 UKey 登录", description = "兼容旧管理端的 UKey 登录语义,校验发行签名、随机数和登录签名。") - public ApiResponse ukeyLogin(@Valid @RequestBody CompatUkeyLoginRequest request) { - return ApiResponse.success(compatAuthService.ukeyLogin(request)); - } -} diff --git a/src/main/java/com/cisd/tms/modules/auth/dto/CaptchaResponse.java b/src/main/java/com/cisd/tms/modules/auth/dto/CaptchaResponse.java new file mode 100644 index 0000000..da97528 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/auth/dto/CaptchaResponse.java @@ -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; + } +} diff --git a/src/main/java/com/cisd/tms/modules/auth/dto/CompatPasswordLoginRequest.java b/src/main/java/com/cisd/tms/modules/auth/dto/CompatPasswordLoginRequest.java deleted file mode 100644 index 471e91e..0000000 --- a/src/main/java/com/cisd/tms/modules/auth/dto/CompatPasswordLoginRequest.java +++ /dev/null @@ -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; - } -} diff --git a/src/main/java/com/cisd/tms/modules/auth/dto/CompatUkeyLoginRequest.java b/src/main/java/com/cisd/tms/modules/auth/dto/CompatUkeyLoginRequest.java deleted file mode 100644 index 2b2a83c..0000000 --- a/src/main/java/com/cisd/tms/modules/auth/dto/CompatUkeyLoginRequest.java +++ /dev/null @@ -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 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 getAuthInfo() { - return authInfo; - } - - public void setAuthInfo(List 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; - } - } -} diff --git a/src/main/java/com/cisd/tms/modules/auth/dto/PasswordLoginRequest.java b/src/main/java/com/cisd/tms/modules/auth/dto/PasswordLoginRequest.java new file mode 100644 index 0000000..efddff8 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/auth/dto/PasswordLoginRequest.java @@ -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; + } +} diff --git a/src/main/java/com/cisd/tms/modules/auth/dto/UkeyBindRequest.java b/src/main/java/com/cisd/tms/modules/auth/dto/UkeyBindRequest.java index eaec17d..dfca74d 100644 --- a/src/main/java/com/cisd/tms/modules/auth/dto/UkeyBindRequest.java +++ b/src/main/java/com/cisd/tms/modules/auth/dto/UkeyBindRequest.java @@ -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() { diff --git a/src/main/java/com/cisd/tms/modules/auth/dto/UkeyLoginProof.java b/src/main/java/com/cisd/tms/modules/auth/dto/UkeyLoginProof.java new file mode 100644 index 0000000..6733ffe --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/auth/dto/UkeyLoginProof.java @@ -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; + } +} diff --git a/src/main/java/com/cisd/tms/modules/auth/dto/UkeyLoginRandomRequest.java b/src/main/java/com/cisd/tms/modules/auth/dto/UkeyLoginRandomRequest.java new file mode 100644 index 0000000..8a50eb2 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/auth/dto/UkeyLoginRandomRequest.java @@ -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; + } +} diff --git a/src/main/java/com/cisd/tms/modules/auth/dto/UkeyLoginRequest.java b/src/main/java/com/cisd/tms/modules/auth/dto/UkeyLoginRequest.java new file mode 100644 index 0000000..4a4da6f --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/auth/dto/UkeyLoginRequest.java @@ -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 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 getUkeyProofs() { + return ukeyProofs; + } + + public void setUkeyProofs(List ukeyProofs) { + this.ukeyProofs = ukeyProofs; + } +} diff --git a/src/main/java/com/cisd/tms/modules/auth/entity/AuthSessionEntity.java b/src/main/java/com/cisd/tms/modules/auth/entity/AuthSessionEntity.java index f72eab3..606ceb7 100644 --- a/src/main/java/com/cisd/tms/modules/auth/entity/AuthSessionEntity.java +++ b/src/main/java/com/cisd/tms/modules/auth/entity/AuthSessionEntity.java @@ -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; } diff --git a/src/main/java/com/cisd/tms/modules/auth/entity/RoleUkeyBindingEntity.java b/src/main/java/com/cisd/tms/modules/auth/entity/RoleUkeyBindingEntity.java index 0f12476..f74cf0d 100644 --- a/src/main/java/com/cisd/tms/modules/auth/entity/RoleUkeyBindingEntity.java +++ b/src/main/java/com/cisd/tms/modules/auth/entity/RoleUkeyBindingEntity.java @@ -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; } diff --git a/src/main/java/com/cisd/tms/modules/auth/enums/AuthMethod.java b/src/main/java/com/cisd/tms/modules/auth/enums/AuthMethod.java new file mode 100644 index 0000000..5926ff2 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/auth/enums/AuthMethod.java @@ -0,0 +1,11 @@ +package com.cisd.tms.modules.auth.enums; + +/** + * 认证方式。 + * PASSWORD 对应旧项目的受限口令登录, + * UKEY 对应旧项目的完整 UKey 登录。 + */ +public enum AuthMethod { + PASSWORD, + UKEY +} diff --git a/src/main/java/com/cisd/tms/modules/auth/service/AuthAdminService.java b/src/main/java/com/cisd/tms/modules/auth/service/AuthAdminService.java index 492c31e..d8aa658 100644 --- a/src/main/java/com/cisd/tms/modules/auth/service/AuthAdminService.java +++ b/src/main/java/com/cisd/tms/modules/auth/service/AuthAdminService.java @@ -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()); diff --git a/src/main/java/com/cisd/tms/modules/auth/service/AuthPolicyService.java b/src/main/java/com/cisd/tms/modules/auth/service/AuthPolicyService.java new file mode 100644 index 0000000..e6df136 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/auth/service/AuthPolicyService.java @@ -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(); + } +} diff --git a/src/main/java/com/cisd/tms/modules/auth/service/AuthService.java b/src/main/java/com/cisd/tms/modules/auth/service/AuthService.java index 2de2c96..bad1b31 100644 --- a/src/main/java/com/cisd/tms/modules/auth/service/AuthService.java +++ b/src/main/java/com/cisd/tms/modules/auth/service/AuthService.java @@ -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 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 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 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 activeBindings = roleUkeyBindingRepository.findActiveByRoleCode(roleCode.getCode()); + validateUkeyCount(roleCode, activeBindings, request.getUkeyProofs()); + Map bindingsByPubKey = activeBindings.stream() + .collect(Collectors.toMap(RoleUkeyBindingEntity::getUkeyPubkey, item -> item, (left, right) -> left, java.util.LinkedHashMap::new)); + Set 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 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 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 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 activeBindings, + List 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; + } } diff --git a/src/main/java/com/cisd/tms/modules/auth/service/CaptchaService.java b/src/main/java/com/cisd/tms/modules/auth/service/CaptchaService.java new file mode 100644 index 0000000..c8f97f2 --- /dev/null +++ b/src/main/java/com/cisd/tms/modules/auth/service/CaptchaService.java @@ -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); +} diff --git a/src/main/java/com/cisd/tms/modules/auth/service/CompatAuthService.java b/src/main/java/com/cisd/tms/modules/auth/service/CompatAuthService.java deleted file mode 100644 index 54e3341..0000000 --- a/src/main/java/com/cisd/tms/modules/auth/service/CompatAuthService.java +++ /dev/null @@ -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 activeBindings = roleUkeyBindingRepository.findActiveByRoleCode(roleCode.getCode()); - validateUkeyCount(roleCode, activeBindings, request.getAuthInfo()); - - Map bindingsByPubKey = activeBindings.stream() - .collect(Collectors.toMap(RoleUkeyBindingEntity::getUkeyPubkey, item -> item, (left, right) -> left, LinkedHashMap::new)); - Set 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 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 activeBindings, - List 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); - } -} diff --git a/src/main/resources/db/migration/V12__refactor_auth_login_model.sql b/src/main/resources/db/migration/V12__refactor_auth_login_model.sql new file mode 100644 index 0000000..b83b331 --- /dev/null +++ b/src/main/resources/db/migration/V12__refactor_auth_login_model.sql @@ -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; diff --git a/src/main/resources/mapper/auth/AuthSessionMapper.xml b/src/main/resources/mapper/auth/AuthSessionMapper.xml index ba40e1e..acbccdb 100644 --- a/src/main/resources/mapper/auth/AuthSessionMapper.xml +++ b/src/main/resources/mapper/auth/AuthSessionMapper.xml @@ -8,6 +8,7 @@ + @@ -21,6 +22,7 @@ SELECT id, session_token, role_code, + auth_method, auth_level, issued_at, last_active_at, diff --git a/src/main/resources/mapper/auth/RoleUkeyBindingMapper.xml b/src/main/resources/mapper/auth/RoleUkeyBindingMapper.xml index f1069ad..eb002b6 100644 --- a/src/main/resources/mapper/auth/RoleUkeyBindingMapper.xml +++ b/src/main/resources/mapper/auth/RoleUkeyBindingMapper.xml @@ -8,6 +8,8 @@ + + @@ -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, diff --git a/src/test/java/com/cisd/tms/modules/auth/controller/AuthControllerTest.java b/src/test/java/com/cisd/tms/modules/auth/controller/AuthControllerTest.java index 5eb9756..67c38ec 100644 --- a/src/test/java/com/cisd/tms/modules/auth/controller/AuthControllerTest.java +++ b/src/test/java/com/cisd/tms/modules/auth/controller/AuthControllerTest.java @@ -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\""))); } } diff --git a/src/test/java/com/cisd/tms/modules/auth/controller/CompatAuthControllerTest.java b/src/test/java/com/cisd/tms/modules/auth/controller/CompatAuthControllerTest.java deleted file mode 100644 index 1e643f8..0000000 --- a/src/test/java/com/cisd/tms/modules/auth/controller/CompatAuthControllerTest.java +++ /dev/null @@ -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\""))); - } -} diff --git a/src/test/java/com/cisd/tms/modules/auth/service/AuthAdminServiceTest.java b/src/test/java/com/cisd/tms/modules/auth/service/AuthAdminServiceTest.java index 0c8f99c..a8c1cac 100644 --- a/src/test/java/com/cisd/tms/modules/auth/service/AuthAdminServiceTest.java +++ b/src/test/java/com/cisd/tms/modules/auth/service/AuthAdminServiceTest.java @@ -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( diff --git a/src/test/java/com/cisd/tms/modules/auth/service/AuthDomainModelTest.java b/src/test/java/com/cisd/tms/modules/auth/service/AuthDomainModelTest.java index 77e828f..8349c7b 100644 --- a/src/test/java/com/cisd/tms/modules/auth/service/AuthDomainModelTest.java +++ b/src/test/java/com/cisd/tms/modules/auth/service/AuthDomainModelTest.java @@ -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); diff --git a/src/test/java/com/cisd/tms/modules/auth/service/AuthServiceTest.java b/src/test/java/com/cisd/tms/modules/auth/service/AuthServiceTest.java index e66c423..db6e096 100644 --- a/src/test/java/com/cisd/tms/modules/auth/service/AuthServiceTest.java +++ b/src/test/java/com/cisd/tms/modules/auth/service/AuthServiceTest.java @@ -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 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"); + } + } + } } diff --git a/src/test/java/com/cisd/tms/modules/auth/service/CompatAuthServiceTest.java b/src/test/java/com/cisd/tms/modules/auth/service/CompatAuthServiceTest.java deleted file mode 100644 index 457592b..0000000 --- a/src/test/java/com/cisd/tms/modules/auth/service/CompatAuthServiceTest.java +++ /dev/null @@ -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 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 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; - } -}