本文介绍安全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。