如何使用OpenID Connect和pac4j保护Spring Boot应用
pac4j 是一个开源的 Java 安全库,用于身份认证和授权。它支持 OpenID Connect、OAuth 2.0、SAML 和 CAS 等协议,并可与多种 Java Web 框架集成。我是 Jérôme LELEU,pac4j 的维护者。在本教程中,我们将把它提供的 OpenID Connect 客户端与 Spring Boot 结合使用。
你希望用户使用 Keycloak、Google、Microsoft Entra ID、Okta、Auth0 甚至 CAS 服务器登录你的 Java 应用。用户在这些平台上已经拥有账号,因此你的应用不应再向他们索取另一个密码。
这正是 OpenID Connect(OIDC)的用途。它在 OAuth 2.0 之上增加了一个身份层:由提供方对用户进行身份认证,你的应用会收到一个 ID token,即一个描述登录者身份的 JWT,通常由提供方签名。用户的密码始终保留在提供方那里。
下面我们使用 pac4j 与 Spring Boot 来实践这一流程。我们将先从公共的 pac4j 测试服务器开始,然后看看针对 Keycloak、Google 或 Azure AD 需要做哪些改动。同样的方法也适用于其他 OpenID Connect 提供方。
你需要准备:
- Java 17+ 和 Maven
- 一个可以注册应用的 OpenID Connect 提供方,或者使用下面用到的公共演示服务器。
1)创建 Maven 项目
创建一个使用 Java 17 的空 Spring Boot 项目:
在项目根目录创建 pom.xml 文件。Spring Boot parent 负责管理 Spring 依赖,其 Maven 插件负责运行应用:
将下面的 Java 类保存在 org.example 包中,这样 Spring Boot 才能发现配置类和控制器。将应用设置放在 src/main/resources/application.yml 中。
2)添加 Maven 依赖
在 <dependencies> 元素中,除了 Spring MVC 之外,还需要两个 pac4j 构件:Spring MVC 集成模块和 OpenID Connect 模块。
为什么需要两个依赖?pac4j-oidc 负责处理协议,它构建在 Nimbus OAuth 2.0 / OpenID Connect SDK 之上;而 spring-webmvc-pac4j 负责将其连接到 Spring MVC。OidcClient 本身对 Spring 一无所知:你同样可以将它与 Jakarta EE、Play、Vert.x、JAX-RS 以及其他 pac4j 集成 一起使用。
3)配置 OpenID Connect(OIDC)认证
创建 src/main/java/org/example/SecurityConfig.java:
仅用于演示:未签名的 ID token
公共的 pac4j 演示服务器签发的是 未签名的 ID token。因此,下面的示例使用
setAllowUnsignedIdTokens(true)仅仅是为了与该服务器配合使用。请勿在生产环境的提供方上启用此选项。 切换到真实提供方时请移除该调用,以便 pac4j 使用提供方的 JWKS 验证 ID token 的签名。演示用的 client ID 和 secret 是公开的测试凭据。
这里需要配置两件事:OIDC 客户端,以及我们应用的 URL。
对于客户端,setDiscoveryURI 指向提供方的 .well-known/openid-configuration 文档。pac4j 会从中读取授权、令牌、用户信息和 JWKS 端点。因此,你无需自行配置每个端点。setClientId 和 setSecret 是你在提供方注册应用后获得的凭据。
在应用一侧,Pac4jSecurityConfig 会注册 /callback 和 /logout。回调 URL 来自 new Config(baseUri + "/callback", ...),pac4j 会在其后追加 ?client_name=OidcClient。这个完整的 URL 就是需要在提供方注册的重定向 URI。
最后,addSecurity(registry, "OidcClient") 保护 /protected/**。匿名用户请求这些页面时会被重定向到提供方进行登录。
默认情况下,pac4j 使用 授权码流程,并在提供方于其发现文档中声明支持时添加 PKCE。
从演示服务器切换到自己的提供方时,请按照上面的警告说明移除 setAllowUnsignedIdTokens(true)。
4)在身份提供方注册应用
每个提供方都需要相同的三样东西:一个应用(或“客户端”),其 重定向 URI 为 http://localhost:8080/callback?client_name=OidcClient,以及由此得到的 client ID 和 secret。不同之处在于发现文档所在的位置,而 pac4j 为常见提供方提供了专用的客户端。
对于 Keycloak:在你的 realm 中创建一个机密客户端,并设置其“Valid redirect URIs”。KeycloakOidcConfiguration 会根据服务器基础 URL 和 realm 名称构建发现 URL:
对于 Google:在 Google Cloud 控制台中创建一个类型为“Web application”的“OAuth client ID”,并将重定向 URI 添加到“Authorized redirect URIs”。GoogleOidcClient 已经知道 Google 的发现 URL:
对于 Microsoft Entra ID(Azure AD):在 Entra 门户中注册一个应用,在“Web”平台下添加重定向 URI,并创建一个客户端密钥。AzureAd2Client 接收租户标识符:
对于 Okta、Auth0、CAS 服务器或任何其他提供方,只要使用带有该提供方发现 URL 的通用 OidcClient 即可。将重定向 URI 中的 client_name=OidcClient 替换为你所用客户端的名称(KeycloakOidcClient、GoogleOidcClient、AzureAd2Client)。
同时也要更新安全拦截器: 传给 addSecurity 的名称必须与配置的客户端一致。例如,使用 KeycloakOidcClient 时,请将第 3 步中的拦截器配置替换为:
对于相应的提供方,请在这两处都使用 GoogleOidcClient 或 AzureAd2Client。
5)访问已认证的用户
创建 src/main/java/org/example/Application.java,对外暴露一个公共页面和一个受保护页面:
现在用户已经通过身份认证,我们该如何获取他们的信息?ProfileManager 会读取保存在会话中的 profile。对于 OIDC,这是一个 OidcProfile,它提供了标准声明的 getter 方法,并可以访问原始令牌:
具体存在哪些声明取决于你请求的 scope。默认是 openid profile email;可以使用 config.setScope("openid profile email phone") 添加更多。
6)登出
/logout 链接会从应用会话中移除 profile。这是 本地登出:用户可能仍在提供方处于登录状态。
若要同时结束提供方那边的会话,请在 application.yml 中启用集中式登出:
对于支持 OIDC 登出的提供方,pac4j 会将浏览器重定向到发现文档中的 end_session_endpoint,并将绝对默认 URL 作为 post_logout_redirect_uri 提供。请在提供方处将 http://localhost:8080/ 注册为允许的登出后重定向 URI。像 / 这样的相对默认 URL 适用于本地登出,但不会作为返回 URL 传给提供方。
如果你的提供方支持 OIDC 登出但没有公布其端点,可以使用 config.setLogoutUrl(...) 显式设置它。登出支持情况和注册要求因提供方而异;专用的 GoogleOidcClient 使用它自己的登出动作,而不是这种基于发现文档的流程。
7)运行应用
创建 src/main/java/org/example/SpringBootDemo.java:
使用 mvn spring-boot:run 启动应用。
打开 http://localhost:8080/ 并点击 Protected area。你将被重定向到身份提供方进行登录,随后返回受保护页面,控制器会打印你的 profile。
如果出现问题:
-
提供方报 “Invalid redirect URI” 或 “redirect_uri_mismatch”:注册的 URI 必须是完整的回调 URL,包括
?client_name=OidcClient(或第 4 步中配置的专用客户端名称)。 -
令牌请求报 “invalid_client”:客户端密钥错误,或者该客户端在提供方处是公共客户端,而 pac4j 发送了密钥。
-
ID token 签名验证失败:检查发现文档是否指向预期的提供方,其 JWKS 是否可访问并包含当前的签名密钥,以及预期的签名算法是否与提供方客户端配置匹配。如果需要显式指定算法,请使用
config.setIdTokenSigningAlgorithm(...)传入提供方所配置的算法;将其改为ES256并不是解决签名错误的通用方法。 -
姓名为空或邮箱为空:scope 或提供方的声明映射没有释放这些信息。请先检查 scope,再检查提供方处的客户端配置。## 8) 了解更多
-
Spring Boot OIDC 演示:完整的参考应用。
-
Java 的 OIDC 客户端文档:所有
OidcConfiguration选项、隐式流、private_key_jwt客户端认证、nonce 与 state 处理。 -
直接 OIDC 认证:使用提供商签发的访问令牌保护 REST API。
-
当你的应用属于某个信任联盟时,可使用 OpenID Federation。
改编自 如何使用 OIDC 保护 Java 客户端应用(基于 Spring Boot),原文最初发布于 pac4j.org。
[LOADING...]
作者
Jérôme LELEU
Jérôme LELEU 是 pac4j 的创建者和维护者。pac4j 是一个开源 Java 安全库,用于跨多种 Web 框架进行身份认证和授权。