认证授权总流程
先来一张自己梳理的,Spring Security 认证授权的全流程图。这一章,主要根据流程,简单走读梳理源码。
认证源码分析
AbstractAuthenticationProcessingFilter
,这个类是认证流程的入口。
AbstractAuthenticationProcessingFilter
根据 Request
创建 Authentication
。
Authentication
具体类型, 由实际使用的 AbstractAuthenticationProcessingFilter
的子 类决定。 常见的有: UsernamePasswordAuthenticationToken
。
默认情况下,使用 UsernamePasswordAuthenticationFilter
类,所以对应的Authentication
是UsernamePasswordAuthenticationToken
。
UsernamePasswordAuthenticationFilter#attemptAuthentication
,//……
表示删除部分次要逻辑。
// UsernamePasswordAuthenticationFilter#attemptAuthentication,`//……`表示删除部分次要逻辑。
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
// ……
String username = obtainUsername(request);
String password = obtainPassword(request);
// ……
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
复制代码
我们可以看到,上面最后一句,调用了AuthenticationManager
。通过官方文档,我们知道ProviderManager
是AuthenticationManager
的默认实现。所以我们需要到ProviderManager
中。
支持 UsernamePasswordAuthenticationToken
的 provider 是 DaoAuthenticationProvider
,DaoAuthenticationProvider
继承自AbstractUserDetailsAuthenticationProvider
。
ProviderManager#authenticate
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
// 获取 Autentication 的类型,这是我们实际是 UsernamePasswordAuthenticationToken
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
AuthenticationException parentException = null;
Authentication result = null;
Authentication parentResult = null;
// 获取所有 AuthenticationProvider
for (AuthenticationProvider provider : getProviders()) {
// 判断 provider 是否支持该 Autentication
// 这里 DaoAuthenticationProvider 通过验证
if (!provider.supports(toTest)) {
continue;
}
// ……
try {
// 执行认证 AbstractUserDetailsAuthenticationProvider#authenticate
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
// ……
}
// …………
if (result != null) {
if (eraseCredentialsAfterAuthentication
&& (result instanceof CredentialsContainer)) {
// Authentication is complete. Remove credentials and other secret data
// from authentication
((CredentialsContainer) result).eraseCredentials();
}
//……………………………………
//……………………………………
}
复制代码
AbstractUserDetailsAuthenticationProvider#supports、authenticate
public boolean supports(Class<?> authentication) {
return (UsernamePasswordAuthenticationToken.class
.isAssignableFrom(authentication));
}
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
// ……
// Determine username
// 获取用户名
String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
: authentication.getName();
boolean cacheWasUsed = true;
// 根据用户名获取 用户主体
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
// 缓存中没有,调用 retrieveUser 方法获取用户
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (UsernameNotFoundException notFound) {
// ……
}
// ……
// ……
return createSuccessAuthentication(principalToReturn, authentication, user);
}
复制代码
DaoAuthenticationProvider#retrieveUser、createSuccessAuthentication
protected final UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
prepareTimingAttackProtection();
try {
// 上述逻辑,如果内存中没有用户,就调用UserDeatilsService#loadUserByUsername 获取用户
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
return loadedUser;
}
// …………
}
@Override
protected Authentication createSuccessAuthentication(Object principal,
Authentication authentication, UserDetails user) {
// 验证密码
boolean upgradeEncoding = this.userDetailsPasswordService != null
&& this.passwordEncoder.upgradeEncoding(user.getPassword());
if (upgradeEncoding) {
String presentedPassword = authentication.getCredentials().toString();
String newPassword = this.passwordEncoder.encode(presentedPassword);
user = this.userDetailsPasswordService.updatePassword(user, newPassword);
}
return super.createSuccessAuthentication(principal, authentication, user);
}
// super.createSuccessAuthentication
protected Authentication createSuccessAuthentication(Object principal,
Authentication authentication, UserDetails user) {
// 创建 UsernamePasswordAuthenticationToken 返回
UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(
principal, authentication.getCredentials(),
authoritiesMapper.mapAuthorities(user.getAuthorities()));
result.setDetails(authentication.getDetails());
return result;
}
复制代码
授权源码分析
授权的入口是 FilterSecurityInterceptor
。
FilterSecurityInterceptor
//……
private FilterInvocationSecurityMetadataSource securityMetadataSource;
// ……
public void invoke(FilterInvocation fi) throws IOException, ServletException {
// ……
InterceptorStatusToken token = super.beforeInvocation(fi);
try {
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
}
finally {
super.finallyInvocation(token);
}
super.afterInvocation(token, null);
}
}
// ……
复制代码
这说明,Spring Security 默认的SecurityMetadataSource
是FilterInvocationSecurityMetadataSource
。
我们观察到,super.beforeInvocation()
,进入其父类继续走流程。
AbstractSecurityInterceptor
protected InterceptorStatusToken beforeInvocation(Object object) {
Assert.notNull(object, "Object was null");
// ……
// 此处调用 FilterInvocationSecurityMetadataSource , 获取访问当前路径需要的权限集合
Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource()
.getAttributes(object);
// 如果为空,则不继续进行鉴权,直接返回
if (attributes == null || attributes.isEmpty()) {
// ……
publishEvent(new PublicInvocationEvent(object));
return null; // no further work post-invocation
}
// ……
// 如果没有认证,抛出异常
if (SecurityContextHolder.getContext().getAuthentication() == null) {
credentialsNotFound(messages.getMessage(
"AbstractSecurityInterceptor.authenticationNotFound",
"An Authentication object was not found in the SecurityContext"),
object, attributes);
}
// ……
// 此处调用 accessDecisionManager 授权,若权限不足则抛出异常。
try {
this.accessDecisionManager.decide(authenticated, object, attributes);
}
catch (AccessDeniedException accessDeniedException) {
publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated,
accessDeniedException));
throw accessDeniedException;
}
// ……
// ……
}
复制代码
AccessDecisionManager
是一个接口。我们可以从日志中得知,Spring Security
默认使用得是其实现类 AffirmativeBased
,并且还使用到了 WebExpressionVoter
。
o.s.s.access.vote.AffirmativeBased : Voter: org.springframework.security.web.access.expression.WebExpressionVoter@bb39abe, returned: -1
复制代码
AffirmativeBased#decide
public void decide(Authentication authentication, Object object,
Collection<ConfigAttribute> configAttributes) throws AccessDeniedException {
int deny = 0;
for (AccessDecisionVoter voter : getDecisionVoters()) {
// 处理授权
int result = voter.vote(authentication, object, configAttributes);
// ……
switch (result) {
case AccessDecisionVoter.ACCESS_GRANTED:
return;
case AccessDecisionVoter.ACCESS_DENIED:
deny++;
break;
default:
break;
}
}
// 上面进行授权操作,授权失败 deny 就加一。到此处如果大于0就抛出异常,否则就正常进行。
if (deny > 0) {
throw new AccessDeniedException(messages.getMessage(
"AbstractAccessDecisionManager.accessDenied", "Access is denied"));
}
// ……
}
复制代码
实际判断授权的操作,是在voter.vote()
中进行的。从日志中得知,是由其实现类WebExpressionVoter
处理。
WebExpressionVoter#vote、findConfigAttribute
public int vote(Authentication authentication, FilterInvocation fi, Collection<ConfigAttribute> attributes) { assert authentication != null; assert fi != null; assert attributes != null; // attributes 转化为 WebExpressionConfigAttribute WebExpressionConfigAttribute weca = findConfigAttribute(attributes); // 如果null,默认是通过的 if (weca == null) { return ACCESS_ABSTAIN; } // 创建判断环境 EvaluationContext ctx = expressionHandler.createEvaluationContext(authentication, fi); ctx = weca.postProcess(ctx, fi); // 执行判定 return ExpressionUtils.evaluateAsBoolean(weca.getAuthorizeExpression(), ctx) ? ACCESS_GRANTED : ACCESS_DENIED; } // 把 attribute 转化为 WebExpressionConfigAttribute,如果没匹配到就返回null private WebExpressionConfigAttribute findConfigAttribute(Collection<ConfigAttribute> attributes) { for (ConfigAttribute attribute : attributes) { if (attribute instanceof WebExpressionConfigAttribute) { return (WebExpressionConfigAttribute) attribute; } } return null; }
复制代码
就此鉴权也就结束了。