背景
在之前的微服务实战:基于Spring Cloud Gateway + AWS Cognito 的BFF案例一文中,我们使用 Spring Security 和 AWS Cognito 为 Spring Cloud Gateway 应用配置了OAuth2 登录。本文将重点介绍注销(登出)功能。
本文基于 Thymeleaf OAuth2 Login with Spring Security 和 AWS Cognito 之上。请务必先通读相关的文章,先对示例代码以及 Spring Security 和 Cognito 有一个基本的了解。
如何管理会话?
在 Gateway 中,我们使用 Redis 管理会话,不能使用本地内存,因为在云环境中,Gateway可能会存在多个实例,会话数据需要保存在外部存储中。我们先在build.gradle
依赖中加入:
// for session
implementation "org.springframework.session:spring-session-data-redis"
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
然后在application.yaml
中加入如下配置:
spring:
session:
store-type: redis # Defines where the session is stored JVM or redis
redis:
flush-mode: immediate #Tells spring to flush the session data immediately into redis
redis:
host: localhost
port: 6379
在登录之后就可以在Redis中查看到保存的Session信息了:
> redis-cli -h localhost -p 6379
localhost:6379> keys spring*
1) "spring:session:sessions:078cfd0a-805c-4608-ad50-27101747fe45"
localhost:6379> HGETALL spring:session:sessions:078cfd0a-805c-4608-ad50-27101747fe45
// ...
使用 OpenID Connect 和 Spring Security,我们的用户将有两个会话:一个特定于应用程序(即Gateway),另一个用于身份提供者(即Cognito)。每当用户注销时,Spring Security(默认情况下)将使第一个会话无效,但我们的用户仍将在身份提供者处仍然处于登录状态。
这样的话,后续在Cognito的登录尝试都会自动登录到我们的应用程序中,而无需用户提供任何凭据。这不是我们想看到的,我们希望完全注销用户,以便他们可以轻松退出应用或者切换帐户。
Spring Security 不支持注销?
当然是支持的。
OIDC 规范定义了客户端如何在身份提供者处执行注销:OpenID Connect RP-Initiated Logout 1.0(目前处于起草状态)。
Spring Security只能注销支持 OpenID Connect RP-Initiated Logout 1.0 的身份提供者,比如Keycloak。对于这类Id Provider,只需要使用OidcClientInitiatedServerLogoutSuccessHandler
就可以实现注销。
@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http,
ReactiveClientRegistrationRepository clientRegistrationRepository,
MyAuthorizationManager authorizationManager,
MyOAuth2AuthorizationRequestResolver oAuth2AuthorizationRequestResolver) {
// ...
// Also logout at the OpenID Connect provider
http.logout(logout -> logout.logoutSuccessHandler(new OidcClientInitiatedServerLogoutSuccessHandler(
clientRegistrationRepository)));
// ...
return http.build();
}
不幸的是,AWS Cognito目前还不支持 OpenID Connect RP-Initiated Logout 1.0,不过它提供了一个注销机制。
Cognito 如何注销?
AWS Cognito定义了一个LOGOUT端点,参数如下。具体规格可以参考AWS Cognito LOGOUT endpoint。
GET https://<DOMAIN_PREFIX>.auth.<AWS_REGION>.amazoncognito.com/logout?
response_type=code&
client_id=<YOUR_CLIENT_ID>&
redirect_uri=https://YOUR_APP/redirect_uri&
state=<STATE>&
scope=openid
让 Spring Security 支持Cognito
基于 Spring Security 的强大的可扩展性,我们可以定义自己的 LogoutHandler。 Spring Security 将调用这个自定义的处理程序,作为其自身注销过程的一部分(使Gateway HTTP 会话无效并清除 SecurityContextHolder)。
默认情况下,注销的端点为 /logout
,我们不需要为这个端点开发Controller(Spring Security已经实现了)。
由于 AWS Cognito 需要 HTTPS 注销请求,我们可以仿照OidcClientInitiatedServerLogoutSuccessHandler
的实现,写一个自己的LogoutHandler。
public class CognitoOidcLogoutSuccessHandler implements ServerLogoutSuccessHandler {
private final String logoutUrl;
private final ReactiveClientRegistrationRepository clientRegistrationRepository;
private final RedirectServerLogoutSuccessHandler serverLogoutSuccessHandler =
new RedirectServerLogoutSuccessHandler();
private final ServerRedirectStrategy redirectStrategy = new DefaultServerRedirectStrategy();
public CognitoOidcLogoutSuccessHandler(String logoutUrl,
ReactiveClientRegistrationRepository clientRegistrationRepository) {
Assert.notNull(logoutUrl, "logoutUrl cannot be null");
Assert.notNull(clientRegistrationRepository, "clientRegistrationRepository cannot be null");
this.logoutUrl = logoutUrl;
this.clientRegistrationRepository = clientRegistrationRepository;
}
@Override
public Mono<Void> onLogoutSuccess(WebFilterExchange exchange, Authentication authentication) {
return Mono.just(authentication)
.filter(OAuth2AuthenticationToken.class::isInstance)
.filter((token) -> authentication.getPrincipal() instanceof OidcUser)
.map(OAuth2AuthenticationToken.class::cast)
.map(OAuth2AuthenticationToken::getAuthorizedClientRegistrationId)
.flatMap(this.clientRegistrationRepository::findByRegistrationId)
.flatMap((clientRegistration) -> {
String clientId = clientRegistration.getClientId();
Set<String> scopes = clientRegistration.getScopes();
URI postLogoutRedirectUri = postLogoutRedirectUri(exchange.getExchange().getRequest(), clientId, scopes);
return Mono.just(postLogoutRedirectUri);
})
.switchIfEmpty(
this.serverLogoutSuccessHandler.onLogoutSuccess(exchange, authentication).then(Mono.empty())
)
.flatMap((endpointUri) -> this.redirectStrategy.sendRedirect(exchange.getExchange(), endpointUri));
}
private URI postLogoutRedirectUri(ServerHttpRequest request, String clientId, Set<String> scopes) {
if (this.logoutUrl == null) {
return null;
}
UriComponents baseUrl = UriComponentsBuilder
.fromUri(request.getURI())
.replacePath(request.getPath().contextPath().value())
.replaceQuery(null)
.fragment(null)
.build();
return UriComponentsBuilder
.fromUri(URI.create(logoutUrl))
.queryParam("client_id", clientId)
.queryParam("logout_uri", baseUrl)
.queryParam("redirect_uri", baseUrl)
.queryParam("response_type", "code")
.queryParam("scope", String.join("+", scopes))
.encode(StandardCharsets.UTF_8)
.build()
.toUri();
}
}
这个实现需要 logoutUrl
作为参数。注销之后将用户重定向到我们应用程序的baseURL
。
最简单的方法是将它们配置到 application.yaml
,并用@Value
注入logoutUrl
。
cognito:
logoutUrl: https://sc-gateway-sample.auth.ap-northeast-1.amazoncognito.com/logout
剩下的工作就是在SecurityConfig
中配置我们自定义的CognitoOidcLogoutSuccessHandler
了。
@Value("${cognito.logoutUrl}")
String logoutUrl;
@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http,
ReactiveClientRegistrationRepository clientRegistrationRepository,
MyAuthorizationManager authorizationManager,
MyOAuth2AuthorizationRequestResolver oAuth2AuthorizationRequestResolver) {
// ...
// Also logout at the OpenID Connect provider
http.logout(l```
logout -> logout.logoutSuccessHandler(new CognitoOidcLogoutSuccessHandler(
logoutUrl,
clientRegistrationRepository)));
// ...
return http.build();
}
这样,就可以一键同时注销两个会话了。
访问令牌失效后需要重新登录吗?
不需要的。
如果Refresh Token
没有失效的话,Spring Security会自动更新Access Token
,是不是很神奇!TokenRelayFilter
会将Access Token
传递给后端API。但需要注意的是,在Gateway的会话 SPRING_SECURITY_CONTEXT
中保存的ID Token
并不会自动刷新。
总结
本文,介绍了如何使用Spring Security注销AWS Cognito,并给出了实现代码。
如果对你有所帮助,请点赞订阅分享,感谢!
相关文章
- 微服务实战:基于Spring Cloud Gateway + AWS Cognito 的BFF案例
- 微服务实战:如何测试基于OAuth认证的微服务
- 你的微服务在用 OAuth2 却不知道 CSRF 和 PKCE ?