commit 38296740c59987d51e3b2b838bed0ceed5f3497a Author: Jeffrey Hsu Date: Tue Oct 21 10:50:49 2025 +0800 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e489a4a --- /dev/null +++ b/.gitignore @@ -0,0 +1,122 @@ +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf +.idea/ + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +# vscode ================================ +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json + +# C++ ================================ + Prerequisites +*.d + +# Compiled Object files +*.slo +*.lo +*.o +*.obj + +# Precompiled Headers +*.gch +*.pch + +# Compiled Dynamic libraries +*.so +*.dylib +*.dll + +# Fortran module files +*.mod +*.smod + +# Compiled Static libraries +*.lai +*.la +*.a +*.lib + +# Executables +*.exe +*.out +*.app + + +# CMake ================================ +bin/ +build/ +CMakeLists.txt.user +CMakeCache.txt +CMakeFiles +CMakeScripts +Testing +Makefile +cmake_install.cmake +install_manifest.txt +compile_commands.json +CTestTestfile.cmake diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..68d0375 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,69 @@ +cmake_minimum_required(VERSION 4.0) +project(AHNU-Portal-Authenticator) + +set(CMAKE_CXX_STANDARD 23) +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) +set(CMAKE_AUTOUIC ON) + +set(APP_NAME AHNU) + +if (WIN32) + set(app_icon_resource_windows "${CMAKE_CURRENT_SOURCE_DIR}/icon.rc") +endif () + +find_package(Qt6 COMPONENTS + Core + Gui + Widgets + Network + REQUIRED +) + +add_executable(${APP_NAME} + WIN32 + main.cpp + mainwindow.cpp + mainwindow.h + mainwindow.ui + res.qrc + ${app_icon_resource_windows} +) + +target_compile_definitions(${APP_NAME} PRIVATE PRIVATE APPNAME="${APP_NAME}") + +target_link_libraries(${APP_NAME} + Qt::Core + Qt::Gui + Qt::Widgets + Qt::Network +) + +if (WIN32 AND NOT DEFINED CMAKE_TOOLCHAIN_FILE) + set(DEBUG_SUFFIX) + if (MSVC AND CMAKE_BUILD_TYPE MATCHES "Debug") + set(DEBUG_SUFFIX "d") + endif () + set(QT_INSTALL_PATH "${CMAKE_PREFIX_PATH}") + if (NOT EXISTS "${QT_INSTALL_PATH}/bin") + set(QT_INSTALL_PATH "${QT_INSTALL_PATH}/..") + if (NOT EXISTS "${QT_INSTALL_PATH}/bin") + set(QT_INSTALL_PATH "${QT_INSTALL_PATH}/..") + endif () + endif () + if (EXISTS "${QT_INSTALL_PATH}/plugins/platforms/qwindows${DEBUG_SUFFIX}.dll") + add_custom_command(TARGET ${APP_NAME} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E make_directory + "$/plugins/platforms/") + add_custom_command(TARGET ${APP_NAME} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy + "${QT_INSTALL_PATH}/plugins/platforms/qwindows${DEBUG_SUFFIX}.dll" + "$/plugins/platforms/") + endif () + foreach (QT_LIB Core Gui Widgets Network) + add_custom_command(TARGET ${APP_NAME} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy + "${QT_INSTALL_PATH}/bin/Qt6${QT_LIB}${DEBUG_SUFFIX}.dll" + "$") + endforeach (QT_LIB) +endif () diff --git a/README.md b/README.md new file mode 100644 index 0000000..7bb544d --- /dev/null +++ b/README.md @@ -0,0 +1,52 @@ +# AHNU Portal Authenticator + +**AHNU Portal Authenticator** 是一个为 **安徽师范大学校园网(AHNU)** 设计的轻量级网络认证工具, +实现了登录与在线状态维持。支持 **开机自启**、**后台运行**、**实时在线检测**, 让校园网登录更加稳定、无感、安全。 +本项目受到此[项目]('https://github.com/SweetCaviar/AHNU-Network-Automatic-Csharp')的启发应运而生。 + +--- + +## 功能特性 + +- **使用 Qt 编写** + 具有直观、简洁的使用界面。 + +- **Portal 协议认证** + 自动完成校园网账号认证流程,支持账户与服务商选择。 + +- **开机自启** + 自动注册到系统启动项,无需手动登录即可联网。 + +- **后台运行** + 无窗口静默运行,不打扰使用体验,可在系统托盘中查看状态。 + +- **实时在线监测** + 定时检测网络连接状态,掉线后提示,保证网络稳定在线。 + +- **多服务商支持** + 支持多个运营商(电信、移动、联通)认证。 + +--- + +## 编译方法 + +1. **环境准备** + +| 名称 | 推荐版本 | 说明 | +|-------|-----------------------|-------------------| +| Qt | ≥ 6.5 | 推荐安装 Qt 6.6 或更新版本 | +| CMake | ≥ 4.0 | 用于跨平台构建 | +| 编译器 | MSVC 2022 / MinGW 11+ | 均可使用 | + +2. **编译** + +```bash +git clone https://github.com/Aurora1949/AHNU-Portal-Authenticator.git +cd AHNU-Portal-Authenticator + +mkdir build +cd build + +cmake .. -DCMAKE_BUILD_TYPE=Release +cmake --build . +``` \ No newline at end of file diff --git a/icon.rc b/icon.rc new file mode 100644 index 0000000..a0368ff --- /dev/null +++ b/icon.rc @@ -0,0 +1 @@ +IDI_ICON1 ICON "images/app.ico" \ No newline at end of file diff --git a/images/app.ico b/images/app.ico new file mode 100644 index 0000000..63f82d5 Binary files /dev/null and b/images/app.ico differ diff --git a/images/banner.png b/images/banner.png new file mode 100644 index 0000000..98471fb Binary files /dev/null and b/images/banner.png differ diff --git a/images/logo.png b/images/logo.png new file mode 100644 index 0000000..5a1be26 Binary files /dev/null and b/images/logo.png differ diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..d691d25 --- /dev/null +++ b/main.cpp @@ -0,0 +1,56 @@ +#include "mainwindow.h" + +#include +#include +#include + +bool isAlreadyRunning() { + QLocalSocket socket; + socket.connectToServer(APPNAME); + + // 已有实例存在 + if (socket.waitForConnected(100)) { + socket.write("raise"); + socket.flush(); + socket.waitForBytesWritten(100); + socket.disconnectFromServer(); + return true; + } + + // 尝试清除之前未正常退出的 socket 文件 + QLocalServer::removeServer(APPNAME); + return false; +} + +int main(int argc, char *argv[]) { + QApplication a(argc, argv); + + if (isAlreadyRunning()) { + return 0; + } + + MainWindow w; + w.show(); + + // 建立本地 server + QLocalServer server; + if (!server.listen(APPNAME)) { + return -1; + } + + QObject::connect(&server, &QLocalServer::newConnection, [&]() { + QLocalSocket *client = server.nextPendingConnection(); + if (!client) return; + + QObject::connect(client, &QLocalSocket::readyRead, [&]() { + QByteArray msg = client->readAll(); + if (msg == "raise") { + w.showFromTray(); + } + }); + + QObject::connect(client, &QLocalSocket::disconnected, client, &QLocalSocket::deleteLater); + }); + + return a.exec(); +} diff --git a/mainwindow.cpp b/mainwindow.cpp new file mode 100644 index 0000000..4373ad6 --- /dev/null +++ b/mainwindow.cpp @@ -0,0 +1,309 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "ui_mainwindow.h" +#include "mainwindow.h" + +MainWindow::MainWindow(QWidget *parent) + : QMainWindow(parent) + , ui(new Ui::MainWindow) + , provider({{"中国移动", "cmcc"}, {"中国电信", "telecom"}, {"中国联通", "unicom"}}) + , baseUrl("http://100.64.4.10:801/eportal/portal/login") + , testUrl("http://www.baidu.com") + , logoutUrl("http://100.64.4.10:801/eportal/portal/logout") + , isOnline(false) + , trayMenu(new QMenu(this)) + , trayIcon(new QSystemTrayIcon(this)) + , networkChecker(new QTimer(this)) { + ui->setupUi(this); + init(); + getUserInfo(); + testOnline(); + doAutoRun(); +} + +void MainWindow::init() { + setWindowTitle("AHNU上号器"); + setWindowIcon(QIcon(":/images/logo.png")); + setWindowFlags(windowFlags() & ~Qt::WindowMaximizeButtonHint & ~Qt::WindowMinimizeButtonHint); + setFixedSize(400, 178); + + manager.setProxy(QNetworkProxy::NoProxy); + + ui->logoLabel->setPixmap(QPixmap(":/images/banner.png")); + for (const auto &p: provider) + ui->providerComboBox->addItem(p.name, p.value); + ui->selfStartup->setChecked(isAutoRunEnabled()); + + trayIcon->setIcon(QIcon(":/images/logo.png")); + trayIcon->setToolTip(TrayIconMsg::Offline); + + QAction *showAction = new QAction("显示窗口", this); + QAction *exitAction = new QAction("退出", this); + trayMenu->addAction(showAction); + trayMenu->addSeparator(); + trayMenu->addAction(exitAction); + + trayIcon->setContextMenu(trayMenu); + + connect(networkChecker, &QTimer::timeout, this, &MainWindow::testOnline); + + connect(showAction, &QAction::triggered, this, &MainWindow::onShowFromTray); + connect(exitAction, &QAction::triggered, this, &MainWindow::onQuitFromTray); + connect(trayIcon, &QSystemTrayIcon::activated, this, [this](QSystemTrayIcon::ActivationReason reason) { + if (reason == QSystemTrayIcon::Trigger) // 单击托盘图标 + onShowFromTray(); + }); +} + +MainWindow::~MainWindow() { + delete ui; +} + +void MainWindow::on_loginBtn_clicked() { + doLogin(); +} + +void MainWindow::online() { + ui->loginBtn->setText("登出"); + ui->loginBtn->setStyleSheet("color: red;"); + trayIcon->setToolTip(TrayIconMsg::Online); + + setInputEnable(false); + networkChecker->start(5000); + + isOnline = true; +} + +void MainWindow::offline() { + ui->loginBtn->setText("登录"); + ui->loginBtn->setStyleSheet("color: black;"); + trayIcon->setToolTip(TrayIconMsg::Offline); + + setInputEnable(true); + if (!this->isVisible()) + onShowFromTray(); + + networkChecker->stop(); + + isOnline = false; +} + +void MainWindow::setInputEnable(bool enable) { + ui->passwdLe->setEnabled(enable); + ui->usernameLe->setEnabled(enable); + ui->providerComboBox->setEnabled(enable); + ui->rememberMe->setEnabled(enable); +} + +void MainWindow::saveUserInfo() { + // 获取 APPDATA 路径 + QString appDataPath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); + QDir dir(appDataPath); + if (!dir.exists()) + dir.mkpath(appDataPath); + + QString filePath = appDataPath + "/userinfo.json"; + QFile file(filePath); + + // 创建 JSON 对象 + QJsonObject userInfo; + userInfo["username"] = ui->usernameLe->text(); + userInfo["password"] = ui->passwdLe->text(); + userInfo["provider"] = ui->providerComboBox->currentText(); + + // 写入文件 + if (file.open(QIODevice::WriteOnly | QIODevice::Truncate)) { + QJsonDocument doc(userInfo); + file.write(doc.toJson(QJsonDocument::Indented)); + file.close(); + } else { + QMessageBox::warning(this, "注意", "无法保存账户与密码"); + } +} + +void MainWindow::getUserInfo() { + QString appDataPath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); + QString filePath = appDataPath + "/userinfo.json"; + + QFile file(filePath); + QJsonObject userInfo; + + if (file.open(QIODevice::ReadOnly)) { + QByteArray data = file.readAll(); + QJsonDocument doc = QJsonDocument::fromJson(data); + if (doc.isObject()) + userInfo = doc.object(); + file.close(); + + ui->usernameLe->setText(userInfo.take("username").toString()); + ui->passwdLe->setText(userInfo.take("password").toString()); + ui->providerComboBox->setEditText(userInfo.take("provider").toString()); + ui->rememberMe->setChecked(true); + } +} + +void MainWindow::testOnline() { + QNetworkRequest request{testUrl}; + request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, + QNetworkRequest::ManualRedirectPolicy); + QNetworkReply *reply{manager.head(request)}; + connect(reply, &QNetworkReply::finished, this, [this, reply] { + if (reply->error() == QNetworkReply::NoError) { + QUrl reUrl{reply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl()}; + if (reUrl.host() == "rz.ahnu.edu.cn") { + ui->statusbar->showMessage(StatusBarMsg::AlreadyOffline); + offline(); + } else { + ui->statusbar->showMessage(StatusBarMsg::AlreadyOnline); + online(); + } + } + reply->deleteLater(); + }); +} + +void MainWindow::setAutoRun(bool isStart) { + QString appName = QApplication::applicationName(); // 获取应用名称 + QSettings settings(RegKey, QSettings::NativeFormat); // 创建QSettings对象 + + if (isStart) { + QString appPath = QApplication::applicationFilePath(); // 获取应用路径 + appPath += " --auto-start"; + settings.setValue(appName, appPath.replace("/", "\\")); // 写入注册表 + } else { + settings.remove(appName); // 从注册表中删除 + } +} + +bool MainWindow::isAutoRunEnabled() { + QSettings settings(RegKey, QSettings::NativeFormat); + QString appPath = QDir::toNativeSeparators(QApplication::applicationFilePath()); + return settings.value(QApplication::applicationName()).toString().split(" ")[0] == appPath; +} + +void MainWindow::on_selfStartup_checkStateChanged() { + setAutoRun(ui->selfStartup->isChecked()); +} + +void MainWindow::closeEvent(QCloseEvent *event) { + hideToBackground(); + event->ignore(); // 阻止真正关闭 +} + +void MainWindow::onShowFromTray() { + showNormal(); + activateWindow(); // 把窗口带到前台 + trayIcon->hide(); +} + +void MainWindow::onQuitFromTray() { + qApp->quit(); +} + +void MainWindow::showFromTray() { + onShowFromTray(); +} + +void MainWindow::doAutoRun() { + if (!QCoreApplication::arguments().contains("--auto-start")) + return; + + if (!isOnline) { + doLogin(); + } + + QTimer::singleShot(500, [this] { + if (isOnline) { + hideToBackground(); + trayIcon->showMessage(APPNAME, TrayIconMsg::AutoHideToBackground); + } + }); +} + +void MainWindow::doLogin() { + QNetworkRequest request; + + if (ui->rememberMe->isChecked()) { + saveUserInfo(); + } + + if (isOnline) { + request.setUrl(logoutUrl); + } else { + ui->statusbar->showMessage(QString(StatusBarMsg::WaitForProvider).arg(ui->providerComboBox->currentText())); + + QUrlQuery query; + query.addQueryItem("user_account", ui->usernameLe->text()); + query.addQueryItem("user_password", ui->passwdLe->text()); + query.addQueryItem("provider", ui->providerComboBox->currentData().toString()); + baseUrl.setQuery(query); + + request.setUrl(baseUrl); + } + + QNetworkReply *reply{manager.get(request)}; + + connect(reply, &QNetworkReply::finished, this, [this, reply] { + if (reply->error() == QNetworkReply::NoError) { + QByteArray d{reply->readAll()}; + d.remove(0, 12); // remove jsonpReturn(" + d.chop(2); // remove ") + + QJsonParseError err; + QJsonDocument jd{QJsonDocument::fromJson(d, &err)}; + QString msg; + + if (err.error != QJsonParseError::NoError) { + QMessageBox::critical(this, "数据解析错误", err.errorString() + "\n" + d); + reply->deleteLater(); + ui->statusbar->clearMessage(); + return; + } + QJsonObject jo{jd.object()}; + + if (isOnline) { + if (jo.contains("msg")) + msg = jo.take("msg").toString(); + offline(); + } else { + LoginStatus result{0}; + RetCode code{0}; + if (jo.contains("msg")) + msg = jo.take("msg").toString(); + + if (jo.contains("result")) + result = static_cast(jo.take("result").toInt()); + + if (jo.contains("ret_code") && jo.value("ret_code").isDouble()) { + code = static_cast(jo.take("ret_code").toInt()); + } + + if (result == LoginStatus::Success + || (result == LoginStatus::Failed && code == RetCode::AlreadyOnline)) { + online(); + } + } + + ui->statusbar->showMessage(QString(StatusBarMsg::MsgFromProvider).arg(msg)); + } else { + ui->statusbar->showMessage(QString(StatusBarMsg::NetworkError).arg(reply->errorString())); + } + + reply->deleteLater(); + }); +} + +void MainWindow::hideToBackground() { + hide(); + trayIcon->show(); +} diff --git a/mainwindow.h b/mainwindow.h new file mode 100644 index 0000000..5afb869 --- /dev/null +++ b/mainwindow.h @@ -0,0 +1,111 @@ +#ifndef MAINWINDOW_H +#define MAINWINDOW_H + +#include +#include +#include +#include +#include + +QT_BEGIN_NAMESPACE + +namespace Ui { + class MainWindow; +} + +QT_END_NAMESPACE + +class MainWindow : public QMainWindow { + Q_OBJECT + +public: + MainWindow(QWidget *parent = nullptr); + + ~MainWindow() override; + + void showFromTray(); + +private slots: + void on_loginBtn_clicked(); + + void on_selfStartup_checkStateChanged(); + + void onShowFromTray(); + + void onQuitFromTray(); + +private: + void init(); + + void online(); + + void offline(); + + void setInputEnable(bool); + + void saveUserInfo(); + + void getUserInfo(); + + void testOnline(); + + void setAutoRun(bool); + + bool isAutoRunEnabled(); + + void doAutoRun(); + + void doLogin(); + + void hideToBackground(); + +protected: + void closeEvent(QCloseEvent *event) override; + +private: + struct Provider { + QString name; + QString value; + }; + + enum class LoginStatus { + Failed, + Success, + }; + + enum class RetCode { + Unknown, + WrongAccountOrPasswd, + AlreadyOnline, + }; + + class TrayIconMsg { + public: + static constexpr char Online[]{"AHNU上号器正在后台运行\n目前状态:在线"}; + static constexpr char Offline[]{"AHNU上号器正在后台运行\n目前状态:离线"}; + static constexpr char AutoHideToBackground[]{"你已成功上线,AHNU上号器正在后台运行"}; + }; + + class StatusBarMsg { + public: + static constexpr char WaitForProvider[]{"正在登录到%1..."}; + static constexpr char AlreadyOnline[]{"你已经连接上互联网"}; + static constexpr char NetworkError[]{"认证服务器连接错误:%1"}; + static constexpr char MsgFromProvider[]{"认证服务器:%1"}; + static constexpr char AlreadyOffline[]{"已断开连接"}; + }; + + static constexpr char RegKey[]{"HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Run"}; + + Ui::MainWindow *ui; + std::vector provider; + QUrl baseUrl; + QUrl testUrl; + QUrl logoutUrl; + QNetworkAccessManager manager; + bool isOnline; + QSystemTrayIcon *trayIcon; + QMenu *trayMenu; + QTimer *networkChecker; +}; +#endif // MAINWINDOW_H diff --git a/mainwindow.ui b/mainwindow.ui new file mode 100644 index 0000000..f9833d0 --- /dev/null +++ b/mainwindow.ui @@ -0,0 +1,242 @@ + + + MainWindow + + + + 0 + 0 + 402 + 219 + + + + + 0 + 0 + + + + + 402 + 178 + + + + + 402 + 219 + + + + MainWindow + + + + + QLayout::SizeConstraint::SetFixedSize + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + 400 + 40 + + + + + 16777215 + 16777209 + + + + TextLabel + + + + + + + 80 + + + 9 + + + 80 + + + 9 + + + + + + + + 0 + 0 + + + + + 40 + 0 + + + + 账 号 + + + Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft|Qt::AlignmentFlag::AlignVCenter + + + + + + + + + + + + + + + 0 + 0 + + + + + 40 + 0 + + + + 密 码 + + + Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft|Qt::AlignmentFlag::AlignVCenter + + + + + + + QLineEdit::EchoMode::Password + + + + + + + + + + + + 0 + 0 + + + + + 40 + 0 + + + + 运营商 + + + Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft|Qt::AlignmentFlag::AlignVCenter + + + + + + + + 0 + 0 + + + + + + + + + + + + Qt::Orientation::Horizontal + + + + 40 + 20 + + + + + + + + 开机自启 + + + + + + + 记住我 + + + + + + + 登录 + + + + + + + + + + + + + + + 0 + 0 + 402 + 21 + + + + + + + + diff --git a/res.qrc b/res.qrc new file mode 100644 index 0000000..f5a8f99 --- /dev/null +++ b/res.qrc @@ -0,0 +1,6 @@ + + + images/banner.png + images/logo.png + +