这一节主要介绍一些在Spring Security中常见且核心的Java类,它们之间的依赖,构建起了整个框架。想要理解整个架构,最起码得对这些类眼熟。java
SecurityContextHolder
用于存储安全上下文(security context)的信息。当前操做的用户是谁,该用户是否已经被认证,他拥有哪些角色权限…这些都被保存在SecurityContextHolder中。SecurityContextHolder
默认使用ThreadLocal
策略来存储认证信息。看到ThreadLocal
也就意味着,这是一种与线程绑定的策略。Spring Security在用户登陆时自动绑定认证信息到当前线程,在用户退出时,自动清除当前线程的认证信息。但这一切的前提,是你在web场景下使用Spring Security,而若是是Swing界面,Spring也提供了支持,SecurityContextHolder
的策略则须要被替换,鉴于个人初衷是基于web来介绍Spring Security,因此这里以及后续,非web的相关的内容都一笔带过。web
由于身份信息是与线程绑定的,因此能够在程序的任何地方使用静态方法获取用户信息。一个典型的获取当前登陆用户的姓名的例子以下所示:spring
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); if (principal instanceof UserDetails) { String username = ((UserDetails)principal).getUsername(); } else { String username = principal.toString(); }
getAuthentication()返回了认证信息,再次getPrincipal()返回了身份信息,UserDetails即是Spring对身份信息封装的一个接口。Authentication和UserDetails的介绍在下面的小节具体讲解,本节重要的内容是介绍SecurityContextHolder这个容器。数据库
<!-- more -->安全
先看看这个接口的源码长什么样:session
package org.springframework.security.core;// <1> public interface Authentication extends Principal, Serializable { // <1> Collection<? extends GrantedAuthority> getAuthorities(); // <2> Object getCredentials();// <2> Object getDetails();// <2> Object getPrincipal();// <2> boolean isAuthenticated();// <2> void setAuthenticated(boolean var1) throws IllegalArgumentException; }
<1> Authentication是spring security包中的接口,直接继承自Principal类,而Principal是位于java.security
包中的。能够见得,Authentication在spring security中是最高级别的身份/认证的抽象。架构
<2> 由这个顶级接口,咱们能够获得用户拥有的权限信息列表,密码,用户细节信息,用户身份信息,认证信息。框架
还记得1.1节中,authentication.getPrincipal()返回了一个Object,咱们将Principal强转成了Spring Security中最经常使用的UserDetails,这在Spring Security中很是常见,接口返回Object,使用instanceof判断类型,强转成对应的具体实现类。接口详细解读以下:ide
1 用户名和密码被过滤器获取到,封装成Authentication
,一般状况下是UsernamePasswordAuthenticationToken
这个实现类。加密
2 AuthenticationManager
身份管理器负责验证这个Authentication
3 认证成功后,AuthenticationManager
身份管理器返回一个被填充满了信息的(包括上面提到的权限信息,身份信息,细节信息,但密码一般会被移除)Authentication
实例。
4 SecurityContextHolder
安全上下文容器将第3步填充了信息的Authentication
,经过SecurityContextHolder.getContext().setAuthentication(…)方法,设置到其中。
这是一个抽象的认证流程,而整个过程当中,若是不纠结于细节,其实只剩下一个AuthenticationManager
是咱们没有接触过的了,这个身份管理器咱们在后面的小节介绍。将上述的流程转换成代码,即是以下的流程:
public class AuthenticationExample { private static AuthenticationManager am = new SampleAuthenticationManager(); public static void main(String[] args) throws Exception { 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(result); break; } catch(AuthenticationException e) { System.out.println("Authentication failed: " + e.getMessage()); } } System.out.println("Successfully authenticated. Security context contains: " + SecurityContextHolder.getContext().getAuthentication()); } } class SampleAuthenticationManager implements AuthenticationManager { static final List<GrantedAuthority> AUTHORITIES = new ArrayList<GrantedAuthority>(); static { AUTHORITIES.add(new SimpleGrantedAuthority("ROLE_USER")); } public Authentication authenticate(Authentication auth) throws AuthenticationException { if (auth.getName().equals(auth.getCredentials())) { return new UsernamePasswordAuthenticationToken(auth.getName(), auth.getCredentials(), AUTHORITIES); } throw new BadCredentialsException("Bad Credentials"); } }
注意:上述这段代码只是为了让你们了解Spring Security的工做流程而写的,不是什么源码。在实际使用中,整个流程会变得更加的复杂,可是基本思想,和上述代码一模一样。
初次接触Spring Security的朋友相信会被AuthenticationManager
,ProviderManager
,AuthenticationProvider
…这么多类似的Spring认证类搞得晕头转向,但只要稍微梳理一下就能够理解清楚它们的联系和设计者的用意。AuthenticationManager(接口)是认证相关的核心接口,也是发起认证的出发点,由于在实际需求中,咱们可能会容许用户使用用户名+密码登陆,同时容许用户使用邮箱+密码,手机号码+密码登陆,甚至,可能容许用户使用指纹登陆(还有这样的操做?没想到吧),因此说AuthenticationManager通常不直接认证,AuthenticationManager接口的经常使用实现类ProviderManager
内部会维护一个List<AuthenticationProvider>
列表,存放多种认证方式,实际上这是委托者模式的应用(Delegate)。也就是说,核心的认证入口始终只有一个:AuthenticationManager,不一样的认证方式:用户名+密码(UsernamePasswordAuthenticationToken),邮箱+密码,手机号码+密码登陆则对应了三个AuthenticationProvider。这样一来四不四就好理解多了?熟悉shiro的朋友能够把AuthenticationProvider理解成Realm。在默认策略下,只须要经过一个AuthenticationProvider的认证,便可被认为是登陆成功。
只保留了关键认证部分的ProviderManager源码:
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; Authentication result = null; // 依次认证 for (AuthenticationProvider provider : getProviders()) { if (!provider.supports(toTest)) { continue; } try { result = provider.authenticate(authentication); if (result != null) { copyDetails(authentication, result); break; } } ... catch (AuthenticationException e) { lastException = e; } } // 若是有Authentication信息,则直接返回 if (result != null) { if (eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) { //移除密码 ((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
中的List<AuthenticationProvider>,会依照次序去认证,认证成功则当即返回,若认证失败则返回null,下一个AuthenticationProvider会继续尝试认证,若是全部认证器都没法认证成功,则ProviderManager
会抛出一个ProviderNotFoundException异常。
到这里,若是不纠结于AuthenticationProvider的实现细节以及安全相关的过滤器,认证相关的核心类其实都已经介绍完毕了:身份信息的存放容器SecurityContextHolder,身份信息的抽象Authentication,身份认证器AuthenticationManager及其认证流程。姑且在这里作一个分隔线。下面来介绍下AuthenticationProvider接口的具体实现。
AuthenticationProvider最最最经常使用的一个实现即是DaoAuthenticationProvider。顾名思义,Dao正是数据访问层的缩写,也暗示了这个身份认证器的实现思路。因为本文是一个Overview,姑且只给出其UML类图:
按照咱们最直观的思路,怎么去认证一个用户呢?用户前台提交了用户名和密码,而数据库中保存了用户名和密码,认证即是负责比对同一个用户名,提交的密码和保存的密码是否相同即是了。在Spring Security中。提交的用户名和密码,被封装成了UsernamePasswordAuthenticationToken,而根据用户名加载用户的任务则是交给了UserDetailsService,在DaoAuthenticationProvider中,对应的方法即是retrieveUser,虽然有两个参数,可是retrieveUser只有第一个参数起主要做用,返回一个UserDetails。还须要完成UsernamePasswordAuthenticationToken和UserDetails密码的比对,这即是交给additionalAuthenticationChecks方法完成的,若是这个void方法没有抛异常,则认为比对成功。比对密码的过程,用到了PasswordEncoder和SaltSource,密码加密和盐的概念相信不用我赘述了,它们为保障安全而设计,都是比较基础的概念。
若是你已经被这些概念搞得晕头转向了,不妨这么理解DaoAuthenticationProvider:它获取用户提交的用户名和密码,比对其正确性,若是正确,返回一个数据库中的用户信息(假设用户信息被保存在数据库中)。
上面不断提到了UserDetails这个接口,它表明了最详细的用户信息,这个接口涵盖了一些必要的用户信息字段,具体的实现类对它进行了扩展。
public interface UserDetails extends Serializable { Collection<? extends GrantedAuthority> getAuthorities(); String getPassword(); String getUsername(); boolean isAccountNonExpired(); boolean isAccountNonLocked(); boolean isCredentialsNonExpired(); boolean isEnabled(); }
它和Authentication接口很相似,好比它们都拥有username,authorities,区分他们也是本文的重点内容之一。Authentication的getCredentials()与UserDetails中的getPassword()须要被区分对待,前者是用户提交的密码凭证,后者是用户正确的密码,认证器其实就是对这二者的比对。Authentication中的getAuthorities()实际是由UserDetails的getAuthorities()传递而造成的。还记得Authentication接口中的getUserDetails()方法吗?其中的UserDetails用户详细信息即是通过了AuthenticationProvider以后被填充的。
public interface UserDetailsService { UserDetails loadUserByUsername(String username) throws UsernameNotFoundException; }
UserDetailsService和AuthenticationProvider二者的职责经常被人们搞混,关于他们的问题在文档的FAQ和issues中家常便饭。记住一点便可,敲黑板!!!UserDetailsService只负责从特定的地方(一般是数据库)加载用户信息,仅此而已,记住这一点,能够避免走不少弯路。UserDetailsService常见的实现类有JdbcDaoImpl,InMemoryUserDetailsManager,前者从数据库加载用户,后者从内存中加载用户,也能够本身实现UserDetailsService,一般这更加灵活。
为了更加形象的理解上述我介绍的这些核心类,附上一张按照个人理解,所画出Spring Security的一张非典型的UML图