天天看点

OAuth2.0原理分析OAuth2.0原理分析

OAuth2.0原理分析

授权服务器

@EnableAuthorizationServer解析

我们都知道 一个授权认证服务器最最核心的就是 @EnableAuthorizationServer , 那么 @EnableAuthorizationServer 主要做了什么呢? 我们看下 @EnableAuthorizationServer 源码:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import({AuthorizationServerEndpointsConfiguration.class, AuthorizationServerSecurityConfiguration.class})
public @interface EnableAuthorizationServer {
}
           

我们可以看到其源码内部导入了 AuthorizationServerEndpointsConfiguration 和 AuthorizationServerSecurityConfiguration 这2个配置类。 接下来我们分别看下这2个配置类具体做了什么。

AuthorizationServerEndpointsConfiguration

@Configuration
@Import(TokenKeyEndpointRegistrar.class)
public class AuthorizationServerEndpointsConfiguration {

    // 省略 其他相关配置代码
  ....

    // 1、 AuthorizationEndpoint 创建
    @Bean
    public AuthorizationEndpoint authorizationEndpoint() throws Exception {
        AuthorizationEndpoint authorizationEndpoint = new AuthorizationEndpoint();
        FrameworkEndpointHandlerMapping mapping = getEndpointsConfigurer().getFrameworkEndpointHandlerMapping();
        authorizationEndpoint.setUserApprovalPage(extractPath(mapping, "/oauth/confirm_access"));
        authorizationEndpoint.setProviderExceptionHandler(exceptionTranslator());
        authorizationEndpoint.setErrorPage(extractPath(mapping, "/oauth/error"));
        authorizationEndpoint.setTokenGranter(tokenGranter());
        authorizationEndpoint.setClientDetailsService(clientDetailsService);
        authorizationEndpoint.setAuthorizationCodeServices(authorizationCodeServices());
        authorizationEndpoint.setOAuth2RequestFactory(oauth2RequestFactory());
        authorizationEndpoint.setOAuth2RequestValidator(oauth2RequestValidator());
        authorizationEndpoint.setUserApprovalHandler(userApprovalHandler());
        authorizationEndpoint.setRedirectResolver(redirectResolver());
        return authorizationEndpoint;
    }

    // 2、 TokenEndpoint 创建
    @Bean
    public TokenEndpoint tokenEndpoint() throws Exception {
        TokenEndpoint tokenEndpoint = new TokenEndpoint();
        tokenEndpoint.setClientDetailsService(clientDetailsService);
        tokenEndpoint.setProviderExceptionHandler(exceptionTranslator());
        tokenEndpoint.setTokenGranter(tokenGranter());
        tokenEndpoint.setOAuth2RequestFactory(oauth2RequestFactory());
        tokenEndpoint.setOAuth2RequestValidator(oauth2RequestValidator());
        tokenEndpoint.setAllowedRequestMethods(allowedTokenEndpointRequestMethods());
        return tokenEndpoint;
    }

    // 省略 其他相关配置代码
   ....
           

通过源码我们可以很明确的知道:

  • AuthorizationEndpoint 用于服务授权请求。预设地址:/oauth/authorize。
  • TokenEndpoint 用于服务访问令牌的请求。预设地址:/oauth/token。

AuthorizationServerSecurityConfiguration

  • ClientDetailsService : 内部仅有 loadClientByClientId 方法。从方法名我们就可知其是通过 clientId 来获取 Client 信息, 官方提供 JdbcClientDetailsService、InMemoryClientDetailsService 2个实现类,我们也可以像UserDetailsService 一样编写自己的实现类。
  • UserDetailsService : 内部仅有 loadUserByUsername 方法。这个类不用我再介绍了吧。不清楚得同学可以看下我之前得文章。
  • ClientDetailsUserDetailsService : UserDetailsService子类,内部维护了 ClientDetailsService 。其 loadUserByUsername 方法重写后调用ClientDetailsService.loadClientByClientId()。
  • ClientCredentialsTokenEndpointFilter** 作用与 UserNamePasswordAuthenticationFilter 类似,通过拦截 /oauth/token 地址,获取到 clientId 和 clientSecret 信息并创建 UsernamePasswordAuthenticationToken 作为 AuthenticationManager.authenticate() 参数 调用认证过程。整个认证过程唯一最大得区别在于 DaoAuthenticationProvider.retrieveUser() 获取认证用户信息时调用的是 ClientDetailsUserDetailsService,根据前面讲述的其内部其实是调用ClientDetailsService 获取到客户端信息。

@EnableResourceServer

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import({ResourceServerConfiguration.class})
public @interface EnableResourceServer {
}
           

从源码中我们可以看到其导入了 ResourceServerConfiguration 配置类,这个配置类最核心的配置是 应用了 ResourceServerSecurityConfigurer ,我这边贴出 ResourceServerSecurityConfigurer 源码 最核心的配置代码如下:

public void configure(HttpSecurity http) throws Exception {
    AuthenticationManager oauthAuthenticationManager = this.oauthAuthenticationManager(http);
    this.resourcesServerFilter = new OAuth2AuthenticationProcessingFilter();
    this.resourcesServerFilter.setAuthenticationEntryPoint(this.authenticationEntryPoint);
    this.resourcesServerFilter.setAuthenticationManager(oauthAuthenticationManager);
    if (this.eventPublisher != null) {
        this.resourcesServerFilter.setAuthenticationEventPublisher(this.eventPublisher);
    }

    if (this.tokenExtractor != null) {
        this.resourcesServerFilter.setTokenExtractor(this.tokenExtractor);
    }

    this.resourcesServerFilter = (OAuth2AuthenticationProcessingFilter)this.postProcess(this.resourcesServerFilter);
    this.resourcesServerFilter.setStateless(this.stateless);
    ((HttpSecurity)http.authorizeRequests().expressionHandler(this.expressionHandler).and()).addFilterBefore(this.resourcesServerFilter, AbstractPreAuthenticatedProcessingFilter.class).exceptionHandling().accessDeniedHandler(this.accessDeniedHandler).authenticationEntryPoint(this.authenticationEntryPoint);
}

private AuthenticationManager oauthAuthenticationManager(HttpSecurity http) {
    OAuth2AuthenticationManager oauthAuthenticationManager = new OAuth2AuthenticationManager();
    if (this.authenticationManager != null) {
        if (!(this.authenticationManager instanceof OAuth2AuthenticationManager)) {
            return this.authenticationManager;
        }

        oauthAuthenticationManager = (OAuth2AuthenticationManager)this.authenticationManager;
    }

    oauthAuthenticationManager.setResourceId(this.resourceId);
    oauthAuthenticationManager.setTokenServices(this.resourceTokenServices(http));
    oauthAuthenticationManager.setClientDetailsService(this.clientDetails());
    return oauthAuthenticationManager;
}
           

源码中最核心的 就是 官方文档中介绍的 OAuth2AuthenticationProcessingFilter 过滤器, 其配置分3步:

  • 1、 创建 OAuth2AuthenticationProcessingFilter 过滤器 对象
  • 2、 创建 OAuth2AuthenticationManager 对象 对将其作为参数设置到 OAuth2AuthenticationProcessingFilter 中
  • 3、 将 OAuth2AuthenticationProcessingFilter 过滤器添加到过滤器链上

AuthorizationEndpoint生成授权码

@RequestMapping(value = "/oauth/authorize")
public ModelAndView authorize(Map<String, Object> model, @RequestParam Map<String, String> parameters,
                              SessionStatus sessionStatus, Principal principal) {

    //  1、 通过 OAuth2RequestFactory 从 参数中获取信息创建 AuthorizationRequest 授权请求对象
    AuthorizationRequest authorizationRequest = getOAuth2RequestFactory().createAuthorizationRequest(parameters);

    Set<String> responseTypes = authorizationRequest.getResponseTypes();

    if (!responseTypes.contains("token") && !responseTypes.contains("code")) {
        throw new UnsupportedResponseTypeException("Unsupported response types: " + responseTypes);
    }

    if (authorizationRequest.getClientId() == null) {
        throw new InvalidClientException("A client id must be provided");
    }

    try {
        // 2、 判断  principal 是否 已授权 : /oauth/authorize 设置为无权限访问 ,所以要判断,如果 判断失败则抛出 InsufficientAuthenticationException (AuthenticationException 子类),其异常会被 ExceptionTranslationFilter 处理 ,最终跳转到 登录页面,这也是为什么我们第一次去请求获取 授权码时会跳转到登陆界面的原因
        if (!(principal instanceof Authentication) || !((Authentication) principal).isAuthenticated()) {
            throw new InsufficientAuthenticationException(
                    "User must be authenticated with Spring Security before authorization can be completed.");
        }

        // 3、 通过 ClientDetailsService.loadClientByClientId() 获取到 ClientDetails 客户端信息
        ClientDetails client = getClientDetailsService().loadClientByClientId(authorizationRequest.getClientId());

        // 4、 获取参数中的回调地址并且与系统配置的回调地址对比
        String redirectUriParameter = authorizationRequest.getRequestParameters().get(OAuth2Utils.REDIRECT_URI);
        String resolvedRedirect = redirectResolver.resolveRedirect(redirectUriParameter, client);
        if (!StringUtils.hasText(resolvedRedirect)) {
            throw new RedirectMismatchException(
                    "A redirectUri must be either supplied or preconfigured in the ClientDetails");
        }
        authorizationRequest.setRedirectUri(resolvedRedirect);

        //  5、 验证 scope 
        oauth2RequestValidator.validateScope(authorizationRequest, client);

        //  6、 检测该客户端是否设置自动 授权(即 我们配置客户端时配置的 autoApprove(true)  )
        authorizationRequest = userApprovalHandler.checkForPreApproval(authorizationRequest,
                (Authentication) principal);
        boolean approved = userApprovalHandler.isApproved(authorizationRequest, (Authentication) principal);
        authorizationRequest.setApproved(approved);

        if (authorizationRequest.isApproved()) {
            if (responseTypes.contains("token")) {
                return getImplicitGrantResponse(authorizationRequest);
            }
            if (responseTypes.contains("code")) {
                // 7 调用 getAuthorizationCodeResponse() 方法生成code码并回调到设置的回调地址
                return new ModelAndView(getAuthorizationCodeResponse(authorizationRequest,
                        (Authentication) principal));
            }
        }
        model.put(AUTHORIZATION_REQUEST_ATTR_NAME, authorizationRequest);
        model.put(ORIGINAL_AUTHORIZATION_REQUEST_ATTR_NAME, unmodifiableMap(authorizationRequest));

        return getUserApprovalPageResponse(model, authorizationRequest, (Authentication) principal);

    }
    catch (RuntimeException e) {
        sessionStatus.setComplete();
        throw e;
    }

}
           
  • 1、 通过 OAuth2RequestFactory 从 参数中获取信息创建 AuthorizationRequest 授权请求对象
  • 2、 判断 principal 是否 已授权 : /oauth/authorize 设置为无权限访问 ,所以要判断,如果 判断失败则抛出 InsufficientAuthenticationException (AuthenticationException 子类),其异常会被 ExceptionTranslationFilter 处理 ,最终跳转到 登录页面,这也是为什么我们第一次去请求获取 授权码时会跳转到登陆界面的原因
  • 3、 通过 ClientDetailsService.loadClientByClientId() 获取到 ClientDetails 客户端信息
  • 4、 获取参数中的回调地址并且与系统配置的回调地址(步骤3获取到的client信息)对比
  • 5、 与步骤4一样 验证 scope
  • 6、 检测该客户端是否设置自动 授权(即 我们配置客户端时配置的 autoApprove(true))
  • 7、 由于我们设置 autoApprove(true) 则 调用 getAuthorizationCodeResponse() 方法生成code码并回调到设置的回调地址
  • 8、 真实生成Code 的方法时 generateCode(AuthorizationRequest authorizationRequest, Authentication authentication) 方法: 其内部是authorizationCodeServices.createAuthorizationCode()方法生成code的

TokenEndpoint 生成token

@RequestMapping(value = "/oauth/token", method=RequestMethod.POST)
public ResponseEntity<OAuth2AccessToken> postAccessToken(Principal principal, @RequestParam
        Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {

    // 1、 验证 用户信息 (正常情况下会经过 ClientCredentialsTokenEndpointFilter 过滤器认证后获取到用户信息 )
    if (!(principal instanceof Authentication)) {
        throw new InsufficientAuthenticationException(
                "There is no client authentication. Try adding an appropriate authentication filter.");
    }

    // 2、 通过 ClientDetailsService().loadClientByClientId() 获取系统配置客户端信息
    String clientId = getClientId(principal);
    ClientDetails authenticatedClient = getClientDetailsService().loadClientByClientId(clientId);

    // 3、 通过客户端信息生成 TokenRequest 对象
    TokenRequest tokenRequest = getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient);

 ......

    // 4、 调用 TokenGranter.grant()方法生成 OAuth2AccessToken 对象(即token)
    OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
    if (token == null) {
        throw new UnsupportedGrantTypeException("Unsupported grant type: " + tokenRequest.getGrantType());
    }
    // 5、 返回token
    return getResponse(token);

}
           
  • 1、 验证 用户信息 (正常情况下会经过 ClientCredentialsTokenEndpointFilter 过滤器认证后获取到用户信息 )
  • 2、 通过 ClientDetailsService().loadClientByClientId() 获取系统配置的客户端信息
  • 3、 通过客户端信息生成 TokenRequest 对象
  • 4、 将步骤3获取到的 TokenRequest 作为TokenGranter.grant() 方法参照 生成 OAuth2AccessToken 对象(即token)
  • 5、 返回 token

TokenGranter

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-i0qaFief-1611489790891)(C:\Users\郑瑶\Desktop\T\imag\鉴权\oauth\1610419689(1)].png)

TokenGranter的设计思路是使用CompositeTokenGranter管理一个List列表,每一种grantType对应一个具体的真正授权者,CompositeTokenGranter 内部就是在循环调用五种TokenGranter实现类的 grant方法,而granter内部则是通过grantType来区分是否是各自的授权类型。

五种类型分别是:

  • ResourceOwnerPasswordTokenGranter ==> password密码模式
  • AuthorizationCodeTokenGranter ==> authorization_code授权码模式
  • ClientCredentialsTokenGranter ==> client_credentials客户端模式
  • ImplicitTokenGranter ==> implicit简化模式
  • RefreshTokenGranter ==>refresh_token 刷新token专用

OAuth2AccessToken

@JsonSerialize(
    using = OAuth2AccessTokenJackson1Serializer.class
)
@JsonDeserialize(
    using = OAuth2AccessTokenJackson1Deserializer.class
)
@com.fasterxml.jackson.databind.annotation.JsonSerialize(
    using = OAuth2AccessTokenJackson2Serializer.class
)
@com.fasterxml.jackson.databind.annotation.JsonDeserialize(
    using = OAuth2AccessTokenJackson2Deserializer.class
)
public interface OAuth2AccessToken {
    String BEARER_TYPE = "Bearer";
    String OAUTH2_TYPE = "OAuth2";
    String ACCESS_TOKEN = "access_token";
    String TOKEN_TYPE = "token_type";
    String EXPIRES_IN = "expires_in";
    String REFRESH_TOKEN = "refresh_token";
    String SCOPE = "scope";

}
           

AuthorizationServerTokenServices

public interface AuthorizationServerTokenServices {
    //创建
    OAuth2AccessToken createAccessToken(OAuth2Authentication var1) throws AuthenticationException;
	//刷新
    OAuth2AccessToken refreshAccessToken(String var1, TokenRequest var2) throws AuthenticationException;
	//获取
    OAuth2AccessToken getAccessToken(OAuth2Authentication var1);
}
           

流程

OAuth2.0原理分析OAuth2.0原理分析

资源服务器

@EnableResourceServer

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import({ResourceServerConfiguration.class})
public @interface EnableResourceServer {
}
           

ResourceServerConfiguration

protected void configure(HttpSecurity http) throws Exception {
    ResourceServerSecurityConfigurer resources = new ResourceServerSecurityConfigurer();
    ResourceServerTokenServices services = this.resolveTokenServices();
    if (services != null) {
        resources.tokenServices(services);
    } else if (this.tokenStore != null) {
        resources.tokenStore(this.tokenStore);
    } else if (this.endpoints != null) {
        resources.tokenStore(this.endpoints.getEndpointsConfigurer().getTokenStore());
    }

    if (this.eventPublisher != null) {
        resources.eventPublisher(this.eventPublisher);
    }

    Iterator var4 = this.configurers.iterator();

    ResourceServerConfigurer configurer;
    while(var4.hasNext()) {
        configurer = (ResourceServerConfigurer)var4.next();
        configurer.configure(resources);
    }

    ((HttpSecurity)((HttpSecurity)http.authenticationProvider(new AnonymousAuthenticationProvider("default")).exceptionHandling().accessDeniedHandler(resources.getAccessDeniedHandler()).and()).sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()).csrf().disable();
    http.apply(resources);
    if (this.endpoints != null) {
        http.requestMatcher(new ResourceServerConfiguration.NotOAuthRequestMatcher(this.endpoints.oauth2EndpointHandlerMapping()));
    }

    var4 = this.configurers.iterator();

    while(var4.hasNext()) {
        configurer = (ResourceServerConfigurer)var4.next();
        configurer.configure(http);
    }

    if (this.configurers.isEmpty()) {
        ((AuthorizedUrl)http.authorizeRequests().anyRequest()).authenticated();
    }

}
           

ResourceServerSecurityConfigurer

public void configure(HttpSecurity http) throws Exception {
    AuthenticationManager oauthAuthenticationManager = this.oauthAuthenticationManager(http);
    //创建OAuth2核心过滤器
    this.resourcesServerFilter = new OAuth2AuthenticationProcessingFilter();
    this.resourcesServerFilter.setAuthenticationEntryPoint(this.authenticationEntryPoint);
    //设置OAuth2的身份认证处理器,没有交给spring管理(避免影响非普通的认证流程)
    this.resourcesServerFilter.setAuthenticationManager(oauthAuthenticationManager);
    if (this.eventPublisher != null) {
        this.resourcesServerFilter.setAuthenticationEventPublisher(this.eventPublisher);
    }

    if (this.tokenExtractor != null) {
        //设置TokenExtractor默认的实现BearerTokenExtractor
        this.resourcesServerFilter.setTokenExtractor(this.tokenExtractor);
    }

    this.resourcesServerFilter = (OAuth2AuthenticationProcessingFilter)this.postProcess(this.resourcesServerFilter);
    this.resourcesServerFilter.setStateless(this.stateless);
    // @formatter:off
    ((HttpSecurity)http.authorizeRequests().expressionHandler(this.expressionHandler).and()).addFilterBefore(this.resourcesServerFilter, AbstractPreAuthenticatedProcessingFilter.class).exceptionHandling().accessDeniedHandler(this.accessDeniedHandler).authenticationEntryPoint(this.authenticationEntryPoint);
}
           

OAuth2AuthenticationProcessingFilter

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
    boolean debug = logger.isDebugEnabled();
    HttpServletRequest request = (HttpServletRequest)req;
    HttpServletResponse response = (HttpServletResponse)res;

    try {
        //从请求中取出身份信息,即access_token,封装到 PreAuthenticatedAuthenticationToken
        Authentication authentication = this.tokenExtractor.extract(request);
        if (authentication == null) {
            .....
        } else {
            request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE, authentication.getPrincipal());
            if (authentication instanceof AbstractAuthenticationToken) {
                AbstractAuthenticationToken needsDetails = (AbstractAuthenticationToken)authentication;
                needsDetails.setDetails(this.authenticationDetailsSource.buildDetails(request));
            }
			//认证身份
            Authentication authResult = this.authenticationManager.authenticate(authentication);
            if (debug) {
                logger.debug("Authentication success: " + authResult);
            }
			
            this.eventPublisher.publishAuthenticationSuccess(authResult);
            //将身份信息绑定到SecurityContextHolder中
            SecurityContextHolder.getContext().setAuthentication(authResult);
        }
    } catch (OAuth2Exception var9) {
        SecurityContextHolder.clearContext();
        if (debug) {
            logger.debug("Authentication request failed: " + var9);
        }

        this.eventPublisher.publishAuthenticationFailure(new BadCredentialsException(var9.getMessage(), var9), new PreAuthenticatedAuthenticationToken("access-token", "N/A"));
        this.authenticationEntryPoint.commence(request, response, new InsufficientAuthenticationException(var9.getMessage(), var9));
        return;
    }

    chain.doFilter(request, response);
}
           

整个filter步骤最核心的是下面2个:

  • 1、 调用 tokenExtractor.extract() 方法从请求中解析出token信息并存放到 authentication 的 principal 字段 中
  • 2、 调用 authenticationManager.authenticate() 认证过程: 注意此时的 authenticationManager 是 OAuth2AuthenticationManager

在解析@EnableResourceServer 时我们讲过 OAuth2AuthenticationManager 与 OAuth2AuthenticationProcessingFilter 的关系,这里不再重述,我们直接看下 OAuth2AuthenticationManager 的 authenticate() 方法实现:

public Authentication authenticate(Authentication authentication) throws AuthenticationException {

    if (authentication == null) {
        throw new InvalidTokenException("Invalid token (token not found)");
    }
    // 1、 从 authentication 中获取 token
    String token = (String) authentication.getPrincipal();
    // 2、 调用 tokenServices.loadAuthentication() 方法  通过 token 参数获取到 OAuth2Authentication 对象 ,这里的tokenServices 就是我们资源服务器配置的。
    OAuth2Authentication auth = tokenServices.loadAuthentication(token);
    if (auth == null) {
        throw new InvalidTokenException("Invalid token: " + token);
    }

    Collection<String> resourceIds = auth.getOAuth2Request().getResourceIds();
    if (resourceId != null && resourceIds != null && !resourceIds.isEmpty() && !resourceIds.contains(resourceId)) {
        throw new OAuth2AccessDeniedException("Invalid token does not contain resource id (" + resourceId + ")");
    }
    // 3、 检测客户端信息,由于我们采用授权服务器和资源服务器分离的设计,所以这个检测方法实际没有检测
    checkClientDetails(auth);

    if (authentication.getDetails() instanceof OAuth2AuthenticationDetails) {
        OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) authentication.getDetails();
        // Guard against a cached copy of the same details
        if (!details.equals(auth.getDetails())) {
            // Preserve the authentication details from the one loaded by token services
            details.setDecodedDetails(auth.getDetails());
        }
    }
    // 4、 设置认证成功标识并返回
    auth.setDetails(authentication.getDetails());
    auth.setAuthenticated(true);
    return auth;

}
           

整个 认证逻辑分4步:

  • 1、 从 authentication 中获取 token
  • 2、 调用 tokenServices.loadAuthentication() 方法 通过 token 参数获取到 OAuth2Authentication 对象 ,这里的tokenServices 就是我们资源服务器配置的。
  • 3、 检测客户端信息,由于我们采用授权服务器和资源服务器分离的设计,所以这个检测方法实际没有检测
  • 4、 设置认证成功标识并返回 ,注意返回的是 OAuth2Authentication (Authentication 子类)。

继续阅读