All checks were successful
continuous-integration/drone/push Build is passing
- 设置 HTTPBearer(auto_error=False) 避免验证错误 - 在 get_current_user 中手动检查并返回 401
348 lines
9.6 KiB
Python
348 lines
9.6 KiB
Python
"""认证路由"""
|
||
import hmac
|
||
from fastapi import APIRouter, Depends, HTTPException, status
|
||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||
from pydantic import BaseModel
|
||
from typing import Optional, List
|
||
from sqlalchemy.orm import Session
|
||
|
||
from ..database import get_db
|
||
from ..services.auth import (
|
||
authenticate_user,
|
||
create_access_token,
|
||
decode_token,
|
||
update_last_login,
|
||
hash_password,
|
||
TokenData,
|
||
UserInfo
|
||
)
|
||
from ..services.crypto import decrypt_config
|
||
from ..models.user import User
|
||
from ..models.tenant_app import TenantApp
|
||
from ..models.tenant_wechat_app import TenantWechatApp
|
||
|
||
router = APIRouter(prefix="/auth", tags=["认证"])
|
||
security = HTTPBearer(auto_error=False)
|
||
|
||
|
||
class LoginRequest(BaseModel):
|
||
"""登录请求"""
|
||
username: str
|
||
password: str
|
||
|
||
|
||
class LoginResponse(BaseModel):
|
||
"""登录响应"""
|
||
success: bool
|
||
token: Optional[str] = None
|
||
user: Optional[UserInfo] = None
|
||
error: Optional[str] = None
|
||
|
||
|
||
class ChangePasswordRequest(BaseModel):
|
||
"""修改密码请求"""
|
||
old_password: str
|
||
new_password: str
|
||
|
||
|
||
# 权限依赖
|
||
|
||
async def get_current_user(
|
||
credentials: Optional[HTTPAuthorizationCredentials] = Depends(security),
|
||
db: Session = Depends(get_db)
|
||
) -> User:
|
||
"""获取当前用户"""
|
||
if not credentials:
|
||
raise HTTPException(
|
||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||
detail="需要登录认证",
|
||
headers={"WWW-Authenticate": "Bearer"}
|
||
)
|
||
|
||
token = credentials.credentials
|
||
token_data = decode_token(token)
|
||
|
||
if not token_data:
|
||
raise HTTPException(
|
||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||
detail="Token 无效或已过期",
|
||
headers={"WWW-Authenticate": "Bearer"}
|
||
)
|
||
|
||
user = db.query(User).filter(User.id == token_data.user_id).first()
|
||
if not user or user.status != 1:
|
||
raise HTTPException(
|
||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||
detail="用户不存在或已禁用",
|
||
headers={"WWW-Authenticate": "Bearer"}
|
||
)
|
||
|
||
return user
|
||
|
||
|
||
async def require_admin(user: User = Depends(get_current_user)) -> User:
|
||
"""要求管理员权限"""
|
||
if user.role != 'admin':
|
||
raise HTTPException(
|
||
status_code=status.HTTP_403_FORBIDDEN,
|
||
detail="需要管理员权限"
|
||
)
|
||
return user
|
||
|
||
|
||
async def require_operator(user: User = Depends(get_current_user)) -> User:
|
||
"""要求操作员以上权限"""
|
||
if user.role not in ('admin', 'operator'):
|
||
raise HTTPException(
|
||
status_code=status.HTTP_403_FORBIDDEN,
|
||
detail="需要操作员以上权限"
|
||
)
|
||
return user
|
||
|
||
|
||
# API 端点
|
||
|
||
@router.post("/login", response_model=LoginResponse)
|
||
async def login(request: LoginRequest, db: Session = Depends(get_db)):
|
||
"""用户登录"""
|
||
user = authenticate_user(db, request.username, request.password)
|
||
|
||
if not user:
|
||
return LoginResponse(success=False, error="用户名或密码错误")
|
||
|
||
# 更新登录时间
|
||
update_last_login(db, user.id)
|
||
|
||
# 生成 Token
|
||
token = create_access_token({
|
||
"user_id": user.id,
|
||
"username": user.username,
|
||
"role": user.role
|
||
})
|
||
|
||
return LoginResponse(
|
||
success=True,
|
||
token=token,
|
||
user=UserInfo(
|
||
id=user.id,
|
||
username=user.username,
|
||
nickname=user.nickname,
|
||
role=user.role
|
||
)
|
||
)
|
||
|
||
|
||
@router.get("/me", response_model=UserInfo)
|
||
async def get_me(user: User = Depends(get_current_user)):
|
||
"""获取当前用户信息"""
|
||
return UserInfo(
|
||
id=user.id,
|
||
username=user.username,
|
||
nickname=user.nickname,
|
||
role=user.role
|
||
)
|
||
|
||
|
||
@router.post("/change-password")
|
||
async def change_password(
|
||
request: ChangePasswordRequest,
|
||
user: User = Depends(get_current_user),
|
||
db: Session = Depends(get_db)
|
||
):
|
||
"""修改密码"""
|
||
from ..services.auth import verify_password
|
||
|
||
if not verify_password(request.old_password, user.password_hash):
|
||
raise HTTPException(status_code=400, detail="原密码错误")
|
||
|
||
new_hash = hash_password(request.new_password)
|
||
db.query(User).filter(User.id == user.id).update({"password_hash": new_hash})
|
||
db.commit()
|
||
|
||
return {"success": True, "message": "密码修改成功"}
|
||
|
||
|
||
@router.get("/users")
|
||
async def list_users(
|
||
user: User = Depends(require_admin),
|
||
db: Session = Depends(get_db)
|
||
):
|
||
"""获取用户列表(仅管理员)"""
|
||
users = db.query(User).all()
|
||
return [
|
||
{
|
||
"id": u.id,
|
||
"username": u.username,
|
||
"nickname": u.nickname,
|
||
"role": u.role,
|
||
"status": u.status,
|
||
"last_login_at": u.last_login_at,
|
||
"created_at": u.created_at
|
||
}
|
||
for u in users
|
||
]
|
||
|
||
|
||
class CreateUserRequest(BaseModel):
|
||
username: str
|
||
password: str
|
||
nickname: Optional[str] = None
|
||
role: str = "viewer"
|
||
|
||
|
||
@router.post("/users")
|
||
async def create_user(
|
||
request: CreateUserRequest,
|
||
user: User = Depends(require_admin),
|
||
db: Session = Depends(get_db)
|
||
):
|
||
"""创建用户(仅管理员)"""
|
||
# 检查用户名是否存在
|
||
exists = db.query(User).filter(User.username == request.username).first()
|
||
if exists:
|
||
raise HTTPException(status_code=400, detail="用户名已存在")
|
||
|
||
new_user = User(
|
||
username=request.username,
|
||
password_hash=hash_password(request.password),
|
||
nickname=request.nickname,
|
||
role=request.role,
|
||
status=1
|
||
)
|
||
db.add(new_user)
|
||
db.commit()
|
||
db.refresh(new_user)
|
||
|
||
return {"success": True, "id": new_user.id}
|
||
|
||
|
||
@router.delete("/users/{user_id}")
|
||
async def delete_user(
|
||
user_id: int,
|
||
user: User = Depends(require_admin),
|
||
db: Session = Depends(get_db)
|
||
):
|
||
"""删除用户(仅管理员)"""
|
||
if user_id == user.id:
|
||
raise HTTPException(status_code=400, detail="不能删除自己")
|
||
|
||
target = db.query(User).filter(User.id == user_id).first()
|
||
if not target:
|
||
raise HTTPException(status_code=404, detail="用户不存在")
|
||
|
||
db.delete(target)
|
||
db.commit()
|
||
|
||
return {"success": True}
|
||
|
||
|
||
# ============ Token 验证 API(供外部应用调用) ============
|
||
|
||
class VerifyTokenRequest(BaseModel):
|
||
"""Token 验证请求"""
|
||
token: str
|
||
app_code: Optional[str] = None # 可选,用于验证 token 是否属于特定应用
|
||
|
||
|
||
class WechatConfig(BaseModel):
|
||
"""企微配置"""
|
||
corp_id: Optional[str] = None
|
||
agent_id: Optional[str] = None
|
||
secret: Optional[str] = None
|
||
|
||
|
||
class VerifyTokenResponse(BaseModel):
|
||
"""Token 验证响应"""
|
||
valid: bool
|
||
tenant_id: Optional[str] = None
|
||
app_code: Optional[str] = None
|
||
wechat_config: Optional[WechatConfig] = None
|
||
error: Optional[str] = None
|
||
|
||
|
||
@router.post("/verify", response_model=VerifyTokenResponse)
|
||
async def verify_token(
|
||
request: VerifyTokenRequest,
|
||
db: Session = Depends(get_db)
|
||
):
|
||
"""
|
||
验证 Token 有效性(供外部应用调用,无需登录)
|
||
|
||
外部应用收到用户请求后,可调用此接口验证 token:
|
||
1. 验证 token 是否存在且有效
|
||
2. 如传入 app_code,验证 token 是否属于该应用
|
||
3. 返回租户信息和企微配置
|
||
|
||
Args:
|
||
token: 访问令牌
|
||
app_code: 应用代码(可选,用于验证 token 是否属于特定应用)
|
||
|
||
Returns:
|
||
valid: 是否有效
|
||
tenant_id: 租户ID
|
||
app_code: 应用代码
|
||
wechat_config: 企微配置(如有)
|
||
"""
|
||
if not request.token:
|
||
return VerifyTokenResponse(valid=False, error="Token 不能为空")
|
||
|
||
# 根据 token 查询租户应用配置
|
||
query = db.query(TenantApp).filter(
|
||
TenantApp.access_token == request.token,
|
||
TenantApp.status == 1
|
||
)
|
||
|
||
# 如果指定了 app_code,验证 token 是否属于该应用
|
||
if request.app_code:
|
||
query = query.filter(TenantApp.app_code == request.app_code)
|
||
|
||
tenant_app = query.first()
|
||
|
||
if not tenant_app:
|
||
return VerifyTokenResponse(valid=False, error="Token 无效或已过期")
|
||
|
||
# 获取关联的企微配置
|
||
wechat_config = None
|
||
if tenant_app.wechat_app_id:
|
||
wechat_app = db.query(TenantWechatApp).filter(
|
||
TenantWechatApp.id == tenant_app.wechat_app_id,
|
||
TenantWechatApp.status == 1
|
||
).first()
|
||
|
||
if wechat_app:
|
||
# 解密 secret
|
||
secret = None
|
||
if wechat_app.secret_encrypted:
|
||
try:
|
||
secret = decrypt_config(wechat_app.secret_encrypted)
|
||
except:
|
||
pass
|
||
|
||
wechat_config = WechatConfig(
|
||
corp_id=wechat_app.corp_id,
|
||
agent_id=wechat_app.agent_id,
|
||
secret=secret
|
||
)
|
||
|
||
return VerifyTokenResponse(
|
||
valid=True,
|
||
tenant_id=tenant_app.tenant_id,
|
||
app_code=tenant_app.app_code,
|
||
wechat_config=wechat_config
|
||
)
|
||
|
||
|
||
@router.get("/verify")
|
||
async def verify_token_get(
|
||
token: str,
|
||
app_code: Optional[str] = None,
|
||
db: Session = Depends(get_db)
|
||
):
|
||
"""
|
||
验证 Token(GET 方式,便于简单测试)
|
||
"""
|
||
return await verify_token(
|
||
VerifyTokenRequest(token=token, app_code=app_code),
|
||
db
|
||
)
|