近几天在网上找了一个 Spring Security 和JWT 的例子来学习,项目地址是: github.com/szerhusenBC…
作为学习Spring Security还是不错的,通过研究该 demo 发现自己对 Spring Security
一知半解,并没有弄清楚Spring Seurity的流程,所以才想写一篇文章先来分析分析Spring Security的核心组件,其中参考了官方文档及其一些大佬写的Spring Security分析文章,有雷同的地方还请见谅。
Spring Security的核心类主要包括以下几个:
SecurityContextHolder
用于存储安全上下文(security context)的信息,即一个存储身份信息,认证信息等的容器。 SecurityContextHolder
默认使用 ThreadLocal
策略来存储认证信息,即一种与线程绑定的策略,每个线程执行时都可以获取该线程中的 安全上下文(security context),各个线程中的安全上下文互不影响。而且如果说要在请求结束后清除安全上下文中的信息,利用该策略Spring Security也可以轻松搞定。
因为身份信息时与线程绑定的,所以我们可以在程序的任何地方使用静态方法获取用户信息,一个获取当前登录用户的姓名的例子如下:
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); if (principal instanceof UserDetails) { String username = ((UserDetails)principal).getUsername(); } else { String username = principal.toString(); } 复制代码
getAuthentication()
方法返回了认证信息,准确的说是一个 Authentication
实例, Authentication
是 Spring Security 中的一个重要接口,直接继承自 Principal类,该接口表示对用户身份信息的抽象,接口源码如下:
public interface Authentication extends Principal, Serializable { //权限信息列表,默认是 GrantedAuthority接口的一些实现 Collection<? extends GrantedAuthority> getAuthorities(); //密码信息,用户输入的密码字符串,认证后通常会被移除,用于保证安全 Object getCredentials(); //细节信息,web应用中通常的接口为 WebAuthenticationDetails,它记录了访问者的ip地址和sessionId的值 Object getDetails(); //身份信息,返回UserDetails的实现类 Object getPrincipal(); //认证状态,默认为false,认证成功后为 true boolean isAuthenticated(); //上述身份信息是否经过身份认证 void setAuthenticated(boolean var1) throws IllegalArgumentException; } 复制代码
AuthenticationManager是身份认证器,认证的核心接口,接口源码如下:
public interface AuthenticationManager { /** * Attempts to authenticate the passed {@link Authentication} object, returning a * fully populated <code>Authentication</code> object (including granted authorities) * @param authentication the authentication request object * * @return a fully authenticated object including credentials * * @throws AuthenticationException if authentication fails */ Authentication authenticate(Authentication authentication) throws AuthenticationException; } 复制代码
该接口只有一个 authenticate()
方法,用于身份信息的认证,如果认证成功,将会返回一个带了完整信息的 Authentication
,在之前提到的 Authentication
所有的属性都会被填充。
在Spring Security中, AuthenticationManager
默认的实现类是 ProviderManager
, ProviderManager
并不是自己直接对请求进行验证,而是将其委派给一个 AuthenticationProvider
列表。列表中的每一个 AuthenticationProvider
将会被依次查询是否需要通过其进行验证,每个 provider的验证结果只有两个情况:抛出一个异常或者完全填充一个 Authentication
对象的所有属性。 ProviderManager
中的部分源码如下:
public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean { //维护一个AuthenticationProvider 列表 private List<AuthenticationProvider> providers = Collections.emptyList(); private AuthenticationManager parent; //构造器,初始化 AuthenticationProvider 列表 public ProviderManager(List<AuthenticationProvider> providers) { this(providers, null); } public ProviderManager(List<AuthenticationProvider> providers, AuthenticationManager parent) { Assert.notNull(providers, "providers list cannot be null"); this.providers = providers; this.parent = parent; checkState(); } public Authentication authenticate(Authentication authentication) throws AuthenticationException { Class<? extends Authentication> toTest = authentication.getClass(); AuthenticationException lastException = null; Authentication result = null; boolean debug = logger.isDebugEnabled(); // AuthenticationProvider 列表中每个Provider依次进行认证 for (AuthenticationProvider provider : getProviders()) { if (!provider.supports(toTest)) { continue; } ... try { //调用 AuthenticationProvider 的 authenticate()方法进行认证 result = provider.authenticate(authentication); if (result != null) { copyDetails(authentication, result); break; } } ... catch (AuthenticationException e) { lastException = e; } } // 如果 AuthenticationProvider 列表中的Provider都认证失败,且之前有构造一个 AuthenticationManager 实现类,那么利用AuthenticationManager 实现类 继续认证 if (result == null && parent != null) { // Allow the parent to try. try { result = parent.authenticate(authentication); } ... catch (AuthenticationException e) { lastException = e; } } //认证成功 if (result != null) { if (eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) { // Authentication is complete. Remove credentials and other secret data // from authentication //成功认证后删除验证信息 ((CredentialsContainer) result).eraseCredentials(); } //发布登录成功事件 eventPublisher.publishAuthenticationSuccess(result); return result; } // 没有认证成功,抛出一个异常 if (lastException == null) { lastException = new ProviderNotFoundException(messages.getMessage( "ProviderManager.providerNotFound", new Object[] { toTest.getName() }, "No AuthenticationProvider found for {0}")); } prepareException(lastException, authentication); throw lastException; } 复制代码
ProviderManager中的 authenticationManager
列表依次去尝试认证,认证成功即返回,认证失败返回null,如果所有的 Provider都认证失败, ProviderManager
将会抛出一个 ProviderNotFoundException
异常。
事实上, AuthenticationProvider
是一个接口,接口定义如下:
public interface AuthenticationProvider { //认证方法 Authentication authenticate(Authentication authentication) throws AuthenticationException; //该Provider是否支持对应的Authentication boolean supports(Class<?> authentication); } 复制代码
在 ProviderManager
的 Javadoc曾提到,
If more than one AuthenticationProvider supports the passed Authentication object, the first one able to successfully authenticate the Authentication object determines the result, overriding any possible AuthenticationException thrown by earlier supporting AuthenticationProvider s. On successful authentication, no subsequent AuthenticationProvider s will be tried. If authentication was not successful by any supporting AuthenticationProvider the last thrown AuthenticationException will be rethrown
大致意思是:
如果有多个 AuthenticationProvider 都支持同一个Authentication 对象,那么 第一个 能够成功验证Authentication的 Provder
将填充其属性并返回结果,从而覆盖早期支持的 AuthenticationProvider抛出的任何可能的 AuthenticationException。一旦成功验证后,将不会尝试后续的 AuthenticationProvider。如果所有的 AuthenticationProvider
都没有成功验证 Authentication,那么将抛出最后一个Provider抛出的AuthenticationException。(AuthenticationProvider可以在Spring Security配置类中配置)
当然有时候我们有多个不同的 AuthenticationProvider
,它们分别支持不同的 Authentication
对象,那么当一个具体的 AuthenticationProvier
传进入 ProviderManager
的内部时,就会在 AuthenticationProvider
列表中挑选其对应支持的provider对相应的 Authentication对象进行验证。
不同的登录方式认证逻辑是不一样的,即 AuthenticationProvider
会不一样,如果使用用户名和密码登录,那么在Spring Security 提供了一个 AuthenticationProvider
的简单实现 DaoAuthenticationProvider
,这也是框架最早的 provider,它使用了一个 UserDetailsService
来查询用户名、密码和 GrantedAuthority
,一般我们要实现 UserDetailsService
接口,,并在Spring Security配置类中将其配置进去,这样也促使使用 DaoAuthenticationProvider
进行认证,然后该接口返回一个 UserDetails
,它包含了更加详细的身份信息,比如从数据库拿取的密码和权限列表,AuthenticationProvider 的认证核心就是加载对应的 UserDetails
来检查用户输入的密码是否与其匹配,即UserDetails和Authentication两者的密码(关于 UserDetailsService
和 UserDetails
的介绍在下面小节介绍。)。
而如果是使用第三方登录,比如QQ登录,那么就需要设置对应的 AuthenticationProvider
,这里就不细说了。
在上面ProviderManager的源码中我还发现一点,在认证成功后清除验证信息,如下:
if (eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) { // Authentication is complete. Remove credentials and other secret data // from authentication //成功认证后删除验证信息 ((CredentialsContainer) result).eraseCredentials(); } 复制代码
从 spring Security 3.1之后,在请求认证成功后 ProviderManager
将会删除 Authentication
中的认证信息,准确的说,一般删除的是 密码信息,这可以保证密码的安全。我跟了一下源码,实际上执行删除操作的步骤如下:
public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken { public void eraseCredentials() { super.eraseCredentials(); //使密码为null this.credentials = null; } } public abstract class AbstractAuthenticationToken implements Authentication, CredentialsContainer { ... public void eraseCredentials() { //擦除密码 this.eraseSecret(this.getCredentials()); this.eraseSecret(this.getPrincipal()); this.eraseSecret(this.details); } private void eraseSecret(Object secret) { if (secret instanceof CredentialsContainer) { ((CredentialsContainer)secret).eraseCredentials(); } } } 复制代码
从源码就可以看出实际上就是擦除密码操作。
UserDetailsService
简单说就是加载对应的 UserDetails
的接口(一般从数据库),而 UserDetails
包含了更详细的用户信息 ,定义如下:
public interface UserDetails extends Serializable { Collection<? extends GrantedAuthority> getAuthorities(); String getPassword(); String getUsername(); boolean isAccountNonExpired(); boolean isAccountNonLocked(); boolean isCredentialsNonExpired(); boolean isEnabled(); } 复制代码
UserDetails 接口与 Authentication接口相似,它们都有 username、authorities。它们的区别如下:
AuthenticationProvider AuthenticationProvider
下面来看一个官方文档提供的例子,代码如下:
public class SpringSecuriryTestDemo { private static AuthenticationManager am = new SampleAuthenticationManager(); public static void main(String[] args) throws IOException { BufferedReader in = new BufferedReader(new InputStreamReader(System.in)); while (true) { System.out.println("Please enter your username:"); String name = in.readLine(); System.out.println("Please enter your password:"); String password = in.readLine(); try { Authentication request = new UsernamePasswordAuthenticationToken(name, password); Authentication result = am.authenticate(request); SecurityContextHolder.getContext().setAuthentication(request); break; } catch (AuthenticationException e) { System.out.println("Authentication failed: " + e.getMessage()); } } System.out.println("Successfully authenticated. Security context contains: " + SecurityContextHolder.getContext().getAuthentication()); } static class SampleAuthenticationManager implements AuthenticationManager { static final List<GrantedAuthority> AUTHORITIES = new ArrayList<GrantedAuthority>(); static { AUTHORITIES.add(new SimpleGrantedAuthority("ROLE_USER")); } @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { if (authentication.getName().equals(authentication.getCredentials())) { return new UsernamePasswordAuthenticationToken(authentication.getName(), authentication.getCredentials(), AUTHORITIES); } throw new BadCredentialsException("Bad Credentials"); } } } 复制代码
测试如下:
Please enter your username: pjmike Please enter your password: 123 Authentication failed: Bad Credentials Please enter your username: pjmike Please enter your password: pjmike Successfully authenticated. Security context contains: org.springframework.security.authentication.UsernamePasswordAuthenticationToken@441d0230: Principal: pjmike; Credentials: [PROTECTED]; Authenticated: true; Details: null; Granted Authorities: ROLE_USER 复制代码
上面的例子很简单,不是源码,只是为了演示认证过程编写的Demo,而且也缺少过滤器链,但是麻雀虽小,五脏俱全,基本包括了Spring Security的核心组件,表达了Spring Security 认证的基本思想。解读一下:
UsernamePasswordAuthentication
的实例中(该类是 Authentication
接口的实现) Authentication
传递给 AuthenticationManager
进行身份验证 AuthenticationManager
会返回一个完全填充的 Authentication
实例,该实例包含权限信息,身份信息,细节信息,但是密码通常会被移除 SecurityContextHolder.getContext().setAuthentication(…)
传入上面返回的填充了信息的 Authentication
对象 通过上面一个简单示例,我们大致明白了Spring Security的基本思想,但是要真正理清楚Spring Security的认证流程这还不够,我们需要深入源码去探究,后续文章会更加详细的分析Spring Security的认证过程。
这篇文章主要分析了Spring Security的一些核心组件,参考了官方文档及其相关译本,对核心组件有一个基本认识后,才便于后续更加详细的分析Spring Security的认证过程。