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