小逆向玩转选课系统

# 作为一名大学生,每次最烦恼的就是选课的时候选课系统巨卡无比,而且有些选课轮次还是先到先得,这让抢课开放时还在认真学习导致网页打不开,登陆不上的我如何是好?但是作为一名爱好者,我们不谈外挂,只是尽量去模拟这个浏览器和人的行为,去尝试自动化的替我们选课。

一、如何实现选课

选课无非就是在浏览器中打开网页,输入账号和密码,有时还有验证码,点击登录后,在一些课程里去选择我心仪的那一门课,点击选择提交。那么这一些列的行为又是如何传递到后端服务器的呢?其实只要你打开开发者工具,就会看到很多的请求在你进行一些列操作的时候进行数据的收发,所以只要能模拟出这些请求,然后将其中的一些信息进行替换,在加上一些定时辅助手段,在规定的时间运行我们的脚本。

二、找出请求

# 本次示例使用的选课系统由http://xk.ynu.edu.cn/xsxkapp/sys/xsxkapp/*default/index.do

独家赞助。

(一)登录

打开网页,打开开发者工具,随便输入一个账号密码,捕获带有login关键字的那条请求。查看标头你会发现这是一个get请求,再看载荷,timestrap,loginName,loginPwd,verifyCode,vtoken,一共就这几个。不难发现timestrap就是时间戳,loginName和loginPwd对应你的账号密码,verifyCode则是你刚刚输入的的验证码,那么这个vtoken又是什么呢?其实我们换个方式就很容易推测出来了,后端如何校验我输入的验证码是不是正确的呢?所以当我们点击验证码更换验证码时会再次多出一条请求,在这条请求的响应中就有token这样一条,形式一致。之后还有一条图片请求其中就有这个token。再次输入账号密码验证码登录,捕获新的请求,再看载荷,vtoken就是刚刚获得新的token。至此,登录模拟分析完成!

(二)选课

在成功后我们进入选课页面,随便选一个可以选的课,捕获该选课请求,发现这是一个post的请求,载荷是

addParam:

{"data":{"operationType":"1","studentCode":"********","electiveBatchCode":"a0d5fd9c11b64029879ba9d65db507ea","teachingClassId":"*********","isMajor":"1","campus":"05","teachingClassType":"FANKC"}}

studentCode 就是学生账号即学生ID,teachingClassId就是课程标识号,刷新可选课程的界面抓一条数据就可以得到,FANKC是课程类别,a0d5fd9c11b64029879ba9d65db507ea是选课轮次有关的。FANKC和a0d5fd9c11b64029879ba9d65db507ea如果是只是在特定轮次和类别才使用的话可以写死。这条请求的数据是{"data":null,"code":"1","msg":"添加选课志愿成功","timestamp":"1719886397774"}格式的,可以对msg进行校验,判断选课是否成功和查看选课信息。

三、python实现

(一)模拟登录

构造登录请求不难,可以直接将该请求以cmd格式复制,然后找一个提供转换的站点,直接转为python的代码。

这里注意,每次的验证码都是动态更新的,所以我们需要三个请求,一个请求得到一个新的验证码token,一个是请求得到验证码,另外一个进行登录请求。为了追求代码的美观和简化的话可以对header进行一定简化和合并。在这里我用正则表达式获取了登录请求的的cookie的一部分值,是为了为后续的选课请求做准备。登录获得的_WEU和JSESSIONID才能让后端正确识别和处理我的请求。这里的验证码识别使用ddddocr。

def get_token():
    """
    获取一个token
    用于请求验证码和登录时载荷,校验验证码
    :return:
    """
    headers = {
        # 这里的cookie是为了登良获取一个token,这里为了方便,直接写死也行
        "Accept": "application/json, text/javascript, */*; q=0.01",
        "Accept-Language": "zh-CN,zh;q=0.9",
        "Cookie": "_WEU=zGqZ8jwIRhXYuPUV62BK9h4m7CPP0bU6qKccdYd6R4L.; JSESSIONID=LXkqgcNSE_Lh8jB2SDbPkWNFwA7K8sRSRsmUKmN1P74NPK2pr3W1!-1276073466",
        "Proxy-Connection": "keep-alive",
        "Referer": "http://xk.ynu.edu.cn/",
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
        "X-Requested-With": "XMLHttpRequest"
    }
    url = "http://xk.ynu.edu.cn/xsxkapp/sys/xsxkapp/student/4/vcode.do"
    params = {
        "timestamp": str(int(time.time() * 1000))
    }
    response = requests.get(url, headers=headers, params=params, verify=False)

    return json.loads(response.text)['data']['token']


def get_sign(vt):
    """
    获取验证码
    利用ddddocr识别验证码
    :return:
    """
    headers = {
        "Accept": "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8",
        "Accept-Language": "zh-CN,zh;q=0.9",
        "Cookie": "_WEU=Cxnbl8nIYhBmODczGWL0dP_hpKp5BiQV00j0UEJXnzDUEhXGUgB_WPD4RZ3fNKvGdCUuGc4s6dP.; JSESSIONID=LXkqgcNSE_Lh8jB2SDbPkWNFwA7K8sRSRsmUKmN1P74NPK2pr3W1!-1276073466",
        "Proxy-Connection": "keep-alive",
        "Referer": "http://xk.ynu.edu.cn/",
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"
    }
    url = "http://xk.ynu.edu.cn/xsxkapp/sys/xsxkapp/student/vcode/image.do"
    params = {
        "vtoken": vt
    }
    response = requests.get(url, headers=headers, params=params, verify=False)
    Docr = ddddocr.DdddOcr()
    return Docr.classification(response.content)


def login(tk, sign):
    """
    登录获得token
    :param tk:
    :param sign:
    :return:
    """
    headers = {
        "Accept": "*/*",
        "Accept-Language": "zh-CN,zh;q=0.9",
        "Cookie": "_WEU=Cxnbl8nIYhBmODczGWL0dP_hpKp5BiQV00j0UEJXnzDUEhXGUgB_WPD4RZ3fNKvGdCUuGc4s6dP.; JSESSIONID=LXkqgcNSE_Lh8jB2SDbPkWNFwA7K8sRSRsmUKmN1P74NPK2pr3W1!-1276073466",
        "Proxy-Connection": "keep-alive",
        "Referer": "http://xk.ynu.edu.cn/",
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
        "X-Requested-With": "XMLHttpRequest"
    }
    url = "http://xk.ynu.edu.cn/xsxkapp/sys/xsxkapp/student/check/login.do"
    params = {
        "timestrap": str(int(time.time() * 1000)),
        "loginName": "loginName",
        "loginPwd": "loginPwd",
        "verifyCode": sign,
        "vtoken": tk
    }
    response = requests.get(url, headers=headers, params=params, verify=False)
    # 获取登录返回的cookie
    _WEU = re.findall(r'_WEU=(.*?);', response.headers['Set-Cookie'])[0]
    JSESSIONID = re.findall(r'JSESSIONID=(.*?);', response.headers['Set-Cookie'])[0]

    return json.loads(response.text)['data']['token'], [_WEU, JSESSIONID]

(二)模拟选课

提前准备好需要选的课程的课程号,然后同理构造请求。cookie的值需要根据上述请求的值进行更新,注意替换studentCode。

def get_course(tk, teachingClassId, ck, teachingClassType):
    """
    :param teachingClassType:
    :param ck:
    :param tk:
    :param teachingClassId:
    :return:
    """
    headers = {
        "Accept": "application/json, text/javascript, */*; q=0.01",
        "Accept-Language": "zh-CN,zh;q=0.9",
        "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
        "Cookie": f"_WEU={ck[0]}; JSESSIONID={ck[1]}",
        "Origin": "http://xk.ynu.edu.cn",
        "Proxy-Connection": "keep-alive",
        "Referer": "http://xk.ynu.edu.cn/xsxkapp/sys/xsxkapp/*default/curriculavariable.do?token=1db3fe41-9f8b-45c3-b80b-8ad96f998952",
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
        "X-Requested-With": "XMLHttpRequest",
        "token": tk
    }
    url = "http://xk.ynu.edu.cn/xsxkapp/sys/xsxkapp/elective/volunteer.do"
    data = {
        "addParam": f'{{"data":{{"operationType":"1","studentCode":"your_studentCode","electiveBatchCode":"a0d5fd9c11b64029879ba9d65db507ea","teachingClassId":"{teachingClassId}","isMajor":"1","campus":"05","teachingClassType":{teachingClassType},"chooseVolunteer":"1"}}}}'
    }
    response = requests.post(url, headers=headers, data=data, verify=False)
    msg = json.loads(response.text)['msg']
    print(msg)

(三)模拟设计

可以事先获取需要抢的的课程号以及对应的信息,可以设置列表,利用循环对每个课程号进行请求,请求次数和判断成功与否可以对msg的内容进行判定。建议一轮或者是多次失败可以直接退出,在外套的循环中重新从登录开始。如果是需要定时的话可以利用下面的代码,几点开始的话我们提前几秒就开始模拟。

    while True:
        now = datetime.datetime.now()
        if now.hour == 8 and now.minute == 59:
            start()
            break
        else:
            time.sleep(20)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值