Spring Security系列教程11--Spring Security核心API讲解

前言

经过前面几个章节的学习,一一哥 带大家实现了基于内存和数据库模型的认证与授权,尤其是基于自定义的数据库模型更是可以帮助我们进行灵活开发。但是前面章节的内容,属于让我们达到了 "会用" 的层级,但是 "为什么这么用",很多小伙伴就会一脸懵逼了。对于技术学习来说,我们追求的不仅要 "知其然",更要 "知其所以然"

本篇文章中,壹哥 就跟各位小伙伴一起来了解剖析Spring Security源码内部,实现认证授权的具体过程及底层原理。接下来请各位做好心理准备,以下的学习过程可能会让你心理 “稍有不适” 哦,因为每次看源码都懵懵的......

一. Spring Security认证请求完整流程图

在开始分析Spring Security关于认证授权的源码之前,壹哥先借用网上的一张图片,先让各位对认证授权有个大致的了解。

下图展示了从发起一个web请求,到经过内存或数据库层面的查询,最后再得到整个用户认证信息的全过程。

 

上图详细的给大家展示了Spring Security是如何实现认证授权的流程及运行原理的,并展示了Spring Security里的核心API。为了能让各位更好的理解掌握本文,请把这个图记下来哦!其实背诵下来也不难的,先这么背,然后那么背,一会就背下来了,hiahia......

在这个图中,涉及到了Spring Security关于认证授权的众多核心API,本文就是对这些核心API进行分析讲解,从而帮助各位掌握认证授权的执行流程,接下来请各位跟着 壹哥 来学习这些核心API吧。

因为Spring Security中关于认证授权相关的API很多,我先把这些API归为如下几类分别进行阐述,这样显得结构会更清晰一些。

二. 核心API之一:SecurityContextHolder、SecurityContext、Authentication

1. Authentication

1、 1Authentication类关系;

 

注:

实线箭头表示继承,虚线箭头表示实现;绿线表示接口与接口之间的关系,蓝线表示类与类之间的关系。

Authentication 是 spring-security-core 核心包中的接口,直接继承自 Principal 接口,而 Principal 是位于 java.security 包中,由此可知 Authentication 是 Spring Security 中的核心接口。Authentication接口中封装了用户信息,它有个很重要的子类UsernamePasswordAuthenticationToken,请各位先记住这个子类!

1、 2Authentication功能;

在用户登录认证之前,用户名密码等信息 会被封装为一个 Authentication 的具体实现类对象。在登录认证成功之后又会生成一个信息更全面的Authentication对象,从该对象中可 得到用户的权限信息列表、密码、用户细节信息、用户身份信息、认证信息等。这个 Authentication对象会被保存在 SecurityContextHolder 所持有的 SecurityContext 中,供后续的程序进行调用,如访问权限的鉴定等。

 

1、 3Authentication核心方法;

 

我们利用上面这些方法就可以获取到关于用户相关的信息,比如用户名、密码、角色等。

2. SecurityContext

安全上下文,该类用于存储认证授权的相关信息,实际上就是存储 "当前用户" 的账号信息和相关权限,即代表当前用户信息的 Authentication 对象会被 SecurityContext 所持有(引用),SecurityContext上下文对象则被SecurityContextHolder 所持有(引用)。

2、 1SecurityContext类源码;

下面是SecurityContext的源码内容,这个接口中只有两个方法,Authentication对象的getter、setter:

public interface SecurityContext extends Serializable {
	// ~ Methods
	// ========================================================================================================

	/**
	 * Obtains the currently authenticated principal, or an authentication request token.
	 *
	 * @return the <code>Authentication</code> or <code>null</code> if no authentication
	 * information is available
	 */
	Authentication getAuthentication();

	/**
	 * Changes the currently authenticated principal, or removes the authentication
	 * information.
	 *
	 * @param authentication the new <code>Authentication</code> token, or
	 * <code>null</code> if no further authentication information should be stored
	 */
	void setAuthentication(Authentication authentication);
}

从源码中可以得知,利用SecurityContext的getAuthentication()方法就可以拿到用户信息。

2、 2获取当前用户信息;

那么我们如果是在自己的项目中,该如何获取当前已登录的用户信息呢?

因为Authentication身份信息与当前执行线程已经绑定在一起,所以我们可以使用以下代码在应用程序中获取当前已认证用户的用户名。

public String getCurrentUsername() {
    //得到当前认证后的用户信息
    Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
    
    if (principal instanceof UserDetails) {
        return ((UserDetails) principal).getUsername();
    }
    
    if (principal instanceof Principal) {
        return ((Principal) principal).getName();
    }
    
    return String.valueOf(principal);
}

3. SecurityContextHolder

3、 1SecurityContextHolder简介;

**由于一个请求从开始到结束都由一个线程处理,这个线程中途不会去处理其他的请求,所以在这段时间内,这个线程就相当于跟当前用户是一一对应的。SecurityContextHolder工具类就是用于把SecurityContext存储在当前线程中,在Web环境下,SecurityContextHolder是利用ThreadLocal来存储SecurityContext对象的。**所以SecurityContextHolder可以用来设置和获取SecurityContext对象,该类主要是给框架内部使用,我们可以利用它获取当前用户的SecurityContext对象,进而进行请求检查,和访问控制等。

因为SecurityContextHolder用于存储当前应用程序的SecurityContext安全上下文对象,而SecurityContext中则存储着当前正在访问系统的Authentication****用户的详细信息,如当前操作的用户是谁,该用户是否已经被认证,拥有哪些角色权限等。所以我们可以用下图来展示SecurityContextHolder、SecurityContext与Authentication三者之间的关系:

 

3、 2SecurityContextHolder的线程安全性保障;

默认情况下,SecurityContextHolder 使用 ThreadLocal 来保存 SecurityContext,这就意味着只要是同一线程中的方法,都可以从 ThreadLocal 中获取到当前的 SecurityContext对象。

因为Sevlet中的线程都是被池化复用的,一旦处理完当前的请求,这个线程可能马上就会被分配去处理其他的请求,而且也不能保证用户下次的请求会被分配到与上次相同的线程。也就是我们每次在请求完成后,Spring Security都会将 ThreadLocal 进行清除,即在每一次 请求 结束后都会自动清除当前线程的 ThreadLocal对象。

你可能会问,这时候如果我们的认证信息没有被保存,岂不是每次请求后都要重新认证登录?这明显不行,我们肯定要保存用户的认证信息!这个保存工作是由SecurityContextPersistenceFilter来完成的!

SecurityContextPersistenceFilter是Security中的一个拦截器,它的执行时机非常早,当请求来临时它会从SecurityContextRepository中把SecurityContext对象取出来,然后放入SecurityContextHolder的ThreadLocal中。在所有拦截器都处理完成后,再把SecurityContext存入SecurityContextRepository,并清除SecurityContextHolder内的SecurityContext引用**。**这样就实现了用户认证信息的保存,也保证了线程的安全性!

3、 3SecurityContextHolder中的方法及属性;

在SecurityContextHolder 中定义了一系列的静态方法,而这些静态方法的内部逻辑基本上都是通过 SecurityContextHolder 所持有的 SecurityContextHolderStrategy 对象来实现的,如 getContext()、setContext()、clearContext()等。如下图所示:

 

3、 4SecurityContextHolder中的3种存储策略;

SecurityContextHolder中定义了3种策略来管理SecurityContext对象的存储,默认使用的 strategy 是基于 ThreadLocal 的 ThreadLocalSecurityContextHolderStrategy,另外还有GlobalSecurityContextHolderStrategyInheritableThreadLocalSecurityContextHolderStrategy 两种策略。

SecurityContextHolder可以通过设置来调整3种存储策略,三种策略详情如下:

  • MODE_THREADLOCAL:表示将 SecurityContext对象 存储在当前线程中;
  • MODE_INHERITABLETHREADLOCAL:表示将 SecurityContext对象 存储在线程中,但子线程可以获取到父线程中的 SecurityContext;
  • MODE_GLOBAL:表示 SecurityContext对象内容 在所有线程中都相同。

**SecurityContextHolder默认使用MODE_THREADLOCAL模式,即存储在当前线程中。**一般而言,我们使用默认的 strategy 就可以了。

4. SecurityContextHolder、SecurityContext、Authentication总结

经过以上3个小节的学习,下面我们来简单总结一下 SecurityContextHolder、SecurityContext 和 Authentication 这三个对象之间的关系。

 

SecurityContextHolder 用来保存 SecurityContext (安全上下文对象),利用SecurityContextHolder的getContext()方法可以得到SecurityContext对象;然后再通过调用 SecurityContext 对象中的getAuthentication()方法,就可以获取 Authentication 对象;最后我们利用Authentication对象,可以进一步获取已认证用户的详细信息。

三. 核心API之二:Filter相关API

1. Filter概念

Spring Security 的底层是通过一系列的 Filter 来管理认证和授权请求的,形成了一组核心的过滤器链,每个 Filter 都有其自身的功能,而且各个 Filter 在功能上还有关联关系,项目启动后将会自动配置生效。其中最核心的就是 BasicAuthenticationFilter ,该Filter用来认证用户的身份。

接下来我们先简单讲解一下各个Filter及其执行顺序。

2. Filter顺序

Spring Security 中定义的一些列Filter,不管实际应用中我们会用到哪些,它们都应当保持如下顺序。

1、 ChannelProcessingFilter:如果你访问的channel错了,那首先就会在channel之间进行跳转,如http变为https;
2、 SecurityContextPersistenceFilter:在一开始进行请求的时候就会在SecurityContextHolder中建立一个SecurityContext,然后在请求结束的时候,任何对SecurityContext的改变都会被copy到HttpSession中;
3、 ConcurrentSessionFilter:因为它需要使用SecurityContextHolder的功能,而且更新对应session的最后更新时间,以及通过SessionRegistry获取当前的SessionInformation以检查当前的session是否已经过期,过期则会调用LogoutHandler;
4、 认证处理机制:如UsernamePasswordAuthenticationFilter,CasAuthenticationFilter,BasicAuthenticationFilter等,以至于SecurityContextHolder可以被更新为包含一个有效的Authentication请求;
5、 SecurityContextHolderAwareRequestFilter:它将会把HttpServletRequest封装成一个继承自HttpServletRequestWrapper的SecurityContextHolderAwareRequestWrapper,同时使用SecurityContext实现了HttpServletRequest中与安全相关的方法;
6、 JaasApiIntegrationFilter:如果SecurityContextHolder中拥有的Authentication是一个JaasAuthenticationToken,那么该Filter将使用包含在JaasAuthenticationToken中的Subject继续执行FilterChain;
7、 RememberMeAuthenticationFilter:如果之前的认证处理机制没有更新SecurityContextHolder,并且用户请求包含了一个Remember-Me对应的cookie,那么一个对应的Authentication将会设给SecurityContextHolder;
8、 AnonymousAuthenticationFilter:如果之前的认证机制都没有更新SecurityContextHolder拥有的Authentication,那么一个AnonymousAuthenticationToken将会设给SecurityContextHolder;
9、 ExceptionTransactionFilter:用于处理在FilterChain范围内抛出的AccessDeniedException和AuthenticationException,并把它们转换为对应的Http错误码返回或者对应的页面;
10、 FilterSecurityInterceptor:保护WebURI,并且在访问被拒绝时抛出异常;

3. Filter整体执行流程图

 

我们这个章节中暂时对过滤器链的执行流程不做详细描述,下一章节再做阐述。

四. 核心API之三:认证管理相关类

1. AuthenticationManager

**AuthenticationManager的作用是校验Authentication,如果验证失败会抛出AuthenticationException异常。**AuthenticationException是抽象类,因此并不能实例化一个AuthenticationException异常并抛出,实际抛出的通常是其实现子类,如DisabledException、LockedException、BadCredentialsException等。AuthenticationManager的核心认证方法authenticate()如下所示:

 

AuthenticationManager是认证相关的核心接口,也是发起认证的起点**。**在实际开发中,我们可能会允许用户使用 用户名 + 密码 登录,同时允许用户使用 邮箱 + 密码,手机号码 + 密码 登录,甚至可能允许用户使用 指纹 登录,所以要求认证系统支持多种认证方式。几种不同的认证方式:用户名 + 密码(UsernamePasswordAuthenticationToken),邮箱 + 密码,手机号码 + 密码登录,可以分别对应三种 不同的 AuthenticationProvider 子类。

2. ProviderManager

**Spring Security 中 AuthenticationManager 接口的默认实现类是 ProviderManager,但它本身并不直接处理身份认证请求,它会委托给内部配置的 AuthenticationProvider 列表providers。**该列表会进行循环遍历,依次对比匹配以查看它是否可以执行身份验证,每个 Provider 验证程序在验证后,将会抛出异常或返回一个完全填充的 Authentication 对象。源码如下所示:

// org/springframework/security/authentication/ProviderManager.java
public class ProviderManager implements AuthenticationManager, MessageSourceAware,
        InitializingBean {
    //维护一个AuthenticationProvider列表
    private List<AuthenticationProvider> providers = Collections.emptyList();
      
    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列表,判断是否支持当前authentication对象的认证方式
        for (AuthenticationProvider provider : getProviders()) {
            if (!provider.supports(toTest)) {
                continue;
            }

            try {
        		// 执行provider的认证方式并获取返回结果
                result = provider.authenticate(authentication);

                if (result != null) {
                    copyDetails(authentication, result);
                    break;
                }
            }
            catch (AccountStatusException | InternalAuthenticationServiceException e) {
                prepareException(e, authentication);
                throw e;
            } catch (AuthenticationException e) {
                lastException = e;
            }
        }

    	// 若当前ProviderManager无法完成认证操作,且其包含父级认证器,则允许转交给父级认证器尝试进行认证
        if (result == null && parent != null) {
            try {
                result = parentResult = parent.authenticate(authentication);
            }
            catch (ProviderNotFoundException e) {
            }
            catch (AuthenticationException e) {
                lastException = parentException = e;
            }
        }

    
        if (result != null) {
            if (eraseCredentialsAfterAuthentication
                    && (result instanceof CredentialsContainer)) {
        		// 完成认证,从authentication对象中移除私密数据
                ((CredentialsContainer) result).eraseCredentials();
            }
      
      		// 若父级AuthenticationManager认证成功,则派发AuthenticationSuccessEvent事件
            if (parentResult == null) {
                eventPublisher.publishAuthenticationSuccess(result);
            }
            return result;
        }
    
    	// 未认证成功,抛出ProviderNotFoundException异常
        if (lastException == null) {
            lastException = new ProviderNotFoundException(messages.getMessage(
                    "ProviderManager.providerNotFound",
                    new Object[] { toTest.getName() },
                    "No AuthenticationProvider found for {0}"));
        }
    
        if (parentException == null) {
            prepareException(lastException, authentication);
        }

        throw lastException;
    }
}

**在ProviderManager 进行认证的过程中,会遍历 providers 列表,判断是否支持对当前 authentication 对象的认证方式。若支持该认证方式时,就会调用所匹配 provider(AuthenticationProvider)对象的 authenticate()方法进行认证操作;**若认证失败则返回 null,下一个 AuthenticationProvider 会继续尝试认证。如果所有认证器都无法认证成功,则 ProviderManager 会抛出一个 ProviderNotFoundException 异常。

五. 核心API之四:认证实现类AuthenticationProvider

1. UsernamePasswordAuthenticationToken

 

UsernamePasswordAuthenticationToken继承自AbstractAuthenticationToken类,实现了Authentication接口。在 Spring Security 中,当我们使用用户名和密码进行登录认证的时候,用户在登录表单中提交的用户名和密码信息,都会被封装到 UsernamePasswordAuthenticationToken 对象****中

这个生成的Token(Authentication)对象,接下来会被交由AuthenticationManager来进行管理,而AuthenticationManager内部又管理了一系列的AuthenticationProvider对象,每一个Provider都会调用UserDetailsService和UserDetail类,来查询返回一个Authentication对象,该对象中包含有用户名、密码、权限等信息。

2. AuthenticationProvider简介

AuthenticationManager是负责管理协调认证工作的,但并不负责认证功能的真正实现,认证功能的真正实现是由 AuthenticationManager 中引用的 AuthenticationProvider 类来完成的,通过源码我们可以看到AuthenticationManager 中引用了一个providers列表**,**如下所示:

 

providers集合的泛型是*AuthenticationProvider接口,*AuthenticationProvider接口有多个实现子类,如下图:

 

3. DaoAuthenticationProvider类关系

AuthenticationProvider接口的一个直接子类是AbstractUserDetailsAuthenticationProvider,该类又有一个直接子类DaoAuthenticationProvider,这三个类之间的关系图如下:

 

Spring Security中默认就是使用DaoAuthenticationProvider来实现基于数据库模型认证授权工作的!

DaoAuthenticationProvider 在进行认证的时候,需要调用 UserDetailsService 对象的loadUserByUsername() 方法来获取用户信息 UserDetails,其中包括用户名、密码和所拥有的权限等。所以如果我们需要改变认证方式,可以实现自己的 AuthenticationProvider;如果需要改变认证的用户信息来源,我们可以实现 UserDetailsService。

4. authenticate()认证方法源码剖析

**在实际项目中,最常见的认证方式是基于数据库模型,使用用户名和密码进行认证,具体是由DaoAuthenticationProvider类实现的。**对于已注册的用户,因为我们在数据库中已保存了正确的用户名和密码,所以认证就是对比用户在登录表单中提交的用户名、密码,与数据库中所保存的用户名、密码是否相同。这个认证的实现,主要是基于如下源码来实现的。

 

首先我们知道,在AuthenticationProvider接口中有个authenticate()方法,但是该方法并没有默认实现,如下所示:

public interface AuthenticationProvider {
	
	Authentication authenticate(Authentication authentication)
			throws AuthenticationException;

	boolean supports(Class<?> authentication);
}

这个authenticate()方法是在AuthenticationProvider的子类AbstractUserDetailsAuthenticationProvider中实现的,实现源码如下:

public Authentication authenticate(Authentication authentication)
			throws AuthenticationException {
		Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
				() -> messages.getMessage(
						"AbstractUserDetailsAuthenticationProvider.onlySupports",
						"Only UsernamePasswordAuthenticationToken is supported"));

		// 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()方法,根据用户名查询用户对象。
                //其中的retrieveUser()方法是个抽象方法,由子类DaoAuthenticationProvider来实现。
				user = retrieveUser(username,
						(UsernamePasswordAuthenticationToken) authentication);
			}
			catch (UsernameNotFoundException notFound) {
				logger.debug("User '" + username + "' not found");

				if (hideUserNotFoundExceptions) {
					throw new BadCredentialsException(messages.getMessage(
							"AbstractUserDetailsAuthenticationProvider.badCredentials",
							"Bad credentials"));
				}
				else {
					throw notFound;
				}
			}

			Assert.notNull(user,
					"retrieveUser returned null - a violation of the interface contract");
		}

		try {
			preAuthenticationChecks.check(user);
			additionalAuthenticationChecks(user,
					(UsernamePasswordAuthenticationToken) authentication);
		}
		catch (AuthenticationException exception) {
			if (cacheWasUsed) {
				// There was a problem, so try again after checking
				// we're using latest data (i.e. not from the cache)
				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();
		}

		return createSuccessAuthentication(principalToReturn, authentication, user);
	}

5. retrieveUser()检索用户方法源码剖析

在上面的authenticate()方法内部,调用了retrieveUser()方法,该方法是抽象方法,根据用户名查询用户信息,由子类DaoAuthenticationProvider来具体实现,如下所示:

 

当我们使用用户名和密码进行认证时,用户在登录页面表单中提交的用户名和密码,会被封装成了 UsernamePasswordAuthenticationToken对象。在 DaoAuthenticationProvider 类中,会对retrieveUser()方法进行具体实现,来根据用户名加载用户的信息。该方法虽然有两个参数,但只有第一个参数起主要作用,该方法会返回一个 UserDetails对象。retrieveUser 方法的具体实现如下:

protected final UserDetails retrieveUser(String username,
			UsernamePasswordAuthenticationToken authentication)throws AuthenticationException {
	prepareTimingAttackProtection();
	try {
        //这里调用了我们自己编写的UserDetailsService对象中的loadUserByUsername()方法!
		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);
	}
}

在DaoAuthenticationProvider 类的 retrieveUser() 方法中,会以传入的 username 作为参数,再调用 UserDetailsService 对象的 loadUserByUsername() 方法加载用户。

6. 完整的authenticate流程

1、 在ProviderManager类中遍历Listproviders集合,判断AuthenticationProvider类是否支持对当前对象进行认证;
2、 如果支持,则调用AuthenticationProvider接口对象的authenticate(authentication)方法进行认证;
3、 结合当前具体的认证实现模型,如果是基于数据库模型来进行表单认证,则执行AbstractUserDetailsAuthenticationProvider类中的authenticate(authentication)方法具体进行认证;
4、 AbstractUserDetailsAuthenticationProvider类中的authenticate(authentication)方法内部会调用retrieveUser()抽象方法加载对象;
5、 在DaoAuthenticationProvider类中具体执行retrieveUser()方法,在该方法中调用UserDetailsService对象的loadUserByUsername(username)方法,从而实现根据用户名从数据库中查询用户信息;

六. 核心API之五:UserDetails 、GrantedAuthority、UserDetailsService与User

1. UserDetails接口

UserDetails 是 Spring Security 中的核心接口,它表示用户的详细信息,这个接口涵盖了一些必要的用户信息字段。我们可以将UserDetails视为自己的用户数据库和SecurityContextHolder所需的适配器,通过具体的实现类对它进行扩展。通常我们可以将UserDetails强制转换为应用程序提供的原始对象,以便我们调用特定的业务方法(例如getEmail(),getEmployeeNumber()等)。UserDetails接口的源码定义如下:

public interface UserDetails extends Serializable {
    //获取用户权限,本质上是用户的角色信息
    Collection<? extends GrantedAuthority> getAuthorities();
    //获取密码
    String getPassword();
    //获取用户名
    String getUsername();
    //判断账户是否未过期
    boolean isAccountNonExpired();
    //判断账户是否未锁定
    boolean isAccountNonLocked();
    //判断密码是否未过期
    boolean isCredentialsNonExpired();
    //判断账户是否可用
    boolean isEnabled();
}

另外我们在前面也介绍过另一个 Authentication 接口,它与 UserDetails 接口的定义如下:

 

虽然Authentication 与 UserDetails 很类似,但它们之间是有区别的。Authentication 的 getCredentials() 与 UserDetails 中的 getPassword() 需要被区分对待,前者是用户从前端传递提交过来的密码凭证,后者是用户存储的正确的密码,认证器其实就是对这两者进行比对。

**此外 Authentication 中的 getAuthorities() 实际是由 UserDetails 的 getAuthorities() 传递而形成的。**还记得 Authentication 接口中的 getUserDetails() 方法吗?其中的 UserDetails 用户详细信息就是经过 provider(AuthenticationProvider) 认证之后被填充的。在Spring Security中有多处引用这个UserDetails接口,比如在 DaoAuthenticationProvider 类中,retrieveUser 方法签名是这样的:

protected final UserDetails retrieveUser(String username,
    UsernamePasswordAuthenticationToken authentication) 
  throws AuthenticationException {
}

2. GrantedAuthority接口

我们在前面介绍的Authentication类中有一个getAuthorities()方法,该方法可以返回当前 Authentication 对象拥有的角色权限,即当前用户拥有的角色权限。该方法的返回值是一个 GrantedAuthority 类型的数组,每一个 GrantedAuthority 对象代表赋予给当前用户的一种角色权限。GrantedAuthority 是一个接口,其通常是通过 UserDetailsService 进行加载,然后再赋值给 UserDetails。

GrantedAuthority 接口中只定义了一个 getAuthority() 方法,该方法会返回一个字符串,表示对应的角色权限,如果对应权限不能用字符串表示,则应当返回 null。

public interface GrantedAuthority extends Serializable {
	
	String getAuthority();
}

该接口的默认实现类是SimpleGrantedAuthority,该类只是简单的接收一个表示权限的字符串。Spring Security 内部的所有 AuthenticationProvider 都是使用 SimpleGrantedAuthority 来封装 Authentication 对象。如下图所示:

 

我们前面讲过的Authentication类中,提供的一个重要方法就是getAuthorities(),该方法会返回GrantedAuthority这个对象的数组。该对象代表的权限通常是“角色”,例如ROLE_ADMINISTRATOR或ROLE_HR_SUPERVISOR。我们可以将这些角色配置为 Web 授权、方法授权和域对象授权。 GrantedAuthority对象通常由UserDetailsService加载。

另外在Spring Security中,角色和权限共用这个GrantedAuthority接口,唯一的不同点在于角色多了"ROLE_"前缀,**而且它没有Shiro中的那种从属关系,即一个角色包含哪些权限等。**在Spring Security看来角色和权限是一样的,它认证的时候,会把所有权限(角色、权限)都取出来,并不是分开验证的。所以,在Spring Security提供的UserDetailsService默认的实现类JdbcDaoImpl中,角色和权限都存储在auhtorities表中。而不是像Shiro那样,角色有个roles表,权限有个permissions表,以及相关的管理表等等。

3. UserDetailsService接口

UserDetailsService也是一个接口,该接口中只有一个方法loadUserByUsername(),该方法可以用来获取UserDetails对象,源码如下:

public interface UserDetailsService {
	/**
	 * Locates the user based on the username. In the actual implementation, the search
	 * may possibly be case sensitive, or case insensitive depending on how the
	 * implementation instance is configured. In this case, the <code>UserDetails</code>
	 * object that comes back may have a username that is of a different case than what
	 * was actually requested..
	 */
	UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

在loadUserByUsername 方法中,可以通过 username 来加载匹配的用户,当找不到 username 对应的用户时,会抛出 UsernameNotFoundException 异常。

**UserDetailsService 常见的实现类有 JdbcDaoImpl,InMemoryUserDetailsManager,前者从数据库中加载用户,后者从内存中加载用户。**JdbcDaoImpl 允许我们从数据库中来加载一个 UserDetails,其底层使用的是 Spring 的 JdbcTemplate 进行操作,所以我们需要给其指定一个数据源。InMemoryDaoImpl 主要是测试用的,其只是简单的将用户信息保存在内存中。

当然我们也可以自己实现 UserDetailsService接口,实现其中的loadUserByUsername()方法,在该方法中可以通过查询数据库(或者是缓存、或者是其他的存储形式)来获取用户信息,然后组装成一个UserDetails并返回。

另外UserDetailsService 和 AuthenticationProvider 两者的职责很像,常常被人们搞混,记住一点即可,UserDetailsService 只负责从特定的地方(通常是数据库)加载用户信息,仅此而已。

4. User类

在Spring Security中自带了一个User类,该类实现了UserDetails接口,内部封装了基本的用户信息,我们其实可以直接使用该User类来封装用户信息,但是也可以对该User类进行扩展。User类的基本关系图如下:

 

User类中包含如下基本的信息:

public class User implements UserDetails, CredentialsContainer {

	private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

	private static final Log logger = LogFactory.getLog(User.class);

	// ~ Instance fields
	// ================================================================================================
	private String password;
	private final String username;
	private final Set<GrantedAuthority> authorities;
	private final boolean accountNonExpired;
	private final boolean accountNonLocked;
	private final boolean credentialsNonExpired;
	private final boolean enabled;
    ......
    ......    
        

User类中包含的主要方法如下图所示:

 

 

七. 总结

到目前为止,我们已经简单介绍了 Spring Security 的几个核心API:

  • SecurityContextHolder,提供对SecurityContext的访问权限。
  • SecurityContext,保存Authentication以及可能特定于请求的安全信息。
  • Authentication,Spring Security 的主体信息。
  • GrantedAuthority,代表授予主体的权限。
  • UserDetails,通过提供必要的信息,从应用程序的 DAO 或其他安全数据源来构建出 Authentication 对象。
  • UserDetailsService,通过传入String类型的用户名(或证书 ID 等),创建出对应的UserDetails。
  • AuthenticationProvider,负责进行用户的认证授权工作。

现在,我们已经了解了这些重要的核心API,接下来我们就可以学习与身份认证授权流程相关的源码了。这些核心API,你都记住了吗?记住的请在评论区666,没记住的请在评论区555吧!

版权声明:本文不是「本站」原创文章,版权归原作者所有 | 原文地址: