前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >spring authorization server oidc客户端发起登出源码分析

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

作者头像
路过君
发布2024-05-24 12:24:40
800
发布2024-05-24 12:24:40
举报

版本

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);
	}
	...
}
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2024-03-12,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 版本
  • 场景
  • 源码
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档