我正在尝试在 Spring webflux 应用程序 (spring-cloud-gateway
) 上实现一个
Back-Channel Logout端点。
为此,我需要:
issuer
和 subject
的授权客户端,但没有用户会话的上下文目前的
ServerWebExchange
是由OIDC提供商自己发起的,由于不涉及用户浏览器,因此没有会话cookie。
相反,提供了一个“注销”JWT 作为请求正文(这是我找到
issuer
和 subject
的地方,它们应该足以识别要失效的会话和要删除的授权客户端)。
使用 Servlet,这个问题可以通过实施
HttpSessionListener, HttpSessionIdListener
(并使用 ServletListenerRegistrationBean
)来解决,每次添加或删除授权客户端时,issuer
和用户 subject
构建和维护会话索引。
不幸的是,
spring-cloud-gateway
是一个反应式应用程序,我找不到 WebSession
的等效会话侦听器。
关于我如何继续的任何线索?
一种可能的方法是覆盖默认
WebSessionManager
配置并使用手动创建的InMemoryWebSessionStore
bean设置会话存储。
@Configuration
public class SessionConfig {
@Bean
public WebSessionManager webSessionManager (WebSessionStore webSessionStore) {
DefaultWebSessionManager webSessionManager = new DefaultWebSessionManager();
webSessionManager.setSessionStore(webSessionStore);
return webSessionManager;
}
@Bean
public InMemoryWebSessionStore inMemoryWebSessionStore() {
return new InMemoryWebSessionStore();
}
}
然后在注销逻辑中我们可以注入
InMemoryWebSessionStore
bean,检索所有会话并执行注销:
@Service
public class SessionService {
private InMemoryWebSessionStore webSessionStore;
public SessionService(InMemoryWebSessionStore webSessionStore) {
this.webSessionStore = webSessionStore;
}
private Mono<Void> performLogout(String issuer, String subject) {
String sessionId = webSessionStore.getSessions()
.entrySet().stream()
.filter(e -> shouldBeRemoved(issuer, subject, e.getValue()))
.findFirst() // assuming issuer and subject combination is always unique
.map(Map.Entry::getKey)
.orElse("dummy-session-id");
return webSessionStore.removeSession(sessionId);
}
private boolean shouldBeRemoved(String issuer, String subject, WebSession session) {
OidcUser oidcUser = oidcUserFromSession(session);
return issuer.equals(oidcUser.getIssuer().toString()) && subject.equals(oidcUser.getSubject());
}
private OidcUser oidcUserFromSession(WebSession session) {
SecurityContext securityContext = session.getAttribute(WebSessionServerSecurityContextRepository
.DEFAULT_SPRING_SECURITY_CONTEXT_ATTR_NAME);
return (OidcUser) securityContext.getAuthentication().getPrincipal();
}
}
@elyorbek-ibrokhimov 的回答没有回答我的需要,即挂钩到会话生命周期,但它确实让我走上了正确的轨道。非常感谢他。这是我如何实现它的(灵感来自
MaxIdleTimeInMemoryWebSessionStore
):
@Bean
WebSessionManager webSessionManager(WebSessionStore webSessionStore) {
DefaultWebSessionManager webSessionManager = new DefaultWebSessionManager();
webSessionManager.setSessionStore(webSessionStore);
return webSessionManager;
}
@Bean
WebSessionStore webSessionStore(SpringAddonsOAuth2ClientProperties clientProperties) {
return new SpringAddonsWebSessionStore(Duration.ofHours(clientProperties.getSessionsDurationInHours()));
}
static class SpringAddonsWebSessionStore implements WebSessionStore {
private final InMemoryWebSessionStore delegate = new InMemoryWebSessionStore();
private final Duration timeout;
public SpringAddonsWebSessionStore(Duration timeout) {
this.timeout = timeout;
}
@Override
public Mono<WebSession> createWebSession() {
// TODO do my own stuff
return delegate.createWebSession().doOnSuccess(this::setMaxIdleTime);
}
@Override
public Mono<Void> removeSession(String sessionId) {
// TODO do my own stuff
return delegate.removeSession(sessionId);
}
@Override
public Mono<WebSession> retrieveSession(String sessionId) {
return delegate.retrieveSession(sessionId);
}
@Override
public Mono<WebSession> updateLastAccessTime(WebSession webSession) {
return delegate.updateLastAccessTime(webSession);
}
private void setMaxIdleTime(WebSession session) {
session.setMaxIdleTime(this.timeout);
}
}
此外,事实证明,只有当删除的授权客户端是用户的最后一个授权客户端时,后台通道注销才需要删除授权客户端并使会话无效:如果您使用同一客户端登录 Google 和 Facebook,则当您仅从两者之一注销时,此客户端上的会话应保持活动状态。