spring authorization server oidc客户端发起登出源码分析

2024-05-24 12:24:40 浏览数 (1)

版本

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
代码语言:javascript复制
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
代码语言:javascript复制
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
代码语言:javascript复制
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);
	}
	...
}

0 人点赞