原文博客地址: pjmike的博客html
近几天在网上找了一个 Spring Security 和JWT 的例子来学习,项目地址是: github.com/szerhusenBC… 做为学习Spring Security仍是不错的,经过研究该 demo 发现本身对 Spring Security
只知其一;不知其二,并无弄清楚Spring Seurity的流程,因此才想写一篇文章先来分析分析Spring Security的核心组件,其中参考了官方文档及其一些大佬写的Spring Security分析文章,有雷同的地方还请见谅。java
Spring Security的核心类主要包括如下几个:git
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是身份认证器,认证的核心接口,接口源码以下:数据库
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
默认的实现类是 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
异常。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二者的密码(关于 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的认证过程。