Django中request.user始终返回AnonymousUser的问题求助
自定义Discord OAuth2登录后request.user始终为AnonymousUser问题
问题详情
未继承AbstractUser或AbstractBaseModel实现Discord OAuth2登录功能,一个月前正常,一周前出现异常:
- 自定义认证流程正常,
authenticate()返回DiscordUser实例 - 调用
login()时已传入自定义backend参数 - sessionid在Cookie和数据库中均有效
- 但
request.user始终返回AnonymousUser,request.user.is_authenticated为False
相关代码
auth.py
from django.contrib.auth.backends import BaseBackend from .models import DiscordUser class DiscordAuthenticationBackend(BaseBackend): def authenticate(self, request, user) -> DiscordUser: try: user = DiscordUser.objects.get(pk=user["id"]) return user except DiscordUser.DoesNotExist: new_user = DiscordUser.objects.create_discord_user(user) new_user.save() return new_user
managers.py
from django.contrib.auth import models from datetime import datetime from utils.cipher import XChaCha20Manager from base64 import b64encode class DiscordUserOAuth2Manager(models.UserManager): def create_discord_user(self, user): # encrypt tokens with the same nonce encrypted_access_token, nonce = XChaCha20Manager().encrypt(user["access_token"].encode()) encrypted_refresh_token, _ = XChaCha20Manager(nonce=nonce).encrypt(user["refresh_token"].encode()) new_user = self.create( user_id=user["id"], username = f"{user["username"]}", avatar = user["avatar"], encrypted_access_token = b64encode(encrypted_access_token).decode(), encrypted_refresh_token = b64encode(encrypted_refresh_token).decode(), encryption_nonce = b64encode(nonce).decode(), last_login = datetime.now(), ) return new_user
models.py
from .managers import DiscordUserOAuth2Manager from utils.cipher import XChaCha20Manager from base64 import b64decode from django.db import models class VerificationSession(models.Model): user_id = models.BigIntegerField(primary_key=True) status = models.BooleanField(default=False) class DiscordUser(models.Model): objects = DiscordUserOAuth2Manager() user_id = models.BigIntegerField(primary_key=True) username = models.CharField(max_length=100, unique=True) avatar = models.CharField(max_length=36) encrypted_access_token = models.CharField(max_length=100) # stored in base64 encrypted_refresh_token = models.CharField(max_length=100) # stored in base64 encryption_nonce = models.CharField(max_length=100) # stored in base64 is_premium = models.BooleanField(default=False) is_authenticated = True last_login = models.DateTimeField(null=True) async def get_token(self, user_id: str, type: str) -> str | None: try: user = await DiscordUser.objects.aget(pk=user_id) match type: case "access": return XChaCha20Manager(nonce=b64decode(user.encryption_nonce)).decrypt(b64decode(user.encrypted_access_token)) case "refresh": return XChaCha20Manager(nonce=b64decode(user.encryption_nonce)).decrypt(b64decode(user.encrypted_refresh_token)) except DiscordUser.DoesNotExist: return None
views.py
from django.http import HttpResponse, HttpRequest, HttpResponseRedirect from django.shortcuts import render, redirect from django.contrib.auth import authenticate, login, logout from django.contrib.auth.decorators import login_required from securifier.settings import BASE_DIR from typing import Union import httpx, dotenv # Create your views here. ENV_PATH = BASE_DIR / ".env" CLIENT_ID = dotenv.get_key(ENV_PATH, "CLIENT_ID") OAUTH2_REDIRECT_URI = dotenv.get_key(ENV_PATH, "OAUTH2_REDIRECT_URI") CLIENT_SECRET = dotenv.get_key(ENV_PATH, "CLIENT_SECRET") DISCORD_AUTH_URL = fr"https://discord.com/api/oauth2/authorize?client_id={CLIENT_ID}&redirect_uri={OAUTH2_REDIRECT_URI}&response_type=code&scope=identify%20guilds" DISCORD_BOT_INVITE = fr"https://discord.com/api/oauth2/authorize?client_id={CLIENT_ID}&permissions=8&scope=bot" def index(request: HttpRequest) -> HttpResponse: print(request.user) print(request.user.is_authenticated) return render(request, "index/index.html") def botInvite(request: HttpRequest) -> HttpResponse: return redirect(DISCORD_BOT_INVITE) # Discord OAuth2 Login def discordLogin(request: HttpRequest) -> HttpResponseRedirect: if request.user.is_authenticated: return redirect("index") return redirect(DISCORD_AUTH_URL) @login_required(login_url="/oauth2") def logoutUser(request: HttpRequest) -> HttpResponseRedirect: logout(request) return redirect("index") def exchangeOauth2Code(code) -> Union[str, None]: BASE_API_URI = r"https://discord.com/api/v10" data = { 'grant_type': 'authorization_code', 'code': code, 'redirect_uri': OAUTH2_REDIRECT_URI } headers = { 'Content-Type': 'application/x-www-form-urlencoded' } try: response = httpx.post(f"{BASE_API_URI}/oauth2/token", data=data, headers=headers, auth=(CLIENT_ID, CLIENT_SECRET)) response.raise_for_status() access_token, refresh_token = response.json()["access_token"], response.json()["refresh_token"] user_data = httpx.get(f"{BASE_API_URI}/users/@me", headers={"Authorization": f"Bearer {access_token}"}) user_data = user_data.json() user_data.update({"access_token": access_token, "refresh_token": refresh_token}) # add tokens to user_data return user_data except Exception as e: print(e) return None def discordLoginCallback(request) -> HttpResponseRedirect: code = request.GET.get("code") user = exchangeOauth2Code(code) if user: # login discord_user = authenticate(request, user=user) login(request, user=discord_user, backend="index.auth.DiscordAuthenticationBackend") print("returning to index") return redirect("index") else: context = { "msg": "An error occurred while the OAuth2 authorization process" } return render(request, "index/message.html", context)
settings.py
""" Django settings for securifier project. Generated by 'django-admin startproject' using Django 4.2.6. For more information on this file, see https://docs.djangoproject.com/en/4.2/topics/settings/ For the full list of settings and their values, see https://docs.djangoproject.com/en/4.2/ref/settings/ """ from pathlib import Path from dotenv import get_key as env_get # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent ENV_PATH = BASE_DIR / ".env" # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = '...' # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True ALLOWED_HOSTS = [] AUTHENTICATION_BACKENDS = [ "index.auth.DiscordAuthenticationBackend", ] # Application definition INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', "adrf", # async django rest framework "index", ] 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', ] ROOT_URLCONF = 'securifier.urls' TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [ BASE_DIR / "templates"], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], }, }, ] WSGI_APPLICATION = 'securifier.wsgi.application' # Database # https://docs.djangoproject.com/en/4.2/ref/settings/#databases DATABASES = { "default": { "ENGINE": "django.db.backends.postgresql", "NAME": env_get(ENV_PATH, "POSTGRE_DB_NAME"), "USER": env_get(ENV_PATH, "POSTGRE_DB_USERNAME"), "PASSWORD": env_get(ENV_PATH, "POSTGRE_DB_PASSWORD"), } } # Password validation # https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators AUTH_PASSWORD_VALIDATORS = [ { 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', }, { 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', }, { 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', }, { 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', }, ] # Internationalization # https://docs.djangoproject.com/en/4.2/topics/i18n/ LANGUAGE_CODE = 'en-us' TIME_ZONE = 'Europe/Amsterdam' USE_I18N = True USE_TZ = False # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/4.2/howto/static-files/ STATIC_URL = 'static/' STATICFILES_DIRS = [ BASE_DIR / "static" ] # Default primary key field type # https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
解决方案
1. 完善认证后端的get_user方法
Django的AuthenticationMiddleware在每个请求中会调用认证后端的get_user方法,根据session中存储的用户ID获取用户实例。当前DiscordAuthenticationBackend缺少该方法,导致无法在后续请求中加载用户,返回AnonymousUser。
修改auth.py:
from django.contrib.auth.backends import BaseBackend from .models import DiscordUser class DiscordAuthenticationBackend(BaseBackend): def authenticate(self, request, user) -> DiscordUser: try: user = DiscordUser.objects.get(pk=user["id"]) return user except DiscordUser.DoesNotExist: new_user = DiscordUser.objects.create_discord_user(user) new_user.save() return new_user # 添加get_user方法 def get_user(self, user_id): try: return DiscordUser.objects.get(pk=user_id) except DiscordUser.DoesNotExist: return None
2. 修正DiscordUser模型的认证属性
Django要求用户模型的is_authenticated是只读属性,当前直接设置类属性可能引发问题,改为@property:
修改models.py中的DiscordUser类:
class DiscordUser(models.Model): objects = DiscordUserOAuth2Manager() # ... 其他字段保持不变 ... @property def is_authenticated(self): # 自定义用户始终返回True,表示已认证 return True # ... 其他方法保持不变 ...
3. 验证session存储的用户ID格式
调用login()时,Django会将用户主键存入session。确认DiscordUser的user_id(主键)为数值类型,且session中_auth_user_id的存储格式与get_user方法接收的参数匹配。
4. 确认Middleware顺序
确保settings.py中SessionMiddleware在AuthenticationMiddleware之前,当前配置已符合要求,无需修改。
内容的提问来源于stack exchange,提问作者asudox
相关产品推荐
相关产品推荐

