Bob's Blog

Web开发、测试框架、自动化平台、APP开发、机器学习等

返回上页首页

Django restframework加Vue打造前后端分离的网站(六)token和LDAP认证



经过前面文章记录的步骤,现在有页面可以展示,也有API可以调用。但是现在是任何人都可以查看、新增、更新,实在是不够安全谨慎,于是在这篇文章会记录加上认证,用来限制访问者的行为。只有认证过的用户才可有进一步的操作许可。

该文章分三个部分: token认证, LDAP认证,token的过期和续期。

一) Token Authentication

在第一篇中已经创建过了用户(python manage.py createsuperuser),这里便会用该用户的token来验证是否有操作权限。

在添加修改前,我们先打开project的api地址,会看到没有登录的情况下,仍然可以做get, post, patch, put之类的操作。然后开始修改。

在settings.py中添加如下内容:

INSTALLED_APPS = [
    ...
    'rest_framework.authtoken',
    ...
]

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework.authentication.TokenAuthentication',
    ],
    ...
}

接着迁移数据库变更

python manage.py migrate
python manage.py drf_create_token your_user_name

# output: Generated token f32fc8e6366422f8f1280388dde4e946e7955097

此时在数据库中能发现增加了一个表authtoken_token,里面就有对应用户的token信息。

同时我们在目前的view中添加permission定义,设置只有登录的可以操作否则只读。

from .models import Project
from .serializers import ProjectSerializer
from rest_framework import generics
from rest_framework.permissions import IsAuthenticatedOrReadOnly


class ProjectList(generics.ListCreateAPIView):
    """
        get:
            Return all projects.

        post:
            Create a new project.
    """
    permission_classes = [IsAuthenticatedOrReadOnly]  # here
    queryset = Project.objects.all().order_by("id")
    serializer_class = ProjectSerializer


class ProjectDetail(generics.RetrieveUpdateAPIView):
    """
        get:
            Return a project instance.

        put:
            Update a project.

        patch:
            Update one or more fields on an existing project.

    """
    permission_classes = [IsAuthenticatedOrReadOnly]  # here
    queryset = Project.objects.all()
    serializer_class = ProjectSerializer

此时再次打开api的地址,就会发现没有post或者put的输入框以及按钮了,因为匿名用户变成只读权限了。

当我们需要新建或更新project时,则需要带上自己账户的token: 'Authorization: Token f32fc8e6366422f8f1280388dde4e946e7955097',例如如下用postman的截图。

另外restframework也提供了相关获取token的接口,我们需要在url.py中定义,然后通过/api-token-auth即可获取token信息。不过这个接口不允许get方法,只能post用户名和密码。

from rest_framework.authtoken.views import obtain_auth_token

urlpatterns = [
    ...
    path('api-token-auth/', obtain_auth_token, name='api_token_auth'),
    ...
]

如下图。

***需要注意的是*** 当添加了token authentication后,打开/api/docs/会只能看到不需要登录的get方法,其他的post/put/patch/delete都看不到,因为设置了权限,也影响到了这里。并且还有一个坑,django-rest-swagger版本2.2.0有已知issue不能支持authentication,于是需要降级到2.1.2, pip install django-rest-swagger==2.1.2. 参考https://github.com/marcgibbons/django-rest-swagger/issues/708 . 需要更新settings.py,如下。

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        'rest_framework.authentication.BasicAuthentication',
        'rest_framework.authentication.TokenAuthentication'
    ],
    ...
}

# 如果没有添加BasicAuthentication,只想用TokenAuthentication,那么还需要添加下面的设置
SWAGGER_SETTINGS = {
    'SECURITY_DEFINITIONS': {
        'api_key': {
            'type': 'apiKey',
            'in': 'header',
            'name': 'Authorization'
        }
    },
    # 'LOGIN_URL': getattr(settings, 'LOGIN_URL', None),
    # 'LOGOUT_URL': getattr(settings, 'LOGOUT_URL', None),
    'DOC_EXPANSION': None,
    'APIS_SORTER': None,
    'OPERATIONS_SORTER': None,
    'JSON_EDITOR': False,
    'SHOW_REQUEST_HEADERS': False,
    'SUPPORTED_SUBMIT_METHODS': [
        'get',
        'post',
        'put',
        'delete',
        'patch'
    ],
    # 'VALIDATOR_URL': '',
}

此时再次打开并验证身份后就能看到所有支持的接口了。

二) LDAP Authentication

有时会需要验证LDAP的用户信息,这样在公司内部就可以直接登录,而不用新建或修改用户。

安装django-auth-ldap

pip install django-auth-ldap

在settings.py中需要配置如下信息,需要了解公司里LDAP里的信息结构

o– organization(组织-公司)
ou – organization unit(组织单元/部门)
c - countryName(国家)
dc - domainComponent(域名组件)
sn – suer name(真实名称)
cn - common name(常用名称)
dn - distinguished name(唯一标识)

import ldap
from django_auth_ldap.config import LDAPSearch


AUTHENTICATION_BACKENDS = ["django_auth_ldap.backend.LDAPBackend",
                           'django.contrib.auth.backends.ModelBackend']  # 如果ldap验证不通过,则会用默认的验证来用本地建的用户来登录

AUTH_LDAP_SERVER_URI = "ldap://your_url:389"  # 服务器地址
AUTH_LDAP_BIND_DN = "CN=cn_name,OU=Users,DC=domain,DC=com"  # 一个样例,管理员的dn
AUTH_LDAP_BIND_PASSWORD = 'your_pwd!'    # 管理员密码
AUTH_LDAP_BASEDN = "OU=Users,DC=domain,DC=com"  # 基础的dn信息
AUTH_LDAP_USER_SEARCH = LDAPSearch("OU=_Sites,DC=domain,DC=com", ldap.SCOPE_SUBTREE, "(mail=%(user)s)")  # 使用mail作为用户名搜索是否存在该mail名,且会验证用户名和密码
# AUTH_LDAP_ALWAYS_UPDATE_USER = True   # 是否每次更新用户,默认为true

AUTH_LDAP_USER_ATTR_MAP = {    # 与db中的字段做一个对应
    "username": "name",
    "email": "mail",
}

此时在django的admin登录后会发现不成功,检查db会发现auth.user里已经存在该用户,不过is_staff字段为false。

需要令is_staff为true,则需要添加group的信息。在上面的基础上再添加如下内容

from django_auth_ldap.config import GroupOfNamesType


# 以下内容都需要根据自己需要修改
AUTH_LDAP_GROUP_SEARCH = LDAPSearch(
    'ou=django,ou=groups,dc=example,dc=com',
    ldap.SCOPE_SUBTREE,
    '(objectClass=groupOfNames)',
)
AUTH_LDAP_GROUP_TYPE = GroupOfNamesType(name_attr='cn')

AUTH_LDAP_USER_FLAGS_BY_GROUP = {
    'is_active': "OU=Users,DC=domain,DC=com",
    'is_staff': "OU=Users,DC=domain,DC=com"
    # 这里没有添加is_superuser字段内容,因为不希望添加多余的超级用户。
}

如果发现在第一次能成功登陆,但是第二次登陆就报错:IntegrityError(1062, "Duplicate entry 'admin' for key 'username'"),那么在settings.py中加上AUTH_LDAP_USER_QUERY_FIELD="email"。

三) Token expiration

这部分不是讲某种验证方式。

因为token验证默认token是不会过期的,那么这就涉及到一个安全问题,如果有人抓到了token,那么就会有你的所有权限,于是需要修改token的过期和更新。

我们在url.py中设置token验证的模块'obtain_auth_token'来源于restframework,我们需要修改其方法:'authenticate_credentials'。

比如我可以在users的模块中的views.py中添加一个新的类,继承obtain_auth_token并重写方法。

from django.utils import timezone
from rest_framework import status
from rest_framework.response import Response
from rest_framework.authtoken.models import Token
from rest_framework.authtoken.views import ObtainAuthToken
import datetime


class ObtainExpiringAuthToken(ObtainAuthToken):

    def post(self, request, *args, **kwargs):
        serializer = self.serializer_class(data=request.data,
                                           context={'request': request})
        if serializer.is_valid(raise_exception=True):
            user = serializer.validated_data['user']
            token, created = Token.objects.get_or_create(user=user)

            utc_now = datetime.datetime.utcnow().replace(tzinfo=timezone.utc)
            if not created and token.created < utc_now - datetime.timedelta(hours=24):
                print("re-generate token for old one is expired")
                token.delete()
                token = Token.objects.create(user=user)
                token.created = datetime.datetime.utcnow()
                token.save()
            return Response({'token': token.key})
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

然后更新url.py,加上:

path('automation/api/api-token-auth/', u_views.ObtainExpiringAuthToken.as_view(), name='api_token_auth'),

现在请求token时只要账户和密码正确就会返回可用的token,如果token过期了,就会删除并重新创建,之前的token即使有人拿到了也不可用了。

不过还需要添加一个文件。因为当你登录时会更新token,那么如果几天不登录,此时token未更新,别人仍然可用。于是需要在需要token时都验证一遍有效性。

在utils里创建文件authentication.py以替换TokenAuthentication,添加如下内容。

# authentication.py

import datetime
from django.utils import timezone
from rest_framework.authentication import TokenAuthentication
from rest_framework import exceptions


class ExpiringTokenAuthentication(TokenAuthentication):

    def authenticate_credentials(self, key):
        model = self.get_model()
        try:
            token = model.objects.select_related('user').get(key=key)
        except model.DoesNotExist:
            raise exceptions.AuthenticationFailed('Invalid token.')

        if not token.user.is_active:
            raise exceptions.AuthenticationFailed('User inactive or deleted.')

        utc_now = datetime.datetime.utcnow().replace(tzinfo=timezone.utc)

        if token.created < utc_now - datetime.timedelta(hours=24):
            raise exceptions.AuthenticationFailed('Token has expired')

        return token.user, token

并修改settings.py。

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': [
        ...
        # 'rest_framework.authentication.TokenAuthentication',
        'utils.authentication.ExpiringTokenAuthentication',
        ...
    ],
    ...
}

到此就完成了token的过期和验证。token过期参考了https://stackoverflow.com/questions/14567586/token-authentication-for-restful-api-should-the-token-be-periodically-changed

下一篇:  Centos安装mongodb并配置用户和权限
上一篇:  Django restframework加Vue打造前后端分离的网站(五)rest和vue的初步结合

共有0条评论

添加评论

暂无评论