@shino_25

Are you sure you want to delete the question?

Leaving a resolved question undeleted may help others!

djangoのリダイレクトについて

解決したいこと

dajngoでWEBアプリを作成しています。
一定期間パスワードを変更していないユーザーがログインした場合にパスワードを変更させるため、強制的に'/admin/password_change/'へリダイレクトさせたいのですが、なぜか管理画面へのログイン画面'/admin/login/?next=/admin/password_change/'にリダイレクトしてしまいます。
※上記についてはmiddlewareを作成しています。

解決方法が分かりませんのでご教示お願い致します。

環境

django 3.2
python 3.7.13

発生している問題・エラー

管理画面へのログイン画面
image.png

パスワード変更画面(こっちに移動してほしい)
image.png

該当するソースコード

password_middleware(自作のmiddleware)

import re
from .models import CustomUser
from django.utils import timezone
import datetime
from django.shortcuts import redirect

class ForcePasswordChangeMiddleware:
    """
    settings.pyのMIDDLEWAREから。
    パスワードをログオン時に強制的に変更させるMiddleware。
    前回のパスワード変更から60日以上経過している場合は、
    強制的にパスワード変更ページにリダイレクトさせる。
    """

    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        global user
        response = self.get_response(request)

        try:
            #ログインできているか
            if CustomUser.objects.get(username=request.user.username) != "":
                user = CustomUser.objects.get(username=request.user.username)

                """パスワードの有効期限チェック:60日以上ならパスワード変更を強制する"""
                d1 = timezone.now().date()  # 今日
                d2 = user.pass_change_date.date() + datetime.timedelta(days=60)  # パスワード変更日+60日
                print("今日:", d1)
                print("60日後:", d2)
                print("残り日数", d2 - d1)
                if d1 > d2:
                    return redirect('/admin/password_change/')#ここで管理画面のログイン画面に移動してしまう
        except:
            pass

        """パスワード変更した場合、pass_change_dateを更新する"""
        if re.match(r'/admin/password_change/done/', request.path):
            try:
                referer = request.environ.get('HTTP_REFERER')
                print("referer:", referer)
                if re.match('http://127.0.0.1:8000/admin/password_change/', referer):
                    #user = CustomUser.objects.get(username=request.user.username)
                    user.pass_change_date = timezone.now()
                    user.save()
            except:
                pass
        return response

プロジェクトのurl.py

from django.contrib import admin
from django.urls import path, include
from myApp.urls import router

urlpatterns = [
    path('admin/', admin.site.urls),
    path('accounts/', include('django.contrib.auth.urls')),
    path('', include('myApp.urls')),
    path('api/', include(router.urls)),

settings.py

import os
from django.contrib import admin
from pathlib import Path

admin.AdminSite.site_title  = '管理画面'
admin.AdminSite.site_header = '○○ 管理画面'
admin.AdminSite.index_title = '○○ メニュー'

#ログイン関連
LOGIN_URL = '/accounts/login'
LOGIN_REDIRECT_URL = '/'
LOGOUT_REDIRECT_URL = '/'

# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent


# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
from django.core.management.utils import get_random_secret_key
SECRET_KEY = get_random_secret_key()


# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

#認証にカスタムユーザーを使用する
AUTH_USER_MODEL = 'myApp.CustomUser'

ALLOWED_HOSTS = ['localhost', '127.0.0.1']


# Application definition

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'myApp.apps.MyappConfig', 
    'myDjango',
    'rest_framework', 
    'django_filters',  
]

MIDDLEWARE = [
    'myApp.password_middleware.ForcePasswordChangeMiddleware', # 自作
    '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 = 'myDjango.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        #'DIRS': [BASE_DIR / 'templates'],
        'DIRS': [os.path.join(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 = 'myDjango.wsgi.application'


# Database
# https://docs.djangoproject.com/en/3.2/ref/settings/#databases

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql_psycopg2',
        'NAME': "DB",
        'USER': "xxx",
        'PASSWORD': "xxxxxxxxxx",
        'HOST': "localhost",
        'PORT': "5432",
    }
}

# APIの設定
# フィルターと結果の1ページの出力件数
REST_FRAMEWORK = {
    'DEFAULT_PERMISSION_CLASSES': ['rest_framework.permissions.IsAdminUser',],
    'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend'],
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
    #'PAGE_SIZE': 50
}

# Password validation
# https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators

AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
        'OPTIONS': {
          'min_length': 10,
        }
    },
    {
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    },
    {
        'NAME': 'myApp.password_validation.CustomValidator',  
    },
]



# Internationalization
# https://docs.djangoproject.com/en/3.2/topics/i18n/

LANGUAGE_CODE = 'ja'

TIME_ZONE = 'Asia/Tokyo'

USE_I18N = True

USE_L10N = True

USE_TZ = True

#SESSION_COOKIE_AGE = 36000

# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.2/howto/static-files/

# staticの設定
STATIC_URL = '/static/'

#pythonanywhereの設定
CSRF_COOKIE_SECURE = True
SESSION_COOKIE_SECURE = True

DEFAULT_AUTO_FIELD='django.db.models.AutoField'

try:
    from .local_settings import *
except:
    pass

自分で試したこと

管理画面へログインできていないのかと思いDBでlast_loginを確認しましたが、時間は更新されていましたのでログインできている。
また、redirect先を'/api/'にした場合は、正しくapiに移動しましたので、書き方は間違っていないのかと思っています。

0 likes

3Answer

あくまで安易な推測なので解決するかは分かりませんが。。。

独自ミドルウェアの読み込み順を後ろにしてみはどうでしょうか?

sessionやcsrf関連が処理される前に、PasswordChangeViewが呼び出されて、未ログインとして認識されているのかなぁ?と思ったり。

私も正解が知りたいです!笑

1Like

Comments

  1. @shino_25

    Questioner

    場所を'django.contrib.sessions.middleware.SessionMiddleware'以降にずらすと、リダイレクトループになってしまいました。

自己解決しました。
views.pyでトップページにアクセスした際にパスワードの有効期限をチェックし、期限が超過しているなら/admin/password_change/にリダイレクトさせる。
ただし、このままでは管理画面内は移動できてしまう為、自作のミドルウェアに"/admin/にいるが/admin/password_change/でない"を追加すると、期限が切れている場合は、/admin/password_change/から移動できないようになりました。

password_middleware(自作のmiddleware);
if d1 > d2:
referer = request.environ.get('HTTP_REFERER')
if re.search('/admin/', referer) and not re.match('/admin/password_change/', request.path):
return redirect('/admin/password_change/')

1Like

非ログイン状態で /admin/password_change/ にアクセスすると
/admin/login/?next=/admin/password_change/ にリダイレクトされるようになってたりしませんか

0Like

Comments

  1. @shino_25

    Questioner

    試しにsuperuserでapiにリダイレクトさせると、"認証情報が含まれていません。"と表示されます。
    apiはDEFAULT_PERMISSION_CLASSESをIsAdminUserにしていますので、superuserでログインできていないということになりますが、データベースのlast_loginの日時は更新されています。ログインがきちんと出来ていない?ということでしょうか?
  2. ???

    ブラウザのシークレットモードとかで非ログイン状態でアクセスするとどうなるんでしょう
  3. @shino_25

    Questioner

    シークレットモードでも/admin/login/?next=/admin/password_change/へリダイレクトしました。

Your answer might help someone💌