Node.js单点登录SSO详解:Session、JWT、CORS让登录更简单(一):https://developer.aliyun.com/article/1628445
三、SSO实现方案
1、安装依赖
启动服务:express
操作cookie:express-session
生成token:jsonwebtoken
解决跨域:cors
npm install express npm install express-session npm install jsonwebtoken npm install cors
2、结构
vueA项目:使用vite创建项目
vueB项目:使用vite创建项目
nodejs端:server/index.js
登录页面:login.html
3、实现原理
- 用户首次访问系统A或B时,需要进行登录。
- 系统统A或B带着appId信息重定向登录页面。
- 认证系统验证用户登录信息。
- 验证通过后,设置session值,用户返回一个token。
- 认证系统带着token重定向给系统A或B,得知用户是已登录状态。
- 系统A或B正常进入系统。
- 用户再访问另一个系统时。
- 通过session值,得知用户是已登录状态。
- 认证系统带着token重定向给系统。
- 系统正常进入系统。
四、示例代码
1、nodejs端 server/index.js
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') })
2、vueA项目app.vue
<script setup lang="ts"> 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> <template> <div>这里是appA</div> </template> <style scoped> </style>
3、vueB项目app.vue
<script setup> const token = location.search.split('token=')[1] if (!token) { fetch('http://localhost:3000/login?appId=DDkq0YYh').then(res => { location.href = res.url }) } else { localStorage.setItem('token', token) } </script> <template> <div>这里是appB</div> </template> <style scoped> </style>
4、登录页面login.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>登录</title> </head> <body> <div> <h1>登录页面</h1> <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> </div> <script> const appId = location.search.split('?')[1].split('=')[1] document.querySelector('input[name="appId"]').value = appId </script> </body> </html>