forked from erp-dev/erp
feat: sse && multi_merchant completed
This commit is contained in:
83
flower/auth.py
Normal file
83
flower/auth.py
Normal file
@@ -0,0 +1,83 @@
|
||||
from ninja.security import HttpBearer
|
||||
from rest_framework_simplejwt.authentication import JWTAuthentication
|
||||
from rest_framework_simplejwt.exceptions import InvalidToken, TokenError
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# ==================== 自定义 JWT 认证 for Django Ninja ====================
|
||||
|
||||
class JWTAuth(HttpBearer):
|
||||
"""
|
||||
Django Ninja 的 JWT 认证类
|
||||
兼容 rest_framework_simplejwt
|
||||
|
||||
与 DRF 使用相同的认证逻辑,验证 JWT Token 并获取用户
|
||||
"""
|
||||
def authenticate(self, request, token):
|
||||
jwt_authenticator = JWTAuthentication()
|
||||
try:
|
||||
# 验证 token 并获取用户
|
||||
validated_token = jwt_authenticator.get_validated_token(token)
|
||||
user = jwt_authenticator.get_user(validated_token)
|
||||
|
||||
# 记录认证成功
|
||||
logger.debug(f"JWT 认证成功: user={user.username}")
|
||||
|
||||
return user
|
||||
except (InvalidToken, TokenError) as e:
|
||||
logger.warning(f"JWT 认证失败: {str(e)}")
|
||||
return None
|
||||
|
||||
|
||||
class JWTAuthWithEmployee(HttpBearer):
|
||||
"""
|
||||
Django Ninja 的 JWT 认证类(要求必须有员工身份)
|
||||
|
||||
与 CustomTokenObtainPairView 的逻辑一致:
|
||||
- 验证 JWT Token
|
||||
- 验证用户必须有关联的员工信息
|
||||
"""
|
||||
|
||||
def __call__(self, request):
|
||||
"""
|
||||
重写 __call__ 方法以添加调试日志
|
||||
这个方法会被 Ninja 调用来执行认证
|
||||
"""
|
||||
logger.info(f"[JWTAuthWithEmployee.__call__] 认证被调用")
|
||||
logger.info(f"[JWTAuthWithEmployee.__call__] Authorization Header: {request.META.get('HTTP_AUTHORIZATION', 'None')[:50]}...")
|
||||
|
||||
# 调用父类的 __call__ 方法
|
||||
result = super().__call__(request)
|
||||
logger.info(f"[JWTAuthWithEmployee.__call__] 认证结果: {result}")
|
||||
return result
|
||||
|
||||
def authenticate(self, request, token):
|
||||
logger.info(f"[JWTAuthWithEmployee.authenticate] 开始认证,token 前 20 字符: {token[:20] if token else 'None'}...")
|
||||
|
||||
if not token:
|
||||
logger.warning(f"[JWTAuthWithEmployee.authenticate] Token 为空")
|
||||
return None
|
||||
|
||||
jwt_authenticator = JWTAuthentication()
|
||||
try:
|
||||
# 验证 token 并获取用户
|
||||
validated_token = jwt_authenticator.get_validated_token(token)
|
||||
user = jwt_authenticator.get_user(validated_token)
|
||||
|
||||
logger.info(f"[JWTAuthWithEmployee.authenticate] Token 验证成功,用户: {user.username}")
|
||||
|
||||
# 检查用户是否有员工身份(与登录逻辑一致)
|
||||
if not hasattr(user, 'employee'):
|
||||
logger.warning(f"[JWTAuthWithEmployee.authenticate] 用户 {user.username} 没有关联的员工信息")
|
||||
return None # 返回 None 会导致 401 Unauthorized
|
||||
|
||||
logger.info(f"[JWTAuthWithEmployee.authenticate] 认证成功: user={user.username}, employee={user.employee.name}")
|
||||
return user
|
||||
|
||||
except (InvalidToken, TokenError) as e:
|
||||
logger.warning(f"[JWTAuthWithEmployee.authenticate] Token 验证失败: {str(e)}")
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"[JWTAuthWithEmployee.authenticate] 认证异常: {str(e)}", exc_info=True)
|
||||
return None
|
||||
@@ -54,10 +54,14 @@ CORS_ALLOW_ALL_ORIGINS = DEBUG # 开发环境允许所有源,生产环境需
|
||||
CORS_ALLOWED_ORIGINS = env.list('CORS_ALLOWED_ORIGINS', default=[
|
||||
'http://localhost:3000',
|
||||
'http://localhost:5173',
|
||||
'http://localhost:5174',
|
||||
'http://127.0.0.1:3000',
|
||||
'http://127.0.0.1:5173',
|
||||
'http://127.0.0.1:5174',
|
||||
])
|
||||
CORS_ALLOW_CREDENTIALS = True # 允许携带凭证(如 Cookie、认证头)
|
||||
|
||||
# SSE 需要的特殊 CORS 配置
|
||||
CORS_ALLOW_HEADERS = [
|
||||
'accept',
|
||||
'accept-encoding',
|
||||
@@ -68,8 +72,22 @@ CORS_ALLOW_HEADERS = [
|
||||
'user-agent',
|
||||
'x-csrftoken',
|
||||
'x-requested-with',
|
||||
'cache-control', # SSE 需要
|
||||
'x-accel-buffering', # SSE 需要
|
||||
'last-event-id', # SSE 重连需要
|
||||
# 注意:不要添加 'connection',这是 hop-by-hop 头部,会被代理过滤
|
||||
]
|
||||
|
||||
# SSE 需要暴露的响应头
|
||||
CORS_EXPOSE_HEADERS = [
|
||||
'content-type',
|
||||
'cache-control',
|
||||
'x-accel-buffering',
|
||||
]
|
||||
|
||||
# 预检请求缓存时间(秒)
|
||||
CORS_PREFLIGHT_MAX_AGE = 86400 # 24小时
|
||||
|
||||
|
||||
# Application definition
|
||||
|
||||
@@ -88,6 +106,8 @@ INSTALLED_APPS = [
|
||||
'stock',
|
||||
'api_v1',
|
||||
'api_man',
|
||||
'stateflow',
|
||||
'sse',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
|
||||
@@ -16,10 +16,9 @@ Including another URLconf
|
||||
"""
|
||||
from django.contrib import admin
|
||||
from django.urls import path, include
|
||||
from stock.views import router
|
||||
from rest_framework.response import Response
|
||||
from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView
|
||||
from sse.views import create_sse_event, push_sse_event
|
||||
from sse.views import create_sse_event, push_test_event, get_sse_status, shutdown_sse
|
||||
from rest_framework_simplejwt.views import (
|
||||
TokenObtainPairView,
|
||||
# TokenRefreshView,
|
||||
@@ -55,9 +54,12 @@ urlpatterns = [
|
||||
path('api/docs/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'),
|
||||
|
||||
path('admin/', admin.site.urls),
|
||||
path('stock/', router.urls),
|
||||
path('api/v1/', include('api_v1.urls')),
|
||||
path('api/backend/', include('api_man.urls')),
|
||||
|
||||
# sse 相关端点
|
||||
path('sse/', create_sse_event, name='sse_event'),
|
||||
path('sse/push/', push_sse_event, name='push_sse_event'),
|
||||
path('sse/push/', push_test_event, name='push_sse_event'),
|
||||
path('sse/status/', get_sse_status, name='sse_status'),
|
||||
path('sse/shutdown/', shutdown_sse, name='shutdown_sse'),
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user