SpringSecurityOAuth2登录流程分析
有了前面两篇的体验后,我们一定会有很多疑惑,到底是怎么走的这个流程,这一篇就来学习下,分析流程。
1. 打开调试
要想跟踪流程,最重要一步就是打开debug,让执行流程log能打印出来,方便我们查看。
- 第一步在
application.yaml
添加配置
logging:
level:
org.springframework.web: trace
org.springframework.security: trace
org.springframework.security.oauth2: debug
- 第二步在SpringConfig配置类上添加注解并设置debug为true:
@EnableWebSecurity(debug = true)
2. 前置知识
在进行正式的流程分析前,有些知识需要先掌握,就是关于SpringSecurity的,可以先看看官方文档,以便能了解SpringSecurity的架构等,避免源码分析过程会犯懵。
3. 开始流程分析
这个简单,我们已经将调试打开了,就直接启动程序,访问http://localhost:8848/hello
,然后再来分析log
- 首先访问了
/hello
,经过的过滤器如下:
Security filter chain: [
DisableEncodeUrlFilter
WebAsyncManagerIntegrationFilter
SecurityContextPersistenceFilter
HeaderWriterFilter
CsrfFilter
LogoutFilter
OAuth2AuthorizationRequestRedirectFilter
OAuth2LoginAuthenticationFilter
DefaultLoginPageGeneratingFilter
DefaultLogoutPageGeneratingFilter
RequestCacheAwareFilter
SecurityContextHolderAwareRequestFilter
AnonymousAuthenticationFilter
SessionManagementFilter
ExceptionTranslationFilter
AuthorizationFilter
]
然后在看log,如下图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YAqaqGRh-1688110876390)(./img/SpringSecurityOAuth2登录流程分析.md/
)]
可以看到,最终走过这些个过滤器后,中间没有一个处理,最终会进入到了AuthorizationFilter
:
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain)
throws ServletException, IOException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
if (this.observeOncePerRequest && isApplied(request)) {
chain.doFilter(request, response);
return;
}
if (skipDispatch(request)) {
chain.doFilter(request, response);
return;
}
String alreadyFilteredAttributeName = getAlreadyFilteredAttributeName();
request.setAttribute(alreadyFilteredAttributeName, Boolean.TRUE);
try {
AuthorizationDecision decision = this.authorizationManager.check(this::getAuthentication, request);
this.eventPublisher.publishAuthorizationEvent(this::getAuthentication, request, decision);
if (decision != null && !decision.isGranted()) {
throw new AccessDeniedException("Access Denied");
}
chain.doFilter(request, response);
}
finally {
request.removeAttribute(alreadyFilteredAttributeName);
}
}
因为还没有登录,因此权限不足,会抛出一个异常AccessDeniedException
,然后按照过滤器的执行流程,这个过滤器处理完了会往前返回,执行前一个过滤器返回,那么就会来到了ExceptionTranslationFilter
了:
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
try {
chain.doFilter(request, response);
}
catch (IOException ex) {
throw ex;
}
catch (Exception ex) {
// Try to extract a SpringSecurityException from the stacktrace
Throwable[] causeChain = this.throwableAnalyzer.determineCauseChain(ex);
RuntimeException securityException = (AuthenticationException) this.throwableAnalyzer
.getFirstThrowableOfType(AuthenticationException.class, causeChain);
if (securityException == null) {
securityException = (AccessDeniedException) this.throwableAnalyzer
.getFirstThrowableOfType(AccessDeniedException.class, causeChain);
}
if (securityException == null) {
rethrow(ex);
}
if (response.isCommitted()) {
throw new ServletException("Unable to handle the Spring Security Exception "
+ "because the response is already committed.", ex);
}
handleSpringSecurityException(request, response, chain, securityException);
}
}
AccessDeniedException
异常会在这里被捕获,进入到handleSpringSecurityException
方法:
private void handleSpringSecurityException(HttpServletRequest request, HttpServletResponse response,
FilterChain chain, RuntimeException exception) throws IOException, ServletException {
if (exception instanceof AuthenticationException) {
handleAuthenticationException(request, response, chain, (AuthenticationException) exception);
}
else if (exception instanceof AccessDeniedException) {
handleAccessDeniedException(request, response, chain, (AccessDeniedException) exception);
}
}
最后由handleAccessDeniedException
处理:
private void handleAccessDeniedException(HttpServletRequest request, HttpServletResponse response,
FilterChain chain, AccessDeniedException exception) throws ServletException, IOException {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
boolean isAnonymous = this.authenticationTrustResolver.isAnonymous(authentication);
if (isAnonymous || this.authenticationTrustResolver.isRememberMe(authentication)) {
if (logger.isTraceEnabled()) {
logger.trace(LogMessage.format("Sending %s to authentication entry point since access is denied",
authentication), exception);
}
sendStartAuthentication(request, response, chain,
new InsufficientAuthenticationException(
this.messages.getMessage("ExceptionTranslationFilter.insufficientAuthentication",
"Full authentication is required to access this resource")));
}
else {
if (logger.isTraceEnabled()) {
logger.trace(
LogMessage.format("Sending %s to access denied handler since access is denied", authentication),
exception);
}
this.accessDeniedHandler.handle(request, response, exception);
}
}
然后就是走开始认证的流程了sendStartAuthentication
,紧接着就重定向到了/login
:
- 重定向到
/login
从log看到,当重定向到/login
后,会被过滤器DefaultLoginPageGeneratingFilter
匹配到,因此会执行这个过滤器,我们看看源码(这里只放部分,太多了放不下):
public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
public static final String DEFAULT_LOGIN_PAGE_URL = "/login";
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain);
}
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
boolean loginError = isErrorPage(request);
boolean logoutSuccess = isLogoutSuccess(request);
if (isLoginUrlRequest(request) || loginError || logoutSuccess) {
String loginPageHtml = generateLoginPageHtml(request, loginError, logoutSuccess);
response.setContentType("text/html;charset=UTF-8");
response.setContentLength(loginPageHtml.getBytes(StandardCharsets.UTF_8).length);
response.getWriter().write(loginPageHtml);
return;
}
chain.doFilter(request, response);
}
private boolean isLoginUrlRequest(HttpServletRequest request) {
return matches(request, this.loginPageUrl);
}
}
从源码看到,这个过滤器默认匹配的就是/login
,而且过滤器中最终执行的就是doFilter
方法,也可以看到就是这个generateLoginPageHtml
方法生成我们所看到的的登录页面:
- 点击Gitee登录
在这一步,我们需要借助浏览器的控制台来看看url的跳转,重点关注圈出来的
- 点击gitee按钮后,应用的请求为:
http://localhost:8844/oauth2/authorization/gitee
,然后会302重定向到授权服务器发起授权请求 - 授权请求为:
https://gitee.com/oauth/authorize?response_type=code&client_id=69859723556cb37accf04c937c2731378619fb4c0915f5569dbdebc6e2e39403&state=L6PjAzQNqwyC0nF1mWgZydNhO46wCdZCXS8AZU14uLM%3D&redirect_uri=http://localhost:8844/login/oauth2/code/gitee
- 授权服务器则会继续302重定向到第三方应用的登录页面:
https://gitee.com/login?redirect_to_url=https%3A%2F%2Fgitee.com%2Foauth%2Fauthorize%3Fresponse_type%3Dcode%26client_id%3D69859723556cb37accf04c937c2731378619fb4c0915f5569dbdebc6e2e39403%26state%3DL6PjAzQNqwyC0nF1mWgZydNhO46wCdZCXS8AZU14uLM%253D%26redirect_uri%3Dhttp%3A%2F%2Flocalhost%3A8844%2Flogin%2Foauth2%2Fcode%2Fgitee
其实这里能看到这些请求的路径,就是之前配置文件配置的Provider里的url+一系列的参数
然后我们来看看log日志是怎样的:
- 第一个请求:
/oauth2/authorization/gitee
,被过滤器OAuth2AuthorizationRequestRedirectFilter
处理
进入这个过滤器OAuth2AuthorizationRequestRedirectFilter
源码:
public class OAuth2AuthorizationRequestRedirectFilter extends OncePerRequestFilter {
public static final String DEFAULT_AUTHORIZATION_REQUEST_BASE_URI = "/oauth2/authorization";
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
try {
OAuth2AuthorizationRequest authorizationRequest = this.authorizationRequestResolver.resolve(request);
if (authorizationRequest != null) {
this.sendRedirectForAuthorization(request, response, authorizationRequest);
return;
}
} catch (Exception var11) {
this.unsuccessfulRedirectForAuthorization(request, response, var11);
return;
}
try {
filterChain.doFilter(request, response);
} catch (IOException var9) {
throw var9;
} catch (Exception var10) {
Throwable[] causeChain = this.throwableAnalyzer.determineCauseChain(var10);
ClientAuthorizationRequiredException authzEx = (ClientAuthorizationRequiredException)this.throwableAnalyzer.getFirstThrowableOfType(ClientAuthorizationRequiredException.class, causeChain);
if (authzEx != null) {
try {
OAuth2AuthorizationRequest authorizationRequest = this.authorizationRequestResolver.resolve(request, authzEx.getClientRegistrationId());
if (authorizationRequest == null) {
throw authzEx;
}
this.requestCache.saveRequest(request, response);
this.sendRedirectForAuthorization(request, response, authorizationRequest);
} catch (Exception var8) {
this.unsuccessfulRedirectForAuthorization(request, response, var8);
}
} else if (var10 instanceof ServletException) {
throw (ServletException)var10;
} else if (var10 instanceof RuntimeException) {
throw (RuntimeException)var10;
} else {
throw new RuntimeException(var10);
}
}
}
}
主要核心还是看doFilterInternal(因为继承了OncePerRequestFilter,doFilter里调用了doFilterInternal),这里最重要的便是这句: OAuth2AuthorizationRequest authorizationRequest = this.authorizationRequestResolver.resolve(request);
authorizationRequestResolver是一个提取器,从请求中提取信息,然后返回一个OAuth2AuthorizationRequest
类型的请求
从构造方法中可看到,this.authorizationRequestResolver = new DefaultOAuth2AuthorizationRequestResolver(clientRegistrationRepository, authorizationRequestBaseUri);
,使用的是一个默认的DefaultOAuth2AuthorizationRequestResolver
,肯定得进去看看他做了啥:
public final class DefaultOAuth2AuthorizationRequestResolver implements OAuth2AuthorizationRequestResolver {
public DefaultOAuth2AuthorizationRequestResolver(ClientRegistrationRepository clientRegistrationRepository, String authorizationRequestBaseUri) {
Assert.notNull(clientRegistrationRepository, "clientRegistrationRepository cannot be null");
Assert.hasText(authorizationRequestBaseUri, "authorizationRequestBaseUri cannot be empty");
this.clientRegistrationRepository = clientRegistrationRepository;
this.authorizationRequestMatcher = new AntPathRequestMatcher(authorizationRequestBaseUri + "/{" + "registrationId" + "}");
}
public OAuth2AuthorizationRequest resolve(HttpServletRequest request) {
String registrationId = this.resolveRegistrationId(request);
if (registrationId == null) {
return null;
} else {
String redirectUriAction = this.getAction(request, "login");
return this.resolve(request, registrationId, redirectUriAction);
}
}
public OAuth2AuthorizationRequest resolve(HttpServletRequest request, String registrationId) {
if (registrationId == null) {
return null;
} else {
String redirectUriAction = this.getAction(request, "authorize");
return this.resolve(request, registrationId, redirectUriAction);
}
}
private OAuth2AuthorizationRequest resolve(HttpServletRequest request, String registrationId, String redirectUriAction) {
if (registrationId == null) {
return null;
} else {
ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(registrationId);
if (clientRegistration == null) {
throw new IllegalArgumentException("Invalid Client Registration with Id: " + registrationId);
} else {
Builder builder = this.getBuilder(clientRegistration);
String redirectUriStr = expandRedirectUri(request, clientRegistration, redirectUriAction);
builder.clientId(clientRegistration.getClientId()).authorizationUri(clientRegistration.getProviderDetails().getAuthorizationUri()).redirectUri(redirectUriStr).scopes(clientRegistration.getScopes()).state(DEFAULT_STATE_GENERATOR.generateKey());
this.authorizationRequestCustomizer.accept(builder);
return builder.build();
}
}
}
private Builder getBuilder(ClientRegistration clientRegistration) {
if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(clientRegistration.getAuthorizationGrantType())) {
Builder builder = OAuth2AuthorizationRequest.authorizationCode().attributes((attrs) -> {
attrs.put("registration_id", clientRegistration.getRegistrationId());
});
if (!CollectionUtils.isEmpty(clientRegistration.getScopes()) && clientRegistration.getScopes().contains("openid")) {
applyNonce(builder);
}
if (ClientAuthenticationMethod.NONE.equals(clientRegistration.getClientAuthenticationMethod())) {
DEFAULT_PKCE_APPLIER.accept(builder);
}
return builder;
} else if (AuthorizationGrantType.IMPLICIT.equals(clientRegistration.getAuthorizationGrantType())) {
return OAuth2AuthorizationRequest.implicit();
} else {
throw new IllegalArgumentException("Invalid Authorization Grant Type (" + clientRegistration.getAuthorizationGrantType().getValue() + ") for Client Registration with Id: " + clientRegistration.getRegistrationId());
}
}
}
在这里定义了一个AntPathRequestMatcher,匹配的路径是/oauth2/authorization/{registrationId}
,正好是我们的请求/oauth2/authorization/gitee
,所以才会在这个过滤器中处理,再接着看resolve方法,resolve方法首先是根据这个registrationId去查找对应的客户端注册信息,然后利用Builder构建一个OAuth2AuthorizationRequest
,注意最后还调用了一个this.authorizationRequestCustomizer.accept(builder);
,从名字Customizer可猜测这个是一个自定义的地方,可以看看这个this.authorizationRequestCustomizer
是什么:
private Consumer<Builder> authorizationRequestCustomizer = (customizer) -> {
};
public void setAuthorizationRequestCustomizer(Consumer<Builder> authorizationRequestCustomizer) {
Assert.notNull(authorizationRequestCustomizer, "authorizationRequestCustomizer cannot be null");
this.authorizationRequestCustomizer = authorizationRequestCustomizer;
}
可以看到初始化时是一个空的实现,但是提供了set方法,也就是我们后续要自定义构建这个OAuth2AuthorizationRequest
请求可以从这里入手
最后再回到这个resolve
的返回:
OAuth2AuthorizationRequest authorizationRequest = this.authorizationRequestResolver.resolve(request);
if (authorizationRequest != null) {
this.sendRedirectForAuthorization(request, response, authorizationRequest);
return;
}
private void sendRedirectForAuthorization(HttpServletRequest request, HttpServletResponse response, OAuth2AuthorizationRequest authorizationRequest) throws IOException {
if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(authorizationRequest.getGrantType())) {
this.authorizationRequestRepository.saveAuthorizationRequest(authorizationRequest, request, response);
}
this.authorizationRedirectStrategy.sendRedirect(request, response, authorizationRequest.getAuthorizationRequestUri());
}
可以看出来,最后就是重定向,这里的重定向是到了第三方的授权服务器,去请求授权码了,也就是第二个请求https://gitee.com/oauth/authorize?response_type=code&client_id=69859723556cb37accf04c937c2731378619fb4c0915f5569dbdebc6e2e39403&state=L6PjAzQNqwyC0nF1mWgZydNhO46wCdZCXS8AZU14uLM%3D&redirect_uri=http://localhost:8844/login/oauth2/code/gitee
(从log中也可以看到)
- 使用Gitee账号进行登录授权
在Gitee的登录页面输入账号和密码,点击登录,授权成功,最后直接就访问到了我们最初始要访问的/hello
这里涉及几个步骤:
- 点击登录后,用户授权后,gitee会302重定向到我们配置好的回调url,并附带了一个code和state:
http://localhost:8844/login/oauth2/code/gitee?code=48733f0e9f740d09b6823edc61e7fe105c84cad5365562a26985891113a7d0ed&state=L6PjAzQNqwyC0nF1mWgZydNhO46wCdZCXS8AZU14uLM%3D
- 然后咱们的应用就会拿到这个授权码code和state去请求gitee,获取access_token,然后拿着access_token去获取用户信息(由于安全性,这一部分是跟浏览器无关的,所以在浏览器上看不到,等到源码log里可以看到)
当我们登录第三方账号后,也同意授权了,gitee就会重定向到我们之前配置的路径,并且将code给我们,看看log部分:
这里涉及的步骤比较多,而且都是后端处理的,咱们一步步看
- gitee的授权服务器重定向到我们的回调url是:
/login/oauth2/code/gitee
,是被过滤器OAuth2LoginAuthenticationProvider
所匹配处理
public class OAuth2LoginAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public static final String DEFAULT_FILTER_PROCESSES_URI = "/login/oauth2/code/*";
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
MultiValueMap<String, String> params = OAuth2AuthorizationResponseUtils.toMultiMap(request.getParameterMap());
if (!OAuth2AuthorizationResponseUtils.isAuthorizationResponse(params)) {
OAuth2Error oauth2Error = new OAuth2Error("invalid_request");
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
} else {
OAuth2AuthorizationRequest authorizationRequest = this.authorizationRequestRepository.removeAuthorizationRequest(request, response);
if (authorizationRequest == null) {
OAuth2Error oauth2Error = new OAuth2Error("authorization_request_not_found");
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
} else {
String registrationId = (String)authorizationRequest.getAttribute("registration_id");
ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(registrationId);
if (clientRegistration == null) {
OAuth2Error oauth2Error = new OAuth2Error("client_registration_not_found", "Client Registration not found with Id: " + registrationId, (String)null);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
} else {
String redirectUri = UriComponentsBuilder.fromHttpUrl(UrlUtils.buildFullRequestUrl(request)).replaceQuery((String)null).build().toUriString();
OAuth2AuthorizationResponse authorizationResponse = OAuth2AuthorizationResponseUtils.convert(params, redirectUri);
Object authenticationDetails = this.authenticationDetailsSource.buildDetails(request);
OAuth2LoginAuthenticationToken authenticationRequest = new OAuth2LoginAuthenticationToken(clientRegistration, new OAuth2AuthorizationExchange(authorizationRequest, authorizationResponse));
authenticationRequest.setDetails(authenticationDetails);
OAuth2LoginAuthenticationToken authenticationResult = (OAuth2LoginAuthenticationToken)this.getAuthenticationManager().authenticate(authenticationRequest);
OAuth2AuthenticationToken oauth2Authentication = (OAuth2AuthenticationToken)this.authenticationResultConverter.convert(authenticationResult);
Assert.notNull(oauth2Authentication, "authentication result cannot be null");
oauth2Authentication.setDetails(authenticationDetails);
OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(authenticationResult.getClientRegistration(), oauth2Authentication.getName(), authenticationResult.getAccessToken(), authenticationResult.getRefreshToken());
this.authorizedClientRepository.saveAuthorizedClient(authorizedClient, oauth2Authentication, request, response);
return oauth2Authentication;
}
}
}
}
}
因为是继承自AbstractAuthenticationProcessingFilter
,内部的doFilter最终会调用attemptAuthentication
方法(尝试认证),这个方法做了以下处理:
- 第一步:从请求中提取出参数(也就是code和state),如果没有,那就是非法请求
- 第二步:从httpsession中取出
OAuth2AuthorizationRequestRedirectFilter
中保存的授权请求,如果找不到,那就是非法请求 - 第三步:从clientRegistrationRepository中找到registration_id(这里是gitee)的客户端应用
- 第四步:从请求中将code,构造一个
OAuth2LoginAuthenticationToken
类型的authenticationRequest - 第五步:调用AuthenticationManager进行认证,
this.getAuthenticationManager().authenticate(authenticationRequest);
- 第六步:认证完成后,将认证结果转换为一个
OAuth2AuthenticationToken
,里面包含了用户信息
- 在上面的流程中,最重要的就是第五步,里面涵盖了两个操作,通过授权码获取access_token以及使用access_token获取用户信息
根据之前的看的官方文档,AuthenticationManager
是委托给了ProviderManager
去操作认证的,在ProviderManager
里有authenticate
方法:
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
AuthenticationException parentException = null;
Authentication result = null;
Authentication parentResult = null;
int currentPosition = 0;
int size = this.providers.size();
Iterator var9 = this.getProviders().iterator();
while(var9.hasNext()) {
AuthenticationProvider provider = (AuthenticationProvider)var9.next();
if (provider.supports(toTest)) {
if (logger.isTraceEnabled()) {
Log var10000 = logger;
String var10002 = provider.getClass().getSimpleName();
++currentPosition;
var10000.trace(LogMessage.format("Authenticating request with %s (%d/%d)", var10002, currentPosition, size));
}
try {
result = provider.authenticate(authentication);
if (result != null) {
this.copyDetails(authentication, result);
break;
}
} catch (InternalAuthenticationServiceException | AccountStatusException var14) {
this.prepareException(var14, authentication);
throw var14;
} catch (AuthenticationException var15) {
lastException = var15;
}
}
}
if (result == null && this.parent != null) {
try {
parentResult = this.parent.authenticate(authentication);
result = parentResult;
} catch (ProviderNotFoundException var12) {
} catch (AuthenticationException var13) {
parentException = var13;
lastException = var13;
}
}
if (result != null) {
if (this.eraseCredentialsAfterAuthentication && result instanceof CredentialsContainer) {
((CredentialsContainer)result).eraseCredentials();
}
if (parentResult == null) {
this.eventPublisher.publishAuthenticationSuccess(result);
}
return result;
} else {
if (lastException == null) {
lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound", new Object[]{toTest.getName()}, "No AuthenticationProvider found for {0}"));
}
if (parentException == null) {
this.prepareException((AuthenticationException)lastException, authentication);
}
throw lastException;
}
}
从代码中看到,ProviderManager
实际上又是通过遍历所有的AuthenticationProvider
来查找合适处理的Provider,我们可以在IDEA用快捷键ctrl+h
查看AuthenticationProvider
的实现类有以下:
很明显可以看到这两个是跟OAuth2 Login有关的,通过debug打断点的方式也可以印证这个,实际找到的就是OAuth2LoginAuthenticationProvider
:
public OAuth2LoginAuthenticationProvider(OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient, OAuth2UserService<OAuth2UserRequest, OAuth2User> userService) {
Assert.notNull(userService, "userService cannot be null");
this.authorizationCodeAuthenticationProvider = new OAuth2AuthorizationCodeAuthenticationProvider(accessTokenResponseClient);
this.userService = userService;
}
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
OAuth2LoginAuthenticationToken loginAuthenticationToken = (OAuth2LoginAuthenticationToken)authentication;
if (loginAuthenticationToken.getAuthorizationExchange().getAuthorizationRequest().getScopes().contains("openid")) {
return null;
} else {
OAuth2AuthorizationCodeAuthenticationToken authorizationCodeAuthenticationToken;
try {
authorizationCodeAuthenticationToken = (OAuth2AuthorizationCodeAuthenticationToken)this.authorizationCodeAuthenticationProvider.authenticate(new OAuth2AuthorizationCodeAuthenticationToken(loginAuthenticationToken.getClientRegistration(), loginAuthenticationToken.getAuthorizationExchange()));
} catch (OAuth2AuthorizationException var9) {
OAuth2Error oauth2Error = var9.getError();
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString(), var9);
}
OAuth2AccessToken accessToken = authorizationCodeAuthenticationToken.getAccessToken();
Map<String, Object> additionalParameters = authorizationCodeAuthenticationToken.getAdditionalParameters();
OAuth2User oauth2User = this.userService.loadUser(new OAuth2UserRequest(loginAuthenticationToken.getClientRegistration(), accessToken, additionalParameters));
Collection<? extends GrantedAuthority> mappedAuthorities = this.authoritiesMapper.mapAuthorities(oauth2User.getAuthorities());
OAuth2LoginAuthenticationToken authenticationResult = new OAuth2LoginAuthenticationToken(loginAuthenticationToken.getClientRegistration(), loginAuthenticationToken.getAuthorizationExchange(), oauth2User, mappedAuthorities, accessToken, authorizationCodeAuthenticationToken.getRefreshToken());
authenticationResult.setDetails(loginAuthenticationToken.getDetails());
return authenticationResult;
}
}
好家伙,从这句authorizationCodeAuthenticationToken = (OAuth2AuthorizationCodeAuthenticationToken)this.authorizationCodeAuthenticationProvider.authenticate(new OAuth2AuthorizationCodeAuthenticationToken(loginAuthenticationToken.getClientRegistration(), loginAuthenticationToken.getAuthorizationExchange()));
可以看到这里面居然是套了OAuth2AuthorizationCodeAuthenticationProvider
,那就得先看看他的authenticate
方法:
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
OAuth2AuthorizationCodeAuthenticationToken authorizationCodeAuthentication = (OAuth2AuthorizationCodeAuthenticationToken)authentication;
OAuth2AuthorizationResponse authorizationResponse = authorizationCodeAuthentication.getAuthorizationExchange().getAuthorizationResponse();
if (authorizationResponse.statusError()) {
throw new OAuth2AuthorizationException(authorizationResponse.getError());
} else {
OAuth2AuthorizationRequest authorizationRequest = authorizationCodeAuthentication.getAuthorizationExchange().getAuthorizationRequest();
// 先比较请求的state和响应的state是否一致
if (!authorizationResponse.getState().equals(authorizationRequest.getState())) {
OAuth2Error oauth2Error = new OAuth2Error("invalid_state_parameter");
throw new OAuth2AuthorizationException(oauth2Error);
} else {
// 调用accessTokenResponseClient的getTokenResponse去获取access_token
OAuth2AccessTokenResponse accessTokenResponse = this.accessTokenResponseClient.getTokenResponse(new OAuth2AuthorizationCodeGrantRequest(authorizationCodeAuthentication.getClientRegistration(), authorizationCodeAuthentication.getAuthorizationExchange()));
// 构造一个带有access_token等信息的result,最后返回
OAuth2AuthorizationCodeAuthenticationToken authenticationResult = new OAuth2AuthorizationCodeAuthenticationToken(authorizationCodeAuthentication.getClientRegistration(), authorizationCodeAuthentication.getAuthorizationExchange(), accessTokenResponse.getAccessToken(), accessTokenResponse.getRefreshToken(), accessTokenResponse.getAdditionalParameters());
authenticationResult.setDetails(authorizationCodeAuthentication.getDetails());
return authenticationResult;
}
}
}
在这个方法中,比较核心的就是this.accessTokenResponseClient.getTokenResponse
去获取access_token的过程,而这个accessTokenResponseClient是一个OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest>
,根据代码追溯或者查看他的实现类,可以看到他是DefaultAuthorizationCodeTokenResponseClient
,再进去看看他的getTokenResponse
:
private static final String INVALID_TOKEN_RESPONSE_ERROR_CODE = "invalid_token_response";
private Converter<OAuth2AuthorizationCodeGrantRequest, RequestEntity<?>> requestEntityConverter = new OAuth2AuthorizationCodeGrantRequestEntityConverter();
private RestOperations restOperations;
public DefaultAuthorizationCodeTokenResponseClient() {
RestTemplate restTemplate = new RestTemplate(Arrays.asList(new FormHttpMessageConverter(), new OAuth2AccessTokenResponseHttpMessageConverter()));
restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
this.restOperations = restTemplate;
}
public OAuth2AccessTokenResponse getTokenResponse(OAuth2AuthorizationCodeGrantRequest authorizationCodeGrantRequest) {
Assert.notNull(authorizationCodeGrantRequest, "authorizationCodeGrantRequest cannot be null");
// 首先对请求做了一个转换
RequestEntity<?> request = (RequestEntity)this.requestEntityConverter.convert(authorizationCodeGrantRequest);
ResponseEntity<OAuth2AccessTokenResponse> response = this.getResponse(request);
return (OAuth2AccessTokenResponse)response.getBody();
}
private ResponseEntity<OAuth2AccessTokenResponse> getResponse(RequestEntity<?> request) {
try {
// 调用RestTemplate去发起http请求
return this.restOperations.exchange(request, OAuth2AccessTokenResponse.class);
} catch (RestClientException var4) {
OAuth2Error oauth2Error = new OAuth2Error("invalid_token_response", "An error occurred while attempting to retrieve the OAuth 2.0 Access Token Response: " + var4.getMessage(), (String)null);
throw new OAuth2AuthorizationException(oauth2Error, var4);
}
}
// 自定义请求实体转换器的set方法
public void setRequestEntityConverter(Converter<OAuth2AuthorizationCodeGrantRequest, RequestEntity<?>> requestEntityConverter) {
Assert.notNull(requestEntityConverter, "requestEntityConverter cannot be null");
this.requestEntityConverter = requestEntityConverter;
}
// 自定义RestTemplate的set方法
public void setRestOperations(RestOperations restOperations) {
Assert.notNull(restOperations, "restOperations cannot be null");
this.restOperations = restOperations;
}
这个方法值得注意,它也是可以自定义的,后续需要自定义的地方会用到的,支持自定义请求实体参数转换以及HTTP请求RestTemplate
RequestEntity<?> request = (RequestEntity)this.requestEntityConverter.convert(authorizationCodeGrantRequest);
这里的this.requestEntityConverter = new OAuth2AuthorizationCodeGrantRequestEntityConverter();
使用了默认提供的转换器:
public class OAuth2AuthorizationCodeGrantRequestEntityConverter extends AbstractOAuth2AuthorizationGrantRequestEntityConverter<OAuth2AuthorizationCodeGrantRequest> {
public OAuth2AuthorizationCodeGrantRequestEntityConverter() {
}
protected MultiValueMap<String, String> createParameters(OAuth2AuthorizationCodeGrantRequest authorizationCodeGrantRequest) {
ClientRegistration clientRegistration = authorizationCodeGrantRequest.getClientRegistration();
OAuth2AuthorizationExchange authorizationExchange = authorizationCodeGrantRequest.getAuthorizationExchange();
MultiValueMap<String, String> parameters = new LinkedMultiValueMap();
parameters.add("grant_type", authorizationCodeGrantRequest.getGrantType().getValue());
parameters.add("code", authorizationExchange.getAuthorizationResponse().getCode());
String redirectUri = authorizationExchange.getAuthorizationRequest().getRedirectUri();
String codeVerifier = (String)authorizationExchange.getAuthorizationRequest().getAttribute("code_verifier");
if (redirectUri != null) {
parameters.add("redirect_uri", redirectUri);
}
if (!ClientAuthenticationMethod.CLIENT_SECRET_BASIC.equals(clientRegistration.getClientAuthenticationMethod()) && !ClientAuthenticationMethod.BASIC.equals(clientRegistration.getClientAuthenticationMethod())) {
parameters.add("client_id", clientRegistration.getClientId());
}
if (ClientAuthenticationMethod.CLIENT_SECRET_POST.equals(clientRegistration.getClientAuthenticationMethod()) || ClientAuthenticationMethod.POST.equals(clientRegistration.getClientAuthenticationMethod())) {
parameters.add("client_secret", clientRegistration.getClientSecret());
}
if (codeVerifier != null) {
parameters.add("code_verifier", codeVerifier);
}
return parameters;
}
}
他的convert方法最终会调用到这里的createParameters方法,这里所做的就是给获取token的请求填入必须的参数,我们可以对比看看gitee的官方文档,就能理解是在干嘛了(所以要是第三方提供的有变化,我们就得自定义了,也就是这里处理):
到了这一步,一切顺利的话,其实是拿到了access_token了,可以回到OAuth2AuthorizationCodeAuthenticationProvider
查看,返回了一个OAuth2AccessTokenResponse
类型的对象,里面就有token:
public final class OAuth2AccessTokenResponse {
private OAuth2AccessToken accessToken;
private OAuth2RefreshToken refreshToken;
private Map<String, Object> additionalParameters;
...
}
然后再做一些封装,返回OAuth2AuthorizationCodeAuthenticationToken
类型的authenticationResult,回到了OAuth2LoginAuthenticationProvider
的authenticate
方法
- 可以看到,在获取到access_token没有问题后,紧接着就是获取用户信息了,这里用了
this.userService.loadUser(new OAuth2UserRequest(loginAuthenticationToken.getClientRegistration(), accessToken, additionalParameters));
这里的userService,按之前的方式找到具体的实现类DefaultOAuth2UserService
,他的loadUser方法就是做了获取用户信息的事:
public class DefaultOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
private Converter<OAuth2UserRequest, RequestEntity<?>> requestEntityConverter = new OAuth2UserRequestEntityConverter();
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
Assert.notNull(userRequest, "userRequest cannot be null");
// 获取Provider的userInfo对应的uri
if (!StringUtils.hasText(userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUri())) {
OAuth2Error oauth2Error = new OAuth2Error("missing_user_info_uri", "Missing required UserInfo Uri in UserInfoEndpoint for Client Registration: " + userRequest.getClientRegistration().getRegistrationId(), (String)null);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
} else {
// 获取对应的用户名的属性名称
String userNameAttributeName = userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName();
if (!StringUtils.hasText(userNameAttributeName)) {
OAuth2Error oauth2Error = new OAuth2Error("missing_user_name_attribute", "Missing required \"user name\" attribute name in UserInfoEndpoint for Client Registration: " + userRequest.getClientRegistration().getRegistrationId(), (String)null);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
} else {
// 这里也是一个请求实体的转换器
RequestEntity<?> request = (RequestEntity)this.requestEntityConverter.convert(userRequest);
// 发起请求,获取响应
ResponseEntity<Map<String, Object>> response = this.getResponse(userRequest, request);
// 处理响应数据
Map<String, Object> userAttributes = (Map)response.getBody();
Set<GrantedAuthority> authorities = new LinkedHashSet();
authorities.add(new OAuth2UserAuthority(userAttributes));
OAuth2AccessToken token = userRequest.getAccessToken();
Iterator var8 = token.getScopes().iterator();
while(var8.hasNext()) {
String authority = (String)var8.next();
authorities.add(new SimpleGrantedAuthority("SCOPE_" + authority));
}
return new DefaultOAuth2User(authorities, userAttributes, userNameAttributeName);
}
}
}
public final void setRequestEntityConverter(Converter<OAuth2UserRequest, RequestEntity<?>> requestEntityConverter) {
Assert.notNull(requestEntityConverter, "requestEntityConverter cannot be null");
this.requestEntityConverter = requestEntityConverter;
}
public final void setRestOperations(RestOperations restOperations) {
Assert.notNull(restOperations, "restOperations cannot be null");
this.restOperations = restOperations;
}
}
这里的代码比较清晰也好理解,这就看看这个请求实体的转换器,默认实现类是OAuth2UserRequestEntityConverter
,同样的也是提供了set方法供我们自定义。
public class OAuth2UserRequestEntityConverter implements Converter<OAuth2UserRequest, RequestEntity<?>> {
private static final MediaType DEFAULT_CONTENT_TYPE = MediaType.valueOf("application/x-www-form-urlencoded;charset=UTF-8");
public OAuth2UserRequestEntityConverter() {
}
public RequestEntity<?> convert(OAuth2UserRequest userRequest) {
ClientRegistration clientRegistration = userRequest.getClientRegistration();
HttpMethod httpMethod = this.getHttpMethod(clientRegistration);
HttpHeaders headers = new HttpHeaders();
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
URI uri = UriComponentsBuilder.fromUriString(clientRegistration.getProviderDetails().getUserInfoEndpoint().getUri()).build().toUri();
RequestEntity request;
if (HttpMethod.POST.equals(httpMethod)) {
headers.setContentType(DEFAULT_CONTENT_TYPE);
MultiValueMap<String, String> formParameters = new LinkedMultiValueMap();
formParameters.add("access_token", userRequest.getAccessToken().getTokenValue());
request = new RequestEntity(formParameters, headers, httpMethod, uri);
} else {
headers.setBearerAuth(userRequest.getAccessToken().getTokenValue());
request = new RequestEntity(headers, httpMethod, uri);
}
return request;
}
private HttpMethod getHttpMethod(ClientRegistration clientRegistration) {
return AuthenticationMethod.FORM.equals(clientRegistration.getProviderDetails().getUserInfoEndpoint().getAuthenticationMethod()) ? HttpMethod.POST : HttpMethod.GET;
}
}
之后就是调用RestTemplate去发起请求,最终返回一个DefaultOAuth2User类型对象,最后逐层返回到了OAuth2LoginAuthenticationFilter
。
基本上到这里就走完了流程,然后就是去访问我们最初的/hello
,因为已经有用户登录过,所以是可以访问到的。
4. 总结
整个流程跟着源码走下来,结合log分析,还是能大体上理解的,不奢求所有细节都掌握,其中实际上跟我们关系比较大的就是几处自定义的地方,毕竟我们如果要自定义一些东西,还是得仰仗他们,比如微信的授权登录,就会用到,他有很多不同之处,也有一些坑,这个就留到下一篇再学习好了。