学习node.js 十五,短链接,单设备登录,扫码登录

发布于:2024-09-18 ⋅ 阅读:(134) ⋅ 点赞:(0)

短链接

短链接介绍

短链接是一种缩短长网址的方法,将原始的长网址转换为更短的形式。它通常由一系列的字母、数字和特殊字符组成,比起原始的长网址,短链接更加简洁、易于记忆和分享。

短链接的主要用途之一是在社交媒体平台进行链接分享。由于这些平台对字符数量有限制,长网址可能会占用大量的空间,因此使用短链接可以节省字符数,并且更方便在推特、短信等限制字数的场景下使用。

另外,短链接还可以用于跟踪和统计链接的点击量。通过在短链接中嵌入跟踪代码,网站管理员可以获得关于点击链接的详细统计数据,包括访问量、来源、地理位置等信息。这对于营销活动、广告推广或分析链接的效果非常有用。

实现原理:

利用一个接口生成一个唯一的短码,将短码与真实的url存到数据库里面,然后利用动态路径,重定向到真实的url

代码实现

需要依赖的库

  1. epxress 启动服务提供接口
  2. mysql2 knex依赖连接数据库
  3. knex orm框架操作mysql
  4. shortid 生成唯一短码
import knex from "knex"
import express from "express"
import mysql2 from "mysql2"
import shortid from "shortid"

const db = knex({
    client: "mysql2",
    connection: {
        host: "localhost",
        user: "root",
        password: "..",
        database: "lianxi"
    }
})
const app = express()
app.use(express.json())
// 解析www-form-urlencoded格式的数据
app.use(express.urlencoded({ extended: true }))

// 使用短链接技术
app.post("/create_url", async (req, res) => {
    const url = req.body.url
    if(!url) return res.send("请输入要缩短的链接")
    const short_id = shortid.generate()
    const result = await db("short").insert({ short_id, url })
    res.send(`http://localhost:3000/${short_id}`)
})

app.get("/:short_id", async (req, res) => {
    const short_id = req.params.short_id
    if(!short_id) return res.send("请输入要访问的链接")
    const url = await db("short").where({ short_id }).first()
    if(!url) return res.send("链接不存在")
    res.redirect(url.url)
})

app.listen(3000, () => {
    console.log("服务器已启动")
})

SDL单设备登录

SDL(Single Device Login)是一种单设备登录的机制,它允许用户在同一时间只能在一个设备上登录,当用户在其他设备上登录时,之前登录的设备会被挤下线。

应用场景

  1. 视频影音,防止一个账号共享,防止一些账号贩子
  2. 对于在线购物和电子支付平台,用户的支付信息和订单详情是敏感的。通过单设备登录,可以在用户进行支付操作时增加额外的安全层级,确保只有授权设备可以进行支付操作

实现思路

设计数据结构
connection = {
    id: {
        socket, // wss实例
        fingerprint // 浏览器指纹
    }
}
  1. 初次登录,那么就使用用户id为key存储wss实例以及浏览器指纹
  2. 二次登录,先使用存好的wss实例给前端发信息,告诉说被挤下线,然后关闭之前的连接,存储新的wss实例
浏览器指纹

这里采用canvas指纹技术

需要利用的库

express ,cors解决跨域,ws 一个webSocket的库

如果不了解webSocket清看:万字详解,带你彻底掌握 WebSocket 用法(至尊典藏版)写的不错_websocket用法-CSDN博客

实现代码

node.js端

import express from 'express';
import cors from 'cors';
import {WebSocketServer} from  "ws"

const app = express();
app.use(cors());
app.use(express.json());

const server = app.listen(3000,()=> {
    console.log("Server is running on port 3000");
})
// 创建websocket服务器
const wss = new WebSocketServer({server});
let connection = {}
// 监听连接事件
wss.on("connection", (ws) => {
    ws.on("message", (message) => {
        const data = JSON.parse(message)
        if(data.action === "login") {
            // 查看是否为第一次登录,
            if(connection[data.id] && connection[data.id].fingerprint) {
                console.log("已登录")
                // 提示旧设备别处登录
                connection[data.id].ws.send(JSON.stringify({
                    action: "logout",
                    message: `您的账号在${new Date().toLocaleDateString()}于别处登录`
                }))
                // 断开连接
                connection[data.id].ws.close()
                // 更新ws
                connection[data.id].ws = ws
            }else { // 第一次登录
                console.log("初次登录")
                connection[data.id] = {
                    ws,
                    fingerprint: data.fingerprint
                }
            }
        }
    })
})

前端

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<script src="./md5.js"></script>
    <script>
        // 生成浏览器指纹函数
        function getFingerprint() {
            const canvas = document.createElement('canvas');
            const ctx = canvas.getContext('2d');
            ctx.font = "18px Arial";
            ctx.fillText("Hello, world!", 10, 50);
            return hex_md5(canvas.toDataURL());
        }
        // 创建wss连接
        const ws = new WebSocket('ws://localhost:3000');
        // 监听连接成功
        ws.addEventListener("open",()=> {
            ws.send(JSON.stringify({
                action: "login",
                id: 1, // 用户id
                fingerprint: getFingerprint() // 浏览器指纹
            }))
        })
        // 监听消息
        ws.addEventListener("message", (message) => {
            const data = JSON.parse(message.data);
            if (data.action === "logout") {
                alert(data.message);
            }
        })
    </script>
</body>
</html>

md5.js

/*
 * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message
 * Digest Algorithm, as defined in RFC 1321.
 * Version 2.1 Copyright (C) Paul Johnston 1999 - 2002.
 * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet
 * Distributed under the BSD License
 * See http://pajhome.org.uk/crypt/md5 for more info.
 */
/*
 * Configurable variables. You may need to tweak these to be compatible with
 * the server-side, but the defaults work in most cases.
 */
var hexcase = 0;  /* hex output format. 0 - lowercase; 1 - uppercase        */
var b64pad  = ""; /* base-64 pad character. "=" for strict RFC compliance   */
var chrsz   = 8;  /* bits per input character. 8 - ASCII; 16 - Unicode      */

/*
 * These are the functions you'll usually want to call
 * They take string arguments and return either hex or base-64 encoded strings
 */
function hex_md5(s){ return binl2hex(core_md5(str2binl(s), s.length * chrsz));}
function b64_md5(s){ return binl2b64(core_md5(str2binl(s), s.length * chrsz));}
function str_md5(s){ return binl2str(core_md5(str2binl(s), s.length * chrsz));}
function hex_hmac_md5(key, data) { return binl2hex(core_hmac_md5(key, data)); }
function b64_hmac_md5(key, data) { return binl2b64(core_hmac_md5(key, data)); }
function str_hmac_md5(key, data) { return binl2str(core_hmac_md5(key, data)); }

/*
 * Perform a simple self-test to see if the VM is working
 */
function md5_vm_test()
{
    return hex_md5("abc") == "900150983cd24fb0d6963f7d28e17f72";
}

/*
 * Calculate the MD5 of an array of little-endian words, and a bit length
 */
function core_md5(x, len)
{
    /* append padding */
    x[len >> 5] |= 0x80 << ((len) % 32);
    x[(((len + 64) >>> 9) << 4) + 14] = len;
    var a =  1732584193;
    var b = -271733879;
    var c = -1732584194;
    var d =  271733878;

    for(var i = 0; i < x.length; i += 16)
    {
        var olda = a;
        var oldb = b;
        var oldc = c;
        var oldd = d;

        a = md5_ff(a, b, c, d, x[i+ 0], 7 , -680876936);
        d = md5_ff(d, a, b, c, x[i+ 1], 12, -389564586);
        c = md5_ff(c, d, a, b, x[i+ 2], 17,  606105819);
        b = md5_ff(b, c, d, a, x[i+ 3], 22, -1044525330);
        a = md5_ff(a, b, c, d, x[i+ 4], 7 , -176418897);
        d = md5_ff(d, a, b, c, x[i+ 5], 12,  1200080426);
        c = md5_ff(c, d, a, b, x[i+ 6], 17, -1473231341);
        b = md5_ff(b, c, d, a, x[i+ 7], 22, -45705983);
        a = md5_ff(a, b, c, d, x[i+ 8], 7 ,  1770035416);
        d = md5_ff(d, a, b, c, x[i+ 9], 12, -1958414417);
        c = md5_ff(c, d, a, b, x[i+10], 17, -42063);
        b = md5_ff(b, c, d, a, x[i+11], 22, -1990404162);
        a = md5_ff(a, b, c, d, x[i+12], 7 ,  1804603682);
        d = md5_ff(d, a, b, c, x[i+13], 12, -40341101);
        c = md5_ff(c, d, a, b, x[i+14], 17, -1502002290);
        b = md5_ff(b, c, d, a, x[i+15], 22,  1236535329);

        a = md5_gg(a, b, c, d, x[i+ 1], 5 , -165796510);
        d = md5_gg(d, a, b, c, x[i+ 6], 9 , -1069501632);
        c = md5_gg(c, d, a, b, x[i+11], 14,  643717713);
        b = md5_gg(b, c, d, a, x[i+ 0], 20, -373897302);
        a = md5_gg(a, b, c, d, x[i+ 5], 5 , -701558691);
        d = md5_gg(d, a, b, c, x[i+10], 9 ,  38016083);
        c = md5_gg(c, d, a, b, x[i+15], 14, -660478335);
        b = md5_gg(b, c, d, a, x[i+ 4], 20, -405537848);
        a = md5_gg(a, b, c, d, x[i+ 9], 5 ,  568446438);
        d = md5_gg(d, a, b, c, x[i+14], 9 , -1019803690);
        c = md5_gg(c, d, a, b, x[i+ 3], 14, -187363961);
        b = md5_gg(b, c, d, a, x[i+ 8], 20,  1163531501);
        a = md5_gg(a, b, c, d, x[i+13], 5 , -1444681467);
        d = md5_gg(d, a, b, c, x[i+ 2], 9 , -51403784);
        c = md5_gg(c, d, a, b, x[i+ 7], 14,  1735328473);
        b = md5_gg(b, c, d, a, x[i+12], 20, -1926607734);

        a = md5_hh(a, b, c, d, x[i+ 5], 4 , -378558);
        d = md5_hh(d, a, b, c, x[i+ 8], 11, -2022574463);
        c = md5_hh(c, d, a, b, x[i+11], 16,  1839030562);
        b = md5_hh(b, c, d, a, x[i+14], 23, -35309556);
        a = md5_hh(a, b, c, d, x[i+ 1], 4 , -1530992060);
        d = md5_hh(d, a, b, c, x[i+ 4], 11,  1272893353);
        c = md5_hh(c, d, a, b, x[i+ 7], 16, -155497632);
        b = md5_hh(b, c, d, a, x[i+10], 23, -1094730640);
        a = md5_hh(a, b, c, d, x[i+13], 4 ,  681279174);
        d = md5_hh(d, a, b, c, x[i+ 0], 11, -358537222);
        c = md5_hh(c, d, a, b, x[i+ 3], 16, -722521979);
        b = md5_hh(b, c, d, a, x[i+ 6], 23,  76029189);
        a = md5_hh(a, b, c, d, x[i+ 9], 4 , -640364487);
        d = md5_hh(d, a, b, c, x[i+12], 11, -421815835);
        c = md5_hh(c, d, a, b, x[i+15], 16,  530742520);
        b = md5_hh(b, c, d, a, x[i+ 2], 23, -995338651);

        a = md5_ii(a, b, c, d, x[i+ 0], 6 , -198630844);
        d = md5_ii(d, a, b, c, x[i+ 7], 10,  1126891415);
        c = md5_ii(c, d, a, b, x[i+14], 15, -1416354905);
        b = md5_ii(b, c, d, a, x[i+ 5], 21, -57434055);
        a = md5_ii(a, b, c, d, x[i+12], 6 ,  1700485571);
        d = md5_ii(d, a, b, c, x[i+ 3], 10, -1894986606);
        c = md5_ii(c, d, a, b, x[i+10], 15, -1051523);
        b = md5_ii(b, c, d, a, x[i+ 1], 21, -2054922799);
        a = md5_ii(a, b, c, d, x[i+ 8], 6 ,  1873313359);
        d = md5_ii(d, a, b, c, x[i+15], 10, -30611744);
        c = md5_ii(c, d, a, b, x[i+ 6], 15, -1560198380);
        b = md5_ii(b, c, d, a, x[i+13], 21,  1309151649);
        a = md5_ii(a, b, c, d, x[i+ 4], 6 , -145523070);
        d = md5_ii(d, a, b, c, x[i+11], 10, -1120210379);
        c = md5_ii(c, d, a, b, x[i+ 2], 15,  718787259);
        b = md5_ii(b, c, d, a, x[i+ 9], 21, -343485551);

        a = safe_add(a, olda);
        b = safe_add(b, oldb);
        c = safe_add(c, oldc);
        d = safe_add(d, oldd);
    }
    return Array(a, b, c, d);

}

/*
 * These functions implement the four basic operations the algorithm uses.
 */
function md5_cmn(q, a, b, x, s, t)
{
    return safe_add(bit_rol(safe_add(safe_add(a, q), safe_add(x, t)), s),b);
}
function md5_ff(a, b, c, d, x, s, t)
{
    return md5_cmn((b & c) | ((~b) & d), a, b, x, s, t);
}
function md5_gg(a, b, c, d, x, s, t)
{
    return md5_cmn((b & d) | (c & (~d)), a, b, x, s, t);
}
function md5_hh(a, b, c, d, x, s, t)
{
    return md5_cmn(b ^ c ^ d, a, b, x, s, t);
}
function md5_ii(a, b, c, d, x, s, t)
{
    return md5_cmn(c ^ (b | (~d)), a, b, x, s, t);
}

/*
 * Calculate the HMAC-MD5, of a key and some data
 */
function core_hmac_md5(key, data)
{
    var bkey = str2binl(key);
    if(bkey.length > 16) bkey = core_md5(bkey, key.length * chrsz);

    var ipad = Array(16), opad = Array(16);
    for(var i = 0; i < 16; i++)
    {
        ipad[i] = bkey[i] ^ 0x36363636;
        opad[i] = bkey[i] ^ 0x5C5C5C5C;
    }

    var hash = core_md5(ipad.concat(str2binl(data)), 512 + data.length * chrsz);
    return core_md5(opad.concat(hash), 512 + 128);
}

/*
 * Add integers, wrapping at 2^32. This uses 16-bit operations internally
 * to work around bugs in some JS interpreters.
 */
function safe_add(x, y)
{
    var lsw = (x & 0xFFFF) + (y & 0xFFFF);
    var msw = (x >> 16) + (y >> 16) + (lsw >> 16);
    return (msw << 16) | (lsw & 0xFFFF);
}

/*
 * Bitwise rotate a 32-bit number to the left.
 */
function bit_rol(num, cnt)
{
    return (num << cnt) | (num >>> (32 - cnt));
}

/*
 * Convert a string to an array of little-endian words
 * If chrsz is ASCII, characters >255 have their hi-byte silently ignored.
 */
function str2binl(str)
{
    var bin = Array();
    var mask = (1 << chrsz) - 1;
    for(var i = 0; i < str.length * chrsz; i += chrsz)
        bin[i>>5] |= (str.charCodeAt(i / chrsz) & mask) << (i%32);
    return bin;
}

/*
 * Convert an array of little-endian words to a string
 */
function binl2str(bin)
{
    var str = "";
    var mask = (1 << chrsz) - 1;
    for(var i = 0; i < bin.length * 32; i += chrsz)
        str += String.fromCharCode((bin[i>>5] >>> (i % 32)) & mask);
    return str;
}

/*
 * Convert an array of little-endian words to a hex string.
 */
function binl2hex(binarray)
{
    var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef";
    var str = "";
    for(var i = 0; i < binarray.length * 4; i++)
    {
        str += hex_tab.charAt((binarray[i>>2] >> ((i%4)*8+4)) & 0xF) +
            hex_tab.charAt((binarray[i>>2] >> ((i%4)*8  )) & 0xF);
    }
    return str;
}

/*
 * Convert an array of little-endian words to a base-64 string
 */
function binl2b64(binarray)
{
    var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
    var str = "";
    for(var i = 0; i < binarray.length * 4; i += 3)
    {
        var triplet = (((binarray[i   >> 2] >> 8 * ( i   %4)) & 0xFF) << 16)
            | (((binarray[i+1 >> 2] >> 8 * ((i+1)%4)) & 0xFF) << 8 )
            |  ((binarray[i+2 >> 2] >> 8 * ((i+2)%4)) & 0xFF);
        for(var j = 0; j < 4; j++)
        {
            if(i * 8 + j * 6 > binarray.length * 32) str += b64pad;
            else str += tab.charAt((triplet >> 6*(3-j)) & 0x3F);
        }
    }
    return str;
}

SCL 扫码登陆

SCL (Scan Code Login) 是一种扫码登录的技术,它允许用户通过扫描二维码来进行登录操作。这种登录方式在许多应用和网站中得到广泛应用,因为它简单、方便且安全。

SCL 扫码登录的优点包括:

  1. 方便快捷:用户只需打开扫码应用程序并扫描二维码即可完成登录,无需手动输入用户名和密码。
  2. 安全性高:扫码登录采用了加密技术,用户的登录信息在传输过程中得到保护,降低了密码被盗取或泄露的风险。
  3. 避免键盘记录:由于用户无需在登录过程中输入敏感信息,如用户名和密码,因此不会受到键盘记录软件的威胁。
  4. 适用性广泛:SCL 扫码登录可以与不同的应用和网站集成,提供统一的登录方式,使用户无需记住多个账户的用户名和密码。

安装依赖

  1. express
  2. jsonwebtoken
  3. qrcode 生成二维码

实现思路

  1. 需要有一个接口去生成二维码,里面应该初始化信息,token标定用户是否登录,id识别用户身份
  2. 确认授权接口,当点击确认授权之后,生成token
  3. 需要检查二维码是否过期的接口,前端去轮询这个接口,当过期或者已登录,就停止轮询

实现代码

node.js

import express from 'express';
import cors from 'cors';
import qrcode from 'qrcode';
import jwt from 'jsonwebtoken';

const app = express();
app.use(cors());
app.use(express.json());
// 静态文件
app.use(express.static('public'));

const user = {

}
const userId = 1
// 生成二维码接口
app.get('/qrcode', async (req, res) => {
    // 1. 初始化数据
    user[userId] = {
        token: null,
        time: Date.now()
    }
    // 2. 生成二维码
    const code = await qrcode.toDataURL(`http://192.168.253.1:3000/manData?userId=${userId}`);
    // 3. 返回二维码
    res.json({
        code,
        userId
    })
})
// 扫码接口
app.post("login/:userId", (req, res) => {
  const userId = req.params.userId;
  user[userId].token = jwt.sign({ userId }, 'secret', { expiresIn: '1h' });
  user[userId].time = Date.now();
  res.json({
    token: user[userId].token
  })
})
// 轮询二维码是否有效
app.get("/check/:userId", (req, res) => {
   const userId = req.params.userId;
   if (user[userId].time + 1000 * 60 * 3 < Date.now()) {
       res.json({
           status: 2 // 超时
       })
   }else if (user[userId].token) {
       res.json({
           status: 1 // 有效
       })
   }else {
       res.json({
           status: 0 // 默认值
       })
   }
})
app.listen(3000, () => console.log('Server is running on port 3000'));

qrcode.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
<img id="qrcode" src="" alt="">
<div id="status-div"></div>
<script>
    const status = {
        0: '未授权',
        1: '已授权',
        2: '超时'
    }
    const qrcode = document.getElementById('qrcode')
    const statusDiv = document.getElementById('status-div')
    let userId = null
    statusDiv.innerText = status[0]
    fetch('http://localhost:3000/qrcode').then(res => res.json()).then(res => {
        qrcode.src = res.code //获取二维码
        userId = res.userId //获取用户id
        let timer = setInterval(() => {
            //轮询调用检查接口
            fetch(`http://localhost:3000/check/${userId}`).then(res => res.json()).then(res => {
                statusDiv.innerText = status[res.status]
                //如果返回的状态是 超时 或者是已授权 就停止轮训
                if (res.status != 0) {
                    clearInterval(timer)
                }
            })
        }, 1000)
    })

</script>
</body>

</html>

manData.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
<div> <button id="btn" style="width: 100%;height: 50px;">同意授权</button></div>
<div> <button style="width: 100%;height: 50px;margin-top: 20px;">拒绝授权</button></div>
<script>
    const btn = document.getElementById('btn')
    let userId = location.search.slice(1).split('=')[1]
    btn.onclick = () => {
        //点击授权按钮
        fetch(`/login/${userId}`, {
            method: 'POST',
        }).then(res => res.json()).then(res => {
            alert(`授权成功`)
        }).catch(err => {
            alert(err)
        })
    }
</script>
</body>

</html>


网站公告

今日签到

点亮在社区的每一天
去签到