Files
track-LICA/backend/app/services/auth_service.py
duxingchen 3286a11bc7 chore: fork from IRIS track 供 LICA 部门独立运行
- 复制来源: /home/yueli/track @ 192c8ee (feature/ai-audit-update)
- 组织隔离目标: LICA
- 端口规划: 前端 8030 / 后端 8031 / 数据库 8032
- 已排除 deploy.sh、deploy_full.sh、docker-compose.prod.yml(IRIS 生产发布脚本)
- 已排除工作区未提交改动,取干净的 192c8ee 状态
2026-09-21 15:56:52 +08:00

150 lines
5.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""认证服务 — 对接 MOM 系统 sys_user 表 + Track 自有 JWT双 Token 架构)"""
from fastapi import HTTPException, status, Depends, Request
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from jose import JWTError, jwt
from werkzeug.security import check_password_hash
from app.core.config import settings
from app.core.security import (
create_access_token,
create_refresh_token,
decode_token,
ALGORITHM,
TOKEN_TYPE_ACCESS,
TOKEN_TYPE_REFRESH,
)
from app.core.mom_database import MomSessionLocal
from app.core.logging import user_var
from app.schemas.user import LoginResponse, UserResponse
security = HTTPBearer()
def login(username: str, password: str) -> LoginResponse:
"""登录 — 签发双 TokenAccess + Refresh"""
db = MomSessionLocal()
try:
# 1. 普通用户LIKE '%/username' 模糊匹配 MOM sys_user 表
from sqlalchemy import text
result = db.execute(
text(
"SELECT id, username, department, role, password_hash "
"FROM sys_user "
"WHERE username LIKE :pattern"
),
{"pattern": f"%/{username}"},
)
row = result.fetchone()
if not row:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="用户名或密码错误",
)
user_id, full_username, department, role, password_hash = row
# 2. Werkzeug scrypt 密码验证
if not check_password_hash(password_hash, password):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="用户名或密码错误",
)
# 3. 解析 display_name"张三/zhangsan01" → "张三"
display_name = full_username.split("/")[0] if "/" in full_username else full_username
token_data = {
"sub": str(user_id),
"role": role or "operator",
"username": username,
"display_name": display_name,
}
return LoginResponse(
access_token=create_access_token(data=token_data),
refresh_token=create_refresh_token(data=token_data),
user=UserResponse(
id=str(user_id),
username=username,
display_name=display_name,
role=role or "operator",
),
)
finally:
db.close()
def refresh_access_token(refresh_token: str) -> dict:
"""
使用 Refresh Token 换取新的 Access Token。
校验:
1. Token 签名是否有效
2. Token type 是否为 "refresh"
3. Token 是否未过期
"""
try:
payload = decode_token(refresh_token)
except JWTError:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Refresh Token 无效或已过期,请重新登录",
)
# 校验 token 类型
if payload.get("type") != TOKEN_TYPE_REFRESH:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="无效的 Token 类型,仅接受 Refresh Token",
)
# 提取用户信息,签发新的 Access Token
access_token = create_access_token(
data={
"sub": payload.get("sub"),
"role": payload.get("role", "operator"),
"username": payload.get("username", ""),
"display_name": payload.get("display_name", ""),
}
)
return {"access_token": access_token, "token_type": "bearer"}
async def get_current_user(
request: Request,
credentials: HTTPAuthorizationCredentials = Depends(security),
) -> dict:
"""从 Bearer Token 解析当前用户(仅接受 Access Token"""
token = credentials.credentials
try:
payload = decode_token(token)
user_id = payload.get("sub")
if not user_id:
raise HTTPException(status_code=401, detail="无效的 Token")
# 校验:仅接受 access token
if payload.get("type") == TOKEN_TYPE_REFRESH:
raise HTTPException(
status_code=401,
detail="请使用 Access Token 访问 APIRefresh Token 仅用于刷新",
)
# 操作人身份要写两处,用途不同,缺一不可:
# 1) contextvar —— 供本请求任务内的业务/service 日志使用;
# 2) request.state —— 中间件在独立 task 中执行Starlette 的
# BaseHTTPMiddleware 用 anyio start_soon 起新 task而 asyncio
# 每个 Task 会复制 context因此中间件读不到路由内改的
# contextvar只能通过 ASGI scope 承载的 state 拿到。
# username 即 assignee_id 口径,比数字 id 直观得多。
user_label = payload.get("username") or user_id
user_var.set(user_label)
request.state.audit_user = user_label
request.state.audit_display_name = payload.get("display_name") or ""
request.state.audit_role = payload.get("role") or ""
return payload
except JWTError:
raise HTTPException(status_code=401, detail="无效的 Token")