掌握Django(十一)

本文介绍安全API保护我们的API访问,我们将介绍基于令牌的认证,这是目前restful APIs的事实标准,添加安全认证到各API端点,包括注册、登陆等,最后我们介绍应用相关权限。


基于令牌token的认证

我们注册一个用户,比如访问端点 /user , 并提供用户密码email等,服务器验证资料并返回给我们注册的用户。下一步我们我们登录,访问端点 /auth,并提供用户密码等信息。

服务器验证用户密码,如果正确的话,就返回我们一个token。

我们将token保存在本地,下次再访问服务器资源的话,只需提供token无需再输入用户密码,服务器检查token的有效性及是否过期,从而授权我们访问。

token相当于一把临时钥匙。


添加认证API端点

Django提供了认证系统,但它不提供API,只提供一些模块及数据库表,我们需要手动实现它。显然,手动实现是乏味且重复的,可以借助完美的第三方库DJoser。官方网址为:

https://djoser.readthedocs.io/en/latest/

我们看到,djoser会为我们建好很多端点。

查看文档并安装djoser。

pipenv install djoser

配置INSTALLED_APPS

INSTALLED_APPS = (
    'django.contrib.auth',
    (...),
    'rest_framework',
    'djoser',
    (...),
)
配置INSTALLED_APPS
urlpatterns = [
    (...),
    path('auth/', include('djoser.urls')),
]
djoser属于一个API层,它有自己的视图类、序列化类等,但还需要给它配置一个后端认证引擎。我们有两个选择:
Token-based 认证
JSON Web Token 认证

这两者有什么区别呢?主要就是rest-framework中自带的Token-based需要将 token保存在数据库,之后再与客户发过来的token作比较;而JSON Web Token不用保存在数据库,服务器可以计算摘要得出Token是否是有效的。

这里我们选择 JSON Web Token,根据官方文档操作:
安装:
pipenv install djangorestframework_simplejwt
再添加settings.py:REST_FRAMEWORK = { 
   ...,
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework_simplejwt.authentication.\
            JWTAuthentication',
    ),}
再添加:
SIMPLE_JWT = {
   'AUTH_HEADER_TYPES': ('JWT',),
}
再在storefront--urls.py 里添加:
urlpatterns = [
    ... 
   path('auth/', include('djoser.urls')),
    path('auth/', include('djoser.urls.jwt')),
]
完成之后,可以访问API:/auth/users/
端点访问正常,提示401未授权,下一节再接着介绍。

注册用户

试着注册一个用户,访问API, /auth/users
并POST 用户密码和Email:
{
    "email": "user1@domain.com", 
   "username": "user1", 
   "password": "1234"
}
提示密码不符合复杂性要求,这个复杂性要求也是在settings.py中的AUTH_PASSWORD_VALIDATORS配置的,我们当然可以修改,但一般不建议,我们post复杂一点的密码就好。
如果我们希望注册时可以提供first_name和last_name,该如何做呢?根据之前学过的知识,是要修改serializer类。查官网文档,有一个默认配置
'user_create': 'djoser.serializers.UserCreateSerializer',我们可以修改这个默认配置,下面是步骤。
我们在core应用里添加 serializers.py文件,这是特定的实现,不应当实现在store应用里。
from djoser.serializers import UserCreateSerializer
     as BaseUserCreateSerializer
class UserCreateSerializer(BaseUserCreateSerializer):
    class Meta(BaseUserCreateSerializer.Meta): 
       fields = [ 
           'id', 'username', 'password', 'email', 
                'first_name', 'last_name' 
       ]
我们自定义了UserCreateSerializer序列类,覆盖添加了自定义字段 first_name和last_name, 再到settings.py里,添加以下配置。
DJOSER = { 
   'SERIALIZERS': {
        'user_create': 'core.serializers.\
                UserCreateSerializer',
    },}
 再访问端点 .../auth/users, 能看到可以注册时提供 first_name和last_name了。我们试着注册 user2.
{ 
   "username": "user2",
    "password": "abcd1234,",
    "email": "user2@domail.com",
    "first_name": "kelemi",
   "last_name": "chen"
}
系统成功注册了。
如果需要再增加一个字段,比如是出生日期 birth_date,我们是否也像上述一样处理呢?
一般是不建议的。
因为出生日期不属于用户模型,它更合适是放在配置文件里,不同的应用可以有不同的用户配置文件。

一个API 应只有一个职责,不要搞得复杂。
客户端注册时同时提供用户字段 和 配置文件字段, 我们要能做到分离到用户模型和配置文件中去。下一节介绍。


创建配置文件API


我们查看djoser官网,发现它完全专注用用户认证等处理,没有配置文件的API端点,我们需要手动创建。放在哪个应用里呢?我们现在要建立是顾客的用户配置文件,显然放在应用store里比较合适。
store--serializers.py,添加:
class CustomerSerializer(serializers.ModelSerializer):
    user_id = serializers.IntegerField()
    class Meta: 
       model = Customer
        fields = ['id', 'user_id', 'phone', 
           'birth_date', 'membership']
在后面会介绍到,实际user_id是不必的,因为该API是要认证才能访问的,而认证的用户会有user_id,这里的API还未加上安全功能,暂时手动设置user_id.

再到store--views.py,添加:
class CustomerViewSet(CreateModelMixin,RetrieveModelMixin,UpdateModelMixin, GenericViewSet):
    queryset = Customer.objects.all() 
   serializer_class = CustomerSerializer
再来添加路由,store--urls.py,添加:
...
router.register('customers', views.CustomerViewSet)
...
再回到浏览器,访问端点 /store/customers,能看到该API端点是正常可用的。我们查看下数据库的 user, 找到最新的user_id, 查到是5,然后POST{
    "user_id": 5,
    "phone": "12345",
    "birth_date": "2000-12-12",
    "membership": "B"
}
再检查store_customer表,检查已新增一条顾客记录。


登陆


djoser的API端点后5个都是认证用的,前2个是Token Base, 由Rest Framework提供,后3个是jwt则由 django-rest-framework-simplejwt提供。


前面我们讲了两者的区别,主要是jwt的token不保存在服务器数据库。我们访问/auth/jwt/create,post正确的用户和密码。得到两个token,
一个是access token, 一个是refresh token,默认情况下 access token是用于访问安全API,有效期是5分钟,而 refresh token是1天,当access token过期时,用refresh token来刷新它。
{
    "refresh": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImV4cCI6MTY3MzcwNzM2NiwianRpIjoiOTNiNDg3N2M4MjdiNDgyMDkzZmE3YmFkMmZkMmY0NGUiLCJ1c2VyX2lkIjo0fQ.tdBM5MZG3ldvzX3SQWdd3zKtWJFFqT1Mg6U-fN4ryG8",
    "access": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNjczNjIxMjY2LCJqdGkiOiJkYTJiYjFiNjVlZDI0YTljYjUxOTUwM2E4ZDdjNjIyYiIsInVzZXJfaWQiOjR9.DGjw_595z2DTARg8pkl4zShq3l0Iqv2Qv6AJEqJD09Q"
}
google 查询 “django rest framework simple_jwt”,网址:https://django-rest-framework-simplejwt.readthedocs.io/en/latest/settings.html有相关设置说明, 我们可以改变默认值 。我们将 access token 过期时间调为1天,这样下面测试方便些。在settings.py中添加:
SIMPLE_JWT = {
    ... 
   'ACCESS_TOKEN_LIFETIME': timedelta(days=1),
}

检查JWT

我们访问jwt.io,这是jwt的官网,提供各种平台的库。比如django使用的就是simple_jwt。我们将上一节的access_token复制过去查看下。

我们看到jwt由三部分组成,头部、负载,以及数字签名。其中数字签名是由头部、负载和保存在服务器的密钥生成,黑客由于没有服务器的密钥,所以不能伪装签名。服务器就能检查该jwt是否是合法的。


刷新Token
当我们登录时,得到access_token和refresh_token, 当access_token过期时,我们可以通过refresh_Token重新得到一个access_token,我们看到有个 /auth/jwt/refresh 端点,并提供refresh_token就能得到一个新的access_token。


获取当前用户
djoser查看当前用户的端点是 /auth/users/me,到的是401未授权响应,我们可以提供access_token访问。为方便调试,安装chrome插件 modheader。然后添加Authorization,值 为“JWT ”+<access_token>.

你可能奇怪为什么是JWT开头,实际上这是之前我们在settings.py中设定的。这样之后,再刷新 /auth/users/me,就能看到自己的用户了,有email,id以及username。
你可能希望看到 first_name和last_name,该如何做呢?很简单,查看下djoser官网,自定义current_user即可。先添加 core--serializers.py:
from djoser.serializers import UserSerializer \
as BaseUserSerializer...class UserSerializer(BaseUserSerializer):
    class Meta(BaseUserSerializer.Meta): 
       fields = [
            'id', 'username', 'email', \ 
        'first_name', 'last_name'
        ]
然后,设置settings.py里。
DJOSER = { 
   'SERIALIZERS': {
       ... 
       'current_user': 'core.serializers.UserSerializer'
    },
}
再访问端点 /auth/users/me,就能看到 first_name和last_name了。

另外,需要将ModHearder设置的Authorization去掉,否则其他访问网站都将以这个jwt作为身份认证。

获取用户配置文件

我们要添加一个端点是 /customers/me 查看用户配置文件。在store--views.py里的CustomerViewSet下添加:
from rest_framework.decorators import action...
class CustomerViewSet(...):
   ...
    @action(detail=False)
   def me(self, request): 
       return Response('OK')
首先我们导入action,这个用于标记 操作方法,因为CurstomerViewSet继承了很多类,我们要在具体的方法里精确地指定特定的操作方法,如PUT,GET等。添加方法 me , 并标记detail=False,这个表示可以用直接在customers后接me, 如 /customers/me , 如果 detail=True的话,需要指定明细才能访问,如 /customers/1/me , 这里我们保持False,并简单输出OK。
访问 /customers/me ,检查是正常的。再来修改下me方法,使之可以看到实际的customer配置文件且能修改,需要添加 method为 GET和PUT。我们修改完整。
...
@action(detail=False, methods=['GET', 'PUT'])    def me(self, request):
        (customer, created) = Customer.objects.\ 
           get_or_create(user_id=request.user.id)
        if request.method == 'GET': 
           serializer = CustomerSerializer(customer)
            return Response(serializer.data)
        elif request.method == 'PUT': 
           serializer = CustomerSerializer(customer,\ 
                           data=request.data)
            serializer.is_valid(raise_exception=True)
            serializer.save()
            return Response(serializer.data)...

再访问 /store/customers/me, 就能看到配置文件,而且也可以修改。

也有一个小问题,就是user_id不应该可以修改,应该为只读。修改 store--serializers.py的 CustomerSerializer 的 user_id为:
user_id = serializers.IntegerField(read_only=True)

应用权限
也有一个访问官网 https://www.django-rest-framework.org/api-guide/permissions/ , 可以看到有很多应用权限,比如AllowAny,IsAuthenticated等,默认情况下,API端点是允许所有人访问的,也就是权限是AllowAny,我们可以修改。
全局修改:通过settings.py中可以修改全局的访问权限。比如全局修改成IsAuthenticated, 表示必须是授权用户才可以访问,默认是AllowAny的。
REST_FRAMEWORK = {
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.IsAuthenticated',
    ]
}
特定的视图修改访问权限:比如我们要修改CustomerViewSet的访问权限。
from rest_framework.permissions import IsAuthenticated
...
class CustomerViewSet(...):
    ... 
   permission_classes = [IsAuthenticated]
   ...

也可以在action装饰符下设置,如:
class CustomerViewSet(...): 
   ... 
   @action(detail=False, methods=['GET', 'PUT'],\ 
       permission_classes=[IsAuthenticated]) 
   def me(self, request): 
       ...
在某些视图方法下,不同的request method可有需要的权限是不同的,比如我们允许所有人可以get,但对其他操作则需要认证用户,这样的放,可以使用 get_permissions方法:
class CustomerViewSet(...): 
   ...
    def get_permissions(self):
        if self.request.method=='GET': 
           return [AllowAny()] 
       return [IsAuthenticated()]
   ...

注意get_permissions方法返回的是权限的对象,所以需要加上括号表示已执行构造函数返回对象,而之前的perssion_classes以及装饰符action提供的都是权限类而不是对象。这是要注意的。

应用自定义权限
查看IsAuthenticated类,得知这些自带权限类继承自BasePermission,然后重载has_permission方法来判断,我们仿照实现自定义类 IsAdminOrReadOnly,该类的权限是除了管理员,其他人员都是只读。store应用下新建 permissions.py文件:
from rest_framework.permissions import BasePermission
class IsAdminOrReadOnly(BasePermission): 
   def has_permission(self, request, view):
        if request.method == 'GET':
            return True
        return bool(request.user and request.user.is_staff)
is_staff表示管理员,上面允许非管理员使用GET方法,实际上还有OPTION,HEAD等方法也应该允许,我们可以改成permissions自带的SAFE_METHODS.
from  rest_framework import permissions
...
class IsAdminOrReadOnly(permissions.BasePermission):
    def has_permission(self, request, view): 
       if request.method in permissions.SAFE_METHODS: 
           return True
        return bool(request.user and request.user.is_staff)
建好自定义权限类IsAdminOrReadOnly后,我们可以使用它。我们在ProductViewSet和CollectionViewSet中分别加入:
...
class ProductViewSet(ModelViewSet):
    ... 
   permission_classes = [IsAdminOrReadOnly]
...
class CollectionViewSet(ModelViewSet): 
   ...
    permission_classes = [IsAdminOrReadOnly]
...
CartViewSet不加,因为它允许所有用户包括匿名用户创建购物车。
而CustomerViewSet有些不同,它只允许管理员列出Customer列表及删除具体的Customer,认证用户可以查询及修改自己的用户配置文件,我们稍修改下。
class CustomerViewSet(ModelViewSet):
    ... 
   permission_classes = [IsAdminUser]
    @action(detail=False, methods=['GET', 'PUT'],
            permission_classes=[IsAuthenticated])
    def me(self, request): 
       ...
我们将CustomerViewSet设置继隔海于ModelViewSet,与其他视图类保持一致,permission_classes设为 IsAdminUser,表示只允许管理员访问,再在具体的方法 me 中覆盖权限类为 IsAuthenticated,表示认证用户都可以使用 me 方法端点。



应用模型权限

前面的CustomerViewSet视图类中,我们设置了权限类IsAdminUser,只允许管理员有相关权限,但要是我们想让具体的用户组有该视图权限,而不是非要管理员权限才行,我们如何做呢?
这就需要用到模型权限DjangoModelPermissions。我们修改一下CustomerViewSet的权限类。
class CustomerViewSet(ModelViewSet): 
   ... 
   permission_classes = [DjangoModelPermissions]
我们再查看下DjangoModelPermissions的原生实现,它有一个对应关系如下:
...
perms_map = { 
       'GET': [],
        'OPTIONS': [],
        'HEAD': [], 
       'POST': ['%(app_label)s.add_%(model_name)s'], 
       'PUT': ['%(app_label)s.change_%(model_name)s'], 
       'PATCH': ['%(app_label)s.change_%(model_name)s'], 
       'DELETE': ['%(app_label)s.delete_%(model_name)s'],
    }
...
该权限类首先需要是已认证用户,然后我们看到这个实现的GET,OPTIONS,HEAD不需要相关权限,而POST,PUT,PATCH,DELETE则需要相对应的权限。在之前,我们创建了一个组Customer Service,该组有Customer模型的相关操作权限,而我们之前创建的kelemi用户加入了该组。所以我们以kelemi登陆,访问端点  /store/customers ,可以进行相关增删改操作,而用其他用户如 user1登陆,则只允许查看而不能增删改。
我们是可以自定议该权限的,比如我们希望查看Customer也需要相应的组权限,则就需要自定义了。

进入 store--permissions.py里,添加:
class FullDjangoModelPermissions( 
       permissions.DjangoModelPermissions):
    def __init__(self): 
       self.perms_map['GET'] = \ 
           ['%(app_label)s.view_%(model_name)s']
我们新建FullDjangoModelPermissions,它继承自DjangoModelPermissions,并仿照设置perms_map的GET,要求有相关的view模型权限,注意%(app_label),%(model_name)都是动态根据应用和模型名生成的。
再在CustomerViewSet中赋于权限类即可。
class CustomerViewSet(ModelViewSet):
    ...
    permission_classes = [FullDjangoModelPermissions]
...
这样修改后,普通认证用户也不能获取Customers的列表了,需要相关模型权限的用户才有获取列表权限。
另外还有一个类似的权限DjangoModelPermissionsOrAnonReadOnly,允许匿名用户查看。而DjangoModelPermissions的查看必须需要认证用户才行。


应用自定义模型权限
先在模型Customer中创建自定权限。store--models.py的Customer模型中添加:
class Customer(models.Model): 
   ...
    class Meta: 
       ... 
       permissions = [ 
           ('view_history', 'can view history')
        ]
添加了permissions属性,其中view_history是权限代码,"can view history"是权限说明。
再迁移到数据库更改:
python manage.py 
makemigrationspython manage.py migrate
检查数据表auth_permission已增加了这条记录。

再在store--permissions.py中添加一个权限类:
class ViewCustomerHistoryPermission(permissions.BasePermission):
    def has_permission(self, request, view): 
       return request.user.has_perm('store.view_history')

再到视图类CustomerViewSet中,添加操作方法的权限。
...
@action(detail=True, permission_classes=[
        ViewCustomerHistoryPermission
    ]) 
   def history(self, request, pk): 
       return Response('OK')
...
再到Django管理后台,将view_history赋给用户比如user1,再以user1登陆,然后访问 /store/customers/4/history, 就能看输出OK了,而其他未赋该权限的用户则无法查看。


小结
本文介绍了安全API,保护我们的API访问。基于令牌的认证,是目前restful APIs的事实标准,添加安全认证到各API端点,包括注册、登陆等,最后介绍了应用相关权限。下一节将是阶段综合练习,设计及创建订单API。

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注