您现在的位置是:首页 > 正文

springcloud — 微服务鉴权管理Spring Security OAuth2原理解析(四)

2024-02-01 03:15:22阅读 1

回顾之前文章:
1. 微服务鉴权管理之OAuth2原理解析(一)
2. 微服务鉴权管理Spring Security原理解析(二)
3. 微服务鉴权管理Spring Security OAuth2原理解析(三)
本文开始从源码的层面,讲解一些Spring Security Oauth2的认证流程。

1、AuthorizationServerConfigurerAdapter

上一篇博客中我们尝试使用了password模式和client模式,有一个比较关键的endpoint:/oauth/token。从这个入口开始分析,spring security oauth2内部是如何生成token的。获取token,与第一篇文章中的两个重要概念之一有关,也就是AuthorizationServer与ResourceServer中的AuthorizationServer。

在之前的配置中:

@Configuration
@EnableAuthorizationServer
protected static class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {}

出现了AuthorizationServerConfigurerAdapter 关键类,他关联了三个重要的配置类,分别是:

public class AuthorizationServerConfigurerAdapter implements AuthorizationServerConfigurer {
	//配置AuthorizationServer安全认证的相关信息,
	//创建ClientCredentialsTokenEndpointFilter核心过滤器
	@Override
	public void configure(AuthorizationServerSecurityConfigurer security) throws Exception{
	}
	//配置OAuth2的客户端相关信息
	@Override
	public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
	}
	// 配置AuthorizationServerEndpointsConfigurer众多相关类,
	// 包括配置身份认证器,配置认证方式,TokenStore,TokenGranter,OAuth2RequestFactory
	@Override
	public void configure(AuthorizationServerEndpointsConfigurer endpoints <3>) throws Exception {
	}

}
2、客户端身份认证核心过滤器ClientCredentialsTokenEndpointFilter

截取关键的代码,可以分析出大概的流程 在请求到达/oauth/token之前经过了ClientCredentialsTokenEndpointFilter这个过滤器,关键方法如下:

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

}
3、顶级身份管理者AuthenticationManager

用来从请求中获取client_id,client_secret,组装成一个UsernamePasswordAuthenticationToken作为身份标识,使用容器中的顶级身份管理器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。

4、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);
		...
        //结合请求信息,创建TokenRequest
		TokenRequest tokenRequest = getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient);
		...
		//将TokenRequest传递给TokenGranter颁发token
		OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
		...
		return getResponse(token);

	}

	private TokenGranter tokenGranter;
}

省略了一些校验代码之后,真正的/oauth/token端点暴露在了我们眼前,其中方法参数中的Principal经过之前的过滤器,已经被填充了相关的信息,而方法的内部则是依赖了一个TokenGranter 来颁发token。其中OAuth2AccessToken的实现类DefaultOAuth2AccessToken就是最终在控制台得到的token序列化之前的原始类:

public class DefaultOAuth2AccessToken implements Serializable, OAuth2AccessToken {
  private static final long serialVersionUID = 914967629530462926L;
  private String value;
  private Date expiration;
  private String tokenType = BEARER_TYPE.toLowerCase();
  private OAuth2RefreshToken refreshToken;
  private Set<String> scope;
  private Map<String, Object> additionalInformation = Collections.emptyMap();
  //getter,setter
}
@org.codehaus.jackson.map.annotate.JsonSerialize(using = OAuth2AccessTokenJackson1Serializer.class)
@org.codehaus.jackson.map.annotate.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 {
	public static String BEARER_TYPE = "Bearer";
	public static String OAUTH2_TYPE = "OAuth2";
	public static String ACCESS_TOKEN = "access_token";
	public static String TOKEN_TYPE = "token_type";
	public static String EXPIRES_IN = "expires_in";
	public static String REFRESH_TOKEN = "refresh_token";
	public static String SCOPE = "scope";
	...
}

一个典型的样例token响应,如下所示,就是上述类序列化后的结果:

{
	"access_token":"950a7cc9-5a8a-42c9-a693-40e817b1a4b0",
	"token_type":"bearer",
	"refresh_token":"773a0fcd-6023-45f8-8848-e141296cb3cb",
	"expires_in":27036,
	"scope":"select"
}
5、TokenGranter

先从UML类图对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;
	}
}

五种类型分别是:

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

以客户端模式为例,思考如何产生token的,则需要继续研究5种授权者的抽象类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);
	}

	...
}
6、AuthorizationServerTokenServices

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进行哪些信息的关联。

@Transactional
public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException {

	OAuth2AccessToken existingAccessToken = tokenStore.getAccessToken(authentication);
	OAuth2RefreshToken refreshToken = null;
	if (existingAccessToken != null) {
		if (existingAccessToken.isExpired()) {
			if (existingAccessToken.getRefreshToken() != null) {
				refreshToken = existingAccessToken.getRefreshToken();
				// The token store could remove the refresh token when the
				// access token is removed, but we want to
				// be sure...
				tokenStore.removeRefreshToken(refreshToken);
			}
			tokenStore.removeAccessToken(existingAccessToken);
		}
		else {
			// Re-store the access token in case the authentication has changed
			tokenStore.storeAccessToken(existingAccessToken, authentication);
			return existingAccessToken;
		}
	}

	// Only create a new refresh token if there wasn't an existing one
	// associated with an expired access token.
	// Clients might be holding existing refresh tokens, so we re-use it in
	// the case that the old access token
	// expired.
	if (refreshToken == null) {
		refreshToken = createRefreshToken(authentication);
	}
	// But the refresh token itself might need to be re-issued if it has
	// expired.
	else if (refreshToken instanceof ExpiringOAuth2RefreshToken) {
		ExpiringOAuth2RefreshToken expiring = (ExpiringOAuth2RefreshToken) refreshToken;
		if (System.currentTimeMillis() > expiring.getExpiration().getTime()) {
			refreshToken = createRefreshToken(authentication);
		}
	}

	OAuth2AccessToken accessToken = createAccessToken(authentication, refreshToken);
	tokenStore.storeAccessToken(accessToken, authentication);
	// In case it was modified
	refreshToken = accessToken.getRefreshToken();
	if (refreshToken != null) {
		tokenStore.storeRefreshToken(refreshToken, authentication);
	}
	return accessToken;

}

简单总结一下AuthorizationServerTokenServices的作用,他提供了创建token,刷新token,获取token的实现。在创建token时,他会调用tokenStore对产生的token和相关信息存储到对应的实现类中,可以是redis,数据库,内存,jwt。

总结

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

网站文章

  • Java报错ClassNotFoundException或者NoSuchMethodError

    Java报错ClassNotFoundException或者NoSuchMethodError

    出现这种报错99%为包版本不同,依赖冲突导致。

    2024-02-01 03:15:15
  • 利用bind9架设智能DNS——postgreSQL数据库篇

    智能DNS工作原理: 在用户解析一个域名的时候,判断一下用户的IP,然后跟DNS服务器内部的IP表匹配一下,看看用户是电信还是网通用户,然后给用户返回对应的IP地址。目前的域名服务运营商不提供智能DNS服务,所以必须自行架设DNS服务或者使用网上免费的智能DNS

    2024-02-01 03:15:08
  • 计算机网络:网络层——数据平面

    计算机网络:网络层——数据平面

    概述 两种重要的网络层功能:转发和路由选择。(forwarding and routing) 数据层面(Data plane): local, per-router function determin...

    2024-02-01 03:15:02
  • 兴趣篇——用C语言写打字母游戏

    昨天说的,以后会写和小游戏相关的博客,事不宜迟,今天就先开始用最简单的C语言编写一个简单的打字母小游戏吧。 应今天我的软件工程老师的一句话:当做一件事时,如果能很快的从中得到正反馈,我们将会有成就感,我们将会提高对它的兴趣。 我们编程的入门语言是C语言,是一门相对简单的编程语言,但是大多数大学生或者其他刚刚接触编程学完C语言的人,往往会产生一个疑问:我们学习了C语言到底能干什么?更有甚者,像计

    2024-02-01 03:14:32
  • 带宽和时延究竟有没有关系

    一、严格意义上来说是不一样的,简单点说,网络为什么会出现延时,一定程度上就是带宽时延和数据量造成的;举个例子来说就很清楚了:带宽,bandwidth,是指每秒钟传输的最大字节数。带宽本来是指某个信号具...

    2024-02-01 03:14:26
  • python django框架orm_利用Python的Django框架中的ORM建立查询API

    摘要在这篇文章里,我将以反模式的角度来直接讨论Django的低级ORM查询方法的使用。作为一种替代方式,我们需要在包含业务逻辑的模型层建立与特定领域相关的查询API,这些在Django中做起来不是非常...

    2024-02-01 03:14:20
  • Php Jquery Load FadeIn FadeOut 无刷新分页

    Php Jquery Load FadeIn FadeOut 无刷新分页index.php<?phpdefine('IN_LOVE',true);require_once('includes/load.php');$smarty->display('index.html');?>index.html<html>&lt...

    2024-02-01 03:13:49
  • java中如何将url地址文件流上传到sftp的某个目录下面?(亲测)

    java中如何将url地址文件流上传到sftp的某个目录下面?(亲测)

    java中如何将url地址文件流上传到sftp的某个目录下面?

    2024-02-01 03:13:43
  • 什么是分布式数据库?我不信,看完这篇你还不懂!

    什么是分布式数据库?我不信,看完这篇你还不懂!

    原文来源: https://tidb.net/blog/eb3cb609 ...

    2024-02-01 03:13:37
  • VMware Workstation 15.0.4 官方版 安装及部分小问题方案答疑

    VMware Workstation 15.0.4 官方版 安装及部分小问题方案答疑

    VMware Workstation软件特色   VMware支持在一台客户机上运行多个操作系统   无需安装或者复杂的设置,即可体验预设置的产品的功能和特色   VMware虚拟机支持在主机和虚拟机...

    2024-02-01 03:13:06