OIDC 登出
一旦终端用户能够登录您的应用程序,考虑他们如何退出登录就变得至关重要。
一般来说,有三种使用场景供您考虑:
-
我只想执行本地登出
-
我想登出我的应用程序和 OIDC 提供商,由我的应用程序发起
-
我想登出我的应用程序和 OIDC 提供商,由 OIDC 提供商发起
本地登出
要执行本地登出,无需特殊的 OIDC 配置。Spring Security 会自动启用一个本地登出端点,你可以通过 logout() DSL 进行配置。
OpenID Connect 1.0 客户端发起的注销
OpenID Connect Session Management 1.0 允许客户端(Client)在提供方(Provider)处注销终端用户。其中一种可用的策略是 RP-Initiated Logout。
spring:
security:
oauth2:
client:
registration:
okta:
client-id: okta-client-id
client-secret: okta-client-secret
...
provider:
okta:
issuer-uri: https://dev-1234.oktapreview.com
此外,您应该配置 OidcClientInitiatedLogoutSuccessHandler(该处理器实现了 RP 发起的注销功能),具体配置如下:
- Java
- Kotlin
@Configuration
@EnableWebSecurity
public class OAuth2LoginSecurityConfig {
@Autowired
private ClientRegistrationRepository clientRegistrationRepository;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
.anyRequest().authenticated()
)
.oauth2Login(withDefaults())
.logout((logout) -> logout
.logoutSuccessHandler(oidcLogoutSuccessHandler())
);
return http.build();
}
private LogoutSuccessHandler oidcLogoutSuccessHandler() {
OidcClientInitiatedLogoutSuccessHandler oidcLogoutSuccessHandler =
new OidcClientInitiatedLogoutSuccessHandler(this.clientRegistrationRepository);
// Sets the location that the End-User's User Agent will be redirected to
// after the logout has been performed at the Provider
oidcLogoutSuccessHandler.setPostLogoutRedirectUri("{baseUrl}");
return oidcLogoutSuccessHandler;
}
}
@Configuration
@EnableWebSecurity
class OAuth2LoginSecurityConfig {
@Autowired
private lateinit var clientRegistrationRepository: ClientRegistrationRepository
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
authorizeHttpRequests {
authorize(anyRequest, authenticated)
}
oauth2Login { }
logout {
logoutSuccessHandler = oidcLogoutSuccessHandler()
}
}
return http.build()
}
private fun oidcLogoutSuccessHandler(): LogoutSuccessHandler {
val oidcLogoutSuccessHandler = OidcClientInitiatedLogoutSuccessHandler(clientRegistrationRepository)
// Sets the location that the End-User's User Agent will be redirected to
// after the logout has been performed at the Provider
oidcLogoutSuccessHandler.setPostLogoutRedirectUri("{baseUrl}")
return oidcLogoutSuccessHandler
}
}
OidcClientInitiatedLogoutSuccessHandler 支持 {baseUrl} 占位符。如果使用该占位符,应用程序的基础 URL(例如 [app.example.org](https://app.example.org))将在请求时替换它。
默认情况下,OidcClientInitiatedLogoutSuccessHandler 使用标准的 HTTP 重定向和 GET 方法重定向到注销 URL。要使用 POST 请求执行注销,请将重定向策略设置为 FormPostRedirectStrategy,例如使用 OidcClientInitiatedLogoutSuccessHandler.setRedirectStrategy(new FormPostRedirectStrategy())。
OpenID Connect 1.0 后端通道注销
OpenID Connect Session Management 1.0 允许通过提供者向客户端发起 API 调用来注销终端用户。这被称为 OIDC 后端通道注销。
要启用此功能,你可以在DSL中像这样设置Back-Channel Logout端点:
- Java
- Kotlin
@Bean
OidcBackChannelLogoutHandler oidcLogoutHandler() {
return new OidcBackChannelLogoutHandler();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
.anyRequest().authenticated()
)
.oauth2Login(withDefaults())
.oidcLogout((logout) -> logout
.backChannel(Customizer.withDefaults())
);
return http.build();
}
@Bean
fun oidcLogoutHandler(): OidcBackChannelLogoutHandler {
return OidcBackChannelLogoutHandler()
}
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
authorizeHttpRequests {
authorize(anyRequest, authenticated)
}
oauth2Login { }
oidcLogout {
backChannel { }
}
}
return http.build()
}
接着,你需要一种方法来监听 Spring Security 发布的事件,以移除旧的 OidcSessionInformation 条目,如下所示:
- Java
- Kotlin
@Bean
public HttpSessionEventPublisher sessionEventPublisher() {
return new HttpSessionEventPublisher();
}
@Bean
open fun sessionEventPublisher(): HttpSessionEventPublisher {
return HttpSessionEventPublisher()
}
这将确保当调用 HttpSession#invalidate 时,会话也会从内存中移除。
就是这样!
这将启动端点 /logout/connect/back-channel/{registrationId},OIDC 提供者可以通过该端点请求使您应用程序中指定终端用户的会话失效。
oidcLogout 要求同时配置 oauth2Login。
oidcLogout 要求会话 cookie 必须命名为 JSESSIONID,以便通过后端通道正确注销每个会话。
后端通道登出架构
考虑一个标识符为 registrationId 的 ClientRegistration。
Back-Channel 注销的整体流程如下:
-
在登录时,Spring Security 会通过其
OidcSessionRegistry实现,将 ID Token、CSRF Token 以及 Provider Session ID(如果有)与您应用程序的会话 ID 关联起来。 -
然后在登出时,您的 OIDC 提供商会向
/logout/connect/back-channel/registrationId发起一个 API 调用,其中包含一个 Logout Token,该令牌指示要登出的sub(终端用户)或sid(提供商会话 ID)。 -
Spring Security 会验证令牌的签名和声明。
-
如果令牌包含
sid声明,则仅终止与该提供商会话关联的客户端会话。 -
否则,如果令牌包含
sub声明,则终止该终端用户的所有客户端会话。
请记住,Spring Security 的 OIDC 支持是多租户的。这意味着它只会终止那些客户端与注销令牌中 aud 声明匹配的会话。
该架构实现中一个值得注意的部分是,它会为每个对应的会话在内部传播传入的后端通道请求。起初,这可能看起来没有必要。然而,回想一下,Servlet API 并不直接提供对 HttpSession 存储的访问。通过进行内部注销调用,现在可以使相应的会话失效。
此外,在内部伪造登出调用,使得每一组 LogoutHandler 都能针对该会话及相应的 SecurityContext 执行。
自定义会话登出端点
随着 OidcBackChannelLogoutHandler 的发布,会话注销端点为 {baseUrl}/logout/connect/back-channel/{registrationId}。
如果 OidcBackChannelLogoutHandler 未被装配,则 URL 为 {baseUrl}/logout/connect/back-channel/{registrationId},此方式不推荐使用,因为它需要传递 CSRF 令牌,而根据应用程序所使用的存储库类型,这可能带来挑战。
如果您需要自定义端点,可以按如下方式提供 URL:
- Java
- Kotlin
http
// ...
.oidcLogout((oidc) -> oidc
.backChannel((backChannel) -> backChannel
.logoutUri("http://localhost:9000/logout/connect/back-channel/+{registrationId}+")
)
);
http {
oidcLogout {
backChannel {
logoutUri = "http://localhost:9000/logout/connect/back-channel/+{registrationId}+"
}
}
}
自定义会话登出 Cookie 名称
默认情况下,会话注销端点使用 JSESSIONID cookie 来将会话与对应的 OidcSessionInformation 关联起来。
然而,Spring Session 中的默认 cookie 名称是 SESSION。
你可以像这样在DSL中配置Spring Session的cookie名称:
- Java
- Kotlin
@Bean
OidcBackChannelLogoutHandler oidcLogoutHandler(OidcSessionRegistry oidcSessionRegistry) {
OidcBackChannelLogoutHandler logoutHandler = new OidcBackChannelLogoutHandler(oidcSessionRegistry);
logoutHandler.setSessionCookieName("SESSION");
return logoutHandler;
}
@Bean
open fun oidcLogoutHandler(val sessionRegistry: OidcSessionRegistry): OidcBackChannelLogoutHandler {
val logoutHandler = OidcBackChannelLogoutHandler(sessionRegistry)
logoutHandler.setSessionCookieName("SESSION")
return logoutHandler
}
自定义 OIDC 提供者会话注册表
默认情况下,Spring Security 会在内存中存储 OIDC 提供者会话与客户端会话之间的所有关联。
在某些情况下,比如集群应用,最好将这些数据存储在单独的位置,比如数据库中。
你可以通过配置一个自定义的 OidcSessionRegistry 来实现这一点,如下所示:
- Java
- Kotlin
@Component
public final class MySpringDataOidcSessionRegistry implements OidcSessionRegistry {
private final OidcProviderSessionRepository sessions;
// ...
@Override
public void saveSessionInformation(OidcSessionInformation info) {
this.sessions.save(info);
}
@Override
public OidcSessionInformation removeSessionInformation(String clientSessionId) {
return this.sessions.removeByClientSessionId(clientSessionId);
}
@Override
public Iterable<OidcSessionInformation> removeSessionInformation(OidcLogoutToken token) {
return token.getSessionId() != null ?
this.sessions.removeBySessionIdAndIssuerAndAudience(...) :
this.sessions.removeBySubjectAndIssuerAndAudience(...);
}
}
@Component
class MySpringDataOidcSessionRegistry: OidcSessionRegistry {
val sessions: OidcProviderSessionRepository
// ...
@Override
fun saveSessionInformation(info: OidcSessionInformation) {
this.sessions.save(info)
}
@Override
fun removeSessionInformation(clientSessionId: String): OidcSessionInformation {
return this.sessions.removeByClientSessionId(clientSessionId);
}
@Override
fun removeSessionInformation(token: OidcLogoutToken): Iterable<OidcSessionInformation> {
return token.getSessionId() != null ?
this.sessions.removeBySessionIdAndIssuerAndAudience(...) :
this.sessions.removeBySubjectAndIssuerAndAudience(...);
}
}