Skip to content
业务前后端开发人员与系统架构师当前实现 · 2026-10内容核验 2026-10-09

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. 应用单一绑定原则与防降级防护 ​

在接入配置时,每个登记单元的角色定位必须单一且清晰:

  1. 角色定位单一:
    • 面向用户的 Web 站点(如 OA 前端、HRM 前端),在 IAM 中登记为 SPA 客户端。
    • 无界面的后台服务或批处理程序,在 IAM 中登记为 服务客户端。
    • 业务后端定位为资源服务器而非客户端:在企业微服务落地中,业务系统通常由前端与后端组成。业务后端主要负责接收并校验前端传来的用户 Token,其在 OAuth 体系中充当资源服务器(Resource Server),无需申请客户端凭据;仅当后端服务需要以自身机器身份调用其他微服务时,才额外注册独立的服务客户端。
  2. 防降级攻击与权限混淆隔离: 若同一个客户端标识同时允许免密 PKCE 模式和服务密钥模式,攻击者即可利用公共客户端免密通道发起降级攻击,绕过机密服务的数据保护策略。系统在领域层实现(OAuthClient.java)中对两者规则进行严格互斥校验,从数据模型层保证最小特权原则。

二、两类客户端核心差异全景对比 ​

差异维度SPA 公共客户端 (SPA)服务客户端 (SERVICE)
运行宿主用户的 Web 浏览器 / 前端单页应用内部私有服务器 / 后端微服务进程
客户端密钥 (Secret)无(认证方式为 none,禁止生成)有(服务端生成 32 位高强度密钥,仅创建时展示一次)
认证方式noneclient_secret_basic
OAuth 授权模式authorization_code (授权码) + refresh_tokenclient_credentials (客户端凭据)
防伪与防拦截机制强制开启 PKCE (S256)依赖高强度 Client Secret 进行私密比对
浏览器回调地址必须配置 精确匹配的 redirect_uris 和退出回调禁止配置 任何浏览器重定向地址
用户授权确认 (Consent)可选开启(是否在登录时提示用户授权确认)不适用(无用户交互)
代表的实体真实用户(User Principal)服务/系统自身(Machine Principal)
令牌生命周期Access Token (10m) + Refresh Token (7d 轮换)仅签发 Access Token (10m),无 Refresh Token
默认 / 典型 Scopeopenid 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 机制自动接管以下处理:

  1. 按需拉取公钥(HTTP GET): 应用启动并收到首个携带 Token 的请求时,Spring Boot 自动向 jwk-set-uri 发起一次 HTTP GET 请求,拉取 IAM 的公开公钥集合(包含 Key ID kid、加密算法、RSA 公钥模数等)。
  2. 本地内存缓存(Local Cache): 获取到的公钥常驻在业务服务进程的本地内存中。
  3. 极速本地验签(纯 CPU 运算): 随后的每一个业务请求:
    • 框架自动从请求头提取 Authorization: Bearer <token>;
    • 从 Token Header 获取密钥编号 kid,直接匹配本地内存中的对应公钥;
    • 使用公钥在本地完成 RSA 签名校验,并验证过期时间(exp)与签发者(iss)。
    • 整个过程耗时为微秒到亚毫秒级,完全不产生网络 I/O 开销。
  4. 证书轮换自动刷新(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 内置的本地公钥验签机制,兼顾安全性与微秒级响应性能。

相关指南 ​

继续查阅

遇到登录、权限或操作问题,可先查阅常见问题。

查看常见问题

EDMA · 企业数字化管理应用