modify: auth插件拆分,将token签发部分交给网关

This commit is contained in:
2026-09-10 16:43:43 +08:00
parent 5ec90a5575
commit c139b0c1d3
23 changed files with 691 additions and 290 deletions
+40 -32
View File
@@ -1,26 +1,36 @@
# Auth Worker 插件
# Auth Worker 插件 — 用户管理
用户认证与授权插件,提供完整的用户管理系统。
auth 域拆分后的**用户管理插件**(可替换的业务部分)。
登录 / token 签发 / admin 种子已**内置在网关**(管理平面底线,不依赖本插件是否
加载,本插件缩容到 0 或全部副本故障时登录与 admin API 仍可用)。
共享契约归 `chongming-auth` 包(`utils/auth/`):Users 模型(唯一权威定义)、
建库、登录校验、JWT 签发。本插件与网关内置登录**共用同一张 users 表**。
---
## 功能
- **用户注册**:用户名 + 邮箱 + 密码注册,自动校验唯一性
- **用户登录**:JWT Token 签发,有效期 3 小时
- **获取用户信息**:根据 JWT Token 获取完整用户档案
- **修改密码**:旧密码验证 + 新密码更新
- **更新资料**:更新全名、邮箱、头像、手机号等信息
- **自动初始化**:首次启动自动创建 `admin` 管理员账户
(登录 `POST /auth/login` 由网关内置提供,路径与响应 shape 不变)
---
## 数据库
使用 SQLite(通过 `chongming-database` 异步封装),数据库文件位于 `data/storage/chongming/auth.db`。
使用 SQLite(通过 `chongming-database` 异步封装),数据库文件位于
`data/auth.db`(`[workers.config].data_dir` 下);配置 `database.url` 时走
PostgreSQL。库位置与网关内置登录读同一份配置(本插件 `config.toml` 的
`[workers.config]`),指向同一张 users 表。
### Users 表结构
权威定义见 `chongming_auth.models.Users`(本仓库 `models/auth.py` 仅为 re-export):
| 字段 | 类型 | 说明 |
| ----------------- | ----------------- | --------------------------------- |
| `id` | `str` (PK) | UUID 格式主键 |
@@ -45,6 +55,27 @@
所有端点路由前缀为 `/auth`。
### `POST /auth/login` — 登录(网关内置)
无需认证。由网关核心 `chongming/app/routes/auth.py` 提供(非本插件),
路径与响应 shape 与拆分前一致。JWT claims 契约见 `chongming-auth` 包 README。
```json
// 请求
{
"username": "admin",
"password": "admin"
}
// 响应
{
"result": "success",
"token": "eyJhbGciOiJIUzI1NiIs...",
"message": "登录成功",
"timestamp": 1720000000.123
}
```
### `POST /auth/register` — 用户注册
无需认证。
@@ -68,28 +99,6 @@
}
```
### `POST /auth/login` — 登录
无需认证。
```json
// 请求
{
"username": "admin",
"password": "admin"
}
// 响应
{
"result": "success",
"token": "eyJhbGciOiJIUzI1NiIs...",
"message": "登录成功",
"timestamp": 1720000000.123
}
```
JWT Token 有效期 3 小时,载荷包含:`sub`(用户ID)、`username`、`email`、`full_name`、`is_superuser` 等。
### `GET /auth/me?user_id=<id>` — 获取用户信息
需要 JWT 鉴权(`Authorization: Bearer <token>`)。
@@ -159,9 +168,9 @@ JWT Token 有效期 3 小时,载荷包含:`sub`(用户ID)、`username`
## 默认管理员账户
插件基于 `Plugin` 基类统一生命周期:`on_init` 中执行管理员种子逻辑
(先按用户名、再按邮箱兜底查询;多进程并发启动时靠唯一约束 + `IntegrityError` 回滚保证幂等),
数据库 `auth.db` 由基类统一初始化并在 cleanup 时释放。
admin 种子逻辑随登录一并内置到网关(网关 lifespan 的 `setup_auth_store`
→ `chongming_auth.seed_admin`,幂等,多进程并发启动安全),不再依赖本插件。
数据库 `auth.db` 本插件仍由 `Plugin` 基类统一初始化并在 cleanup 时释放。
首次启动时自动创建:
@@ -190,8 +199,7 @@ JWT Token 有效期 3 小时,载荷包含:`sub`(用户ID)、`username`
| 工具包 | 用途 |
| -------------------- | ------------------------------------ |
| `chongming-auth` | Users 模型 / 建库 / 登录校验 / 签发(共享契约) |
| `chongming-database` | 异步 SQLite 数据库操作 |
| `chongming-jwt` | Rust JWT 编码/验证 |
| `chongming-password` | Rust Argon2id 密码哈希 |
| `chongming-config` | 全局配置加载 |
| `chongming-worker` | `@rpc_method` 装饰器 + Worker 运行时 |
+11 -44
View File
@@ -1,19 +1,22 @@
"""
Auth Worker 插件 — 鉴权管理(登录 / 注册 / 资料 / 改密)
Auth Worker 插件 — 用户管理(注册 / 资料 / 改密)
基于 Plugin 基类统一生命周期:数据库由基类初始化(auth.db),
on_init 中执行 admin 幂等种子逻辑,on_cleanup 无需重复 dispose。
auth 域拆分后本插件只保留用户管理功能(可替换的业务部分);
登录 / token 签发 / admin 种子已内置在网关(chongming_auth 包 +
chongming.app.routes.auth),不依赖本插件是否加载。
基于 Plugin 基类统一生命周期:数据库由基类初始化(auth.db,与网关内置
登录共用同一张 users 表),on_cleanup 无需重复 dispose。
"""
import logging
from datetime import datetime
from chongming_worker.plugin import Plugin
from chongming_password import hash_password
from .router import * # noqa: F401,F403 触发 @rpc_method 注册
# 模块加载时即注册 SQLModel 元数据 —— 基类 _bootstrap 建表(create_all)只创建
# 已导入的表,若拖到 on_init 才导入会导致全新库缺少 users 表(no such table)。
# Users 权威定义在 chongming_auth 包,此处 re-export 注册元数据。
from .models.auth import Users # noqa: F401
logger = logging.getLogger(__name__)
@@ -23,49 +26,13 @@ class AuthPlugin(Plugin):
"""Auth 插件 — 统一生命周期。"""
type = "auth"
db_name = "auth.db" # 自动在 data_dir 下建 SQLite 库
db_name = "auth.db" # 自动在 data_dir 下建 SQLite 库(与网关内置登录共用)
auto_bind = True # 首次初始化时自动 auto_bind_models("auth")
async def on_init(self):
"""首次 create_workers 时调用一次:初始化 admin 账号(幂等)。"""
from .models.auth import Users
from sqlalchemy import select
from sqlalchemy.exc import IntegrityError
db = self.db
# 初始化管理员(多进程并发启动时必须幂等,避免 UNIQUE 约束冲突)
async with db.get_session() as sess:
select_stmt = select(Users) \
.where(Users.username == "admin") # type: ignore
result = await sess.execute(select_stmt)
user: Users | None = result.scalar_one_or_none()
# 按用户名未命中时,再按邮箱兜底,避免历史数据中已存在同邮箱用户
if user is None:
select_stmt = select(Users) \
.where(Users.email == "admin@example.com") # type: ignore
result = await sess.execute(select_stmt)
user = result.scalar_one_or_none()
if user is None:
user = Users(
username="admin",
hashed_password=hash_password("admin"),
email="admin@example.com",
created_at=datetime.now(),
updated_at=datetime.now(),
is_superuser=True
)
sess.add(user)
try:
await sess.commit()
except IntegrityError:
# 其他并发进程已抢先创建 admin,当前进程忽略即可
await sess.rollback()
"""首次 create_workers 时调用一次。"""
# 写入全局变量供 router 使用(数据库由基类在 cleanup 时统一 dispose)
self.set_var("db", db)
self.set_var("db", self.db)
async def on_cleanup(self):
"""cleanup 时调用(数据库由基类处理)。"""
+4 -7
View File
@@ -1,14 +1,13 @@
from typing import Any
from chongming_config import get_config
from chongming_jwt import JWT
from chongming_database import Database
from .models import *
def set_global_var(name: str, value: Any) -> None:
"""
设置当前模块的全局变量。
Args:
name: 变量名(字符串)
value: 任意值
@@ -19,11 +18,9 @@ config = get_config()
db: Database | None = None
secret = config["security"]["secret_key"]
algorithm = config["security"]["algorithm"]
jwt = JWT(secret, algorithm)
# JWT 签发已随登录内置到网关(chongming.app.routes.auth / chongming_auth 包),
# 插件不再持有 jwt 实例
__all__ = [
"db",
"jwt"
]
]
+1 -4
View File
@@ -1,13 +1,12 @@
"""
模型导出 — auth
自动生成,请勿手动修改
(LoginPostRequest/LoginPostResponse 已随内置登录迁移至 chongming_auth 包)
"""
from .pydantic import AuthResponse
from .pydantic import ChangePasswordPostRequest
from .pydantic import ChangePasswordPostResponse
from .pydantic import LoginPostRequest
from .pydantic import LoginPostResponse
from .pydantic import MeGetRequest
from .pydantic import MeGetResponse
from .pydantic import RegisterPostRequest
@@ -21,8 +20,6 @@ __all__ = [
"AuthResponse",
"ChangePasswordPostRequest",
"ChangePasswordPostResponse",
"LoginPostRequest",
"LoginPostResponse",
"MeGetRequest",
"MeGetResponse",
"RegisterPostRequest",
+8 -100
View File
@@ -1,102 +1,10 @@
import os
import time
from typing import Optional
from datetime import datetime
from uuid import uuid4
"""Users 模型 re-export —— 权威定义已迁移至共享包 chongming_auth。
from sqlmodel import Field, SQLModel, Column
from sqlalchemy import func, DateTime
auth 域拆分后:Users 模型 / 建库 / 登录校验 / JWT 签发归 chongming_auth 包
(网关内置登录与 auth 插件共用,指向同一张 users 表)。本模块保留 re-export
以兼容插件内既有相对导入(from ..models.auth import Users);
re-export 保证全进程单一模型定义,SQLModel 元数据不会重复注册。
"""
from chongming_auth import Users # noqa: F401
def primary_key_create():
return f"{uuid4()}_{os.getpid()}_{int(time.time())}"
class Users(SQLModel, table=True):
__tablename__ = "users" # type: ignore
# ---------- 主键 ----------
id: str = Field(
default_factory=primary_key_create,
primary_key=True,
index=True,
description="用户唯一ID(UUID格式)"
)
# ---------- 登录凭证(唯一索引) ----------
username: str = Field(
max_length=50,
unique=True,
index=True,
nullable=False,
description="登录用户名"
)
email: str = Field(
max_length=255,
unique=True,
index=True,
nullable=False,
description="电子邮箱"
)
hashed_password: str = Field(
max_length=255,
nullable=False,
description="加密后的密码(不存明文)"
)
# ---------- 个人信息(可选) ----------
full_name: Optional[str] = Field(
default=None,
max_length=100,
description="用户全名"
)
avatar_url: Optional[str] = Field(
default=None,
max_length=500,
description="头像图片链接"
)
phone: Optional[str] = Field(
default=None,
max_length=20,
description="手机号码"
)
# ---------- 安全与状态标识 ----------
is_active: bool = Field(
default=True,
description="账户是否启用(用于软禁用)"
)
is_verified: bool = Field(
default=False,
description="邮箱/手机是否已验证"
)
is_superuser: bool = Field(
default=False,
description="是否为超级管理员"
)
# ---------- 审计时间戳(数据库自动维护) ----------
created_at: datetime = Field(
sa_column=Column(
DateTime(timezone=True),
server_default=func.now(),
nullable=False,
comment="创建时间"
)
)
updated_at: datetime = Field(
sa_column=Column(
DateTime(timezone=True),
server_default=func.now(),
onupdate=func.now(),
nullable=False,
comment="最后更新时间"
)
)
# ---------- 最后登录记录(可选) ----------
last_login_at: Optional[datetime] = Field(
default=None,
sa_column=Column(DateTime(timezone=True), nullable=True),
description="最后一次登录时间"
)
__all__ = ["Users"]
+2 -13
View File
@@ -20,18 +20,6 @@ class ChangePasswordPostResponse(BaseModel):
message: str = Field(...)
timestamp: Optional[float] = Field(default=None)
class LoginPostRequest(BaseModel):
"""登录 — 请求体"""
username: str = Field(...)
password: str = Field(...)
class LoginPostResponse(BaseModel):
"""登录 — 响应体"""
result: str = Field(...)
token: str = Field(...)
message: str = Field(...)
timestamp: Optional[float] = Field(default=None)
class MeGetRequest(BaseModel):
"""获取当前用户信息 — 请求体"""
user_id: str = Field(...)
@@ -83,4 +71,5 @@ class UpdateProfilePostResponse(BaseModel):
# 所有响应模型的联合类型
AuthResponse = LoginPostResponse | ChangePasswordPostResponse | MeGetResponse | UpdateProfilePostResponse | RegisterPostResponse
# (登录响应 LoginPostResponse 已随内置登录迁移至 chongming_auth 包)
AuthResponse = ChangePasswordPostResponse | MeGetResponse | UpdateProfilePostResponse | RegisterPostResponse
+1 -2
View File
@@ -1,8 +1,7 @@
from .login import login
from .register import register
from .me import me
from .update_profile import update_profile
from .change_password import change_password
__all__ = ["login", "register", "me", "update_profile", "change_password"]
__all__ = ["register", "me", "update_profile", "change_password"]
-85
View File
@@ -1,85 +0,0 @@
import time
from datetime import datetime
import orjson
from sqlalchemy import select
from chongming_worker import rpc_method
from chongming_password import verify_password
from ..models.pydantic import (
LoginPostRequest,
LoginPostResponse
)
from ..models.auth import Users
@rpc_method(
subject="auth.login",
method="POST",
summary="登录",
response_model=LoginPostResponse,
)
async def login(login_post_request: LoginPostRequest) -> LoginPostResponse:
from ..global_var import jwt, db
if db is None:
raise RuntimeError("Database not initialized")
async with db.get_session() as session:
statement = select(Users).filter_by(
username=login_post_request.username,
)
result = await session.execute(statement)
user: Users | None = result.scalar_one_or_none()
if user is None:
return LoginPostResponse(
result="fail",
message="用户名错误",
token="",
timestamp=time.time()
)
if not verify_password(login_post_request.password, user.hashed_password):
return LoginPostResponse(
result="fail",
message="密码错误",
token="",
timestamp=time.time()
)
if not user.is_active:
return LoginPostResponse(
result="fail",
message="用户已经注销或被禁用",
token="",
timestamp=time.time()
)
user.last_login_at = datetime.now()
payload = {
"sub": user.id,
"username": user.username,
"email": user.email,
"full_name": user.full_name,
"avatar_url": user.avatar_url,
"phone": user.phone,
"is_verified": user.is_verified,
"is_superuser": user.is_superuser,
"created_at": user.created_at.isoformat(),
"updated_at": user.updated_at.isoformat(),
"last_login_at": user.last_login_at.isoformat()
}
token = jwt.encode(
orjson.dumps(payload).decode(),
expires_in_seconds=10800 # 3小时有效期
)
return LoginPostResponse(
result="success",
message="登录成功",
token=token,
timestamp=time.time()
)