Bootstrap

Django 使用 token 认证

场景说明

web 网站登录认证一般常用的有三种方式:

  1. session:早期以 web 为主
  2. token:适用于 web、app
  3. oauth:微信、QQ登录

目前在 django 中使用 session 认证的方式比较多,因为 django 内置了强大的用户认证系统–auth模块。

下面会结合 session 和 token 两种认证方式做一个登录的示例。

session 登录认证示例

views.py:

def query(request):
    if request.method == 'GET':
        return render(request, 'login.html')
    else:

        username = request.POST.get('username')
        password = request.POST.get('password')
        user_date = {'username': username, 'password': password}
        user_obj = authenticate(request, **user_date)  # 验证用户名与密码是否正确,可以自定义 backend
        if not user_obj:
            return HttpResponse('用户名或者密码错误!')
        else:
            login(request, user_obj)  # 登录,将当前用户信息存储在会话中
            return redirect('index')

urls.py:

path('login/', views.query, name='login'),

模板:

<h1>登录</h1>
<form action="" method="post">
    {% csrf_token %}
    <div>
        用户名:<input type="text" name="username" id="username">
    </div>
    <div>
        密码:<input type="password" name="password" id="password">
    </div>
    <div>
         <input type="submit" id="btn" value="确认">
    </div>
</form>

页面效果:
在这里插入图片描述

登录成功之后会在 cookie 中添加一个 session_id:
在这里插入图片描述

auth 系统中的 login 方法:
并且将一个已验证的用户附加到当前会话(session)中。

def login(request, user, backend=None):
    """
    Persist a user id and a backend in the request. This way a user doesn't
    have to reauthenticate on every request. Note that data set during
    the anonymous session is retained when the user logs in.
    """
    session_auth_hash = ''
    if user is None:             # 如果用户为None则设置为request的user,因为进过了auth的中间间处理所以肯定有user属性
        user = request.user
    if hasattr(user, 'get_session_auth_hash'):  #如果有get_session_auth_hash方法则调用该方法生成hash值
        session_auth_hash = user.get_session_auth_hash()

    if SESSION_KEY in request.session: # 检查该key是否在会话中存在值
        if _get_user_session_key(request) != user.pk or (
                session_auth_hash and
                not constant_time_compare(request.session.get(HASH_SESSION_KEY, ''), session_auth_hash)):
            # To avoid reusing another user's session, create a new, empty
            # session if the existing session corresponds to a different
            # authenticated user.
            request.session.flush()
    else:
        request.session.cycle_key() # 重新加载和生成新的session保持该会话数据
    
   try:
        backend = backend or user.backend
    except AttributeError:
        backends = _get_backends(return_tuples=True)
        if len(backends) == 1:
            _, backend = backends[0]
        else:
            raise ValueError(
                'You have multiple authentication backends configured and '
                'therefore must provide the `backend` argument or set the '
                '`backend` attribute on the user.'
            )
    else:
        if not isinstance(backend, str):
            raise TypeError('backend must be a dotted import path string (got %r).' % backend)

    request.session[SESSION_KEY] = user._meta.pk.value_to_string(user)
    request.session[BACKEND_SESSION_KEY] = backend
    request.session[HASH_SESSION_KEY] = session_auth_hash
    if hasattr(request, 'user'):
        request.user = user
    rotate_token(request)
    user_logged_in.send(sender=user.__class__, request=request, user=user)
        
在请求中保留用户ID和后端。这样,用户不必在每个请求上都重新进行身份验证。请注意,当用户登录时,匿名会话期间设置的数据将保留。
首先先判断是否有会话冲突,判断该会话是否与其他已经存在的值发生了冲突,如果没有冲突则重新生成一个新的会话并将旧数据保存到新会话中,
然后将 user 的pk转换成字符串放到session中,然后给request设置user属性,此时登录验证的操作就执行完成。

那么上述登录认证做好了,下一次登录时怎么获取用户登录信息呢?

Django是通过django.contrib.auth.middleware.AuthenticationMiddleware中间件来完成的。

def get_user(request):
    if not hasattr(request, '_cached_user'):
        request._cached_user = auth.get_user(request)
    return request._cached_user

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)) # 当前登录用户对象
        
get_user 核心代码:
def get_user(request):
    """
    Return the user model instance associated with the given request session.
    If no user is retrieved, return an instance of `AnonymousUser`.
    """
    from .models import AnonymousUser
    user = None
    try:
        user_id = _get_user_session_key(request)
        backend_path = request.session[BACKEND_SESSION_KEY]
    except KeyError:
        pass
    else:
        if backend_path in settings.AUTHENTICATION_BACKENDS:
            backend = load_backend(backend_path)
            user = backend.get_user(user_id)
            # Verify the session
            if hasattr(user, 'get_session_auth_hash'):
                session_hash = request.session.get(HASH_SESSION_KEY)
                session_hash_verified = session_hash and constant_time_compare(
                    session_hash,
                    user.get_session_auth_hash()
                )
                if not session_hash_verified:
                    if not (
                        session_hash and
                        hasattr(user, '_legacy_get_session_auth_hash') and
                        constant_time_compare(session_hash, user._legacy_get_session_auth_hash())
                    ):
                        request.session.flush()
                        user = None

    return user or AnonymousUser()

从 session 中获取用户模型实例,如果未检索到用户,则返回AnonymousUser

上述就是 django 的登录认证功能,其中还有很多方法,有兴趣的可以看我关于 auth 章节的讲解。

认证过程大致如下:

1.设置session:
* 在cookie里面加上了一个键值对,session_id:随机字符串
* 将用户的数据进行加密,并保存到Django-session表里面
2.判断session是否合法:
* 从cookie里面拿出session_id:随机字符串
* 去Django-session表里面查询到对应的数据
* 反解加密的用户数据,并获取用户的数据,判断是否合法

基于JWT的Token认证

JSON Web Token(JWT)是一个非常轻巧的规范。这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息。

一共分为三部分:
1.header:声明类型,声明加密的算法

​ 2.payload:公共的一些声明:签发者,过期时间,签发时间

​ 3.signature:签证信息,由后端签发,secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证

生成 token:

>>> import jwt
>>> encoded = jwt.encode({"some": "payload"}, "secret", algorithm="HS256")
>>> print(encoded)
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzb21lIjoicGF5bG9hZCJ9.Joh1R2dYzkRvDkqv3sygm5YyK8Gi4ShZqbhK2gxcs2U
>>> jwt.decode(encoded, "secret", algorithms=["HS256"])
{'some': 'payload'}

同样的问题,下一次接口登录的时候,怎么获取已经登录的用户信息呢? django 中是有中间件处理,现在使用 token 怎么办,那么也要自定义一个中间件,需要实现的功能也是和 AuthenticationMiddleware 中处理逻辑类似,它是从会话中去拿用户的信息,这里我们需要解码 token,获取用户的信息(生成token的时候可以将用户信息放进去)。

from django.conf import settings
from django.utils.deprecation import MiddlewareMixin
from jose import jwt, ExpiredSignatureError, JWTError

from xx.models import User


class AuthMiddleware(MiddlewareMixin):
    def process_request(self, request):
        token = request.META.get('HTTP_AUTHORIZATION')
        if token:
            token = token.replace('Bearer ', '')
            try:
                jwt_token = jwt.decode(token, settings.SECRET_KEY)
            except ExpiredSignatureError:
                return False
            except JWTError:
                return False

            user = User.objects.get(id=jwt_token.get('sub'))
            request.user = user

        return

后续在其他地方也可以使用 request.user
注意点:中间件定义好一定要在 settings.py 中注册

MIDDLEWARE = [
    'xx.middleware.auth.AuthMiddleware',
]

参考文献

django 中 auth 模块:https://blog.csdn.net/qq_39253370/article/details/108464419
pyjwt:https://github.com/jpadilla/pyjwt#usage
cookie 和 session:https://blog.csdn.net/qq_39253370/article/details/105462124

;