ClientCredentialsTokenEndpointFilter
介绍
Spring Security对于获取TOKEN的请求(默认是"/oauth/token"),需要认证client_id和client_secret。认证client_id和client_secret可以有2种方式,一种是通过本节讲的ClientCredentialsTokenEndpointFilter,另一种是通过BasicAuthenticationFilter。ClientCredentialsTokenEndpointFilter首先比对请求URL是否是TOKEN请求路径以及请求参数中是否包含client_id,如果满足以上条件,再调用ProviderManager认证client_id和client_secret是否与配置的一致。如果通过认证,会把身份认证信息保存打SecurityContext上下文中。
代码分析
步骤1
Spring Security建议用BasicAuthenticationFilter替换ClientCredentialsTokenEndpointFilter,因此默认ClientCredentialsTokenEndpointFilter是不被开启的,我们为了讲解这个过滤器,可以按照如下方式开启过滤,配置如下:
@RefreshScope
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.allowFormAuthenticationForClients();
}
}
步骤2
ClientCredentialsTokenEndpointFilter继承自AbstractAuthenticationProcessingFilter,doFilter()方法在父类当中。请求经过过滤器时先判断url是否为与配置的获取access token的url进行匹配,并且请求参数中client_id不能为空时,然后将参数中的client_id和client_sercet与内存或者数据库(取决于ClientDetailService的实现方式)的client_id和client_sercet进行匹配,匹配成功则验证成功,反之则验证失败,代码如下:
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
//判断请求是否需要进行认证:1)url与获取token的url一致 2)请求参数中client_id不能为空
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);
return;
}
if (logger.isDebugEnabled()) {
logger.debug("Request is to process authentication");
}
Authentication authResult;
try {
//进行client_id和client_sercet认证
authResult = attemptAuthentication(request, response);
if (authResult == null) {
// return immediately as subclass has indicated that it hasn't completed
// authentication
return;
}
sessionStrategy.onAuthentication(authResult, request, response);
}
catch (InternalAuthenticationServiceException failed) {
...
//处理认证失败
unsuccessfulAuthentication(request, response, failed);
return;
}
catch (AuthenticationException failed) {
//处理认证失败
unsuccessfulAuthentication(request, response, failed);
return;
}
// Authentication success
//是否在认证成功处理之前调用下一级过滤链
if (continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
//认证成功处理,1:SecurityContext上下文存储身份认证信息 2:rememberMeServices处理登录成功 3:发布登录成功Spring事件
successfulAuthentication(request, response, chain, authResult);
}
requiresAuthentication()最终会调用ClientCredentialsTokenEndpointFilter#matches()
protected boolean requiresAuthentication(HttpServletRequest request,
HttpServletResponse response) {
//requiresAuthenticationRequestMatcher是一个ClientCredentialsRequestMatcher
return requiresAuthenticationRequestMatcher.matches(request);
}
@Override
public boolean matches(HttpServletRequest request) {
String uri = request.getRequestURI();
int pathParamIndex = uri.indexOf(';');
if (pathParamIndex > 0) {
// strip everything after the first semi-colon
uri = uri.substring(0, pathParamIndex);
}
//参数中必须要含有client_id参数
String clientId = request.getParameter("client_id");
if (clientId == null) {
// Give basic auth a chance to work instead (it's preferred anyway)
return false;
}
if ("".equals(request.getContextPath())) {
return uri.endsWith(path);
}
//请求路径一定要是获取token的URL,一般为"/oauth/token"
return uri.endsWith(request.getContextPath() + path);
}
步骤3
若上下无有效的身份认证信息,attemptAuthentication()最终会调用ProviderManager#authenticate()方法进行验证,authenticationManager中包含1.AnoymousAuthenticationProvider 2.DaoAuthenticationProvider,实际上只有DaoAuthenticationProvider在起作用,DaoAuthenticationProvider#authenticate()根据client_id获取client详情,然后判断client是否禁用、过期、锁定、密码是否一致等,若都满足条件则验证通过。截图和代码如下:
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException, IOException, ServletException {
...
//取client_id
String clientId = request.getParameter("client_id");
//取client_secret
String clientSecret = request.getParameter("client_secret");
// If the request is already authenticated we can assume that this
// filter is not needed
//从SpringSecurity上下文中取一下身份认证信息,看看已经认证过的就无需重复认证了
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.isAuthenticated()) {
return authentication;
}
if (clientId == null) {
throw new BadCredentialsException("No client credentials presented");
}
if (clientSecret == null) {
clientSecret = "";
}
clientId = clientId.trim();
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(clientId,
clientSecret);
//进行认证
return this.getAuthenticationManager().authenticate(authRequest);
}
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();
//providers包含2个Provider:1.AnoymousAuthenticationProvider 2.DaoAuthenticationProvider
for (AuthenticationProvider provider : getProviders()) {
//这里的support逻辑很简单,就是判断一下authentication.getClass()与Provider支持的类类型是否一致或父子继承关系
//AnoymousAuthenticationProvider是不支持的,DaoAuthenticationProvider支持
if (!provider.supports(toTest)) {
continue;
}
try {
//provider实例是DaoAuthenticationProvider
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;
}
}
throw lastException;
}
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
...
// Determine username
String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
: authentication.getName();
boolean cacheWasUsed = true;
//默认是NullUserCache,即不加缓存
UserDetails user = this.userCache.getUserFromCache(username);
//缓存中没用命中
if (user == null) {
//设置命中标识
cacheWasUsed = false;
try {
//调用ClientDetailsUserDetailsService根据client_id获取用户
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (UsernameNotFoundException notFound) {
...
}
...
}
try {
//检查账户是否锁定、启用、过期等
preAuthenticationChecks.check(user);
//检查凭据[密码]是否非空、以及存储密码与输入密码是否一致
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (AuthenticationException exception) {
...
}
//检查凭据是否未过期
postAuthenticationChecks.check(user);
if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
if (forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
//创建Authentication[身份认证信息]
return createSuccessAuthentication(principalToReturn, authentication, user);
}