单点登录(Single Sign On),简称为 SSO,是比较流行的企业业务整合的解决方案之一。SSO的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。

如图所示,图中有4个系统,分别是Application1、Application2、Application3、和SSO。Application1、Application2、Application3没有登录模块,而SSO只有登录模块,没有其他的业务模块,当Application1、Application2、Application3需要登录时,将跳到SSO系统,SSO系统完成登录,其他的应用系统也就随之登录了。这完全符合我们对单点登录(SSO)的定义。

Express 是一个保持最小规模的灵活的 Node.js Web 应用程序开发框架,为 Web 和移动应用程序提供一组强大的功能。
npm install express
const express = require('express')
const express = require('express')
const app = express()
const port = 3000
app.get('/', (req, res) => {
res.send('Hello World!')
})
app.post('/', (req, res) => {
res.send('Got a POST request')
})
app.listen(port, () => {
console.log(`Example app listening on port ${port}`)
})
cors 是 Express 的一个第三方中间件。通过安装和配置 cors 中间件,可以很方便地解决跨域问题。
CORS (Cross-Origin Resource Sharing,跨域资源共享)由一系列 HTTP 响应头组成,这些 HTTP 响应头决定浏览器是否阻止前端 JS 代码跨域获取资源。
npm install cors
const cors = require('cors')
app.use(cors())
app.use(cors({ origin: 'http://127.0.0.1:5500' }))
app.get('/data', cors(), (req, res) => {
res.json({
name: 'cors in node.js',
language: 'JavaScript',
server: 'Express.js',
})
})
const options = {
origin: 'http://127.0.0.1:5500',
methods: 'GET, PUT',
}
app.use(cors(options))
const options = {
origin: dynamicConfiguration(),
methods: 'GET, PUT',
}
const dynamicConfiguration = async (req) => {
const db = getDB() // simulating database object
let origin = await db.getOrigin(req.headers) //simulating fetching origin from DB based on the headers
return origin
}
app.use(cors(options))
express-session中间件将会话数据存储在服务器上;它仅将会话标识(而非会话数据)保存在 cookie 中。从1.5.0版本开始, express-session不再依赖cookie-parser,直接通过req/res读取/写入;默认存储位置内存存储(服务器端)
npm install express-session
const session = require('express-session')
app.use(session({
secret: 'YOUR_SESSION_SECRET',//加密字符串。 使用该字符串来加密session数据,自定义
resave: false,//强制保存session即使它并没有变化
saveUninitialized: true,//强制将未初始化的session存储。当新建了一个session且未设定属性或值时,它就处于未初始化状态。
cookie: {
maxAge: 30 * 60 * 1000
}
}))
//设置
req.session.userName = userName;
//获取
const userName = req.session.userName
JSON Web Token(JWT)是一种用于在web上传递信息的标准,它以JSON格式表示信息,通常用于身份验证和授权。
JWT由三个部分组成:Header(头部)、Payload(负载)和Signature(签名)。它们用点号分隔开,形成了一个JWT令牌。
在现代web应用中,用户身份认证是非常重要且必不可少的一环。而使用Node.js和Express框架,可以方便地实现用户身份认证。而在这个过程中,jsonwebtoken这个基于JWT协议的模块可以帮助我们实现安全且可靠的身份认证机制,可以让我们轻松地生成、解析和验证JWT。
npm install jsonwebtoken
const jwt = require('jsonwebtoken')
// 生成token
const token = jwt.sign({
id: 'appId',
name: 'zhangsan',
secret: 'YOUR_SECRET_KEY'
}, '123456', {
expiresIn: '2h'
})
//验证token
jwt.verify(token, 'shhhhh', (err, decoded) => {
if (err) {
console.error('无效的令牌');
} else {
// 使用解码后的令牌数据
console.log(decoded);
}
});
| 对比因素 | JWT | Session |
|---|---|---|
| 存储 | 存储在客户端,不需要服务器保持会话状态。 | 存储在服务器,需要服务器维护会话信息。 |
| 安全性 | 加密较严密,但如果token被窃取,攻击者可以任意使用。 | 如果sessionID被窃取,攻击者可以冒充用户登陆。 |
| 性能 | 在每次请求时需要验证和解码token,性能较差。 | 只需查找sessionID就能获取会话信息,性能较好。 |
| 扩展性 | 在多服务器或者跨域环境中更易扩展。 | 在多服务器环境中需要同步session,扩展性较差。 |
| 数据大小 | JWT的大小比sessionID大,因此需要更多的带宽。 | sessionID大小稳定,对带宽需求较小。 |
| 到期时间 | 可以为每个token设置不同的过期时间。 | 所有session的过期时间通常相同。 |
| 客户端存储位置 | 可以存储在Cookie, LocalStorage, SessionStorage中 | 存储在Cookie中。 |
| 跨域问题 | 无跨域问题,且对于移动应用而言友好。 | 跨域问题复杂,需要服务器支持CORS。 |
| 状态 | 无状态,服务器不需要保存用户信息。 | 有状态,服务器需要保存用户信息。 |
| 使用场景 | 用于认证和信息交换,尤其适合单页应用(SPA)和前后端分离的项目。 | 主要用于记录用户状态,适配传统的后端渲染的Web服务。 |
启动服务:express
操作cookie:express-session
生成token:jsonwebtoken
解决跨域:cors
npm install express
npm install express-session
npm install jsonwebtoken
npm install cors
vueA项目:使用vite创建项目
vueB项目:使用vite创建项目
nodejs端:server/index.js
登录页面:login.html

import express from "express"
import session from 'express-session'
import fs from "node:fs"
import cors from "cors"
import jwt from 'jsonwebtoken'
// 应用列表
const appToMapUrl = {
'fd8xIoDC': {
url: 'http://localhost:5173',
name: 'appA',
secret: '123456',
token: ''
},
'DDkq0YYh': {
url: 'http://localhost:5174',
name: 'appB',
secret: '789102',
token: ''
}
}
// 创建服务器
const app = express()
// 解析post请求体
app.use(express.json())
// 跨域
app.use(cors())
// 创建session配置项,注册为express-session中间件
app.use(session({
secret: '123456',//加密字符串。 使用该字符串来加密session数据,自定义
resave: false,//强制保存session即使它并没有变化
saveUninitialized: true,//强制将未初始化的session存储。当新建了一个session且未设定属性或值时,它就处于未初始化状态。
cookie: {
maxAge: 30 * 60 * 1000
}
}))
//获取token
const getToken = (appId) => {
const appInfo = appToMapUrl[appId]
if (!appInfo) {
return null;
}
// 生成token
const token = jwt.sign({
id: appId,
name: appInfo.name,
secret: appInfo.secret
}, '123456', {
expiresIn: 60 * 60
})
return token;
}
//是否登录
app.get('/login', (req, res) => {
const {appId} = req.query
if (!appId) {
return res.send('请输入appId')
}
// 判断是否登录
if (req.session.userName) {
let token
if (appToMapUrl[appId].token) {
// 获取token
token = appToMapUrl[appId].token
} else {
// 生成token
token = getToken(appId)
// 存入appToMapUrl
appToMapUrl[appId].token = token
}
// 跳转
res.redirect(`${appToMapUrl[appId].url}?token=${token}`)
return;
} else {
// 读取登录页面
const html = fs.readFileSync('./login.html', 'utf-8')
res.send(html)
}
})
// 解析表单数据
app.use(express.urlencoded({ extended: true }));
// 登录
app.post('/protected', (req, res) => {
const {username,password,appId} = req.body
if (username === 'admin' && password === '123456') {
const token = getToken(appId);
// 存入appToMapUrl
appToMapUrl[appId].token = token;
//存入session,证明已经登录
req.session.userName = username;
res.redirect(`${appToMapUrl[appId].url}?token=${token}`)
} else {
res.send('用户名或密码错误')
}
})
// 监听端口
app.listen(3000, () => {
console.log('http://localhost:3000')
})
这里是appA
这里是appB
登录
登录页面