Spring Security对于获取TOKEN的请求(/oauth/token),需要认证client_id和client_secret。认证client_id和client_secret可以有2种方式,一种是通过 ClientCredentialsTokenEndpointFilter,另一种是通过BasicAuthenticationFilter。
ClientCredentialsTokenEndpointFilter 继承自 AbstractAuthenticationProcessingFilter,调用授权接口获取token值的请求(/oauth/token)需要认证client_id和client_secret。该请求会被 AbstractAuthenticationProcessingFilter 过滤器拦截,执行父类的doFilter() 方法:
public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean
implements ApplicationEventPublisherAware, MessageSourceAware {
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
// 判断是否需要认证:判断url是否为与配置的获取access token的url进行匹配
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);
return;
}
Authentication authResult;
try {
// 调用实现类中的attemptAuthentication方法认证
authResult = attemptAuthentication(request, response);
if (authResult == null) {
return;
}
// session存储
sessionStrategy.onAuthentication(authResult, request, response);
}catch (InternalAuthenticationServiceException failed) {
// 认证失败处理
unsuccessfulAuthentication(request, response, failed);
return;
}catch (AuthenticationException failed) {
// Authentication failed
unsuccessfulAuthentication(request, response, failed);
return;
}
// Authentication success
if (continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
// 回调认证成功的自定义处理逻辑
successfulAuthentication(request, response, chain, authResult);
}
protected boolean requiresAuthentication(HttpServletRequest request,
HttpServletResponse response) {
return requiresAuthenticationRequestMatcher.matches(request);
}
}
requiresAuthentication(request, response) 最终会调用 ClientCredentialsTokenEndpointFilter 中内部类 ClientCredentialsRequestMatcher#matches 方法:
@Deprecated
public class ClientCredentialsTokenEndpointFilter extends AbstractAuthenticationProcessingFilter {
// ...
// 内部类
protected static class ClientCredentialsRequestMatcher implements RequestMatcher {
// /oauth/token
private String path;
public ClientCredentialsRequestMatcher(String path) {
this.path = path;
}
// 判断请求的 url 与获取 access token 的默认 url(/oauth/token)是否一致
// 判断 client_id 是否为空
public boolean matches(HttpServletRequest request) {
// 请你路径uri : /ngsoc/AUTH/oauth/token
String uri = request.getRequestURI();
int pathParamIndex = uri.indexOf(59);
if (pathParamIndex > 0) {
uri = uri.substring(0, pathParamIndex);
}
// ngsoc
String clientId = request.getParameter("client_id");
if (clientId == null) {
return false;
} else {
// 服务路径 request.getContextPath() : /ngosc/AUTH
// path : /oauth/token
return "".equals(request.getContextPath()) ? uri.endsWith(this.path) : uri.endsWith(request.getContextPath() + this.path);
}
}
}
}
当认证请求需要被认证时,authResult = attemptAuthentication(request, response); 就会调用子类ClientCredentialsTokenEndpointFilter#attemptAuthentication 方法,在方法中将参数中的client_id和client_sercet封装成Authentication对象UsernamePasswordAuthenticationToken,然后交给AuthenticationManager的实现类去认证。
@Deprecated
public class ClientCredentialsTokenEndpointFilter extends AbstractAuthenticationProcessingFilter {
private AuthenticationEntryPoint authenticationEntryPoint;
private boolean allowOnlyPost;
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
// 如果不是post请求,抛出异常
if (this.allowOnlyPost && !"POST".equalsIgnoreCase(request.getMethod())) {
throw new HttpRequestMethodNotSupportedException(request.getMethod(), new String[]{"POST"});
} else {
// 从请求参数client_id中获取clientId
String clientId = request.getParameter("client_id"); // ngsoc
// 从请求参数client_secret中获取clientSecret
String clientSecret = request.getParameter("client_secret"); // ngsoc
// 从SecurityContextHolder中获取认证成功的认证用户信息,如果不为空,直接返回
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.isAuthenticated()) {
return authentication;
} else if (clientId == null) {
throw new BadCredentialsException("No client credentials presented");
} else {
if (clientSecret == null) {
clientSecret = "";
}
clientId = clientId.trim();
// 将clientId和clientSecret封装为UsernamePasswordAuthenticationToken对象
UsernamePasswordAuthenticationToken authRequest
= new UsernamePasswordAuthenticationToken(clientId, clientSecret);
// 将UsernamePasswordAuthenticationToken交给AuthenticationManager的子类认证
return this.getAuthenticationManager().authenticate(authRequest);
}
}
}
}
AuthenticationManager接口源码:
public interface AuthenticationManager {
// 身份认证
// 请求数据:待认证的 Authentication
// 响应数据:认证成功的 Authentication
Authentication authenticate(Authentication authentication) throws AuthenticationException;
}
AuthenticationManager接口的默认实现类是ProviderManager,因此this.getAuthenticationManager().authenticate(authRequest) 最终会调用ProviderManager#authenticate方法完成认证。
public class ProviderManager implements AuthenticationManager, MessageSourceAware,
InitializingBean {
// AuthenticationProvider列表
private List<AuthenticationProvider> providers = Collections.emptyList();
// 父类的 AuthenticationManager
private AuthenticationManager parent;
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
Authentication result = null;
Authentication parentResult = null;
// 遍历AuthenticationProvider列表
for (AuthenticationProvider provider : getProviders()) {
// 判断当前遍历 AuthenticationProvider 是否支持Authentication对象的认证
if (!provider.supports(toTest)) {
continue;
}
try {
// 如果支持,将使用该AuthenticationProvider完成认证
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
// ....
}
if (result == null && parent != null) {
// Allow the parent to try.
try {
result = parentResult = parent.authenticate(authentication);
} catch (ProviderNotFoundException e) {
}catch (AuthenticationException e) {
lastException = parentException = e;
}
}
// ...
}
}
说明:
在 Spring Seourity 中,允许系统同时⽀持多种不同的认证⽅式,例如同时⽀持⽤户名/密码认证、 ReremberMe 认证、⼿机号码动态认证等,⽽不同的认证⽅式对应了不同的 AuthenticationProvider,所以⼀个完整的认证流程可能由多个AuthenticationProvider 来提供。
多个AuthenticationProvider将组成⼀个列表,这个列表将由ProviderManager 代理。换句话说,在ProviderManager 中存在⼀个AuthenticationProvider列表,在ProviderManager中遍历列表中的每⼀个AuthenticationProvider去执⾏身份认证,最终得到认证结果。
ProviderManager 本身也可以再配置⼀个 AuthenticationManager 作为parent,这样当ProviderManager 认证失败之后,就可以进⼊到 parent 中再次进⾏认证。理论上来说, ProviderManager 的 parent 可以是任意类型的AuthenticationManager,但是通常都是由ProviderManager 来扮演 parent 的⻆⾊,也就是 ProviderManager 是ProviderManager 的 parent。
默认情况下,ProviderManager的AuthenticationProvider列表中包含两个实现类:AnoymousAuthenticationProvider 和DaoAuthenticationProvider。
for循环内第一次得到AnoymousAuthenticationProvider,执行AnonymousAuthenticationProvider#supports方法判断该类是否支持UsernamePasswordAuthenticationToken类型的认证,结果不支持,代码如下:
public class AnonymousAuthenticationProvider implements AuthenticationProvider,
MessageSourceAware {
public boolean supports(Class<?> authentication) {
return (AnonymousAuthenticationToken.class.isAssignableFrom(authentication));
}
}
for循环内第二次得到DaoAuthenticationProvider,该类继承自AbstractUserDetailsAuthenticationProvider类,会调用AbstractUserDetailsAuthenticationProvider#supports方法判断该类是否支持UsernamePasswordAuthenticationToken类型的认证,结果支持。
public abstract class AbstractUserDetailsAuthenticationProvider implements
AuthenticationProvider, InitializingBean, MessageSourceAware {
public boolean supports(Class<?> authentication) {
return (UsernamePasswordAuthenticationToken.class
.isAssignableFrom(authentication));
}
}
因此 result = provider.authenticate(authentication) 最终会调用AbstractUserDetailsAuthenticationProvider#authenticate方法对UsernamePasswordAuthenticationToken对象完成认证,在该方法中根据clientId获取数据源中存储的用户user,然后判断user是否禁用、过期、锁定、密码是否一致等,若都满足条件则验证通过。
public abstract class AbstractUserDetailsAuthenticationProvider implements
AuthenticationProvider, InitializingBean, MessageSourceAware {
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
// Determine username
String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
: authentication.getName();
boolean cacheWasUsed = true;
// 从缓存中根据clientId获取UserDetails对象
UserDetails user = this.userCache.getUserFromCache(username);
// 如果缓存中获取不到
if (user == null) {
cacheWasUsed = false;
try {
// 从数据源中获取
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
}catch (UsernameNotFoundException notFound) {
// 如果根据clientId从数据源中获取UserDetails用户详情,如果为空,认证失败抛出异常
logger.debug("User '" + username + "' not found");
if (hideUserNotFoundExceptions) {
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
else {
throw notFound;
}
}
}
try {
// 检查账户是否锁定、启用、过期等
pr eAuthenticationChecks.check(user);
//检查凭据[密码]是否非空、以及存储密码与输入密码是否一致
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (AuthenticationException exception) {
if (cacheWasUsed) {
cacheWasUsed = false;
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
else {
throw exception;
}
}
// 检查凭据是否未过期
postAuthenticationChecks.check(user);
// 将查询到到的用户放入缓存中
if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
if (forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
// 创建Authentication[身份认证信息]
return createSuccessAuthentication(principalToReturn, authentication, user);
}
}
到这儿认证流程就结束了,但是我们可以继续往下看一下,底层如何根据username获取客户端用户信息的。
retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication); 获取用户信息会调用DaoAuthenticationProvider#retrieveUser方法,该方法中,会调用UserDetailsService接口实现类的loadUserByUsername方法根据clientId获取客户端详情信息,代码如下:
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
protected final UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
prepareTimingAttackProtection();
try {
// 调用UserDetailsService接口实现类的loadUserByUsername方法根据username获取用户信息
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
return loadedUser;
}
catch (UsernameNotFoundException ex) {
mitigateAgainstTimingAttack(authentication);
throw ex;
}
catch (InternalAuthenticationServiceException ex) {
throw ex;
}
catch (Exception ex) {
throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
}
}
protected UserDetailsService getUserDetailsService() {
return userDetailsService;
}
}
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username); 会调用ClientDetailsUserDetailsService#loadUserByUsername方法,获取客户端详情信息:
@Deprecated
public class ClientDetailsUserDetailsService implements UserDetailsService {
private final ClientDetailsService clientDetailsService;
private String emptyPassword = "";
public ClientDetailsUserDetailsService(ClientDetailsService clientDetailsService) {
this.clientDetailsService = clientDetailsService;
}
public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
this.emptyPassword = passwordEncoder.encode("");
}
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
ClientDetails clientDetails;
try {
// 虎获取客户端详情
clientDetails = this.clientDetailsService.loadClientByClientId(username);
} catch (NoSuchClientException var4) {
throw new UsernameNotFoundException(var4.getMessage(), var4);
}
String clientSecret = clientDetails.getClientSecret();
if (clientSecret == null || clientSecret.trim().length() == 0) {
clientSecret = this.emptyPassword;
}
// 将客户端信息 ClientDetails 封装成UserDetails并返回
return new User(username, clientSecret, clientDetails.getAuthorities());
}
}
在该方法中会调用ClientDetailsService接口实现类BaseClientDetails#loadClientByClientId方法获取ClientDetails信息:
@Deprecated
public interface ClientDetailsService {
ClientDetails loadClientByClientId(String var1) throws ClientRegistrationException;
}
public interface ClientDetails extends Serializable {
String getClientId();//客户端id
Set<String> getResourceIds();//此客户端可以访问的资源。如果为空,则调用者可以忽略
boolean isSecretRequired();//验证此客户端是否需要secret
String getClientSecret();//获取客户端的secret
boolean isScoped();//此客户端是否仅限于特定范围
Set<String> getScope();//此客户端的范围。如果客户端未确定作用域,则为空
Set<String> getAuthorizedGrantTypes();//此客户端被授权的授权类型
Set<String> getRegisteredRedirectUri();//此客户端的预定义重定向redirect_url
Collection<GrantedAuthority> getAuthorities();//权限集合
Integer getAccessTokenValiditySeconds();//访问令牌有效期
Integer getRefreshTokenValiditySeconds();//刷新令牌有效期
boolean isAutoApprove(String scope);//测试客户端是否需要特定范围的用户批准
Map<String, Object> getAdditionalInformation();//额外的信息
}
BaseClientDetails [
clientId=ngsoc,
clientSecret={bcrypt}$2a$10$Dcn01QtYjhoXeeX0LPsn/.DBiiosgsFFKHVC1/tQiWk5dht1TgtKy,
scope=[],
resourceIds=[],
authorizedGrantTypes=[password, refresh_token, client_credentials],
registeredRedirectUris=[],
authorities=[],
accessTokenValiditySeconds=43200,
refreshTokenValiditySeconds=86400,
additionalInformation={}
]