浅析 Spring Security 核心组件
前言
近几天在网上找了一个 Spring Security 和 JWT 的例子来学习,项目地址是
https://github.com/szerhusenBC/jwt-spring-security-demo
还是有发现的,作为该研究 的一篇文章,通过对Spring Security
Spring Security 的一个优秀的流程进行分析,并没有参考这篇文章先对 Spring Security 的流程进行分析,并想写一篇官方文档和 Spring Security 的核心组件一些大佬写的Spring Security分析文章,有雷同的地方还请见谅。
Spring Security的核心类
Spring Security 的核心类主要包括以下几个:
- SecurityContextHolder :存放身份信息的容器
- Authentication : 身份信息的抽象接口
- AuthenticationManager : 身份认证器,认证的核心接口
- UserDetailsService:一般用于从数据库中加载身份信息
- UserDetails : 比较认证,有更详细的身份信息
SecurityContextHolder、Securityontext和Authentication
SecurityContextHolder
存储安全信息(安全上下文)的信息,即用于存储身份,认证信息等的包含。SecurityContextHolder
默认使用 ThreadLocal
策略来存储认证信息,即一种与线程绑定的策略,各个线程执行时都可以获取该线程中的安全时间(安全上下文),影响线程中的线程如果在请求中的安全时间互不影响。
因为身份信息是与当前会话的关系,所以我们可以在程序的任何地方使用获取用户信息的方法,登录用户的姓名的例子如下:
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、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
所有属性都会被填充。
Spring Security 中,一个自己AuthenticationManager
默认 对进行请求ProviderManager
,ProviderManager
将派给给自己 AuthenticationProvider
的每个人都需要在一个目标列表中 实现目标的AuthenticationProvider
跟踪服务提供者。验证结果只有两种情况:抛出一个异常或者完全填充一个 对象的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
成功成功失败成功尝试,认证即返回失败,如果所有的Provider都认证,认证失败返回 ProviderManager
无效 ProviderNotFoundException
。
是一个接口,接口定义如下:AuthenticationProvider
public interface AuthenticationProvider { //认证方法 Authentication authenticate(Authentication authentication) throws AuthenticationException; //该Provider是否支持对应的Authentication boolean supports(Class<?> authentication); }
在 ProviderManager
的 Javadoc 曾提到,
如果多个 AuthenticationProvider 支持传递的 Authentication 对象,则第一个能够成功验证 Authentication 对象的人会确定结果,并覆盖之前支持的 AuthenticationProvider 抛出的任何可能的 AuthenticationException 。成功认证后,不会尝试后续的 AuthenticationProvider 。如果任何支持 AuthenticationProvider 的身份验证未成功,则最后抛出的 AuthenticationException 将被重新抛出
大致意思是:
如果有多个AuthenticationProvider都支持同一个Authentication对象,那么第一个能够成功验证的Provder将填充 其返回结果,那么早期支持的AuthenticationProvider抛出可能的AuthenticationException。成功验证后,将如果所有验证都不会在中验证)的AuthenticationProvider。如果 AuthenticationProvider
没有成功验证,则将抛出最后的ProviderProvider抛出异常。(AuthenticationProvider可以配置配置类的配置)
PS:
不同的AuthenticationProvider
认证支持不同的不同的对象,分别对应不同的Authentication
对象,那么当一个 不同的对象AuthenticationProvier
进入ProviderManager
的内部时, 它们会在AuthenticationProvider
它们中挑选其对的提供者相应的对象进行验证。
不同的认证逻辑是不同的,即 使用用户名和密码,使用用户名和密码,登录提供了简单的实现 ,这也是 一个用户在登录时使用的方式AuthenticationProvider
,如果用户名和密码不一样,那么 它也是一个用户可以登录的方式。密码和 我们一般要在接口中,并把 Spring Security 配置类实现其配置,这样也用于在使用中进行详细的认证,然后该接口返回一个,它包含了身份信息,比如从数据库拿取的信息密码和AuthenticationProvider的认证核心,即加载的 来列表用户的密码是否匹配,用户详情和验证以及其他的密码(关于 和就是使用的介绍在下面介绍。)。,比如QQ登录,那么就需要设置的 ,这里就不细说了。AuthenticationProviderDaoAuthenticationProviderUserDetailsServiceGrantedAuthorityUserDetailsServiceDaoAuthenticationProviderUserDetailsUserDetailsUserDetailsServiceUserDetailsAuthenticationProvider
认证成功后清除验证信息
在 ProviderManager 的源中我还发现一点,在认证成功后清除验证信息,如下:
if (eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) { // Authentication is complete. Remove credentials and other secret data // from authentication //成功认证后删除验证信息 ((CredentialsContainer) result).eraseCredentials(); }
从安全 3.1,在请求认证成功后 ProviderManager
删除 Authentication
spring 中的认证信息,准确地说,一般删除是密码信息,这保证跟密码的安全。我可以直接执行删除操作的步骤如下:
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 接口类似,它们有用户名、权限。它们的区别如下:
- Authentication的getCredentials()一般与Users中的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 核心才能解释 Spring Security 的一些详细的分析认证过程。
