Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 101 additions & 0 deletions apps/chat/serializers/chat_user_serializer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import json

from django.core.cache import cache
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers

from application.models import ApplicationAccessToken
from common.auth.common import ChatToken, FileToken
from common.auth.constants.operate_constants import Operate
from common.constants.authentication_type import AuthenticationType
from common.constants.cache_version import Cache_Version
from common.exception.app_exception import AppApiException
from common.utils.common import password_encrypt
from common.utils.common import password_verify, needs_password_upgrade
from common.utils.rsa_util import decrypt
from system_manage.models import ChatUser
from users.serializers.login import LoginRequest

system_version, system_get_key = Cache_Version.SYSTEM.value


class ChatUserAccessTokenV3Serializer(serializers.Serializer):

@staticmethod
def create_token_and_cache(user, request):
_type = AuthenticationType.CHAT_USER
token = ChatToken(str(user.id),_type, str(Operate.LOCAL)).to_token()
return token, FileToken(str(user.id),_type).to_token()

@staticmethod
def get_auth_setting():
application_access_token = ApplicationAccessToken.objects.filter(
is_active=True
).first()

if not application_access_token:
raise AppApiException(1005, _('Invalid access token'))

return application_access_token.authentication_value

@staticmethod
def local_login(instance):
username = instance.get("username", "")
encryptedData = instance.get("encryptedData", "")
if encryptedData:
json_data = json.loads(decrypt(encryptedData))
instance.update(json_data)
try:
LoginRequest(data=instance).is_valid(raise_exception=True)
except Exception as e:
raise e
auth_setting = ChatUserAccessTokenV3Serializer.get_auth_setting()

max_attempts = auth_setting.get("max_attempts", 1)
password = instance.get("password")
captcha = instance.get("captcha", "")

# 判断是否需要验证码
need_captcha = True
if max_attempts == -1:
need_captcha = False
elif max_attempts > 0:
fail_count = cache.get(system_get_key(f'chat_{username}'), version=system_version) or 0
need_captcha = fail_count >= max_attempts

if need_captcha:
if not captcha:
raise AppApiException(1005, _("Captcha is required"))

captcha_cache = cache.get(
Cache_Version.CAPTCHA.get_key(captcha=f"chat_{username}"),
version=Cache_Version.CAPTCHA.get_version()
)
if captcha_cache is None or captcha.lower() != captcha_cache:
raise AppApiException(1005, _("Captcha code error or expiration"))

user = ChatUser.objects.filter(username=username).first()

if not user or not password_verify(password, user.password):
record_login_fail(username)
raise AppApiException(500, _('The username or password is incorrect'))

if needs_password_upgrade(user.password):
user.password = password_encrypt(password)
user.save(update_fields=['password'])
if not user.is_active:
raise AppApiException(1005, _("The user has been disabled, please contact the administrator!"))
cache.delete(system_get_key(f'chat_{username}'), version=system_version)
return user


def record_login_fail(username: str, expire: int = 600):
"""记录登录失败次数"""
if not username:
return
fail_key = system_get_key(f'chat_{username}')
fail_count = cache.get(fail_key, version=system_version)
if fail_count is None:
cache.set(fail_key, 1, timeout=expire, version=system_version)
else:
cache.incr(fail_key, 1, version=system_version)
2 changes: 1 addition & 1 deletion apps/chat/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
path('embed', v3_views.ChatEmbedView.as_view()),
path('mcp', v3_views.mcp_view),
path('auth/anonymous', v3_views.AnonymousAuthentication.as_view()),
path('auth/login/<str:access_token>', v3_views.LocalLoginView.as_view()),
path('auth/login', v3_views.LocalLoginView.as_view()),
path('auth/logout', v3_views.Logout.as_view(), name='v3_logout'),
path('profile', v3_views.AuthProfile.as_view()),
path('captcha', v3_views.CaptchaView.as_view(), name='v3_captcha'),
Expand Down
22 changes: 11 additions & 11 deletions apps/chat/views/v3/chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
from models_provider.api.model import DefaultModelResponse
from oss.serializers.file import FileSerializer
from system_manage.serializers.chat_user import RePasswordSerializer, ChatUserProfileSerializer
from system_manage.serializers.chat_user_serializer import ChatUserAccessTokenSerializer
from chat.serializers.chat_user_serializer import ChatUserAccessTokenV3Serializer
from users.api import CaptchaAPI, LoginAPI
from users.api.user import ResetPasswordAPI, UserProfileAPI
from users.serializers.login import CaptchaSerializer
Expand Down Expand Up @@ -135,7 +135,7 @@ def post(self, request: Request):
key="mk_file_auth",
value=f_token,
max_age=7 * 24 * 3600,
path=f'{CONFIG.get_chat_path()}/{request.data.get("access_token")}',
path=CONFIG.get_chat_path(),
secure=is_https,
httponly=True,
samesite="None" if is_https else "Lax",
Expand Down Expand Up @@ -263,8 +263,8 @@ class CaptchaView(APIView):
responses=CaptchaAPI.get_response())
def get(self, request: Request):
username = request.query_params.get('username', None)
accessToken = request.query_params.get('accessToken', None)
return result.success(CaptchaSerializer().chat_generate(username, 'chat', accessToken))
application_id = request.query_params.get('application_id', None)
return result.success(CaptchaSerializer().chat_generate(username, 'chat', application_id))


class SpeechToText(APIView):
Expand Down Expand Up @@ -383,13 +383,13 @@ class ChatUserProfileView(APIView):
responses=UserProfileAPI.get_response(),
)
def get(self, request: Request):
return result.success(ChatUserProfileSerializer().profile(request.user))
return result.success(ChatUserProfileSerializer().profile(request.user.profile))


class BaseAuthView(APIView):
@staticmethod
def create_token_and_cache(access_token, user, request):
token = ChatUserAccessTokenSerializer.create_token_and_cache(access_token, user, request)
def create_token_and_cache(user, request):
token, f_token = ChatUserAccessTokenV3Serializer.create_token_and_cache(user, request)
version, get_key = Cache_Version.CHAT_USER_TOKEN.value
cache.set(get_key(token), user, timeout=60 * 60 * 2, version=version)
return token, FileToken(str(user.id), AuthenticationType.CHAT_USER.value).to_token()
Expand Down Expand Up @@ -420,12 +420,12 @@ class LocalLoginView(BaseAuthView):
request=LoginAPI.get_request(),
responses=LoginAPI.get_response(),
)
def post(self, request: Request, access_token: str = None):
user = ChatUserAccessTokenSerializer.local_login(request.data, access_token)
def post(self, request: Request):
user = ChatUserAccessTokenV3Serializer.local_login(request.data)
user.source = "LOCAL"
token, f_token = self.create_token_and_cache(access_token, user, request)
token, f_token = self.create_token_and_cache(user, request)
response = result.success({'token': token})
return self.generate(request, f_token, response, path=f'/chat/{access_token}/')
return self.generate(request, f_token, response, path=f'/chat/')


class Logout(APIView):
Expand Down
12 changes: 7 additions & 5 deletions apps/common/auth/handle/impl/chat_user_token.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from common.constants.authentication_type import AuthenticationType
from common.exception.app_exception import AppUnauthorizedFailed
from system_manage.models import ResourceChatUserGroupAuthorize, ResourceType, ResourceChatUserAuthorize, \
UserGroupRelation
UserGroupRelation, ChatUser

login_type_list = [Operate.LOCAL.value, Operate.CAS.value, Operate.DINGTALK.value, Operate.WECOM.value,
Operate.LARK.value, Operate.OIDC.value, Operate.LDAP.value,
Expand All @@ -41,6 +41,7 @@ def handle(self, request, token: str, get_token_details):
)
_type = ChatUserType.ANONYMOUS_USER
login_type = auth_details.get('login_type')
user_id = auth_details.get('user_id')
application_id = (auth_details.get('kwargs') or {}).get('application_id')
if login_type.upper() == str(Operate.ANNOTATION_AUTH):
application_access_token_list = application_access_token_list.filter(authentication=False)
Expand All @@ -50,7 +51,6 @@ def handle(self, request, token: str, get_token_details):

elif login_type_list.__contains__(login_type.upper()):
_type = ChatUserType.CHAT_USER
user_id = auth_details.get('id')
user_group_ids = QuerySet(UserGroupRelation).filter(
user_id=user_id,
).values_list('group_id', flat=True)
Expand Down Expand Up @@ -92,11 +92,13 @@ def handle(self, request, token: str, get_token_details):
permission_list.append(ChatPermissionConstants.CHAT_USER_ANONYMOUS.value)
k = f"{Group.CHAT_USER}:r:{application_access_token.application_id}"
permissions[k] = reduce(lambda x, y: x | y, [p.bit() for p in permission_list], 0)
chat_user = QuerySet(ChatUser).filter(id=user_id).first()
if application_id:
# 指定了 application_id(v2 流程)时,直接校验该应用是否有权限,无权限直接抛错,
# 避免返回一个空权限的 Principal 造成静默失败。
if not permissions.get(f"{Group.CHAT_USER}:r:{application_id}"):
raise AppUnauthorizedFailed(403, _('No permission to access'))
return Principal(auth_details.get('user_id'), _type, application_id=application_id), Auth(set(),
permissions)
return Principal(auth_details.get('user_id'), _type), Auth(set(), permissions)
return Principal(auth_details.get('user_id'), _type, application_id=application_id,
profile=chat_user), Auth(set(),
permissions)
return Principal(auth_details.get('user_id'), _type, profile=chat_user), Auth(set(), permissions)
Loading