Spring 授权服务器:如何使用托管在单独应用程序上的登录表单?
Spring Authorization Server: How to use login form hosted on a separate application?
我正在使用 Spring 安全和 Spring 授权服务器并尝试创建一个授权服务器。
我有一个基本流程,允许我使用预建的登录页面登录(来自 baledung guide - 这是我正在处理的代码)。我假设此登录页面表单来自 formLogin()
,如下所示:
http.authorizeRequests(authorizeRequests ->
authorizeRequests.anyRequest().authenticated()
)
//.formLogin(withDefaults());
return http.build();
我不想使用这个预建表单,因为我需要完全独立地托管和运行登录表单前端应用程序。即在不同的服务器、域和代码库上。
问这个问题的另一种方式是:
- 如何禁用
authorization-server
中的内置表单,以便我可以将其与完全独立的表单一起使用?
- 是否有任何推荐的方法来了解如何按照这些思路自定义我的
SecurityFilterChain
?这是正确的地方吗?我发现 baledung 文章(以及类似的文章)作为起点很有帮助,但很少适用于更实际的用例。我有信心 Spring 安全性和 oauth2 库将允许我做我想做的事,但并不完全清楚。
关于您的评论:“我正在尝试构建授权服务器”:
编写您自己的授权服务器 (AS) 或必须自己构建其代码是非常不可取的,因为很容易陷入管道或犯安全错误。
尽管如此,请务必在您的应用程序中使用 Spring OAuth Security。在不进行额外工作的情况下,让这些按预期工作已经够难的了。
建议的方法
选择一个免费的 AS,运行 将其作为 Docker 容器,然后从您的应用程序连接到其端点。
如果您需要自定义登录,使用插件模型,编写少量代码,然后将一两个 JAR 文件部署到 Docker 容器。
这会让你很快起床 运行宁神。此外,由于 Spring 安全是基于标准的,您可以自由改变对提供商的看法,并推迟对最终提供商的决定。
实施示例
Curity 以及 Keycloak 或 Ory Hydra 等其他不错的选择都是 Java 并支持插件:
在与您讨论后,我了解到您要做的基本上是对通过另一个(单独托管的)登录页面(实际上是一个单独的系统)进行身份验证的用户进行预验证。这个想法是另一个系统会在查询参数中使用 signed JWT 重定向回来。
这实际上更像是一个联合登录问题,这正是 SAML 2.0 和 OAuth 2.0 旨在解决的问题。但是,如果您必须坚持使用签名的 JWT(类似于 SAML 断言)之类的东西,我们可以使用 Spring 授权服务器对一个相当简单的预验证 authorization_code
流进行建模。
注意:我还没有探索 JWT Profile for OAuth 2.0 Client Authentication and Authorization Grants but it could be a viable alternative. See this issue (#59) 的选项。
附加说明:下面概述的方法涉及许多安全注意事项。以下是该方法的草图。其他注意事项包括 CSRF 保护、使用表单 Post 响应模式(类似于 SAML 2.0)来保护访问令牌而不是查询参数、主动使访问令牌过期(2 分钟或更短时间)等。换句话说,在可能的情况下,始终建议使用 SAML 2.0 或 OAuth 2.0 等联合登录方法而不是这种方法。
您可以从现有的 Spring Authorization Server sample 开始,然后从那里发展它。
这是一种重定向到外部身份验证提供程序并在重定向返回时包含预身份验证机制的变体:
@Bean
@Order(1)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
// @formatter:off
http
.exceptionHandling(exceptionHandling -> exceptionHandling
.authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("https://some-other-sso.example/login"))
);
// @formatter:on
return http.build();
}
@Bean
@Order(2)
public SecurityFilterChain standardSecurityFilterChain(HttpSecurity http) throws Exception {
// @formatter:off
http
.authorizeRequests(authorize -> authorize
.anyRequest().authenticated()
)
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
// @formatter:on
return http.build();
}
@Bean
public JwtDecoder jwtDecoder(PublicKey publicKey) {
return NimbusJwtDecoder.withPublicKey((RSAPublicKey) publicKey).build();
}
@Bean
public BearerTokenResolver bearerTokenResolver() {
DefaultBearerTokenResolver bearerTokenResolver = new DefaultBearerTokenResolver();
bearerTokenResolver.setAllowUriQueryParameter(true);
return bearerTokenResolver;
}
第一个过滤器链在授权服务器端点上运行,例如 /oauth2/authorize
、/oauth2/token
等。请注意,/oauth2/authorize
端点需要经过身份验证的用户才能运行,这意味着如果调用端点,必须对用户进行身份验证,否则将调用身份验证入口点,从而重定向到外部提供程序。另请注意,双方之间必须存在信任关系,因为我们没有将 OAuth 用于外部 SSO。
当来自 oauth 客户端的重定向到达 /oauth2/authorize?...
端点时,请求由 Spring 安全缓存,以便稍后重播(请参阅下面的控制器)。
第二个过滤器链使用签名的 JWT 对用户进行身份验证。它还包括一个自定义的 BearerTokenResolver
,它从 URL (?access_token=...
).
中的查询参数中读取 JWT
注入 JwtDecoder
的 PublicKey
将来自外部 SSO 提供商,因此您可以插入它,但它对您的设置有意义。
我们可以创建一个存根身份验证端点,将已签名的 JWT 转换为授权服务器上经过身份验证的会话,如下所示:
@Controller
public class SsoController {
private AuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler();
@GetMapping("/login")
public void login(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws ServletException, IOException {
this.successHandler.onAuthenticationSuccess(request, response, authentication);
}
}
.oauth2ResourceServer()
DSL 会导致在调用 /login
端点时对用户进行身份验证。它需要一个 access_token
参数(由 BearerTokenResolver
使用)通过验证已签名的 JWT 作为用户已通过外部身份验证的断言来预验证用户。此时,将创建一个会话,该会话将验证此浏览器的所有未来请求。
然后调用控制器,并使用 SavedRequestAwareAuthenticationSuccessHandler
简单地重定向回真正的授权端点,这将愉快地启动 authorization_code
流程。
我正在使用 Spring 安全和 Spring 授权服务器并尝试创建一个授权服务器。
我有一个基本流程,允许我使用预建的登录页面登录(来自 baledung guide - 这是我正在处理的代码)。我假设此登录页面表单来自 formLogin()
,如下所示:
http.authorizeRequests(authorizeRequests ->
authorizeRequests.anyRequest().authenticated()
)
//.formLogin(withDefaults());
return http.build();
我不想使用这个预建表单,因为我需要完全独立地托管和运行登录表单前端应用程序。即在不同的服务器、域和代码库上。
问这个问题的另一种方式是:
- 如何禁用
authorization-server
中的内置表单,以便我可以将其与完全独立的表单一起使用? - 是否有任何推荐的方法来了解如何按照这些思路自定义我的
SecurityFilterChain
?这是正确的地方吗?我发现 baledung 文章(以及类似的文章)作为起点很有帮助,但很少适用于更实际的用例。我有信心 Spring 安全性和 oauth2 库将允许我做我想做的事,但并不完全清楚。
关于您的评论:“我正在尝试构建授权服务器”:
编写您自己的授权服务器 (AS) 或必须自己构建其代码是非常不可取的,因为很容易陷入管道或犯安全错误。
尽管如此,请务必在您的应用程序中使用 Spring OAuth Security。在不进行额外工作的情况下,让这些按预期工作已经够难的了。
建议的方法
选择一个免费的 AS,运行 将其作为 Docker 容器,然后从您的应用程序连接到其端点。
如果您需要自定义登录,使用插件模型,编写少量代码,然后将一两个 JAR 文件部署到 Docker 容器。
这会让你很快起床 运行宁神。此外,由于 Spring 安全是基于标准的,您可以自由改变对提供商的看法,并推迟对最终提供商的决定。
实施示例
Curity 以及 Keycloak 或 Ory Hydra 等其他不错的选择都是 Java 并支持插件:
在与您讨论后,我了解到您要做的基本上是对通过另一个(单独托管的)登录页面(实际上是一个单独的系统)进行身份验证的用户进行预验证。这个想法是另一个系统会在查询参数中使用 signed JWT 重定向回来。
这实际上更像是一个联合登录问题,这正是 SAML 2.0 和 OAuth 2.0 旨在解决的问题。但是,如果您必须坚持使用签名的 JWT(类似于 SAML 断言)之类的东西,我们可以使用 Spring 授权服务器对一个相当简单的预验证 authorization_code
流进行建模。
注意:我还没有探索 JWT Profile for OAuth 2.0 Client Authentication and Authorization Grants but it could be a viable alternative. See this issue (#59) 的选项。
附加说明:下面概述的方法涉及许多安全注意事项。以下是该方法的草图。其他注意事项包括 CSRF 保护、使用表单 Post 响应模式(类似于 SAML 2.0)来保护访问令牌而不是查询参数、主动使访问令牌过期(2 分钟或更短时间)等。换句话说,在可能的情况下,始终建议使用 SAML 2.0 或 OAuth 2.0 等联合登录方法而不是这种方法。
您可以从现有的 Spring Authorization Server sample 开始,然后从那里发展它。
这是一种重定向到外部身份验证提供程序并在重定向返回时包含预身份验证机制的变体:
@Bean
@Order(1)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
// @formatter:off
http
.exceptionHandling(exceptionHandling -> exceptionHandling
.authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("https://some-other-sso.example/login"))
);
// @formatter:on
return http.build();
}
@Bean
@Order(2)
public SecurityFilterChain standardSecurityFilterChain(HttpSecurity http) throws Exception {
// @formatter:off
http
.authorizeRequests(authorize -> authorize
.anyRequest().authenticated()
)
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
// @formatter:on
return http.build();
}
@Bean
public JwtDecoder jwtDecoder(PublicKey publicKey) {
return NimbusJwtDecoder.withPublicKey((RSAPublicKey) publicKey).build();
}
@Bean
public BearerTokenResolver bearerTokenResolver() {
DefaultBearerTokenResolver bearerTokenResolver = new DefaultBearerTokenResolver();
bearerTokenResolver.setAllowUriQueryParameter(true);
return bearerTokenResolver;
}
第一个过滤器链在授权服务器端点上运行,例如 /oauth2/authorize
、/oauth2/token
等。请注意,/oauth2/authorize
端点需要经过身份验证的用户才能运行,这意味着如果调用端点,必须对用户进行身份验证,否则将调用身份验证入口点,从而重定向到外部提供程序。另请注意,双方之间必须存在信任关系,因为我们没有将 OAuth 用于外部 SSO。
当来自 oauth 客户端的重定向到达 /oauth2/authorize?...
端点时,请求由 Spring 安全缓存,以便稍后重播(请参阅下面的控制器)。
第二个过滤器链使用签名的 JWT 对用户进行身份验证。它还包括一个自定义的 BearerTokenResolver
,它从 URL (?access_token=...
).
注入 JwtDecoder
的 PublicKey
将来自外部 SSO 提供商,因此您可以插入它,但它对您的设置有意义。
我们可以创建一个存根身份验证端点,将已签名的 JWT 转换为授权服务器上经过身份验证的会话,如下所示:
@Controller
public class SsoController {
private AuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler();
@GetMapping("/login")
public void login(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws ServletException, IOException {
this.successHandler.onAuthenticationSuccess(request, response, authentication);
}
}
.oauth2ResourceServer()
DSL 会导致在调用 /login
端点时对用户进行身份验证。它需要一个 access_token
参数(由 BearerTokenResolver
使用)通过验证已签名的 JWT 作为用户已通过外部身份验证的断言来预验证用户。此时,将创建一个会话,该会话将验证此浏览器的所有未来请求。
然后调用控制器,并使用 SavedRequestAwareAuthenticationSuccessHandler
简单地重定向回真正的授权端点,这将愉快地启动 authorization_code
流程。