SpringSecurityOAuth2登录流程分析

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

  1. 首先访问了/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:
在这里插入图片描述

  1. 重定向到/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方法生成我们所看到的的登录页面:
在这里插入图片描述

  1. 点击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中也可以看到)

  1. 使用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部分:
在这里插入图片描述

这里涉及的步骤比较多,而且都是后端处理的,咱们一步步看

  1. 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,里面包含了用户信息
  1. 在上面的流程中,最重要的就是第五步,里面涵盖了两个操作,通过授权码获取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,回到了OAuth2LoginAuthenticationProviderauthenticate方法
在这里插入图片描述

  1. 可以看到,在获取到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分析,还是能大体上理解的,不奢求所有细节都掌握,其中实际上跟我们关系比较大的就是几处自定义的地方,毕竟我们如果要自定义一些东西,还是得仰仗他们,比如微信的授权登录,就会用到,他有很多不同之处,也有一些坑,这个就留到下一篇再学习好了。

posted @ 2024-02-04 17:48  CharyGao  阅读(122)  评论(0编辑  收藏  举报