【测试平台学习3】 Django 实现LDAP连接Freeipa登录,重写AuthToken
前言
初步使用了Django搭建了一个后端接口,本文单独就django的登录进行解读。
关于登录
Django的登录编写是比较简单的,主要现在做系统或者平台一般是接入成熟的登录,例如: 钉钉、freeipa、公司内部账号管理系统等等。
关于接入钉钉的登录,网上很多,本文就不再赘述,主要介绍一种通过Django对接freeipa的方式,登录认证方式通过LDAP。
1. 安装ldap3
(网上很多介绍让你安装另外一种方式Python-LDAP的,你安装下去会吐血。基本不可用了,会出很多问题)
pip install ldap3
2. settings配置
这些值需要和你系统内的FREEIPA对齐
LDAP_AUTH_URL
LDAP_AUTH_SEARCH_BASE
LDAP_AUTH_URL = 'ldap://ipa.xxx.cn:389' LDAP_AUTH_USE_TLS = False LDAP_AUTH_SEARCH_BASE = 'dc=in,dc=***,dc=cn' LDAP_AUTH_OBJECT_CLASS = "*" LDAP_AUTH_USER_FIELDS = { "username": "uid", } # LDAP_AUTH_USER_LOOKUP_FIELDS = ("uid",) LDAP_AUTH_CLEAN_USER_DATA = "django_python3_ldap.utils.clean_user_data" LDAP_AUTH_SYNC_USER_RELATIONS = "django_python3_ldap.utils.sync_user_relations" LDAP_AUTH_FORMAT_SEARCH_FILTERS = "django_python3_ldap.utils.format_search_filters" LDAP_AUTH_FORMAT_USERNAME = "django_python3_ldap.utils.format_username_openldap"
3. 登录脚本的编写
这里面有几个坑:
3.1 登录是接入式的,你需要新增一个用户UserToken表来存储用户数据和token以及设置用户token的失效时间等等
加入你用django自带的user表【亲测,非常不好用,里面的字段都定制化了,例如包含了first_name,last_name,email 但是和可能你接入的用户完全没有这些信息】
还有就是自带的user_token 后续你使用的请求里面带token或者id会不方便, 它是用request.user.pk 去携带用户id的,实际使用的时候这个pk值很可能是null (原因可能就是你没有first_name什么的)
解决方案: 新增一个UserToken表, 重写AuthToken 逻辑,加上token过期判断
3.2 LDAP连接的时候报错,[LDAP bind failed: LDAPInvalidCredentialsResult - 49 - invalidCredentials]
解决方案:修改 依赖包site-packages/django_python3_ldap/ldap.py
原因是django-ldap里面设置了user查询的参数默认是错误的,一定要修改成你项目组Freeipa要求的形式,如下。
if kwargs: password = kwargs.pop("password") username = format_username(kwargs) # 需要改造 username='uid={},cn=***,cn=***,dc=in,dc=***,dc=cn'.format(kwargs.pop("username"))
class Login_LDAP(APIView):
# 访问登录接口不需要授权 authentication_classes = [] permission_classes = ()
# 序列化请求 serializer_class = AuthTokenSerializer def post(self, request, *args, **kwargs): """ 用户登录 :param request: :param args: :param kwargs: :return: """ serializer = self.serializer_class(data=request.data, context={"request": request}) serializer.is_valid(raise_exception=True) user = serializer.validated_data["username"] password = serializer.validated_data["password"] print(user, password) server = Server(host=ldap_host, port=ldap_port, use_ssl=False, get_info='ALL') dn = 'uid={},cn=***,cn=***,'.format(user) + search_dn # 通过用户名和密码去访问Freeipa,获取Freeipa的认证信息 conn = Connection(server, dn, password, auto_bind=True) if conn.result["description"] == "success": time_now = datetime.datetime.now() user_instance = UserToken.objects.filter(username=user) # 生成一个MD5格式的token,切记别用Django自带的token
token = md5(user) if user_instance:
# 更新你的用户表 UserToken.objects.update(username=user, token=token, expiration_time=time_now) else: UserToken.objects.create(username=user, token=token, expiration_time=time_now) return JsonResponse(data=token, code="200", msg="成功") else: return JsonResponse(data={'用户Freeipa登录失败'}, code="400", msg="失败") obtain_auth_token = Login_LDAP.as_view()
4. 重写认证AuthToken
4.1 这里可以用 cache 存储登录的信息, 发请求的时候调用这里,使用 request.user.id 即可调用用户id , request.user.username则是用户名
4.2 设置了token超时时间,可以配置到settings.py里面去, REST_FRAMEWORK_TOKEN_EXPIRE_MINUTES=60
4.3 其他接口调用该认证则是在接口下使用: authentication_classes = [TokenAuthentication,] 另外要设置到settings.py 里面添加这个
'DEFAULT_AUTHENTICATION_CLASSES': [
# 'rest_framework.authentication.BasicAuthentication',
'utils.authentication.TokenAuthentication', ],
from django.conf import settings
from rest_framework.authentication import BaseAuthentication
from django.core.cache import cache
from rest_framework.exceptions import APIException
EXPIRE_MINUTES = getattr(settings, 'REST_FRAMEWORK_TOKEN_EXPIRE_MINUTES', 1) class TokenAuthentication(BaseAuthentication): """Set up token expired time""" def authenticate(self, request): token = request.META.get('HTTP_TOKEN') # 将token放入请求头中 user = cache.get(token) token_obj = UserToken.objects.filter(token=token).first() user = {"user": user, "id": token_obj.id} if user['user'] is not None: print(user) return user, token if not token_obj: raise APIException('Auth Failed!!') create_time = token_obj.expiration_time now_time = datetime.datetime.now() delta = now_time - create_time if delta < datetime.timedelta(days=EXPIRE_MINUTES): remain_time = datetime.timedelta(days=EXPIRE_MINUTES) - delta cache.set(token_obj.token, token_obj.username, min(remain_time.total_seconds(), 3600 * 24)) user = { "user": cache.get(token), "id": token_obj.id } print("user:", user) return user, token else: raise APIException('认证超时')
5. 设置UserToken的Models
5.1 本来django有内置的User表,问题是它不适合大部分的情况,上面也说了原因,所以才有如下的设置。其他表在调用用户表的时候使用外键链接既可
5.2 注意这里的外键只能是int型的id,有些设置的时候返回一些str类型会报错,包括序列化的时候。
5.3 比较诡异的是在设置如下的用户表,进行数据库迁移,表名没有按照我写的db_name来, 而是 【appname_usertoken】
# 存放用户登录成功后的token class UserToken(models.Model): id = models.AutoField(primary_key=True, verbose_name='id主键') username = models.CharField(max_length=50, unique=True, verbose_name='用户名') token = models.CharField(max_length=64, verbose_name="用户token") expiration_time = models.DateTimeField(default=datetime.now, verbose_name="Token过期时间") class Mate: db_table = "test_user_token" verbose_name = "用户Token" verbose_name_plural = verbose_name def __str__(self): return self.token # 通过外键关联其他的SQL表 user = models.ForeignKey(UserToken, on_delete=models.CASCADE, max_length=50, verbose_name='创建人')
结语
Django 提供了太多定制化的操作,不是很好用,单单对于初学者来说,写个用户认证+Token都这么麻烦... 优缺点总结如下:
1. 框架本身提供的model-view-urls-form 这一套可以和spring mvc架构媲美,而且简易程度差不多,用户不用写sql ,而可以关注业务本身
2. 框架提供的User-Token 过于定制化了,肯定不适用于公司项目,要开发人员自行去修改大量的配置以及重写等等,这一块是完全不合适的。
3. 用户千万别一不小心升级django, 升级后会造成一系列不适配的问题,只能回退原版本,这一点来说java的spring是碾压,因为java的高版本肯定是兼容低版本的。
未完待续...