完成70%
This commit is contained in:
@@ -1,128 +1,52 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$root = Split-Path -Parent $PSScriptRoot
|
||||
$json = Get-Content -LiteralPath (Join-Path $root 'APP.openapi.json') -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||
$yaml = Get-Content -LiteralPath (Join-Path $root 'APP.openapi.yaml') -Raw -Encoding UTF8
|
||||
$blockers = [System.Collections.Generic.List[string]]::new()
|
||||
$apiPath = Join-Path $root 'utils/api.js'
|
||||
$a01Path = Join-Path $root 'pages/auth/a01-entry.vue'
|
||||
$runtimePath = Join-Path $PSScriptRoot 'auth-api-runtime-smoke.js'
|
||||
$issues = New-Object System.Collections.Generic.List[string]
|
||||
|
||||
function Require-Operation {
|
||||
param([string]$Path, [string]$Method)
|
||||
$pathProperty = $json.paths.PSObject.Properties[$Path]
|
||||
if ($null -eq $pathProperty -or $null -eq $pathProperty.Value.PSObject.Properties[$Method]) {
|
||||
throw "认证源合同缺少操作:$($Method.ToUpperInvariant()) $Path"
|
||||
}
|
||||
if ($yaml -notmatch [regex]::Escape(" $Path`:") -or $yaml -notmatch "(?m)^ $Method`:\s*$") {
|
||||
throw "YAML 认证源合同缺少操作:$($Method.ToUpperInvariant()) $Path"
|
||||
}
|
||||
return $pathProperty.Value.PSObject.Properties[$Method].Value
|
||||
}
|
||||
|
||||
function Require-Schema {
|
||||
param([string]$Name)
|
||||
$property = $json.components.schemas.PSObject.Properties[$Name]
|
||||
if ($null -eq $property) { throw "认证源合同缺少 schema:$Name" }
|
||||
if ($yaml -notmatch "(?m)^ $([regex]::Escape($Name)):\s*$") { throw "YAML 认证源合同缺少 schema:$Name" }
|
||||
return $property.Value
|
||||
}
|
||||
|
||||
function Assert-ExactSet {
|
||||
param([object[]]$Actual, [object[]]$Expected, [string]$Label)
|
||||
$actualSet = @($Actual | ForEach-Object { [string]$_ } | Sort-Object -Unique)
|
||||
$expectedSet = @($Expected | ForEach-Object { [string]$_ } | Sort-Object -Unique)
|
||||
if (($actualSet -join ',') -ne ($expectedSet -join ',')) {
|
||||
throw "$Label 漂移:actual=[$($actualSet -join ',')] expected=[$($expectedSet -join ',')]"
|
||||
foreach ($path in @($apiPath, $a01Path, $runtimePath)) {
|
||||
if (-not (Test-Path -LiteralPath $path -PathType Leaf)) {
|
||||
$issues.Add("missing authentication owner: $path")
|
||||
}
|
||||
}
|
||||
|
||||
$operations = @(
|
||||
@('/captcha/require', 'get'),
|
||||
@('/captcha/challenge', 'post'),
|
||||
@('/captcha/verify', 'post'),
|
||||
@('/genealogy/app/auth/sms/code', 'post'),
|
||||
@('/genealogy/app/auth/login', 'post'),
|
||||
@('/genealogy/app/auth/login/sms', 'post'),
|
||||
@('/genealogy/app/auth/register', 'post'),
|
||||
@('/genealogy/app/auth/password/reset', 'put')
|
||||
)
|
||||
foreach ($entry in $operations) { [void](Require-Operation -Path $entry[0] -Method $entry[1]) }
|
||||
|
||||
$expectedVerificationScenes = @('APP_SMS_LOGIN', 'APP_REGISTER', 'APP_FORGOT_PASSWORD', 'APP_PHONE_CHANGE', 'APP_ACCOUNT_DEACTIVATE')
|
||||
foreach ($schemaName in @('VerificationChallengeBody', 'VerificationCheckBody')) {
|
||||
$schema = Require-Schema -Name $schemaName
|
||||
Assert-ExactSet -Actual @($schema.properties.sceneCode.enum) -Expected $expectedVerificationScenes -Label "$schemaName.sceneCode"
|
||||
}
|
||||
|
||||
$check = Require-Schema -Name 'VerificationCheckBody'
|
||||
Assert-ExactSet -Actual @($check.properties.payload.oneOf.'$ref') -Expected @('#/components/schemas/TianaiVerificationPayload', '#/components/schemas/SystemImageVerificationPayload') -Label 'VerificationCheckBody.payload.oneOf'
|
||||
$requiredCheckFields = @('tenantId', 'clientId', 'sceneCode', 'subject', 'challengeId', 'providerCode', 'captchaType', 'payload')
|
||||
$missingCheckFields = @($requiredCheckFields | Where-Object { $_ -notin @($check.required) })
|
||||
if ($missingCheckFields.Count -gt 0) {
|
||||
$blockers.Add("VerificationCheckBody 未强制字段:$($missingCheckFields -join '、')。")
|
||||
}
|
||||
if ($check.additionalProperties -ne $false) {
|
||||
$blockers.Add('VerificationCheckBody 未设置 additionalProperties=false,服务端校验边界仍可接受未声明字段。')
|
||||
}
|
||||
if (@($check.oneOf).Count -lt 2 -or $check.discriminator.propertyName -ne 'providerCode') {
|
||||
$blockers.Add('VerificationCheckBody 未用 providerCode 判别至少两个 oneOf 分支,providerCode、captchaType 与 payload 形态无法被原子约束。')
|
||||
}
|
||||
$tianaiPayload = Require-Schema -Name 'TianaiVerificationPayload'
|
||||
Assert-ExactSet -Actual @($tianaiPayload.required) -Expected @('track') -Label 'TianaiVerificationPayload.required'
|
||||
if ($tianaiPayload.properties.track.'$ref' -ne '#/components/schemas/TianaiCaptchaTrack') { throw '天爱校验载荷必须唯一包装为 payload.track' }
|
||||
if ($tianaiPayload.additionalProperties -ne $false) {
|
||||
$blockers.Add('TianaiVerificationPayload 未设置 additionalProperties=false,历史直传字段仍可能绕过 payload.track 约束。')
|
||||
}
|
||||
$systemImagePayload = Require-Schema -Name 'SystemImageVerificationPayload'
|
||||
if ($systemImagePayload.additionalProperties -ne $false) {
|
||||
$blockers.Add('SystemImageVerificationPayload 未设置 additionalProperties=false,系统图形验证码载荷边界未闭合。')
|
||||
}
|
||||
$track = Require-Schema -Name 'TianaiCaptchaTrack'
|
||||
Assert-ExactSet -Actual @($track.required) -Expected @('bgImageWidth', 'bgImageHeight', 'startTime', 'stopTime', 'trackList') -Label 'TianaiCaptchaTrack.required'
|
||||
if ($track.properties.trackList.minItems -ne 1) { throw '天爱行为轨迹不得为空' }
|
||||
|
||||
$smsCode = Require-Schema -Name 'SmsCodeBody'
|
||||
Assert-ExactSet -Actual @($smsCode.required) -Expected @('clientId', 'grantType', 'tenantId', 'sceneCode', 'phone', 'validToken') -Label 'SmsCodeBody.required'
|
||||
if ($smsCode.additionalProperties -ne $false) { throw 'SmsCodeBody 必须拒绝历史供应商字段' }
|
||||
$expectedPublicSmsScenes = @('APP_SMS_LOGIN', 'APP_REGISTER', 'APP_FORGOT_PASSWORD', 'APP_ACCOUNT_DEACTIVATE')
|
||||
$actualPublicSmsScenes = @($smsCode.properties.sceneCode.enum | ForEach-Object { [string]$_ } | Sort-Object -Unique)
|
||||
$expectedPublicSmsScenes = @($expectedPublicSmsScenes | Sort-Object -Unique)
|
||||
if (($actualPublicSmsScenes -join ',') -ne ($expectedPublicSmsScenes -join ',')) {
|
||||
$blockers.Add('公共 SmsCodeBody.sceneCode 必须删除 APP_PHONE_CHANGE;换绑发码只能由需要 SaToken 的专用 /auth/phone/sms/code operation 持有。')
|
||||
}
|
||||
|
||||
$smsSecretProperty = $json.components.schemas.PSObject.Properties['SmsCodeSecret']
|
||||
if ($null -eq $smsSecretProperty) {
|
||||
$blockers.Add('缺少全认证场景共用的 SmsCodeSecret;当前四位码必须原子升级为严格六位 ASCII 数字,不能保留 4/6 双接受。')
|
||||
} else {
|
||||
$smsSecret = $smsSecretProperty.Value
|
||||
if ($smsSecret.type -ne 'string' -or $smsSecret.writeOnly -ne $true -or
|
||||
[int]$smsSecret.minLength -ne 6 -or [int]$smsSecret.maxLength -ne 6 -or
|
||||
[string]$smsSecret.pattern -ne '^[0-9]{6}$' -or $smsSecret.example) {
|
||||
$blockers.Add('SmsCodeSecret 必须是无示例、保留前导零的 writeOnly 六位 ASCII 数字字符串。')
|
||||
if ($issues.Count -eq 0) {
|
||||
$api = Get-Content -Raw -Encoding UTF8 -LiteralPath $apiPath
|
||||
$a01 = Get-Content -Raw -Encoding UTF8 -LiteralPath $a01Path
|
||||
$passwordOwner = [regex]::Match($api, '(?s)async loginWithPassword\(\{ phone, passwordHash \}.*?(?=\s+async loginWithSms)')
|
||||
if (-not $passwordOwner.Success) {
|
||||
$issues.Add('missing bounded password login owner')
|
||||
} else {
|
||||
foreach ($required in @("url: '/genealogy/app/auth/login'", "grantType: 'password'", 'password: assertPasswordHash(passwordHash)')) {
|
||||
if (-not $passwordOwner.Value.Contains($required)) { $issues.Add("password login owner missing: $required") }
|
||||
}
|
||||
if ($passwordOwner.Value.Contains('validToken')) {
|
||||
$issues.Add('password login must not upload validToken')
|
||||
}
|
||||
}
|
||||
foreach ($required in @('const preparePasswordLogin = async () =>', 'appApi.loginWithPassword', 'TAC')) {
|
||||
if (-not $a01.Contains($required)) { $issues.Add("A01 missing password TAC precondition: $required") }
|
||||
}
|
||||
$smsOwner = [regex]::Match($api, '(?s)async sendSmsCode\(\{ sceneCode, phone, validToken \}.*?(?=\s+async loginWithPassword)')
|
||||
if (-not $smsOwner.Success -or -not $smsOwner.Value.Contains('validToken: assertValidToken(validToken)')) {
|
||||
$issues.Add('SMS owner must remain the sole validToken consumer')
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($schemaName in @('SmsLoginBody', 'PasswordRegisterBody', 'PasswordResetBody')) {
|
||||
$schema = Require-Schema -Name $schemaName
|
||||
$actualRef = [string]$schema.properties.smsCode.'$ref'
|
||||
if ($actualRef -ne '#/components/schemas/SmsCodeSecret') {
|
||||
$blockers.Add("$schemaName.smsCode 必须引用唯一 SmsCodeSecret,禁止继续内联四位码或接受双长度。")
|
||||
if ($issues.Count -eq 0) {
|
||||
$runtimeOutput = @(& node $runtimePath 2>&1)
|
||||
if ($LASTEXITCODE -ne 0 -or 'AUTH-API-RUNTIME-SMOKE PASS' -notin $runtimeOutput) {
|
||||
$issues.Add("password and SMS wire runtime smoke failed: $($runtimeOutput -join ' | ')")
|
||||
}
|
||||
}
|
||||
|
||||
$passwordLogin = Require-Schema -Name 'PasswordLoginBody'
|
||||
$passwordFields = @($passwordLogin.properties.PSObject.Properties.Name)
|
||||
$passwordRequired = @($passwordLogin.required)
|
||||
if ('validToken' -notin $passwordFields -or 'validToken' -notin $passwordRequired) {
|
||||
$blockers.Add('PasswordLoginBody 未定义并强制消费 validToken,密码登录无法形成服务端 TAC 闭环,客户端先滑后登录仍可被绕过。')
|
||||
}
|
||||
|
||||
if ($blockers.Count -gt 0) {
|
||||
$details = $blockers | ForEach-Object { "- $_" }
|
||||
throw (@(
|
||||
'AUTH-TAC-OPENAPI-CONTRACT BLOCKED'
|
||||
$details
|
||||
'- 关闭条件:后端同步更新同版本 JSON/YAML;校验体按 providerCode 严格区分供应商并拒绝缺字段/多余字段;密码登录原子消费绑定租户、客户端、场景、手机号的一次性 TAC 票据;全活动短信码原子迁移为六位;APP_PHONE_CHANGE 改由专用受保护发码 operation;全部部署到 HTTPS 环境并通过反向用例。'
|
||||
) -join [Environment]::NewLine)
|
||||
if ($issues.Count -gt 0) {
|
||||
Write-Output 'AUTH-TAC-OPENAPI-CONTRACT BLOCKED'
|
||||
foreach ($issue in $issues) { Write-Output "- $issue" }
|
||||
Write-Output '- Password login is gated by native TAC on the client and must not send validToken. SMS operations consume their own validToken only.'
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Output 'AUTH-TAC-OPENAPI-CONTRACT PASS'
|
||||
|
||||
Reference in New Issue
Block a user