单点登录(Single Sign-On,简称SSO)是一种身份认证和访问控制的机制,允许用户使用一组凭据(如用户名和密码)登录后,其它多个系统项目可直接访问。
单点登录的优点:
- 用户只需登录一次,即可访问多个应用程序,提供了更好的用户体验和便利性。
- 通过集中的身份验证,可以减少密码泄露和密码管理问题。此外,SSO还可以与其他身份验证机制(如多因素身份验证)结合使用,提供更强的安全性。
- SSO可以减少管理员的工作量,因为他们不需要为每个应用程序单独管理用户凭据和权限。
完整代码已上传github:https://github.com/benxiaohaihuiwan/SingleSignOn
需要安装的插件
js
npm i express 启动服务编写接口
npm i express-session 操作cookie
npm i jsonwebtoken 生成token
npm i cors 解决跨域
需要建立A项目,B项目,server项目,来验证单点登录效果。A,B项目可以直接使用 npm init vite 建立项目,这里我建立的都是vue项目。根目录下建sso.html,A,B项目统一用的登录页面
- 首先说server项目,建一个index.js文件,顺便安装上述说的四个插件。
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 = {
// A 项目的 appId
as6s2ipA: {
url: "http://localhost:5188",
name: "vue",
secretKey: "%Y&*VGHJKLsjkas",
token: "",
},
// B 项目的 appId
bs789ipB: {
url: "http://localhost:5189",
secretKey: "%Y&*FRTYGUHJIOKL",
name: "react",
token: "",
},
};
// 搭建服务
const app = express();
// 解析客户端发送的数据
app.use(express.json());
// 解决跨域
app.use(cors());
// 生成一个 cookie
app.use(
session({
secret: "$%^&*()_+DFGHJKL",
cookie: {
maxAge: 1000 * 60 * 60 * 24 * 7, //过期时间
},
})
);
// 生成token
const getToken = (appId) => {
/**
* 1 第一个参数就是荷载,存我们的信息
* 2 第二个参数是一个密钥,记录在服务器中,在验证时需要用到此参数
* 3 第三个参数通常是从redis取,设置过期时间,在这里先不设置
*/
return jwt.sign({ appId }, appToMapUrl[appId].secretKey);
};
/**
* 一进到页面,就调用登录接口
* 1 登录过,就返回一个token
* 2 没登录过,则跳转到登录页面
*/
app.get("/login", (req, res) => {
const appId = req.query.appId;
const url = appToMapUrl[appId].url
if (req.session.username) {
// 若是有值,证明登录过
let token;
if (appToMapUrl[appId].token) {
// 第一个项目访问
token = appToMapUrl[appId].token;
} else {
// 后面项目访问
token = getToken(appId);
appToMapUrl[appId].token = token;
}
// 如果 登录过 则 重定向
res.redirect(`${url}?token=${token}`);
return;
}
const html = fs.readFileSync("../sso.html", "utf-8");
res.send(html);
});
// 登录成功接口
app.get("/loginSuccess", (req, res) => {
const { username, password, appId } = req.query;
// 实际情况下,需要在判断下账号密码是否对应。
// 生成响应的token
const token = getToken(appId);
appToMapUrl[appId].token = token; // 存一份token值
req.session.username = username; // 存一个标识证明登录过
const url = appToMapUrl[appId].url; // 获取 url
// 登录后,重定向页面
res.redirect(`${url}?token=${token}`);
// console.log(username, password, appId);
res.send("ok");
});
// 服务
app.listen(3000, () => {
console.log("启动一个3000的服务");
});
- sso.html 登录页面,进行登录
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
登录页面
<form action="/loginSuccess" method="get">
<label for="username">
账号:<input name="username" id="username" type="text" />
</label>
<label for="password"
>密码:<input name="password" id="password" type="password"
/></label>
<!-- 隐藏appId 作为参数传递 登录的时候 -->
<label for="appId"
><input name="appId" value="" id="appId" type="hidden"
/></label>
<button type="submit" id="button">登录</button>
</form>
<script>
// 获取 appId 的参数,提交表单时候要传递
const appId = location.search.split("=")[1];
document.getElementById("appId").value = appId;
</script>
</body>
</html>
- A项目 App.vue
html
<script setup lang="ts">
// 获取token值
const token = location.search.split("=")[1];
// 若是没有 token,则才调用登录接口
if (!token) {
// 调用登录接口,跳转页面 传递appId 让用户知道是从那个项目上跳转, 数值需要与appToMapUrl对应
fetch("http://localhost:3000/login?appId=as6s2ipA").then((res) => {
location.href = res.url;
});
}
// else {
// // 存储下 token
// localStorage.setItem("token", token);
// }
</script>
<template>
<div>项目A页面</div>
</template>
- B项目 App.vue
html
<script setup>
// 获取token值
const token = location.search.split("=")[1];
// 若是没有 token,则才调用登录接口
if (!token) {
// 调用登录接口,跳转页面 传递appId 让用户知道是从那个项目上跳转的,, 数值需要与appToMapUrl对应
fetch("http://localhost:3000/login?appId=bs789ipB").then((res) => {
location.href = res.url;
});
}
// else {
// // 存储下 token
// localStorage.setItem("token", token);
// }
</script>
<template>
<div>项目B页面</div>
</template>