跳转至

二维码远程签到机

声明

本文仅为技术讨论,不鼓励翘课行为。

部分老师有设置 5 秒二维码签到的坏毛病,不能惯着他们。

那么,我们的需求是:

  • 发送者在前端扫描二维码
  • 后端接收发送者获取的二维码数据,低延迟同步给接收者
  • 接收者扫描前端生成的二维码

虽然用长轮询也能实现以上需求,但 Websocket 是更加优雅、便捷的选择。 本文将使用原生 JS 编写前端(没必要用框架),Golang 编写后端(减少心智负担)。

后端

虽然这只是个小工具,但毕竟部署在公网,裸跑大概率会被脚本小子撅。 所以随便签个证书吧,静态资源用 BasicAuth 意思一下,可以加个错误等待时间什么,自行发挥。

//go:embed html
var html embed.FS
var hfs http.Handler

func init() {
    fsys, err := fs.Sub(html, "html")
    if err != nil {
        panic(err)
    }
    hfs = http.FileServer(http.FS(fsys))
}

func handleHtml(w http.ResponseWriter, r *http.Request) {
    name, passwd, ok := r.BasicAuth()
    if !ok {
        w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
        w.WriteHeader(http.StatusUnauthorized)
        return
    }
    if name != "fkqrcode" || passwd != "nopasswd" {
        http.Error(w, "Fuck off!", http.StatusUnauthorized)
        return
    }
    hfs.ServeHTTP(w, r)
}

Websocket 部分抄样例即可,用一个 channel 保存二维码。

var qrch = make(chan string, 3)
var upgrader = websocket.Upgrader{
    ReadBufferSize:  1024,
    WriteBufferSize: 1024,
    CheckOrigin: func(r *http.Request) bool {
        return true
    },
}

func handleSend(w http.ResponseWriter, r *http.Request) {
    conn, err := upgrader.Upgrade(w, r, nil)
    if err != nil {
        return
    }
    defer conn.Close()

    for {
        _, data, err := conn.ReadMessage()
        if err != nil {
            log.Printf("failed to read data from %s : %v\n", r.RemoteAddr, err)
            return
        }
        qr := string(data)
        log.Println(qr)
        select {
        case qrch <- qr:
        default:
        }
    }
}

func handleReceive(w http.ResponseWriter, r *http.Request) {
    conn, err := upgrader.Upgrade(w, r, nil)
    if err != nil {
        return
    }
    defer conn.Close()

    for {
        qr := <-qrch
        err = conn.WriteMessage(websocket.TextMessage, []byte(qr))
        if err != nil {
            log.Printf("failed to send qr to %s : %v\n", r.RemoteAddr, err)
            return
        }
    }
}

前端

前端使用 adapterinstascanqrcode 三个库。

发送端:

<video id="preview"></video>
<select id="select">
</select>
<button onclick="startScan()" id="start" disabled>Start</button>

<script>
    let ws = new WebSocket("wss://" + location.host + "/send");
    let err = () => location.reload();
    ws.onclose = err;
    ws.onerror = err;

    let opts = {};
    let scanner = new Instascan.Scanner({ video: document.getElementById('preview') });
    scanner.refractoryPeriod = 1000;
    scanner.mirror = false;
    scanner.addListener('scan', (content) => ws.send(content));
    Instascan.Camera.getCameras()
        .then((cameras) => {
            if (cameras.length > 0) {
                let select = document.getElementById('select');
                for (camera of cameras) {
                    opts[camera.name] = camera;
                    select.appendChild(new Option(camera.name));
                }
                document.getElementById('start').disabled = '';
            } else {
                err();
            }
        })
        .catch(err);

    function startScan() {
        scanner.stop()
            .then(() => {
                let select = document.getElementById('select');
                scanner.start(opts[select.item(select.selectedIndex).text]).catch(err)
            })
            .catch(err);
    }
</script>

接收端:

<div id="qrcode"></div>
<script>
    let qrcode = new QRCode(document.getElementById("qrcode"), "https://" + location.host);
    let ws = new WebSocket("wss://" + location.host + "/receive");
    ws.onmessage = () => {
        qrcode.clear();
        qrcode.makeCode(event.data);
    };
    ws.onclose = location.reload();
    ws.onerror = location.reload();
</script>

OK,齐活。

评论