记一次自动创建禅道用户实践

 

背景

需求:实现自己的研发协同平台,可以一键免密登录禅道、自动创建禅道用户,从而打通各个系统,达到统一化管理。

禅道版本:专业版 9.0.3

python版本:3.8.0

实践

1. 打开禅道后台管理页面,查看禅道API接口

针对上述需求,开始进行摸索。因禅道数据库管理严格,不能直接操作数据库写入数据,且为了更友好,决定通过禅道提供的API,进行用户创建。

 

 

这里不得不吐槽一下禅道的API接口,虽然提供了创建用户的这个API地址,但是参数很明显不全,我创建用户的用户名,密码啥的呢?也是醉了...

看样子只查阅API文档,很明显解决不了问题。

2. 查看禅道创建用户的页面,看看是否可以通过后台接口请求,模拟提交表单创建用户

找到禅道创建用户的页面,并点击“保存”,触发提交表单

 

这里因为我没有填写任何信息,故意提交验证失败的表单,来观察下创建用户需要哪些参数,从上图可以看到包括不少参数,很多都是页面上的必填字段,字段内容也都很好理解,一眼就知道对应的是哪个输入框,我继续填写一些简单信息,再次点击“保存”提交表单试下。

这里我故意将我的密码输入错误,来看下对应的输入字段信息。在此发现了一个比较难办的事情,这个密码一看就是被加密过了,加密的算法是啥呢?我后台接口请求的话,肯定是要知道这个加密规则的,不然我提交明文过去肯定会有问题。(其实这里我还有一点想要吐槽,禅道登录也没有密码加密,我之前模拟登录没做密码加密,在这里却要加密。。。)

3. 打开F12,查看sources源码寻找密码加密规则

没办法了,现在首要任务是要解决,这个密码到底该怎么加密,这个加密规则如何获取呢。当时我想到了两种方案,第一,是看禅道源码,禅道是PHP写的,简单语法我还能看的明白,但是源码厂商没有提供,只能登录禅道服务器去看,而且我也不知道是哪台机器;第二,打开F12看下禅道加载的源码,先看看能不能找出点儿线索。这里我想着先F12本地看下,找不到在看源码吧。说干就干,打开sources,搜索password1

一番寻找过后,发现了一个很重要的线索,这坨代码,不就是加密密码的代码嘛,先将密码MD5,然后加上rand,等等,这个rand是啥,我记得页面上没有这个参数吧,再次寻找

嗯?是个表单里的隐藏字段,value是一坨数字,并不是时间戳,这个是啥呢?写死的吗?刷新了一下页面,我草,变了!!!

这个是个什么值呢,每次页面刷新后还不一样,这个rand的值的规则是啥,不会真如其名,就是个随机数吧???如果是这样,那我自己在后台代码里面随机个数字看看是否可行,附上代码

import os
import sys
import json
import time
import random
import hashlib
import logging
import requests
from enum import Enum


if not os.environ.get('DJANGO_SETTINGS_MODULE'):
    import django
    root_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
    sys.path.extend([root_path])
    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings.dev")
    django.setup()

from django.conf import settings
from libs import file_lock

LOGGER = logging.getLogger(__name__)


class ZentaoAccountException(Exception):
    """
    禅道账号异常
    """
    def __init__(self, msg):
        super().__init__(self)
        self.msg = msg

    def __str__(self):
        return "zentao account exception, %s" % self.msg


class ZentaoApi(requests.Session):
    """
    禅道API接口
    """
    class BypassLoginStatusCodeChoices(Enum):
        SUCCESS = 200
        ACCOUNT_NOT_EXIST = 406

    def __init__(self, account=settings.CONF_ATTR.zentao_web_user):
        super().__init__()
        self.base_params = {
            't': 'json'
        }
        self.account = account
        self.base_url = settings.CONF_ATTR.zentao_url

    def _get(self, url, **kwargs):
        """
        通用get方法
        :param url:
        :param params:
        :return:
        """
        resp = self.get(url, **kwargs)
        LOGGER.info("GET %s status:%s, cookies:%s", url, resp.status_code, self.cookies)
        resp.encoding = 'uft-8'
        return resp.status_code, resp.text

    def _post(self, url, data=None, **kwargs):
        """
        通用post方法
        :return:
        :param url:
        :param params:
        :return:
        """
        resp = self.post(url, data=data, **kwargs)
        LOGGER.info("POST %s body:%s, status:%s, text:%s, cookies:%s",
                    url, data, resp.status_code, resp.text, self.cookies)
        resp.encoding = 'uft-8'
        return resp.status_code, resp.text

    def _login_bypass(self, account=None):
        """
        免密登录
        :return:
        """
        err_code = self.BypassLoginStatusCodeChoices.SUCCESS.value
        account = self.account if account is None else account

        lock_obj = file_lock.FileLock()
        # 加锁
        lock_obj.acquire()

        serializer_zentao_cookie = lock_obj.read()
        if account == settings.CONF_ATTR.zentao_web_user and serializer_zentao_cookie:
            self.cookies.set('zentaosid', serializer_zentao_cookie)
            LOGGER.info('already logged, skip...')
            # 释放锁
            lock_obj.release()
            return err_code

        # 禅道免密登录算法
        timestamp = int(time.time())
        token = self._md5(f'{settings.CONF_ATTR.zentao_bypass_code}{settings.CONF_ATTR.zentao_bypass_key}{timestamp}')
        url = settings.CONF_ATTR.zentao_bypass_url.format(account=account,
                                                          code=settings.CONF_ATTR.zentao_bypass_code,
                                                          timestamp=timestamp,
                                                          token=token)
        resp = self.get(url)
        LOGGER.info("bypass login executed, url:%s, cookies:%s, text:%s", url, self.cookies, resp.text)
        if 'self.location' not in resp.text:
            err_code = resp.json().get('errcode')

        # 禅道免密登录成功,设置cookies缓存
        if account == settings.CONF_ATTR.zentao_web_user and \
                err_code == self.BypassLoginStatusCodeChoices.SUCCESS.value:
            zentaosid = requests.utils.dict_from_cookiejar(self.cookies).get('zentaosid')
            lock_obj.write(zentaosid)
        # 释放锁
        lock_obj.release()
        return err_code


    def _md5(self, value):
        """
        md5加密
        :param value:
        :return:
        """
        m = hashlib.md5()
        m.update(value.encode('utf-8'))
        return m.hexdigest()

    def create_zentao_account(self):
        """
        创建禅道账号
        :return:
        """

        # 使用管理员登录禅道
        self._login_bypass(account=settings.CONF_ATTR.zentao_web_user)
        # 根据部门名称,获取部门id
        dept_id = 47

        # 根据用户所在部门,获取用户职位
        role_name = '研发'

        # 根据用户职位,获取用户组织
        group_id = 2

        # 随机数
        verify_rand = str(random.randint(1000, 100000))

        # 密码加密
        password = self._md5(settings.CONF_ATTR.zentao_default_account_password) + verify_rand
        verify_password = self._md5((self._md5(settings.CONF_ATTR.zentao_web_password) + verify_rand))

        data = {
            'dept': dept_id,
            'account': self.account,
            'password1': password,
            'password2': password,
            'realname': '朱宝',
            'join': time.strftime('%Y-%m-%d'),
            'role': role_name,
            'group': group_id,
            'email': '',
            'gender': 'm',
            'verifyPassword': verify_password,
            'passwordStrength': 2,
        }
        params = dict(m='user', f='create')
        status, result = self._post(self.base_url, data=data, params=params)
        print('create_zentao_account executed, status:%s, result:%s' % (status, result))


if __name__ == "__main__":
    obj = ZentaoApi()
    print(obj.create_zentao_account())

执行后出现

很明显,这个随机数是不行的,那我怎么获取这个rand呢?看样子这个rand是每次页面加载后,后端代码生成的一个随机数,当我点击保存提交表单时,禅道后端代码会拿着这个随机数进行校验,我自己伪造的rand肯定和它后端代码生成的不一样,导致校验失败了,这下可咋办?真是让人头大...

4. 模拟页面加载,正则解析出rand值

抓抓耳,挠挠腮,上厕所,喝喝水。咦?我是不是可以模拟页面加载,然后拿到这个页面上的rand,在拿这个rand去加密密码呢?很简单,我只要在我创建用户的代码前,去访问下禅道创建用户的页面,然后正则解析拿到这个rand不就行了么?说干就干,附上代码

import os
import sys
import re
import json
import time
import random
import hashlib
import logging
import requests
from enum import Enum


if not os.environ.get('DJANGO_SETTINGS_MODULE'):
    import django
    root_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
    sys.path.extend([root_path])
    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings.dev")
    django.setup()

from django.conf import settings
from libs import file_lock

LOGGER = logging.getLogger(__name__)


class ZentaoAccountException(Exception):
    """
    禅道账号异常
    """
    def __init__(self, msg):
        super().__init__(self)
        self.msg = msg

    def __str__(self):
        return "zentao account exception, %s" % self.msg


class ZentaoApi(requests.Session):
    """
    禅道API接口
    """
    class BypassLoginStatusCodeChoices(Enum):
        SUCCESS = 200
        ACCOUNT_NOT_EXIST = 406

    def __init__(self, account=settings.CONF_ATTR.zentao_web_user):
        super().__init__()
        self.base_params = {
            't': 'json'
        }
        self.account = account
        self.base_url = settings.CONF_ATTR.zentao_url

    def _get(self, url, **kwargs):
        """
        通用get方法
        :param url:
        :param params:
        :return:
        """
        resp = self.get(url, **kwargs)
        LOGGER.info("GET %s status:%s, cookies:%s", url, resp.status_code, self.cookies)
        resp.encoding = 'uft-8'
        return resp.status_code, resp.text

    def _post(self, url, data=None, **kwargs):
        """
        通用post方法
        :return:
        :param url:
        :param params:
        :return:
        """
        resp = self.post(url, data=data, **kwargs)
        LOGGER.info("POST %s body:%s, status:%s, text:%s, cookies:%s",
                    url, data, resp.status_code, resp.text, self.cookies)
        resp.encoding = 'uft-8'
        return resp.status_code, resp.text

    def _login_bypass(self, account=None):
        """
        免密登录
        :return:
        """
        err_code = self.BypassLoginStatusCodeChoices.SUCCESS.value
        account = self.account if account is None else account

        lock_obj = file_lock.FileLock()
        # 加锁
        lock_obj.acquire()

        serializer_zentao_cookie = lock_obj.read()
        if account == settings.CONF_ATTR.zentao_web_user and serializer_zentao_cookie:
            self.cookies.set('zentaosid', serializer_zentao_cookie)
            LOGGER.info('already logged, skip...')
            # 释放锁
            lock_obj.release()
            return err_code

        # 禅道免密登录算法
        timestamp = int(time.time())
        token = self._md5(f'{settings.CONF_ATTR.zentao_bypass_code}{settings.CONF_ATTR.zentao_bypass_key}{timestamp}')
        url = settings.CONF_ATTR.zentao_bypass_url.format(account=account,
                                                          code=settings.CONF_ATTR.zentao_bypass_code,
                                                          timestamp=timestamp,
                                                          token=token)
        resp = self.get(url)
        LOGGER.info("bypass login executed, url:%s, cookies:%s, text:%s", url, self.cookies, resp.text)
        if 'self.location' not in resp.text:
            err_code = resp.json().get('errcode')

        # 禅道免密登录成功,设置cookies缓存
        if account == settings.CONF_ATTR.zentao_web_user and \
                err_code == self.BypassLoginStatusCodeChoices.SUCCESS.value:
            zentaosid = requests.utils.dict_from_cookiejar(self.cookies).get('zentaosid')
            lock_obj.write(zentaosid)
        # 释放锁
        lock_obj.release()
        return err_code


    def _md5(self, value):
        """
        md5加密
        :param value:
        :return:
        """
        m = hashlib.md5()
        m.update(value.encode('utf-8'))
        return m.hexdigest()

    def _get_account_verify_rand(self, content):
        """
        获取创建禅道用户所需要的verify_rand值
        :param content:
        :return:
        """
        verify_rand_list = re.findall(r"id='verifyRand' value='(\d+)'", content)
        LOGGER.info('_get_account_verify_rand executed, verify_rand_list:%s', verify_rand_list)
        if verify_rand_list:
            return verify_rand_list[0]
        return None

    def create_zentao_account(self):
        """
        创建禅道账号
        :return:
        """

        # 使用管理员登录禅道
        self._login_bypass(account=settings.CONF_ATTR.zentao_web_user)
        # 根据部门名称,获取部门id
        dept_id = 47

        # 根据用户所在部门,获取用户职位
        role_name = '研发'

        # 根据用户职位,获取用户组织
        group_id = 2

        # 获取禅道verify_rand,用于加密密码
        status, result = self._get(self.base_url, params=dict(m='user', f='create'))
        verify_rand = self._get_account_verify_rand(result)

        # 密码加密
        password = self._md5(settings.CONF_ATTR.zentao_default_account_password) + verify_rand
        verify_password = self._md5((self._md5(settings.CONF_ATTR.zentao_web_password) + verify_rand))

        data = {
            'dept': dept_id,
            'account': 'helloworld123',
            'password1': password,
            'password2': password,
            'realname': '朱宝',
            'join': time.strftime('%Y-%m-%d'),
            'role': role_name,
            'group': group_id,
            'email': '',
            'gender': 'm',
            'verifyPassword': verify_password,
            'passwordStrength': 2,
        }
        params = dict(m='user', f='create')
        status, result = self._post(self.base_url, data=data, params=params)
        print('create_zentao_account executed, status:%s, result:%s' % (status, result))


if __name__ == "__main__":
    obj = ZentaoApi()
    print(obj.create_zentao_account())





执行后,没有报错了

看下用户是否创建成功了

哈哈,用户已经创建成功了,在验证下是否可以正常登录

哎,可以登录,用户创建成功喽!!!

总结

1.上述解决问题的方案,可以给禅道二次开发的用户提供点经验,也可以给初级爬虫用户提供点思路;

2.遇到问题需要不断探索,一步一步地深挖最终解决问题的快乐,只有实践了才能体会到;

3.如果自己开发系统对外服务,接口文档还是要好好写的,不然会被吐槽、被骂娘,^_^。

 

 

 

 

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值