backend: 添加token验证功能,重构用户登录逻辑,优化消息发送与接收

This commit is contained in:
Starlight-0208 2026-01-21 18:17:25 +08:00
parent adc339af8f
commit 04b32791e9
6 changed files with 475 additions and 254 deletions

47
app.js
View File

@ -5,8 +5,9 @@ import apiRouter from "./router/api.js"
import { Server } from "socket.io"; import { Server } from "socket.io";
import { saveToDatabase } from "./utils/storage.js"; import { saveToDatabase } from "./utils/storage.js";
import { onlineList, clientMap } from "./data.js"; import { onlineList, clientMap } from "./data.js";
import { verifyToken } from "./utils/auth.js";
const PORT = process.env.PORT || 5691; const PORT = process.env.PORT || 1111;
const app = express(); const app = express();
@ -52,25 +53,37 @@ io.on("connection", (socket) => {
const { opt, args } = JSON.parse(data); const { opt, args } = JSON.parse(data);
logger.trace(opt, args) logger.trace(opt, args)
if (opt == "signin") { if (opt == "signin") {
if (!args || !args.username) { if (!args || !args.token) {
socket.emit("system", JSON.stringify(new Response(false, "username is empty."))) socket.emit("system", JSON.stringify(new Response(false, "token is empty.")))
return; return;
} }
const { username } = args const { token } = args
if (username.length > 20) { // const username = token.trim();
socket.emit("system", JSON.stringify(new Response(false, "username is too long."))) verifyToken(token)
} .then(res => {
if (onlineList.indexOf(username) != -1) { if (res.data.code != 200) {
socket.emit("system", JSON.stringify(new Response(false, "username is already taken."))) socket.emit("system", JSON.stringify(new Response(false, "token is invaild.")))
username = res.data.UserName;
uid = res.data.ID;
return;
}
if (onlineList.indexOf(username) != -1) {
socket.emit("system", JSON.stringify(new Response(false, "token is already taken.")))
return;
}
socket.emit("system", JSON.stringify(new Response(true, "sign in successfully.")))
socket.emit("username", username)
socket.username = username;
onlineList.push(username);
clientMap.set(socket.id, username);
io.emit("notice", JSON.stringify(new Response(true, `${socket.username} join the chat.`)))
logger.debug(`socket(${socket.id}) -- ${username} signed in.`)
})
.catch(err => {
logger.error(`socket(${socket.id}) -- ${err}.`)
socket.emit("system", JSON.stringify(new Response(false, "token is invaild.")))
return; return;
} })
socket.username = username;
onlineList.push(username);
clientMap.set(socket.id, username);
socket.emit("username", socket.username);
socket.emit("system", JSON.stringify(new Response(true, `welcome, ${socket.username}`)))
io.emit("notice", JSON.stringify(new Response(true, `${socket.username} join the chat.`)))
logger.debug(`socket(${socket.id}) -- ${username} signed in.`)
} }
} catch (err) { } catch (err) {

View File

@ -11,6 +11,7 @@
"author": "star", "author": "star",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"axios": "^1.13.2",
"express": "^5.1.0", "express": "^5.1.0",
"log4js": "^6.9.1", "log4js": "^6.9.1",
"sequelize": "^6.37.7", "sequelize": "^6.37.7",

View File

@ -4,267 +4,63 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title> <title>Chat</title>
<style> <link rel="stylesheet" href="style/style.css" />
body { </head>
font-family: 'HarmonyOS Sans SC Medium';
}
.title {
font-size: 24px;
font-weight: bolder;
}
.card {
border: 1px solid #ddd;
border-radius: 8px;
padding: 15px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
background-color: #f9f9f9;
}
#container {
display: flex;
flex-direction: row;
gap: 5px;
}
#msg-list {
width: 100%;
display: flex;
flex-direction: column;
gap: 5px;
}
#online-list {
display: flex;
flex-direction: column;
gap: 5px;
width: 800px;
}
#sysmsg {
display: flex;
flex-direction: column;
gap: 2px;
}
#usrmsg > span {
display: flex;
flex-direction: column;
gap: 2px;
}
#online {
display: grid;
grid-template-columns: repeat(6, 1fr);
gap: 5px;
padding-top: 10px;
}
#online > span {
border: 1px solid #ddd;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
background-color: #f9f9f9;
padding: 10px;
text-align: center;
}
input[type="text"] {
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
margin-right: 5px;
}
button, input[type="button"] {
padding: 8px 12px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background-color: #45a049;
}
@media screen and (max-width: 768px) {
#container {
flex-direction: column-reverse;
}
#online-list {
width: auto;
}
}
</style>
</head>
<body> <body>
<div id="container"> <div id="container">
<div id="msg-list"> <div id="msg-list">
<div class="card"> <div class="card">
<div class="title">系统消息</div> <div class="title">系统消息</div>
<div id="sysmsg"></div> <div id="sysmsg"></div>
</div> </div>
<div class="card"> <div class="card" style="flex: 1; display: flex; flex-direction: column;">
<div class="title">用户消息</div> <div class="title">消息</div>
<div id="usrmsg"></div> <div id="usrmsg" style="flex: 1;"></div>
</div> </div>
<div class="card"> <div class="card">
<div style="display: flex;"> <div class="input-group">
<input type="text" id="message" style="width: 100%;" /> <input type="text" id="message" placeholder="输入消息..." />
<button onclick="sendMsg()">Submit</button> <button onclick="sendMsg()">发送</button>
</div> </div>
</div> </div>
</div> </div>
<div id="online-list"> <div id="online-list">
<div class="card"> <div class="card">
<div class="title">在线列表</div> <div class="title">在线用户</div>
<div style="color: gray;">在线人数:<span id="online-number">NaN</span></div> <div style="color: var(--text-secondary); font-size: 13px; margin-bottom: 8px;">
在线人数:<span id="online-number">0</span>
</div>
<div id="online"></div> <div id="online"></div>
</div> </div>
<div class="card"> <div class="card">
<div class="title">服务器选项</div> <div class="title">服务器状态</div>
<div>连接状态: <span id="stat"></span></div> <div style="margin-bottom: 12px;">
<button onclick="connect()">连接</button> <span id="stat" class="status-badge disconnected">
<button onclick="disconnect()">断开</button> <span class="status-dot"></span>
<span id="stat-text">disconnected</span>
</span>
</div>
<div class="input-group">
<button onclick="connect()">连接</button>
<button onclick="disconnect()" style="background-color: var(--text-secondary);">断开</button>
</div>
</div> </div>
<div class="card"> <div class="card">
<div class="title">设置用户名</div> <div class="title">用户信息</div>
<div>当前用户名:<span id="current-username">未设置用户名</span></div> <div id="user-info">
<input type="text" id="username" maxlength="20" /> <div class="username-display" id="current-username">未登录</div>
<button onclick="register()">提交</button> <div class="login-prompt" id="login-prompt" style="display: none;">
<p>请先登录</p>
<button onclick="showLogin()">去登录</button>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
<div style="padding-top: 20px" id="control">
<div>
</div>
<div>
</div>
</div>
<script src="https://cdn.socket.io/4.7.5/socket.io.min.js"></script> <script src="https://cdn.socket.io/4.7.5/socket.io.min.js"></script>
<script> <script src="script/main.js"></script>
window.onload = () => {
fetchHistory()
}
const socket = io("ws://localhost:5691");
socket.on("connect", () => {
console.log("Connected");
updateState(true)
writeLogSystem("You are connected to server", true, "client")
});
socket.on("system", data => {
console.log("system", data)
data = JSON.parse(data)
writeLogSystem(data.msg, data.status, "system")
})
socket.on("username", data => {
updateUsername(data);
})
socket.on("notice", data => {
console.log("notice", data)
data = JSON.parse(data)
writeLogSystem(data.msg, data.status, "notice")
})
socket.on("msg", data => {
console.log("user", data)
data = JSON.parse(data);
let msg = ` ${data.msg}`
writeLogUser(msg, data.sender)
})
socket.on("disconnect", () => {
updateState(false);
console.log("Disconnected")
writeLogSystem("You are disconnected from server", false, "client")
updateUsername("未设置用户名")
});
const connect = () => socket.connect()
const disconnect = () => socket.disconnect();
const register = () => socket.emit("system", JSON.stringify({ opt: "signin", args: { username: document.getElementById("username").value } }));
const sendMsg = () => {
socket.emit("msg", document.getElementById("message").value)
document.getElementById("message").value = ""
}
const updateState = (value) => {
document.getElementById('stat').textContent = value ? "connected" : "disconnected";
}
document.getElementById("message").addEventListener("keydown", (e) => {
if (e.key === "Enter") {
sendMsg();
}
});
const updateUsername = data => document.getElementById("current-username").textContent = data;
function fetchHistory() {
fetch("/history")
.then(res => res.json())
.then(res => {
res.data.reverse().forEach(element => {
writeLogUser(element.message, element.sender)
});
})
.catch(err => {
console.log(err)
})
}
function fetchOnline() {
fetch("/online").then(res => res.json()).then(res => {
const c = document.getElementById('online');
// console.log(res)
document.getElementById('online-number').textContent = res.online;
c.innerHTML = "";
res.user.forEach(element => {
const u = document.createElement('span');
u.textContent = element;
c.append(u);
});
});
}
const onlineChecker = setInterval(() => {
fetchOnline();
}, 1000)
function writeLogSystem(msg, status = true, topic = "system") {
const c = document.getElementById('sysmsg');
const m = document.createElement('span');
m.style.color = status ? 'green' : 'red'
m.textContent = `[${new Date().toLocaleDateString()} ${new Date().toLocaleTimeString()}][${topic}] ${msg}`
c.append(m);
}
function writeLogUser(msg, sender) {
const c = document.getElementById('usrmsg');
const m = document.createElement('span');
m.textContent = `[${new Date().toLocaleDateString()} ${new Date().toLocaleTimeString()}][${sender}] ${msg}`
c.append(m);
}
document.getElementById("username").addEventListener("keydown", (e) => {
if (e.key === "Enter") {
register();
}
});
</script>
</body> </body>
</html> </html>

145
static/script/main.js Normal file
View File

@ -0,0 +1,145 @@
window.onload = () => {
fetchHistory()
updateLoginState(false)
}
const socket = io("ws://localhost:5691");
socket.on("connect", () => {
console.log("Connected");
updateState(true)
writeLogSystem("已连接到服务器", true, "client")
});
socket.on("system", data => {
console.log("system", data)
data = JSON.parse(data)
writeLogSystem(data.msg, data.status, "system")
})
socket.on("username", data => {
updateUsername(data);
})
socket.on("notice", data => {
console.log("notice", data)
data = JSON.parse(data)
writeLogSystem(data.msg, data.status, "notice")
})
socket.on("msg", data => {
console.log("user", data)
data = JSON.parse(data);
let msg = ` ${data.msg}`
writeLogUser(msg, data.sender)
})
socket.on("disconnect", () => {
updateState(false);
console.log("Disconnected")
writeLogSystem("已断开服务器连接", false, "client")
updateLoginState(false)
});
const connect = () => socket.connect()
const disconnect = () => socket.disconnect();
const register = () => socket.emit("system", JSON.stringify({ opt: "signin", args: { username: document.getElementById("username").value } }));
const sendMsg = () => {
socket.emit("msg", document.getElementById("message").value)
document.getElementById("message").value = ""
}
const updateState = (value) => {
const statEl = document.getElementById('stat');
const statText = document.getElementById('stat-text');
if (value) {
statEl.className = 'status-badge connected';
statText.textContent = 'connected';
} else {
statEl.className = 'status-badge disconnected';
statText.textContent = 'disconnected';
}
}
document.getElementById("message").addEventListener("keydown", (e) => {
if (e.key === "Enter") {
sendMsg();
}
});
const updateUsername = data => {
document.getElementById("current-username").textContent = data;
updateLoginState(true, data);
}
const updateLoginState = (isLoggedIn, username = '') => {
const usernameEl = document.getElementById("current-username");
const loginPrompt = document.getElementById("login-prompt");
if (isLoggedIn) {
usernameEl.textContent = username || '已登录';
usernameEl.style.display = 'block';
if (loginPrompt) loginPrompt.style.display = 'none';
} else {
usernameEl.textContent = '未登录';
if (loginPrompt) loginPrompt.style.display = 'block';
}
}
const showLogin = () => {
const username = prompt("请输入用户名:");
if (username && username.trim()) {
socket.emit("system", JSON.stringify({ opt: "signin", args: { username: username.trim() } }));
}
}
function fetchHistory() {
fetch("/history")
.then(res => res.json())
.then(res => {
res.data.reverse().forEach(element => {
writeLogUser(element.message, element.sender)
});
})
.catch(err => {
console.log(err)
})
}
function fetchOnline() {
fetch("/online").then(res => res.json()).then(res => {
const c = document.getElementById('online');
document.getElementById('online-number').textContent = res.online;
c.innerHTML = "";
res.user.forEach(element => {
const u = document.createElement('span');
u.textContent = element;
c.append(u);
});
});
}
const onlineChecker = setInterval(() => {
fetchOnline();
}, 1000)
function writeLogSystem(msg, status = true, topic = "system") {
const c = document.getElementById('sysmsg');
const m = document.createElement('span');
m.style.color = status ? 'var(--success-color)' : 'var(--danger-color)'
m.textContent = `[${new Date().toLocaleTimeString()}][${topic}] ${msg}`
c.appendChild(m);
c.scrollTop = c.scrollHeight;
}
function writeLogUser(msg, sender) {
const c = document.getElementById('usrmsg');
const m = document.createElement('span');
m.textContent = `[${new Date().toLocaleTimeString()}][${sender}] ${msg}`
c.appendChild(m);
c.scrollTop = c.scrollHeight;
}
document.getElementById("username")?.addEventListener("keydown", (e) => {
if (e.key === "Enter") {
register();
}
});

251
static/style/style.css Normal file
View File

@ -0,0 +1,251 @@
:root {
--primary-color: #3b82f6;
--primary-hover: #2563eb;
--bg-color: #f8fafc;
--card-bg: #ffffff;
--border-color: #e2e8f0;
--text-primary: #1e293b;
--text-secondary: #64748b;
--success-color: #22c55e;
--danger-color: #ef4444;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'HarmonyOS Sans SC Medium', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background-color: var(--bg-color);
color: var(--text-primary);
line-height: 1.6;
padding: 20px;
}
.title {
font-size: 18px;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 12px;
}
.card {
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 20px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
background-color: var(--card-bg);
transition: box-shadow 0.2s ease;
}
.card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
#container {
display: flex;
flex-direction: row;
gap: 20px;
max-width: 1400px;
margin: 0 auto;
}
#msg-list {
flex: 1;
display: flex;
flex-direction: column;
gap: 16px;
min-width: 0;
}
#online-list {
display: flex;
flex-direction: column;
gap: 16px;
width: 320px;
flex-shrink: 0;
}
#sysmsg {
display: flex;
flex-direction: column;
gap: 8px;
max-height: 200px;
overflow-y: auto;
padding-right: 8px;
}
#sysmsg::-webkit-scrollbar {
width: 4px;
}
#sysmsg::-webkit-scrollbar-thumb {
background-color: var(--border-color);
border-radius: 4px;
}
#usrmsg {
display: flex;
flex-direction: column;
gap: 8px;
flex: 1;
overflow-y: auto;
max-height: calc(100vh - 280px);
padding-right: 8px;
}
#usrmsg::-webkit-scrollbar {
width: 4px;
}
#usrmsg::-webkit-scrollbar-thumb {
background-color: var(--border-color);
border-radius: 4px;
}
#usrmsg > span {
padding: 10px 14px;
background-color: var(--bg-color);
border-radius: 8px;
font-size: 14px;
word-break: break-word;
}
#online {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
padding-top: 12px;
}
#online > span {
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 8px 12px;
text-align: center;
font-size: 13px;
background-color: var(--bg-color);
transition: all 0.2s ease;
}
#online > span:hover {
border-color: var(--primary-color);
color: var(--primary-color);
}
input[type="text"] {
padding: 10px 14px;
border: 1px solid var(--border-color);
border-radius: 8px;
font-size: 14px;
width: 100%;
transition: border-color 0.2s ease, box-shadow 0.2s ease;
outline: none;
}
input[type="text"]:focus {
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
button {
padding: 10px 20px;
background-color: var(--primary-color);
color: white;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
button:hover {
background-color: var(--primary-hover);
transform: translateY(-1px);
}
button:active {
transform: translateY(0);
}
.input-group {
display: flex;
gap: 8px;
align-items: center;
}
.input-group input {
flex: 1;
}
.input-group button {
flex-shrink: 0;
}
.status-badge {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 10px;
border-radius: 20px;
font-size: 12px;
font-weight: 500;
}
.status-badge.connected {
background-color: rgba(34, 197, 94, 0.1);
color: var(--success-color);
}
.status-badge.disconnected {
background-color: rgba(239, 68, 68, 0.1);
color: var(--danger-color);
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background-color: currentColor;
}
.username-display {
font-size: 16px;
font-weight: 600;
color: var(--primary-color);
padding: 12px 16px;
background-color: rgba(59, 130, 246, 0.05);
border-radius: 8px;
text-align: center;
}
.login-prompt {
text-align: center;
padding: 20px;
}
.login-prompt p {
color: var(--text-secondary);
margin-bottom: 12px;
font-size: 14px;
}
@media screen and (max-width: 900px) {
#container {
flex-direction: column-reverse;
}
#online-list {
width: 100%;
flex-direction: row;
flex-wrap: wrap;
}
#online-list .card {
flex: 1;
min-width: 280px;
}
}

15
utils/auth.js Normal file
View File

@ -0,0 +1,15 @@
import axios from "axios"
var apiUrl = "https://www.cloudy233.top:6699/bearer";
class Resp {
constructor(success, message, data = null) {
this.success = success
this.message = message
this.data = data
}
}
export function verifyToken(token) {
return axios.get(apiUrl, {headers: {Authorization: "Bearer " + token}})
}