Bootstrap

Spring OAuth2.0 OIDC详解

OIDC 简介

作用

  1. 身份验证:OIDC 在 OAuth2 授权的基础上增加了身份验证功能,通过 ID Token 验证用户身份的真实性。
  2. 用户信息获取:通过 ID Token 和用户信息端点,客户端可以获取用户的详细信息。
  3. 安全性增强:通过引入 nonce 参数和 ID Token,OIDC 增强了请求的安全性,防止重放攻击等安全问题。

总的来说,OIDC 提供了比 OAuth2 更加完整和安全的身份验证和授权机制,确保在授权第三方应用访问用户资源的同时,也能验证用户的身份。


具体解释

  1. 身份认证

    • 在 OAuth2 授权码流程中,客户端请求授权码并使用授权码换取访问令牌和 ID Token。
    • ID Token 是一个 JWT(JSON Web Token),包含了用户的身份信息,如用户 ID、颁发者、受众和过期时间等。
    • 客户端使用 ID Token 验证用户的身份。

  2. 用户信息获取

    • 客户端可以使用访问令牌访问用户信息端点(UserInfo Endpoint),获取更多用户详细信息(如姓名、电子邮件等)。
    • 这种机制使客户端可以获得用户信息,而无需直接访问资源服务器。

  3. 安全性增强

    • OIDC 引入了 nonce 参数,用于防止重放攻击。
    • ID Token 是签名的,可以验证其完整性和真实性。



资源服务器的角色

在 OIDC 中,资源服务器主要用于保护用户资源。当客户端访问受保护的资源时,资源服务器通过以下方式验证请求的合法性:

  1. 访问令牌验证

    • 资源服务器检查客户端提供的访问令牌,以确定客户端是否有权访问特定资源。
    • 访问令牌是由授权服务器颁发的,可以包含用户的权限信息。

  2. 用户信息验证(可选)

    • 在某些情况下,资源服务器可能会使用 ID Token 验证用户身份,但这不是 OIDC 的主要用途。
    • 更常见的做法是,资源服务器依赖访问令牌来验证客户端的权限。



OIDC 流程图示

  1. 授权请求
    • 客户端向授权服务器发起包含 openid scope 的授权请求。
    • 用户通过授权服务器认证并同意授权。

  2. 授权码交换
    • 授权服务器返回授权码给客户端。
    • 客户端使用授权码向授权服务器请求访问令牌和 ID Token。

  3. 身份验证和用户信息获取
    • 客户端使用 ID Token 验证用户身份。
    • 客户端可以使用访问令牌访问用户信息端点获取更多用户信息。

  4. 资源访问
    • 客户端使用访问令牌访问资源服务器上的受保护资源。
    • 资源服务器验证访问令牌的有效性,并允许或拒绝访问。

Client AuthServer ResourceServer User Authorization Request (scope: openid) Authenticate User User Authenticated Authorization Code Token Request (Authorization Code) Access Token and ID Token Validate ID Token (Verify User Identity) Access Resource (Access Token) Protected Resource Client AuthServer ResourceServer User

验证用户身份的目的和作用

  1. 确保用户身份的真实性

    • ID Token 是由授权服务器生成并签名的,包含了用户的身份信息。通过验证 ID Token,客户端可以确保用户身份的真实性,即确认这个用户确实是授权服务器声称的那个用户。

  2. 防止身份伪造

    • 验证 ID Token 可以防止恶意用户伪造身份。客户端通过验证 ID Token 的签名和内容,确保其没有被篡改。

  3. 安全性增强

    • ID Token 包含了诸如 nonce 等安全参数,防止重放攻击和其他安全威胁。客户端通过验证这些参数,进一步增强安全性。

  4. 提供用户信息

    • ID Token 包含用户的基本信息(如用户ID、电子邮件等),客户端可以使用这些信息来个性化用户体验,无需再向资源服务器发送额外请求。

  5. 单点登录(SSO)

    • 在单点登录场景中,ID Token 可以在多个应用之间传递用户身份信息,使用户只需登录一次即可访问多个应用。

  6. 实现用户会话管理

    • 客户端可以通过 ID Token 管理用户会话,跟踪用户登录状态和会话时间,提供更好的用户体验。


ID Token 的内容

ID Token 是一个 JWT(JSON Web Token),通常包含以下信息:

  • iss(Issuer):颁发者的标识符。
  • sub(Subject):用户的唯一标识符。
  • aud(Audience):ID Token 的受众,即客户端的标识符。
  • exp(Expiration Time):ID Token 的过期时间。
  • iat(Issued At):ID Token 的颁发时间。
  • nonce:防止重放攻击的随机值。
  • 用户信息:如用户名、电子邮件等。



OAuth2.0 与 OIDC 区别联系

OAuth2.0 和 OpenID Connect (OIDC) 是两个紧密相关但又不同的协议。了解它们之间的联系和区别对于实现安全的身份验证和授权系统至关重要。

联系

  1. 基础协议:OIDC 是基于 OAuth2.0 的一个身份验证层。换句话说,OIDC 扩展了 OAuth2.0 的授权功能,添加了身份验证的功能。
  2. 使用相同的授权流程:OIDC 使用 OAuth2.0 的授权码流程来获取访问令牌和身份令牌。OAuth2.0 的授权码流程允许客户端代表用户访问资源,而 OIDC 在此基础上引入了身份令牌,用于验证用户身份。

区别

  1. 目的不同
    • OAuth2.0:主要用于授权,允许第三方应用获取访问资源服务器的权限。
    • OIDC:主要用于身份验证,验证用户的身份并获取用户的基本信息,同时也提供授权功能。

  2. 令牌类型
    • OAuth2.0:返回访问令牌(Access Token),用于访问受保护的资源。
    • OIDC:除了返回访问令牌,还返回身份令牌(ID Token)。身份令牌是一个 JWT(JSON Web Token),包含用户的身份信息。

  3. 端点
    • OAuth2.0:主要使用授权端点(Authorization Endpoint)和令牌端点(Token Endpoint)。
    • OIDC:除了 OAuth2.0 的端点,还引入了用户信息端点(UserInfo Endpoint),用于获取用户的详细信息。

  4. 用户信息
    • OAuth2.0:不包含用户身份信息,仅提供授权功能。
    • OIDC:通过身份令牌和用户信息端点提供用户的身份信息。

  5. 规范
    • OAuth2.0:规范较为松散,主要定义了授权框架。
    • OIDC:规范更为严格,定义了标准的身份验证流程和用户信息获取方式。

示例流程

OAuth2.0 授权码流程

  1. 用户授权:用户在授权服务器上同意第三方应用访问其资源。
  2. 获取授权码:授权服务器返回授权码给客户端应用。
  3. 交换授权码:客户端应用使用授权码向授权服务器请求访问令牌。
  4. 获取访问令牌:授权服务器返回访问令牌,客户端应用使用该令牌访问受保护的资源。

OIDC 授权码流程

  1. 用户授权和身份验证:用户在授权服务器上同意第三方应用访问其资源,并进行身份验证。
  2. 获取授权码:授权服务器返回授权码给客户端应用。
  3. 交换授权码:客户端应用使用授权码向授权服务器请求访问令牌和身份令牌。
  4. 获取访问令牌和身份令牌:授权服务器返回访问令牌和身份令牌,客户端应用使用访问令牌访问受保护的资源,并使用身份令牌验证用户身份。
  5. 获取用户信息:客户端应用可以使用访问令牌从用户信息端点获取用户的详细信息。

总结

  • OAuth2.0:用于授权,允许第三方应用代表用户访问资源。
  • OIDC:在 OAuth2.0 基础上增加了身份验证功能,提供用户身份验证和用户信息获取。

两者的结合使得开发者既可以安全地授权应用访问资源,又可以验证用户身份和获取用户信息。




项目中开启OIDC

授权服务oauth2-server配置类

@EnableWebSecurity
@Configuration
public class AuthorizationServerConfig {

    @Bean
    public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {

        // 定义授权服务配置
        OAuth2AuthorizationServerConfigurer configurer = new OAuth2AuthorizationServerConfigurer();

        // 获取授权服务器相关的请求端点
        RequestMatcher endpointsMatcher = configurer.getEndpointsMatcher();

        http
                .authorizeHttpRequests(authorize -> authorize
                        // 配置放行的请求
                        .antMatchers("/login").permitAll()
                        // 其他任何请求都需要认证
                        .anyRequest().authenticated()
                )
                // 设置登录表单页面
                .formLogin()
                .and()
                // 忽略掉相关端点的 CSRF(跨站请求): 对授权端点的访问可以是跨站的
                .csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher))

                // 使用BearerTokenAuthenticationFilter对AccessToken及idToken进行解析验证
                // idToken是开启OIDC时授权服务连同AccessToken一起返回给客户端的,用于客户端验证用户身份,结合此处配置使用BearerTokenAuthenticationFilter来验证idToken
                .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)

                // 应用授权服务器的配置
                .apply(configurer);


        configurer
                //开启oidc,客户端会对资源所有者进行身份认证,确保用户身份的真实性、防止身份伪造、增强安全性。
                // 开启后,除了访问令牌access_token,还会多一个用户身份认证的idToken
                .oidc(Customizer.withDefaults());
        
        return http.build();
    }
    
}

客户端oauth2-client的yaml

spring:
  application:
    name: oauth-client
  security:
    oauth2:
      client:
        registration:
          #自定义客户端名称
          test-client:
            #对应下面的provider
            provider: authorization-server
            client-id: test-client
            client-secret: FjKNY8p2&Xw9Lqe$G3Bt*5mZ4Pv#CV2sE6J!n
            client-authentication-method: client_secret_basic
            authorization-grant-type: authorization_code
            #redirect-uri: "{baseUrl}/{action}/oauth2/code/{registrationId}"
            redirect-uri: "http://127.0.0.1:8000/login/oauth2/code/test-client"       
            #要使用oidc,权限必须包括openid,以及profile、email、address、phone中的一个或多个
            scope: openid,profile,message.read,message.write
        provider:
          # 服务提供地址
          authorization-server:
            # issuer-uri 可以简化配置,配置了issuer-uri,其他配置可以从此配置的路径中自动获取
            issuer-uri: http://127.0.0.1:9000

向授权服务注册客户端

注意注册的客户端信息要与客户端yaml配置一致,尤其是oidc的scope

RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
     // 客户端ID和密码
    .clientId("test-client")
    //客户端认证方式,这里指定使用请求头的Authentication: Basic Auth
    .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
    .clientSecret("{bcrypt}" + new BCryptPasswordEncoder().encode("FjKNY8p2&Xw9Lqe$GH7Rd3Bt*5mZ4Pv#CV2sE6J!n"))
	//配置支持多种授权模式
    .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)//授权码模式
    // 回调地址:授权码模式下,授权服务器会携带code向当前客户端的如下地址进行重定向。只能使用IP或域名,不能使用 localhost
    .redirectUri("http://127.0.0.1:8000/login/oauth2/code/test-client")
    // OIDC 支持, 用于客户端对用户(资源所有者)的身份认证
    .scope(OidcScopes.OPENID) //OIDC 并不是授权码模式的必需部分,但如果客户端请求包含 openid scope,就必须启用 OIDC 支持。
    .scope(OidcScopes.PROFILE)
    // 授权范围(当前客户端的授权范围)
    .scope("message.read")
    .scope("message.write")
    .build();


OIDC 在 OAuth2 授权码模式中的源码流程

OpenID Connect (OIDC) 是在 OAuth2 之上构建的身份认证协议,它引入了 ID Token 来表示用户的身份。OIDC 结合 OAuth2 授权码模式,允许客户端在获取授权后,获取用户身份信息。以下是 OIDC 在 OAuth2 授权码模式中的运行流程及其作用。

1.授权服务生成 ID Token

OAuth2AuthorizationCodeAuthenticationProvider 生成 ID Token

在 OAuth2 授权码模式中,当客户端请求授权码并换取访问令牌时,如果请求包含 openid scope,ID Token 将被服务端生成并返回给客户端(与Access_Token一起返回)。

源码中,当客户端OAuth2LoginAuthenticationFilter过滤器携带授权码并换取访问令牌时,授权服务OAuth2TokenEndpointFilter过滤器接收请求,并使用 OAuth2AuthorizationCodeAuthenticationProviderauthenticate 方法生成 ID Token ,以下是关键代码片段:

// ----- ID token -----
OidcIdToken idToken;
if (authorizationRequest.getScopes().contains(OidcScopes.OPENID)) {
    tokenContext = tokenContextBuilder
            .tokenType(ID_TOKEN_TOKEN_TYPE)
            .authorization(authorizationBuilder.build())
            .build();

    OAuth2Token generatedIdToken = this.tokenGenerator.generate(tokenContext);
    if (!(generatedIdToken instanceof Jwt)) {
        OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
                "The token generator failed to generate the ID token.", ERROR_URI);
        throw new OAuth2AuthenticationException(error);
    }

    idToken = new OidcIdToken(generatedIdToken.getTokenValue(), generatedIdToken.getIssuedAt(),
            generatedIdToken.getExpiresAt(), ((Jwt) generatedIdToken).getClaims());
    authorizationBuilder.token(idToken, (metadata) ->
            metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, idToken.getClaims()));
} else {
    idToken = null;
}
  1. 检查 openid scope:如果请求的 scope 中包含 openid,则表明这是一个 OIDC 请求,需要生成 ID Token。
  2. 生成 ID Token:使用 tokenGenerator 生成 ID Token,并将其添加到授权信息中。
  3. 构建并保存授权信息:构建包含 ID Token 的授权信息,并将其保存。

最后OAuth2TokenEndpointFilter过滤器会将token响应给客户端


2. 客户端OIDC请求处理

OidcAuthorizationCodeAuthenticationProvider 会验证并处理 ID Token

在处理授权码交换访问令牌和 ID Token 的过程中,客户端OAuth2LoginAuthenticationFilter过滤器会使用OidcAuthorizationCodeAuthenticationProviderorg.springframework.security.oauth2.client.oidc.authentication包)用于验证并处理 OIDC 请求。

以下是OidcAuthorizationCodeAuthenticationProvider关键代码片段,核心步骤是根据获得的idToken,向授权服务发起"/userinfo"请求,验证idToken,获得用户信息:

public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    OAuth2LoginAuthenticationToken authorizationCodeAuthentication = (OAuth2LoginAuthenticationToken) authentication;
	
    //如果请求的 scope 中不包含 `openid`,则返回 `null`,表明不是 OIDC 请求
    if (!authorizationCodeAuthentication.getAuthorizationExchange().getAuthorizationRequest().getScopes()
            .contains(OidcScopes.OPENID)) {
        return null;
    }

    OAuth2AuthorizationRequest authorizationRequest = authorizationCodeAuthentication.getAuthorizationExchange()
            .getAuthorizationRequest();
    OAuth2AuthorizationResponse authorizationResponse = authorizationCodeAuthentication.getAuthorizationExchange()
            .getAuthorizationResponse();
	
    //两个if,检查授权响应是否包含错误,state 参数是否匹配
    if (authorizationResponse.statusError()) {
        throw new OAuth2AuthenticationException(authorizationResponse.getError(),
                authorizationResponse.getError().toString());
    }
    if (!authorizationResponse.getState().equals(authorizationRequest.getState())) {
        OAuth2Error oauth2Error = new OAuth2Error(INVALID_STATE_PARAMETER_ERROR_CODE);
        throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
    }
    
	
    //获取accessToken,即客户端携带code去请求token,返回的响应包括accessToken与idToken
    OAuth2AccessTokenResponse accessTokenResponse = getResponse(authorizationCodeAuthentication);
    ClientRegistration clientRegistration = authorizationCodeAuthentication.getClientRegistration();
    Map<String, Object> additionalParameters = accessTokenResponse.getAdditionalParameters();
	
    //如果附加参数中不包含 ID Token,抛出异常
    if (!additionalParameters.containsKey(OidcParameterNames.ID_TOKEN)) {
        OAuth2Error invalidIdTokenError = new OAuth2Error(INVALID_ID_TOKEN_ERROR_CODE,
                "Missing (required) ID Token in Token Response for Client Registration: "
                        + clientRegistration.getRegistrationId(),
                null);
        throw new OAuth2AuthenticationException(invalidIdTokenError, invalidIdTokenError.toString());
    }
	
    //从授权服务返回结果中获取idToken
    OidcIdToken idToken = createOidcToken(clientRegistration, accessTokenResponse);
    //验证 nonce 参数,确保 ID Token 的有效性和安全性
    validateNonce(authorizationRequest, idToken);
	
    // 根据获得的idToken,向授权服务发起"/userinfo"请求,验证idToken,获得用户信息
    OidcUser oidcUser = this.userService.loadUser(new OidcUserRequest(clientRegistration,
            accessTokenResponse.getAccessToken(), idToken, additionalParameters));
    Collection<? extends GrantedAuthority> mappedAuthorities = this.authoritiesMapper
            .mapAuthorities(oidcUser.getAuthorities());
	
    //创建并返回包含用户信息和权限的 `OAuth2LoginAuthenticationToken` 认证结果
    OAuth2LoginAuthenticationToken authenticationResult = new OAuth2LoginAuthenticationToken(
            authorizationCodeAuthentication.getClientRegistration(),
            authorizationCodeAuthentication.getAuthorizationExchange(), oidcUser, mappedAuthorities,
            accessTokenResponse.getAccessToken(), accessTokenResponse.getRefreshToken());

    authenticationResult.setDetails(authorizationCodeAuthentication.getDetails());
    return authenticationResult;
}
  1. 检查 openid scope:如果请求的 scope 中不包含 openid,则返回 null,表明不是 OIDC 请求。
  2. 验证授权响应:检查授权响应是否包含错误,state 参数是否匹配。
  3. 获取访问令牌响应:获取访问令牌和其他附加参数。
  4. 检查 ID Token:如果附加参数中不包含 ID Token,抛出异常。
  5. 创建并验证 ID Token:创建 ID Token 并验证 nonce 参数,确保 ID Token 的有效性和安全性。
  6. 加载 OIDC 用户信息:通过 ID Token 和访问令牌加载用户信息,并将用户权限映射到 Spring Security 的权限体系中。(下一步详细说明)
  7. 创建认证结果:创建并返回包含用户信息和权限的 OAuth2LoginAuthenticationToken 认证结果。

3.客户端加载用户信息

由第2步OidcAuthorizationCodeAuthenticationProviderauthenticate方法中进行,用于使用idToken验证和加载用户信息

代码如下:

 OidcUser oidcUser = this.userService.loadUser(new OidcUserRequest(clientRegistration,
            accessTokenResponse.getAccessToken(), idToken, additionalParameters));

上面代码会先调用OidcUserServiceloadUser方法,通过shouldRetrieveUserInfo方法进行信息检查,然后再由OidcUserService调用DefaultOAuth2UserService

OidcUserServiceloadUser方法

@Override
public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException {
    Assert.notNull(userRequest, "userRequest cannot be null");

    OidcUserInfo userInfo = null;
    
    //检查UserInfo信息,检查通过后才向授权服务进行OIDC验证
    if (this.shouldRetrieveUserInfo(userRequest)) {
        //if成立后调用`DefaultOAuth2UserService` 的loadUser方法
        OAuth2User oauth2User = this.oauth2UserService.loadUser(userRequest);
        Map<String, Object> claims = getClaims(userRequest, oauth2User);
        userInfo = new OidcUserInfo(claims);

        if (userInfo.getSubject() == null) {
            OAuth2Error oauth2Error = new OAuth2Error(INVALID_USER_INFO_RESPONSE_ERROR_CODE);
            throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
        }

        if (!userInfo.getSubject().equals(userRequest.getIdToken().getSubject())) {
            OAuth2Error oauth2Error = new OAuth2Error(INVALID_USER_INFO_RESPONSE_ERROR_CODE);
            throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
        }
    }

    
    Set<GrantedAuthority> authorities = new LinkedHashSet<>();
    authorities.add(new OidcUserAuthority(userRequest.getIdToken(), userInfo));
    OAuth2AccessToken token = userRequest.getAccessToken();
    for (String authority : token.getScopes()) {
        authorities.add(new SimpleGrantedAuthority("SCOPE_" + authority));
    }
    return getUser(userRequest, userInfo, authorities);
}

shouldRetrieveUserInfo方法,判断以下两项内容,必须同时满足,否则不会向授权服务发起OIDC验证:

  • UserInfo Endpoint URI不能为空。
  • 如果是授权码模式,访问令牌的权限范围必须包括profile、email、address、phone中的一个或多个。
private boolean shouldRetrieveUserInfo(OidcUserRequest userRequest) {
	ProviderDetails providerDetails = userRequest.getClientRegistration().getProviderDetails();
	if (StringUtils.isEmpty(providerDetails.getUserInfoEndpoint().getUri())) {
		return false;
	}

	if (AuthorizationGrantType.AUTHORIZATION_CODE
			.equals(userRequest.getClientRegistration().getAuthorizationGrantType())) {

		return 
            	//accessibleScopes默认不是空
            this.accessibleScopes.isEmpty()
            	//访问令牌的Scopes不是空
			|| CollectionUtils.isEmpty(userRequest.getAccessToken().getScopes())
            	//访问令牌的权限范围是否包括profile、email、address、phone中的一个或多个
			|| CollectionUtils.containsAny(userRequest.getAccessToken().getScopes(), this.accessibleScopes);
			
	}
	return false;
}

// OidcUserService中的accessibleScopes
private Set<String> accessibleScopes = new HashSet<>(
			Arrays.asList(OidcScopes.PROFILE, OidcScopes.EMAIL, OidcScopes.ADDRESS, OidcScopes.PHONE));

DefaultOAuth2UserService 是 Spring Security OAuth2 的userService的默认实现,在开启oidc后,用于从 OAuth2 提供者的用户信息端点加载用户信息。

以下是 DefaultOAuth2UserService 类中 loadUser 方法的详细解读:

@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
    Assert.notNull(userRequest, "userRequest cannot be null");

    // 检查 UserInfoEndpoint 的 URI 是否为空
    if (!StringUtils.hasText(userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUri())) {
        OAuth2Error oauth2Error = new OAuth2Error(MISSING_USER_INFO_URI_ERROR_CODE,
                "Missing required UserInfo Uri in UserInfoEndpoint for Client Registration: "
                        + userRequest.getClientRegistration().getRegistrationId(),
                null);
        throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
    }

    // 获取 userNameAttributeName,这里值为'sub',在userInfoEndpoint端点参数中
    String userNameAttributeName = userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint()
            .getUserNameAttributeName();
    if (!StringUtils.hasText(userNameAttributeName)) {
        OAuth2Error oauth2Error = new OAuth2Error(MISSING_USER_NAME_ATTRIBUTE_ERROR_CODE,
                "Missing required \"user name\" attribute name in UserInfoEndpoint for Client Registration: "
                        + userRequest.getClientRegistration().getRegistrationId(),
                null);
        throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
    }

    // 创建请求实体
    RequestEntity<?> request = this.requestEntityConverter.convert(userRequest);

    // 发送请求并获取响应, 即向授权服务发起"/userinfo"请求
    ResponseEntity<Map<String, Object>> response = getResponse(userRequest, request);
    // map的键值对为 sub -> 用户名
    Map<String, Object> userAttributes = response.getBody();

    // 创建权限集合
    Set<GrantedAuthority> authorities = new LinkedHashSet<>();
    authorities.add(new OAuth2UserAuthority(userAttributes));

    //获取token对象,内部包含:token类型-Bearer, 权限范围-scopes,token字符串-tokenValue,过期时间-expiresAt等
    OAuth2AccessToken token = userRequest.getAccessToken();
    //添加范围权限,将上面token中的scopes添加到结合中
    for (String authority : token.getScopes()) {
        authorities.add(new SimpleGrantedAuthority("SCOPE_" + authority));
    }

    // 添加用户、权限信息,创建并返回 DefaultOAuth2User 实例
    return new DefaultOAuth2User(authorities, userAttributes, userNameAttributeName);
}

详细解析

  1. 参数校验
    • Assert.notNull(userRequest, "userRequest cannot be null");:确保 userRequest 不为空,否则抛出异常。

  2. 检查 UserInfoEndpoint 的 URI
    • 通过 userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUri() 获取 UserInfoEndpoint 的 URI。如果 URI 为空,则构造一个 OAuth2Error 并抛出 OAuth2AuthenticationException

  3. 获取 userNameAttributeName
    • 通过 userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName() 获取 UserInfoEndpoint 中的 userNameAttributeName。如果 userNameAttributeName 为空,同样构造一个 OAuth2Error 并抛出 OAuth2AuthenticationException

  4. 创建请求实体
    • 使用 this.requestEntityConverter.convert(userRequest)userRequest 转换为 RequestEntity

  5. 发送请求并获取响应
    • 调用 getResponse(userRequest, request) 向授权服务发起"/userinfo"请求并接收响应,得到包含用户属性的 Map<String, Object>

  6. 创建权限集合
    • 创建一个 LinkedHashSet<GrantedAuthority> 来存储用户的权限。首先将 userAttributes 转换为 OAuth2UserAuthority 并添加到权限集合中。

  7. 添加范围权限
    • 获取 userRequest 中的 OAuth2AccessToken,并将其所有的范围(scopes)添加为 SimpleGrantedAuthority,例如 SCOPE_read

  8. 返回 DefaultOAuth2User 实例
    • 使用权限集合 authorities、用户属性 userAttributesuserNameAttributeName 创建并返回一个 DefaultOAuth2User 实例。

该方法主要用于根据 OAuth2UserRequest 加载用户信息。它首先检查必要的配置(如 UserInfoEndpoint URI 和 userNameAttributeName),然后发送请求以获取用户信息,最后将这些信息封装为一个 DefaultOAuth2User 对象,并添加相关的权限。

通过这种方式,Spring Security 能够在 OAuth2 登录流程中正确加载并处理用户信息,从而完成用户认证和授权。


4.授权服务验证idToken

授权服务接收到客户端"/userinfo"请求,先由BearerTokenAuthenticationFilter解析idToken,再由OidcUserInfoEndpointFilter处理"/userinfo"请求

先由BearerTokenAuthenticationFilter过滤器处解析idToken

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
        throws ServletException, IOException {
    String token;
    try {
        //解析 Token
        token = this.bearerTokenResolver.resolve(request);
    } catch (OAuth2AuthenticationException invalid) {
        this.logger.trace("Sending to authentication entry point since failed to resolve bearer token", invalid);
        //解析失败处理
        this.authenticationEntryPoint.commence(request, response, invalid);
        return;
    }
    
    //如果没有找到 Token,直接调用过滤器链的下一个过滤器
    if (token == null) {
        this.logger.trace("Did not process request since did not find bearer token");
        filterChain.doFilter(request, response);
        return;
    }
	
    //根据token创建认证对象,并设置认证详情,即设置详细信息
    BearerTokenAuthenticationToken authenticationRequest = new BearerTokenAuthenticationToken(token);
    authenticationRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));

    try {
        //获取Manager用于验证认证对象。没有单独配置,默认请款下返回`ProviderManager`
        AuthenticationManager authenticationManager = this.authenticationManagerResolver.resolve(request);
        //这里idToken的验证使用的是JwtAuthenticationProvider,得到的结果是JwtAuthenticationToken
        Authentication authenticationResult = authenticationManager.authenticate(authenticationRequest);
        //将验证结果JwtAuthenticationToken保存到上下文中
        SecurityContext context = SecurityContextHolder.createEmptyContext();
        context.setAuthentication(authenticationResult);
        SecurityContextHolder.setContext(context);
        this.securityContextRepository.saveContext(context, request, response);
        if (this.logger.isDebugEnabled()) {
            this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", authenticationResult));
        }
        filterChain.doFilter(request, response);
    } catch (AuthenticationException failed) {
        SecurityContextHolder.clearContext();
        this.logger.trace("Failed to process authentication request", failed);
        this.authenticationFailureHandler.onAuthenticationFailure(request, response, failed);
    }
}

JwtAuthenticationProvider解析JWT的idToken:

@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
	BearerTokenAuthenticationToken bearer = (BearerTokenAuthenticationToken) authentication;
	Jwt jwt = getJwt(bearer);
    //使用JwtAuthenticationConverter进行转换
	AbstractAuthenticationToken token = this.jwtAuthenticationConverter.convert(jwt);
	token.setDetails(bearer.getDetails());
	this.logger.debug("Authenticated token");
	return token;
}

OidcUserInfoEndpointFilter处理"/userinfo"请求

以下是 OidcUserInfoEndpointFilter 类中 doFilterInternal 方法的详细解析:

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
        throws ServletException, IOException {

    // 检查请求是否与 userInfoEndpointMatcher 匹配
    if (!this.userInfoEndpointMatcher.matches(request)) {
        filterChain.doFilter(request, response);
        return;
    }

    try {
        // 将请求转换为 Authentication 对象
        Authentication userInfoAuthentication = this.authenticationConverter.convert(request);

        // 通过 AuthenticationManager 进行认证
        Authentication userInfoAuthenticationResult =
                this.authenticationManager.authenticate(userInfoAuthentication);

        // 处理认证成功情况
        this.authenticationSuccessHandler.onAuthenticationSuccess(request, response, userInfoAuthenticationResult);
    } catch (OAuth2AuthenticationException ex) {
        // 处理 OAuth2 认证异常
        if (this.logger.isTraceEnabled()) {
            this.logger.trace(LogMessage.format("User info request failed: %s", ex.getError()), ex);
        }
        this.authenticationFailureHandler.onAuthenticationFailure(request, response, ex);
    } catch (Exception ex) {
        // 处理其他异常情况
        OAuth2Error error = new OAuth2Error(
                OAuth2ErrorCodes.INVALID_REQUEST,
                "OpenID Connect 1.0 UserInfo Error: " + ex.getMessage(),
                "https://openid.net/specs/openid-connect-core-1_0.html#UserInfoError");
        if (this.logger.isTraceEnabled()) {
            this.logger.trace(error.getDescription(), ex);
        }
        this.authenticationFailureHandler.onAuthenticationFailure(request, response,
                new OAuth2AuthenticationException(error));
    } finally {
        // 清理 SecurityContextHolder
        SecurityContextHolder.clearContext();
    }
}

详细解析

  1. 匹配请求

    if (!this.userInfoEndpointMatcher.matches(request)) {
        filterChain.doFilter(request, response);
        return;
    }
    
    • 这段代码首先检查请求是否与 userInfoEndpointMatcher 匹配。匹配的是"/userinfo"路径的getpost请求。
  • 如果不匹配,则调用 filterChain.doFilter 方法将请求传递到过滤器链中的下一个过滤器,然后返回。
  1. 尝试处理认证

    try {
     Authentication userInfoAuthentication = this.authenticationConverter.convert(request);
    
        Authentication userInfoAuthenticationResult =
             this.authenticationManager.authenticate(userInfoAuthentication);
    
        this.authenticationSuccessHandler.onAuthenticationSuccess(request, response, userInfoAuthenticationResult);
    }
    

    尝试将请求转换为 Authentication 对象。

    默认情况下,使用的是OidcUserInfoEndpointConfigurer中如下的createDefaultAuthenticationConverters方法进行转换,实际就是直接从上下文中获取 Authentication 对象,取出的上下文对象就是BearerTokenAuthenticationFilter解析idToken时在上下文中保存的 Authentication 对象

    • private static List<AuthenticationConverter> createDefaultAuthenticationConverters() {
          
      	List<AuthenticationConverter> authenticationConverters = new ArrayList<>();
           
      	authenticationConverters.add(
      		(request) -> {
      			Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
      			return new OidcUserInfoAuthenticationToken(authentication);
      		}
      	);
           
      	return authenticationConverters;
      }
      

    调用 authenticationManager.authenticate 方法进行认证。使用的是OidcUserInfoAuthenticationProviderauthenticate方法:

    /*
    	该方法主要用于处理OpenID Connect 1.0用户信息端点的身份验证。它首先验证访问令牌的有效性,然后通过访问令牌查找授权信息,并验证授权信息的有效性和范围。最后,它构建认证上下文,并根据上下文映射用户信息,返回认证成功的OidcUserInfoAuthenticationToken
    	
    */
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        //将传入的Authentication对象强制转换为OidcUserInfoAuthenticationToken
        OidcUserInfoAuthenticationToken userInfoAuthentication =
                (OidcUserInfoAuthenticationToken) authentication;
    
        AbstractOAuth2TokenAuthenticationToken<?> accessTokenAuthentication = null;
        //检查userInfoAuthentication的Principal是否是AbstractOAuth2TokenAuthenticationToken的子类,如果是,则进行类型转换
        if (AbstractOAuth2TokenAuthenticationToken.class.isAssignableFrom(userInfoAuthentication.getPrincipal().getClass())) {
            accessTokenAuthentication = (AbstractOAuth2TokenAuthenticationToken<?>) userInfoAuthentication.getPrincipal();
        }
        
        //检查accessTokenAuthentication是否为空或未通过身份验证。如果是,则抛出OAuth2AuthenticationException
        if (accessTokenAuthentication == null || !accessTokenAuthentication.isAuthenticated()) {
            throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_TOKEN);
        }
        
    	//获取访问令牌值
        String accessTokenValue = accessTokenAuthentication.getToken().getTokenValue();
    	
        //使用访问令牌值查找授权信息,如果找不到,则抛出OAuth2AuthenticationException
        //authorizationService有内存与数据库两种默认提供,数据库实现的表名为'oauth2_authorization'
        OAuth2Authorization authorization = this.authorizationService.findByToken(
                accessTokenValue, OAuth2TokenType.ACCESS_TOKEN);
        if (authorization == null) {
            throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_TOKEN);
        }
    
        if (this.logger.isTraceEnabled()) {
            this.logger.trace("Retrieved authorization with access token");
        }
    	
        //检查访问令牌有效性
        OAuth2Authorization.Token<OAuth2AccessToken> authorizedAccessToken = authorization.getAccessToken();
        if (!authorizedAccessToken.isActive()) {
            throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_TOKEN);
        }
    
        //检查访问令牌的范围是否包含openid
        if (!authorizedAccessToken.getToken().getScopes().contains(OidcScopes.OPENID)) {
            throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INSUFFICIENT_SCOPE);
        }
    
        //获取idToken并验证其有效性
        OAuth2Authorization.Token<OidcIdToken> idToken = authorization.getToken(OidcIdToken.class);
        if (idToken == null) {
            throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_TOKEN);
        }
    
        if (this.logger.isTraceEnabled()) {
            this.logger.trace("Validated user info request");
        }
    
        //构建OidcUserInfoAuthenticationContext并获取用户信息
        OidcUserInfoAuthenticationContext authenticationContext =
                OidcUserInfoAuthenticationContext.with(userInfoAuthentication)
                        .accessToken(authorizedAccessToken.getToken())
                        .authorization(authorization)
                        .build();
        OidcUserInfo userInfo = this.userInfoMapper.apply(authenticationContext);
    
        if (this.logger.isTraceEnabled()) {
            this.logger.trace("Authenticated user info request");
        }
    
        //返回认证成功的OidcUserInfoAuthenticationToken
        return new OidcUserInfoAuthenticationToken(accessTokenAuthentication, userInfo);
    }
    

    如果认证成功,调用 authenticationSuccessHandler.onAuthenticationSuccess 方法处理认证成功的情况。这里通过http响应返回UserInfo信息,对接到客户端DefaultOAuth2UserServiceloadUser方法中的getResponse

    // authenticationSuccessHandler.onAuthenticationSuccess处理对应使用的是OidcUserInfoEndpointFilter过滤器的如下方法:
    private void sendUserInfoResponse(HttpServletRequest request, HttpServletResponse response,
    		Authentication authentication) throws IOException {
    	OidcUserInfoAuthenticationToken userInfoAuthenticationToken = (OidcUserInfoAuthenticationToken) authentication;
    	ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
    	this.userInfoHttpMessageConverter.write(userInfoAuthenticationToken.getUserInfo(), null, httpResponse);
    }
    

  2. 处理异常

    catch (OAuth2AuthenticationException ex) {
        if (this.logger.isTraceEnabled()) {
            this.logger.trace(LogMessage.format("User info request failed: %s", ex.getError()), ex);
        }
        this.authenticationFailureHandler.onAuthenticationFailure(request, response, ex);
    } catch (Exception ex) {
        OAuth2Error error = new OAuth2Error(
                OAuth2ErrorCodes.INVALID_REQUEST,
                "OpenID Connect 1.0 UserInfo Error: " + ex.getMessage(),
                "https://openid.net/specs/openid-connect-core-1_0.html#UserInfoError");
        if (this.logger.isTraceEnabled()) {
            this.logger.trace(error.getDescription(), ex);
        }
        this.authenticationFailureHandler.onAuthenticationFailure(request, response,
                new OAuth2AuthenticationException(error));
    } finally {
        SecurityContextHolder.clearContext();
    }
    
    • 捕获 OAuth2AuthenticationException 异常,并使用 authenticationFailureHandler.onAuthenticationFailure 方法处理认证失败的情况。
    • 捕获其他异常,并构造 OAuth2Error 对象,使用 authenticationFailureHandler.onAuthenticationFailure 方法处理认证失败的情况。
    • 无论成功还是失败,最终都会调用 SecurityContextHolder.clearContext() 方法清理安全上下文。

代码关键点

  1. userInfoEndpointMatcher:这是一个 RequestMatcher 对象,用于检查请求是否匹配 UserInfo 端点的 URI。
  2. authenticationConverter:用于将 HttpServletRequest 转换为 Authentication 对象。
  3. authenticationManager:用于处理认证逻辑,调用其 authenticate 方法进行用户认证。
  4. authenticationSuccessHandlerauthenticationFailureHandler:分别用于处理认证成功和失败的情况。
  5. SecurityContextHolder.clearContext():用于在请求处理完毕后清理安全上下文,以确保安全性。

该方法的主要功能是处理 OpenID Connect 的 UserInfo 端点请求。它首先检查请求是否匹配 UserInfo 端点,然后尝试将请求转换为 Authentication 对象并进行认证,最后根据认证结果处理成功或失败的情况。在整个过程中,它确保了安全上下文的清理。




客户端UserInfoEndpointConfig配置

OAuth2LoginConfigurer的UserInfoEndpointConfig配置

UserInfoEndpointConfigOAuth2LoginConfigurer 类的内部配置类,负责配置 OAuth 2.0 用户信息端点。这段代码主要用于设置从用户信息端点获取用户属性的服务。以下是每个部分的详细解释:

属性

private OAuth2UserService<OAuth2UserRequest, OAuth2User> userService;

用于处理 OAuth 2.0 请求并返回用户信息的服务。

配置的是OAuth2LoginAuthenticationProvider中使用的的userService

private OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService;

用于处理 OpenID Connect (OIDC) 请求并返回用户信息的服务。

配置的是OidcAuthorizationCodeAuthenticationProvider中使用的userService,对应上面源码讲解第3步客户端加载用户信息

private Map<String, Class<? extends OAuth2User>> customUserTypes = new HashMap<>();

存储自定义的 OAuth2User 类型及其对应的客户端注册ID。


构造函数

  • private UserInfoEndpointConfig() {} 私有构造函数,防止外部直接实例化该类

方法

userService

public UserInfoEndpointConfig userService(OAuth2UserService<OAuth2UserRequest, OAuth2User> userService) {
    Assert.notNull(userService, "userService cannot be null");
    this.userService = userService;
    return this;
}
  • 设置 OAuth 2.0 用户服务,用户通过该服务获取用户信息。

oidcUserService

public UserInfoEndpointConfig oidcUserService(OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService) {
    Assert.notNull(oidcUserService, "oidcUserService cannot be null");
    this.oidcUserService = oidcUserService;
    return this;
}
  • 设置 OIDC 用户服务,用于从 OIDC 用户信息端点获取用户信息。

customUserType

@Deprecated
public UserInfoEndpointConfig customUserType(Class<? extends OAuth2User> customUserType, String clientRegistrationId) {
    Assert.notNull(customUserType, "customUserType cannot be null");
    Assert.hasText(clientRegistrationId, "clientRegistrationId cannot be empty");
    this.customUserTypes.put(clientRegistrationId, customUserType);
    return this;
}
  • 设置自定义的 OAuth2User 类型,并与特定的客户端注册ID关联。这个方法已经被弃用。

userAuthoritiesMapper

public UserInfoEndpointConfig userAuthoritiesMapper(GrantedAuthoritiesMapper userAuthoritiesMapper) {
    Assert.notNull(userAuthoritiesMapper, "userAuthoritiesMapper cannot be null");
    OAuth2LoginConfigurer.this.getBuilder().setSharedObject(GrantedAuthoritiesMapper.class, userAuthoritiesMapper);
    return this;
}
  • 设置 GrantedAuthoritiesMapper,用于映射用户的权限。
  • 在客户端进行OIDC验证获得用户信息后,会使用userAuthoritiesMapper获取用户权限信息,然后保存在客户端认证结果中
  • 自定义此配置可以进行额外的权限处理

作用总结

UserInfoEndpointConfig 提供了一组方法,用于配置从用户信息端点获取用户信息的服务以及权限映射。通过这些方法,开发者可以指定自定义的用户服务和权限映射器,从而控制如何从 OAuth 2.0 或 OIDC 提供者处获取和处理用户信息。

;