# kw-idp-simple **Repository Path**: LiuKewenSc/kw-idp-simple ## Basic Information - **Project Name**: kw-idp-simple - **Description**: 一个简单的 idp应用,包含了oauth2、oidc、saml 协议对接方式 - **Primary Language**: Unknown - **License**: Apache-2.0 - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 1 - **Created**: 2026-05-10 - **Last Updated**: 2026-06-05 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # IDP Demo3 — 轻量级身份认证中心(Identity Provider) 一个基于 Spring Boot 2.7 的轻量级 SSO 认证中心(IdP),单应用即可同时对外提供 **SAML 2.0 / OAuth 2.0 / OpenID Connect 1.0** 三种协议能力,支持 **SP-Initiated / IdP-Initiated** 两种 SAML SSO 流程,内置管理后台、内置 Demo SP,可作为企业内部 SSO 网关的学习参考或二次开发起点。 > 适用场景:企业内部应用接入统一登录、学习 SAML/OAuth2/OIDC 协议、二次开发自研 IdP。 --- ## 目录 - [一、核心特性](#一核心特性) - [二、技术栈](#二技术栈) - [三、架构与目录](#三架构与目录) - [四、快速开始](#四快速开始) - [五、配置说明](#五配置说明) - [六、管理后台](#六管理后台) - [七、协议接入指南](#七协议接入指南) - [7.1 OAuth 2.0 接入(授权码模式)](#71-oauth-20-接入授权码模式) - [7.2 OpenID Connect 接入](#72-openid-connect-接入) - [7.3 SAML 2.0 接入(SP-Initiated)](#73-saml-20-接入sp-initiated) - [7.4 SAML 2.0 接入(IdP-Initiated / Unsolicited)](#74-saml-20-接入idp-initiated--unsolicited) - [八、完整端点一览](#八完整端点一览) - [九、数据库表结构](#九数据库表结构) - [十、内置 Demo SP](#十内置-demo-sp) - [十一、常见问题](#十一常见问题) --- ## 一、核心特性 - ✅ **三协议统一**:OAuth 2.0 授权码 + 刷新令牌、OpenID Connect(Discovery / JWKS / UserInfo)、SAML 2.0(HTTP-Redirect / HTTP-POST 双 Binding)。 - ✅ **PKCE 支持**:完整实现 RFC 7636 PKCE(Proof Key for Code Exchange),支持 `S256` 和 `plain` 两种 code_challenge_method,可按应用强制要求 PKCE。 - ✅ **按应用隔离的证书体系**:每个接入应用在首次使用时自动生成独立的 **RSA 2048** 密钥对和自签名 X.509 证书(有效期 1 年),持久化到 `cert_key_pair` 表,签名/加密互不干扰。 - ✅ **SP-Initiated + IdP-Initiated 两种 SAML 流程**:支持 SP 发起的标准流程,也支持 IdP 主动推送的 Unsolicited Response(无 `InResponseTo`)。 - ✅ **内置管理后台**:`/admin/clients` 一站式完成应用创建、编辑、软删除、证书刷新、SAML 元数据下载。 - ✅ **内置 Demo SP**:`/sp-demo` 提供 SAML / OAuth2 登录对接样例,可用于自测 IdP 输出是否合规。 - ✅ **会话与缓存可替换**:会话、授权码、登录恢复上下文均存在抽象缓存(默认内存实现 `MemoryCacheServiceImpl`),可以无缝替换为 Redis。 - ✅ **登录中断自动恢复**:OAuth2 / SAML 请求进入时如用户未登录,会缓存完整上下文(5 分钟),登录后通过 `resumeKey` 恢复原协议流程。 --- ## 二、技术栈 | 类别 | 组件 | 版本 | |------|------|------| | JDK | Java | **1.8** | | 后端框架 | Spring Boot | 2.7.18 | | 持久化 | MyBatis-Plus | 3.5.3.1 | | 数据库 | MySQL(默认) / H2(可切换) | — | | 前端框架 | Vue 3 + Vue Router + Pinia | 3.x | | 前端构建 | Vite | 5.x | | 前端 UI | Element Plus(按需自动引入) | 2.x | | SAML | OpenSAML | 3.4.6 | | JWT | jjwt | 0.9.1(RS256) | | 证书 | Bouncy Castle | 1.70 | | 工具 | Lombok、JAXB | — | > Maven 构建时需要访问 Shibboleth 仓库获取 OpenSAML:`https://build.shibboleth.net/nexus/content/repositories/releases/`(已在 `pom.xml` 配置)。 --- ## 三、架构与目录 ``` src/main/java/com/idp/idpdemo3 ├── IdpDemo3Application.java # Spring Boot 启动入口 ├── cache/ # 抽象缓存(可替换为 Redis) ├── common/ # JWT / Session / SAML 工具 ├── config/ # Web 拦截器配置 ├── controller/ # 登录、应用管理、后台、Demo SP ├── dto/ / vo/ # 请求 / 响应对象 ├── entity/ # User / Application / CertKeyPair ├── interceptor/ # LoginInterceptor(公共路径放行) ├── mapper/ # MyBatis-Plus Mapper ├── oauth/ # OAuth2 授权码模式控制器与服务 ├── oidc/ # OIDC Discovery / JWKS / UserInfo ├── saml/ # OpenSAML 协议层(解析 / 签名 / 元数据) └── service/ # 业务服务(用户、应用、会话、证书) src/main/resources ├── application.yml # 全局配置 ├── schema.sql / data.sql # 建表脚本 + 默认数据 └── static/ # Vite 构建产物(Maven 打包时自动生成) frontend/ # Vue 3 + Vite 前端工程 ├── index.html ├── package.json # 前端依赖 ├── vite.config.js # Vite 配置:proxy 到 9010、打包输出到 src/main/resources/static └── src/ ├── main.js ├── App.vue ├── api/ # axios 封装(统一 Result 拦截,401 跳登录) ├── router/ # Vue Router(history 模式) ├── store/ # Pinia user store └── views/ # Login / Home / admin/Clients / oauth/Authorize / sp-demo/** ``` 主要分层关系: ```mermaid graph TB SP[接入方应用 SP] -->|OAuth2/OIDC/SAML| IDP[IDP Controller 层] IDP --> SVC[Service 层] SVC --> MP[MyBatis-Plus Mapper] MP --> DB[(MySQL / H2)] SVC --> CACHE[CacheService] SVC --> CERT[CertKeyPairService] CERT --> DB ``` --- ## 四、快速开始 ### 4.1 环境要求 - JDK 1.8+(已测试 1.8) - Maven 3.6+ - MySQL 5.7+ / 8.x(可选切换为内置 H2) - **开发模式额外要求**:Node.js 18+ / npm 9+(生产打包时由 `frontend-maven-plugin` 自动安装,无需手动准备) ### 4.2 准备数据库(MySQL 模式) ```sql CREATE DATABASE kw_idp_simple DEFAULT CHARACTER SET utf8mb4; ``` 然后在 `kw_idp_simple` 库里执行 `src/main/resources/schema.sql` 建表,并执行 `data.sql` 插入默认账号。 > 也可以取消 `application.yml` 中的注释,使用 **H2 内存数据库** + Spring Boot 的 `sql.init` 自动建表,无需手工准备。 ### 4.3 修改配置 编辑 `src/main/resources/application.yml`,按你的环境改 `spring.datasource`: ```yaml spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/kw_idp_simple?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai username: root password: 你的密码 ``` 如果要用 H2: ```yaml spring: datasource: driver-class-name: org.h2.Driver url: jdbc:h2:mem:idpdb;DB_CLOSE_DELAY=-1;MODE=MySQL username: sa password: sql: init: mode: always schema-locations: classpath:schema.sql data-locations: classpath:data.sql ``` ### 4.4 启动 项目采用“前后端同仓库 + 同一个端口”的形式,**开发期双进程联调,生产期单端口单 jar**。 #### 方式 A:开发模式(推荐,热更新) 打开两个终端分别启动后端与前端: ```bash # 终端 1:后端,监听 9010 mvn spring-boot:run # 终端 2:前端开发服务器,监听 5173 cd frontend npm install # 首次运行 npm run dev ``` 打开 http://localhost:5173 即可联调,Vite 会自动将 `/api`、`/oauth2`、`/oidc`、`/saml`、`/sp-demo`、`/.well-known`、`/h2-console` 等路径 proxy 到后端 9010。 #### 方式 B:生产模式(单 jar、单端口) ```bash # 一键打包,frontend-maven-plugin 会自动下载 Node/npm、安装依赖、执行 npm run build # 前端产物直接输出到 src/main/resources/static/ 并随 jar 打包 mvn clean package -DskipTests java -jar target/idp-demo3-1.0.0.jar ``` 启动后访问(默认端口 **9010**): | 入口 | URL | |------|-----| | 首页 | http://localhost:9010/ | | 登录页 | http://localhost:9010/login | | 管理后台 | http://localhost:9010/admin/clients | | Demo SP | http://localhost:9010/sp-demo | | OIDC Discovery | http://localhost:9010/oidc/.well-known/openid-configuration | | H2 控制台(如启用) | http://localhost:9010/h2-console | 默认账号:**`admin` / `123456`**(明文存储,详见 `data.sql`,生产使用请自行改造为 BCrypt)。 --- ## 五、配置说明 `application.yml` 关键配置(全部位于 `idp.*` 前缀下): ```yaml server: port: 9010 # 服务端口 idp: web: host: http://localhost:5173 # 开发阶段前端地址,(打包部署会整合到后端,则不需要配置) session: timeout: 1800 # 会话过期秒数(默认 30 分钟) saml: entity-id: idp-demo3 # IdP 的 entityID(元数据里的 Issuer) sso-service-url: http://localhost:9010/saml/sso # IdP SSO 端点 URL oauth2: authorization-endpoint: /oauth2/authorize token-endpoint: /oauth2/token issuer: http://localhost:9010 # OAuth2/OIDC 的 issuer,务必改为生产域名 ``` > ⚠️ **部署到生产** 时,`idp.saml.sso-service-url` 和 `idp.oauth2.issuer` 必须改为对外可访问的完整 URL(含协议 + 域名 + 端口),否则 SP 侧无法校验。 --- ## 六、管理后台 访问 `http://localhost:9010/admin/clients`(需登录)。可完成: | 操作 | 入口 | |------|------| | 新增应用 | "新增" 按钮,按协议类型填写字段 | | 编辑应用 | 列表行 "编辑"(`clientId` / `clientSecret` 不可改) | | 软删除应用 | 列表行 "删除" | | 上传 SP Metadata | 应用编辑页面 "📄 上传 SP Metadata" 按钮(自动解析 SAML 配置) | | 查看 SAML 元数据 | "元数据" 按钮,返回 `metadataUrl` 供 SP 下载 | | 查看 SP 证书 | 应用详情页 "SP 证书" 字段旁的 "查看" 按钮 | | 刷新应用证书 | "刷新证书" 按钮(生成新 RSA 密钥对) | 应用注册后,底层 REST API 亦可直接调用(见 [八、完整端点一览](#八完整端点一览))。 --- ## 七、协议接入指南 > 下文以 **你的应用叫 `my-app`** 为例,一步步演示三种协议的接入流程。 --- ### 7.1 OAuth 2.0 接入(授权码模式) 本 IdP 实现 RFC 6749 授权码模式 + 可选 refresh_token。 #### 步骤 ①:在管理后台注册应用 打开 `/admin/clients` → 新增,填写下列字段: | 字段 | 含义 | 示例 | |------|------|------| | `clientName` | 应用展示名(授权页会显示) | `我的业务系统` | | `clientId` | 留空自动生成(`client_xxxxxxxxxxxxxxxx`),也可自填 | `my-app` | | `clientSecret` | 留空自动生成(64 位),也可自填 | `<64位字符串>` | | `redirectUris` | **允许的回调地址**,多个用英文逗号分隔,**必须完全匹配** | `http://my-app.com/oauth/callback,http://localhost:8080/oauth/callback` | | `grantTypes` | 支持的授权类型,默认 `authorization_code` | `authorization_code,refresh_token` | | `scopes` | 默认作用域(记录备注,接入时请求的 scope 才是实际生效的) | `openid,profile,email` | | `requirePkce` | 是否强制要求 PKCE,默认 0(否) | `1`(是) | 创建成功后,请**务必保存 `clientId` 和 `clientSecret`**。 > 仅使用 OAuth2 时,SAML 相关字段(`samlEntityId`、`samlAcsUrl` 等)可留空。 #### 步骤 ②:发起授权请求(浏览器重定向) 在 SP 前端发起跳转: ``` GET http://localhost:9010/oauth2/authorize ?response_type=code &client_id=my-app &redirect_uri=http%3A%2F%2Fmy-app.com%2Foauth%2Fcallback &scope=openid%20profile%20email &state=随机防CSRF值 &code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM // PKCE 参数(可选) &code_challenge_method=S256 // PKCE 参数(可选) ``` | 参数 | 必填 | 说明 | |------|------|------| | `response_type` | 是 | 固定 `code` | | `client_id` | 是 | 应用的 `clientId` | | `redirect_uri` | 是 | URL 编码,必须与注册的 `redirectUris` 中某一项完全一致 | | `scope` | 否 | 空格分隔,包含 `openid` 才会返回 `id_token` | | `state` | 建议 | CSRF 校验,IdP 会原样返回 | | `code_challenge` | 条件 | PKCE 参数,若应用开启了 `require_pkce` 则必填 | | `code_challenge_method` | 条件 | PKCE 参数,`S256`(推荐)或 `plain`,默认 `S256` | IdP 行为: - 若用户未登录 → 将请求**暂存缓存(5 分钟)并 302 到 `/login?redirect=/oauth2/authorize?resumeKey=xxx`**,登录完成后自动恢复。 - 若已登录 → 显示授权确认页(`/templates/oauth/authorize.html`),用户点同意后进入步骤 ③。 #### 步骤 ③:用户同意 → 回调携带 code IdP 重定向到: ``` http://my-app.com/oauth/callback?code=<授权码>&state=<原样返回> ``` 授权码 **5 分钟内有效,一次性使用**。 #### 步骤 ④:用 code 换 token(后端服务器调用) ```http POST http://localhost:9010/oauth2/token Content-Type: application/x-www-form-urlencoded Authorization: Basic base64(client_id:client_secret) # 或放在 body 中 grant_type=authorization_code &code=<步骤③拿到的code> &redirect_uri=http%3A%2F%2Fmy-app.com%2Foauth%2Fcallback &code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk # PKCE 参数(如果授权时带了 code_challenge) ``` 成功响应: ```json { "access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjEi...", "token_type": "Bearer", "expires_in": 3600, "refresh_token": "eyJhbGciOiJSUzI1NiIs...", "id_token": "eyJhbGciOi...(仅 scope 含 openid 时返回)", "scope": "openid profile email" } ``` > `access_token` / `refresh_token` / `id_token` 均为 **RS256 签名的 JWT**,使用**该应用的私钥**签名,SP 端可通过 OIDC JWKS 端点拉公钥验签(见 7.2)。 #### 步骤 ⑤:刷新令牌 ```http POST /oauth2/token Authorization: Basic base64(client_id:client_secret) grant_type=refresh_token &refresh_token=<上一步拿到的 refresh_token> ``` 响应结构同步骤 ④。 #### 步骤 ⑥:调用 UserInfo(可选,OAuth2 也可用) ```http GET http://localhost:9010/oidc/userinfo Authorization: Bearer ``` 响应: ```json { "sub": "1", "name": "管理员", "preferred_username": "admin", "email": "admin@example.com" } ``` #### OAuth2 错误响应约定 所有参数异常会 302 回 `redirect_uri?error=&error_description=&state=<原值>`: | error | 含义 | |-------|------| | `invalid_client` | `client_id` 不存在 | | `invalid_redirect_uri` | `redirect_uri` 未在白名单 | | `unsupported_response_type` | `response_type` 不是 `code` | | `access_denied` | 用户点了拒绝 | | `invalid_grant` | `code` 失效或参数不匹配(Token 端点返回 400 JSON) | --- ### 7.2 OpenID Connect 接入 OIDC **在 OAuth2 之上扩展**,流程与 7.1 完全相同,唯一区别:`scope` 必须包含 `openid`。 #### Discovery 文档 ```http GET http://localhost:9010/oidc/.well-known/openid-configuration ``` 返回: ```json { "issuer": "http://localhost:9010", "authorization_endpoint": "http://localhost:9010/oauth2/authorize", "token_endpoint": "http://localhost:9010/oauth2/token", "userinfo_endpoint": "http://localhost:9010/oidc/userinfo", "jwks_uri": "http://localhost:9010/oidc/jwks", "response_types_supported": ["code"], "grant_types_supported": ["authorization_code", "refresh_token"], "subject_types_supported": ["public"], "id_token_signing_alg_values_supported": ["RS256"], "scopes_supported": ["openid", "profile", "email"], "token_endpoint_auth_methods_supported": ["client_secret_basic", "client_secret_post"], "claims_supported": ["sub", "iss", "aud", "exp", "iat", "name", "preferred_username", "email"] } ``` #### JWKS 端点(验签公钥) ```http GET http://localhost:9010/oidc/jwks ``` 返回所有已启用应用的公钥集合: ```json { "keys": [ { "kty": "RSA", "kid": "my-app", "use": "sig", "alg": "RS256", "n": "", "e": "" }, { "kty": "RSA", "kid": "another-app", "use": "sig", "alg": "RS256", "n": "", "e": "" } ] } ``` > 客户端通过 ID Token 的 `kid`(Key ID)头部来匹配应该使用哪个公钥进行验签。 #### ID Token 字段 | claim | 说明 | |-------|------| | `iss` | = `idp.oauth2.issuer` 配置 | | `sub` | 用户主键 ID(字符串) | | `aud` | 应用主键 ID(字符串) | | `iat` / `exp` | 签发 / 过期(秒) | | `kid`(header) | 应用主键 ID,用于 JWKS 匹配 | | `name` | `user.display_name` | | `preferred_username` | `user.username` | | `email` | `user.email` | #### OIDC 接入总步骤 1. 在管理后台注册应用(同 7.1); 2. SP 拉取 Discovery 文档(可选,也可硬编码); 3. 走 OAuth2 授权码流程,**确保 scope 包含 `openid`**; 4. 获取 `id_token` 后,通过 `jwks?client_id=...` 拿公钥 **RS256** 验签; 5. 需要完整用户信息时调 `/oidc/userinfo`。 --- ### 7.3 SAML 2.0 接入(SP-Initiated) 本 IdP 同时支持 HTTP-Redirect Binding(`GET`,请求需 Deflate 压缩)与 HTTP-POST Binding(`POST`,仅 Base64)。 #### 步骤 ①:注册 SAML 应用 在 `/admin/clients` 新增应用时填写 SAML 配置。**支持两种方式配置 SAML 参数:** **方式一:上传 SP Metadata(推荐)** 在应用编辑页面的 "SAML 2.0 配置" 区域,点击 **"📄 上传 SP Metadata"** 按钮,选择 SP 提供的 Metadata XML 文件,系统将自动解析并填充以下字段: - `samlEntityId` - SP EntityID - `samlAcsUrl` - ACS URL(优先选择 POST Binding) - `nameidFormat` - NameID 格式 - `samlCertificate` - SP 签名证书(X.509,Base64 编码) **方式二:手动填写** | 字段 | 含义 | 示例 | |------|------|------| | `clientName` | 应用名称 | `我的 SAML 应用` | | `samlEntityId` | **SP 的 EntityID**(必须与 SP 发来的 `` 一致) | `my-app-sp` | | `samlAcsUrl` | **SP 的 AssertionConsumerService URL**(必须与 SP `AuthnRequest` 中的 `AssertionConsumerServiceURL` 完全一致) | `http://my-app.com/saml/acs` | | `nameidFormat` | NameID 格式 | `urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress`(默认) | | `nameidAttribute` | NameID 取值字段:`email` / `username` / `displayName` | `email` | | `samlCertificate` | **SP 签名证书**(X.509 证书 Base64 编码,用于验证 SP 签名请求) | `-----BEGIN CERTIFICATE-----...` | | `attributeMappings` | 自定义属性映射 JSON(可选) | 见下方 | > 💡 **提示**:上传 Metadata 后,所有字段仍可手动修改。证书字段支持手动粘贴或从 Metadata 自动解析,在表单中可以点击"查看"按钮查看完整证书内容。 `attributeMappings` 格式(未配置时默认返回 `username` / `displayName` / `email` 三项): ```json [ {"samlAttr": "email", "userAttr": "email"}, {"samlAttr": "displayName", "userAttr": "displayName"}, {"samlAttr": "uid", "userAttr": "username"} ] ``` 保存后,点击 "元数据" 获取 IdP 的 SAML 元数据下载地址: ``` http://localhost:9010/saml/metadata?id=<应用主键 id> ``` 将该 XML(含 IdP EntityID、SSO 端点、签名证书、NameIDFormat、Binding 列表)导入 SP 完成 IdP 端配置。 #### 步骤 ②:SP 构造 AuthnRequest 发给 IdP 最小 AuthnRequest XML: ```xml my-app-sp ``` 两种发送方式: **HTTP-Redirect Binding(推荐,浏览器重定向)** 1. 对 XML 执行 `Deflate 压缩 → Base64 编码 → URL 编码`; 2. 浏览器 302 到: ``` http://localhost:9010/saml/sso?SAMLRequest=&RelayState=<任意状态> ``` **HTTP-POST Binding** 1. 对 XML 直接 Base64 编码; 2. 构造 POST 表单自动提交: ```html
``` #### 步骤 ③:IdP 处理并回推 SAML Response IdP 处理流程: 1. 解析 `SAMLRequest`(按 Binding 选择解压/解码)→ 提取 `Issuer` / `AssertionConsumerServiceURL`; 2. 未登录 → 缓存上下文 + 302 到登录页,登录后自动恢复; 3. 按 `Issuer` 查找应用配置,**校验 `AssertionConsumerServiceURL` 必须等于 `samlAcsUrl`**; 4. 用**该应用独立的证书**构建 ``,其中 `` 经 **RSA-SHA256** 数字签名,包含: - `` / `` - `` + `SubjectConfirmationData`(`InResponseTo` / `Recipient` / `NotOnOrAfter=+10m`) - `` + `` - ``(`AuthnInstant` / `SessionIndex` / PasswordProtectedTransport) - ``(按应用配置的属性映射) 5. 直接返回 auto-submit HTML(后端字符串拼接),**HTML 自动 POST 到 SP 的 `samlAcsUrl`**,字段名固定为 `SAMLResponse`(Base64 编码 XML)和 `RelayState`。 #### 步骤 ④:SP 侧验证 SP 收到 POST 后应: 1. Base64 解码得到 `` XML; 2. **用 IdP 元数据中的 X.509 证书验证 `` 的签名**(RSA-SHA256,Exclusive Canonicalization); 3. 校验 `Response.InResponseTo` == 自己之前发出的 `AuthnRequest.ID`; 4. 校验 `Conditions.NotBefore` / `NotOnOrAfter` / `AudienceRestriction.Audience == 自身 EntityID`; 5. 校验 `Destination` == SP 的 ACS URL; 6. 提取 `` 和 `` 完成登录。 --- ### 7.3.1 SP Metadata 上传解析功能 为简化 SAML 应用配置,IdP 提供了 **SP Metadata 自动解析** 功能。 #### 功能说明 在应用管理的新增/编辑页面,点击 **"📄 上传 SP Metadata"** 按钮,选择 SP 提供的 Metadata XML 文件,系统将自动解析并填充: | 解析字段 | 说明 | |---------|------| | `EntityID` | SP 的唯一标识,填充到 `samlEntityId` | | `ACS URL` | AssertionConsumerService 地址(优先选择 POST Binding),填充到 `samlAcsUrl` | | `NameIDFormat` | SP 支持的 NameID 格式(使用第一个),填充到 `nameidFormat` | | `X.509 Certificate` | SP 的签名证书(Base64 编码),填充到 `samlCertificate` | #### 使用方式 **方式一:通过 Metadata 自动配置** 1. 从 SP 系统下载 Metadata XML 文件(通常命名为 `sp-metadata.xml` 或通过 URL 提供) 2. 在 IdP 管理后台创建/编辑应用 3. 在 "SAML 2.0 配置" 区域点击 "📄 上传 SP Metadata" 4. 选择 XML 文件,系统自动解析并填充表单 5. 检查并调整其他配置(如属性映射),保存即可 **方式二:手动配置** 1. 从 SP Metadata 中复制对应字段值 2. 手动粘贴到表单对应输入框 3. 证书字段支持直接粘贴 Base64 编码的 X.509 证书 4. 点击 "查看" 按钮可弹框查看完整证书内容 #### 注意事项 - Metadata 文件仅用于解析,**不会存储到数据库** - 解析后的字段可以手动修改,提供灵活性 - 证书字段可选,如果 SP Metadata 中没有证书信息,可以留空或手动配置 - ACS URL 优先选择 HTTP-POST Binding 的地址,如果没有则选择第一个 --- ### 7.4 SAML 2.0 接入(IdP-Initiated / Unsolicited) 标准 SAML 2.0 允许 IdP **在无 `AuthnRequest` 的情况下** 直接向 SP 推送 Response(常用于门户登录后一键跳到目标应用)。 #### 步骤 ①:在管理后台注册 SAML 应用(同 7.3) 应用必须配置 `samlEntityId` 和 `samlAcsUrl`。 #### 步骤 ②:用户(已登录 IdP)点击此链接 任选其一: ``` # 按应用主键 id GET http://localhost:9010/saml/idp-init/sso?appId= # 按 SP entityId GET http://localhost:9010/saml/idp-init/sso?entityId=my-app-sp ``` 未登录时会先跳登录页。 #### 步骤 ③:IdP 构造 Unsolicited Response 与 SP-Initiated 的唯一区别: - `` **不包含 `InResponseTo`**; - `` **也不包含 `InResponseTo`**。 其余签名、断言结构、属性映射与 SP-Initiated 完全一致。IdP 返回 auto-submit HTML(后端字符串拼接)自动 POST 到 SP 的 `samlAcsUrl`。 #### 步骤 ④:SP 验证 SP 侧需要支持 Unsolicited Response(许多商业 SP 默认允许,但部分实现要求显式开启),**不要校验 `InResponseTo`**。其余校验项同 SP-Initiated。 --- ## 八、完整端点一览 ### 8.1 用户登录 | Method | Path | 说明 | |--------|------|------| | GET | `/login?redirect=<回跳地址>` | 登录页;已登录自动跳 `redirect` | | POST | `/api/login` | JSON `{username, password}`,成功后下发 `IDP_SESSION` Cookie | | POST | `/api/logout` | 清除会话 | | GET | `/` | 首页,展示当前登录用户 | ### 8.2 OAuth 2.0 / OIDC | Method | Path | 说明 | |--------|------|------| | GET | `/oauth2/authorize` | 授权端点(SPA 页面),参数见 7.1 | | POST | `/api/oauth2/authorize-data` | 授权确认页数据接口(内部) | | POST | `/api/oauth2/approve` | 授权确认页提交(内部) | | POST | `/oauth2/token` | Token 端点,支持 `authorization_code` / `refresh_token` | | GET | `/oidc/.well-known/openid-configuration` | OIDC Discovery | | GET | `/oidc/jwks` | JWKS(返回所有应用的公钥集合) | | GET | `/oidc/userinfo` | UserInfo(`Authorization: Bearer `) | ### 8.3 SAML 2.0 | Method | Path | 说明 | |--------|------|------| | GET | `/saml/sso` | SP-Initiated,HTTP-Redirect Binding(Deflate+Base64) | | POST | `/saml/sso` | SP-Initiated,HTTP-POST Binding(Base64) | | GET | `/saml/metadata?id=` | 指定应用的 IdP 元数据 XML | | GET | `/saml/idp-init/sso?appId=... \| entityId=...` | IdP-Initiated Unsolicited SSO | ### 8.4 应用管理(管理后台 REST API) | Method | Path | 说明 | |--------|------|------| | GET | `/api/clients` | 列表 | | GET | `/api/clients/{id}` | 详情 | | POST | `/api/clients` | 新增(Body:`Application` JSON;未传 `clientId`/`clientSecret` 会自动生成) | | PUT | `/api/clients/{id}` | 更新(不允许修改 `clientId` / `clientSecret`) | | DELETE | `/api/clients/{id}` | 软删除 | | GET | `/api/clients/{id}/metadata` | 获取 SAML 元数据摘要(`metadataUrl` 等) | | POST | `/api/clients/{id}/refresh-cert` | 刷新应用证书(重建 RSA 密钥对) | ### 8.5 管理后台页面 | Method | Path | 说明 | |--------|------|------| | GET | `/admin/clients` | 管理后台 HTML | ### 8.6 Demo SP(内置自测) | Method | Path | 说明 | |--------|------|------| | GET | `/sp-demo` | Demo SP 首页(SPA) | | GET | `/api/sp-demo/saml/login` | 发起 SAML SSO(返回重定向 URL) | | POST | `/sp-demo/saml/acs` | SAML ACS 接收断言 | | GET | `/api/sp-demo/oauth/login` | 发起 OAuth2 授权(返回重定向 URL) | | GET | `/sp-demo/oauth/callback` | OAuth2 回调 + 解析 ID Token + 调用 UserInfo | | GET | `/api/sp-demo/result-data?key=...` | 结果页数据接口(内部) | > Demo SP 默认使用 `clientId=test-app` / `clientSecret=test-secret`、`samlEntityId=sp-demo-app`、`samlAcsUrl=http://localhost:8080/sp-demo/saml/acs`,请在管理后台新建对应应用(注意 8080 与 9010 端口的区别,也可改 `SpDemoController` 里的常量)。 --- ## 九、数据库表结构 ### `sys_user`(用户表) | 字段 | 类型 | 说明 | |------|------|------| | `id` | BIGINT | 主键 | | `username` | VARCHAR(64) | 用户名,唯一 | | `password` | VARCHAR(128) | **明文**(生产请改 BCrypt) | | `email` | VARCHAR(128) | 邮箱 | | `phone` | VARCHAR(128) | 手机号 | | `display_name` | VARCHAR(64) | 展示名 | | `status` | TINYINT | 0=禁用 / 1=启用 | | `deleted` | TINYINT | 逻辑删除(0/1) | | `create_time`/`update_time` | TIMESTAMP | 时间戳 | ### `application`(应用表) | 字段 | 类型 | 说明 | |------|------|------| | `id` | BIGINT | 主键,证书关联键 | | `client_id` | VARCHAR(64) | OAuth2/OIDC 客户端 ID | | `client_secret` | VARCHAR(128) | OAuth2/OIDC 密钥 | | `client_name` | VARCHAR(128) | 应用名称 | | `redirect_uris` | TEXT | 回调地址,逗号分隔 | | `grant_types` | VARCHAR(128) | 默认 `authorization_code` | | `scopes` | VARCHAR(256) | 默认 `openid,profile` | | `require_pkce` | TINYINT | 是否强制要求 PKCE:0否 1是(仅对authorization_code流程生效) | | `saml_entity_id` | VARCHAR(256) | SP EntityID | | `saml_acs_url` | VARCHAR(512) | SP ACS URL | | `nameid_format` | VARCHAR(64) | SAML NameID 格式 | | `nameid_attribute` | VARCHAR(32) | NameID 取自哪个字段 | | `attribute_mappings` | TEXT | 属性映射 JSON | | `saml_certificate` | TEXT | SP 签名证书(Base64 编码的 X.509 证书,从 SP Metadata 解析或手动配置) | | `status` / `deleted` | TINYINT | 启用 / 软删 | ### `cert_key_pair`(应用证书表) | 字段 | 说明 | |------|------| | `application_id` | 关联 `application.id`,唯一 | | `public_key` / `private_key` | Base64 编码(X.509 SubjectPublicKeyInfo / PKCS8) | | `certificate` | Base64 编码的 X.509 证书 | | `modulus` / `exponent` | Base64URL(JWKS 使用) | | `algorithm` / `key_size` | 默认 RSA / 2048 | | `not_before` / `not_after` | 有效期 1 年 | | `status` | 0禁用 1启用 | --- ## 十、内置 Demo SP `/sp-demo` 是一个嵌在 IdP 进程里的"假 SP",用来一键验证 IdP 输出合规。使用步骤: 1. 登录 IdP(`/login`,`admin/123456`); 2. 在管理后台新增应用: - OAuth2 Demo:`clientId=test-app`、`clientSecret=test-secret`、`redirectUris=http://localhost:8080/sp-demo/oauth/callback`; - SAML Demo:`samlEntityId=sp-demo-app`、`samlAcsUrl=http://localhost:8080/sp-demo/saml/acs`; 3. 访问 `/sp-demo`,分别点击 SAML / OAuth2 登录按钮,将看到原始 XML、解析后的 NameID/Attribute、access_token、ID Token 头和载荷、UserInfo 响应等。 > Demo SP 里 `http://localhost:8080/...` 只是一个占位主机地址,实际上 Demo 和 IdP 是同一个进程(9010),SAML POST 表单由浏览器自己回跳。如果改了端口/域名,请同步改 `SpDemoController` 里的常量和管理后台的应用配置。 --- ## 十一、常见问题 **Q1:启动报 Lombok 语法错误?** A:IDEA 需安装 Lombok 插件并开启 `Enable annotation processing`,Maven 需确保 `maven-compiler-plugin` 的 `annotationProcessorPaths` 显式包含 Lombok。 **Q2:首次 OIDC 登录拿到 `id_token` 但 SP 验签失败?** A:确认 JWKS 调用时携带了 `client_id`,因为每个应用证书不同,不带参数会返回空 `keys`。 **Q3:SAML SP 报 `InResponseTo` 不匹配?** A:若使用 IdP-Initiated,请确认 SP 已开启 Unsolicited Response;若使用 SP-Initiated,请检查 SP 本地是否保留了原始 `AuthnRequest.ID`。 **Q4:`AssertionConsumerServiceURL 不匹配`?** A:SP 发来的 `AuthnRequest` 中的 `AssertionConsumerServiceURL` **必须** 与后台注册时填写的 `samlAcsUrl` **一字不差**(含协议、端口、路径)。 **Q5:能否替换内存缓存为 Redis?** A:实现 `com.idp.idpdemo3.cache.CacheService` 的 Redis 版本并标注 `@Primary`(或移除默认实现)即可,业务代码无需改动。 **Q6:生产部署要改哪些配置?** A:最少改三处:①`spring.datasource`;②`idp.oauth2.issuer` 为对外可访问 URL;③`idp.saml.sso-service-url` 为对外 HTTPS 地址。另外建议:把用户密码改 BCrypt、把 `server.port` 改成标准 443/80、关闭 H2 控制台、使用持久化缓存(Redis)。 --- ## License 本项目仅作学习与二次开发参考,生产使用前请自行完成安全评审(密码加密、CSRF、HTTPS、令牌防重放、日志审计等)。