新的学期已经开始, 我的大学生活已经过了一半, 当年不熟悉的事物现在越来越变得习以为常. 但就上网还需要登陆这件事让我感到很是不习惯. 为一个会写程序的人, 怎能不通过程序去简化这一过程呢? 想到这, 我这已经干涸的大脑就好像注入了润滑油一般高速运转起来...

分析登录原理&&

&&连接到校园网, 打开需要浏览的网址, 观察抓包信息:

&&抓包&&

浏览 http://202.108.22.5 时返回的状态码是&200, 但也进行了重定向. 那么应该是通过网页代码进行的跳转. 打开 view-source:http://202.108.22.5/ 发现了跳转语句:

202&源码&&

请求的地址为 http://192.168.1.93/switch.php, 参数如下所示&&(均为猜测):

&&名称&&&&值描述&&
wlanuserip10.6.4.205DHCP&分配的&IP&地址
wlanacnameD6-3&&宿舍楼和楼层&&
ssid &&空的...
nasip10.0.0.63服务器&IP&地址&&
mac206a8a69a51e&&我的&MAC&地址&&
twireless-v2-plain&&接入方式&&
urlhttp://202.108.22.5/&&需要浏览的网址&&

&&访问 http://192.168.1.93/switch.php?wlanuserip=10.6.4.205&wlanacname=D6-3&ssid=&nasip=10.0.0.63&mac=206a8a69a51e&t=wireless-v2-plain&url=http://202.108.22.5/ , 状态码为&302.

跳转到 http://192.168.1.93/portalcloud/page/3/PC/chn/Login.html?wlanuserip=10.6.4.205&wlanacname=D6-3&ssid=&nasip=10.0.0.63&mac=206a8a69a51e&t=wireless-v2-plain&url=http://202.108.22.5/ 的同时, 给我存了一个&Cookie:

名称&&&&值描述&&
PHPSESSID0q560qo79g7osdld27p4lgit01Session

&&现在所显示的就是登录页面了:

login

输入账号密码, 点击登录, 请求的地址为 http://192.168.1.93/post.php , post&参数为:

名称&&&&值描述&&
pageId3
username******************&&用户名&&
password********&&密码&&
0MKKey&&登录&&

&&顺便, 经过实验, 若无 pageId0MKKey 参数提交, 也可以登录. 但如果没有 PHPSESSID 或 PHPSESSID 错误, 则无法登录.

提交表单后, 302&重定向到 http://192.168.1.93/switch.php?p=status , 再&302&重定向到 http://192.168.1.93/portalcloud/page/3/PC/chn/Query.html?p=status . 显示&&"&&登录成功&&"&&页面:

successful

获取网络信息&&

&&我的思路是构造用于获取 PHPSESSID 的连接并访问. 需要知道的信息为本机的 MAC 和 IP 地址.

在&QT&中用于获取网络相关信息的类是 QNetworkInterface , 可以获取到以下信息:

  • hardwareAddress() 设备&MAC&地址.
  • humanReadableName() 设备可读名&&(如 "&&本地连接&&"/ "VMware Network Adapter VMnet1"&&等).
  • name() 设备名(如 "{29585BF8-EB77-4F50-B75B-47F675BC80B5}" 等).
  • flags() 设备状态, 取值为:

    • IsUp 已经激活.
    • IsRunning 正在运行.
    • CanBroadcast 广播模式.
    • IsLoopBack 虚拟回环接口.
    • IsPointToPoint 点对点.
    • CanMulticast 支持多播.
  • addressEntries() IP&地址相关信息, 需要遍历, 每一组数据包含:

    • ip() ip&地址.
    • netmask() 子网掩码.
    • broadcast() 广播地址.

&&在这里我们需要传入 humanReadableName , 获取&MAC&地址和&IP&地址, 于是就写出了这个函数:

AutoLogin::AutoLogin(QString devicename, QObject *parent) : QObject(parent){ //&重载构造函数以指定网卡名称
    for (const QNetworkInterface& netinterface : QNetworkInterface::allInterfaces()){ //&遍历所有网络信息
        if(netinterface.humanReadableName() == devicename){ //&判断设备名
            macaddr = netinterface.hardwareAddress(); //&获取&MAC&地址
            ipv4addr = netinterface.addressEntries()[0].ip().toString(); //&获取&IP&地址
        }
    }
}

鉴于大多人只有一个网卡, 接一条网线, 设一个&IP&地址, 我们还可以让这个函数更智能一些:

AutoLogin::AutoLogin(QObject *parent) : QObject(parent){ //&默认构造函数
    for (const QNetworkInterface& netinterface : QNetworkInterface::allInterfaces()){ //&遍历所有网络信息
        QNetworkInterface::InterfaceFlags flags = netinterface.flags(); //&获取网络状态信息
        if (flags.testFlag(QNetworkInterface::IsRunning) && !flags.testFlag(QNetworkInterface::IsLoopBack)){ //&正在运行且非回环
            if (netinterface.addressEntries()[0].ip().toString().left(3) == "10.") //&如果&IP&地址以&10&开头(学校是&A&类内网地址)
                macaddr = netinterface.hardwareAddress(); //&获取&MAC&地址
                ipv4addr = netinterface.addressEntries()[0].ip().toString(); //&获取&IP&地址
        }
    }
}

如此, 我们就成功获得了本机的&IP&地址和&MAC&地址.

写到这里我发现自己的思路有点儿不太对, 比起这样通过程序获取我的网络信息并构造连接地址, 不如模仿我们手动上网登录流程, 打开网页的同时从其源码中获取连接地址. 这样就算是有多个网卡多个 IP 或者不在&6&号楼也可以正常连接.

获取 PHPSESSID

重整思路, 我应该做的其实是先访问任意网页, 获取其源码, 然后读取到需要跳转的地址并访问.

在任何操作前, 我们应当先确定网络已经联通:

bool AutoLogin::getConnectionState() { //&测试网络连通性
    QTcpSocket tcpClient; //TCP&客户端
    tcpClient.abort(); //&终止之前的连接并复位
    tcpClient.connectToHost("202.108.22.5", 80); //&连接百度&ip, 80&端口
    if (!tcpClient.waitForConnected(256)) { //&如果&256&毫秒之内不能连接百度
        return (false); //&返回&false
    }
    return (true); //&如果连接成功, 返回&true
}

先初始化网络访问类库并关联信号槽(设置回调函数), 用于获取 "&&用来获取 PHPSESSID" 的&url:

void AutoLogin::initGetUrl() { //&初始化获取&url
    QNetworkAccessManager* manager = new QNetworkAccessManager(); //&实例化网络访问类
    connect (manager, SIGNAL(finished(QNetworkReply*)),
                this, SLOT(onReplyFinished(QNetworkReply*))); //&关联信号和槽
    manager->get(QNetworkRequest(QUrl("http://202.108.22.5"))); //&访问百度的服务器
}

用于判断是否已经登录:

bool AutoLogin::getLoginState(QString html) { //&获取是否登录信息
    QRegExp* reg = new QRegExp("<title>(.*)</title>"); //&匹配&<title>&标签
    qint8 con = reg->indexIn(html); //&获取匹配位置, 若无匹配返回&-1
    if (con != -1) { //&如果有匹配
        if (reg->cap(1) == "&百度一下,你就知道&") { //&如果匹配值为百度标题
            return (true); //&返回&true
        }
    }
    return (false); //&若无匹配或标题不为&"&百度一下,&你就知道&", 返回&false
}

正则匹配出需要访问的连接地址:

QString AutoLogin::getSessionUrl(QString html) { //&获取&"&用来获取&Session"&的&Url
    QRegExp* reg = new QRegExp("self\.location\.href='(.*)'"); //&匹配跳转目的页面
    qint8 con = reg->indexIn(html); //&获取匹配位置, 若无匹配返回&-1
    if (con != -1) { //&如果有匹配
        return (reg->cap(1)); //&返回匹配到的&url
    }
    return (""); //&如果没匹配则返回空字符串
}

然后是关于 Cookie 的操作了, 写的时候发现 QNetworkCookie::allCookies() 是一个 private 函数, 我不得已继承了一下:

//networkcookie.h
#ifndef NETWORKCOOKIE_H
#define NETWORKCOOKIE_H

#include <QNetworkCookie>
#include <QNetworkCookieJar>

class NetworkCookie : public QNetworkCookieJar{
    public:
        NetworkCookie();
        QList<QNetworkCookie> getCookies();
        void setCookies(const QList<QNetworkCookie> &cookieList);
};

#endif // NETWORKCOOKIE_H

//--------------------------------//
//networkcookie.cpp
#include "networkcookie.h"

NetworkCookie::NetworkCookie(){}

QList<QNetworkCookie> NetworkCookie::getCookies(){return (allCookies());
}

void NetworkCookie::setCookies(const QList<QNetworkCookie> &cookieList){setAllCookies(cookieList);
}

OK, 可以外部访问了. 再次实例化网络访问类, 这次是用来获取 PHPSESSID 的.

void AutoLogin::initGetCookies(QString url) { //&初始化获取&cookie
    QNetworkAccessManager* manager = new QNetworkAccessManager(); //&实例化网络访问类
    cookiejar = new NetworkCookie(); //cookie&容器
    manager->setCookieJar(cookiejar); //&设置网络访问类的&cookie&容器
    connect (manager, SIGNAL(finished(QNetworkReply*)),
                this, SLOT(onCookieFinished(QNetworkReply*))); //&关联信号和槽
    manager->get(QNetworkRequest(QUrl(url))); //&访问&url
}

至此 PHPSESSID 就获取到了.

提交 PSOT 请求&&

&&最后就是登录了,

void AutoLogin::initLogin() { //&初始化登录
    QNetworkAccessManager* manager = new QNetworkAccessManager(); //&实例化网络访问类
    manager->setCookieJar(cookiejar); //&设置&cookie&为之前获取到的&cookie
    connect (manager, SIGNAL(finished(QNetworkReply*)),
                this, SLOT(onLoginFinished(QNetworkReply*))); //&关联信号和槽
    QNetworkRequest* request = new QNetworkRequest(); //&实例化网络请求
    request->setUrl(QUrl(LOGINURL)); //&设置请求&URL
    request->setHeader(QNetworkRequest::ContentTypeHeader,
                      "application/x-www-form-urlencoded"); //&设置请求头
    QByteArray parameter; //post&参数
    parameter.append(REGULAR.arg(Identity).arg(Password)); //&构造&post&参数
    manager->post(*request, parameter); //&发送&post&请求
}

三个槽&&(回调函数)&&只需要处理逻辑.

void AutoLogin::onReplyFinished(QNetworkReply* reply) { //&百度访问完成回调函数
    if (reply->error() != QNetworkReply::NoError) { //&如果请求失败
        std::cout << "Get url error." << std::endl; //&提示获取&url&错误
        emit quit(); //&触发关闭程序的信号
        return; //&跳出
    }

    QString html = reply->readAll(); //&读取获取的&html&代码
    if (getLoginState(html)) { //&如果正常访问百度
        std::cout << "Connected network." << std::endl; //&提示已经连接网络
        emit quit(); //&触发关闭程序的信号
        return; //&跳出
    }

    QString url = getSessionUrl(html); //&获取需要跳转的地址
    if (url == "") { //&如果没有获取到需要跳转的地址
        std::cout << "Only use in QCHM." << std::endl; //&只能用于青岛酒店管理学院
        emit quit(); //&触发关闭程序的信号
        return; //&跳出
    }

    initGetCookies(url); //&初始化获取&cookie
}

void AutoLogin::onCookieFinished(QNetworkReply* reply) { //&获取&cookie&完成回调函数
    if (reply->error() != QNetworkReply::NoError) { //&如果请求失败
        std::cout << "Get cookies error." << std::endl; //&提示获取&cookie&错误
        emit quit(); //&触发关闭程序的信号
        return; //&跳出
    }

    std::cout << "Your PHPSESSID is: "
              << cookiejar->getCookies()[0].value().data()
              << std::endl; //&输出&PHPSESSID&信息

    initLogin(); //&初始化登录}

void AutoLogin::onLoginFinished(QNetworkReply* reply) { //&登录完成回调函数
    if (reply->error() != QNetworkReply::NoError) { //&如果请求失败
        std::cout << "Login error." << std::endl; //&提示登录错误
    } else { //&如果成功
        if (reply->readAll() == "<!--post ver:1.0.0 -->\n") { //&如果有返回值
            std::cout << "Login successful." << std::endl; //&提示登录成功
        } else { //&如果没有
            std::cout << "Identity or password error." << std::endl; //&提示账号密码错误
        }
    }

    emit quit(); //&触发关闭程序的信号
    return; //&跳出
}

主函数&&

&&主函数的难点也只有信号和槽的关联了, 由于没有继承自 QObject , 所以 connect 操作需要使用 QObject::connect 调用. AutoLogin 中的 quit() 不要忘记声明为 SIGNALS .

#include <QCoreApplication> //Qt&核心
#include <autologin.h> //&自己写的类库

int main(int argc, char *argv[]) //&主函数
{QCoreApplication a(argc, argv); //Qt&核心类库

    AutoLogin *al = new AutoLogin(); //&实例化自己写的类库
    QObject::connect(al, SIGNAL(quit()), //&信号是我类库中的&quit()信号
                     &a, SLOT(quit())); //&槽是&Qt&核心退出
    al->setIdentity(argv[1]); //&把用户名作为第一个参数传入
    al->setPassword(argv[2]); //&把密码作为第二个参数传入
    al->autoLogin(); //&开始登录

    return a.exec(); //&进入&Qt&事件循环}

顺便提及一下 main 函数中的两个形参:

参数名&&&&意义&&
argc&&程序运行时传入参数的个数&&
argv[0]&&当前程序的绝对路径&&
argv[1]&&及以后&&&&调用此程序时输入的参数&&

&&设置开机启动&&

&&我在众多 Linux 发行版中选择 LinuxMint 作为我主力办公机系统, 也是不无原因的. 不仅因它基于 Ubuntu(Debian) , 有着庞大的社区支持, 非常方便开发者进行软件开发. 而且还有简洁的桌面, 丰富的内置应用. 也是我选择它的原因. 对我这种 Linux 小白而言, 在需要快速实现某项功能却不知道敲什么命令的时候, 给出一个简介强大的界面. 简直不能太赞.

编译完成后把可执行文件复制到适当的目录. 添加一个新的开机启动项, 命令设置为 /home/moe8023/Program/AutoLogin/AutoLogin ****************** ******** , 前部分的 * 表示我的账号&&(身份证号), 后部分是密码.

start

&&结语&&

&&如此, 我的计算机只要一开机, 不出意料&10&秒后就自动登录了校园网. 虽说自个手动打开浏览器登录也不是什么费事儿的事. 但可以给博客凑数, 跟同学装逼啊&& 但时不时写写程序, 记记笔记打发一下无聊的时间, 也是有益身心健康的.

&&最后附上项目的 Github 和 Coding 地址, 如果有需要的, 可以自行下载. 如果有什么好的意见和建议, 可以在这篇文章下面留言, 也可以在 Github 或 Coding 提交 issues.