modify: auth插件拆分,将token签发部分交给网关
This commit is contained in:
+40
-32
@@ -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
@@ -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
@@ -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,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
@@ -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
@@ -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,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"]
|
||||
|
||||
@@ -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()
|
||||
)
|
||||
Reference in New Issue
Block a user