Browse Source

feat: 接入第三方app token认证

zhangwl 2 months ago
parent
commit
3d43539dbd
4 changed files with 97 additions and 26 deletions
  1. 7 18
      app/dependencies/auth.py
  2. 0 1
      app/routers/ai_config.py
  3. 3 5
      app/routers/chat.py
  4. 87 2
      app/routers/users.py

+ 7 - 18
app/dependencies/auth.py

@@ -18,36 +18,25 @@ class UserContext(BaseModel):
 
 async def resolve_user_id(
     jwt_token: Annotated[Optional[str], Depends(oauth2_scheme_optional)] = None,
-    source: Optional[str] = Query(default=None),
-    token: Optional[str] = Query(default=None),
 ) -> UserContext:
-    if source == "app" and token:
-        app_user = get_app_user(token)
-        if not app_user:
-            raise HTTPException(status_code=401, detail="无效的 App token")
-
-        user_id = app_user.get("userId")
-        app_name = app_user.get("appName")
-        if not user_id or not app_name:
-            raise HTTPException(status_code=401, detail="无效的 App token")
-
-        return UserContext(
-            user_id=user_id,
-            app_name=app_name,
-        )
-
     if not jwt_token:
         raise HTTPException(status_code=401, detail="未提供认证令牌")
 
     try:
         payload = jwt.decode(jwt_token, config.SECRET_KEY, algorithms=[config.ALGORITHM])
+
+        appName=payload.get("appName")
+
         user_id = payload.get("userId")
         if not user_id:
             raise HTTPException(status_code=401, detail="无效的令牌")
 
+        if not appName:
+            appName = "com.yunxiangshengtai"
+
         return UserContext(
             user_id=user_id,
-            app_name="com.yunxingshnegtai",
+            app_name=appName,
         )
     except jwt.PyJWTError:
         raise HTTPException(status_code=401, detail="无效的令牌")

+ 0 - 1
app/routers/ai_config.py

@@ -9,7 +9,6 @@ from ..db.ai_config import (
     delete_config,
     get_all_configs
 )
-from ..dependencies.auth import resolve_user_id
 
 router = APIRouter()
 

+ 3 - 5
app/routers/chat.py

@@ -45,12 +45,10 @@ async def generate_stream_response(request: ChatRequest, user_context: UserConte
         )
 
         system_prompt = f"""
-           你是字节跳动公司开发的智能助手。你的核心行为准则如下:
+           你是云悦公司开发的智能助手。你的核心行为准则如下:
 
             ## 一、身份与基本行为规范
-            1. 输出保持和豆包 APP 日常对话一致;
-            2. 在回复用户时,不要自称“豆包”;
-            3. 你具备以下能力:
+             你具备以下能力:
                - 可接收和读取各类文档(PDF、Excel、PPT、Word 等),并执行总结、分析、翻译、润色等任务;
                - 可读取图片/照片、网址、抖音链接的内容;
                - 可根据用户提供的文本描述生成或绘制图片;
@@ -312,7 +310,7 @@ async def chat(
     try:
         if request.stream:
             return StreamingResponse(
-                generate_stream_response(request, UserContext(user_id=user_id, app_name=app_name)),
+                generate_stream_response(request, user_context),
                 media_type="text/plain",
                 headers={
                     "Cache-Control": "no-cache",

+ 87 - 2
app/routers/users.py

@@ -7,7 +7,7 @@ from fastapi.security import OAuth2PasswordBearer
 from pydantic import BaseModel
 from pwdlib import PasswordHash
 from ..config.config import Config
-from ..db.redis_client import redis_client
+from ..db.redis_client import redis_client, get_app_user
 
 config = Config()
 # =====================================================
@@ -45,6 +45,14 @@ class LoginRequest(BaseModel):
     appName: Optional[str] = "com.yunxiangshengtai"
 
 
+class ThirdPartyLoginRequest(BaseModel):
+    """
+    第三方免登输入的模型
+    """
+    token: str
+    source: str
+
+
 class Token(BaseModel):
     """
     访问令牌响应模型
@@ -266,6 +274,22 @@ async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]) -> Use
         if username is None:
             raise credentials_exception
 
+        # 检查是否为第三方用户(JWT中有source字段)
+        source: str = payload.get("source")
+        if source:
+            # 第三方用户,直接从JWT构建User对象
+            user_id: str = payload.get("userId")
+            if not user_id:
+                raise credentials_exception
+            return UserInDB(
+                userId=user_id,
+                username=username,
+                email=None,
+                full_name=None,
+                disabled=False,
+                hashed_password=""
+            )
+
         # 创建令牌数据对象
         token_data = TokenData(username=username)
 
@@ -273,7 +297,7 @@ async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]) -> Use
         # JWT解码失败(令牌无效、过期等)
         raise credentials_exception
 
-    # 从数据库中获取用户信息
+    # 从数据库中获取用户信息(本地用户)
     user = get_user(fake_users_db, username=token_data.username)
     if user is None:
         raise credentials_exception
@@ -363,6 +387,67 @@ async def login_for_access_token(
     }
 
 
+@router.post("/token/third-party", response_model=Token, summary="第三方免登", description="使用第三方token进行免登")
+async def login_with_third_party(
+        login_data: ThirdPartyLoginRequest
+) -> Token:
+    """
+    第三方免登端点
+
+    接受第三方token和source,验证token有效性后返回JWT访问令牌
+
+    Args:
+        login_data: 包含第三方token和source的json数据
+
+    Returns:
+        Token: 包含访问令牌和令牌类型的对象
+
+    Raises:
+        HTTPException: 如果token无效或source不是"app"
+    """
+    # 验证source是否为"app"
+    if login_data.source != "app":
+        raise HTTPException(
+            status_code=status.HTTP_400_BAD_REQUEST,
+            detail="Invalid source"
+        )
+
+    # 调用第三方接口验证token
+    user_info = get_app_user(login_data.token)
+    if not user_info:
+        raise HTTPException(
+            status_code=status.HTTP_401_UNAUTHORIZED,
+            detail="Invalid token",
+            headers={"WWW-Authenticate": "Bearer"},
+        )
+
+    # 创建访问令牌和刷新令牌
+    access_token = create_access_token(
+        data={"sub": user_info["nickName"], "userId": user_info["userId"], "source": login_data.source},
+        expires_delta=timedelta(minutes=config.ACCESS_TOKEN_EXPIRE_MINUTES),
+        token_type="access"
+    )
+    refresh_token = create_access_token(
+        data={"sub": user_info["nickName"], "userId": user_info["userId"], "source": login_data.source},
+        expires_delta=timedelta(days=7),
+        token_type="refresh"
+    )
+
+    # 存储刷新令牌哈希到Redis
+    refresh_hash = hashlib.sha256(refresh_token.encode()).hexdigest()
+    redis_client.setex(f"refresh:{user_info['userId']}", 7*24*60*60, refresh_hash)
+
+    # 返回令牌和令牌类型
+    return {
+        "message": "登录成功",
+        "access_token": access_token,
+        "refresh_token": refresh_token,
+        "token_type": "bearer",
+        "username": user_info["nickName"],
+        "appName": user_info["appName"]
+    }
+
+
 @router.post("/register", response_model=User, summary="用户注册", description="创建新用户账户")
 async def register_user(user: UserCreate) -> User:
     """