认证和授权学习5:spring security认证原理分析
一、结构总览
spring security通过Filter技术实现对web资源的保护。
当初始化spring security时会创建一个名为SpringSecurityFilterChain
的servlet,类型是
org.springframework.security.web.FilterChainProxy
,它实现了Filter接口,所以所有的请求都会经过它。
FilterChainProxy是一个代理,真正起作用的是FilterChainProxy中包含的各个过滤器,这些过滤器被spring容器管理,spring security通过这些过滤器实现认证和授权。但这些过滤器不直接处理认证和授权,而是调用认证管理器和决策管理器来实现。
总结一下就是spring security功能的实现是通过一些过滤器和认证管理器跟决策管理器相互配合实现的。
二、几个重点的过滤器介绍
2.1 SecurityContextPersistenceFilter
这个Filter是整个拦截过程的入口和出口
2.2 UsernamePasswordAuthenticationFilter
用于处理来自表单提交的认证,其内部还有登录成功和登录失败的处理器,AuthenticationSuccessHandler
,
AuthenticationFailureHandler
2.3 FilterSecurityInterceptor
用来进行授权的过滤器,通过AccessDecisionManager 对登录用户进行授权访问
当用户访问一个系统资源时,会进入这个过滤器,在这个过滤器中用决策管理器判断当前用户是否可以访问此资源。
2.4 ExceptionTranslationFilter
用来处理认证和授权过程中抛出的异常,能够捕获来自过滤器链的所有异常,但它只会处理AuthenticationException和AccessDeniedException这两类,其余的会继续向上抛出。
三、表单登录认证过程分析
(1) 用户提交用户名和密码被UsernamePasswordAuthenticationFilter拦截到,将用户信息封装为接口Authentication 对象,实际是UsernamePasswordAuthenticationToken
这个实现类的对象
(2) 将Authentication 对象提交到认证管理器AuthenticationManager
进行认证
(3) 认证成功后AuthenticationManager
会返回一个填充了用户信息的Authentication对象。
(4) SecurityContextHolder
将设置了用户信息的Authentication对象设置到其内部。
3.1Authentication 接口源码分析
下面是封装用户信息的Authentication
接口的结构,里边维护了当前用户的身份信息
public interface Authentication extends Principal, Serializable {
//权限列表
Collection<? extends GrantedAuthority> getAuthorities();
//登录凭据,比如密码,登录成功后会被移除
Object getCredentials();
//登录的细节信息,通常是WebAuthenticationDetails
//记录登录者的ip和sessionId等值
Object getDetails();
//身份信息,UserDetails接口的实现类对象
Object getPrincipal();
boolean isAuthenticated();
void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}
3.2 UsernamePasswordAuthenticationFilter源码分析
UsernamePasswordAuthenticationFilter
这个过滤器用来处理表单登录请求,其继承自抽象类
AbstractAuthenticationProcessingFilter
并实现了父类的抽象方法attemptAuthentication()
登录请求过来后,实际是先走了这个filter的doFilter方法,
AbstractAuthenticationProcessingFilter的部分源码
public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean
implements ApplicationEventPublisherAware, MessageSourceAware {
...//省略
//过滤器的doFilter方法
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);
return;
}
if (logger.isDebugEnabled()) {
logger.debug("Request is to process authentication");
}
Authentication authResult;
try {
//这里调用子类 UsernamePasswordAuthenticationFilter 的认证方法
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) {
logger.error(
"An internal error occurred while trying to authenticate the user.",
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);
}
}
AbstractAuthenticationProcessingFilter
的doFilter方法中调用了子类的 attemptAuthentication方法进行认证
在这个方法中获取当前登录的用户信息,封装成Authentication
接口的实现类
UsernamePasswordAuthenticationToken
的对象,然后把这个对象提交给认证管理器进行认证,这里说的提交实际上就是调用认证管理器的认证方法。
public class UsernamePasswordAuthenticationFilter extends
AbstractAuthenticationProcessingFilter {
//这是开始认证的方法
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}
String username = obtainUsername(request);
String password = obtainPassword(request);
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);
setDetails(request, authRequest);
//提交认证管理器(实际就是调用认证管理器的认证方法)
return this.getAuthenticationManager().authenticate(authRequest);
}
}
其中 this.getAuthenticationManager().authenticate(authRequest)
这句是调用认证管理器的认证方法,如果认证成功会返回一个填充了用户的权限等信息的Authentication
对象,但其中的用户密码会被清空
3.3 AuthenticationManager 分析
上面讲到UsernamePasswordAuthenticationFilter
中会调用认证管理器AuthenticationManager
的方法进行认证。AuthenticationManager
是一个接口,其中只有一个方法,
Authentication authenticate(Authentication authentication) throws AuthenticationException;
springsecurity 提供了一个实现类ProviderManager
,所以进行认证时实际调用的是这个类中的方法
这个类的名字可以看出它的功能,Provider管理,其实真正的认证逻辑是由各种AuthenticationProvider
完成的,
这也是一个接口,不同的实现类对应不同的认证方式
public interface AuthenticationProvider {
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
boolean supports(Class<?> authentication);
}
其中的support方法用来判断当前传递过来的这个Authentication对象是不是自己该验证的,authenticate方法是具体的验证方法。
ProviderManager
中维护着一个AuthenticationProvider
的集合
private List<AuthenticationProvider> providers = Collections.emptyList();
UsernamePasswordAuthenticationFilter
中调用的authenticate方法,实际就是ProviderManager
中的方法
3.4 ProviderManager源码分析
ProviderManager类会调用具体的provider进行认证。外部调用的是其中的authenticate方法
public class ProviderManager implements AuthenticationManager, MessageSourceAware,
InitializingBean {
...//省略变量定义
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
//获取当前被认证的Authentication的class
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
AuthenticationException parentException = null;
Authentication result = null;
Authentication parentResult = null;
boolean debug = logger.isDebugEnabled();
//循环其中维护的provider列表,看那个可以认证当前的authentication对象
for (AuthenticationProvider provider : getProviders()) {
//调用provider.supports()方法判断这个provider能否处理
if (!provider.supports(toTest)) {
//不能处理循环下一个
continue;
}
if (debug) {
logger.debug("Authentication attempt using "
+ provider.getClass().getName());
}
try {
//可以处理的话就调用具体provider的authenticate方法,
//如果是表单登录,对应的是 DaoAuthenticationProvider
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
catch (AccountStatusException e) {
prepareException(e, authentication);
// SEC-546: Avoid polling additional providers if auth failure is due to
// invalid account status
throw e;
}
catch (InternalAuthenticationServiceException e) {
prepareException(e, authentication);
throw e;
}
catch (AuthenticationException e) {
lastException = e;
}
}
//只有没有进行认证 result才是null,认证失败会抛出异常,认证成功result返回authenticaion的对象
if (result == null && parent != null) {
// Allow the parent to try.
try {
//如果循环完了provider列表还没有进行认证,就调用parent的方法,这个parent是构造方法传
//进来的
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)) {
// Authentication is complete. Remove credentials and other secret data
// from authentication
((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);
}
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;
}
}
针对表单登录,最后进行认证的是 DaoAuthenticationProvider
,这个类继承自AbstractUserDetailsAuthenticationProvider
,其中authenticate方法在父类中,通过用户名查找用户的逻辑定义在父类中,是一个抽象方法protected abstract UserDetails retrieveUser(...)
,子类实现这个方法,调用
userDetailsService
去数据库查询用户对象,然后和过滤器传过来的authentication对象进行比较。
三、总结
整个认证过程:
登录请求先走到AbstractAuthenticationProcessingFilter
的doFilter方法,在这个方法中调用了子类
UsernamePasswordAuthenticationFilter
的attemptAuthentication()
方法,然后在这个方法中封装用户信息为
UsernamePasswordAuthenticationToken
对象,调用认证管理器AuthenticationManager.authenticate()方法进行校验,校验成功后再回到AbstractAuthenticationProcessingFilter
的doFilter方法中,执行登录成功后的处理逻辑,登录成功后会把当前用户填充到 SecurityContextHolder
中