教务系统直链登录之 Node.js 实现与前端设计

之前用 Python 把学校教务系统的登录直链生成逻辑实现了一下,但毕竟是个命令行程序,极大限制了运行的便利程度。想来如此简单的计算方式,用任何一门语言应该都能很快实现。

自从上次初学 Node.js,搭了个 Webhook 之后,就感觉自己终于有可能不再被拘束于黑漆漆的终端,而可以自己做个前端界面玩玩了。于是着手开始使用 Node.js 写脚本。

链接生成规则详见《NJUST 教务系统免验证码直链登录初探》,这里不再赘述。其关键是计算一个 MD5 值。js 里似乎没有直接计算的函数,需要用别人的轮子。这里我引入了 blueimp/JavaScript-MD5 这个实现方案,可以点击这里直接下载 md5.js,放到我们正在做的 js 脚本同目录下。注意,这个模块的输出是小写的 MD5 值,需要使用 toUpperCase 函数做一个转换。

链接生成核心语句如下:

1
2
3
4
5
var link =
"http://202.119.81.112:9080/njlgdx/xk/LoginToXk?method=verify&USERNAME=" +
POST.id.toUpperCase() +
"&PASSWORD=" +
md5(POST.pw).toUpperCase()

其中 POST.idPOST.pw 都来自用户从 Web 界面提交的表单。

用 HTML 写一个简易(简陋)的表单提交页面很容易。整个服务器的代码参考之前 Webhook 的模式。完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
var http = require('http');
var querystring = require('querystring');
var md5 = require('./md5');

var postHTML =
  '<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title> 教务处免密登录链接生成 </title></head>' +
  '<body>' +
  '<form method="post">' +
  ' 学号: <input name="id"><br>' +
  ' 密码: <input name="pw" type="password"><br>' +
  '<input type="submit">' +
  '</form>' +
  '</body></html>';

http.createServer(function (req, res) {
  var POST = "";
  req.on('data', function (chunk) {
    POST += chunk;
  });
  req.on('end', function () {
    // 解析参数
    POST = querystring.parse(POST);
    // 设置响应头部信息及编码
    res.writeHead(200, { 'Content-Type': 'text/html; charset=utf8' });
    if (POST.id && POST.pw) {
      var link = "http://202.119.81.112:9080/njlgdx/xk/LoginToXk?method=verify&USERNAME=" + POST.id.toUpperCase() + "&PASSWORD=" + md5(POST.pw).toUpperCase()
      res.write(" 免密登录链接:" + "<a href=\"" + link + "\" target=\"_blank\">" + link + "</a>");
    } else {  // 输出表单
      res.write(postHTML);
    }
    res.end();
  });
}).listen(7400);

console.log('Server running at http://0.0.0.0:7400/');

手机上的运行效果如下:

jwc_min.js_1

提交之后:

jwc_min.js_2

可见,基本功能是实现了。然而尽管特地加入了 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 这句响应式布局的门槛,Web 界面还是显得很丑相当简约。

于是,为了做出真正意义上的、我人生的第一个亲手写的、能让人看得下去的网页,我下了很大功夫,研究了一下各种主流的前端框架,终于从入门到放弃了决定这些统统都不用。

最后,我从 OneIndex 的密码输入页面找到了灵感:

oneindex

完美的响应式布局!简约而舒服的 MD 配色!还有动画效果!

虽然 OneIndex 的网页是用 PHP 生成的,但这并不妨碍我直接扒下它的 HTML 源码。从源码里可以看出,这个网页的 CSS 用的是一个叫做 MDUI 的框架。搜了一下,这个 CSS 框架用于开发 Material Design 网页,比较小众,但好评很多,控件也覆盖得比较丰富。就决定是你了,MDUI!

套 CSS 框架可比用什么三大框架(React、Vue、Angular)、Bootstrap、jQuery 简单多了,无非是开头引入一下 css 和 js:

1
2
<link rel="stylesheet" href="https://cdnjs.loli.net/ajax/libs/mdui/0.4.3/css/mdui.min.css">
<script src="https://cdnjs.loli.net/ajax/libs/mdui/0.4.3/js/mdui.min.js"></script>

然后对着开发文档魔改就行了。

最终成果如图:

mdui

用 Node.js 输出外部的一个 HTML 文件作为响应也很简单,首先要引入 fs 模块:

1
var fs = require('fs');

然后即可在需要的地方读取文件。如 response 以 jwc.html 结束:

1
res.end(fs.readFileSync('./jwc.html', 'UTF-8'))

在 Web 界面上填写完学号密码,点击生成后,会打开新的页面展示链接,这个界面就比较简陋了,没有再去做美化,类似刚才的版本。

此外,由于毕竟涉及密码的填写,url 直接用 <ip>:<port> 的形式未免显得太不安全,最好还是希望能够使用域名配合 https 协议。在 Node.js 里搭 https 服务器与 http 的区别在于:

  1. 开头引入的是 https 模块:

    1
    let https = require("https");
    
  2. 需要添加域名的 SSL 证书和密钥文件:

    1
    2
    3
    4
    const httpsOption = {
      key: fs.readFileSync("./privkey.pem"),
      cert: fs.readFileSync("./fullchain.pem")
    }
    

    我之前申请的是域名通配符证书,所以就直接拿来用了。

  3. 创建服务器:

    1
    2
    3
    https.createServer(httpsOption, function (req, res) {
      //回调函数体
    }).listen(httpsPort);
    

其他与 http 服务器完全一致。

这样做之后只能就只能通过 https 来访问了。要想实现 http 自动跳转到 https 的话,则需要一些其他的手段。

传统的方法是重定向,比如:

1
2
3
res.writeHead(301,{
  'Location':'https://...'
});

然而,我的 url 是 <domain>:<port> 形式的,想要通过 301 做到 http://<domain>:<port>https://<domain>:<port> 显然是不可能的,因为 http 服务器监听的端口肯定是不能与 https 重复的。

《【node】express 请求 http 与 https 一起能访问》 一文提供了一种可行的实现思路。Node.js 中 http 与 https 都是工作于应用层的,可以通过更底层的 TCP 协议来进行控制。

简单来说,就是 http 和 https 分别监听不同的端口,如 http 7401,https 7402;然后引入 net 模块,监听最后真正要访问的那个端口,如 7400,它负责判断请求的是 http 还是 https,然后从各自的端口取数据进行响应(实际是反向代理);https 服务器可以直接执行 301 重定向到 https://<domain>:<port>

net 部分的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 1、引入依赖
var net = require('net'); // 使用代理

// 2、创建服务器进行代理
net.createServer(function(socket){
 socket.once('data', function(buf){
  console.log(buf[0]);
  // https数据流的第一位是十六进制“16”,转换成十进制就是22
  var address = buf[0] === 22 ? httpsPort : httpPort;
  //创建一个指向https或http服务器的链接
  var proxy = net.createConnection(address, function() {
   proxy.write(buf);
   //反向代理的过程,tcp接受的数据交给代理链接,代理链接服务器端返回数据交由socket返回给客户端
   socket.pipe(proxy).pipe(socket);
  });
  proxy.on('error', function(err) {
   console.log(err);
  });
 });
 socket.on('error', function(err) {
  console.log(err);
 });
},app).listen(7400); // 此处是真正能够访问的端口,网站默认是80端口。

整合一下 http 和 https 服务器的代码,最终完整版代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
var net = require('net');
var http = require('http');
let https = require("https");
var querystring = require('querystring');
var md5 = require('./md5');
var fs = require('fs');

var httpPort = 7401;
var httpsPort = 7402;

net.createServer(function (socket) {
  socket.once('data', function (buf) {
    console.log(buf[0]);
    // https数据流的第一位是十六进制“16”,转换成十进制就是22
    var address = buf[0] === 22 ? httpsPort : httpPort;
    //创建一个指向https或http服务器的链接
    var proxy = net.createConnection(address, function () {
      proxy.write(buf);
      //反向代理的过程,tcp接受的数据交给代理链接,代理链接服务器端返回数据交由socket返回给客户端
      socket.pipe(proxy).pipe(socket);
    });
    proxy.on('error', function (err) {
      console.log(err);
    });
  });
  socket.on('error', function (err) {
    console.log(err);
  });
}).listen(7400);

// Configuare https
const httpsOption = {
  key: fs.readFileSync("./privkey.pem"),
  cert: fs.readFileSync("./fullchain.pem")
}

http.createServer(function (req, res) {
  // 重定向
  res.writeHead(301, {
    'Location': '<domain>:<port>'
  });
  res.end();
}).listen(httpPort);

https.createServer(httpsOption, function (req, res) {
  //console.log('hello');
  var POST = "";
  req.on('data', function (chunk) {
    POST += chunk;
  });
  req.on('end', function () {
    // 解析参数
    POST = querystring.parse(POST);
    // 设置响应头部信息及编码
    res.writeHead(200, { 'Content-Type': 'text/html; charset=utf8' });
    if (POST.id && POST.pw) {
      var link = "http://202.119.81.112:9080/njlgdx/xk/LoginToXk?method=verify&USERNAME=" + POST.id.toUpperCase() + "&PASSWORD=" + md5(POST.pw).toUpperCase()
      res.write("<html><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>请妥善保存链接</title></head><body>");
      res.write(" 免密登录链接:<br>" + "<a href=\"" + link + "\" target=\"_blank\">" + link + "</a>");
      res.write("</body></html>");
    }
    else {  // 输出表单
      res.end(fs.readFileSync('./jwc.html', 'UTF-8'))
    }
    res.end();
  });
}).listen(httpsPort);

console.log('Server running at http://0.0.0.0:7400/');

这样就能够完美实现 http 与 https 均可访问,且 https 自动跳转 https 啦。

以上所有相关代码(包括 HTML)都可以在我的 repo jasongzy/njust-jwc 里找到。

彩蛋:我选择的服务器访问端口是 7400。EE 专业的可能知道,7400 芯片是四组二入与非门。至于与非门和南理工有什么关系嘛……有 0 出 1,全 1 出 0

-------------本文结束    感谢您的阅读-------------
0%