diff --git a/oedp-server/resources/settings.py b/oedp-server/resources/settings.py index 757a7ab0080d672e11f4ea9c7c0bc2bdd190b947..7b062cde08c1aa591c8cd7839899720a0a7c4b08 100644 --- a/oedp-server/resources/settings.py +++ b/oedp-server/resources/settings.py @@ -43,7 +43,6 @@ MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', @@ -112,5 +111,10 @@ DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' # Rest Framework REST_FRAMEWORK = { + # 分页 'DEFAULT_PAGINATION_CLASS': 'utils.pagination.CustomPageNumberPagination', + # 身份认证 + 'DEFAULT_AUTHENTICATION_CLASSES': ( + 'usermanager.jwt_auth.authentication.TokenAuthentication', + ), } diff --git a/oedp-server/usermanager/jwt_auth/authentication.py b/oedp-server/usermanager/jwt_auth/authentication.py index 8eb9ba68c7dc5f774490085e151428b19b166e76..ad839cde7672bfa654ec1df0b92fd46bca48cdbd 100644 --- a/oedp-server/usermanager/jwt_auth/authentication.py +++ b/oedp-server/usermanager/jwt_auth/authentication.py @@ -11,17 +11,32 @@ # See the Mulan PSL v2 for more details. # Create: 2025-01-24 # ====================================================================================================================== +from datetime import datetime import jwt +import pytz +from django.contrib.auth import get_user_model from django.utils.encoding import smart_text -from rest_framework import exceptions from rest_framework.authentication import BaseAuthentication +from rest_framework.exceptions import AuthenticationFailed from constants.auth import JWT_AUTH_HEADER_PREFIX from usermanager.jwt_auth.jwt_manager import JWTManager +from utils.time import get_time_zone -class JSONWebTokenAuthentication(BaseAuthentication): +class TokenAuthentication(BaseAuthentication): + + def __init__(self): + self.user = None + super().__init__() + + def _get_user(self, user_id): + user_model = get_user_model() + try: + self.user = user_model.objects.get(pk=user_id) + except user_model.DoesNotExist: + raise AuthenticationFailed('User not exists.') def _get_token(self, request): # 从 cookie 中获取 token @@ -33,20 +48,47 @@ class JSONWebTokenAuthentication(BaseAuthentication): if not token or smart_text(auth_header[0].lower()) != JWT_AUTH_HEADER_PREFIX.lower(): return None if len(auth_header) != 2: - raise exceptions.AuthenticationFailed('Invalid Authorization header.') + raise AuthenticationFailed('Invalid Authorization header.') return auth_header[1] - def authenticate(self, request): - token = self._get_token(request) - # 解码 token + def _check_token(self, token): try: payload = JWTManager().decode_token(token) # 签名过期 except jwt.ExpiredSignatureError as error: - raise exceptions.AuthenticationFailed('Signature has expired.') from error + raise AuthenticationFailed('Token has expired.') from error # 解码错误 except jwt.DecodeError as error: - raise exceptions.AuthenticationFailed('Decoding signature error.') from error + raise AuthenticationFailed('Decoding token error.') from error # 无效token except jwt.InvalidTokenError as error: - raise exceptions.AuthenticationFailed() from error + raise AuthenticationFailed('Invalid token.') from error + user_id = payload.get('user_id', None) + username = payload.get("username", "") + if not (username and user_id): + raise AuthenticationFailed('Invalid payload.') + self._get_user(user_id) + + def _check_csrf_token(self, csrf_token): + if not self.user.csrf_token: + raise AuthenticationFailed('User not logged in.') + if self.user.csrf_token != csrf_token: + raise AuthenticationFailed('Invalid CSRF token.') + current_datetime = datetime.now(tz=pytz.timezone(get_time_zone())) + if current_datetime > self.user.expires_at: + raise AuthenticationFailed('CSRF token has expired.') + + def authenticate(self, request): + token = self._get_token(request) + csrf_token = request.COOKIES.get('csrf_token', '') + if token is None or csrf_token is None: + return None + self._check_token(token) + self._check_csrf_token(csrf_token) + return self.user, None + + def authenticate_header(self, request): + """ + 认证 jwt 头部 + """ + return f'{JWT_AUTH_HEADER_PREFIX} realm="api"' diff --git a/oedp-server/usermanager/jwt_auth/jwt_manager.py b/oedp-server/usermanager/jwt_auth/jwt_manager.py index 47849989d6cf8fd59c9370e240c49550db7a939b..270a6e7ea4a514c83468dfee548428ece4a01545 100644 --- a/oedp-server/usermanager/jwt_auth/jwt_manager.py +++ b/oedp-server/usermanager/jwt_auth/jwt_manager.py @@ -44,9 +44,8 @@ class JWTManager: """ username_field = get_user_model().USERNAME_FIELD username = self._get_username(user) - uuid_ = str(uuid.uuid1()) expiration_time = datetime.now(tz=pytz.timezone(get_time_zone())) + timedelta(days=JWT_EXPIRY_DAYS) - return {'user_id': user.pk, username_field: username, 'uuid': uuid_, 'exp': expiration_time} + return {'user_id': user.pk, username_field: username, 'exp': expiration_time} def decode_token(self, token): options = {'verify_exp': False} diff --git a/oedp-server/usermanager/models.py b/oedp-server/usermanager/models.py index d68eba33f5cd57728a762e1ca741a3d0437c3214..25298483faa1ac5b300943b7c5f500332bf1d4c3 100644 --- a/oedp-server/usermanager/models.py +++ b/oedp-server/usermanager/models.py @@ -54,6 +54,10 @@ class User(AbstractBaseUser): role = models.IntegerField('用户角色', choices=RoleChoices.choices) has_reset = models.BooleanField('用户是否重设密码', default=False, blank=True, null=True) last_login = models.DateTimeField('上次登陆时间', blank=True, null=True) + + csrf_token = models.CharField('CSRF token', max_length=255, blank=True, null=True) + expires_at = models.DateTimeField('CSRF token 失效时间', blank=True, null=True) + created_at = models.DateTimeField('创建时间', auto_now_add=True) updated_at = models.DateTimeField('更新时间', auto_now=True) diff --git a/oedp-server/usermanager/serializers.py b/oedp-server/usermanager/serializers.py index c88b7a535e259cc20869ac184f567eea0e4b72b7..bb074cd3e9c966f8b15a70a17aead080b1c941b4 100644 --- a/oedp-server/usermanager/serializers.py +++ b/oedp-server/usermanager/serializers.py @@ -13,10 +13,12 @@ # ====================================================================================================================== import re +from datetime import datetime, timedelta from django.contrib.auth.hashers import make_password from rest_framework import serializers +from constants.auth import JWT_EXPIRY_DAYS from constants.configs.account_config import ADMIN_ID, USERNAME_MIN_LEN, USERNAME_MAX_LEN from usermanager.models import User from utils.cipher import get_salt @@ -71,7 +73,7 @@ class UserSerializer(serializers.ModelSerializer): ) -class CreateUserSerializer(serializers.ModelSerializer): +class UserSerializerForCreate(serializers.ModelSerializer): password = serializers.CharField(max_length=32, min_length=8, validators=[validate_password_valid]) confirmed_password = serializers.CharField(max_length=32, min_length=8) @@ -96,7 +98,6 @@ class CreateUserSerializer(serializers.ModelSerializer): return False def validate(self, data): - # TODO 判断管理源用户是否登陆 # 校验两次输入的密码是否相等 confirm_password(data) # 校验是否把用户名作为密码 @@ -138,7 +139,7 @@ class UserSerializerForResetPW(serializers.ModelSerializer): def validate(self, data): # 校验用户 id 是否和管理员 id 相同。 - if int(self.context.get('request').get('id')[0]) != ADMIN_ID: + if int(data.get('id')[0]) != ADMIN_ID: raise serializers.ValidationError({'id': 'This id is not the id of the administrator user.'}) # 判断用户是否存在 if not User.objects.filter(id=ADMIN_ID).exists(): @@ -146,9 +147,9 @@ class UserSerializerForResetPW(serializers.ModelSerializer): # 判断用户是否是管理员 if User.objects.get(id=ADMIN_ID).role != User.RoleChoices.ADMIN: raise serializers.ValidationError('The role of user is not administrator.') - # # 判断是否重置过密码 - # if User.objects.get(id=ADMIN_ID).has_reset: - # raise serializers.ValidationError('The Admin user has reset password.') + # 判断是否重置过密码 + if User.objects.get(id=ADMIN_ID).has_reset: + raise serializers.ValidationError('The Admin user has reset password.') # 校验两次输入的密码是否相等 confirm_password(data) # 校验是否把用户名作为密码 @@ -193,6 +194,8 @@ class UserSerializerForLogin(serializers.ModelSerializer): return data def update(self, instance, validated_data): - pass - - + instance.csrf_token = validated_data.get('csrf_token') + instance.expires_at = datetime.now() + timedelta(days=JWT_EXPIRY_DAYS) + instance.last_login = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + instance.save() + return instance diff --git a/oedp-server/usermanager/views.py b/oedp-server/usermanager/views.py index 62b5c942e4e82c337cec67322ae66b9ec13c0261..ae9fab9a6a6abdf104e08fecbf8e803a89f78fe0 100644 --- a/oedp-server/usermanager/views.py +++ b/oedp-server/usermanager/views.py @@ -12,18 +12,17 @@ # Create: 2025-01-21 # ====================================================================================================================== +from django.contrib.auth import authenticate, login, logout from rest_framework import status from rest_framework.decorators import action from rest_framework.response import Response from rest_framework.viewsets import ViewSet -from django.contrib.auth import authenticate, login, logout from constants.configs.account_config import ADMIN_ID from usermanager.jwt_auth.jwt_manager import JWTManager from usermanager.models import User from usermanager.serializers import ( UserSerializer, - CreateUserSerializer, UserSerializerForResetPW, UserSerializerForLogin, ) @@ -31,31 +30,22 @@ from usermanager.serializers import ( class UserViewSet(ViewSet): - def create(self, request): + @action(methods=['PUT'], detail=False, authentication_classes=[]) + def reset_password(self, request): """ - 创建用户接口 + 在管理员用户首次登陆前修改密码 """ - serializer = CreateUserSerializer(data=request.data) - if not serializer.is_valid(): + try: + admin = User.objects.get(id=ADMIN_ID) + except User.DoesNotExist: return Response({ "is_success": False, - "message": "Please check input.", - "errors": serializer.errors, + "message": "Please check admin user.", + "errors": { + 'id': [f'The user whose id is {ADMIN_ID} does not exist.'] + } }, status=status.HTTP_400_BAD_REQUEST) - user = serializer.save() - return Response({ - "is_success": True, - "message": "Create user successfully.", - "data": UserSerializer(user).data, - }, status=status.HTTP_201_CREATED) - - @action(methods=['PUT'], detail=False) - def reset_password(self, request): - """ - 在管理员用户首次登陆前修改密码 - """ - admin = User.objects.get(id=ADMIN_ID) - serializer = UserSerializerForResetPW(admin, data=request.data, partial=True, context={'request': request.data}) + serializer = UserSerializerForResetPW(admin, data=request.data, partial=True) if not serializer.is_valid(): return Response({ "is_success": False, @@ -67,10 +57,9 @@ class UserViewSet(ViewSet): "is_success": True, "message": "Reset password successfully.", "data": UserSerializer(user).data, - "user": user }, status=status.HTTP_201_CREATED) - @action(methods=['POST'], detail=False) + @action(methods=['POST'], detail=False, authentication_classes=[]) def login(self, request): user = User.objects.get(username=request.data.get('username')) serializer = UserSerializerForLogin(user, data=request.data) @@ -96,15 +85,21 @@ class UserViewSet(ViewSet): "message": "Login successfully.", "data": UserSerializer(user).data, }, status=status.HTTP_200_OK) + # 设置 JWT token 和 CSRF token jwt_manager = JWTManager() token = jwt_manager.generate_token(user) csrf_token = jwt_manager.generate_csrf_token() - response.headers.setdefault('csrf_token', csrf_token) + response.set_cookie('csrf_token', csrf_token) response.set_cookie("token", token) + serializer.save(csrf_token=csrf_token) return response - @action(methods=['GET'], detail=False) + @action(methods=['POST'], detail=False) def logout(self, request): + user = request.user + user.csrf_token = "" + user.expires_at = None + user.save() logout(request) return Response({ "is_success": True,