From c6dbda5a8836f92b585584d2360578f5a43edecb Mon Sep 17 00:00:00 2001 From: jlj <3042504846@qq.com> Date: Tue, 20 May 2025 12:24:53 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E9=83=A8=E5=88=86token?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/daren_detail/views.py | 1 - apps/user/authentication.py | 42 ++ apps/user/migrations/0003_usertoken.py | 27 + .../0004_user_is_active_user_is_staff.py | 23 + apps/user/models.py | 88 ++- apps/user/views.py | 549 +++++++++--------- daren/settings.py | 35 +- daren/urls.py | 6 + 8 files changed, 477 insertions(+), 294 deletions(-) create mode 100644 apps/user/authentication.py create mode 100644 apps/user/migrations/0003_usertoken.py create mode 100644 apps/user/migrations/0004_user_is_active_user_is_staff.py diff --git a/apps/daren_detail/views.py b/apps/daren_detail/views.py index 2b02e22..6a41284 100644 --- a/apps/daren_detail/views.py +++ b/apps/daren_detail/views.py @@ -1373,7 +1373,6 @@ def get_creator_trends(request, creator_id=None): (datetime.now().date() - timedelta(days=30 - i)).strftime('%Y-%m-%d') for i in range(31) ] - # 设定随机的起始值和波动 import random base_gmv = random.uniform(500, 3000) diff --git a/apps/user/authentication.py b/apps/user/authentication.py new file mode 100644 index 0000000..e690a3e --- /dev/null +++ b/apps/user/authentication.py @@ -0,0 +1,42 @@ +from rest_framework import authentication +from rest_framework import exceptions +from django.contrib.auth.models import AnonymousUser +from .models import User, UserToken +from django.utils import timezone + +class CustomTokenAuthentication(authentication.BaseAuthentication): + keyword = 'Token' # 设置认证头关键字 + + def authenticate(self, request): + # 从请求头获取token + auth_header = request.META.get('HTTP_AUTHORIZATION') + + if not auth_header: + return None + + try: + # 提取token + parts = auth_header.split() + + if len(parts) != 2 or parts[0] != self.keyword: + raise exceptions.AuthenticationFailed('无效的认证头格式,应为: Token ') + + token = parts[1] + + # 查找token记录并确保token存在且有效 + try: + token_obj = UserToken.objects.select_related('user').get( + token=token, + expired_at__gt=timezone.now() # 确保token未过期 + ) + except UserToken.DoesNotExist: + raise exceptions.AuthenticationFailed('无效的token') + + # 检查用户是否激活 + if not token_obj.user.is_active: + raise exceptions.AuthenticationFailed('用户已被禁用') + + return (token_obj.user, None) + + except Exception as e: + raise exceptions.AuthenticationFailed(f'认证失败: {str(e)}') \ No newline at end of file diff --git a/apps/user/migrations/0003_usertoken.py b/apps/user/migrations/0003_usertoken.py new file mode 100644 index 0000000..1244fd9 --- /dev/null +++ b/apps/user/migrations/0003_usertoken.py @@ -0,0 +1,27 @@ +# Generated by Django 5.1.5 on 2025-05-20 03:49 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('user', '0002_remove_user_is_active'), + ] + + operations = [ + migrations.CreateModel( + name='UserToken', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('token', models.CharField(max_length=40, unique=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('expired_at', models.DateTimeField()), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tokens', to='user.user')), + ], + options={ + 'db_table': 'user_token', + }, + ), + ] diff --git a/apps/user/migrations/0004_user_is_active_user_is_staff.py b/apps/user/migrations/0004_user_is_active_user_is_staff.py new file mode 100644 index 0000000..26b566c --- /dev/null +++ b/apps/user/migrations/0004_user_is_active_user_is_staff.py @@ -0,0 +1,23 @@ +# Generated by Django 5.1.5 on 2025-05-20 03:52 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('user', '0003_usertoken'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='is_active', + field=models.BooleanField(default=True), + ), + migrations.AddField( + model_name='user', + name='is_staff', + field=models.BooleanField(default=False), + ), + ] diff --git a/apps/user/models.py b/apps/user/models.py index 39c1cd7..9115417 100644 --- a/apps/user/models.py +++ b/apps/user/models.py @@ -1,23 +1,65 @@ -from django.db import models - -# Create your models here. -class User(models.Model): - """用户模型,用于登录和账户管理""" - email = models.EmailField(max_length=255, unique=True, verbose_name="电子邮箱") - password = models.CharField(max_length=255, verbose_name="密码") - company = models.CharField(max_length=255, blank=True, null=True, verbose_name="公司名称") - name = models.CharField(max_length=255, blank=True, null=True, verbose_name="用户姓名") - is_first_login = models.BooleanField(default=True, verbose_name="是否首次登录") - last_login = models.DateTimeField(blank=True, null=True, verbose_name="最近登录时间") - - # 时间戳 - created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") - updated_at = models.DateTimeField(auto_now=True, verbose_name="更新时间") - - class Meta: - verbose_name = "用户" - verbose_name_plural = verbose_name - db_table = "users" - - def __str__(self): - return self.email +from django.db import models +from django.utils import timezone +from datetime import timedelta +from django.contrib.auth.models import AbstractBaseUser, BaseUserManager + +class UserManager(BaseUserManager): + def create_user(self, email, password=None, **extra_fields): + if not email: + raise ValueError('邮箱地址不能为空') + email = self.normalize_email(email) + user = self.model(email=email, **extra_fields) + if password: + user.set_password(password) + user.save(using=self._db) + return user + +class User(AbstractBaseUser): + """用户模型,用于登录和账户管理""" + email = models.EmailField(max_length=255, unique=True, verbose_name="电子邮箱") + password = models.CharField(max_length=255, verbose_name="密码") + company = models.CharField(max_length=255, blank=True, null=True, verbose_name="公司名称") + name = models.CharField(max_length=255, blank=True, null=True, verbose_name="用户姓名") + is_first_login = models.BooleanField(default=True, verbose_name="是否首次登录") + last_login = models.DateTimeField(blank=True, null=True, verbose_name="最近登录时间") + is_active = models.BooleanField(default=True) + is_staff = models.BooleanField(default=False) + + # 时间戳 + created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") + updated_at = models.DateTimeField(auto_now=True, verbose_name="更新时间") + + objects = UserManager() + + USERNAME_FIELD = 'email' + REQUIRED_FIELDS = [] + + class Meta: + verbose_name = "用户" + verbose_name_plural = verbose_name + db_table = "users" + + def __str__(self): + return self.email + + @property + def is_authenticated(self): + return True + +class UserToken(models.Model): + user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='tokens') + token = models.CharField(max_length=40, unique=True) + created_at = models.DateTimeField(auto_now_add=True) + expired_at = models.DateTimeField() + + def save(self, *args, **kwargs): + if not self.expired_at: + # 设置token有效期为30天 + self.expired_at = timezone.now() + timedelta(days=30) + super().save(*args, **kwargs) + + def is_expired(self): + return timezone.now() > self.expired_at + + class Meta: + db_table = 'user_token' diff --git a/apps/user/views.py b/apps/user/views.py index b24dde7..616fd0a 100644 --- a/apps/user/views.py +++ b/apps/user/views.py @@ -1,268 +1,281 @@ -from django.http import JsonResponse -# from .models import TiktokUserVideos -import logging -import os -from django.views.decorators.http import require_http_methods -from django.views.decorators.csrf import csrf_exempt -from django.shortcuts import render -import json -import requests -import concurrent.futures -import shutil -import dotenv -import random - -dotenv.load_dotenv() - -# 添加logger定义 -logger = logging.getLogger(__name__) - -directory_monitoring = {} - -# 全局变量来控制检测线程 -monitor_thread = None -is_monitoring = False - -@csrf_exempt -@require_http_methods(["POST"]) -def user_login(request): - """用户登录接口,首次登录会返回需要填写信息的标志""" - try: - from .models import User - import json - from django.contrib.auth.hashers import check_password, make_password - from datetime import datetime - - data = json.loads(request.body) - - # 获取登录参数 - email = data.get('email') - password = data.get('password') - - if not email or not password: - return JsonResponse({ - 'code': 400, - 'message': '缺少必要参数: email 或 password', - 'data': None - }, json_dumps_params={'ensure_ascii': False}) - - # 查询用户 - try: - user = User.objects.get(email=email) - - # 验证密码 - # 注意:这里假设密码已经进行了哈希存储,实际使用时需要采用适当的密码验证方法 - # 如果密码未哈希存储,直接比较原始密码 - password_valid = (user.password == password) - - if not password_valid: - return JsonResponse({ - 'code': 401, - 'message': '用户名或密码错误', - 'data': None - }, json_dumps_params={'ensure_ascii': False}) - - # 检查是否首次登录 - is_first_login = user.is_first_login - - # 更新最后登录时间 - user.last_login = datetime.now() - user.save() - - # 构造返回数据 - user_data = { - 'user_id': user.id, - 'email': user.email, - 'is_first_login': is_first_login, - 'name': user.name, - 'company': user.company - } - - return JsonResponse({ - 'code': 200, - 'message': '登录成功', - 'data': user_data - }, json_dumps_params={'ensure_ascii': False}) - - except User.DoesNotExist: - # 用户不存在,创建新用户 - new_user = User.objects.create( - email=email, - password=password, # 注意:实际使用时应该哈希存储密码 - is_first_login=True, - last_login=datetime.now() - ) - - return JsonResponse({ - 'code': 200, - 'message': '登录成功', - 'data': { - 'user_id': new_user.id, - 'email': new_user.email, - 'is_first_login': True, - 'name': None, - 'company': None - } - }, json_dumps_params={'ensure_ascii': False}) - - except Exception as e: - logger.error(f"用户登录失败: {e}") - import traceback - logger.error(f"详细错误: {traceback.format_exc()}") - return JsonResponse({ - 'code': 500, - 'message': f'登录失败: {str(e)}', - 'data': None - }, json_dumps_params={'ensure_ascii': False}) - - -@csrf_exempt -@require_http_methods(["POST"]) -def update_user_info(request): - """更新用户信息,首次登录时填写公司和姓名""" - try: - from .models import User - import json - - data = json.loads(request.body) - - # 获取参数 - user_id = data.get('user_id') - company = data.get('company') - name = data.get('name') - - if not user_id: - return JsonResponse({ - 'code': 400, - 'message': '缺少必要参数: user_id', - 'data': None - }, json_dumps_params={'ensure_ascii': False}) - - # 如果是首次登录,需要填写公司和姓名 - if not company or not name: - return JsonResponse({ - 'code': 400, - 'message': '首次登录需要填写公司和姓名', - 'data': None - }, json_dumps_params={'ensure_ascii': False}) - - # 查询用户并更新信息 - try: - user = User.objects.get(id=user_id) - - # 更新信息 - user.company = company - user.name = name - user.is_first_login = False # 更新后不再是首次登录 - user.save() - - return JsonResponse({ - 'code': 200, - 'message': '信息更新成功', - 'data': { - 'user_id': user.id, - 'email': user.email, - 'is_first_login': False, - 'name': user.name, - 'company': user.company - } - }, json_dumps_params={'ensure_ascii': False}) - - except User.DoesNotExist: - return JsonResponse({ - 'code': 404, - 'message': f'找不到ID为 {user_id} 的用户', - 'data': None - }, json_dumps_params={'ensure_ascii': False}) - - except Exception as e: - logger.error(f"更新用户信息失败: {e}") - import traceback - logger.error(f"详细错误: {traceback.format_exc()}") - return JsonResponse({ - 'code': 500, - 'message': f'更新用户信息失败: {str(e)}', - 'data': None - }, json_dumps_params={'ensure_ascii': False}) - - -@csrf_exempt -@require_http_methods(["POST"]) -def user_register(request): - """用户注册接口,允许用户创建新账户,可选填写公司和姓名""" - try: - from .models import User - import json - from datetime import datetime - - data = json.loads(request.body) - - # 获取注册参数 - email = data.get('email') - password = data.get('password') - company = data.get('company') # 可选参数 - name = data.get('name') # 可选参数 - - # 检查必要参数 - if not email or not password: - return JsonResponse({ - 'code': 400, - 'message': '缺少必要参数: email 或 password', - 'data': None - }, json_dumps_params={'ensure_ascii': False}) - - # 检查邮箱是否已注册 - if User.objects.filter(email=email).exists(): - return JsonResponse({ - 'code': 409, - 'message': '该邮箱已注册', - 'data': None - }, json_dumps_params={'ensure_ascii': False}) - - # 创建用户 - try: - # 根据是否提供公司和姓名决定是否为首次登录 - is_first_login = not (company and name) - - # 创建用户 - user = User.objects.create( - email=email, - password=password, # 注意:实际使用时应该哈希存储密码 - company=company, - name=name, - is_first_login=is_first_login, - last_login=datetime.now() - ) - - # 构造返回数据 - user_data = { - 'user_id': user.id, - 'email': user.email, - 'is_first_login': is_first_login, - 'company': user.company, - 'name': user.name - } - - return JsonResponse({ - 'code': 200, - 'message': '注册成功', - 'data': user_data - }, json_dumps_params={'ensure_ascii': False}) - - except Exception as e: - logger.error(f"创建用户失败: {e}") - return JsonResponse({ - 'code': 500, - 'message': f'注册失败: {str(e)}', - 'data': None - }, json_dumps_params={'ensure_ascii': False}) - - except Exception as e: - logger.error(f"用户注册失败: {e}") - import traceback - logger.error(f"详细错误: {traceback.format_exc()}") - return JsonResponse({ - 'code': 500, - 'message': f'注册失败: {str(e)}', - 'data': None - }, json_dumps_params={'ensure_ascii': False}) +from django.http import JsonResponse +# from .models import TiktokUserVideos +import logging +import os +from django.views.decorators.http import require_http_methods +from django.views.decorators.csrf import csrf_exempt +from django.shortcuts import render +import json +import requests +import concurrent.futures +import shutil +import dotenv +import random +from rest_framework.decorators import api_view, permission_classes +from rest_framework.permissions import IsAuthenticated, AllowAny +import hashlib +import time +from django.contrib.auth.hashers import check_password +from django.utils import timezone + +dotenv.load_dotenv() + +# 添加logger定义 +logger = logging.getLogger(__name__) + +directory_monitoring = {} + +# 全局变量来控制检测线程 +monitor_thread = None +is_monitoring = False + +def generate_token(user_id): + """生成简单的token""" + # 使用用户ID和当前时间戳生成token + token_string = f"{user_id}:{time.time()}" + return hashlib.sha1(token_string.encode()).hexdigest() + +def create_user_token(user): + """创建并保存用户token""" + from .models import UserToken + # 删除该用户的所有旧token + UserToken.objects.filter(user=user).delete() + # 生成新token + token = generate_token(user.id) + # 保存到数据库 + user_token = UserToken.objects.create( + user=user, + token=token + ) + return token + +@csrf_exempt +@api_view(['POST']) +@permission_classes([AllowAny]) +def user_login(request): + """ + 用户登录接口 + + 返回的 token 使用格式: + 在请求头中添加: + Authorization: Token + + 例如: + Authorization: Token fa6931ec4cf5bd46d8dc3a671fe9862c467426b3 + """ + try: + from .models import User + import json + from django.contrib.auth.hashers import check_password + from datetime import datetime + + data = json.loads(request.body) + + # 获取登录参数 + email = data.get('email') + password = data.get('password') + + if not email or not password: + return JsonResponse({ + 'code': 400, + 'message': '缺少必要参数: email 或 password', + 'data': None + }, json_dumps_params={'ensure_ascii': False}) + + # 查询用户 + try: + user = User.objects.get(email=email) + + # 验证密码 + if not user.check_password(password): + return JsonResponse({ + 'code': 401, + 'message': '用户名或密码错误', + 'data': None + }, json_dumps_params={'ensure_ascii': False}) + + # 生成并保存token + token = create_user_token(user) + + # 检查是否首次登录 + is_first_login = user.is_first_login + + # 更新最后登录时间 + user.last_login = timezone.now() + user.save() + + # 构造返回数据 + user_data = { + 'user_id': user.id, + 'email': user.email, + 'is_first_login': is_first_login, + 'name': user.name, + 'company': user.company, + 'token': token + } + + return JsonResponse({ + 'code': 200, + 'message': '登录成功', + 'data': user_data + }, json_dumps_params={'ensure_ascii': False}) + + except User.DoesNotExist: + return JsonResponse({ + 'code': 404, + 'message': '用户不存在', + 'data': None + }, json_dumps_params={'ensure_ascii': False}) + + except Exception as e: + logger.error(f"用户登录失败: {e}") + import traceback + logger.error(f"详细错误: {traceback.format_exc()}") + return JsonResponse({ + 'code': 500, + 'message': f'登录失败: {str(e)}', + 'data': None + }, json_dumps_params={'ensure_ascii': False}) + + +@csrf_exempt +@api_view(['POST']) +@permission_classes([IsAuthenticated]) +def update_user_info(request): + """更新用户信息,需要认证""" + try: + data = json.loads(request.body) + + # 获取参数 + company = data.get('company') + name = data.get('name') + + # 获取当前认证用户 + user = request.user + + # 如果请求中包含 user_id 且与当前用户不匹配,返回错误 + if 'user_id' in data and int(data['user_id']) != user.id: + return JsonResponse({ + 'code': 403, + 'message': '您只能修改自己的信息', + 'data': None + }, json_dumps_params={'ensure_ascii': False}) + + # 如果是首次登录,需要填写公司和姓名 + if not company or not name: + return JsonResponse({ + 'code': 400, + 'message': '首次登录需要填写公司和姓名', + 'data': None + }, json_dumps_params={'ensure_ascii': False}) + + # 更新信息 + user.company = company + user.name = name + user.is_first_login = False # 更新后不再是首次登录 + user.save() + + return JsonResponse({ + 'code': 200, + 'message': '信息更新成功', + 'data': { + 'user_id': user.id, + 'email': user.email, + 'is_first_login': False, + 'name': user.name, + 'company': user.company + } + }, json_dumps_params={'ensure_ascii': False}) + + except Exception as e: + logger.error(f"更新用户信息失败: {e}") + import traceback + logger.error(f"详细错误: {traceback.format_exc()}") + return JsonResponse({ + 'code': 500, + 'message': f'更新用户信息失败: {str(e)}', + 'data': None + }, json_dumps_params={'ensure_ascii': False}) + + +@csrf_exempt +@api_view(['POST']) +@permission_classes([AllowAny]) +def user_register(request): + """用户注册接口""" + try: + from .models import User + import json + from datetime import datetime + + data = json.loads(request.body) + + # 获取注册参数 + email = data.get('email') + password = data.get('password') + company = data.get('company') # 可选参数 + name = data.get('name') # 可选参数 + + # 检查必要参数 + if not email or not password: + return JsonResponse({ + 'code': 400, + 'message': '缺少必要参数: email 或 password', + 'data': None + }, json_dumps_params={'ensure_ascii': False}) + + # 检查邮箱是否已注册 + if User.objects.filter(email=email).exists(): + return JsonResponse({ + 'code': 409, + 'message': '该邮箱已注册', + 'data': None + }, json_dumps_params={'ensure_ascii': False}) + + # 创建用户 + try: + # 根据是否提供公司和姓名决定是否为首次登录 + is_first_login = not (company and name) + + # 创建用户 + user = User.objects.create_user( + email=email, + password=password, + company=company, + name=name, + is_first_login=is_first_login, + last_login=timezone.now() + ) + + # 构造返回数据 + user_data = { + 'user_id': user.id, + 'email': user.email, + 'is_first_login': is_first_login, + 'company': user.company, + 'name': user.name + } + + return JsonResponse({ + 'code': 200, + 'message': '注册成功', + 'data': user_data + }, json_dumps_params={'ensure_ascii': False}) + + except Exception as e: + logger.error(f"创建用户失败: {e}") + return JsonResponse({ + 'code': 500, + 'message': f'注册失败: {str(e)}', + 'data': None + }, json_dumps_params={'ensure_ascii': False}) + + except Exception as e: + logger.error(f"用户注册失败: {e}") + import traceback + logger.error(f"详细错误: {traceback.format_exc()}") + return JsonResponse({ + 'code': 500, + 'message': f'注册失败: {str(e)}', + 'data': None + }, json_dumps_params={'ensure_ascii': False}) diff --git a/daren/settings.py b/daren/settings.py index ac6f466..36e8cbc 100644 --- a/daren/settings.py +++ b/daren/settings.py @@ -44,7 +44,8 @@ INSTALLED_APPS = [ "apps.discovery.apps.DiscoveryConfig", "apps.template.apps.TemplateConfig", "apps.brands.apps.BrandsConfig", - + 'rest_framework', + 'rest_framework_simplejwt', ] MIDDLEWARE = [ @@ -173,4 +174,34 @@ LOGGING = { 'propagate': True, }, }, -} \ No newline at end of file +} + +# 自定义用户模型 +AUTH_USER_MODEL = 'user.User' + +# REST Framework 设置 +REST_FRAMEWORK = { + 'DEFAULT_AUTHENTICATION_CLASSES': ( + 'apps.user.authentication.CustomTokenAuthentication', + ), +} + +# JWT 设置 +from datetime import timedelta +SIMPLE_JWT = { + 'ACCESS_TOKEN_LIFETIME': timedelta(days=1), + 'REFRESH_TOKEN_LIFETIME': timedelta(days=7), + 'ROTATE_REFRESH_TOKENS': False, + 'ALGORITHM': 'HS256', + 'SIGNING_KEY': SECRET_KEY, + 'AUTH_HEADER_TYPES': ('Bearer',), + 'USER_ID_FIELD': 'id', + 'USER_ID_CLAIM': 'user_id', + 'UPDATE_LAST_LOGIN': False, # 不在 token 中记录最后登录时间 + 'TOKEN_TYPE_CLAIM': None, # 不在 token 中包含 token 类型 + 'JTI_CLAIM': None, # 不在 token 中包含 JWT ID +} + +# JWT配置 +JWT_SECRET_KEY = 'your-secret-key-here' # 建议使用更安全的密钥 +JWT_EXPIRATION_DELTA = 24 * 60 * 60 # token有效期24小时(秒) \ No newline at end of file diff --git a/daren/urls.py b/daren/urls.py index e9a654f..c7f260f 100644 --- a/daren/urls.py +++ b/daren/urls.py @@ -1,8 +1,14 @@ from django.contrib import admin from django.urls import path, include +from rest_framework_simplejwt.views import ( + TokenObtainPairView, + TokenRefreshView, +) urlpatterns = [ path('admin/', admin.site.urls), + path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'), + path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'), path('api/user/', include('apps.user.urls')), path('api/daren_detail/', include('apps.daren_detail.urls')), path('api/operation/', include('apps.expertproducts.urls')),