目录

##1. 账号密码登录
##2. 二维码登录

因为要登录学某通查看一些东西,看着登录界面心血来潮,于是乎简单做一下逆向分析。此教程仅为学习记录。

1. 账号密码登录

先随便输入一下账号密码,调用了这个接口b0d395da-ce43-4b98-b99b-e0a28920b90f.png

可以看到账号密码都进行了base64编码,看一下函数调用栈。79075ee3-7c7e-4974-b1de-50b7baee5cb6.png

可以看到在发送XHR请求前调用了两个方法

   loginByPhoneAndPwd
   loginByPhoneAndPwdSubmit

跟进loginByPhoneAndPwdSubmit方法,打个断点。很不巧的就看到了输入的明文以及调用的加密函数,

0e4284c5-4f80-4783-bcd0-276534cb4aab.png
跳入加密函数中
2025-11-16T10:49:25.png
可以看到使用的是一个AES-CBC加密,并且iv向量与key相等都为

u2oh6Vu^HWe4_AES

function encryptByAES(message, key) {
    let CBCOptions = {
        iv: CryptoJS.enc.Utf8.parse(key),
        mode: CryptoJS.mode.CBC,
        padding: CryptoJS.pad.Pkcs7
    };
    let aeskey = CryptoJS.enc.Utf8.parse(key);
    let secretData = CryptoJS.enc.Utf8.parse(message);
    let encrypted = CryptoJS.AES.encrypt(
        secretData,
        aeskey,
        CBCOptions
    );
    return CryptoJS.enc.Base64.stringify(encrypted.ciphertext);
}

加密之后再对密文进行base64,由于账号密码都是同种加密方式,因此在了解加密方式和密钥之后,就可以自己封装实现了。

2. 二维码登录

没有看到请求返回二维码的XHR,所以登录二维码是在生成html的时候直接嵌入的。fb2764bd-e2a4-4104-ac96-c0c640fb878e.png
并且二维码是由uuid来进行获取。

扫描一下二维码看看提交的参数
2025-11-16T11:03:30.png

这里用到的uuid和enc参数,这里我们不需要单独去分析uuid和enc是怎么生成的,因为这个是返回html的时候就写在里面的,所以只需要爬取页面,获得这两个参数就行了。

一直看到浏览器不断的在请求https://passport2.chaoxing.com/getauthstatus/v2来获取登录结果
2025-11-16T11:07:45.png
同时也是根据uuid和enc来进行判断
2025-11-16T11:09:39.png

被扫描之后判断用户是否确认登录再进行跳转。

分别使用php、python、node.js对上面的流程进行代码封装之后的运行结果。
2025-11-16T11:20:21.png

下面直接贴出代码
php

<?php
mb_internal_encoding('UTF-8');
mb_http_output('UTF-8');
mb_regex_encoding('UTF-8');

date_default_timezone_set('Asia/Shanghai');

if (php_sapi_name() === 'cli') {
    if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
        @exec('chcp 65001 >nul 2>&1');
    }
}


function encryptByAES($message, $key) {
    $key = mb_substr($key, 0, 16, 'UTF-8');
    $iv = $key;
    $encrypted = openssl_encrypt(
        $message,
        'AES-128-CBC',
        $key,
        OPENSSL_RAW_DATA,
        $iv
    );
    return base64_encode($encrypted);
}


function loginWithPassword($username, $password) {
    $url = 'https://passport2.chaoxing.com/fanyalogin';
    $encryptKey = "u2oh6Vu^HWe4_AES";
    $encryptedUsername = encryptByAES($username, $encryptKey);
    $encryptedPassword = encryptByAES($password, $encryptKey);
    $postData = http_build_query([
        'fid' => '-1',
        'uname' => $encryptedUsername,
        'password' => $encryptedPassword,
        'refer' => 'http%3A%2F%2Fi.mooc.chaoxing.com%2Fspace%2Findex%3Fnull',
        't' => 'true',
        'forbidotherlogin' => '0',
        'validate' => '',
        'doubleFactorLogin' => '0',
        'independentId' => '0',
        'independentNameId' => '0'
    ]);
    
    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_POST            => true,
        CURLOPT_POSTFIELDS      => $postData,
        CURLOPT_RETURNTRANSFER  => true,
        CURLOPT_HEADER          => true,
        CURLOPT_ENCODING        => '',
        CURLOPT_SSL_VERIFYPEER  => false,
        CURLOPT_SSL_VERIFYHOST  => false,
        CURLOPT_TIMEOUT         => 10,
        CURLOPT_HTTPHEADER      => [
            'Content-Type: application/x-www-form-urlencoded',
            'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36 Edg/138.0.0.0',
            'Accept: application/json, text/plain, */*',
            'Origin: https://passport2.chaoxing.com',
            'Referer: https://passport2.chaoxing.com/login'
        ]
    ]);
    
    $response = curl_exec($ch);
    if ($response === false) {
        curl_close($ch);
        return false;
    }
    
    $headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
    $headers = substr($response, 0, $headerSize);
    $body = substr($response, $headerSize);
    
    curl_close($ch);
    
    $result = json_decode($body, true);
    if ($result === null) {
        return false;
    }
    
    $cookies = [];
    if (preg_match_all('/set-cookie:\s*([^\r\n]+)/i', $headers, $matches)) {
        foreach ($matches[1] as $cookieLine) {
            // 提取 cookie 键值对(第一个分号之前的部分)
            $cookieParts = explode(';', $cookieLine, 2);
            $cookie = trim($cookieParts[0]);
            if (!empty($cookie)) {
                $cookies[] = $cookie;
            }
        }
    }
    
    return [
        'status' => $result,
        'cookies' => $cookies,
        'cookieString' => implode('; ', $cookies)
    ];
}

/**
 * 检查二维码扫描状态
 */
function checkQrCodeStatus($uuid, $enc) {
    $url = 'https://passport2.chaoxing.com/getauthstatus/v2';
    
    $postData = http_build_query([
        'enc' => $enc,
        'uuid' => $uuid,
        'doubleFactorLogin' => '0'
    ]);
    
    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_POST            => true,
        CURLOPT_POSTFIELDS      => $postData,
        CURLOPT_RETURNTRANSFER  => true,
        CURLOPT_HEADER          => true,
        CURLOPT_ENCODING        => '',
        CURLOPT_SSL_VERIFYPEER  => false,
        CURLOPT_SSL_VERIFYHOST  => false,
        CURLOPT_TIMEOUT         => 10,
        CURLOPT_HTTPHEADER      => [
            'Content-Type: application/x-www-form-urlencoded',
            'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36 Edg/138.0.0.0',
            'Accept: application/json, text/plain, */*',
            'Origin: https://passport2.chaoxing.com',
            'Referer: https://passport2.chaoxing.com/login'
        ]
    ]);
    
    $response = curl_exec($ch);
    if ($response === false) {
        curl_close($ch);
        return false;
    }

    $headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
    $headers = substr($response, 0, $headerSize);
    $body = substr($response, $headerSize);
    curl_close($ch);
    $result = json_decode($body, true);
    if ($result === null) {
        return false;
    }
    
    $cookies = [];
    if (preg_match_all('/set-cookie:\s*([^\r\n]+)/i', $headers, $matches)) {
        foreach ($matches[1] as $cookieLine) {
            $cookieParts = explode(';', $cookieLine, 2);
            $cookie = trim($cookieParts[0]);
            if (!empty($cookie)) {
                $cookies[] = $cookie;
            }
        }
    }
    
    return [
        'status' => $result,
        'cookies' => $cookies,
        'cookieString' => implode('; ', $cookies)
    ];
}


function getCourseList($cookieString) {
    $url = 'https://mooc1-1.chaoxing.com/mooc-ans/visit/courselistdata';
    
    $postData = http_build_query([
        'courseType' => '1',
        'courseFolderId' => '0',
        'baseEducation' => '0',
        'superstarClass' => '',
        'courseFolderSize' => '1'
    ]);
    
    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_POST            => true,
        CURLOPT_POSTFIELDS      => $postData,
        CURLOPT_RETURNTRANSFER  => true,
        CURLOPT_ENCODING        => '',
        CURLOPT_SSL_VERIFYPEER  => false,
        CURLOPT_SSL_VERIFYHOST  => false,
        CURLOPT_TIMEOUT         => 10,
        CURLOPT_HTTPHEADER      => [
            'Content-Type: application/x-www-form-urlencoded',
            'Cookie: ' . $cookieString,
            'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36 Edg/138.0.0.0',
            'Accept: application/json, text/plain, */*',
            'Origin: https://mooc1-1.chaoxing.com',
            'Referer: https://mooc1-1.chaoxing.com/'
        ]
    ]);
    
    $response = curl_exec($ch);
    if ($response === false) {
        curl_close($ch);
        return false;
    }
    
    curl_close($ch);
    
    // 尝试解析JSON响应,如果失败则返回原始HTML
    $result = json_decode($response, true);
    if ($result !== null && json_last_error() === JSON_ERROR_NONE) {
        return $result;
    } else {
        // 返回HTML字符串
        return $response;
    }
}

/**
 * 格式化输出课程列表
 * @param string $html HTML内容
 */
function formatCourseList($html) {
    libxml_use_internal_errors(true);
    $dom = new DOMDocument('1.0', 'UTF-8');
    // 确保HTML使用UTF-8编码声明
    if (!preg_match('/<meta[^>]+charset/i', $html) && !preg_match('/<\?xml[^>]+encoding/i', $html)) {
        $html = '<?xml encoding="UTF-8"?>' . $html;
    }
    @$dom->loadHTML($html);
    libxml_clear_errors();
    $xpath = new DOMXPath($dom);
    
    // 查找所有课程
    $courses = $xpath->query('//li[contains(@class, "course")]');
    
    if ($courses->length == 0) {
        echo "未找到课程\n";
        return;
    }
    
    echo "\n" . str_repeat("=", 80) . "\n";
    echo "课程列表(共 " . $courses->length . " 门课程)\n";
    echo str_repeat("=", 80) . "\n\n";
    
    $index = 1;
    foreach ($courses as $course) {
        // 获取课程ID
        $courseId = $course->getAttribute('courseId');
        $clazzId = $course->getAttribute('clazzId');
        $personId = $course->getAttribute('personId');
        
        // 获取课程名称
        $courseNameNodes = $xpath->query('.//span[contains(@class, "course-name")]', $course);
        $courseName = $courseNameNodes->length > 0 ? trim($courseNameNodes->item(0)->textContent) : '未知课程';
        
        // 获取课程链接
        $linkNodes = $xpath->query('.//a[contains(@class, "color1")]', $course);
        $courseUrl = $linkNodes->length > 0 ? $linkNodes->item(0)->getAttribute('href') : '';
        
        // 获取学校/机构
        $schoolNodes = $xpath->query('.//p[contains(@class, "margint10")]', $course);
        $school = $schoolNodes->length > 0 ? trim($schoolNodes->item(0)->textContent) : '';
        
        // 获取教师 - 查找所有包含title属性的p标签
        $teacherNodes = $xpath->query('.//p[@title and contains(@class, "line2")]', $course);
        $teacher = '';
        if ($teacherNodes->length > 0) {
            // 优先使用title属性
            foreach ($teacherNodes as $node) {
                $title = trim($node->getAttribute('title'));
                $text = trim($node->textContent);
                if (!empty($title) && $title !== $school && !preg_match('/开课时间|课程已结束/', $title)) {
                    $teacher = $title;
                    break;
                } elseif (!empty($text) && $text !== $school && !preg_match('/开课时间|课程已结束/', $text)) {
                    $teacher = $text;
                    break;
                }
            }
            // 如果还没找到,尝试查找所有line2类的p标签
            if (empty($teacher)) {
                $allLine2Nodes = $xpath->query('.//p[contains(@class, "line2")]', $course);
                foreach ($allLine2Nodes as $node) {
                    $text = trim($node->textContent);
                    if (!empty($text) && $text !== $school && !preg_match('/开课时间|课程已结束|^\s*$/', $text)) {
                        $teacher = $text;
                        break;
                    }
                }
            }
        }
        
        // 获取开课时间
        $timeNodes = $xpath->query('.//p[contains(text(), "开课时间")]', $course);
        $courseTime = $timeNodes->length > 0 ? trim($timeNodes->item(0)->textContent) : '';
        
        // 检查是否已结束
        $isEnded = $xpath->query('.//a[contains(@class, "not-open-tip")]', $course)->length > 0;
        
        // 格式化输出
        echo sprintf("%d. %s\n", $index, $courseName);
        if (!empty($school)) {
            echo "   学校/机构: {$school}\n";
        }
        if (!empty($teacher)) {
            echo "   授课教师: {$teacher}\n";
        }
        if (!empty($courseTime)) {
            echo "   {$courseTime}\n";
        }
        echo "   课程ID: {$courseId} | 班级ID: {$clazzId} | 人员ID: {$personId}\n";
        if ($isEnded) {
            echo "   状态: 课程已结束\n";
        }
        if (!empty($courseUrl)) {
            echo "   链接: {$courseUrl}\n";
        }
        echo "\n" . str_repeat("-", 80) . "\n\n";
        
        $index++;
    }
}

// 选择登录方式
$loginMethod = '';
if (php_sapi_name() === 'cli') {
    // 命令行模式:可以选择登录方式
    // 可以通过命令行参数指定
    if (isset($argv[1]) && $argv[1] === 'password') {
        $loginMethod = 'password';
    } elseif (isset($argv[1]) && $argv[1] === 'qr') {
        $loginMethod = 'qr';
    } else {
        // 如果没有指定,提示用户选择
        echo "\n请选择登录方式:\n";
        echo "1. 二维码登录(默认)\n";
        echo "2. 账号密码登录\n";
        echo "请输入选项 (1 或 2,直接回车默认选择1): ";
        $input = trim(fgets(STDIN));
        
        if ($input === '2' || strtolower($input) === 'password') {
            $loginMethod = 'password';
        } else {
            $loginMethod = 'qr';
        }
    }
} else {
    // Web模式默认使用二维码
    $loginMethod = 'qr';
}

// 如果是账号密码登录
if ($loginMethod === 'password') {
    // 从命令行参数或环境变量获取账号密码
    $username = '';
    $password = '';
    
    if (php_sapi_name() === 'cli') {
        if (isset($argv[2])) {
            $username = $argv[2];
        }
        if (isset($argv[3])) {
            $password = $argv[3];
        }
        
        // 如果没有提供,提示输入
        if (empty($username)) {
            echo "请输入用户名: ";
            $username = trim(fgets(STDIN));
        }
        if (empty($password)) {
            echo "请输入密码: ";
            // 隐藏密码输入(Windows下可能不支持)
            $password = trim(fgets(STDIN));
        }
    }
    
    if (empty($username) || empty($password)) {
        echo "错误:用户名和密码不能为空\n";
        echo "使用方法: php xxt.php password <用户名> <密码>\n";
        exit(1);
    }
    
    echo "正在使用账号密码登录...\n";
    $loginResult = loginWithPassword($username, $password);
    
    if ($loginResult === false) {
        echo "登录请求失败\n";
        exit(1);
    }
    
    $statusData = $loginResult['status'];
    $statusStr = json_encode($statusData, JSON_UNESCAPED_UNICODE);
    echo "[" . date('Y-m-d H:i:s') . "] 登录响应: {$statusStr}\n";
    
    // 检查登录是否成功
    if (isset($statusData['status']) && $statusData['status'] === true) {
        echo "\n✓ 登录成功!\n";
        
        // 提取 cookie 并获取课程列表
        if (!empty($loginResult['cookieString'])) {
            echo "\n正在获取课程列表...\n";
            $courseList = getCourseList($loginResult['cookieString']);
            
            if ($courseList !== false) {
                // 检查返回的是HTML还是JSON
                if (is_string($courseList) && (stripos($courseList, '<html') !== false || stripos($courseList, '<li') !== false)) {
                    // 如果是HTML格式,使用格式化函数
                    formatCourseList($courseList);
                } else {
                    // 如果是JSON格式
                    echo "\n课程列表数据:\n";
                    echo json_encode($courseList, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "\n";
                }
            } else {
                echo "获取课程列表失败\n";
            }
        } else {
            echo "警告:未获取到 Cookie\n";
        }
    } else {
        $errorMsg = isset($statusData['msg2']) ? $statusData['msg2'] : (isset($statusData['msg']) ? $statusData['msg'] : '登录失败');
        echo "\n✗ 登录失败: {$errorMsg}\n";
        exit(1);
    }
    
    exit(0);
}

// 二维码登录方式
$url = 'https://passport2.chaoxing.com/login';
$headers = [
    'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
    'Accept-Encoding: gzip, deflate, br, zstd',
    'Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6',
    'Connection: keep-alive',
    'Host: passport2.chaoxing.com',
    'Sec-Fetch-Dest: document',
    'Sec-Fetch-Mode: navigate',
    'Sec-Fetch-Site: none',
    'Sec-Fetch-User: ?1',
    'Upgrade-Insecure-Requests: 1',
    'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36 Edg/138.0.0.0',
    'sec-ch-ua: "Not)A;Brand";v="8", "Chromium";v="138", "Microsoft Edge";v="138"',
    'sec-ch-ua-mobile: ?0',
    'sec-ch-ua-platform: "Windows"'
];

$ch = curl_init($url);
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER     => $headers,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_ENCODING       => '',
    CURLOPT_SSL_VERIFYPEER => false,
    CURLOPT_SSL_VERIFYHOST => false,
    CURLOPT_TIMEOUT        => 10
]);

$html = curl_exec($ch);
if ($html === false) {
    exit('cURL Error: ' . curl_error($ch));
}
curl_close($ch);
$uuid = $enc = null;

libxml_use_internal_errors(true);
$dom = new DOMDocument('1.0', 'UTF-8');
@$dom->loadHTML($html);
libxml_clear_errors();
$xpath = new DOMXPath($dom);
$uuidNodes = $xpath->query('//input[@id="uuid"]');
if ($uuidNodes->length > 0) {
    $uuid = $uuidNodes->item(0)->getAttribute('value');
}

$encNodes = $xpath->query('//input[@id="enc"]');
if ($encNodes->length > 0) {
    $enc = $encNodes->item(0)->getAttribute('value');
}
if ($uuid) {
    $qrCodeUrl = "https://passport2.chaoxing.com/createqr?uuid={$uuid}&fid=-1";

    $ch = curl_init($qrCodeUrl);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_ENCODING       => '',
        CURLOPT_SSL_VERIFYPEER => false,
        CURLOPT_SSL_VERIFYHOST => false,
        CURLOPT_TIMEOUT        => 10,
        CURLOPT_HTTPHEADER     => [
            'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36 Edg/138.0.0.0',
            'Accept: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8'
        ]
    ]);
    
    $qrImage = curl_exec($ch);
    if ($qrImage === false) {
        echo "获取二维码失败: " . curl_error($ch) . "\n";
    } else {
        if (php_sapi_name() === 'cli') {
            echo "uuid: {$uuid}\n";
            echo "enc: " . ($enc ?: '未找到') . "\n";
            echo "二维码URL: {$qrCodeUrl}\n\n";
            $base64 = base64_encode($qrImage);
            echo "\033]1337;File=inline=1;width=auto:" . $base64 . "\a\n";
            echo "\033_Ga=T,f=100,s=200,v=200;" . $base64 . "\033\\\n";
            echo "\n提示:如果二维码未显示,请复制上面的URL到浏览器查看\n";
            if ($enc) {
                echo "\n开始检查二维码扫描状态...\n";
                echo "按 Ctrl+C 可以停止检查\n\n";
                
                $maxAttempts = 300;
                $attempt = 0;
                
                while ($attempt < $maxAttempts) {
                    $result = checkQrCodeStatus($uuid, $enc);
                    
                    if ($result === false) {
                        if ($attempt % 10 == 0) {
                            echo "[" . date('Y-m-d H:i:s') . "] 检查失败,重试中...\n";
                        }
                    } else {
                        $statusData = $result['status'];
                        if (isset($statusData['status']) && $statusData['status'] === true) {
                            $statusStr = json_encode($statusData, JSON_UNESCAPED_UNICODE);
                            echo "[" . date('Y-m-d H:i:s') . "] 状态: {$statusStr}\n";
                            echo "\n✓ 二维码已被扫描,登录成功!\n";
                            if (!empty($result['cookieString'])) {
                                echo "\n正在获取课程列表...\n";
                                $courseList = getCourseList($result['cookieString']);
                                if ($courseList !== false) {
                                    if (is_string($courseList) && (stripos($courseList, '<html') !== false || stripos($courseList, '<li') !== false)) {
                                        formatCourseList($courseList);
                                    } else {
                                        echo "\n课程列表数据:\n";
                                        echo json_encode($courseList, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "\n";
                                    }
                                } else {
                                    echo "获取课程列表失败\n";
                                }
                            } else {
                                echo "警告:未获取到 Cookie\n";
                            }
                            
                            break;
                        } else {
                            if ($attempt % 5 == 0) {
                                $statusStr = json_encode($statusData, JSON_UNESCAPED_UNICODE);
                                echo "[" . date('Y-m-d H:i:s') . "] 等待扫描... 状态: {$statusStr}\n";
                            }
                        }
                    }
                    
                    $attempt++;
                    sleep(1);
                }
                
                if ($attempt >= $maxAttempts) {
                    echo "\n检查超时,请重新运行程序\n";
                }
            }
        } else {
            // Web模式:直接输出图片
            header('Content-Type: image/png');
            echo $qrImage;
            exit; // 输出图片后退出
        }
    }
    curl_close($ch);
} else {
    echo "uuid: 未找到,无法生成二维码\n";
    echo "enc: " . ($enc ?: '未找到') . "\n";
}
?>

python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import requests
import re
import json
import time
import sys
import os
from datetime import datetime
from bs4 import BeautifulSoup
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
import base64
import pytz
tz = pytz.timezone('Asia/Shanghai')

if sys.platform == 'win32':
    try:
        os.system('chcp 65001 >nul 2>&1')
    except:
        pass

def encrypt_by_aes(message, key):
    key = key[:16].encode('utf-8')
    iv = key
    cipher = AES.new(key, AES.MODE_CBC, iv)
    padded_data = pad(message.encode('utf-8'), AES.block_size)
    encrypted = cipher.encrypt(padded_data)
    return base64.b64encode(encrypted).decode('utf-8')

def login_with_password(username, password):
    url = 'https://passport2.chaoxing.com/fanyalogin'
    encrypt_key = "u2oh6Vu^HWe4_AES"
    encrypted_username = encrypt_by_aes(username, encrypt_key)
    encrypted_password = encrypt_by_aes(password, encrypt_key)
    
    data = {
        'fid': '-1',
        'uname': encrypted_username,
        'password': encrypted_password,
        'refer': 'http%3A%2F%2Fi.mooc.chaoxing.com%2Fspace%2Findex%3Fnull',
        't': 'true',
        'forbidotherlogin': '0',
        'validate': '',
        'doubleFactorLogin': '0',
        'independentId': '0',
        'independentNameId': '0'
    }
    
    headers = {
        'Content-Type': 'application/x-www-form-urlencoded',
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36 Edg/138.0.0.0',
        'Accept': 'application/json, text/plain, */*',
        'Origin': 'https://passport2.chaoxing.com',
        'Referer': 'https://passport2.chaoxing.com/login'
    }
    
    try:
        response = requests.post(url, data=data, headers=headers, verify=False, timeout=10)
        result = response.json()
        cookies = []
        for cookie in response.cookies:
            cookies.append(f"{cookie.name}={cookie.value}")
        
        return {
            'status': result,
            'cookies': cookies,
            'cookieString': '; '.join(cookies)
        }
    except Exception as e:
        print(f"登录请求失败: {e}")
        return None

def check_qr_code_status(uuid, enc):
    """
    检查二维码扫描状态
    """
    url = 'https://passport2.chaoxing.com/getauthstatus/v2'
    
    data = {
        'enc': enc,
        'uuid': uuid,
        'doubleFactorLogin': '0'
    }
    
    headers = {
        'Content-Type': 'application/x-www-form-urlencoded',
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36 Edg/138.0.0.0',
        'Accept': 'application/json, text/plain, */*',
        'Origin': 'https://passport2.chaoxing.com',
        'Referer': 'https://passport2.chaoxing.com/login'
    }
    
    try:
        response = requests.post(url, data=data, headers=headers, verify=False, timeout=10)
        result = response.json()
        cookies = []
        for cookie in response.cookies:
            cookies.append(f"{cookie.name}={cookie.value}")
        return {
            'status': result,
            'cookies': cookies,
            'cookieString': '; '.join(cookies)
        }
    except Exception as e:
        return None

def get_course_list(cookie_string):
    """
    获取课程列表
    """
    url = 'https://mooc1-1.chaoxing.com/mooc-ans/visit/courselistdata'
    
    data = {
        'courseType': '1',
        'courseFolderId': '0',
        'baseEducation': '0',
        'superstarClass': '',
        'courseFolderSize': '1'
    }
    
    headers = {
        'Content-Type': 'application/x-www-form-urlencoded',
        'Cookie': cookie_string,
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36 Edg/138.0.0.0',
        'Accept': 'application/json, text/plain, */*',
        'Origin': 'https://mooc1-1.chaoxing.com',
        'Referer': 'https://mooc1-1.chaoxing.com/'
    }
    
    try:
        response = requests.post(url, data=data, headers=headers, verify=False, timeout=10)
        return response.text
    except Exception as e:
        return None

def format_course_list(html):
    soup = BeautifulSoup(html, 'html.parser')
    courses = soup.find_all('li', class_='course')
    if not courses:
        print("未找到课程")
        return
    print("\n" + "=" * 80)
    print(f"课程列表(共 {len(courses)} 门课程)")
    print("=" * 80 + "\n")
    
    for index, course in enumerate(courses, 1):
        course_id = course.get('courseid', '')
        clazz_id = course.get('clazzid', '')
        person_id = course.get('personid', '')
        course_name_elem = course.find('span', class_='course-name')
        course_name = course_name_elem.get('title', course_name_elem.text.strip()) if course_name_elem else '未知课程'
        link_elem = course.find('a', class_='color1')
        course_url = link_elem.get('href', '') if link_elem else ''
        school_elem = course.find('p', class_='margint10')
        school = school_elem.text.strip() if school_elem else ''
        teacher = ''
        line2_elems = course.find_all('p', class_='line2')
        for elem in line2_elems:
            text = elem.text.strip()
            title = elem.get('title', '')
            if text and text != school and '开课时间' not in text and '课程已结束' not in text:
                teacher = title if title else text
                break
        

        time_elem = course.find('p', string=re.compile('开课时间'))
        course_time = time_elem.text.strip() if time_elem else ''
        
        # 检查是否已结束
        is_ended = course.find('a', class_='not-open-tip') is not None
        
        # 格式化输出
        print(f"{index}. {course_name}")
        if school:
            print(f"   学校/机构: {school}")
        if teacher:
            print(f"   授课教师: {teacher}")
        if course_time:
            print(f"   {course_time}")
        print(f"   课程ID: {course_id} | 班级ID: {clazz_id} | 人员ID: {person_id}")
        if is_ended:
            print("   状态: 课程已结束")
        if course_url:
            print(f"   链接: {course_url}")
        print("\n" + "-" * 80 + "\n")

def main():
    login_method = ''
    if len(sys.argv) > 1:
        if sys.argv[1] == 'password':
            login_method = 'password'
        elif sys.argv[1] == 'qr':
            login_method = 'qr'
    
    if not login_method:
        print("\n请选择登录方式:")
        print("1. 二维码登录(默认)")
        print("2. 账号密码登录")
        choice = input("请输入选项 (1 或 2,直接回车默认选择1): ").strip()
        
        if choice == '2' or choice.lower() == 'password':
            login_method = 'password'
        else:
            login_method = 'qr'
    
    # 账号密码登录
    if login_method == 'password':
        username = ''
        password = ''
        
        if len(sys.argv) > 2:
            username = sys.argv[2]
        if len(sys.argv) > 3:
            password = sys.argv[3]
        
        if not username:
            username = input("请输入用户名: ").strip()
        if not password:
            password = input("请输入密码: ").strip()
        
        if not username or not password:
            print("错误:用户名和密码不能为空")
            print("使用方法: python xxt.py password <用户名> <密码>")
            sys.exit(1)
        
        print("正在使用账号密码登录...")
        login_result = login_with_password(username, password)
        
        if not login_result:
            print("登录请求失败")
            sys.exit(1)
        
        status_data = login_result['status']
        status_str = json.dumps(status_data, ensure_ascii=False)
        current_time = datetime.now(tz).strftime('%Y-%m-%d %H:%M:%S')
        print(f"[{current_time}] 登录响应: {status_str}")
        
        if status_data.get('status') == True:
            print("\n✓ 登录成功!")
            
            if login_result.get('cookieString'):
                print("\n正在获取课程列表...")
                course_list = get_course_list(login_result['cookieString'])
                
                if course_list:
                    if '<li' in course_list or '<html' in course_list.lower():
                        format_course_list(course_list)
                    else:
                        print("\n课程列表数据:")
                        print(json.dumps(json.loads(course_list), ensure_ascii=False, indent=2))
                else:
                    print("获取课程列表失败")
            else:
                print("警告:未获取到 Cookie")
        else:
            error_msg = status_data.get('msg2') or status_data.get('msg', '登录失败')
            print(f"\n✗ 登录失败: {error_msg}")
            sys.exit(1)
        
        sys.exit(0)
    
    # 二维码登录
    url = 'https://passport2.chaoxing.com/login'
    headers = {
        'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
        'Accept-Encoding': 'gzip, deflate, br, zstd',
        'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6',
        'Connection': 'keep-alive',
        'Host': 'passport2.chaoxing.com',
        'Sec-Fetch-Dest': 'document',
        'Sec-Fetch-Mode': 'navigate',
        'Sec-Fetch-Site': 'none',
        'Sec-Fetch-User': '?1',
        'Upgrade-Insecure-Requests': '1',
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36 Edg/138.0.0.0',
        'sec-ch-ua': '"Not)A;Brand";v="8", "Chromium";v="138", "Microsoft Edge";v="138"',
        'sec-ch-ua-mobile': '?0',
        'sec-ch-ua-platform': '"Windows"'
    }
    
    try:
        response = requests.get(url, headers=headers, verify=False, timeout=10)
        html = response.text
        
        # 提取 uuid 和 enc
        soup = BeautifulSoup(html, 'html.parser')
        uuid_elem = soup.find('input', {'id': 'uuid'})
        enc_elem = soup.find('input', {'id': 'enc'})
        
        uuid = uuid_elem.get('value') if uuid_elem else None
        enc = enc_elem.get('value') if enc_elem else None
        
        if uuid:
            qr_code_url = f"https://passport2.chaoxing.com/createqr?uuid={uuid}&fid=-1"
            
            # 获取二维码图片
            qr_response = requests.get(qr_code_url, verify=False, timeout=10)
            if qr_response.status_code == 200:
                print(f"uuid: {uuid}")
                print(f"enc: {enc if enc else '未找到'}")
                print(f"二维码URL: {qr_code_url}\n")
                
                # 保存二维码图片
                qr_filename = f'qrcode_{uuid}.png'
                with open(qr_filename, 'wb') as f:
                    f.write(qr_response.content)
                print(f"二维码已保存为: {qr_filename}\n")
                print("提示:请使用学习通APP扫描二维码\n")
                
                # 轮询检查二维码扫描状态
                if enc:
                    print("开始检查二维码扫描状态...")
                    print("按 Ctrl+C 可以停止检查\n")
                    
                    max_attempts = 300
                    attempt = 0
                    
                    while attempt < max_attempts:
                        result = check_qr_code_status(uuid, enc)
                        
                        if not result:
                            if attempt % 10 == 0:
                                current_time = datetime.now(tz).strftime('%Y-%m-%d %H:%M:%S')
                                print(f"[{current_time}] 检查失败,重试中...")
                        else:
                            status_data = result['status']
                            
                            if status_data.get('status') == True:
                                status_str = json.dumps(status_data, ensure_ascii=False)
                                current_time = datetime.now(tz).strftime('%Y-%m-%d %H:%M:%S')
                                print(f"[{current_time}] 状态: {status_str}")
                                print("\n✓ 二维码已被扫描,登录成功!")
                                
                                if result.get('cookieString'):
                                    print("\n正在获取课程列表...")
                                    course_list = get_course_list(result['cookieString'])
                                    
                                    if course_list:
                                        if '<li' in course_list or '<html' in course_list.lower():
                                            format_course_list(course_list)
                                        else:
                                            print("\n课程列表数据:")
                                            print(json.dumps(json.loads(course_list), ensure_ascii=False, indent=2))
                                    else:
                                        print("获取课程列表失败")
                                else:
                                    print("警告:未获取到 Cookie")
                                
                                break
                            else:
                                if attempt % 5 == 0:
                                    status_str = json.dumps(status_data, ensure_ascii=False)
                                    current_time = datetime.now(tz).strftime('%Y-%m-%d %H:%M:%S')
                                    print(f"[{current_time}] 等待扫描... 状态: {status_str}")
                        
                        attempt += 1
                        time.sleep(1)
                    
                    if attempt >= max_attempts:
                        print("\n检查超时,请重新运行程序")
            else:
                print("获取二维码失败")
        else:
            print("uuid: 未找到,无法生成二维码")
            print(f"enc: {enc if enc else '未找到'}")
    
    except Exception as e:
        print(f"错误: {e}")
        sys.exit(1)

if __name__ == '__main__':
    import urllib3
    urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
    main()
Last modification:November 18, 2025
If you think my article is useful to you, please feel free to appreciate