文件
hyb-oa/server/scripts/regression-auth-security.sh
T
wangsli f755ff0dda feat(server): P5 基础能力补齐(组织/报销/代办/认证安全)
- 组织/人员/委派对齐:费用类型、部门、员工别名、配置只读与回归脚本(F2/F3/F26/F27/F29/F31/F40)

- 报销单 PATCH/DELETE 与 items CRUD(事务内重算 total_amount)(F4/F5)

- 行政代提交/代上传后端校验与审计(F7/F8)

- 认证安全基线:首次登录改密、失败锁定、弱密码拦截与回归脚本(F36/F37/F39/F41)
2026-02-09 16:20:31 +08:00

216 行
8.6 KiB
Bash
可执行文件

#!/usr/bin/env bash
set -euo pipefail
BASE_URL="${BASE_URL:-http://127.0.0.1:3000}"
DEVICE_FINGERPRINT="${DEVICE_FINGERPRINT:-regression-auth-security}"
ADMIN_IDENTIFIER="${ADMIN_IDENTIFIER:-admin@hyinda.com}"
ADMIN_PASSWORD="${ADMIN_PASSWORD:-AdminPassword1234}"
LOGIN_FAIL_MAX_ATTEMPTS="${LOGIN_FAIL_MAX_ATTEMPTS:-${AUTH_LOGIN_FAIL_MAX_ATTEMPTS:-5}}"
if ! command -v curl >/dev/null 2>&1; then
echo "[ERROR] curl not found" >&2
exit 1
fi
if ! command -v jq >/dev/null 2>&1; then
echo "[ERROR] jq not found" >&2
exit 1
fi
TMP_DIR="$(mktemp -d)"
cleanup() {
rm -rf "$TMP_DIR"
}
trap cleanup EXIT
extract_json_field() {
local file_path="$1"
local jq_expr="$2"
jq -er "$jq_expr" "$file_path"
}
assert_status() {
local actual="$1"
local expected="$2"
local name="$3"
local body_file="${4:-}"
if [[ "$actual" != "$expected" ]]; then
echo "[ERROR] $name expected HTTP $expected but got $actual" >&2
if [[ -n "$body_file" && -f "$body_file" ]]; then
cat "$body_file" >&2
fi
exit 1
fi
}
assert_error_code() {
local file_path="$1"
local expected_code="$2"
local name="$3"
local actual_code
actual_code="$(jq -r '.error.code // empty' "$file_path")"
if [[ "$actual_code" != "$expected_code" ]]; then
echo "[ERROR] $name expected error code $expected_code but got $actual_code" >&2
cat "$file_path" >&2
exit 1
fi
}
login() {
local identifier="$1"
local password="$2"
local fingerprint="$3"
local out_body="$4"
local status
status="$(curl -sS -o "$out_body" -w "%{http_code}" \
-X POST "$BASE_URL/api/auth/login" \
-H "content-type: application/json" \
-H "x-device-fingerprint: $fingerprint" \
-d "{\"identifier\":\"$identifier\",\"password\":\"$password\"}")"
echo "$status"
}
echo "[1/8] Login admin"
admin_login_body="$TMP_DIR/admin-login.json"
admin_login_status="$(login "$ADMIN_IDENTIFIER" "$ADMIN_PASSWORD" "${DEVICE_FINGERPRINT}-admin" "$admin_login_body")"
assert_status "$admin_login_status" "200" "admin login" "$admin_login_body"
ADMIN_TOKEN="$(extract_json_field "$admin_login_body" '.data.accessToken')"
echo "[2/8] Weak password should be rejected"
weak_employee_no="W$(date +%s)$RANDOM"
weak_employee_email="weak_${weak_employee_no}@hyinda.com"
weak_employee_phone="139${RANDOM}${RANDOM}"
weak_create_body="$TMP_DIR/weak-create.json"
weak_create_status="$(curl -sS -o "$weak_create_body" -w "%{http_code}" \
-X POST "$BASE_URL/api/users" \
-H "authorization: Bearer $ADMIN_TOKEN" \
-H "content-type: application/json" \
-d "{\"employeeNo\":\"$weak_employee_no\",\"name\":\"Weak User\",\"phone\":\"$weak_employee_phone\",\"email\":\"$weak_employee_email\",\"title\":\"测试\",\"directManagerId\":null,\"hireDate\":\"2020-01-01\",\"roles\":[\"employee\"],\"password\":\"Password123456\"}")"
assert_status "$weak_create_status" "400" "weak password rejected" "$weak_create_body"
assert_error_code "$weak_create_body" "VALID_001" "weak password rejected"
if ! jq -e '.error.details.password | type == "array" and (map(tostring) | join(" ") | contains("常见"))' "$weak_create_body" >/dev/null; then
echo "[ERROR] weak password error message missing" >&2
cat "$weak_create_body" >&2
exit 1
fi
echo "[3/8] Create employee (initial password => mustChangePassword)"
employee_no="A$(date +%s)$RANDOM"
employee_email="auth_${employee_no}@hyinda.com"
employee_phone="138${RANDOM}${RANDOM}"
INITIAL_PASSWORD="InitA1$(date +%s%N)Xy"
create_body="$TMP_DIR/create.json"
create_status="$(curl -sS -o "$create_body" -w "%{http_code}" \
-X POST "$BASE_URL/api/users" \
-H "authorization: Bearer $ADMIN_TOKEN" \
-H "content-type: application/json" \
-d "{\"employeeNo\":\"$employee_no\",\"name\":\"Auth User\",\"phone\":\"$employee_phone\",\"email\":\"$employee_email\",\"title\":\"测试\",\"directManagerId\":null,\"hireDate\":\"2020-01-01\",\"roles\":[\"employee\"],\"password\":\"$INITIAL_PASSWORD\"}")"
assert_status "$create_status" "200" "create employee" "$create_body"
EMPLOYEE_ID="$(extract_json_field "$create_body" '.data.id')"
login_body_1="$TMP_DIR/login1.json"
login_status_1="$(login "$employee_email" "$INITIAL_PASSWORD" "${DEVICE_FINGERPRINT}-employee" "$login_body_1")"
assert_status "$login_status_1" "200" "initial login" "$login_body_1"
if [[ "$(jq -r '.data.mustChangePassword' "$login_body_1")" != "true" ]]; then
echo "[ERROR] expected mustChangePassword=true on initial login" >&2
cat "$login_body_1" >&2
exit 1
fi
ACCESS_TOKEN_1="$(extract_json_field "$login_body_1" '.data.accessToken')"
echo "[4/8] Change password should revoke old token"
NEW_PASSWORD="NewA1$(date +%s%N)Zz"
change_body="$TMP_DIR/change.json"
change_status="$(curl -sS -o "$change_body" -w "%{http_code}" \
-X POST "$BASE_URL/api/auth/change-password" \
-H "authorization: Bearer $ACCESS_TOKEN_1" \
-H "content-type: application/json" \
-H "x-device-fingerprint: ${DEVICE_FINGERPRINT}-employee" \
-d "{\"oldPassword\":\"$INITIAL_PASSWORD\",\"newPassword\":\"$NEW_PASSWORD\"}")"
assert_status "$change_status" "200" "change password" "$change_body"
ACCESS_TOKEN_2="$(extract_json_field "$change_body" '.data.accessToken')"
if [[ "$(jq -r '.data.mustChangePassword' "$change_body")" != "false" ]]; then
echo "[ERROR] expected mustChangePassword=false after change" >&2
cat "$change_body" >&2
exit 1
fi
old_me_body="$TMP_DIR/old-me.json"
old_me_status="$(curl -sS -o "$old_me_body" -w "%{http_code}" \
-X GET "$BASE_URL/api/me" \
-H "authorization: Bearer $ACCESS_TOKEN_1")"
assert_status "$old_me_status" "401" "old token revoked" "$old_me_body"
assert_error_code "$old_me_body" "AUTH_004" "old token revoked"
new_me_body="$TMP_DIR/new-me.json"
new_me_status="$(curl -sS -o "$new_me_body" -w "%{http_code}" \
-X GET "$BASE_URL/api/me" \
-H "authorization: Bearer $ACCESS_TOKEN_2")"
assert_status "$new_me_status" "200" "new token works" "$new_me_body"
echo "[5/8] Admin reset password => mustChangePassword=true"
reset_body="$TMP_DIR/reset.json"
reset_status="$(curl -sS -o "$reset_body" -w "%{http_code}" \
-X POST "$BASE_URL/api/users/$EMPLOYEE_ID/reset-password" \
-H "authorization: Bearer $ADMIN_TOKEN" \
-H "content-type: application/json" \
-d '{}')"
assert_status "$reset_status" "200" "admin reset password" "$reset_body"
ONE_TIME_PASSWORD="$(extract_json_field "$reset_body" '.data.oneTimePassword')"
login_body_2="$TMP_DIR/login2.json"
login_status_2="$(login "$employee_email" "$ONE_TIME_PASSWORD" "${DEVICE_FINGERPRINT}-employee-reset" "$login_body_2")"
assert_status "$login_status_2" "200" "login with reset password" "$login_body_2"
if [[ "$(jq -r '.data.mustChangePassword' "$login_body_2")" != "true" ]]; then
echo "[ERROR] expected mustChangePassword=true after admin reset" >&2
cat "$login_body_2" >&2
exit 1
fi
echo "[6/8] Login failure lockout (AUTH_002)"
wrong_password="WrongPassword1234"
locked=0
for i in $(seq 1 "$LOGIN_FAIL_MAX_ATTEMPTS"); do
attempt_body="$TMP_DIR/lock-attempt-$i.json"
attempt_status="$(login "$employee_email" "$wrong_password" "${DEVICE_FINGERPRINT}-lock-$i" "$attempt_body")"
if [[ "$i" -lt "$LOGIN_FAIL_MAX_ATTEMPTS" ]]; then
assert_status "$attempt_status" "401" "lock attempt $i" "$attempt_body"
assert_error_code "$attempt_body" "AUTH_001" "lock attempt $i"
else
assert_status "$attempt_status" "423" "lock attempt $i triggers lock" "$attempt_body"
assert_error_code "$attempt_body" "AUTH_002" "lock attempt $i triggers lock"
if [[ "$(jq -r '.error.details.lockedUntil // empty' "$attempt_body")" == "" ]]; then
echo "[ERROR] expected lockedUntil in AUTH_002 details" >&2
cat "$attempt_body" >&2
exit 1
fi
locked=1
fi
done
if [[ "$locked" != "1" ]]; then
echo "[ERROR] expected account to be locked" >&2
exit 1
fi
echo "[7/8] Locked account cannot login, unlock restores"
locked_login_body="$TMP_DIR/locked-login.json"
locked_login_status="$(login "$employee_email" "$ONE_TIME_PASSWORD" "${DEVICE_FINGERPRINT}-locked-correct" "$locked_login_body")"
assert_status "$locked_login_status" "423" "locked account login blocked" "$locked_login_body"
assert_error_code "$locked_login_body" "AUTH_002" "locked account login blocked"
unlock_body="$TMP_DIR/unlock.json"
unlock_status="$(curl -sS -o "$unlock_body" -w "%{http_code}" \
-X POST "$BASE_URL/api/users/$EMPLOYEE_ID/unlock" \
-H "authorization: Bearer $ADMIN_TOKEN")"
assert_status "$unlock_status" "200" "unlock user" "$unlock_body"
post_unlock_login_body="$TMP_DIR/post-unlock-login.json"
post_unlock_login_status="$(login "$employee_email" "$ONE_TIME_PASSWORD" "${DEVICE_FINGERPRINT}-post-unlock" "$post_unlock_login_body")"
assert_status "$post_unlock_login_status" "200" "login after unlock" "$post_unlock_login_body"
echo "[8/8] Done"
echo "[OK] Auth security regression passed"