feat: 添加后台登录验证码、记住密码功能,优化媒体资源与前端规范
- 新增后台登录图形验证码功能,完善登录安全防护 - 新增登录rememberMe参数,控制Refresh Token的会话持久化策略 - 实现OSS私有桶媒体URL自动签名,统一处理图片资源的临时访问签名 - 新增素材库数据库表与上传API,规范媒体资源管理流程 - 统一前端UI图标使用@element-plus/icons-vue,重构布局图标组件 - 登录页新增验证码输入、刷新功能,添加账号记忆与记住密码逻辑 - 更新全套文档,补充API契约、技术决策记录与集成流程说明 - 修复多个业务页面的图标展示问题,新增认证流程相关测试用例
This commit is contained in:
1 parent
2c8c327de7
commit
6245159e7c
35 files changed
+914
-79
No files matched your search
@@ -76,6 +76,32 @@ def test_in_memory_store_supports_login_limit_and_permission_cache():
|
||||
assert store.get_permission_context("admin-1") == {"permissions": ["admin:read"]}
|
||||
|
||||
|
||||
def test_in_memory_captcha_is_single_use():
|
||||
store = InMemoryAdminSessionStore()
|
||||
store.create_captcha("captcha-1", "ABCD", 120)
|
||||
|
||||
assert store.consume_captcha("captcha-1", "ABCD") is True
|
||||
assert store.consume_captcha("captcha-1", "ABCD") is False
|
||||
|
||||
|
||||
def test_admin_captcha_endpoint_returns_graphical_challenge():
|
||||
store = InMemoryAdminSessionStore()
|
||||
app = create_app()
|
||||
app.dependency_overrides[get_admin_session_store] = lambda: store
|
||||
|
||||
try:
|
||||
response = TestClient(app).get("/api/admin/auth/captcha")
|
||||
finally:
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()["data"]
|
||||
assert data["captchaEnabled"] is True
|
||||
assert data["captchaId"]
|
||||
assert data["image"].startswith("data:image/svg+xml;base64,")
|
||||
assert data["expiresIn"] == 120
|
||||
|
||||
|
||||
def test_admin_access_token_contains_scoped_session_claims():
|
||||
user = AdminUser(
|
||||
id="admin-1",
|
||||
@@ -109,12 +135,20 @@ def test_admin_login_keeps_legacy_fields_and_sets_http_only_refresh_cookie():
|
||||
)
|
||||
app = create_app()
|
||||
app.dependency_overrides[get_db] = lambda: LoginDb(user)
|
||||
app.dependency_overrides[get_admin_session_store] = lambda: InMemoryAdminSessionStore()
|
||||
store = InMemoryAdminSessionStore()
|
||||
store.create_captcha("captcha-login", "ABCD", 120)
|
||||
app.dependency_overrides[get_admin_session_store] = lambda: store
|
||||
|
||||
try:
|
||||
response = TestClient(app).post(
|
||||
"/api/admin/auth/login",
|
||||
json={"email": "admin@example.com", "password": "ChangeMe123!"},
|
||||
json={
|
||||
"email": "admin@example.com",
|
||||
"password": "ChangeMe123!",
|
||||
"captchaId": "captcha-login",
|
||||
"captchaCode": "ABCD",
|
||||
"rememberMe": False,
|
||||
},
|
||||
)
|
||||
finally:
|
||||
app.dependency_overrides.clear()
|
||||
@@ -127,6 +161,7 @@ def test_admin_login_keeps_legacy_fields_and_sets_http_only_refresh_cookie():
|
||||
cookie = response.headers["set-cookie"]
|
||||
assert "HttpOnly" in cookie
|
||||
assert "Path=/api/admin/auth" in cookie
|
||||
assert "Max-Age=" not in cookie
|
||||
|
||||
|
||||
def test_refresh_rotates_access_token_and_logout_revokes_it():
|
||||
@@ -145,9 +180,16 @@ def test_refresh_rotates_access_token_and_logout_revokes_it():
|
||||
|
||||
try:
|
||||
client = TestClient(app)
|
||||
store.create_captcha("captcha-refresh", "ABCD", 120)
|
||||
login_response = client.post(
|
||||
"/api/admin/auth/login",
|
||||
json={"email": "admin@example.com", "password": "ChangeMe123!"},
|
||||
json={
|
||||
"email": "admin@example.com",
|
||||
"password": "ChangeMe123!",
|
||||
"captchaId": "captcha-refresh",
|
||||
"captchaCode": "ABCD",
|
||||
"rememberMe": False,
|
||||
},
|
||||
)
|
||||
old_token = login_response.json()["data"]["accessToken"]
|
||||
|
||||
@@ -155,6 +197,7 @@ def test_refresh_rotates_access_token_and_logout_revokes_it():
|
||||
new_token = refresh_response.json()["data"]["accessToken"]
|
||||
|
||||
assert refresh_response.status_code == 200
|
||||
assert "Max-Age=" not in refresh_response.headers["set-cookie"]
|
||||
assert new_token != old_token
|
||||
assert client.get("/api/admin/me", headers={"Authorization": f"Bearer {old_token}"}).status_code == 401
|
||||
assert client.get("/api/admin/me", headers={"Authorization": f"Bearer {new_token}"}).status_code == 200
|
||||
@@ -166,6 +209,45 @@ def test_refresh_rotates_access_token_and_logout_revokes_it():
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
|
||||
def test_remember_me_keeps_persistent_cookie_after_refresh_rotation():
|
||||
user = AdminUser(
|
||||
id="admin-1",
|
||||
email="admin@example.com",
|
||||
name="Admin",
|
||||
role="admin",
|
||||
passwordHash=hash_password("ChangeMe123!", rounds=4),
|
||||
isActive=True,
|
||||
)
|
||||
store = InMemoryAdminSessionStore()
|
||||
app = create_app()
|
||||
app.dependency_overrides[get_db] = lambda: LoginDb(user)
|
||||
app.dependency_overrides[get_admin_session_store] = lambda: store
|
||||
|
||||
try:
|
||||
client = TestClient(app)
|
||||
store.create_captcha("captcha-remember", "ABCD", 120)
|
||||
login_response = client.post(
|
||||
"/api/admin/auth/login",
|
||||
json={
|
||||
"email": "admin@example.com",
|
||||
"password": "ChangeMe123!",
|
||||
"captchaId": "captcha-remember",
|
||||
"captchaCode": "ABCD",
|
||||
"rememberMe": True,
|
||||
},
|
||||
)
|
||||
|
||||
assert login_response.status_code == 200
|
||||
assert "Max-Age=" in login_response.headers["set-cookie"]
|
||||
|
||||
refresh_response = client.post("/api/admin/auth/refresh")
|
||||
|
||||
assert refresh_response.status_code == 200
|
||||
assert "Max-Age=" in refresh_response.headers["set-cookie"]
|
||||
finally:
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
|
||||
def test_rbac_models_define_normalized_association_tables():
|
||||
assert AdminRole.__tablename__ == "AdminRole"
|
||||
assert AdminMenu.__tablename__ == "AdminMenu"
|
||||
|
||||
Reference in new issue
Block a user