본문 바로가기

web/Django_rest_framework

JWT에서 Django request.user 까지의 여정

Django에서 request의 유저를 알기 위해선 request.user를 통해 알 수 있다.

Django에서 지원하는 session 방식의 로그인 / rest_framework에서 지원하는 JWT 등 로그인을 하면 request.user의 정보를 가져올 수 있다.

 

이번 글은 request.user에 정보가 들어가기까지 어떤 동작을 하는지 살펴보는 글입니다.

django == 2.1.2 // djangorestframework == 3.9.0 에서 진행하였습니다. 해당 버전에 따라 소스 코드가 달라질 수 있습니다.

 

 

1 번은 일반 로그인 방식이며, 2번은 JWT 방식이다.

1번 일반 로그인 방식은 먼저 cookie에 있는 세션값을 request.session에 저장하고 session 값을 통해 유저 정보를 찾습니다.

2번 JWT 방식은 header의 token값을 통해 유저 정보를 찾습니다.

 

 

1. request.session 저장 부분

1.1 django 로그인 사용일 경우 (이미 로그인이 되어 있어 session값을 넘길 경우 )

middleware → sessionStore 에서 request._session 값을 세팅을 합니다. 아래 이미지는 session값을 통해 _auth_user_id를 세팅한 결과입니다.

 

 

1. 먼저 process_request()에서 request header 부분에 cookie에 저장된 session값을 가져옵니다.

2. session_key 값을 파라미터로 SessionStore() 를 실행합니다.

1
2
3
4
5
6
# django.contrib.sessions.middleware.SessionMiddleware.process_request
 
def process_request(self, request):
      session_key = request.COOKIES.get(settings.SESSION_COOKIE_NAME) # request.COOKIES.get(sessionid)
      request.session = self.SessionStore(session_key) # django.contrib.sessions.backends.db.SessionStore
 
cs
 

3. SessionStore() 는 SessionBase를 상속합니다.

1
2
3
4
5
6
# django.contrib.sessions.middleware.SessionMiddleware.process_request
 
def process_request(self, request):
      session_key = request.COOKIES.get(settings.SESSION_COOKIE_NAME) # request.COOKIES.get(sessionid)
      request.session = self.SessionStore(session_key) # django.contrib.sessions.backends.db.SessionStore
 
cs

 

4. SessionBase에는 property가 설정되어 있습니다. 처음 이미지에 보았던 _session입니다.

_session값을 설정하기 위해 load()를 호출하는데 이 함수는 SessionStore()에 정의되어 있습니다.

1
2
3
4
5
6
7
8
#django.contrib.sessions.backends.base.SessionBase
_session = property(_get_session) # get property
 
def _get_session(self, no_load=False):
            ......
    self._session_cache = self.load() # <- SessionStore load() 
    return self._session_cache
 
cs

 

5. load()에서는 get_session_from_db()를 호출합니다.

_get_session_from_db()에서는 session_key를 이용해서 디비 값을 가져옵니다.

해당 모델의 테이블은 django_session테이블입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# django.contrib.sessions.backends.db.SessionStore
def load(self):
    s = self._get_session_from_db()
    return self.decode(s.session_data) if s else {}
 
def _get_session_from_db(self):
    try:
        return self.model.objects.get(
            session_key=self.session_key,
            expire_date__gt=timezone.now()
        )
    except (self.model.DoesNotExist, SuspiciousOperation) as e:
        if isinstance(e, SuspiciousOperation):
            logger = logging.getLogger('django.security.%s' % e.__class__.__name__)
            logger.warning(str(e))
      self._session_key = None
 
cs

 

6. session_key로 session_data를 가져옵니다.

 

 

7. 위의 5번 load()에서 6번에서 가져온 session_data를 decode() 함수의 파라미터로 호출합니다.

signing 모듈은 hmac의 암호화로 base64 압축 JSON 문자열을 반환하는 함수이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# django.contrib.sessions.backends.base.SessionBase.decode
def decode(self, session_data):
    try:
        return signing.loads(session_data, salt=self.key_salt, serializer=self.serializer)
    except signing.BadSignature:
        try:
            return self._legacy_decode(session_data)
        except Exception:
            logger = logging.getLogger('django.security.SuspiciousSession')
            logger.warning('Session data corrupted')
            return {}
    except Exception:
        return self._legacy_decode(session_data)
 
cs

 

7-1. 아래는 signing의 암호화 함수 dumps(), 복호화 함수 loads()의 예제이다.

salt의 기본값은 'django.core.signing'으로 되어 있다.

1
2
3
4
5
6
7
# django.core.signing.dumps
 
>> signing.dumps("hello")
Out[43]: 'ImhlbGxvIg:1ldMLE:d6TJOi5Q8lnDAzAHSgYxI8kjPijAdkh2FDadza46nbc'
>> signing.loads('ImhlbGxvIg:1ldMLE:d6TJOi5Q8lnDAzAHSgYxI8kjPijAdkh2FDadza46nbc')
Out[44]: 'hello'
 
cs

 

8. 6번에서 얻은 session_data 값은 signing를 통해 user_id를 얻을 수 있습니다.

최종적으로 request._session에 user_id와 hash 값을 저장합니다.

1
2
3
4
5
#request.session.session._session
{'_auth_user_id''111',
 '_auth_user_backend''django.contrib.auth.backends.ModelBackend',
 '_auth_user_hash''e779c33fcdc4fc1479ca7105f9f1fdcc4a512345df2'}
 
cs

 

 


 

1.2 login → session을 통한 request.user 저장 부분

 

1. 미들웨어 AuthenticationMiddleware에서 get_user(request) 실행 후 request.user로 유저 객체 설정합니다.

1
2
3
4
5
6
7
8
9
10
11
# django.contrib.auth.middleware.AuthenticationMiddleware
class AuthenticationMiddleware(MiddlewareMixin):
    def process_request(self, request):
        assert hasattr(request, 'session'), (
            "The Django authentication middleware requires session middleware "
            "to be installed. Edit your MIDDLEWARE setting to insert "
            "'django.contrib.sessions.middleware.SessionMiddleware' before "
            "'django.contrib.auth.middleware.AuthenticationMiddleware'."
        )
        request.user = SimpleLazyObject(lambda: get_user(request))
 
cs

 

2. get_user()는 안의 동작 방식입니다.

_get_user_session_key(request)에서는 앞써 session에 설정한 _auth_user_id의 값을 가져와서 user_id로 세팅합니다.

backend.get_user(user_id)를 통해 유저를 가져옵니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# django.contrib.auth.get_user
def get_user(request):
    try:
        user_id = _get_user_session_key(request) # session에서 유저 아이디 get
            ........
            ........
        backend = load_backend(backend_path) <- 유저 디비 
        user = backend.get_user(user_id)  <- 아래 코드 
        # Verify the session
        if hasattr(user, 'get_session_auth_hash'):
            session_hash_verified = session_hash and constant_time_compare(
                session_hash,
                user.get_session_auth_hash()
            )
            ........
 
cs

 

3. 위의 backend는 ModelBackend 클래스로 auth 관련 모든 함수 관리합니다. ( 유저 검색, 권한 체크 등 )

여기에서 get_user(user_id)는 유저 모델에서 해당 pk에 해당하는 유저 정보를 리턴합니다.

1
2
3
4
5
6
7
8
9
# django.contrib.auth.backends.ModelBackend
class ModelBackend(BaseBackend):
        def get_user(self, user_id):
            try:
                user = UserModel._default_manager.get(pk=user_id)
            except UserModel.DoesNotExist:
                return None
            return user if self.user_can_authenticate(user) else None
 
cs

 

 

4. 결국 request에 user 정보가 들어가게 됩니다.

 

 


 

2. JWT를 이용한 사용자 request.user 세팅

미들웨어는 settings에 선언된 순서대로 동작하게 됩니다.

위의 session // auth 미들웨어를 먼저 실행 후 request.user에는 AnonymousUser로 세팅됩니다.

JWT를 이용하면 cookie값에 session데이터가 없기 때문에 알 수 없는 유저(로그인하지 않은 유저)로 판단하고 request.user에 AnonymousUser 값이 들어갑니다.

먼저 django → rest_freamwork 연동 부분입니다.

1. django의 request의 미들웨어 → view 미들웨어(cors, csrf 미들웨어) → make_view_atomic을 통한 → rest_framework를 view 부분을 실행합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# django.core.handlers.base.BaseHandler.resolve_request
def resolve_request(self, request):
    resolver_match = resolver.resolve(request.path_info) # <- 여기에서 url 주소에 따라 어떤 view 를 참조해야 되는지 리턴 합니다. 
    request.resolver_match = resolver_match
    return resolver_match
 
def make_view_atomic(self, view):
    non_atomic_requests = getattr(view, '_non_atomic_requests', set())
    for db in connections.all():
        if db.settings_dict['ATOMIC_REQUESTS'and db.alias not in non_atomic_requests:
            if asyncio.iscoroutinefunction(view):
                raise RuntimeError(
                    'You cannot use ATOMIC_REQUESTS with async views.'
                )
            view = transaction.atomic(using=db.alias)(view)
    return view
 
cs

 

2. rest_framework → 뷰

url을 통해 동작해야 하는 view를 통해 해당 view.py가 동작합니다. 해당 부분은 rest_framework를 상속받았기 때문에 rest_framework의 dispatch()가 동작합니다.

initialize_request(request)를 통해 request를 초기화하는데 이때 authenticators의 파라미터로 settings에 설정한 DEFAULT_AUTHENTICATION_CLASS:{}의 미들웨어가 실행됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# rest_framework.views.APIView.dispatch
def dispatch(self, request, *args, **kwargs):
        ....
    request = self.initialize_request(request, *args, **kwargs)
        ....
 
# rest_framework.views.APIView.initialize_request
def initialize_request(self, request, *args, **kwargs):
    return Request(
        request,
        parsers=self.get_parsers(),
        authenticators=self.get_authenticators(), # <- setting.py에 설정한 auth 동작 
        negotiator=self.get_content_negotiator(),
        parser_context=parser_context
    )
 
cs

저의 경우 settings에 세 개의 미들웨어를 설정하였습니다. 이 중에서 TokenAuthentication()이 JWT를 담당하는 미들웨어입니다.

 

 

 

3. 위에서 설정된 rest_framework의 auth 미들웨어가 for문을 통해 하나씩 실행합니다.

1
2
3
4
5
6
7
8
9
# rest_framework.views.APIView.get_authenticators
def get_authenticators(self):
      return [auth() for auth in self.authentication_classes]
"""
[rest_framework.authentication.BasicAuthentication,
 rest_framework.authentication.SessionAuthentication,
 rest_framework.authentication.TokenAuthentication]
"""
 
cs

 

 

4. 해당 auth 미들웨어에서 user를 찾아내면 바로 return 합니다.

현재 JWT token 방식을 사용하므로, TokenAuthentication에서 user를 찾아냅니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# rest_framework.request.Request._authenticate
def _authenticate(self):
    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
 
cs

 

5. TokenAuthentication 미들웨어에서는 먼저 header의 token을 가져옵니다. 

1
2
3
4
5
6
7
# rest_framework.authentication.get_authorization_header
def get_authorization_header(request):
    auth = request.META.get('HTTP_AUTHORIZATION', b'')
    if isinstance(auth, str):
        auth = auth.encode(HTTP_HEADER_ENCODING)
    return auth
 
cs

 

6. authenticate_credentials(token) 호출로 유저 정보를 리턴합니다.

self.get_model()에서 바라보고 있는 테이블은 auth_token로 'user' 테이블과 조인(select_related)을 걸어서 key 값으로 해당 토큰 값으로 유저 정보를 가져올 수 있다.

1
2
3
4
5
6
7
8
9
10
# rest_framework.authentication.TokenAuthentication.authenticate_credentials
def authenticate_credentials(self, key):
    model = self.get_model() # <- rest_framework.authtoken.models.Token
    try:
        token = model.objects.select_related('user').get(key=key)
    except model.DoesNotExist:
        raise exceptions.AuthenticationFailed(_('Invalid token.'))
        ......
    return (token.user, token)
 
cs

 

 

7. auth_token 테이블은 다음과 같습니다. 

 

이로써 JWT 사용 시 토큰에서 request.user가 어떻게 값이 세팅되는지 확인해 봤습니다.

이번 과정을 통해서 django 내부에서 미들웨어가 어떻게 동작하는지 살펴볼 수 있는 좋은 기회였습니다. 단순히 소스를 따라가시지 마시고, 꼭 디버깅을 통해서 한번 실습해 보시길 바랍니다.

 

 

 

 

번외

마지막으로 http 요청 시 파라미터(JWT의 token 등)와 헤더가 request의 어느 부분으로 들어가는지가 궁금했습니다.

1. django에 요청시 WSGI (비동기 호출인 asgi의 경우 달라질 수 있습니다. )에서 요청이 들어오면 ServerHandler()에서 get_environ()을 통해서 header 파라미터로 들어온 변수들을 체크합니다.

1
2
3
4
5
6
7
8
# django.core.servers.basehttp.WSGIRequestHandler.handle_one_request
class WSGIRequestHandler(simple_server.WSGIRequestHandler):
    def handle_one_request(self):
        .....
        handler = ServerHandler(
          self.rfile, self.wfile, self.get_stderr(), self.get_environ()
      )
 
cs

 

2. get_environ()에서는 파라미터를 체크 후 dict()로 리턴해 주는데 해당 값들은 request.MEAT에 들어가게 됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# wsgiref.simple_server.WSGIRequestHandler
class WSGIRequestHandler(BaseHTTPRequestHandler):
 
    server_version = "WSGIServer/" + __version__
 
    def get_environ(self):
        env = self.server.base_environ.copy()
        env['SERVER_PROTOCOL'] = self.request_version
        env['SERVER_SOFTWARE'] = self.server_version
        env['REQUEST_METHOD'] = self.command
        if '?' in self.path:
            path,query = self.path.split('?',1)
        else:
            path,query = self.path,''
 
        env['PATH_INFO'] = urllib.parse.unquote(path, 'iso-8859-1')
        env['QUERY_STRING'] = query
 
        host = self.address_string()
        if host != self.client_address[0]:
            env['REMOTE_HOST'] = host
        env['REMOTE_ADDR'] = self.client_address[0]
 
        if self.headers.get('content-type') is None:
            env['CONTENT_TYPE'] = self.headers.get_content_type()
        else:
            env['CONTENT_TYPE'] = self.headers['content-type']
 
        length = self.headers.get('content-length')
        if length:
            env['CONTENT_LENGTH'] = length
 
        for k, v in self.headers.items():
            k=k.replace('-','_').upper(); v=v.strip()
            if k in env:
                continue                    # skip content length, type,etc.
            if 'HTTP_'+k in env:
                env['HTTP_'+k] += ','+v     # comma-separate multiple headers
            else:
                env['HTTP_'+k] = v
        return env
 
cs