diadia

興味があることをやってみる。自分のメモを残しておきます。

APIViewのpermission_classesはどんな仕組みで動いているのか

APIViewのプロパティは以下の通り。

# APIViewのプロパティ  

class APIView(View):

    # The following policies may be set at either globally, or per-view.
    renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES
    parser_classes = api_settings.DEFAULT_PARSER_CLASSES
    authentication_classes = api_settings.DEFAULT_AUTHENTICATION_CLASSES
    throttle_classes = api_settings.DEFAULT_THROTTLE_CLASSES
    permission_classes = api_settings.DEFAULT_PERMISSION_CLASSES #コイツ
    content_negotiation_class = api_settings.DEFAULT_CONTENT_NEGOTIATION_CLASS
    metadata_class = api_settings.DEFAULT_METADATA_CLASS
    versioning_class = api_settings.DEFAULT_VERSIONING_CLASS

    # Allow dependency injection of other settings to make testing easier.
    settings = api_settings

    schema = DefaultSchema()

次にpermission_classesを使っているメソッドは何があるか??
それはget_permissionsだった。このメソッドはpermisson_classをインスタンス化したリストを作成する役割である。

# https://github.com/encode/django-rest-framework/blob/3db88778893579e1d7609b584ef35409c8aa5a22/rest_framework/views.py#L274

    def get_permissions(self):
        """
        Instantiates and returns the list of permissions that this view requires.
        """
        return [permission() for permission in self.permission_classes]

じゃあ、このget_permissionsメソッドはどの文脈で使われているかということが次の疑問になる。
それは以下の通りcheck_permissionsとcheck_object_permissionsメソッドで使われていることが判明した。

    def check_permissions(self, request):
        """
        Check if the request should be permitted.
        Raises an appropriate exception if the request is not permitted.
        """
        for permission in self.get_permissions():
            if not permission.has_permission(request, self):
                self.permission_denied(
                    request,
                    message=getattr(permission, 'message', None),
                    code=getattr(permission, 'code', None)
                )

    def check_object_permissions(self, request, obj):
        """
        Check if the request should be permitted for a given object.
        Raises an appropriate exception if the request is not permitted.
        """
        for permission in self.get_permissions():
            if not permission.has_object_permission(request, self, obj):
                self.permission_denied(
                    request,
                    message=getattr(permission, 'message', None),
                    code=getattr(permission, 'code', None)
                )

こレラのメソッドはパーミッションがない場合にパーミッション拒否のメソッドが実行される。そういう意味で、permission.has_permission()メソッドがパーミッションのチェックとなっている。 このメソッドはpermissons.pyの各クラスメソッドに規定されている。

https://github.com/encode/django-rest-framework/blob/d635bc9c71b6cf84d137a68610ae2e628f8b62b3/rest_framework/permissions.py

django permissonでリソースを制御する

webアプリの場合、認証する->認可するの流れをたどる。 この認可されたパーミッションにしたがってリソースのアクセス制御を実施したい。

djangoの場合どうすれば実装できるか?

下準備

  1. Userオブジェクトにパーミッションを付加する
  2. Groupオブジェクトにパーミッションを付加する
  3. パーミッションを全く持たないUserオブジェクトをGroupオブジェクトに追加する

結果から言うと1のUserオブジェクトもアクセスでき、3のUserオブジェクトもアクセスできる。そしてパーミッションを全く付加していないsuperuserもリソースにアクセスできた。当然、パーミッションを追加していない普通のUserオブジェクトはリソースにアクセスできないのは言うまでもない。

下準備に関してはdjangoのあどみんからパーミッションを付与するか、コードベースで付与するか考えられる。コードベースでやる場合は以下を参考にすると良い.

Djangoのpermissionを付与するサンプルコード - diadia

実装する

# models.py リソース制御対象のリソースモデル

from django.db import models
from django.contrib.auth.models import User
# Create your models here.

class Articulo(models.Model):
    title = models.CharField(max_length=255)
    content = models.TextField()
    created_by = models.ForeignKey(User, on_delete=models.PROTECT)
    created_at = models.DateTimeField()

    def __str__(self):
        return self.title
# views.py
from django.shortcuts import render
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.views.generic import View
from .models import Articulo

class ArticulosListView(PermissionRequiredMixin, View):

    permission_required = ('articulos.view_articulo')

    def get(self, request):
        articulos_objects = Articulo.objects.all()
        context = {"articulos_objects": articulos_objects}
        return render(request, 'articulos/list.html', context)

これでリソース制御できる。 permission_requiredはPermissionRequiredMixinのプロパティである。値はstr型でも何らかのシーケンスで定める。

値はapp_label.view_小文字のモデルで定める。これがいわゆるリソースに対するパーミッションである。

https://docs.djangoproject.com/ja/3.0/topics/auth/default/#default-permissions

リソース操作 例 app_labelがhogesでHogeモデルの場合
読み取り hoges.view_hoge
作成 hogs.add_hoge
変更編集 hoges.change_hoge
削除 hoges.delete_hoge

djangoのコードがどうなっているか

https://github.com/django/django/blob/master/django/contrib/auth/mixins.py#L74

class PermissionRequiredMixin(AccessMixin):
    """Verify that the current user has all specified permissions."""
    permission_required = None

    def get_permission_required(self):
        """
        Override this method to override the permission_required attribute.
        Must return an iterable.
        """
        if self.permission_required is None:
            raise ImproperlyConfigured(
                '{0} is missing the permission_required attribute. Define {0}.permission_required, or override '
                '{0}.get_permission_required().'.format(self.__class__.__name__)
            )
        if isinstance(self.permission_required, str):
            perms = (self.permission_required,)
        else:
            perms = self.permission_required
        return perms

    def has_permission(self):
        """
        Override this method to customize the way permissions are checked.
        """
        perms = self.get_permission_required()
        return self.request.user.has_perms(perms)

    def dispatch(self, request, *args, **kwargs):
        if not self.has_permission():
            return self.handle_no_permission()
        return super().dispatch(request, *args, **kwargs)

classmethodのas_viewがdispatchを呼び出す事になっているが、このdispatchを呼び出す前にプロパティのパーミッションを取得(get_permission_required)して、 Userオブジェクトのメソッドであるhas_permでパーミッションがあるかどうかをチェックする(has_permission)。このフラグをmixinのdispatch内で判定し、Trueであれば、各Viewのget, postメソッドにつないでいく仕組みになる。

まだ、どうやってas_viewとdispatchの間にこのロジックを差し込む原因があるのか調べきれていない。おそらくas_view()になんか書いてあるのか???

参考資料

https://thinkami.hatenablog.com/entry/2016/02/03/062159

email認証する方法

一つはUserモデルを自分で作成する方法。
もう一つは認証バックエンドにemailで認証するバックエンドを実装して、settings.pyでそれを使う宣言する方法。

後者を最近試してできたので時間があれば詳しく書きたい。

要点だけまとめておく。

1.備え付けのdjango.contrib.auth.authenticate関数を使う
2.認証方法としてメールアドレス認証のバックエンドを作る
3.そのバックエンドを使う旨をsettings.pyで宣言する

リポジトリ

https://github.com/chiaki1990/authentication_backends_sample

1.備え付けのdjango.contrib.auth.authenticate関数を使って認証する

from django.shortcuts import render
from django.views.generic import View
from django.contrib.auth import authenticate, login 
# Create your views here.

class SignInView(View):

    def get(self, request):
        return render(request, 'observe_login/signin.html')

    def post(self, request):
        email = request.POST["email"]
        password = request.POST["password"]
        user_obj = authenticate(request=request, email=email, password=password)
        print("認証されればUserオブジェクト、失敗ならNoneの表示 : ",user_obj)
        if user_obj:
            login(request, user_obj)
        return render(request, 'observe_login/signin.html')

2.認証方法としてメールアドレス認証のバックエンドを作る

from django.conf import settings
from django.contrib.auth.backends import BaseBackend
from django.contrib.auth.hashers import check_password
from django.contrib.auth.models import User

# Emailで認証する方法を選択肢を増やす

class EmailAuthBackend(BaseBackend):

    def authenticate(self, request, email, password):
        print("EmailAuthBackendが呼び出されている")
        print(request, email, password)
        if email and password:
            try:
                user = User.objects.get(email=email)
            except User.DoesNotExist:
                return None
            else:
                if user.check_password(password) and self.user_can_authenticate(user):
                    print("認証成功!!!")
                    return user
        return None

    def user_can_authenticate(self, user):
        """
        Reject users with is_active=False. Custom user models that don't have
        that attribute are allowed.
        """
        is_active = getattr(user, 'is_active', None)
        return is_active or is_active is None


    def get_user(self, user_id):
        try:
            return User.objects.get(pk=user_id)
        except User.DoesNotExist:
            return None

3.そのバックエンドを使う旨をsettings.pyで宣言する

settings.pyにて

# https://docs.djangoproject.com/ja/3.1/ref/settings/#auth
# https://docs.djangoproject.com/ja/3.1/topics/auth/customizing/#specifying-authentication-backends

AUTHENTICATION_BACKENDS = [
    #'django.contrib.auth.backends.ModelBackend',
    'observe_login.utils.EmailAuthBackend'
    ]

pipenvを使ってみる

ドキュメント

Pipenvの基本的な使い方 — pipenv 2018.11.27.dev0 ドキュメント

前提

os ... macos
python... anacondaではなくbrewでインストールしたpython

インストール

brew install pipenv

仮想環境作る

pipenv --python 3.8
# 3.8でも3でよい。指定したものが環境で使われるpythonのバージョンになる

環境にライブラリを追加する

pipenv install django==3.0
# 普通仮想環境に入ってインストールするけど、pipenvであれば
# プロジェクトディレクトリでいることが必要なだけで仮想環境に入ることは要求されない。

仮想環境入る

pipenv shell

# プロジェクトのディレクトリの名前が仮想環境名

仮想環境から出る

deactivate 
exit

pipenvで作成したライブラリをrequirements.txtに出力する

pipenv run pip freeze  > requirements.txt 

Generate requirements.txt from Pipfile.lock · Issue #3493 · pypa/pipenv · GitHub

APIViewのauthentication_classesとpermission_classesについて分かったところまで。。。

書きかけ。。。

APIViewのオリジナルコード

django-rest-framework/views.py at master · encode/django-rest-framework · GitHub

# APIViewのプロパティ  

class APIView(View):

    # The following policies may be set at either globally, or per-view.
    renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES
    parser_classes = api_settings.DEFAULT_PARSER_CLASSES
    authentication_classes = api_settings.DEFAULT_AUTHENTICATION_CLASSES #コイツ
    throttle_classes = api_settings.DEFAULT_THROTTLE_CLASSES
    permission_classes = api_settings.DEFAULT_PERMISSION_CLASSES #コイツ
    content_negotiation_class = api_settings.DEFAULT_CONTENT_NEGOTIATION_CLASS
    metadata_class = api_settings.DEFAULT_METADATA_CLASS
    versioning_class = api_settings.DEFAULT_VERSIONING_CLASS

    # Allow dependency injection of other settings to make testing easier.
    settings = api_settings

    schema = DefaultSchema()

APIViewの継承時にauthentication_classesを設定すると、その認証が必要になり、またpermission_classesを設定するとその権限を持っているものしかAPIViewを起動することができなくなる。これはとてもわかりやすく使いやすいと思っていたが具体的にどんな仕組みで動いているのか理解できていなかったので少し調べてみる。

APIView継承のコード例




authentication_classesはどのように使われているか?

authentication_classesが使われているメソッドは何か?

# https://github.com/encode/django-rest-framework/blob/master/rest_framework/views.py#L268

    def get_authenticators(self):
        """
        Instantiates and returns the list of authenticators that this view can use.
        """
        return [auth() for auth in self.authentication_classes]

これのみである。このメソッドでautehtication_classのインスタンスのリストを作成する。

つぎにこのリストはどこで使われているかが次の疑問である。てことでこのメソッドがどこで使われているか見ると以下の2つのメソッドが見つかった。

  1. get_authenticate_header
  2. initialize_request
# https://github.com/encode/django-rest-framework/blob/master/rest_framework/views.py#L183

    def get_authenticate_header(self, request):
        """
        If a request is unauthenticated, determine the WWW-Authenticate
        header to use for 401 responses, if any.
        """
        authenticators = self.get_authenticators()
        if authenticators:
            return authenticators[0].authenticate_header(request)

適切な認証情報が与えられずに失敗した場合にどのような認証タイプを提供するかリクエストヘッダに提供するために使うメソッドだと考えられる。

# https://github.com/encode/django-rest-framework/blob/master/rest_framework/views.py#L385

    def initialize_request(self, request, *args, **kwargs):
        """
        Returns the initial request object.
        """
        parser_context = self.get_parser_context(request)

        return Request(
            request,
            parsers=self.get_parsers(),
            authenticators=self.get_authenticators(),
            negotiator=self.get_content_negotiator(),
            parser_context=parser_context
        )

このメソッドで使われているのは分かったけど、このメソッドはどこで使われているか??? それはdisatchメソッドである。

# https://github.com/encode/django-rest-framework/blob/master/rest_framework/views.py#L485

    def dispatch(self, request, *args, **kwargs):
        """
        `.dispatch()` is pretty much the same as Django's regular dispatch,
        but with extra hooks for startup, finalize, and exception handling.
        """
        self.args = args
        self.kwargs = kwargs
        request = self.initialize_request(request, *args, **kwargs)
        self.request = request
        self.headers = self.default_response_headers  # deprecate?

        try:
            self.initial(request, *args, **kwargs)

            # Get the appropriate handler method
            if request.method.lower() in self.http_method_names:
                handler = getattr(self, request.method.lower(),
                                  self.http_method_not_allowed)
            else:
                handler = self.http_method_not_allowed

            response = handler(request, *args, **kwargs)

このメソッドはdjangoのオリジナルのViewにas_viewメソッドが存在する。これによってdispatchメソッドが呼び出される。

このdispatchメソッド内のinitialize_requestメソッドが呼ばれ、requestオブジェクトが初期化される。またその後、initialメソッドがdispatchメソッド内で呼ばれる。
これが何なのか見てみると認証につながっていることが分かる。

# https://github.com/encode/django-rest-framework/blob/3db88778893579e1d7609b584ef35409c8aa5a22/rest_framework/views.py#L399


    def initial(self, request, *args, **kwargs):
        """
        Runs anything that needs to occur prior to calling the method handler.
        """
        self.format_kwarg = self.get_format_suffix(**kwargs)

        # Perform content negotiation and store the accepted info on the request
        neg = self.perform_content_negotiation(request)
        request.accepted_renderer, request.accepted_media_type = neg

        # Determine the API version, if versioning is in use.
        version, scheme = self.determine_version(request, *args, **kwargs)
        request.version, request.versioning_scheme = version, scheme

        # Ensure that the incoming request is permitted
        self.perform_authentication(request)
        self.check_permissions(request)
        self.check_throttles(request)

perform_authenticationメソッドにつながっている。
perform_authenticationメソッドでは、request.userを実行しているだけである。しかしここが肝でrequest.userのuserは@propertyがついているものでその内部は、認証を行っている。てことで、perform_authenticationとRequest,userメソッドを参照する。

# https://github.com/encode/django-rest-framework/blob/3db88778893579e1d7609b584ef35409c8aa5a22/rest_framework/views.py#L316

    def perform_authentication(self, request):
        """
        Perform authentication on the incoming request.
        Note that if you override this and simply 'pass', then authentication
        will instead be performed lazily, the first time either
        `request.user` or `request.auth` is accessed.
        """
        request.user
# https://github.com/encode/django-rest-framework/blob/3db88778893579e1d7609b584ef35409c8aa5a22/rest_framework/request.py#L140


class Request:
    """
    Wrapper allowing to enhance a standard `HttpRequest` instance.
    Kwargs:
        - request(HttpRequest). The original request instance.
        - parsers(list/tuple). The parsers to use for parsing the
          request content.
        - authenticators(list/tuple). The authenticators used to try
          authenticating the request's user.
    """

    def __init__(self, request, parsers=None, authenticators=None,
                 negotiator=None, parser_context=None):
        assert isinstance(request, HttpRequest), (
            'The `request` argument must be an instance of '
            '`django.http.HttpRequest`, not `{}.{}`.'
            .format(request.__class__.__module__, request.__class__.__name__)
        )

        self._request = request
        self.parsers = parsers or ()
        self.authenticators = authenticators or ()
        self.negotiator = negotiator or self._default_negotiator()
        self.parser_context = parser_context
        self._data = Empty
        self._files = Empty
        self._full_data = Empty
        self._content_type = Empty
        self._stream = Empty

        if self.parser_context is None:
            self.parser_context = {}
        self.parser_context['request'] = self
        self.parser_context['encoding'] = request.encoding or settings.DEFAULT_CHARSET

        force_user = getattr(request, '_force_auth_user', None)
        force_token = getattr(request, '_force_auth_token', None)
        if force_user is not None or force_token is not None:
            forced_auth = ForcedAuthentication(force_user, force_token)
            self.authenticators = (forced_auth,)

 ...省略

    @property
    def user(self):
        """
        Returns the user associated with the current request, as authenticated
        by the authentication classes provided to the request.
        """
        if not hasattr(self, '_user'):
            with wrap_attributeerrors():
                self._authenticate()
        return self._user
 
...省略

    def _authenticate(self):
        """
        Attempt to authenticate the request using each authentication instance
        in turn.
        """
        for authenticator in self.authenticators:
            try:
                user_auth_tuple = authenticator.authenticate(self)
            except exceptions.APIException:
                self._not_authenticated()
                raise

            if user_auth_tuple is not None:
                self._authenticator = authenticator
                self.user, self.auth = user_auth_tuple
                return

        self._not_authenticated()

user_auth_tupleは各authenticateクラスのauthenticateメソッドの戻り値である。 Basic, Session, Tokenでの認証が存在していて、おのおの戻り値は以下のようになっている。

認証クラス 認証クラスのauthenticateメソッドの戻り値(=user_auth_tuple) 該当コード
BasicAuthentication (user, None) https://github.com/encode/django-rest-framework/blob/3db88778893579e1d7609b584ef35409c8aa5a22/rest_framework/authentication.py#L53
SessionAuthentication (user, None) https://github.com/encode/django-rest-framework/blob/3db88778893579e1d7609b584ef35409c8aa5a22/rest_framework/authentication.py#L112
TokenAuthentication (token.user, token) https://github.com/encode/django-rest-framework/blob/3db88778893579e1d7609b584ef35409c8aa5a22/rest_framework/authentication.py#L151

そういうわけで認証が成功するとuserオブジェクトがrequestオブジェクトに格納される流れとなる。

permissonについては以下で。
https://torajirousan.hatenadiary.jp/entry/2021/01/03/223928

djangoログイン時にクッキーにセッションIDをセットする仕組みを見てみたのでメモ

まず認証

認証はユーザーが誰であるかを特定するものである。そのためクレデンシャル情報を用いて認証を行う。 クレデンシャル情報は種類がある。

  • ユーザーの記憶によるもの(something you know)
  • ユーザーが所有しているもの(something you have)
  • ユーザーの身体的な特徴に基づくもの (something you are)

下の方が一般的に認証強度が強いと言われている。

認証コード部分とdjango自体のコード

# views.py

from django.shortcuts import render, redirect
from django.views import View
from django.urls import reverse
from django.http import HttpResponse
from django.contrib.auth.models import User
from django.contrib.auth.hashers import make_password
from django.contrib.auth import authenticate, login

class SignInView(View):
    # csrftokenとcsrftokenmiddlewareの関係性について備忘
    # https: // stackoverflow.com/questions/48002861/whats-the-relationship-between-csrfmiddlewaretoken-and-csrftoken

    def get(self, request, *args, **kwargs):
        context = {"title": "ログイン"}
        # return HttpResponse("200")
        return render(request, 'observe_login/signin.html', context)

    def post(self, request, *args, **kwargs):
        # https://djangoproject.jp/doc/ja/1.0/topics/auth.html
        username = self.request.POST["username"]
        password = self.request.POST["password"]
        # print(username, password)
        user = authenticate(username=username, password=password)
        if user is not None:
            login(request, user)
            return HttpResponse("認証成功")
        else:
            return HttpResponse("認証失敗")

        return render(request, 'observe_login/signin.html')

上記のコードは結論から言うと、django.contrib.auth.init.authenticate関数で認証を行っている。

そしてdjango.contrib.auth.init.login関数でセッション開始し、ブラウザのクッキー部分にsessionIDをセットしている。

つまりauthenticate()でユーザーが誰であるか特定し、特定したユーザーをlogin()関数でセッション開始している。

authenticate()関数を見ていく。

#https://github.com/django/django/blob/98ad327864aed8df245fd19ea9d2743279e11643/django/contrib/auth/__init__.py


@sensitive_variables('credentials')
def authenticate(request=None, **credentials):
    """
    If the given credentials are valid, return a User object.
    """
    for backend, backend_path in _get_backends(return_tuples=True):
        backend_signature = inspect.signature(backend.authenticate)
        try:
            backend_signature.bind(request, **credentials)
        except TypeError:
            # This backend doesn't accept these credentials as arguments. Try the next one.
            continue
        try:
            user = backend.authenticate(request, **credentials)
        except PermissionDenied:
            # This backend says to stop in our tracks - this user should not be allowed in at all.
            break
        if user is None:
            continue
        # Annotate the user object with the path of the backend.
        user.backend = backend_path
        return user

    # The credentials supplied are invalid to all backends, fire signal
    user_login_failed.send(sender=__name__, credentials=_clean_credentials(credentials), request=request)

コメントからクレデンシャル情報が適正であればユーザーオブジェクトを返す関数であることがわかる。

backendはModelBackendインスタンスである。 そのauthenticate()メソッドを使っているわけだけど、その中身はcheck_password()を使って入力されたpasswordとUserにセットされているpasswordを比較して同値か検討してしいると思われる。check_password()はdjango.contrib.auth.models. AbstractUserのメソッドである。check_password()はreturnとしてdjango.contrib.auth.hashers.check_password()を呼び出し、その先でtrue,falseを返す。

#https://github.com/django/django/blob/98ad327864aed8df245fd19ea9d2743279e11643/django/contrib/auth/backends.py#L9

class ModelBackend(BaseBackend):
    """
    Authenticates against settings.AUTH_USER_MODEL.
    """

    def authenticate(self, request, username=None, password=None, **kwargs):
        if username is None:
            username = kwargs.get(UserModel.USERNAME_FIELD)
        if username is None or password is None:
            return
        try:
            user = UserModel._default_manager.get_by_natural_key(username)
        except UserModel.DoesNotExist:
            # Run the default password hasher once to reduce the timing
            # difference between an existing and a nonexistent user (#20760).
            UserModel().set_password(password)
        else:
            if user.check_password(password) and self.user_can_authenticate(user):
                return user

check_passwordについては続いてくので以下をチェックされたし。

https://github.com/django/django/blob/98ad327864aed8df245fd19ea9d2743279e11643/django/contrib/auth/hashers.py#L30

def check_password(password, encoded, setter=None, preferred='default'):
    """
    Return a boolean of whether the raw password matches the three
    part encoded digest.
    If setter is specified, it'll be called when you need to
    regenerate the password.
    """
    if password is None or not is_password_usable(encoded):
        return False

    preferred = get_hasher(preferred)
    try:
        hasher = identify_hasher(encoded)
    except ValueError:
        # encoded is gibberish or uses a hasher that's no longer installed.
        return False

    hasher_changed = hasher.algorithm != preferred.algorithm
    must_update = hasher_changed or preferred.must_update(encoded)
    is_correct = hasher.verify(password, encoded)

    # If the hasher didn't change (we don't protect against enumeration if it
    # does) and the password should get updated, try to close the timing gap
    # between the work factor of the current encoded password and the default
    # work factor.
    if not is_correct and not hasher_changed and must_update:
        hasher.harden_runtime(password, encoded)

    if setter and is_correct and must_update:
        setter(password)
    return is_correct

次にクッキーにsessionIDをセットする。どうやってdjangoがブラウザのCookieにsessionIDをセットするのかとても興味深かった。
結果は、django自体がブラウザを操作するのではなく、django自体はブラウザが解読可能なレスポンスを作成する。解読可能なレスポンスとしてヘッダが存在し、そのヘッダにsessionIDをSet-Cookieとしてセットしているのではと思った。ここはまだ定かではないので調査したい。

Cookieを操作することだけで言えばpythonの標準モジュールhttp.cookiesのモジュールにoutput_jsが存在してこれを使ってクッキーをセットしている可能性を考えられた。しかしながらdjangoでそのようなモジュールを使っている部分は見当たらなかったので、その辺はないのでしょう。

以下考え中。。。

まずmiddlewareのdjango.contrib.sessions.middleware.SessionMiddlewareがsessionIDをクッキーにセットしている。
セットすると言っても特別にブラウザ操作するコードを準備していると思っていたわけだけれども、pythonの標準モジュールであるhttp.cookiesモジュールを使っているだけだった。(クラスとしてSimpleCookieクラスをdjangoは作っている。)

21.23. http.cookies --- HTTPの状態管理 — Python 3.6.12 ドキュメント

クッキーのsessionIDがセットされる流れをざっと見てみる。

まずどうやってミドルウエアが呼ばれるかという疑問である。
ドキュメントのミドルウェアは以下の通り。

ミドルウェアは、Django のリクエスト/レスポンス処理にフックを加えるためのフレームワークです。これは、Django の入力あるいは出力をグローバルに置き換えるための、軽量で低レベルの「プラグイン」システムです。

ミドルウェア (Middleware) | Django ドキュメント | Django

要するに各リクエスト、レスポンスに対しミドルウェアが働いているってこと。 てことでCookieにsessionIDをセットする場面はレスポンスに対してなのでそこを中心に見ていけば良いとわかる。
またセッションの開始はユーザーがログイン時であり、この辺を見てみるとわかるかもしれない。
てことでlogin関数を詳しく見てみる。




loginが成功するとCookieにはsessionidというキーがセットされる。これは、django/conf/global_settings.pyのSESSION_COOKIE_NAMEの値がsessionidだからであることは分かった。

みんなのPython Webアプリ編 - レスポンスの処理 | TRIVIAL TECHNOLOGIES 4 @ats のイクメン日記

上記の記事ではwebアプリのレスポンスを0から作ることをやっていた。概要としてはヘッダーとボディを作って文字列かしたらレスポンスになるような感じだった。てことで"クッキーをセットする"=レスポンスにSet-Cookieをセットする それは単純に文字列で書けば良い。

authtokenをrestframeworkだけで実装して観察してみる

昔Token認可で実装したことがあるけど、rest-authを使って実装したのでライブラリを使わないで試してみる。

ドキュメント

https://www.django-rest-framework.org/api-guide/authentication/#tokenauthentication

settings.py

まず、Tokenで認可させたい場合Tokenテーブルが必要なんだけど、これはrestframework.authtoken.modelsにTokenモデルがある。 したがってINSTALLED_APPSにrestframework.authtokenを加える必要がある。

# settings.pyにて
# Application definition
DJANGO_CONTRIB_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    # 'django.contrib.sessions',  # ステートレス通信のため削除
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

THIRD_PARTY_APPS = [
    'rest_framework',
    'rest_framework.authtoken'  # Tokenテーブルを作成するためのアプリか? JWTを使うときも必要か?
]

ORIGINAL_APPS = [
    "observe_login"
]
INSTALLED_APPS = DJANGO_CONTRIB_APPS + THIRD_PARTY_APPS + ORIGINAL_APPS


#  ステートレス通信のため
#  'django.contrib.sessions.middleware.SessionMiddleware', 
# は不要だと思っていたが、削除するとエラーが出てしまったため残しておく。
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',
]

https://github.com/encode/django-rest-framework/blob/master/rest_framework/authtoken/models.py

views.py

class SignInAPIView(APIView):
    def get(self, request, *args, **kwargs):
        # ログインデータテンプレートを表示する
        return Response({'username': '', 'password': ''})

    def post(self, request, *args, **kwargs):
        ''' ユーザー認証した場合にはtokenを返す '''
        username = request.data['username']
        password = request.data['password']
        user_obj = authenticate(username=username, password=password)
        if user_obj is None:
            return Response({'result': 'credential is not valid'})
        try:
            # クライアントにセットするためtokenを出力する
            token = Token.objects.get(user=user_obj.id)
            return Response({"result": "success", 'tokenKey': token.key})
        except:
            return Response({'result': 'token fail'})


class SignUpAPIView(APIView):
    def get(self, request, *args, **kwargs):
        # 送信データテンプレートを表示する
        return Response(data={'username': '', 'password': ''}, status=200)

    # User登録と同時に当該UserにTokenをセット、発行する
    def post(self, request, *args, **kwargs):
        serializer = UserModelSerializer(data=request.data)
        if not serializer.is_valid():
            return Response({"result": "fail"})
        username = serializer.validated_data['username']
        password = serializer.validated_data["password"]
        user_obj = User.objects.create_user(
            username=username, password=password)
        token = Token.objects.create(user=user_obj)
        print("TOKEN KEY : ", token.key)
        # クライアントにセットするためtokenを出力する
        return Response({"result": "success", "tokenKey": token.key})

受け取ったtokenを使う

cookieやlocalStorageを操作するには? - diadia

cookieやlocalStorageを操作するには?

アンドロイドアプリを作る場合はアプリのストレージ領域に暗号化したTokenを保管し、通信を行うときだけそれを引っ張り出してリクエストヘッダにセットして通信を行っていた。クライアントがブラウザの場合にはブラウザに保存するのでどうやって保存して取り出すのか気になる。

てことでトークンやクッキーをブラウザにセットする方法をすこし調査してみる。

また、djangoの場合はどうやってcookieにセットしているかも分かったら嬉しいけどそこまで調べきれるか、興味が続くところまでやってみる。

ブラウザにデータを保存する場合には、3種類の選択肢が存在する。

  • cookie
  • sessionStorage
  • localStorage
名称 有効期限 データ量 サーバへのデータ送信 memo
cookie 指定期限まで 4KB リクエスト毎に自動送信
session storage ウィンドウ・タブを閉じるまで 5MB データ利用時のみ web storageの一種
localStorage 永続的に有効 5MB データ利用時のみ web storageの一種

1. javascriptcookieを操作する方法

1 データの保存 document.cookieで設定する

document.cookie = "key=value";

2 データの取得 document.cookieで取得する

let cookies_get = document.cookie;

3 データの削除 document.cookiecookieを削除する

// max-age=0を設定する
document.cookie = "key=; max-age=0"
javascriptcookieを操作する方法を紹介した資料

cookieをjavascriptで設定、取得、削除する簡単な方法

2. javascriptでsessionStorageを操作する方法

1 データの保存

//下記3行は全て同じ意味です。
sessionStorage.key = 'value';
sessionStorage['key'] = 'value';
sessionStorage.setItem('key', 'value');

2 データの取得

sessionStorage.getItem('key')

3 データの削除

// キーを指定して削除
sessionStorage.removeItem('key')
// 初期化
sessionStorage.clear()
sessionStorageの使い方を説明した資料

sessionStorageをつかってみる - Qiita

3. javascriptでlocalStorageを操作する方法

1 データの保存 localStorage.setItem()を使う。

localStorage.setItem('キー', '値');

2 データの取得 データの取得はlocalStorage.getItem()を使う。

localStorage.getItem('キー');

3 データの削除

// キーを指定して削除
localStorage.removeItem('キー');
// 初期化
localStorage.clear()
localStorageの使い方を説明した資料

https://www.granfairs.com/blog/staff/local-storage-01:titlle

djangoのキャッシュを使ったセッションの設定

ドキュメント:

https://docs.djangoproject.com/ja/3.1/topics/http/sessions/#using-cached-sessions

https://docs.djangoproject.com/ja/3.1/topics/cache/#django-s-cache-framework

手順

1 . 概要

今回memcachedにデータを保存するセッション方式の実装を試してみた。
手順としてはdjangoのキャッシュフレームワークを利用する準備を行い、その後セッションエンジンにキャッシュを使う旨を宣言する。
そして実際にキャッシュのソフトウエアをローカルで起動する。 するとキャッシュをセッションデータの保存先にできる。

2 . キャッシュフレームワークmemcachedを使う宣言をする

settings.pyにキャッシュフレームワークの設定を書く。

# settings.pyにて
CACHES = {
    'default': {
        'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
        'LOCATION': '127.0.0.1:11211',
    }
}

一言:キャッシュの設定はCACHES={}で設定する。キャッシュの種類は、memcashed, databaseに保存するcache, fileに保存するcacheとかいろいろ種類あり。

3 . セッションデータをキャッシュに保存する宣言をする
# settings.pyにてセッションエンジンをキャッシュ使用を宣言する
SESSION_ENGINE = 'django.contrib.sessions.backends.cache'
4 . macで動かす場合brew services start memcashedを実行する

memcachedはソフトウエアであることが分かった。てことで環境にmemcachedが存在していなければまずそれをインストールするところから、あればそれを起動する。起動していない状態でdjangoアプリを実行するとsessionがうまく働かずエラーが起きてしまう。

brew install memcashed

brew services start memcached

memcachedについて:

memcached - Wikipedia

第1回 memcachedの基本:memcachedを知り尽くす|gihyo.jp … 技術評論社

補足

  • installed_appsから'django.contrib.sessions'を削除してもそのまま動き続けるのかという疑問??
    => 何ごともなく動いた。クライアントのクッキーにも登録されていることを確認した。
    上記の結果から、セッションデータをdatabaseに登録する場合に限ってdjango.contrib.sessionsを使うという仮説を立てる。

  • MIDDLEWAREからSessionMiddlewareを削除したらそのまま動くのかという疑問??
    => MIDDLEWAREからSessionMiddlewareを削除したら以下のエラーが生じた。したがってmemcachedでセッションを使っている場合でもSessionMiddlewareは必要という結論になった。

ERRORS:
?: (admin.E410) 'django.contrib.sessions.middleware.SessionMiddleware' must be in MIDDLEWARE in order to use the admin application.

☆調査できるならmemcachedの内部構造をユーザーの行動と紐付けて観察したい。

参考資料

memcachedを使う場合にはbrew services start memcachedが必要であることを示す資料

Djangoのキャッシュ機能を使って画面表示をもっと高速化する Memcached編 - Qiita

sessionの使い方全容をスッキリまとめた資料

【Django】Sessionの使い方(基本編) | idealive tech blog

sessionデータの保存をどこにするか示す例

Djangoのセッション設定 - 知的好奇心

vscode 使い方メモ

ショートカットキーを使ってエディタとターミナルを移動する設定

https://torajirousan.hatenadiary.jp/entry/2021/01/13/170804

デバッグモード

最近デバッグモードの使い方が分かってきた。 自宅でも使えるように設定する際に調べたことを記録しておく。

自分が成功したのは以下の通り。 必要なのは.vscodeディレクトリにlaunch.jsonとsettings.jsonを準備することである。

  1. settings.jsonにはデバッグ時の仮想環境を設定する。
  2. launch.jsonにはデバッグ起動時に行うコマンドやそのコマンドを行うディレクトリの設定等を記述する。
// settings.json

{
    "python.pythonPath": "/Users/chiaki/opt/anaconda3/envs/try_typescript/bin/python3.8"
}

envs以下にあるtry_typescriptは自分が作成した仮想環境である。

# こんな感じで仮想環境を作成した
conda create -n try_typescript
# この仮想環境でpythonシェルを起動するには以下のコマンドが必要になる。
/Users/chiaki/opt/anaconda3/envs/try_typescript/bin/python3.8

デバッグモードで必要な仮想環境の起動とは、いわゆるsource activate みたいなことではなく、pythonのシェル状態であることが分かった。
したがって/Users/chiaki/opt/anaconda3/envs/try_typescript/bin/python3.8のようなことがsettings.jsonに書く必要があると分かった。

具体的な手順 (VSCodeに仮想環境のパスを設定)

ワークスペースのみに適用する場合、、、

code -> 基本設定 -> 設定 を選択。 ワークスペースの設定タブを選択し、設定の検索に python.pythonPathを入力します。
/Users/chiaki/opt/anaconda3/envs/try_typescript/bin/python3.8を設定。 VSCodeを終了し、再度VSCodeを開き、デバッグ実行すると、指定した仮想環境でプログラムが実行されました。

{
    // IntelliSense を使用して利用可能な属性を学べます。
    // 既存の属性の説明をホバーして表示します。
    // 詳細情報は次を確認してください: https://go.microsoft.com/fwlink/?linkid=830387

    "version": "0.2.0",
    "configurations": [
        
        {
            "name": "Python: Django",
            "type": "python",
            "request": "launch",
            "program": "${workspaceFolder}/database_session_sample/manage.py",
            "args": ["runserver"],
            "django": true,
     
        }
    ]
}

launch.jsonにおいては以下を参照。

VS CodeでPythonコードのデバッグも楽々!!:Visual Studio Codeで始めるPythonプログラミング(4/4 ページ) - @IT

エクスプローラービューに表示するファイルを制御

vscode|エクスプローラービューに表示するファイルを制御したい - diadia

インテリセンスの設定

djangoのインテリセンスを有効化する - diadia

コマンドラインから起動する

ドキュメント:

https://code.visualstudio.com/docs/setup/mac#_launching-from-the-command-line

VSCODE pipenvの環境からデバッグを実行したいときには

VSCODE pipenvの環境からデバッグを実行したいときには - diadia

vscodeの機能

開発をしやすくなるための環境ってことだけど

自分が使えてる機能はこれくらいしかない

  • インテリセンス
  • コード検索、置換、ジャンプ機能
  • cligui化(git, docker)
  • デバッグ

自分が使ってる(認識してる)拡張機能

自分が使用してる拡張機能を把握したい場合は、 サイドバーの拡張機能を選択。 上川にあるフィルタのマークをクリック。 インストール済みの拡張機能の表示を選ぶ。これでインストール済みの拡張機能すべて表示される。

marketplace.visualstudio.com

marketplace.visualstudio.com

marketplace.visualstudio.com

marketplace.visualstudio.com

marketplace.visualstudio.com

marketplace.visualstudio.com

vuexのメモ

コンポーネント間のデータの受け渡しについて。

コンポーネント間のデータの受け渡しの方法はいくつか方法があることがわかった。
props, emitを使った方法。 vuexを使った方法。

vuexを使った方法を考える。

コンポーネント間の通信:もうコンポーネントじゃない。。。

vuex にはgetterってのが存在している。これはなにか??? vue.jsの算出プロパティに当たるもの。
https://vuex.vuejs.org/ja/guide/getters.html

親から子に値を受け渡すprops

親ではテンプレートに属性を書く。この時ケバブケースで書くことになる。

oya.vue

<template>
<div>
<s-template s-num='number'></s-template>
</div>
</template>

<script>
import Stemplate from '.STemplate.vue'
export default{
    data: {
        number: 14
    },
    comonents : {'s-template': STemplate}
}
</script>

STemplate.vue

<template>
<div >
{{ s-num *4 }}
</div>
</template>
<script>
export default{
    props: ["s-num"]
}
</script>

今更だけれどもコンポーネントにはelは必要ない事がわかった。

子から親にデータを渡すemit

oya.vue

<template>
<div>
<p v-on:emit_event_name='number = $event'></p>
<s-template s-num='number'></s-template>
</div>
</template>

<script>
import Stemplate from '.STemplate.vue'
export default{
    data: {
        number: 14
    },
    comonents : {'s-template': STemplate}
}
</script>

STemplate.vue

<template>
<div >
<p>s-template</p>
</div>
</template>

<script>
export default{
    data: {
        number: 5
    }
    methods: {
        send_data(){
            this.$emit('emit_event_name', number * 4)
        }
    }
}
</script>

コンポーネント間のデータやり取り

まずコンポーネントにはマウントするってことがないので、elやmountメソッドを使わない。 そのようなコンポーネント間のデータのやり取りにおいて共通点があったのでイメージしやすい形としてメモしておく。 #####構造 データを送信する側とデータを受け取る側にコードを書く。 親コンポーネントから子コンポーネントにデータを渡す場合には、親ではtemplateタグ内の送り先子コンポーネントタグの属性に送るデータの名前と属性値にデータを書く。
子ではpropsに親で定めたデータの名前を加えておく。

コンポーネントから親コンポーネントにデータを渡す場合には、親ではtemplateタグ内のデータ受取先である子コンポーネントタグの属性としてv-onを使い、その引数にイベント名を書く。そして属性値に$eventを書くが、この値が子から送られたデータである。 子では、methods等の関数内にthis.$emit()を書く。emit内には第1引数にイベント名を書き、第2引数に送りたいデータを書く。

つまり親ではtemplateタグ内の子コンポーネントタグをいじるだけでいいし、子ではscriptタグ内のpropsかmethods内の関数内でthis.$emit()を使うという形式でデータ交換ができるとイメージすればよい。

チーム開発で使うgitについて

プログラミングを始めてあと少しで3年になりますが、がっつり開発をやっている会社につとめるのは今回が初めてでgitによるチーム開発について分かったことを少しまとめる。

ブランチの扱い

自分の勤める環境では、master, dev, featureというブランチが存在しており、masterはデプロイ用の製品コードでdevをマージする。
で、dev自体はmasterのバージョンを上げるために存在するのだけれどもこいつ自体を直接弄って開発することはない。devリポジトリからブランチを切って機能追加し、devが切ったブランチをマージしていく。そのdevからブランチを切ったのがfeatureといブランチ名である。

機能追加時に開発者は何をするべきか(概要)

まず機能追加のためにdevからブランチを切る。具体的にはdevからブランチを切ってfeature/hogeの機能追加のような名前をつける。
そしてそのブランチで開発を行う。
それで機能を開発したらgit のaddやcommitしていくけれど、この時にコマンドを使うよりはvscodeやpycharmのツールで変更点を把握する方が確認しやすいし、すごい効率的だと感じた。 で、コミットしたらgitのプッシュを行い、その後リモートリポジトリにてプルリクエストを作成して終了する。これが全体の流れである。

何をするべきか(具体的な手順)

1 ブランチを切る

# まずブランチのベースを確認する 自分のブランチがdevかどうか確認する
$ git branch

# devじゃなかった場合devに変更する
$ git checkout dev

# devからブランチを切る
$ git checkout -b feature/hoge機能追加

2 開発していく 特にgitで考えることはない

3 git add, commit

# ここはvscodeのようなツールを使って変更点を確認しながらaddやcommitする方がよい

4 git pushを行う

# git push
$ git push origin HEAD
# HEADは自分の作業しているブランチを指し、プッシュ先をリモートリポジトリの同一名ブランチにプッシュされる。同一名ブランチが存在していれば、リモートリポジトリの当該ブランチが更新される。当該ブランチが存在していなければ新たにブランチが作成される仕組みになっているらしい。

5 プルリクエスト作成 このあとリモートリポジトリでプルリクエストを作成して終了。

javascriptのasync awaitについてメモ

typescriptをやってPromiseの存在を知った。がPromiseという概念が未だに分からない。。。

少し触ってみて多少のasyc awaitを使う場合と使わない場合で何か異なることを発見したので備忘録としてメモしておく。

async await を使わない場合

private uploadImage(event: Event): void{
    const url = 'http://localhost:8000/myapp/image/'
    const sss = axios.get(url);
    console.warn(typeof (sss));
    console.warn(sss);
// 結果
object
Promise {<pending>}
//ちなみにPromiseの内部
__proto__: Promise
[[PromiseState]]: "fulfilled"
[[PromiseResult]]: Object

で、このPromiseResultの内部にdataプロパティとかを有するオブジェクトが含まれていた。

//その内部
config: {url: "http://localhost:8000/myapp/image/", method: "get", headers: {…}, transformRequest: ...
data: {result: "success"}
headers: {content-length: "20", content-type: "application/json"} 
request: XMLHttpRequest {readyState: 4, timeout: 0, withCredentials: false, upload: ...
status: 200
statusText: "OK"
__proto__: Object

重要なのはPromiseにPromiseResultが含まれていて、その内部にconfig, data, headers等が含まれている事実だ。

async awaitを使う場合

ちなみにasync awaitは何なのかはよく分かっていないが、使い方のイメージは理解している。関数名またはメソッド名にasyncを使い、通信を行う時にawaitを付帯して通信するコードを書く。

private async uploadImage(event: Event): void{
    const url = 'http://localhost:8000/myapp/image/'
    const sss = await axios.get(url);
    console.warn(typeof (sss));
    console.warn(sss);
//結果
object
{data: {…}, status: 200, statusText: "OK", headers: {…}, config: {…}, …}

typeはobjectと表示されるが、2つめのコンソールに表示されるのはおそらくPromiseResultであると思われる。中身が同じであるからそうだろう。

つまり?

async awaitを使わないと通信を行った際に返される値がPromise型を受け取り、PromiseResultを引っ張ってくるためにthen()等を使っていた。
しかしながらasync awaitを使うとPromiseResultが返される。したがってそのままdataプロパティを使ってレスポンスの内容を引っ張り上げる事ができる。