导航
持久登录允许用户即使在关闭浏览器后重新访问应用时也保持登录状态。为实现这一功能,我们将使用 Token (例如 JWT) 存储会话信息,并通过 HTTP-Only Cookie 保护 Token,以防止客户端 JavaScript 访问。

JWT,将 JWT 存储在 HTTP-Only Cookie 中,以便浏览器自动在后续请求中发送。Cookie 持久化 Token,自动在后续请求中验证用户是否已登录。JWT 中找到用户会话信息,并更新应用状态。<template>
<div>
<h2>Login</h2>
<form @submit.prevent="login">
<input v-model="username" placeholder="Username" required />
<input v-model="password" placeholder="Password" type="password" required />
<button type="submit">Login</button>
</form>
<p v-if="error">{{ error }}</p>
</div>
</template>
<script>
import axios from 'axios';
export default {
data() {
return {
username: '',
password: '',
error: null,
};
},
methods: {
async login() {
try {
const res = await axios.post('/api/login', {
username: this.username,
password: this.password,
}, { withCredentials: true });
if (res.data.success) {
this.$router.push('/home');
}
} catch (err) {
this.error = 'Login failed. Please check your credentials.';
}
},
},
};
</script>
const express = require('express');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
const router = express.Router();
const SECRET_KEY = 'your_jwt_secret_key'; // 换成更安全的密钥
const COOKIE_NAME = 'auth_token';
router.post('/login', async (req, res) => {
const { username, password } = req.body;
// 假设此用户从数据库查询
const user = { id: 1, username: 'test', passwordHash: await bcrypt.hash('password', 10) };
if (user && await bcrypt.compare(password, user.passwordHash)) {
const token = jwt.sign({ id: user.id, username: user.username }, SECRET_KEY, { expiresIn: '7d' });
res.cookie(COOKIE_NAME, token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
});
return res.json({ success: true, message: 'Login successful' });
}
return res.status(401).json({ success: false, message: 'Invalid credentials' });
});
module.exports = router;
const jwt = require('jsonwebtoken');
const SECRET_KEY = 'your_jwt_secret_key';
const COOKIE_NAME = 'auth_token';
module.exports = (req, res, next) => {
const token = req.cookies[COOKIE_NAME];
if (token) {
try {
const user = jwt.verify(token, SECRET_KEY);
req.user = user;
next();
} catch (err) {
res.clearCookie(COOKIE_NAME);
return res.status(401).json({ success: false, message: 'Token expired' });
}
} else {
return res.status(401).json({ success: false, message: 'Unauthorized' });
}
};
在 Express.js 中使用此中间件保护路由:
const express = require('express');
const authMiddleware = require('./authMiddleware');
const app = express();
app.use('/api/protected', authMiddleware, (req, res) => {
res.json({ success: true, message: `Hello ${req.user.username}, you are authorized.` });
});
在不同源的一个系统下,任意一个站点一个程序做了登录操作,其他站点或者程序同样处于登录状态的这种机制叫做单点登录机制。

SSO 一般都需要一个独立的认证中心(passport),子系统的登录均得通过 passport,子系统本身将不参与登录操作,当一个系统成功登录以后,passport 将会颁发一个令牌给各个子系统,子系统可以拿着令牌会获取各自的受保护资源,为了减少频繁认证,各个子系统在被 passport 授权以后,会建立一个局部会话,在一定时间内可以无需再次向 passport 发起认证。
具体流程是:
单设备登录和单点登录完全不同,是两个独立维度。
| 维度 | 关心的问题 | 术语 |
|---|---|---|
| 维度 A:跨应用 | 用户在 A 应用登录后,B 应用要不要也算登录? | SSO(单点登录) / 应用各自独立登录 |
| 维度 B:同账号会话上限 | 同一个账号最多允许几个"活跃会话"?多出来的怎么办? | 单设备登录 / 多设备登录 / 互踢 / 多端共存 |
常见策略如下:
一个账号同一时间只能登录一台设备,而且后登录优先。即单设备互踢
步骤流程:
1. 用户在新设备登录成功后,读取该账号当前所有活跃会话。
3. 只要存在旧会话,就全部删除。
4. 创建当前这次的新 session,并签发新的 JWT。
6. 旧设备下一次带旧 token 请求时,因为已经不在会话表里,会收到 `401`。
一个账号同一时间只能有一处登录,而且先登录优先。即单设备独占
步骤流程:
1. 用户登录成功后,先读取该账号当前所有活跃会话。
3. 先找有没有同一 `deviceId` 的旧会话。
4. 如果有同设备旧会话,说明这是同一设备重新登录,会先删掉旧会话,再允许当前登录通过。
5. 如果没有同设备旧会话,但账号下已经有别的活跃会话,直接返回 '该账号已在其他设备登录,请先退出后再试。',不会创建新 session。
7. 只有账号当前没有活跃会话,或者属于同设备刷新登录,这次登录才会被放行。
除了 deviceId,还要再加两个配置:maxDevices 和超限后的处理方式 mode。
mode:
步骤流程:
1. 用户登录成功后,读取该账号当前所有活跃会话。
3. 如果发现同一 `deviceId` 已经登录过,直接删除旧会话并放行。这种情况视为同设备刷新,不占新名额。
4. 如果不是同设备刷新,再判断当前活跃会话数是否已经达到 `maxDevices`。
5. 如果还没达到上限,直接放行。
6. 如果已经达到上限且 `mode = 'reject'`,就拒绝本次登录。
7. 如果已经达到上限且 `mode = 'kick-oldest'`,就从最早创建的会话开始踢,直到腾出 1 个位置给当前新设备。
8. 返回放行后,创建当前会话并签发 token。