前言
- 最近项目需要迁入SSO登录认证功能
- 于是需要搞懂一件事情就是什么是单点登录
- 最好本地实现一个比较规范的单点登录事情
- 你将可以更加深入的了解什么是单点登录等
单点登录(Single Sign-On,SSO)是一种认证过程,允许用户使用一个集中的身份验证服务登录多个相关但独立的软件系统。在一个系统中成功登录后,用户能够自动访问其他子系统,而无需重新登录。实现子系统A登录后B系统自动可以访问,通常需要以下几个步骤:
- 选择SSO协议:常见的SSO协议有OAuth 2.0、OpenID Connect和SAML(Security Assertion Markup Language)。根据具体需求选择合适的协议。
- 配置身份提供者(Identity Provider,IdP) :IdP是负责用户身份验证的中心服务。常见的IdP有Keycloak、Auth0、Okta等。IdP将管理用户的认证和授权。
- 配置服务提供者(Service Provider,SP) :SP是需要使用SSO的各个子系统,如子系统A和子系统B。SP与IdP进行交互,以验证用户身份。
- 共享认证凭证:当用户在子系统A中成功登录后,IdP会生成一个认证凭证(如JWT token或SAML assertion),并通过安全的方式传递给子系统A。子系统A可以将该凭证保存在用户的会话或cookie中。
- 传递凭证:子系统B在用户访问时,会检查用户的会话或cookie中是否存在有效的认证凭证。如果存在,子系统B将凭证发送给IdP进行验证。如果验证通过,用户将被认为是已登录状态。
- 跨域问题:如果子系统A和子系统B在不同的域名下,需要解决跨域问题。可以使用浏览器的跨域cookie机制(如SameSite=None; Secure),或者通过反向代理等方式解决。
工作原理
- 用户认证:用户通过SSO系统的登录页面输入凭证(如用户名和密码)。
- 凭证验证:SSO系统验证用户的凭证。如果验证成功,生成一个认证令牌(Token)。
- 令牌传递:用户访问其他应用时,SSO系统将认证令牌传递给这些应用。
- 验证令牌:每个应用接收到令牌后,向SSO系统验证令牌的有效性。如果令牌有效,允许用户访问该应用。
优势
- 简化用户体验:用户只需一次登录,便可访问多个系统,避免频繁输入用户名和密码。
- 提高安全性:集中管理用户认证,便于统一控制和监控,减少了弱密码和重复密码的风险。
- 减少管理成本:减少了密码重置和管理的次数,提高了IT管理效率。
- 统一身份管理:便于对用户的访问权限进行集中管理和审计。
本次我们模拟**OpenID Connect**标准搭建一套sso认证
-
我们采用docker部署搭建本地
keycloak环境
// 拉取镜像
docker pull quay.io/keycloak/keycloak:latest
// 运行
docker run -d \
-e KEYCLOAK_ADMIN=admin \
-e KEYCLOAK_ADMIN_PASSWORD=admin \
-p 8080:8080 \
--name keycloak \
quay.io/keycloak/keycloak:latest \
start-dev
这里:
- `-e KEYCLOAK_ADMIN=admin` 和 `-e KEYCLOAK_ADMIN_PASSWORD=admin` 设置了初始的管理员用户名和密码。
- `-p 8080:8080` 将容器的 8080 端口映射到主机的 8080 端口。
- `--name keycloak` 为容器指定了一个名字。
- `start-dev` 是 Keycloak 的开发模式启动命令。
或者 本地创建docker-compose.yml 文件
version: '3'
services:
keycloak:
image: quay.io/keycloak/keycloak:latest
environment:
- KEYCLOAK_ADMIN=admin
- KEYCLOAK_ADMIN_PASSWORD=admin
ports:
- "8080:8080"
command:
- start-dev
docker-compose up -d启动也可以
上面我们是基于开发环境搭建的keycloak环境
生产环境需要构建数据库等配置,并允许https和证书等自行查阅
nginx反向代理
server {
listen 80;
server_name your_keycloak_hostname;
location / {
proxy_pass http://localhost:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
步骤 1: 登录到 Keycloak 管理控制台
- 打开浏览器,访问你的 Keycloak 管理控制台。例如,如果你在本地运行 Keycloak Docker 容器,访问
http://localhost:8080。 - 使用你在启动 Keycloak 容器时设置的管理员用户名和密码登录(例如,用户名为
admin,密码为admin)。 访问 Keycloak: 在浏览器中打开 http://localhost:8080,你将看到 Keycloak 的登录界面。使用你在上一步中设置的管理员用户名和密码进行登录
步骤 2: 创建一个 Realm
- 登录后,点击左上角的
Master下拉菜单,选择Add realm。 - 在
Add Realm页面中,填写Realm Name,例如myrealm。 - 点击
Create。
步骤 3: 创建一个客户端
- 进入
myrealm后,在左侧菜单中选择Clients。 - 点击右上角的
Create按钮。 - 填写客户端 ID,例如
myclient,然后点击Save。
步骤 4: 配置客户端
-
在客户端配置页面,选择
Settings选项卡。 -
找到
Access Type选项,设置为confidential。该选项可能在页面中间部分。 -
配置其他必要的选项:
Valid Redirect URIs:填写你的应用程序的回调 URL,例如http://localhost:3000/auth/callback。Web Origins:可以填写*或者你的应用程序的根 URL,例如http://localhost:3000。
-
点击
Save。
步骤 5: 获取客户端密钥
- 在客户端配置页面,选择
Credentials选项卡。 - 在
Client Authenticator下拉菜单中选择Client Id and Secret。 - 你会看到一个
Secret字段,复制这个值,这就是你的客户端密钥。
实现方案
前端搭建两套系统 采用vite + react
后端 模拟采用node + express等
代码分析
假设A系统前端页面
// clientA/src/App.js
import React, { useEffect, useState } from "react";
import axios from "axios";
function App() {
const [user, setUser] = useState(null);
useEffect(() => {
axios.get("/api/user", { withCredentials: true }).then((response) => {
setUser(response.data.user);
});
}, []);
return (
<div>
{user ? (
<div>
<h1>Welcome, {user.displayName}</h1>
<a href="http://localhost:8011">Go to Subsystem B</a>
</div>
) : (
<a href="/api/login">Login</a>
)}
</div>
);
}
export default App;
vite代理允许跨域访问
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
server: {
port: 8010,
proxy: {
"/api": {
target: "http://localhost:3000",
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ""),
},
},
},
});
系统A后端代码
// serverA.js
import express from "express";
import session from "express-session";
import passport from "passport";
import OpenIDConnectStrategy from "passport-openidconnect";
import crypto from "crypto";
const secret = crypto.randomBytes(64).toString("hex");
const app = express();
app.use(session({ secret, resave: false, saveUninitialized: true }));
app.use(passport.initialize());
app.use(passport.session());
passport.use(
new OpenIDConnectStrategy(
{
issuer: "http://localhost:8080/realms/myrealm",
authorizationURL:
"http://localhost:8080/realms/myrealm/protocol/openid-connect/auth",
tokenURL:
"http://localhost:8080/realms/myrealm/protocol/openid-connect/token",
userInfoURL:
"http://localhost:8080/realms/myrealm/protocol/openid-connect/userinfo",
clientID: "myclient",
clientSecret: "dSC9rxQOhh8MYgzOnJ45DUMaoIOaq4pX",
callbackURL: "http://localhost:3000/auth/callback",
},
(issuer, profile, cb) => {
try {
// Here you can handle the profile data and access tokens as needed
return cb(null, profile); // Call done with `null` for error and `user` object
} catch (error) {
return cb(error); // Call done with error if something goes wrong
}
}
)
);
passport.serializeUser((user, done) => {
console.log("---serializeUser------");
done(null, user);
});
passport.deserializeUser((obj, done) => {
console.log("-----deserializeUser------");
done(null, obj);
});
const isAuthenticated = (req, res, next) => {
if (req.isAuthenticated()) {
return next();
}
res.redirect("/login");
};
app.get("/login", passport.authenticate("openidconnect"));
app.get(
"/auth/callback",
passport.authenticate("openidconnect", {
failureRedirect: "/login",
failureMessage: true,
}),
(req, res) => {
res.redirect("/");
}
);
app.get("/", isAuthenticated, (req, res) => {
res.send(`Hello, ${req.user.displayName}`);
});
app.get("/user", (req, res) => {
if (req.isAuthenticated()) {
res.json({ user: req.user });
} else {
res.json({ user: null });
}
});
app.listen(3000, () => {
console.log("Server A started on http://localhost:3000");
});
参数解释
-
issuer:认证服务器的标识符,在这里是 Keycloak 的地址以及具体的 Realm。这告诉 Passport.js 要使用哪个认证服务器。issuer: 'https://your-keycloak-domain/auth/realms/myrealm' -
authorizationURL:用户认证的 URL,用户会被重定向到这个 URL 进行登录。authorizationURL: 'https://your-keycloak-domain/auth/realms/myrealm/protocol/openid-connect/auth' -
tokenURL:用于交换认证代码以获取访问令牌的 URL。tokenURL: 'https://your-keycloak-domain/auth/realms/myrealm/protocol/openid-connect/token' -
userInfoURL:用于获取用户信息的 URL。userInfoURL: 'https://your-keycloak-domain/auth/realms/myrealm/protocol/openid-connect/userinfo' -
clientID:在 Keycloak 上注册的客户端 ID,这个 ID 标识了哪个客户端(子系统B)在请求认证。clientID: 'clientB' -
clientSecret:客户端的密钥,用于身份验证,确保请求来自合法的客户端。clientSecret: 'your-client-secret' -
callbackURL:认证成功后,Keycloak 会重定向用户到这个 URL。这个 URL 通常是客户端应用程序的一个端点,用于处理认证响应。callbackURL: 'http://localhost:3001/auth/callback'启动命令测试
后台 node server_a.js # 启动a服务 node server_b.js # 启动b服务 前台 a系统前台服务 cd react-app-one pnpm dev b系统前台服务 cd react-app-two pnpm dev
- 访问
localhost:8010
- 点击登录去认证
3.登录我们创建好的授权用户
- username: abc
- password:123456
这在我们的管理系统可以查阅