【测试平台学习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的高版本肯定是兼容低版本的。

未完待续...