切换主题
SPA 公共客户端与服务客户端深度解析
在 EDMA IAM 的统一认证管理控制台(/modules/module-06 客户端管理)中,登记接入应用时必须在 SPA 公共客户端 和 服务客户端 之间二选一。
本文系统阐述这两种客户端类型的选型依据、前后端交互架构、以及 Spring Boot 后端如何实现高性能的自动本地公钥验签。
一、SPA 与服务客户端分离原因及应用单一绑定原则
1. 运行环境安全边界与机密性差异(Public vs. Confidential)
根据 OAuth 2.0 / RFC 6749 规范,客户端按照其是否能够安全保管客户端凭证(Client Secret)划分为两类:
- 公共客户端(Public Client / SPA 应用):
- 运行宿主:最终用户的 Web 浏览器、单页应用(Vue、React 等)。
- 保密能力:零保密能力。前端 HTML/JavaScript 源码、网络通信及浏览器内存对用户和攻击者完全开放。任何硬编码进前端代码或分发给浏览器的固定密钥(Client Secret),均可通过浏览器开发者工具或反编译提取。
- 安全对策:不分配任何固定密钥,依靠 动态一次性校验机制(PKCE,Proof Key for Code Exchange) 和 严格受限的回调地址(Redirect URI) 保障授权过程安全。
- 机密客户端(Confidential Client / 服务客户端):
- 运行宿主:受控内网中的后端微服务、数据调度进程、定时任务程序。
- 保密能力:高保密能力。部署在受防火墙保护的受信任服务端,可通过环境变量、配置文件或密钥管理系统安全保存高强度的
client_secret。 - 安全对策:使用基于服务端私密凭据的 HTTP Basic / POST 认证(
client_secret_basic)。
2. 身份主体差异:自然人(User Principal)与机器实体(Machine Principal)
- SPA 客户端代表自然人: 用户通过浏览器向统一认证中心输入账号密码或扫码登录,签发的令牌携带具体用户的身份上下文,例如用户的
user_id、tenant_id、所属组织机构以及分配给该用户的业务角色权限。 - 服务客户端代表机器与服务自身: 无需具体用户参与,属于系统与系统之间的 M2M(Machine-to-Machine)调用。例如定时同步任务、系统级离线报表计算或跨系统消息订阅,令牌代表的是服务账号本身的内部访问权限(如
iam.internal)。
3. 应用单一绑定原则与防降级防护
在接入配置时,每个登记单元的角色定位必须单一且清晰:
- 角色定位单一:
- 面向用户的 Web 站点(如 OA 前端、HRM 前端),在 IAM 中登记为 SPA 客户端。
- 无界面的后台服务或批处理程序,在 IAM 中登记为 服务客户端。
- 业务后端定位为资源服务器而非客户端:在企业微服务落地中,业务系统通常由前端与后端组成。业务后端主要负责接收并校验前端传来的用户 Token,其在 OAuth 体系中充当资源服务器(Resource Server),无需申请客户端凭据;仅当后端服务需要以自身机器身份调用其他微服务时,才额外注册独立的服务客户端。
- 防降级攻击与权限混淆隔离: 若同一个客户端标识同时允许免密 PKCE 模式和服务密钥模式,攻击者即可利用公共客户端免密通道发起降级攻击,绕过机密服务的数据保护策略。系统在领域层实现(
OAuthClient.java)中对两者规则进行严格互斥校验,从数据模型层保证最小特权原则。
二、两类客户端核心差异全景对比
| 差异维度 | SPA 公共客户端 (SPA) | 服务客户端 (SERVICE) |
|---|---|---|
| 运行宿主 | 用户的 Web 浏览器 / 前端单页应用 | 内部私有服务器 / 后端微服务进程 |
| 客户端密钥 (Secret) | 无(认证方式为 none,禁止生成) | 有(服务端生成 32 位高强度密钥,仅创建时展示一次) |
| 认证方式 | none | client_secret_basic |
| OAuth 授权模式 | authorization_code (授权码) + refresh_token | client_credentials (客户端凭据) |
| 防伪与防拦截机制 | 强制开启 PKCE (S256) | 依赖高强度 Client Secret 进行私密比对 |
| 浏览器回调地址 | 必须配置 精确匹配的 redirect_uris 和退出回调 | 禁止配置 任何浏览器重定向地址 |
| 用户授权确认 (Consent) | 可选开启(是否在登录时提示用户授权确认) | 不适用(无用户交互) |
| 代表的实体 | 真实用户(User Principal) | 服务/系统自身(Machine Principal) |
| 令牌生命周期 | Access Token (10m) + Refresh Token (7d 轮换) | 仅签发 Access Token (10m),无 Refresh Token |
| 默认 / 典型 Scope | openid profile 及业务权限(如 oa:document:read) | 内部系统级 scope(如 iam.internal) |
三、前端 SPA 认证流程与业务后端零参与机制
在统一认证体系下,业务后端(如 OA API、HRM API)不需要提供任何登录接口,也不参与登录凭据的处理与会话维护。
1. 认证时序图(Archify 可视化呈现)
以下时序展示了从用户未登录发起访问、重定向至统一认证中心换取令牌,到最终携带 Token 调用业务后端接口的全链路。
💡 提示:可点击 全屏查看交互式时序图 ↗,支持明暗主题切换与高亮跟踪。
文本步骤解析
mermaid
sequenceDiagram
autonumber
actor U as 最终用户
participant SPA as 业务前端 (SPA)
participant GW as 统一网关 (Gateway)
participant IAM as 认证中心 (IAM)
participant API as 业务后端 (Resource Server)
Note over U,API: 阶段一:SPA 登录认证(业务后端零参与)
U->>SPA: 1. 访问业务页面 (未登录)
SPA->>GW: 2. 发起授权 GET /oauth2/authorize (PKCE challenge)
GW->>IAM: 3. 路由重定向至 IAM 统一登录页面
U->>IAM: 4. 用户输入账号密码与验证码登录
IAM-->>SPA: 5. 302 重定向至业务前端回调 (?code=...)
SPA->>GW: 6. POST /oauth2/token (携带 code + verifier)
GW->>IAM: 7. 网关转发至 IAM 授权端点
IAM-->>GW: 8. 验证 PKCE 签发 JWT Access & Refresh Token
GW-->>SPA: 9. 返回 Token 集合 (业务后端全程零参与)
Note over U,API: 阶段二:业务 API 调用与本地公钥验签
SPA->>GW: 10. 请求业务 API (Header: Bearer Token)
GW->>API: 11. 清洗并转发请求 + 租户上下文头
API->>API: 12. 本地 JWK 公钥验签成功,执行业务逻辑
API-->>GW: 13. 返回业务数据响应
GW-->>SPA: 14. 200 OK 渲染业务数据2. 凭据隔离与架构解耦收益
- 核心凭据不流经业务系统:用户的账号、密码、验证码仅直接提交给 IAM,各业务系统的后端与数据库全程不接触密码凭据。
- 业务团队免去重复造轮子:业务系统不需要编写登录控制器、Session 状态同步、图形验证码或密码强度策略。
- 多系统单点登录(SSO)体验:用户在 IAM 建立登录会话后,切换访问其他业务系统的 SPA 时,可自动静默完成授权。
四、接口调用与 Spring Boot 本地自动公钥验签
当前端获取 Access Token 并发起业务请求时,业务后端充当 OAuth 2.0 资源服务器(Resource Server)。
业务接口的 Token 校验并不需要向 IAM 发起远程 HTTP 请求,因此不会给集中式认证服务带来流量压力或造成单点瓶颈。其底层完全由 Spring Boot 在本地内存中完成非对称加密运算。
1. Spring Boot 自动验签机制剖析
业务微服务引入 spring-boot-starter-oauth2-resource-server 依赖,并在配置中声明 IAM 的 JWK 端点:
yaml
spring:
security:
oauth2:
resourceserver:
jwt:
# IAM 暴露的 JSON Web Key Set 公钥端点
jwk-set-uri: ${EDMA_IAM_INTERNAL_URL:http://localhost:6112}/oauth2/jwks配置该参数后,Spring Boot 内部的 NimbusJwtDecoder 与 RemoteJWKSet 机制自动接管以下处理:
- 按需拉取公钥(HTTP GET): 应用启动并收到首个携带 Token 的请求时,Spring Boot 自动向
jwk-set-uri发起一次 HTTP GET 请求,拉取 IAM 的公开公钥集合(包含 Key IDkid、加密算法、RSA 公钥模数等)。 - 本地内存缓存(Local Cache): 获取到的公钥常驻在业务服务进程的本地内存中。
- 极速本地验签(纯 CPU 运算): 随后的每一个业务请求:
- 框架自动从请求头提取
Authorization: Bearer <token>; - 从 Token Header 获取密钥编号
kid,直接匹配本地内存中的对应公钥; - 使用公钥在本地完成 RSA 签名校验,并验证过期时间(
exp)与签发者(iss)。 - 整个过程耗时为微秒到亚毫秒级,完全不产生网络 I/O 开销。
- 框架自动从请求头提取
- 证书轮换自动刷新(Key Rotation): 当 IAM 轮换签名证书并使用新密钥签发 Token 时,Token 头部将携带新的
kid。Spring Boot 发现本地内存缺少该公钥时,会自动触发一次重新拉取更新本地公钥缓存,保障服务平滑过渡。
2. 业务后端所需定制代码
加密验签和基础字段校验完全由框架自动完成,业务后端仅需定义权限映射与租户隔离逻辑。
以平台实际的微服务安全配置(如 HRM 服务的 SecurityConfiguration)为例:
java
@Bean
SecurityFilterChain security(HttpSecurity http) throws Exception {
var converter = new JwtAuthenticationConverter();
var scopes = new JwtGrantedAuthoritiesConverter();
// 将 EDMA 自定义的 permissions 权限数组映射为 Spring Security Authority
converter.setJwtGrantedAuthoritiesConverter(jwt -> {
var all = new LinkedHashSet<>(scopes.convert(jwt));
var permissions = jwt.getClaimAsStringList("permissions");
if (permissions != null) {
permissions.forEach(p -> all.add(new SimpleGrantedAuthority(p)));
}
return all;
});
return http
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(a -> a
.requestMatchers("/actuator/health/**").permitAll()
.anyRequest().authenticated()
)
// 启用 Resource Server 并挂载转换器
.oauth2ResourceServer(o -> o.jwt(j -> j.jwtAuthenticationConverter(converter)))
.build();
}在业务控制器(Controller)中直接注入已验签的 Jwt 对象获取租户上下文:
java
@GetMapping("/api/documents")
public List<Document> listDocuments(@AuthenticationPrincipal Jwt jwt) {
// 强制使用已校验 Token 内部的 tenant_id,防止客户端伪造租户传参
UUID tenantId = UUID.fromString(jwt.getClaimAsString("tenant_id"));
UUID userId = UUID.fromString(jwt.getSubject());
return documentService.listByTenant(tenantId, userId);
}五、开发者常见误区与最佳实践
误区 1:在前端代码中配置 Client Secret
- 错误做法:在前端工程的
.env配置文件中写入VITE_CLIENT_SECRET=xxx并在换取令牌时提交。 - 正确做法:SPA 应用必须注册为公共客户端(
SPA),不分配密钥,严格使用 PKCE(code_verifier+code_challenge)完成鉴权防伪。
误区 2:在业务微服务中重复实现用户登录与 JWT 签发
- 错误做法:业务系统自建用户表与
/api/login接口,自行利用私钥生成 JWT。 - 正确做法:所有用户统一由 IAM 认证;业务微服务作为资源服务器,仅持有公钥进行验证,不执行签发。
误区 3:在业务过滤器中发起远程 HTTP 校验 Token
- 错误做法:在自定义 Filter 中每次通过 HTTP 客户端调用 IAM 接口核验 Token 合法性。
- 正确做法:基于非对称加密签名原理,使用 Spring Boot 内置的本地公钥验签机制,兼顾安全性与微秒级响应性能。