单点登录(SSO)适合多套业务系统共用一套认证入口的场景。原文示例把登录能力集中在Node.js认证服务:业务系统A和B本身不保存账号体系,访问时携带appId跳到认证服务;认证服务用Session记录登录状态,用JWT生成业务系统令牌,再带token重定向回业务系统,前端把token写入localStorage。这样用户一次登录后可访问相互信任的应用。原文将其好处归纳为方便用户一次登录、管理员维护统一账号、新系统直接复用认证服务,减少重复开发用户认证程序。
认证服务依赖Express、cors、express-session和jsonwebtoken,安装命令如下:- npm install express
- npm install express-session
- npm install jsonwebtoken
- npm install cors
复制代码 原文项目结构为:server/index.js负责认证服务,login.html为登录页,vueA和vueB由Vite创建,分别运行在5173和5174,认证服务监听3000端口。这个结构把业务应用和认证中心分开,appId作为业务系统标识。
服务端核心代码按原文逻辑整理如下:- 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()
- app.use(express.json())
- app.use(cors())
- app.use(session({
- secret: '123456',
- resave: false,
- saveUninitialized: true,
- cookie: { maxAge: 30 * 60 * 1000 }
- }))
- const getToken = (appId) => {
- const appInfo = appToMapUrl[appId]
- if (!appInfo) return null
- return jwt.sign({ id: appId, name: appInfo.name, secret: appInfo.secret }, '123456', { expiresIn: 60 * 60 })
- }
- app.get('/login', (req, res) => {
- const { appId } = req.query
- if (!appId) return res.send('请输入appId')
- if (req.session.userName) {
- let token = appToMapUrl[appId].token
- if (!token) {
- token = getToken(appId)
- appToMapUrl[appId].token = token
- }
- res.redirect(`${appToMapUrl[appId].url}?token=${token}`)
- return
- }
- 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[appId].token = token
- req.session.userName = username
- res.redirect(`${appToMapUrl[appId].url}?token=${token}`)
- } else {
- res.send('用户名或密码错误')
- }
- })
- app.listen(3000, () => console.log('http://localhost:3000'))
复制代码
这段代码的关键点:appToMapUrl保存appId、业务系统地址、名称、secret和token;express.json解析JSON请求体,cors启用跨域,express-session的cookie maxAge为30分钟,secret为示例值123456;getToken按appId查配置,用jwt.sign生成token,payload包含id、name、secret,expiresIn为60*60秒;GET /login检查appId,有session.userName时复用或生成token并重定向到业务系统url?token=...,无session时读取login.html返回;POST /protected接收username、password、appId,原文校验admin/123456,通过后生成token、写入appToMapUrl[appId].token、设置req.session.userName并重定向,失败返回用户名或密码错误。
业务系统接入时,原文Vue A在script setup中从location.search取token;没有token就fetch认证服务/login?appId=fd8xIoDC,并把响应URL赋给location.href;有token则写入localStorage。Vue B逻辑相同,只把appId换成DDkq0YYh。示例片段如下:- <script setup>
- const token = location.search.split('token=')[1]
- if (!token) {
- fetch('http://localhost:3000/login?appId=fd8xIoDC').then(res => {
- location.href = res.url
- })
- } else {
- localStorage.setItem('token', token)
- }
- </script>
复制代码 登录页通过表单POST到/protected,隐藏域appId由页面脚本从URL查询串取出并填充:- <form action='/protected' method='post'>
- <input type='text' name='username'>
- <input type='password' name='password'>
- <input type='hidden' name='appId'>
- <input type='submit' value='登录'>
- </form>
- <script>
- const appId = location.search.split('?')[1].split('=')[1]
- document.querySelector('input[name=appId]').value = appId
- </script>
复制代码
从访问链路看,用户首次访问业务系统A或B,因本地无token而请求认证服务;认证服务没有session时返回登录页;用户提交账号密码后,认证服务校验并把用户标识写入Session,同时发放JWT;浏览器重定向回业务系统并携带token,业务系统保存token后进入。用户再访问另一个系统时,业务系统再次请求认证服务;认证服务发现Session中已有userName,就直接复用或生成对应appId的token并重定向,实现一次登录多处使用。原文的Session负责认证中心会话状态,JWT负责业务系统间的令牌传递,CORS负责前后端分离和不同端口之间的跨域请求。
原文还对比了JWT与Session:JWT存储在客户端,服务器不保持会话状态,扩展性好,适合跨域和前后端分离;Token每次请求需要验证和解码,且体积通常大于sessionID;Session存储在服务器,只需查找sessionID,性能较好,但多服务器需要同步会话,跨域配置更复杂。到期时间上JWT可给每个token设置不同过期时间,Session通常统一;存储位置JWT可放Cookie、LocalStorage、SessionStorage,Session通常依赖Cookie。按原文方案,认证中心用Session维持登录态,业务系统拿到JWT,兼顾集中认证和业务侧无状态校验。
调试时可按原文返回值定位:GET /login缺少appId会返回请输入appId;未登录会读取login.html;登录失败返回用户名或密码错误;登录成功发生302重定向,业务系统URL会带token参数。需要注意几个兼容性和安全问题:原文/login在appToMapUrl[appId]未命中时仍访问其token字段,实际应补充appId合法性校验;示例中appToMapUrl把token缓存在内存,服务重启后缓存丢失,生产环境应使用可靠存储;Session secret和JWT密钥在示例中为123456,账号密码也是明文示例值,不能直接用于生产;cors()默认放行所有来源,部署时应按业务域名收窄;原文jsonwebtoken中间件介绍中生成token使用密钥123456,而验证示例使用shhhhh,实际生成与验证必须使用同一密钥,否则jwt.verify会返回err;Session的maxAge是30分钟,JWT的expiresIn是3600秒,两者过期策略要按业务统一。掌握这些参数和调用关系后,即可按原文思路搭建Node.js SSO示例并排查登录回跳、跨域和令牌校验问题。 |