您的当前位置:首页正文

Java中关于OAuth2.0的原理分析

2024-11-04 来源:个人技术集锦

授权服务器

@EnableAuthorizationServer解析

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

我们看下 @EnableAuthorizationServer 源码:

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

我们可以看到其源码内部导入了 AuthorizationServerEndpointsConfigurationAuthorizationServerSecurityConfiguration 这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

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);
}

流程

资源服务器

@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 子类)。

您可能感兴趣的文章:
Top