导航


HTML

CSS

JavaScript

浏览器 & 网络

版本管理

框架

构建工具

TypeScript

性能优化

算法

UI、组件库

Node

业务技能

针对性攻坚

公共类

基础设施

AI


持久登录

概述

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

Untitled

实现步骤

  1. 用户登录
    1. 前端提交用户的登录信息(用户名和密码)到后端。
    2. 后端验证凭据成功后生成 JWT,将 JWT 存储在 HTTP-Only Cookie 中,以便浏览器自动在后续请求中发送。
  2. Token 持久化与验证
    1. 通过 HTTP-Only Cookie 持久化 Token,自动在后续请求中验证用户是否已登录。
    2. 在用户重新访问或刷新页面时,前端会在后端自动返回的 JWT 中找到用户会话信息,并更新应用状态。

示例代码

前端:Vue3 登录组件(Login.vue)

<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>

后端:Express 路由与登录处理(auth.js)

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;

验证用户会话的中间件(authMiddleware.js)

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.` });
});

🔥 单点登录

在不同源的一个系统下,任意一个站点一个程序做了登录操作,其他站点或者程序同样处于登录状态的这种机制叫做单点登录机制。

Untitled

SSO 一般都需要一个独立的认证中心(passport),子系统的登录均得通过 passport,子系统本身将不参与登录操作,当一个系统成功登录以后,passport 将会颁发一个令牌给各个子系统,子系统可以拿着令牌会获取各自的受保护资源,为了减少频繁认证,各个子系统在被 passport 授权以后,会建立一个局部会话,在一定时间内可以无需再次向 passport 发起认证。

具体流程是:

  1. 用户访问系统 1 的受保护资源,系统 1 发现用户未登录,跳转至 sso 认证中心,并将自己的地址作为参数
  2. sso 认证中心发现用户未登录,将用户引导至登录页面
  3. 用户输入用户名密码提交登录申请
  4. sso 认证中心校验用户信息,创建用户与 sso 认证中心之间的会话,称为全局会话,同时创建授权令牌
  5. sso 认证中心带着令牌跳转回最初的请求地址(系统 1)
  6. 系统 1 拿到令牌,去 sso 认证中心校验令牌是否有效
  7. sso 认证中心校验令牌,返回有效,注册系统 1
  8. 系统 1 使用该令牌创建与用户的会话,称为局部会话,返回受保护资源
  9. 用户访问系统 2 的受保护资源
  10. 系统 2 发现用户未登录,跳转至 sso 认证中心,并将自己的地址作为参数
  11. sso 认证中心发现用户已登录,跳转回系统 2 的地址,并附上第一次登录时创建的授权令牌(一般拼接在url)
  12. 系统 2 拿到令牌,去 sso 认证中心校验令牌是否有效(通过url获取token并发送给后端校验)
  13. sso 认证中心校验令牌,返回有效,注册系统 2
  14. 系统 2 使用该令牌创建与用户的局部会话,返回受保护资源

🔥 单设备登录

单设备登录和单点登录完全不同,是两个独立维度。

维度 关心的问题 术语
维度 A:跨应用 用户在 A 应用登录后,B 应用要不要也算登录? SSO(单点登录) / 应用各自独立登录
维度 B:同账号会话上限 同一个账号最多允许几个"活跃会话"?多出来的怎么办? 单设备登录 / 多设备登录 / 互踢 / 多端共存

常见策略如下:

单设备登录,后登录顶掉前登录

一个账号同一时间只能登录一台设备,而且后登录优先。即单设备互踢

步骤流程:

1. 用户在新设备登录成功后,读取该账号当前所有活跃会话。
3. 只要存在旧会话,就全部删除。
4. 创建当前这次的新 session,并签发新的 JWT。
6. 旧设备下一次带旧 token 请求时,因为已经不在会话表里,会收到 `401`。

单设备登录,先登录占坑,后登录拒绝

一个账号同一时间只能有一处登录,而且先登录优先。即单设备独占

步骤流程:

1. 用户登录成功后,先读取该账号当前所有活跃会话。
3. 先找有没有同一 `deviceId` 的旧会话。
4. 如果有同设备旧会话,说明这是同一设备重新登录,会先删掉旧会话,再允许当前登录通过。
5. 如果没有同设备旧会话,但账号下已经有别的活跃会话,直接返回 '该账号已在其他设备登录,请先退出后再试。',不会创建新 session。
7. 只有账号当前没有活跃会话,或者属于同设备刷新登录,这次登录才会被放行。

从单设备推广到 N 设备

除了 deviceId,还要再加两个配置:maxDevices 和超限后的处理方式 mode

mode:

步骤流程:

1. 用户登录成功后,读取该账号当前所有活跃会话。
3. 如果发现同一 `deviceId` 已经登录过,直接删除旧会话并放行。这种情况视为同设备刷新,不占新名额。
4. 如果不是同设备刷新,再判断当前活跃会话数是否已经达到 `maxDevices`。
5. 如果还没达到上限,直接放行。
6. 如果已经达到上限且 `mode = 'reject'`,就拒绝本次登录。
7. 如果已经达到上限且 `mode = 'kick-oldest'`,就从最早创建的会话开始踢,直到腾出 1 个位置给当前新设备。
8. 返回放行后,创建当前会话并签发 token。