参考基于 Token 的身份验证、JSON Web Token - 在Web应用间安全地传递信息、基于cookie的django-rest-jwt认证
前两篇是关于原理的,最后一篇是和django-restframework相关的。
JWT原理
除了使用session外,还可以使用token进行用户认证(Authentication)。
一个很大的区别是,session需要在服务端存储能够通过session_id而获取的信息,每次请求到达服务端时,需要根据session_id这个key值,获取存储在内存/磁盘/数据库中的信息。而token的话,信息均在token里面,服务端只需要根据token中定义的算法进行解析,即可获得所需认证信息。所以一个是memory cost,一个是time cost。
现在使用token比较流行。
JWT(Json Web Token)是实现token认证的一种通用标准。
JWT分为三部分:header,payload,signature。中间用点分开,均使用base64进行编码,所以看起来像是这样:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VyX3R5cGUiOjEsIm5iZiI6MTUyNjg4NjYzM30.CTZH48xD_TdtDZcgAd8exiCxkryXASruDCbRHsFFD5Y
header基本固定,包含token使用的类型,使用的算法等。
payload是具体信息,有些字段是标准字段,当然也可以依据需求添加自定义的字段,非常方便。为了每次请求获取的jwt token稍有不同,我会在payload中加入一些合时间有关的字段,比如exp(Expiration time,过期时间),iat(Issued at,发行时间),nbf(Not before)。
signature是把前两部分使用base64编码后,用"."连接,然后使用在header中定义的算法(默认是HS256)进行加密。此过程需要额外提供一个密钥(secret)。
三部分拼起来就是jwt token。由于有时间信息,payload和signature总是会变的。
和Django结合
一、REST
一般而言是在restful的接口中使用jwt token,相关的Django库是djangorestframework
和djangorestframework-jwt
。
需要在setting中进行配置:
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': [
'common.permissions.IsAuthenticated',
],
'DEFAULT_AUTHENTICATION_CLASSES': [
'common.authentication.CookieJSONWebTokenAuthentication',
],
}
设置项DEFAULT_AUTHENTICATION_CLASSES
是用来进行用户认证的,因为第三方库默认token保存在http header中,所以自定义一个类,将token保存到cookie中。
这个类可以参考rest_framework_jwt.authentication.JSONWebTokenAuthentication
。
需要实现两个方法:
-
authenticate(self, request)
逻辑是:从request中获取token,从token中获取payload,从payload中获取用户认证信息。 -
authenticate_header(self, request)
作用是:当需要返回401 Unauthenticated
或403 Permission Denied
时,如何配置http header中的WWW-Authenticate
项。
当在rest_framework中访问request.user时,调用此方法。
具体如下。
代码在rest_framework.request
中:
@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
在self._authenticate()
中,self.authenticators
即是在settings中设置的。
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()
再往上走,会发现是在rest_framework.views
中的APIView类中的dispatch
方法来调用的。
另一个设置项是DEFAULT_PERMISSION_CLASSES
,做的是用户授权(Authorization)判断,由于该设置项的存在,所以所有的APIView均会做用户鉴权判断。自定义类可参考rest_framework.permissions.IsAuthenticated
,目前这个类仅做了该用户是否已authenticated的判断,若有其他方面权限的判断,可以在具体的view中加装饰器。
比如,判断用户是否是雇员(我的request.user是一个dict):
class IsEmployeeUser(BasePermission):
"""
Allows access only to employee users.
"""
def has_permission(self, request, view):
return request.user and request.user['is_authenticated'] and request.user['is_employee']
然后,装饰器:
from rest_framework.decorators import permission_classes
def required(func):
return permission_classes([IsEmployeeUser])(func)
具体的授权判断,同认证判断一样,均是在rest_framework.views
中的APIView类中的dispatch
方法中,具体是调用check_permissions(self, request)
方法,其中的self.get_permissions()
就是取的settings中的设置。
因为有默认的授权判断类,所以如果不需要进行授权判断,装饰器中应该这样写
def anonymous(func):
permission_classes([])(func)
二、HTML
使用以上定义的方法,只会对restful的接口进行认证和授权判断。
若想对.html
或.js
等也想使用jwt token,首先需要配置settings:
MIDDLEWARE = [
...
'django.contrib.auth.middleware.AuthenticationMiddleware',
'common.middleware.AuthenticationMiddleware',
...
]
在默认的AuthenticationMiddleware
下添加自定义的中间件。
代码如下,参考的是默认的django.contrib.auth.middleware.AuthenticationMiddleware
,其实也是一个Django中间件的普通实现:
class AuthenticationMiddleware(MiddlewareMixin):
def process_request(self, request):
assert hasattr(request, 'session'), (
"The svc_auth authentication middleware requires session middleware "
"to be installed. Edit your MIDDLEWARE%s setting to insert "
"'django.contrib.sessions.middleware.SessionMiddleware' before "
"'svc_auth.middleware.AuthenticationMiddleware'."
) % ("_CLASSES" if settings.MIDDLEWARE is None else "")
user, _ = authenticator.authenticate(request=request)
if user is not None:
request.user = user
其中authenticator
就是上一节的CookieJSONWebTokenAuthentication
的实例。
这样就完成了用户认证的判断。
接下来是用户授权的判断,参考的是django.contrib.auth.decorators
中的login_required
。
def required(function=None, redirect_field_name=REDIRECT_FIELD_NAME, login_url=None):
actual_decorator = user_passes_test(
lambda user: user and user.get('is_authenticated', False) and user.get('is_employee', False),
login_url=login_url() if login_url else SimpleLazyObject(lambda: default_login_url('employee')),
redirect_field_name=redirect_field_name,
)
if function:
return actual_decorator(function)
return actual_decorator
这样,在需要的view中添加@required
即可。
与REST的设置不同,没有默认的配置,所以如果没有添加装饰器的话,不会去进行授权判断,也就是说,不需要有anonymous装饰器。
三、关于User
如果使用的Django的User,那么以上很多都不用自定义。而如果需要使用自定义的User,那么就需要对每一个获取user
对象的地方进行重写。
网友评论