litefs.auth.oauth2 源代码

#!/usr/bin/env python
# coding: utf-8

"""
OAuth2 认证管理

提供 OAuth2 认证的完整实现,支持多种 OAuth2 提供商。
"""

import time
import urllib.parse
from typing import Any, Callable, Dict, Optional

from .providers import (  # noqa: F401  # Provider 类为对外再导出,兼容历史导入路径
    OAuth2Provider,
    OAuth2UserInfo,
    GitHubProvider,
    GoogleProvider,
    WeChatProvider,
    WeComProvider,
    get_provider,
)


[文档] class OAuth2State: """OAuth2 状态管理""" def __init__(self, ttl: int = 600): self._states: Dict[str, Dict[str, Any]] = {} self._ttl = ttl
[文档] def create( self, provider: str, redirect_uri: str = None, **kwargs ) -> str: """ 创建状态 Args: provider: 提供商名称 redirect_uri: 回调地址 **kwargs: 额外数据 Returns: 状态字符串 """ import secrets state = secrets.token_urlsafe(32) self._states[state] = { 'provider': provider, 'redirect_uri': redirect_uri, 'created_at': time.time(), **kwargs } return state
[文档] def verify(self, state: str) -> Optional[Dict[str, Any]]: """ 验证状态 Args: state: 状态字符串 Returns: 状态数据或 None """ data = self._states.pop(state, None) if not data: return None if time.time() - data['created_at'] > self._ttl: return None return data
[文档] def cleanup(self): """清理过期状态""" now = time.time() expired = [ state for state, data in self._states.items() if now - data['created_at'] > self._ttl ] for state in expired: del self._states[state]
[文档] class OAuth2: """ OAuth2 认证管理器 提供完整的 OAuth2 认证流程管理。 使用示例:: oauth = OAuth2(app) oauth.register( name='github', client_id='your-client-id', client_secret='your-client-secret', redirect_uri='http://localhost:8080/auth/github/callback' ) @route('/login/github') def github_login(request): return oauth.authorize_redirect(request, 'github') @route('/auth/github/callback') def github_callback(request): user_info = oauth.authorize_user(request, 'github') if user_info: return {'user': user_info.to_dict()} return {'error': '认证失败'} """ def __init__( self, app=None, state_ttl: int = 600, user_mapper: Callable[[OAuth2UserInfo], Any] = None, ): """ 初始化 OAuth2 管理器 Args: app: Litefs 应用实例 state_ttl: 状态有效期(秒) user_mapper: 用户映射函数 """ self._providers: Dict[str, OAuth2Provider] = {} self._state = OAuth2State(ttl=state_ttl) self._user_mapper = user_mapper self._app = app if app: self.init_app(app)
[文档] def init_app(self, app): """ 初始化应用 Args: app: Litefs 应用实例 """ self._app = app
[文档] def register( self, name: str, client_id: str, client_secret: str, redirect_uri: str = '', scope: str = None, provider_class: type = None, **kwargs ): """ 注册 OAuth2 提供商 Args: name: 提供商名称 client_id: 客户端 ID client_secret: 客户端密钥 redirect_uri: 回调地址 scope: 权限范围 provider_class: 提供商类 **kwargs: 额外参数 """ if provider_class: provider_cls = provider_class else: provider_cls = get_provider(name) if not provider_cls: raise ValueError(f"Unknown provider: {name}") provider = provider_cls( client_id=client_id, client_secret=client_secret, redirect_uri=redirect_uri, scope=scope, **kwargs ) self._providers[name] = provider
[文档] def get_provider(self, name: str) -> Optional[OAuth2Provider]: """ 获取提供商 Args: name: 提供商名称 Returns: OAuth2Provider 实例 """ return self._providers.get(name)
[文档] def authorize_redirect( self, request, provider_name: str, callback_url: str = None, **kwargs ): """ 生成授权重定向响应 Args: request: 请求对象 provider_name: 提供商名称 callback_url: 回调地址 **kwargs: 额外参数 Returns: 重定向响应 """ from litefs.handlers import Response provider = self._providers.get(provider_name) if not provider: return Response.json({'error': f'Unknown provider: {provider_name}'}, status_code=400) redirect_uri = callback_url or provider.redirect_uri state = self._state.create( provider=provider_name, redirect_uri=redirect_uri, **kwargs ) authorize_url = provider.get_authorize_url( state=state, redirect_uri=redirect_uri ) return Response.redirect(authorize_url)
[文档] def authorize_user( self, request, provider_name: str = None, **kwargs ) -> Optional[OAuth2UserInfo]: """ 完成授权并获取用户信息 Args: request: 请求对象 provider_name: 提供商名称(可选,从状态中获取) **kwargs: 额外参数 Returns: 用户信息或 None """ code = self._get_code(request) state = self._get_state(request) if not code: return None state_data = self._state.verify(state) if not state_data: return None provider_name = provider_name or state_data.get('provider') provider = self._providers.get(provider_name) if not provider: return None token_data = provider.get_access_token( code=code, redirect_uri=state_data.get('redirect_uri') ) if 'error' in token_data: return None access_token = token_data.get('access_token') if not access_token: return None if provider_name == 'wechat': openid = token_data.get('openid') user_info = provider.get_user_info(access_token, openid=openid) elif provider_name == 'wecom': user_info = provider.get_user_info(access_token, code=code) else: user_info = provider.get_user_info(access_token) if user_info and self._user_mapper: return self._user_mapper(user_info) return user_info
[文档] async def authorize_user_async( self, request, provider_name: str = None, **kwargs ) -> Optional[OAuth2UserInfo]: """ 异步完成授权并获取用户信息 使用 aiohttp 进行异步 HTTP 请求,不会阻塞请求处理线程。 如果未安装 aiohttp,回退到同步方式。 Args: request: 请求对象 provider_name: 提供商名称(可选,从状态中获取) **kwargs: 额外参数 Returns: 用户信息或 None """ code = self._get_code(request) state = self._get_state(request) if not code: return None state_data = self._state.verify(state) if not state_data: return None provider_name = provider_name or state_data.get('provider') provider = self._providers.get(provider_name) if not provider: return None token_data = await provider._async_request( provider.access_token_url, method='POST', data=self._build_token_request_data(provider, code, state_data.get('redirect_uri')), headers={'Accept': 'application/json'} if provider_name == 'github' else None, ) if 'error' in token_data: return None access_token = token_data.get('access_token') if not access_token: return None if provider_name == 'wechat': openid = token_data.get('openid') user_info = await self._get_user_info_async( provider, access_token, provider_name, openid=openid, code=code ) elif provider_name == 'wecom': user_info = await self._get_user_info_async( provider, access_token, provider_name, code=code ) else: user_info = await self._get_user_info_async(provider, access_token, provider_name) if user_info and self._user_mapper: return self._user_mapper(user_info) return user_info
def _build_token_request_data(self, provider, code: str, redirect_uri: str = None) -> Dict: """构建获取 access token 的请求数据""" if provider.name == 'wechat': return { 'appid': provider.client_id, 'secret': provider.client_secret, 'code': code, 'grant_type': 'authorization_code', } elif provider.name == 'wecom': return { 'corpid': provider.client_id, 'corpsecret': provider.client_secret, } else: data = { 'client_id': provider.client_id, 'client_secret': provider.client_secret, 'code': code, 'redirect_uri': redirect_uri or provider.redirect_uri, } if provider.name == 'google': data['grant_type'] = 'authorization_code' return data async def _get_user_info_async( self, provider, access_token: str, provider_name: str, openid: str = None, code: str = None, ) -> Optional[OAuth2UserInfo]: """异步获取用户信息""" if provider_name == 'wechat' and openid: params = {'access_token': access_token, 'openid': openid} url = f"{provider.userinfo_url}?{urllib.parse.urlencode(params)}" user_data = await provider._async_request(url) if 'errcode' in user_data: return None return OAuth2UserInfo( provider=provider.name, provider_user_id=str(user_data.get('unionid') or user_data.get('openid', '')), username=user_data.get('nickname', ''), name=user_data.get('nickname'), avatar_url=user_data.get('headimgurl'), raw_data=user_data, ) if provider_name == 'wecom' and code: params = {'access_token': access_token, 'code': code} url = f"{provider.userinfo_url}?{urllib.parse.urlencode(params)}" user_data = await provider._async_request(url) if 'errcode' in user_data and user_data['errcode'] != 0: return None return OAuth2UserInfo( provider=provider.name, provider_user_id=str(user_data.get('UserId', '')), username=user_data.get('UserId', ''), name=user_data.get('name'), raw_data=user_data, ) # GitHub / Google headers = {'Authorization': f'Bearer {access_token}'} if provider_name == 'github': headers['Accept'] = 'application/vnd.github.v3+json' user_data = await provider._async_request(provider.userinfo_url, headers=headers) if 'error' in user_data: return None if provider_name == 'github': emails = await provider._async_request( 'https://api.github.com/user/emails', headers=headers ) primary_email = None if isinstance(emails, list): for email_info in emails: if email_info.get('primary') and email_info.get('verified'): primary_email = email_info.get('email') break return OAuth2UserInfo( provider=provider.name, provider_user_id=str(user_data.get('id', '')), username=user_data.get('login', ''), email=primary_email or user_data.get('email'), name=user_data.get('name') or user_data.get('login'), avatar_url=user_data.get('avatar_url'), raw_data=user_data, ) # Google return OAuth2UserInfo( provider=provider.name, provider_user_id=str(user_data.get('sub', '')), username=user_data.get('email', '').split('@')[0], email=user_data.get('email'), name=user_data.get('name'), avatar_url=user_data.get('picture'), raw_data=user_data, ) def _get_code(self, request) -> Optional[str]: """从请求中获取授权码""" if hasattr(request, 'query'): return request.query.get('code') if hasattr(request, 'query_string'): from urllib.parse import parse_qs params = parse_qs(request.query_string) codes = params.get('code', []) return codes[0] if codes else None return None def _get_state(self, request) -> Optional[str]: """从请求中获取状态""" if hasattr(request, 'query'): return request.query.get('state') if hasattr(request, 'query_string'): from urllib.parse import parse_qs params = parse_qs(request.query_string) states = params.get('state', []) return states[0] if states else None return None
[文档] def get_authorize_url( self, provider_name: str, callback_url: str = None, state: str = None, ) -> str: """ 获取授权 URL Args: provider_name: 提供商名称 callback_url: 回调地址 state: 状态参数 Returns: 授权 URL """ provider = self._providers.get(provider_name) if not provider: raise ValueError(f"Unknown provider: {provider_name}") redirect_uri = callback_url or provider.redirect_uri if not state: state = self._state.create( provider=provider_name, redirect_uri=redirect_uri ) return provider.get_authorize_url( state=state, redirect_uri=redirect_uri )
[文档] def set_user_mapper(self, mapper: Callable[[OAuth2UserInfo], Any]): """ 设置用户映射函数 Args: mapper: 映射函数,接收 OAuth2UserInfo 返回用户对象 """ self._user_mapper = mapper
[文档] def create_oauth2_blueprint(oauth: OAuth2, prefix: str = '/auth'): """ 创建 OAuth2 蓝图 Args: oauth: OAuth2 管理器 prefix: URL 前缀 Returns: 路由列表 """ from litefs.routing import get routes = [] for name, provider in oauth._providers.items(): def make_login_handler(provider_name): def login_handler(request): return oauth.authorize_redirect(request, provider_name) return login_handler def make_callback_handler(provider_name): def callback_handler(request): user_info = oauth.authorize_user(request, provider_name) if user_info: return {'success': True, 'user': user_info.to_dict()} return {'success': False, 'error': '认证失败'} return callback_handler login_route = get(f'{prefix}/{name}')(make_login_handler(name)) callback_route = get(f'{prefix}/{name}/callback')(make_callback_handler(name)) routes.extend([login_route, callback_route]) return routes