首页 > 图灵资讯 > 技术篇>正文

浅析Spring Security 核心组件

2023-04-20 17:03:47

  前言最近几天在网上找到了一个 Spring Security 和JWT 学习的例子,项目地址是: github.com/szerhusenBC… 学习Spring 通过研究,Security还是不错的。 demo 发现自己对 Spring Security一知半解,Springg尚不清楚 Seurity的流程,所以我想写一篇文章来分析Spring Security的核心组件,参考官方文件和一些大人物写的Spring Security分析文章,有相似之处请原谅。

  Spring Security的核心Spring Security的核心类别主要包括以下几个:

  SecurityContextHolder: 存储身份信息的容器

  Authentication: 抽象接口的身份信息

  AuthenticationManager: 身份认证器,认证的核心接口

  UserDetailsService: 一般用于从数据库中加载身份信息

  UserDetails: 与Authentication相比,身份信息更详细

  SecurityContextHolder、Securityontext和authenticationsecuritycontextholder用于存储安全(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是Authentication Spring Security 其中一个重要接口是直接继承的 Principal类,表示用户身份信息抽象,接口源代码如下:

  public interface Authentication extends Principal, Serializable { ///权限信息列表,默认为 Grantedauthority接口的一些实现了Collection getAuthorities(); ///密码信息,用户输入的密码字符串通常在认证后删除,以确保Object的安全 getCredentials();//细节信息,web应用程序中通常的接口是 WebAuthenticationDetails,它记录了访问者的ip地址和sessionid值object getDetails();//身份信息,返回UserDetails实现Objectter getPrincipal();//认证状态,默认为false,认证成功后为 trueboolean isAuthenticated();//上述身份信息是否通过身份认证 void setAuthenticated(boolean var1) throws IllegalArgumentException;}AuthenticationManager、ProviderManager 和 AuthenticationProviderAuthenticationmanager是身份认证器,核心接口认证,接口源代码如下:

  public interface AuthenticationManager {/*** Attempts to authenticate the passed {@link Authentication} object, returning a* fully populated Authentication 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中,AuthenticationManger默认实现类是 ProviderManager,ProviderManager并没有直接验证请求,而是将其委托给了一个 AuthenticationProvider列表。列表中的每一个 AuthenticationProvider将依次查询是否需要验证。 provider的验证结果只有两种:抛出异常或完全填充一种 Authentication对象的所有属性。ProviderManager中的一些源代码如下:

  public class ProviderManager implements AuthenticationManager, MessageSourceAware,InitializingBean {///维护AuthenticationProvider 列表private List providers = Collections.emptyList();private AuthenticationManager parent;//构造器,初始化 AuthenticationProvider public列表 ProviderManager(List providers) {this(providers, null);}public ProviderManager(List 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 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 实现类,然后使用Authenticationmanger 实现类 继续认证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是否支持相应的Authenticationbolean? 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。如果所有的 AuthenticationProvider没有成功验证 Authentication,然后抛出最后一个Provider抛出的Authenticationexception。(AuthenticationProvider,Springnger Security配置类配置)PS:

  当然,有时候我们会有很多不同 AuthenticationProvider,它们分别支持不同的 Authentication对象,所以当一个具体的对象 AuthenticationProvier传输进入 当ProviderManager内部时,就会在 在AuthenticationProvider列表中选择相应支持的provider 验证Authentication对象。不同的登录方式有不同的认证逻辑,即 如果用户名和密码登录,AuthenticationProvider将会有所不同。 Security 提供了一个 AuthenticationProvider的简单实现 DaoAuthenticationProvider,这也是最早的框架 provider,它用了一个 Userdetailservice查询用户名 GrantedAuthority,一般来说,我们需要实现Userdetailsservice接口和Springg 在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之后,请求认证成功后 ProviderManger将被删除 准确地说,Authentication中的认证信息通常被删除 密码信息,可以保证密码的安全。我跟踪了源代码。事实上,删除操作的步骤如下:

  public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {public void eraseCredentials() {super.eraseCredentials();//使密码为nullthis.credentials = null;}}public abstract class AbstractAuthenticationToken implements Authentication, CredentialsContainer {...public void eraseCredentials() {///擦除密码thiss.eraseSecret(this.getCredentials());this.eraseSecret(this.getPrincipal());this.eraseSecret(this.details);}

  private void eraseSecret(Object secret) {if (secret instanceof CredentialsContainer) {((CredentialsContainer)secret).eraseCredentials();}}}从源码可以看出,实际上是擦除密码操作。

  UserDetailsService 和 简单来说,Userdetailsuserdetailservice就是加载相应的Userdetails接口(通常来自数据库),而Userdetails包含更详细的用户信息,定义如下:

  public interface UserDetails extends Serializable { Collection 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();static {AUTHORITIES.add(new SimpleGrantedAuthority("ROLE_USER"));}@Overridepublic 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:pjmikePlease enter your password:123Authentication failed: Bad CredentialsPlease enter your username:pjmikePlease enter your password:pjmikeSuccessfully 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 认证的基本思想。解读一下:

  用户名和密码被封装 在UsernamePaswordauthentication的实例中(这类是 实现Authentication接口)

  该 Authentication传递给 AuthenticationManager身份验证

  认证成功后,Authenticationmanager将返回完全填充 Authentication实例包含权限信息、身份信息和细节信息,但通常会删除密码

  通过调用 SecurityContextHolder.getContext().setAuthentication(..)输入上面返回的填充信息 Authentication对象

  通过以上简单的例子,我们大致了解了Spring Security的基本思想,但要真正理清Spring Security的认证流程还不够。我们需要深入探索源代码。后续文章将对Spring进行更详细的分析 Security的认证过程。

  本文主要分析了Spring Security的一些核心组件参考了官方文件及其相关译本,对核心组件有了基本的了解,便于对Spring进行更详细的分析 Security的认证过程。

上一篇 java泛型类和泛型方法定义
下一篇 经典算法题每日演练——第二十二题 奇偶排序

文章素材均来源于网络,如有侵权,请联系管理员删除。