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方法
上图中第二步attemptAuthentication()方法很关键!!!将请求和响应传递给下游处理
上面也解释了attemptAuthentication方法的实现
如果你用的是OAuth2 那么它会走ClientCredentialsTokenEndpointFilter中的attemptAuthentication()
如果是Spring-Security 那么它会走UsernamePasswordAuthenticationFilter中的attemptAuthentication()
本次我们看OAuth2的,所以它会走ClientCredentialsTokenEndpointFilter中的attemptAuthentication()方法
好,说明白,那么我们继续往下看
第二步
ClientCredentialsTokenEndpointFilter类的attemptAuthentication()方法
上图中第四步,创建了一个UsernamePasswordAuthenticationToken对象,将客户端id,和密钥传进去,点进这个类中我们来看看,这个类属于Authentication的子类,Authentication是身份认证请求对象,往下的流程我们都是传递的这个对象,并且认证成功会调用UsernamePasswordAuthenticationToken类中三个参数的构造方法,返回认证通过的对象Authentication,返回到方法的调用处,也就是我们起始的AbstractAuthenticationProcessingFilter中的doFilter方法中
UsernamePasswordAuthenticationToken类
封装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 {
第一步
这个方法中有一个for循环轮训成员变量List AuthenticationProvider 集合providers。该集合中有两个对象AnonymousAuthenticationToken(匿名用户)DaoAuthenticationProvider(用于认证用户)他们都是AuthenticationProvider的子类,该providers中如果有一个AuthenticationProvider的supports函数返回true,那么就会调用该AuthenticationProvider的authenticate函数认证,
如果认证成功则返回成功之后的Authentication对象。
但它是一个 接口,那么由它的子AbstractUserDetailsAuthenticationProvider来实现具体的方法,在spring-Security中传递用户账号,认证流程也是走这里的
来看!AbstractUserDetailsAuthenticationProvider中的authenticate方法,将前端传的客户端信息对象传进来
public Authentication authenticate(Authentication authentication)
第一步获取客户端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()方法是如何处理的、
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
获取UserDetailsService并调用loadUserByUsername方法,但它是一个接口,,对于oauth2客户端的验证,在spring-security-oauth2 jar包中给我们提供了一个类,ClientDetailsUserDetailsService 它实现了UserDetailsService接口,我们来看看这个实现类中干了什么
ClientDetailsUserDetailsService 很关键!它是实现自 UserDetailsService接口
ClientDetailsUserDetailsService 在oauth2认证环境中,它负责从数据库查询客户端的详细信息,在它的loadUserByUsername方法中
通过前端传入的客户端id,从数据库获取客户端的完整信息,包括客户端密码,客户端角色,客户端scope等等,然后返回user,user是UserDetails的子类,三参数构造函数(客户端id,客户端密钥。角色)
(但前提是,在oauth2的配置中,需要配置客户端的存储策列数据库存储,注入数据源)
玩过springSecurity的同学都知道,我们经常会自己去实现UserDetailsService接口,在loadUserByUsername方法中,
从数据库查询用户的信息(去判断用户是否被冻结,账号是否被锁定等,但注意!密码不需要我们验证,密码是springSecurity来验证的)
然后将用户,密码封装成UserDetails对象返回出去,最后由springSecurity在内部会将数据库查到的密码和前端传入的密码做对比认证。
那么在oauth2中,客户端的验证不需要我们自己做,由ClientDetailsUserDetailsService 类帮我们做了(查询数据库,然后返回)
至此客户端的查询工作完成,接下来,方法应该往回反,回到方法的调用处,终点doFilter那里,继续跟代码,看它拿到客户端信息做些什么
上图可以看出回到方法的调用处DaoAuthenticationProvider之后,又进行了return,在往回就到了DaoAuthenticationProvider的父类AbstractUserDetailsAuthenticationProvider类
上图中可以看到调用了additionalAuthenticationChecks进行了客户端密码的校验工作,是由其子类DaoAuthenticationProvider去做的,
密码校验完成之后,返回到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方法
在往回就会发现回到了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方法
五种授权类型,都继承自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方法获取令牌
来看看AbstractTokenGranter对grant方法的实现
可以看到,匹配不成功返回null,成功则去获取客户端信息,获取令牌
TokenGranter是访问令牌授予者的接口,提供了grant方法,它有两个直接的子类,一个是CompositeTokenGranter实现了grant方法,在实现方法中负责循环调用它的兄弟AbstractTokenGranter的grant方法
一个是AbstractTokenGranter此类实现TokenGranter中grant方法,并在方法内负责比较授权类型是否匹配,匹配则往下获取令牌的,
上图中可以看到最后 调用getAccessToken方法,去获取令牌,此getAccessToken就在AbstractTokenGranter类中,来看看
可以看到调用AbstractTokenGranter中的getOAuth2Authentication方法,去验证用户的身份信息,上面讲过OAuth2 提供了五种不同的授权方式,具体getOAuth2Authentication方法都是由其子类去实现的,所以会执行对应密码模式的子类ResourceOwnerPasswordTokenGranter的getOAuth2Authentication方法,那我们来ResourceOwnerPasswordTokenGranter重写的getOAuth2Authentication做了什么
可以看到WebSecurityConfigurerAdapter对象调用它自己类中的authenticate方法进行认证,但是注意,在这个方法中,还会继续往下调用,但是,调用的对象就变了,看下图
可以看到最后认证还是交给SpringSecurity了,最后贴一下ProviderManager类,和之前客户端的认证首尾呼应
我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 令牌发放的流程差不多就是这样了吧,跟踪源码有助于我们理解框架,知其然之气所以然,加深理解,避免健忘。