SpringCloud-OAuth2提供了获取令牌的端点/oauth/token
那么它,客户端是如何认证的?,以及用户信息是如何认证?让我们一起来看看吧

1. 先说客户端的认证

客户端的认证我们从AbstractAuthenticationProcessingFilter抽象类来开始看(因为spring-security安全框架会经过一系列的filter接口,请求进来就走这个filter,其实不管是Spring-Security还是SpringCloud-OAuth2,都是从这个抽象类开始的,不过OAuth2客户端的认证,由其子类ClientCredentialsTokenEndpointFilter来处理,而SpringSecurity是由UsernamePasswordAuthenticationFilter来处理,它两都实现了父类中的attemptAuthentication方法,(两者的区别在于Spring-Security只需要验证用户的帐号密码,而OAuth2密码模式,首先需要验证客户端帐号密码是否正确,之后拿着客户端信息颁发令牌,在颁发令牌的时候,如果是密码模式,需要再去验证用户的帐号和密码,验证成功, 颁发令牌,在对于用户身份的验证两者是一模一样的,都是使用Spring-Security的认证方式)

AbstractAuthenticationProcessingFilter它在spring-security-web jar包中,它其实是一个filter,它的父亲GenericFilterBean抽象类是实现自filter接口的,但是没有实现doFilter方法
交由AbstractAuthenticationProcessingFilter自己去实现了,那么我们就可以将debug打到doFilter方法中,看看它做了什么
第一步
AbstractAuthenticationProcessingFilter的doFilter方法

若依springcloud登录验证码不显示 springcloud登录认证_spring

上图中第二步attemptAuthentication()方法很关键!!!将请求和响应传递给下游处理
上面也解释了attemptAuthentication方法的实现

如果你用的是OAuth2 那么它会走ClientCredentialsTokenEndpointFilter中的attemptAuthentication()

如果是Spring-Security 那么它会走UsernamePasswordAuthenticationFilter中的attemptAuthentication()

本次我们看OAuth2的,所以它会走ClientCredentialsTokenEndpointFilter中的attemptAuthentication()方法

好,说明白,那么我们继续往下看

第二步
ClientCredentialsTokenEndpointFilter类的attemptAuthentication()方法

若依springcloud登录验证码不显示 springcloud登录认证_ide_02


上图中第四步,创建了一个UsernamePasswordAuthenticationToken对象,将客户端id,和密钥传进去,点进这个类中我们来看看,这个类属于Authentication的子类,Authentication是身份认证请求对象,往下的流程我们都是传递的这个对象,并且认证成功会调用UsernamePasswordAuthenticationToken类中三个参数的构造方法,返回认证通过的对象Authentication,返回到方法的调用处,也就是我们起始的AbstractAuthenticationProcessingFilter中的doFilter方法中

UsernamePasswordAuthenticationToken类

若依springcloud登录验证码不显示 springcloud登录认证_客户端_03

封装UsernamePasswordAuthenticationToken成功后,接下来看这个方法的最后一行代码

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

这行代码获取认证管理器AuthenticationManager,AuthenticationManager 是一个接口,提供了统一的认证入口,就是authenticate方法,接收一个Authentication对象作为参数,但是AuthenticationManager是个接口,由它的子类ProviderManager来处理

AuthenticationManager接口中的抽象认证方法

Authentication authenticate(Authentication authentication)
			throws AuthenticationException;

接下来,一块来看ProviderManager子类中的authenticate方法

public class ProviderManager implements AuthenticationManager, MessageSourceAware,
		InitializingBean {

若依springcloud登录验证码不显示 springcloud登录认证_spring_04


第一步

这个方法中有一个for循环轮训成员变量List AuthenticationProvider 集合providers。该集合中有两个对象AnonymousAuthenticationToken(匿名用户)DaoAuthenticationProvider(用于认证用户)他们都是AuthenticationProvider的子类,该providers中如果有一个AuthenticationProvider的supports函数返回true,那么就会调用该AuthenticationProvider的authenticate函数认证,

如果认证成功则返回成功之后的Authentication对象。

但它是一个 接口,那么由它的子AbstractUserDetailsAuthenticationProvider来实现具体的方法,在spring-Security中传递用户账号,认证流程也是走这里的

来看!AbstractUserDetailsAuthenticationProvider中的authenticate方法,将前端传的客户端信息对象传进来
public Authentication authenticate(Authentication authentication)

若依springcloud登录验证码不显示 springcloud登录认证_ide_05

第一步获取客户端id,如果为null那么就给NONE_PROVIDED,不为空就取出,还记得我前面说过UsernamePasswordAuthenticationToken类,封装了客户端id和密码,一直传递下拉,它是Authentication的子类

第三步

user = retrieveUser(username,
						(UsernamePasswordAuthenticationToken) authentication);

调用它本类的一个抽象方法,retrieveUser()
将客户端id和Authentication对象传进去(保存客户端id,和密码)由具体的实现类DaoAuthenticationProvider来处理,给你们贴上实现类

public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider

确实是继承了AbstractUserDetailsAuthenticationProvider

接下拉我们来看具体的实现类DaoAuthenticationProvider的retrieveUser()方法是如何处理的

若依springcloud登录验证码不显示 springcloud登录认证_spring_06

UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);

获取UserDetailsService并调用loadUserByUsername方法,但它是一个接口,,对于oauth2客户端的验证,在spring-security-oauth2 jar包中给我们提供了一个类,ClientDetailsUserDetailsService 它实现了UserDetailsService接口,我们来看看这个实现类中干了什么

若依springcloud登录验证码不显示 springcloud登录认证_客户端_07


ClientDetailsUserDetailsService 很关键!它是实现自 UserDetailsService接口
ClientDetailsUserDetailsService 在oauth2认证环境中,它负责从数据库查询客户端的详细信息,在它的loadUserByUsername方法中
通过前端传入的客户端id,从数据库获取客户端的完整信息,包括客户端密码,客户端角色,客户端scope等等,然后返回user,user是UserDetails的子类,三参数构造函数(客户端id,客户端密钥。角色)
(但前提是,在oauth2的配置中,需要配置客户端的存储策列数据库存储,注入数据源)

玩过springSecurity的同学都知道,我们经常会自己去实现UserDetailsService接口,在loadUserByUsername方法中,
从数据库查询用户的信息(去判断用户是否被冻结,账号是否被锁定等,但注意!密码不需要我们验证,密码是springSecurity来验证的)
然后将用户,密码封装成UserDetails对象返回出去,最后由springSecurity在内部会将数据库查到的密码和前端传入的密码做对比认证。
那么在oauth2中,客户端的验证不需要我们自己做,由ClientDetailsUserDetailsService 类帮我们做了(查询数据库,然后返回)

至此客户端的查询工作完成,接下来,方法应该往回反,回到方法的调用处,终点doFilter那里,继续跟代码,看它拿到客户端信息做些什么

若依springcloud登录验证码不显示 springcloud登录认证_ide_08


上图可以看出回到方法的调用处DaoAuthenticationProvider之后,又进行了return,在往回就到了DaoAuthenticationProvider的父类AbstractUserDetailsAuthenticationProvider类

若依springcloud登录验证码不显示 springcloud登录认证_spring_09

上图中可以看到调用了additionalAuthenticationChecks进行了客户端密码的校验工作,是由其子类DaoAuthenticationProvider去做的,

若依springcloud登录验证码不显示 springcloud登录认证_客户端_10

密码校验完成之后,返回到AbstractUserDetailsAuthenticationProvider,将客户端信息进行了缓存postAuthenticationChecks.check(user);
并且调用此类的createSuccessAuthentication创建认证成功的对象返回,也就是我上面提到过的UsernamePasswordAuthenticationToken对象

return createSuccessAuthentication(principalToReturn, authentication, user);

看看createSuccessAuthentication方法

protected Authentication createSuccessAuthentication(Object principal,
			Authentication authentication, UserDetails user) {
		// Ensure we return the original credentials the user supplied,
		// so subsequent attempts are successful even with encoded passwords.
		// Also ensure we return the original getDetails(), so that future
		// authentication events after cache expiry contain the details
		UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(
				principal, authentication.getCredentials(),
				authoritiesMapper.mapAuthorities(user.getAuthorities()));
		result.setDetails(authentication.getDetails());

		return result;

继续回到代码的调用处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;
		boolean debug = logger.isDebugEnabled();

		for (AuthenticationProvider provider : getProviders()) {
			if (!provider.supports(toTest)) {
				continue;
			}

			if (debug) {
				logger.debug("Authentication attempt using "
						+ provider.getClass().getName());
			}

			try {
					//代码的调用处,result 认证成功的客户端对象
				result = provider.authenticate(authentication);
					认证成功,跳出循环
				if (result != null) {
					copyDetails(authentication, result);
					break;
				}
			}
			catch (AccountStatusException | InternalAuthenticationServiceException e) {
				prepareException(e, authentication);
				// SEC-546: Avoid polling additional providers if auth failure is due to
				// invalid account status
				throw e;
			} catch (AuthenticationException e) {
				lastException = e;
			}
		}
		//第一次认证客户端信息成功之后不会执行if,
		//oauth在客户端认证成功之后,颁发令牌,密码模式还会
		//调用此方法,来验证用户信息,到时候上面的for循环集合只有一个对象
		//AnonymousAuthenticationToken,并不会执行for中的
		//result = provider.authenticate(authentication);
		//会走这个if块,在第二次调用此方法,这个if条件是成立的
		//并且会执行if中	result = parentResult = parent.authenticate(authentication);
		//去认证用户信息,通过我们实现UserDetailsService
		//去查询数据库,返回用户对象,认证成功,
		//返回,最终通过客户端信息,用户信息,组装生成令牌
		if (result == null && parent != null) {
			// Allow the parent to try.
			try {
				result = parentResult = parent.authenticate(authentication);
			}
			catch (ProviderNotFoundException e) {
				// ignore as we will throw below if no other exception occurred prior to
				// calling parent and the parent
				// may throw ProviderNotFound even though a provider in the child already
				// handled the request
			}
			catch (AuthenticationException e) {
				lastException = parentException = e;
			}
		}

		if (result != null) {
			if (eraseCredentialsAfterAuthentication
					&& (result instanceof CredentialsContainer)) {
				//验证完成。从身份验证中删除凭据和/或其他机密数据
				((CredentialsContainer) result).eraseCredentials();
			}

			// If the parent AuthenticationManager was attempted and successful than it will publish an AuthenticationSuccessEvent
			// This check prevents a duplicate AuthenticationSuccessEvent if the parent AuthenticationManager already published it
			if (parentResult == null) {
				eventPublisher.publishAuthenticationSuccess(result);
			}
			//最终返回result
			return result;
		}

		// Parent was null, or didn't authenticate (or throw an exception).

		if (lastException == null) {
			lastException = new ProviderNotFoundException(messages.getMessage(
					"ProviderManager.providerNotFound",
					new Object[] { toTest.getName() },
					"No AuthenticationProvider found for {0}"));
		}

		// If the parent AuthenticationManager was attempted and failed than it will publish an AbstractAuthenticationFailureEvent
		// This check prevents a duplicate AbstractAuthenticationFailureEvent if the parent AuthenticationManager already published it
		if (parentException == null) {
			prepareException(lastException, authentication);
		}

		throw lastException;
	}

继续回到方法的调用处ClientCredentialsTokenEndpointFilter类的attemptAuthentication方法

若依springcloud登录验证码不显示 springcloud登录认证_客户端_11

在往回就会发现回到了AbstractAuthenticationProcessingFilter的doFilter方法,在doFilter方法中,执行了

successfulAuthentication(request, response, chain, authResult);

到这里客户端的认证就结束了,接下来交由下一个filter…

客户端认证通过,现在我们来看看OAuth2提供的/oauth/token端点如何来颁发令牌的,颁发令牌又要做哪些事情
在spring-security-oauth2-2.3.4.RELEASE.jar,中提供了一个类TokenEndpoint,来提供获取令牌的端点,可以看到这个类中有一个注解@FrameworkEndpoint,此注解相当于controller注解,不同的是它是框架内部使用,不会与我们用controller注解的路径冲突

TokenEndpoint 类是令牌请求的端点,如OAuth2规范中所述。客户发布带有 grant_type 参数(例如“ authorization_code”)和由授权类型确定的其他参数的请求。支持的授予类型由提供的{@link #setTokenGranter(org.springframework.security.oauth2.provider.TokenGranter)令牌授权者}处理。
必须使用Spring Security {@link Authentication}对客户端进行身份验证才能访问此端点,并且从身份验证令牌中提取客户端ID

@FrameworkEndpoint
public class TokenEndpoint extends AbstractEndpoint

上面 一段话是我从代码注释中翻译的,可以看到,它说客户端必须首先经过Spring Security {@link Authentication}认证才可访问此端点,这也就说明了,为什么当我们访问/oauth/token端点获取令牌时,首先去验证了客户端的信息是否正确,接下来看看/oauth/token端点方法中做了什么

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

		if (!(principal instanceof Authentication)) {
			throw new InsufficientAuthenticationException(
					"There is no client authentication. Try adding an appropriate authentication filter.");
		}
		//获取认证后的客户端id
		String clientId = getClientId(principal);
		//通过认证后的客户端id,数据库查询获取客户端详细信息
		ClientDetails authenticatedClient = getClientDetailsService().loadClientByClientId(clientId);
        //将前端传入的参数,与客户端信息,构建TokenRequest对象
		TokenRequest tokenRequest = getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient);
       
		if (clientId != null && !clientId.equals("")) {
			// Only validate the client details if a client authenticated during this
			// request.
			//仅当客户端在此请求期间通过身份验证时才验证客户端详细信息。
			if (!clientId.equals(tokenRequest.getClientId())) {
				// double check to make sure that the client ID in the token request is the same as that in the
				// authenticated client
				throw new InvalidClientException("Given client ID does not match authenticated client");
			}
		}
	    //校验客户端的scope,范围
		if (authenticatedClient != null) {
			oAuth2RequestValidator.validateScope(tokenRequest, authenticatedClient);
		}
		 //校验授权类型是否合法
		if (!StringUtils.hasText(tokenRequest.getGrantType())) {
			throw new InvalidRequestException("Missing grant type");
		}
		if (tokenRequest.getGrantType().equals("implicit")) {
			throw new InvalidGrantException("Implicit grant type not supported from token endpoint");
		}
			
		if (isAuthCodeRequest(parameters)) {
			// The scope was requested or determined during the authorization step
			if (!tokenRequest.getScope().isEmpty()) {
				logger.debug("Clearing scope of incoming token request");
				tokenRequest.setScope(Collections.<String> emptySet());
			}
		}

		if (isRefreshTokenRequest(parameters)) {
			// A refresh token has its own default scopes, so we should ignore any added by the factory here.
			tokenRequest.setScope(OAuth2Utils.parseParameterList(parameters.get(OAuth2Utils.SCOPE)));
		}
		//重点,获取令牌
		//这行代码,循环选择合适的授权类型,
		//然后验证用户名,密码,
		//最后生成令牌返回,这些步骤会在接下来去验证
		OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
		if (token == null) {
			throw new UnsupportedGrantTypeException("Unsupported grant type: " + tokenRequest.getGrantType());
		}

		return getResponse(token);

	}

上面说到了这行代码

OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);

调用AuthorizationServerEndpointsConfigurer中的grant方法,使用CompositeTokenGranter对象调用它的grant方法

若依springcloud登录验证码不显示 springcloud登录认证_spring_12

五种授权类型,都继承自AbstractTokenGranter类
AbstractTokenGranter实现自TokenGranter 接口

授权类型

说明

RefreshTokenGranter

通过授权获得的刷新令牌 来获取 新的令牌。

AuthorizationCodeTokenGranter

授权码类型,授权系统针对登录用户下发code,应用系统拿着code去授权系统换取token。

ImplicitTokenGranter

隐式授权类型。authorization_code的简化类型,授权系统针对登录系统直接下发token,302 跳转到应用系统url

ClientCredentialsTokenGranter

客户端凭据(客户端ID以及Key)类型。没有用户参与,应用系统单纯的使用授权系统分配的凭证访问授权系统

ResourceOwnerPasswordTokenGranter

资源所有者(即用户)密码类型。应用系统采集到用户名密码,调用授权系统获取token

在CompositeTokenGranter类中grant方法,循环OAuth2提供的五种授权类型,也就是循环上面表格中的五种类,调用他们自己的grant方法,如果他们没有重写grant方法,调用了父类AbstractTokenGranter的grant方法获取令牌

若依springcloud登录验证码不显示 springcloud登录认证_ide_13


来看看AbstractTokenGranter对grant方法的实现

若依springcloud登录验证码不显示 springcloud登录认证_客户端_14


可以看到,匹配不成功返回null,成功则去获取客户端信息,获取令牌

TokenGranter是访问令牌授予者的接口,提供了grant方法,它有两个直接的子类,一个是CompositeTokenGranter实现了grant方法,在实现方法中负责循环调用它的兄弟AbstractTokenGranter的grant方法

一个是AbstractTokenGranter此类实现TokenGranter中grant方法,并在方法内负责比较授权类型是否匹配,匹配则往下获取令牌的,

上图中可以看到最后 调用getAccessToken方法,去获取令牌,此getAccessToken就在AbstractTokenGranter类中,来看看

若依springcloud登录验证码不显示 springcloud登录认证_ide_15

可以看到调用AbstractTokenGranter中的getOAuth2Authentication方法,去验证用户的身份信息,上面讲过OAuth2 提供了五种不同的授权方式,具体getOAuth2Authentication方法都是由其子类去实现的,所以会执行对应密码模式的子类ResourceOwnerPasswordTokenGranter的getOAuth2Authentication方法,那我们来ResourceOwnerPasswordTokenGranter重写的getOAuth2Authentication做了什么

若依springcloud登录验证码不显示 springcloud登录认证_spring_16


可以看到WebSecurityConfigurerAdapter对象调用它自己类中的authenticate方法进行认证,但是注意,在这个方法中,还会继续往下调用,但是,调用的对象就变了,看下图

若依springcloud登录验证码不显示 springcloud登录认证_spring_17

可以看到最后认证还是交给SpringSecurity了,最后贴一下ProviderManager类,和之前客户端的认证首尾呼应

若依springcloud登录验证码不显示 springcloud登录认证_spring_18


我debug发现执行,result = parentResult = parent.authenticate(authentication);这行代码,相当于递归,又重新调用此类的authenticate方法

当第二次调用时getProviders集合,中对象就为DaoAuthenticationProvider对象,这个对象会获取UserDetailsService()对象,然后调用loadUserByUsername(username);方法,此方法我们自己实现,查询数据,经过SpringSecurity对密码验证成功,最后封装UsernamePasswordAuthenticationToken对象返回(Authentication子类),回到方法的调用处WebSecurityConfigurerAdapter类,在往回反到ResourceOwnerPasswordTokenGranter类的getOAuth2Authentication方法,在此方法构建:return new OAuth2Authentication(storedOAuth2Request, userAuth);返回

回到AbstractTokenGranter的getAccessToken方法

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

上面代码获取到认证成功的用户,调用createAccessToken创建jwt令牌

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

	}

上面类中,可以看到,根据用户信息,客户端信息,构建jwt令牌。最终返回令牌到开始的/oauth/token接口

整个OAuth2 令牌发放的流程差不多就是这样了吧,跟踪源码有助于我们理解框架,知其然之气所以然,加深理解,避免健忘。