版本
spring-security-oauth2-authorization-server:1.2.1
场景
spring authorization server OIDC协议,支持处理依赖方(客户端)发起的登出请求,注销授权服务器端的会话
流程: 客户端登出成功->跳转到授权服务端OIDC登出端点->授权服务端注销会话->跳转回客户端(可选)
源码
- OIDC 登出端点配置器 org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OidcLogoutEndpointConfigurer
- OIDC 登出请求端点过滤器 org.springframework.security.oauth2.server.authorization.oidc.web.OidcLogoutEndpointFilter
public final class OidcLogoutEndpointFilter extends OncePerRequestFilter {
// 默认的端点地址,用于处理OIDC依赖方发起的登出请求
private static final String DEFAULT_OIDC_LOGOUT_ENDPOINT_URI = "/connect/logout";
...
// 登出处理器,实现为SecurityContextLogoutHandler
private final LogoutHandler logoutHandler;
...
// 认证处请求转换器,默认实现为OidcLogoutAuthenticationConverter
private AuthenticationConverter authenticationConverter;
// 登出请求成功处理器
private AuthenticationSuccessHandler authenticationSuccessHandler = this::performLogout;
// 登出请求失败处理器
private AuthenticationFailureHandler authenticationFailureHandler = this::sendErrorResponse;
...
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
if (!this.logoutEndpointMatcher.matches(request)) {
filterChain.doFilter(request, response);
return;
}
try {
// 获取OIDC登出请求信息,OidcLogoutAuthenticationToken
Authentication oidcLogoutAuthentication = this.authenticationConverter.convert(request);
// 对请求信息进行认证处理
Authentication oidcLogoutAuthenticationResult =
this.authenticationManager.authenticate(oidcLogoutAuthentication);
// 进行登出处理
this.authenticationSuccessHandler.onAuthenticationSuccess(request, response, oidcLogoutAuthenticationResult);
} catch (OAuth2AuthenticationException ex) {
if (this.logger.isTraceEnabled()) {
this.logger.trace(LogMessage.format("Logout request failed: %s", ex.getError()), ex);
}
this.authenticationFailureHandler.onAuthenticationFailure(request, response, ex);
} catch (Exception ex) {
...
// 发送失败响应
this.authenticationFailureHandler.onAuthenticationFailure(request, response,
new OAuth2AuthenticationException(error));
}
}
...
// 执行登出
private void performLogout(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
OidcLogoutAuthenticationToken oidcLogoutAuthentication = (OidcLogoutAuthenticationToken) authentication;
// 检查激活的用户会话
if (oidcLogoutAuthentication.isPrincipalAuthenticated() &&
StringUtils.hasText(oidcLogoutAuthentication.getSessionId())) {
// 执行登出 (清除安全上下文,废弃当前会话)
this.logoutHandler.logout(request, response,
(Authentication) oidcLogoutAuthentication.getPrincipal());
}
// 处理请求的登出后跳转地址
if (oidcLogoutAuthentication.isAuthenticated() &&
StringUtils.hasText(oidcLogoutAuthentication.getPostLogoutRedirectUri())) {
UriComponentsBuilder uriBuilder = UriComponentsBuilder
.fromUriString(oidcLogoutAuthentication.getPostLogoutRedirectUri());
String redirectUri;
if (StringUtils.hasText(oidcLogoutAuthentication.getState())) {
uriBuilder.queryParam(
OAuth2ParameterNames.STATE,
UriUtils.encode(oidcLogoutAuthentication.getState(), StandardCharsets.UTF_8));
}
redirectUri = uriBuilder.build(true).toUriString(); // build(true) -> Components are explicitly encoded
this.redirectStrategy.sendRedirect(request, response, redirectUri);
} else {
// 执行默认跳转
this.logoutSuccessHandler.onLogoutSuccess(request, response,
(Authentication) oidcLogoutAuthentication.getPrincipal());
}
}
...
}
- OIDC登出请求转换器 org.springframework.security.oauth2.server.authorization.oidc.web.authentication.OidcLogoutAuthenticationConverter
public final class OidcLogoutAuthenticationConverter implements AuthenticationConverter {
...
@Override
public Authentication convert(HttpServletRequest request) {
// 如果是GET请求获取url参数,否则获取表单参数
MultiValueMap<String, String> parameters =
"GET".equals(request.getMethod()) ?
OAuth2EndpointUtils.getQueryParameters(request) :
OAuth2EndpointUtils.getFormParameters(request);
// 必要参数id_token_hint (OIDC TOKEN)
String idTokenHint = parameters.getFirst("id_token_hint");
if (!StringUtils.hasText(idTokenHint) ||
parameters.get("id_token_hint").size() != 1) {
throwError(OAuth2ErrorCodes.INVALID_REQUEST, "id_token_hint");
}
// 获取当前会话用户,如果当前会话没有认证信息,则使用匿名用户认证信息
Authentication principal = SecurityContextHolder.getContext().getAuthentication();
if (principal == null) {
principal = ANONYMOUS_AUTHENTICATION;
}
// 获取当前会话
String sessionId = null;
HttpSession session = request.getSession(false);
if (session != null) {
sessionId = session.getId();
}
// 可选参数client_id (客户端ID)
String clientId = parameters.getFirst(OAuth2ParameterNames.CLIENT_ID);
if (StringUtils.hasText(clientId) &&
parameters.get(OAuth2ParameterNames.CLIENT_ID).size() != 1) {
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.CLIENT_ID);
}
// 可选参数post_logout_redirect_uri (登出授权服务器后跳转的地址,可用于跳转回客户端站点)
String postLogoutRedirectUri = parameters.getFirst("post_logout_redirect_uri");
if (StringUtils.hasText(postLogoutRedirectUri) &&
parameters.get("post_logout_redirect_uri").size() != 1) {
throwError(OAuth2ErrorCodes.INVALID_REQUEST, "post_logout_redirect_uri");
}
// 可选参数state (状态码)
String state = parameters.getFirst(OAuth2ParameterNames.STATE);
if (StringUtils.hasText(state) &&
parameters.get(OAuth2ParameterNames.STATE).size() != 1) {
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.STATE);
}
return new OidcLogoutAuthenticationToken(idTokenHint, principal,
sessionId, clientId, postLogoutRedirectUri, state);
}
...
}
- OIDC 登出请求认证提供者 org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcLogoutAuthenticationProvider
public final class OidcLogoutAuthenticationProvider implements AuthenticationProvider {
...
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
OidcLogoutAuthenticationToken oidcLogoutAuthentication =
(OidcLogoutAuthenticationToken) authentication;
// 根据参数提交的ID TOKEN查询已存在的OAuth2认证记录
OAuth2Authorization authorization = this.authorizationService.findByToken(
oidcLogoutAuthentication.getIdTokenHint(), ID_TOKEN_TOKEN_TYPE);
...
// 获取ID TOKEN授权记录
OAuth2Authorization.Token<OidcIdToken> authorizedIdToken = authorization.getToken(OidcIdToken.class);
// 根据认证记录获取注册客户端信息
RegisteredClient registeredClient = this.registeredClientRepository.findById(
authorization.getRegisteredClientId());
// 获取ID TOKEN
OidcIdToken idToken = authorizedIdToken.getToken();
// 校验客户端ID,是否包含在ID TOKEN订阅者清单中
List<String> audClaim = idToken.getAudience();
if (CollectionUtils.isEmpty(audClaim) ||
!audClaim.contains(registeredClient.getClientId())) {
throwError(OAuth2ErrorCodes.INVALID_TOKEN, IdTokenClaimNames.AUD);
}
// 如果请求中携带了客户端ID,校验是否与ID TOKEN对应客户端注册信息的ID一致
if (StringUtils.hasText(oidcLogoutAuthentication.getClientId()) &&
!oidcLogoutAuthentication.getClientId().equals(registeredClient.getClientId())) {
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.CLIENT_ID);
}
// 如果请求中携带了登出后跳转地址,校验是否包含在客户端注册信息的登出后跳转地址清单中
if (StringUtils.hasText(oidcLogoutAuthentication.getPostLogoutRedirectUri()) &&
!registeredClient.getPostLogoutRedirectUris().contains(oidcLogoutAuthentication.getPostLogoutRedirectUri())) {
throwError(OAuth2ErrorCodes.INVALID_REQUEST, "post_logout_redirect_uri");
}
...
// 如果当前会话用户不是匿名用户,则进行用户校验
if (oidcLogoutAuthentication.isPrincipalAuthenticated()) {
// 校验ID TOKEN是否包含用户信息,当前用户是否与授权用户一致
Authentication currentUserPrincipal = (Authentication) oidcLogoutAuthentication.getPrincipal();
Authentication authorizedUserPrincipal = authorization.getAttribute(Principal.class.getName());
if (!StringUtils.hasText(idToken.getSubject()) ||
!currentUserPrincipal.getName().equals(authorizedUserPrincipal.getName())) {
throwError(OAuth2ErrorCodes.INVALID_TOKEN, IdTokenClaimNames.SUB);
}
// 校验ID TOKEN的 sid 是否与请求的会话ID一致
if (StringUtils.hasText(oidcLogoutAuthentication.getSessionId())) {
SessionInformation sessionInformation = findSessionInformation(
currentUserPrincipal, oidcLogoutAuthentication.getSessionId());
if (sessionInformation != null) {
String sessionIdHash;
try {
sessionIdHash = createHash(sessionInformation.getSessionId());
} catch (NoSuchAlgorithmException ex) {
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
"Failed to compute hash for Session ID.", null);
throw new OAuth2AuthenticationException(error);
}
String sidClaim = idToken.getClaim("sid");
if (!StringUtils.hasText(sidClaim) ||
!sidClaim.equals(sessionIdHash)) {
throwError(OAuth2ErrorCodes.INVALID_TOKEN, "sid");
}
}
}
}
...
return new OidcLogoutAuthenticationToken(idToken, (Authentication) oidcLogoutAuthentication.getPrincipal(),
oidcLogoutAuthentication.getSessionId(), oidcLogoutAuthentication.getClientId(),
oidcLogoutAuthentication.getPostLogoutRedirectUri(), oidcLogoutAuthentication.getState());
}
// 用于OidcLogoutAuthenticationToken对象的认证处理
@Override
public boolean supports(Class<?> authentication) {
return OidcLogoutAuthenticationToken.class.isAssignableFrom(authentication);
}
...
}