上一篇博客说完了Spring Security是如何构建这样一个安全拦截的体系的,这篇博客就来谈一谈是如何进行认证的。
和之前一样,和官方文档同步讲解,在梳理认证的流程之前,需要把其中的几个重要的角色先理明白了。
首先是SecurityContextHolder
如图,SecurityContextHolder中共有5个角色,SecurityContext,Authentication,Principal,Credentials,Authorities。
SecurityContextHolder就相当于一个存储安全信息的大容器,所有的认证后的信息都存储在SecurityContextHolder中,看代码,
这是SecurityContextHolder的初始化,SecurityContextHolder的实际实现都由SecurityContextHolderStrategy实现,默认情况下是一个使用ThreadLocal线程安全的strategy,如果希望能被子线程共享,可以指定InheritableThreadLocalSecurityContextHolderStrategy,如果是Swing客户端这种只会有一个认证的,那可以指定GlobalSecurityContextHolderStrategy,又或者是什么都不使用,自定义。
更改策略的方式,一个是指定系统参数,一个是通过调用setStrategyName静态方法设置。
private static void initialize() {
//空的话,就使用 MODE_THREADLOCAL
if (!StringUtils.hasText(strategyName)) {
// Set default
strategyName = MODE_THREADLOCAL;
}
//ThreadLocal线程安全
if (strategyName.equals(MODE_THREADLOCAL)) {
strategy = new ThreadLocalSecurityContextHolderStrategy();
}
//InheritableThreadLocal线程安全,可与子线程共享
else if (strategyName.equals(MODE_INHERITABLETHREADLOCAL)) {
strategy = new InheritableThreadLocalSecurityContextHolderStrategy();
}
//全局共享
else if (strategyName.equals(MODE_GLOBAL)) {
strategy = new GlobalSecurityContextHolderStrategy();
}
else {
// Try to load a custom strategy
//还可以加载自定义的策略
try {
Class<?> clazz = Class.forName(strategyName);
Constructor<?> customStrategy = clazz.getConstructor();
strategy = (SecurityContextHolderStrategy) customStrategy.newInstance();
}
catch (Exception ex) {
ReflectionUtils.handleReflectionException(ex);
}
}
initializeCount++;
}
SecurityContextHolder是一个面向全局所有认证信息的容器,而SecurityContext就是相当于一个单独认证信息的容器,其中保存了一个认证后的信息。参考SecurityContextImpl
Authentication就是这个所谓的认证后的信息了,其中包括了principal用户信息,credentials密码信息,authorities角色权限信息。
然后是AuthenticationManager,这个类定义了authentication认证的方法,在这里面,Authentication被设置到SecurityContextHolder中,如果没有用Spring MVC的Filter,也可以不通过AuthenticationManager将Authentication设置到SecurityContextHolder中。
而AuthenticationManager是一个接口,其最主要的实现类就是ProviderManager
看图,ProviderManager将具体的认证交给了一个或多个AuthenticationProvider,每个AuthenticationProvider都可以认证并认证成功或者认证失败,或者交给下游的继续认证,认证成功返回一个Authentication,认证失败抛出异常,返回null则表示交给下一个AuthenticationProvider。
而如果所有的AuthenticationProvider都返回null时,还会去尝试parent进行认证,parent同样通常是ProviderManager
parent被多个ProviderManager共享
来看看代码中的实现,根据代码去验证之前说的,看WebSecurityConfigurerAdapter中的逻辑。
protected final HttpSecurity getHttp() throws Exception {
if (http != null) {
return http;
}
//获取AuthenticationEventPublisher
AuthenticationEventPublisher eventPublisher = getAuthenticationEventPublisher();
localConfigureAuthenticationBldr.authenticationEventPublisher(eventPublisher);
//配置parent的AuthenticationManager,可覆盖实现自定义方法
AuthenticationManager authenticationManager = authenticationManager();
authenticationBuilder.parentAuthenticationManager(authenticationManager);
Map<Class<?>, Object> sharedObjects = createSharedObjects();
//新建HttpSecurity,并构建一个默认的HttpSecurity
http = new HttpSecurity(objectPostProcessor, authenticationBuilder,
sharedObjects);
if (!disableDefaults) {
// @formatter:off
http
.csrf().and()
.addFilter(new WebAsyncManagerIntegrationFilter())
.exceptionHandling().and()
.headers().and()
.sessionManagement().and()
.securityContext().and()
.requestCache().and()
.anonymous().and()
.servletApi().and()
.apply(new DefaultLoginPageConfigurer<>()).and()
.logout();
// @formatter:on
ClassLoader classLoader = this.context.getClassLoader();
//spi 加载 AbstractHttpConfigurer的实现类,加入HttpSecurity中
List<AbstractHttpConfigurer> defaultHttpConfigurers =
SpringFactoriesLoader.loadFactories(AbstractHttpConfigurer.class, classLoader);
for (AbstractHttpConfigurer configurer : defaultHttpConfigurers) {
http.apply(configurer);
}
}
//配置HttpSecurity,可自定义实现
configure(http);
return http;
}
这里有一个authenticationBuilder,向authenticationBuilder中设置parent,并将authenticationBuilder放入HttpSecurity中。
public HttpSecurity(ObjectPostProcessor<Object> objectPostProcessor,
AuthenticationManagerBuilder authenticationBuilder,
Map<Class<?>, Object> sharedObjects) {
super(objectPostProcessor);
Assert.notNull(authenticationBuilder, "authenticationBuilder cannot be null");
setSharedObject(AuthenticationManagerBuilder.class, authenticationBuilder);
for (Map.Entry<Class<?>, Object> entry : sharedObjects
.entrySet()) {
setSharedObject((Class<Object>) entry.getKey(), entry.getValue());
}
ApplicationContext context = (ApplicationContext) sharedObjects
.get(ApplicationContext.class);
this.requestMatcherConfigurer = new RequestMatcherConfigurer(context);
}
而这个authenticationBuilder又是什么?
如下,是一个DefaultPasswordEncoderAuthenticationManagerBuilder
public void setApplicationContext(ApplicationContext context) {
this.context = context;
ObjectPostProcessor<Object> objectPostProcessor = context.getBean(ObjectPostProcessor.class);
LazyPasswordEncoder passwordEncoder = new LazyPasswordEncoder(context);
authenticationBuilder = new DefaultPasswordEncoderAuthenticationManagerBuilder(objectPostProcessor, passwordEncoder);
localConfigureAuthenticationBldr = new DefaultPasswordEncoderAuthenticationManagerBuilder(objectPostProcessor, passwordEncoder) {
@Override
public AuthenticationManagerBuilder eraseCredentials(boolean eraseCredentials) {
authenticationBuilder.eraseCredentials(eraseCredentials);
return super.eraseCredentials(eraseCredentials);
}
@Override
public AuthenticationManagerBuilder authenticationEventPublisher(AuthenticationEventPublisher eventPublisher) {
authenticationBuilder.authenticationEventPublisher(eventPublisher);
return super.authenticationEventPublisher(eventPublisher);
}
};
}
接着去HttpSecurity的beforeConfigure方法中,构建AuthenticationManager
@Override
protected void beforeConfigure() throws Exception {
//设置多个SecurityConfigurer之间共享的AuthenticationManager
setSharedObject(AuthenticationManager.class, getAuthenticationRegistry().build());
}
这部分逻辑在上一篇博客中都有提到,最后执行的是performBuild
@Override
protected ProviderManager performBuild() throws Exception {
if (!isConfigured()) {
logger.debug("No authenticationProviders and no parentAuthenticationManager defined. Returning null.");
return null;
}
ProviderManager providerManager = new ProviderManager(authenticationProviders,
parentAuthenticationManager);
if (eraseCredentials != null) {
providerManager.setEraseCredentialsAfterAuthentication(eraseCredentials);
}
if (eventPublisher != null) {
providerManager.setAuthenticationEventPublisher(eventPublisher);
}
providerManager = postProcess(providerManager);
return providerManager;
}
这里会构建一个ProviderManager,并且将authenticationProviders列表以及parent放入
而在ProviderManager中,认证过程如下,对authenticationProviders列表依次进行认证,如果都返回null,再继续对parent进行认证,认证不通过抛出认证异常
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();
//获取当前的AuthenticationProvider列表,依次执行具体认证
for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue;
}
if (debug) {
logger.debug("Authentication attempt using "
+ provider.getClass().getName());
}
try {
//如果认证成功,返回认证后的Authentication,否则返回null
result = provider.authenticate(authentication);
if (result != null) {
//将authentication中的用户源信息拷贝到result中
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;
}
}
//如果所有的AuthenticationProvider都没有认证成功,执行parent中的认证
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)) {
// Authentication is complete. Remove credentials and other secret data
// from authentication
((CredentialsContainer) result).eraseCredentials();
}
// If the parent AuthenticationManager was attempted and successful then 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;
}
接着是AuthenticationEntryPoint,这个角色的功能就是当用户没有执行登录认证就来访问受保护的功能时,跳转到登录页面
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
String redirectUrl = null;
if (useForward) {
if (forceHttps && "http".equals(request.getScheme())) {
// First redirect the current request to HTTPS.
// When that request is received, the forward to the login page will be
// used.
redirectUrl = buildHttpsRedirectUrlForRequest(request);
}
if (redirectUrl == null) {
String loginForm = determineUrlToUseForThisRequest(request, response,
authException);
if (logger.isDebugEnabled()) {
logger.debug("Server side forward to: " + loginForm);
}
RequestDispatcher dispatcher = request.getRequestDispatcher(loginForm);
dispatcher.forward(request, response);
return;
}
}
else {
// redirect to login page. Use https if forceHttps true
redirectUrl = buildRedirectUrlToLoginPage(request, response, authException);
}
redirectStrategy.sendRedirect(request, response, redirectUrl);
}
以上角色都弄明白之后,再接着就可以看AbstractAuthenticationProcessingFilter
当AuthenticationEntryPoint重定向之后,用户输入用户名密码,发起登录请求
AbstractAuthenticationProcessingFilter会对请求进行认证
认证的流程如图
首先会根据HttpServletRequest请求创建一个Authentication对象,接着将Authentication传入AuthenticationManager中进行认证。
如果认证失败SecurityContextHolder被清除,执行rememberMeServices.loginFail,调用failureHandler。
如果成功
sessionStrategy被调用,用于更新session,将Authentication放入SecurityContextHolder中,rememberMeServices.loginSuccess被调用,发布一个登录成功事件,最后调用successHandler。
不过AbstractAuthenticationProcessingFilter是一个抽象类,具体的实现还是需要子类去完成,下一篇博客就来说这个具体的认证过程。