From bc28ef00f138ce5e966f0609e602300c3b6e83ba Mon Sep 17 00:00:00 2001 From: repair-agent Date: Mon, 9 Feb 2026 18:24:58 +0800 Subject: [PATCH] add phone login --- apps/users/serializers.py | 14 ++----- apps/users/views.py | 22 +++++++---- config/settings.py | 6 +++ requirements.txt | 1 + utils/phone_auth.py | 82 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 107 insertions(+), 18 deletions(-) create mode 100644 utils/phone_auth.py diff --git a/apps/users/serializers.py b/apps/users/serializers.py index 548c92d..8b7daf4 100644 --- a/apps/users/serializers.py +++ b/apps/users/serializers.py @@ -23,17 +23,9 @@ class UserDetailSerializer(serializers.ModelSerializer): class PhoneLoginSerializer(serializers.Serializer): - """手机号一键登录序列化器""" - - phone = serializers.CharField(max_length=20, help_text='手机号') - # 实际项目中应该有验证码或token - # code = serializers.CharField(max_length=10, help_text='验证码') - - def validate_phone(self, value): - # 简单验证手机号格式 - if not value.isdigit() or len(value) != 11: - raise serializers.ValidationError('手机号格式不正确') - return value + """手机号一键登录序列化器(阿里云号码认证)""" + + token = serializers.CharField(help_text='阿里云号码认证 SDK 返回的 accessToken') class UpdateUserSerializer(serializers.ModelSerializer): diff --git a/apps/users/views.py b/apps/users/views.py index 7c3b0fa..05275fc 100644 --- a/apps/users/views.py +++ b/apps/users/views.py @@ -12,6 +12,7 @@ from django.utils import timezone from utils.response import success, error from utils.exceptions import ErrorCode from utils.sms import send_sms_code, verify_sms_code +from utils.phone_auth import get_phone_auth_client from utils.oss import get_oss_client from apps.admins.authentication import AppJWTAuthentication, AdminJWTAuthentication from apps.admins.permissions import IsAdminUser @@ -51,27 +52,34 @@ class AuthViewSet(viewsets.ViewSet): @action(detail=False, methods=['post'], url_path='phone-login') def phone_login(self, request): """ - 手机号一键登录 + 手机号一键登录(阿里云号码认证) POST /api/v1/auth/phone-login + App 端通过阿里云 SDK 获取 token,后端用 token 换取真实手机号 """ serializer = PhoneLoginSerializer(data=request.data) if not serializer.is_valid(): return error(message=str(serializer.errors)) - - phone = serializer.validated_data['phone'] - + + access_token = serializer.validated_data['token'] + + # 用 token 换取真实手机号 + client = get_phone_auth_client() + phone, err = client.get_mobile(access_token) + if not phone: + return error(code=102, message=err or '号码认证失败') + # 获取或创建用户 user, created = User.objects.get_or_create( phone=phone, defaults={'nickname': f'用户{phone[-4:]}'} ) - + if not user.is_active: return error(code=101, message='账号已被禁用') - + # 生成JWT Token(带user_type='app'标识) tokens = get_app_tokens(user) - + return success(data={ 'user': UserSerializer(user).data, 'token': tokens, diff --git a/config/settings.py b/config/settings.py index 1487abf..36df6b3 100644 --- a/config/settings.py +++ b/config/settings.py @@ -180,6 +180,12 @@ ALIYUN_SMS = { 'SEND_INTERVAL': 60, # 发送间隔(秒) 60秒 } +# Aliyun Phone Auth (号码认证/一键登录) +ALIYUN_PHONE_AUTH = { + 'ACCESS_KEY_ID': os.environ.get('PHONE_AUTH_ACCESS_KEY_ID', ALIYUN_ACCESS_KEY_ID), + 'ACCESS_KEY_SECRET': os.environ.get('PHONE_AUTH_ACCESS_KEY_SECRET', ALIYUN_ACCESS_KEY_SECRET), +} + # Swagger/OpenAPI Settings SPECTACULAR_SETTINGS = { 'TITLE': 'RTC API', diff --git a/requirements.txt b/requirements.txt index 3bf8278..0abefa2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -28,3 +28,4 @@ sqlparse==0.5.5 urllib3==2.6.3 drf-spectacular==0.27.1 alibabacloud_dysmsapi20170525>=4.4.0 +alibabacloud_dypnsapi20170525>=3.0.0 diff --git a/utils/phone_auth.py b/utils/phone_auth.py new file mode 100644 index 0000000..7b6edbf --- /dev/null +++ b/utils/phone_auth.py @@ -0,0 +1,82 @@ +""" +阿里云号码认证服务工具类 +用于一键登录:App 端获取 token → 后端用 token 换取真实手机号 +""" +import logging +from django.conf import settings + +logger = logging.getLogger(__name__) + +try: + from alibabacloud_dypnsapi20170525.client import Client + from alibabacloud_tea_openapi.models import Config + from alibabacloud_dypnsapi20170525.models import GetMobileRequest + PHONE_AUTH_SDK_AVAILABLE = True +except ImportError: + PHONE_AUTH_SDK_AVAILABLE = False + + +class PhoneAuthClient: + """阿里云号码认证客户端(单例)""" + + _instance = None + + def __new__(cls): + if cls._instance is None: + cls._instance = super().__new__(cls) + cls._instance._initialized = False + return cls._instance + + def __init__(self): + if self._initialized: + return + + if not PHONE_AUTH_SDK_AVAILABLE: + self.client = None + self._initialized = True + return + + auth_config = settings.ALIYUN_PHONE_AUTH + if not auth_config.get('ACCESS_KEY_ID'): + self.client = None + self._initialized = True + return + + config = Config( + access_key_id=auth_config['ACCESS_KEY_ID'], + access_key_secret=auth_config['ACCESS_KEY_SECRET'], + endpoint='dypnsapi.aliyuncs.com', + ) + self.client = Client(config) + self._initialized = True + + def get_mobile(self, access_token): + """ + 用 App 端 SDK 获取的 token 换取真实手机号 + :param access_token: App 端 SDK 返回的 token + :return: (phone: str|None, error_msg: str) + """ + if not self.client: + logger.warning('号码认证 SDK 未配置') + return None, '号码认证服务未配置' + + try: + request = GetMobileRequest(access_token=access_token) + response = self.client.get_mobile(request) + + if response.body.code == 'OK': + mobile = response.body.get_mobile_result_dto.mobile + logger.info('号码认证成功: %s', mobile) + return mobile, '' + else: + msg = response.body.message or response.body.code + logger.error('号码认证失败: %s', msg) + return None, msg + except Exception as e: + logger.error('号码认证异常: %s', str(e)) + return None, str(e) + + +def get_phone_auth_client(): + """获取号码认证客户端单例""" + return PhoneAuthClient()