总的来说,OIDC 提供了比 OAuth2 更加完整和安全的身份验证和授权机制,确保在授权第三方应用访问用户资源的同时,也能验证用户的身份。
具体解释
身份认证:
用户信息获取:
安全性增强:
在 OIDC 中,资源服务器主要用于保护用户资源。当客户端访问受保护的资源时,资源服务器通过以下方式验证请求的合法性:
访问令牌验证:
用户信息验证(可选):
openid
scope 的授权请求。确保用户身份的真实性:
防止身份伪造:
安全性增强:
提供用户信息:
单点登录(SSO):
实现用户会话管理:
ID Token 是一个 JWT(JSON Web Token),通常包含以下信息:
OAuth2.0 和 OpenID Connect (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();
OpenID Connect (OIDC) 是在 OAuth2 之上构建的身份认证协议,它引入了 ID Token 来表示用户的身份。OIDC 结合 OAuth2 授权码模式,允许客户端在获取授权后,获取用户身份信息。以下是 OIDC 在 OAuth2 授权码模式中的运行流程及其作用。
OAuth2AuthorizationCodeAuthenticationProvider 生成 ID Token
在 OAuth2 授权码模式中,当客户端请求授权码并换取访问令牌时,如果请求包含
openid
scope,ID Token 将被服务端生成并返回给客户端(与Access_Token
一起返回)。
源码中,当客户端OAuth2LoginAuthenticationFilter
过滤器携带授权码并换取访问令牌时,授权服务OAuth2TokenEndpointFilter
过滤器接收请求,并使用 OAuth2AuthorizationCodeAuthenticationProvider
的 authenticate
方法生成 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;
}
openid
scope:如果请求的 scope 中包含 openid
,则表明这是一个 OIDC 请求,需要生成 ID Token。tokenGenerator
生成 ID Token,并将其添加到授权信息中。最后OAuth2TokenEndpointFilter
过滤器会将token响应给客户端
OidcAuthorizationCodeAuthenticationProvider 会验证并处理 ID Token
在处理授权码交换访问令牌和 ID Token 的过程中,客户端
OAuth2LoginAuthenticationFilter
过滤器会使用OidcAuthorizationCodeAuthenticationProvider
(org.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;
}
openid
scope:如果请求的 scope 中不包含 openid
,则返回 null
,表明不是 OIDC 请求。OAuth2LoginAuthenticationToken
认证结果。由第2步
OidcAuthorizationCodeAuthenticationProvider
的authenticate
方法中进行,用于使用idToken
验证和加载用户信息
代码如下:
OidcUser oidcUser = this.userService.loadUser(new OidcUserRequest(clientRegistration,
accessTokenResponse.getAccessToken(), idToken, additionalParameters));
上面代码会先调用OidcUserService
的loadUser
方法,通过shouldRetrieveUserInfo
方法进行信息检查,然后再由OidcUserService
调用DefaultOAuth2UserService
。
OidcUserService
的loadUser
方法
@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验证:
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);
}
详细解析
Assert.notNull(userRequest, "userRequest cannot be null");
:确保 userRequest
不为空,否则抛出异常。userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUri()
获取 UserInfoEndpoint 的 URI。如果 URI 为空,则构造一个 OAuth2Error
并抛出 OAuth2AuthenticationException
。userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName()
获取 UserInfoEndpoint 中的 userNameAttributeName
。如果 userNameAttributeName
为空,同样构造一个 OAuth2Error
并抛出 OAuth2AuthenticationException
。this.requestEntityConverter.convert(userRequest)
将 userRequest
转换为 RequestEntity
。getResponse(userRequest, request)
向授权服务发起"/userinfo"
请求并接收响应,得到包含用户属性的 Map
。LinkedHashSet
来存储用户的权限。首先将 userAttributes
转换为 OAuth2UserAuthority
并添加到权限集合中。userRequest
中的 OAuth2AccessToken
,并将其所有的范围(scopes)添加为 SimpleGrantedAuthority
,例如 SCOPE_read
。DefaultOAuth2User
实例:
authorities
、用户属性 userAttributes
和 userNameAttributeName
创建并返回一个 DefaultOAuth2User
实例。该方法主要用于根据 OAuth2UserRequest
加载用户信息。它首先检查必要的配置(如 UserInfoEndpoint URI 和 userNameAttributeName),然后发送请求以获取用户信息,最后将这些信息封装为一个 DefaultOAuth2User
对象,并添加相关的权限。
通过这种方式,Spring Security 能够在 OAuth2 登录流程中正确加载并处理用户信息,从而完成用户认证和授权。
授权服务接收到客户端"/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();
}
}
详细解析
匹配请求:
if (!this.userInfoEndpointMatcher.matches(request)) {
filterChain.doFilter(request, response);
return;
}
userInfoEndpointMatcher
匹配。匹配的是"/userinfo"
路径的get
或post
请求。filterChain.doFilter
方法将请求传递到过滤器链中的下一个过滤器,然后返回。尝试处理认证:
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
方法进行认证。使用的是OidcUserInfoAuthenticationProvider
的authenticate
方法:
/*
该方法主要用于处理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
信息,对接到客户端DefaultOAuth2UserService
的loadUser
方法中的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);
}
处理异常:
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()
方法清理安全上下文。代码关键点
userInfoEndpointMatcher
:这是一个 RequestMatcher
对象,用于检查请求是否匹配 UserInfo 端点的 URI。authenticationConverter
:用于将 HttpServletRequest
转换为 Authentication
对象。authenticationManager
:用于处理认证逻辑,调用其 authenticate
方法进行用户认证。authenticationSuccessHandler
和 authenticationFailureHandler
:分别用于处理认证成功和失败的情况。SecurityContextHolder.clearContext()
:用于在请求处理完毕后清理安全上下文,以确保安全性。该方法的主要功能是处理 OpenID Connect 的 UserInfo 端点请求。它首先检查请求是否匹配 UserInfo 端点,然后尝试将请求转换为 Authentication
对象并进行认证,最后根据认证结果处理成功或失败的情况。在整个过程中,它确保了安全上下文的清理。
OAuth2LoginConfigurer的UserInfoEndpointConfig配置
UserInfoEndpointConfig
是 OAuth2LoginConfigurer
类的内部配置类,负责配置 OAuth 2.0 用户信息端点。这段代码主要用于设置从用户信息端点获取用户属性的服务。以下是每个部分的详细解释:
private OAuth2UserService
userService;
用于处理 OAuth 2.0 请求并返回用户信息的服务。
配置的是OAuth2LoginAuthenticationProvider
中使用的的userService
private OAuth2UserService
oidcUserService;
用于处理 OpenID Connect (OIDC) 请求并返回用户信息的服务。
配置的是OidcAuthorizationCodeAuthenticationProvider
中使用的userService
,对应上面源码讲解第3步客户端加载用户信息
private Map
> 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;
}
oidcUserService
public UserInfoEndpointConfig oidcUserService(OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService) {
Assert.notNull(oidcUserService, "oidcUserService cannot be null");
this.oidcUserService = oidcUserService;
return this;
}
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
,用于映射用户的权限。userAuthoritiesMapper
获取用户权限信息,然后保存在客户端认证结果中UserInfoEndpointConfig
提供了一组方法,用于配置从用户信息端点获取用户信息的服务以及权限映射。通过这些方法,开发者可以指定自定义的用户服务和权限映射器,从而控制如何从 OAuth 2.0 或 OIDC 提供者处获取和处理用户信息。