浅析Spring Security 核心组件

原文博客地址: pjmike的博客html

前言

近几天在网上找了一个 Spring Security 和JWT 的例子来学习,项目地址是: github.com/szerhusenBC… 做为学习Spring Security仍是不错的,经过研究该 demo 发现本身对 Spring Security只知其一;不知其二,并无弄清楚Spring Seurity的流程,因此才想写一篇文章先来分析分析Spring Security的核心组件,其中参考了官方文档及其一些大佬写的Spring Security分析文章,有雷同的地方还请见谅。java

Spring Security的核心类

Spring Security的核心类主要包括如下几个:git

  • SecurityContextHolder: 存放身份信息的容器
  • Authentication: 身份信息的抽象接口
  • AuthenticationManager: 身份认证器,认证的核心接口
  • UserDetailsService: 通常用于从数据库中加载身份信息
  • UserDetails: 相比Authentication,有更详细的身份信息

SecurityContextHolder、Securityontext和Authentication

SecurityContextHolder用于存储安全上下文(security context)的信息,即一个存储身份信息,认证信息等的容器。SecurityContextHolder默认使用 ThreadLocal策略来存储认证信息,即一种与线程绑定的策略,每一个线程执行时均可以获取该线程中的 安全上下文(security context),各个线程中的安全上下文互不影响。并且若是说要在请求结束后清除安全上下文中的信息,利用该策略Spring Security也能够轻松搞定。github

由于身份信息时与线程绑定的,因此咱们能够在程序的任何地方使用静态方法获取用户信息,一个获取当前登陆用户的姓名的例子以下:web

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类,该接口表示对用户身份信息的抽象,接口源码以下:spring

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、ProviderManager 和 AuthenticationProvider

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全部的属性都会被填充。api

在Spring Security中,AuthenticationManager默认的实现类是 ProviderManagerProviderManager并非本身直接对请求进行验证,而是将其委派给一个 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异常。bash

事实上,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配置类中配置)

PS:

固然有时候咱们有多个不一样的 AuthenticationProvider,它们分别支持不一样的 Authentication对象,那么当一个具体的 AuthenticationProvier传进入 ProviderManager的内部时,就会在 AuthenticationProvider列表中挑选其对应支持的provider对相应的 Authentication对象进行验证。

不一样的登陆方式认证逻辑是不同的,即 AuthenticationProvider会不同,若是使用用户名和密码登陆,那么在Spring Security 提供了一个 AuthenticationProvider的简单实现 DaoAuthenticationProvider,这也是框架最先的 provider,它使用了一个 UserDetailsService来查询用户名、密码和 GrantedAuthority,通常咱们要实现UserDetailsService接口,,并在Spring Security配置类中将其配置进去,这样也促使使用DaoAuthenticationProvider进行认证,而后该接口返回一个UserDetails,它包含了更加详细的身份信息,好比从数据库拿取的密码和权限列表,AuthenticationProvider 的认证核心就是加载对应的 UserDetails来检查用户输入的密码是否与其匹配,即UserDetails和Authentication二者的密码(关于 UserDetailsServiceUserDetails的介绍在下面小节介绍。)。而若是是使用第三方登陆,好比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

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。它们的区别以下:

  • Authentication 的 getCredentials() 与 UserDetails 中的 getPassword() 不同,前者是用户提交的密码凭证,后者是用户正确的密码,(通常是从数据库中载入的密码),AuthenticationProvider就会对二者进行对比。
  • Authentication 中的 getAuthorities() 其实是由 UserDetails 的 getAuthorities()传递造成的。
  • Authentication 中的 getUserDetails() 中的 UserDetails 用户详细信息时通过 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的认证过程。

参考资料 & 鸣谢

相关文章
相关标签/搜索