Spring Security Oauth2 - 源码解析之授权相关组件【03】

一、准备工作

Oauth2 分为4种模式,但是内部运行逻辑大致类似。我们先拿client模式的例子来进行辅助调试,稍微容易一些。

还是从上一篇中,把请求搬过来:

http://localhost:8080/oauth/token?grant_type=client_credentials&scope=select&client_id=client_1&client_secret=123456

响应如下:

{
    "access_token": "652b8bd0-79ab-4c0e-993d-e618b59c090f",
    "token_type": "bearer",
    "expires_in": 43199,
    "scope": "select"
}

二、涉及到的类

ClientCredentialsTokenEndpointFilter
AuthenticationManager --> DaoAuthenticationProvider
TokenEndpoint
TokenGranter
AuthorizationServerTokenServices 

三、代码分析

1、客户端身份认证核心过滤器ClientCredentialsTokenEndpointFilter
public final class AuthorizationServerSecurityConfigurer extends
		SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
    
    private String realm = "oauth2/client";
    
    private boolean allowFormAuthenticationForClients = false;
    
    .....
        
    @Override
	public void configure(HttpSecurity http) throws Exception {
		
		// ensure this is initialized
		frameworkEndpointHandlerMapping();
		if (allowFormAuthenticationForClients) {
			clientCredentialsTokenEndpointFilter(http);
		}

        // 将
		for (Filter filter : tokenEndpointAuthenticationFilters) {
			http.addFilterBefore(filter, BasicAuthenticationFilter.class);
		}

		http.exceptionHandling().accessDeniedHandler(accessDeniedHandler);
		if (sslOnly) {
			http.requiresChannel().anyRequest().requiresSecure();
		}

	}

	private ClientCredentialsTokenEndpointFilter clientCredentialsTokenEndpointFilter(HttpSecurity http) {
		ClientCredentialsTokenEndpointFilter clientCredentialsTokenEndpointFilter = new ClientCredentialsTokenEndpointFilter(
				frameworkEndpointHandlerMapping().getServletPath("/oauth/token"));
		clientCredentialsTokenEndpointFilter
				.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
        
        // 指定授权认证端点 OAuth2AuthenticationEntryPoint
		OAuth2AuthenticationEntryPoint authenticationEntryPoint = new OAuth2AuthenticationEntryPoint();
		authenticationEntryPoint.setTypeName("Form");
		authenticationEntryPoint.setRealmName(realm);
		clientCredentialsTokenEndpointFilter.setAuthenticationEntryPoint(authenticationEntryPoint);
        
        // 调用 clientCredentialsTokenEndpointFilter postProcess() 方法
		clientCredentialsTokenEndpointFilter = postProcess(clientCredentialsTokenEndpointFilter);
		http.addFilterBefore(clientCredentialsTokenEndpointFilter, BasicAuthenticationFilter.class);
		return clientCredentialsTokenEndpointFilter;
	}
    
    private ClientDetailsService clientDetailsService() {
		return getBuilder().getSharedObject(ClientDetailsService.class);
	}

	private FrameworkEndpointHandlerMapping frameworkEndpointHandlerMapping() {
		return getBuilder().getSharedObject(FrameworkEndpointHandlerMapping.class);
	}
    
    .....
    
}    

摘取了核心配置代码,ClientCredentialsTokenEndpointFilter 拦截 /oauth/token 请求,并采用 OAuth2AuthenticationEntryPoint 授权端点进行处理

public class ClientCredentialsTokenEndpointFilter extends AbstractAuthenticationProcessingFilter {

	private AuthenticationEntryPoint authenticationEntryPoint = new OAuth2AuthenticationEntryPoint();

    .....
        
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
            throws AuthenticationException, IOException, ServletException {
        ...
        String clientId = request.getParameter("client_id");
        String clientSecret = request.getParameter("client_secret");

        ...
        clientId = clientId.trim();
        UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(clientId, clientSecret);

        return this.getAuthenticationManager().authenticate(authRequest);

    }
    
    .....
}

用来从请求中获取client_id,client_secret,组装成一个UsernamePasswordAuthenticationToken作为身份标识,使用容器中的顶级身份管理器AuthenticationManager去进行身份认证

2、顶级身份管理者 AuthenticationManager

AuthenticationManager的实现类一般是ProviderManager。而ProviderManager内部维护了一个List,真正的身份认证是由一系列AuthenticationProvider去完成。而AuthenticationProvider的常用实现类则是DaoAuthenticationProvider,DaoAuthenticationProvider内部又聚合了一个UserDetailsService接口,UserDetailsService才是获取用户详细信息的最终接口,而我们上一篇文章中在内存中配置用户,就是使用了UserDetailsService的一个实现类InMemoryUserDetailsManager。UML类图可以大概理解下这些类的关系,省略了授权部分。

在这里插入图片描述

在这里插入图片描述

可能机智的读者会发现一个问题,我前面一篇文章已经提到了client模式是不存在“用户”的概念的,那么这里的身份认证是在认证什么呢?debug可以发现UserDetailsService的实现被适配成了ClientDetailsUserDetailsService,这个设计是将client客户端的信息(client_id,client_secret)适配成用户的信息(username,password),这样我们的认证流程就不需要修改了。

经过ClientCredentialsTokenEndpointFilter之后,身份信息已经得到了AuthenticationManager的验证。接着便到达了TokenEndpoint

3、Token处理端点TokenEndpoint

前面的两个ClientCredentialsTokenEndpointFilter和AuthenticationManager可以理解为一些前置校验,和身份封装,而这个类一看名字就知道和我们的token是密切相关的。

@FrameworkEndpoint
public class TokenEndpoint extends AbstractEndpoint {

	@RequestMapping(value = "/oauth/token", method=RequestMethod.POST)
	public ResponseEntity<OAuth2AccessToken> postAccessToken(Principal principal, @RequestParam
	Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {
		 ...
		String clientId = getClientId(principal);
		ClientDetails authenticatedClient = getClientDetailsService().loadClientByClientId(clientId);				// 《1》
		...
		TokenRequest tokenRequest = getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient);	// 《2》
		...
		OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);				// 《3》
		...
		return getResponse(token);

	}

	private TokenGranter tokenGranter;
}

《1》加载客户端信息,getClientDetailsService() 这里获取到的是我们先前定义的 InMemoryClientDetailsService 内存服务,回去查询client信息

《2》 结合请求信息,创建TokenRequest

《3》 将TokenRequest传递给TokenGranter颁发token

省略了一些校验代码之后,真正的 /oauth/token 端点暴露在了我们眼前,其中方法参数中的Principal经过之前的过滤器,已经被填充了相关的信息,而方法的内部则是依赖了一个TokenGranter 来颁发token。

4、TokenGranter

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

public class CompositeTokenGranter implements TokenGranter {

	private final List<TokenGranter> tokenGranters;

	public CompositeTokenGranter(List<TokenGranter> tokenGranters) {
		this.tokenGranters = new ArrayList<TokenGranter>(tokenGranters);
	}
	
	public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
		for (TokenGranter granter : tokenGranters) {
			OAuth2AccessToken grant = granter.grant(grantType, tokenRequest);
			if (grant!=null) {
				return grant;
			}
		}
		return null;
	}
	
	public void addTokenGranter(TokenGranter tokenGranter) {
		if (tokenGranter == null) {
			throw new IllegalArgumentException("Token granter is null");
		}
		tokenGranters.add(tokenGranter);
	}

}

在这里插入图片描述

五种类型分别是:

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

上面的每一个业务场景的 TokenGranter 都会继承 AbstractTokenGranter

public abstract class AbstractTokenGranter implements TokenGranter {
	protected final Log logger = LogFactory.getLog(getClass());
	//与token相关的service,重点
	private final AuthorizationServerTokenServices tokenServices;
	//与clientDetails相关的service,重点
	private final ClientDetailsService clientDetailsService;
	//创建oauth2Request的工厂,重点
	private final OAuth2RequestFactory requestFactory;

	private final String grantType;
	...

	public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {

		...
		String clientId = tokenRequest.getClientId();
		ClientDetails client = clientDetailsService.loadClientByClientId(clientId);
		validateGrantType(grantType, client);

		logger.debug("Getting access token for: " + clientId);

		return getAccessToken(client, tokenRequest);

	}

	protected OAuth2AccessToken getAccessToken(ClientDetails client, TokenRequest tokenRequest) {
		return tokenServices.createAccessToken(getOAuth2Authentication(client, tokenRequest));
	}

	protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {
		OAuth2Request storedOAuth2Request = requestFactory.createOAuth2Request(client, tokenRequest);
		return new OAuth2Authentication(storedOAuth2Request, null);
	}

	...
}

看到这里,我们大致有一个比较清晰的思路了。

  • ClientCredentialsTokenEndpointFilter:主要做的是面向请求,根据设置的安全策略进行拦截,拦截后进行身份认证,并填充到上下文中。
  • AuthenticationManager:是认证管理器,最终的身份认证顶层抽象
  • TokenEndpoint:获取token的接入层,请求进来之后,会经过一系列校验,最终调用 TokenGranter 来创建token
  • TokenGranter:也并非是最终的调用组件,只是一个token创建的服务层,中间会经过一些权限的校验等,最终会委托给 AuthorizationServerTokenServices 去真正创建token

后续的每一步的细节,就不展开了,到这个地方一般后续的也就比较容易去梳理了

5、AuthorizationServerTokenServices 认证token服务

AuthorizationServer端的token操作service,接口设计如下:

public interface AuthorizationServerTokenServices {

	// 创建token
	OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException;

	// 刷新token
	OAuth2AccessToken refreshAccessToken(String refreshToken, TokenRequest tokenRequest)
			throws AuthenticationException;

	// 获取token
	OAuth2AccessToken getAccessToken(OAuth2Authentication authentication);

}

在默认的实现类DefaultTokenServices中,可以看到token是如何产生的,并且了解了框架对token进行哪些信息的关联。

public class DefaultTokenServices implements AuthorizationServerTokenServices, ResourceServerTokenServices,
		ConsumerTokenServices, InitializingBean {
     
    private int refreshTokenValiditySeconds = 60 * 60 * 24 * 30; // default 30 days.

	private int accessTokenValiditySeconds = 60 * 60 * 12; // default 12 hours.

	private boolean supportRefreshToken = false;

	private boolean reuseRefreshToken = true;

	private TokenStore tokenStore;

	private ClientDetailsService clientDetailsService;

	private TokenEnhancer accessTokenEnhancer;

	private AuthenticationManager authenticationManager;
           
     .......
}            

DefaultTokenServices 同时实现了认证服务token、资源服务的token的业务

下面是资源服务对token的业务依赖

public interface ResourceServerTokenServices {

	// 根据token获取认证信息
	OAuth2Authentication loadAuthentication(String accessToken) throws AuthenticationException, InvalidTokenException;

    // 读取token
	OAuth2AccessToken readAccessToken(String accessToken);

}

还有一点要注意的是token的存储问题,TokenStore 存放到哪。

tokenStore对产生的token和相关信息存储到对应的实现类中,可以是redis,数据库,内存,jwt

并且TokenStore的设置,其实早在上一篇内容中提到的 AuthorizationServerEndpointsConfigurer 中就进行了指定

public AuthorizationServerTokenServices getDefaultAuthorizationServerTokenServices() {
    if (defaultTokenServices != null) {
        return defaultTokenServices;
    }
    this.defaultTokenServices = createDefaultTokenServices();
    return this.defaultTokenServices;
}

...
    
private ResourceServerTokenServices resourceTokenServices() {
    if (resourceTokenServices == null) {
        if (tokenServices instanceof ResourceServerTokenServices) {
            return (ResourceServerTokenServices) tokenServices;
        }
        resourceTokenServices = createDefaultTokenServices();
    }
    return resourceTokenServices;
}

... 
    
private DefaultTokenServices createDefaultTokenServices() {
    DefaultTokenServices tokenServices = new DefaultTokenServices();
    tokenServices.setTokenStore(tokenStore());
    tokenServices.setSupportRefreshToken(true);
    tokenServices.setReuseRefreshToken(reuseRefreshToken);
    tokenServices.setClientDetailsService(clientDetailsService());
    tokenServices.setTokenEnhancer(tokenEnhancer());
    addUserDetailsService(tokenServices, this.userDetailsService);
    return tokenServices;
}

...

// 从这里可以看出,如果不指定,默认是存放到内存中,但是在分布式环境下,这种显然是不允许的
private TokenStore tokenStore() {
    if (tokenStore == null) {
        if (accessTokenConverter() instanceof JwtAccessTokenConverter) {
            this.tokenStore = new JwtTokenStore((JwtAccessTokenConverter) accessTokenConverter());
        }
        else {
            this.tokenStore = new InMemoryTokenStore();
        }
    }
    return this.tokenStore;
}

四、总结

本篇总结了使用客户端模式获取Token时,spring security oauth2内部的运作流程,重点是在分析AuthenticationServer相关的类。其他模式有一定的不同,但抽象功能是固定的,只是具体的实现类会被相应地替换。阅读spring的源码,会发现它的设计中出现了非常多的抽象接口,这对我们理清楚内部工作流程产生了不小的困扰,我的方式是可以借助UML类图,先从宏观理清楚作者的设计思路,这会让我们的分析事半功倍。

下一篇文章重点分析用户携带token访问受限资源时,spring security oauth2内部的工作流程。即ResourceServer相关的类。

参考:https://www.cnkirito.moe/Spring-Security-OAuth2-2/

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值