Spring Security 架构简介

1、技术概述

1.1 Spring vs Spring Boot vs Spring Security

1.1.1 Spring Framework

Spring Framework 为开发 Java 应用程序提供了全面的基础架构支持。它包含了一些不错的功能,如 "依赖注入",以及一些现成的模块:html

  • Spring JDBC
  • Spring MVC
  • Spring Security
  • Spring AOP
  • Spring ORM

这些模块能够大大减小应用程序的开发时间。例如,在 Java Web 开发的早期,咱们须要编写大量样板代码以将记录插入数据源。可是,经过使用 Spring JDBC 模块的 JDBCTemplate,咱们能够仅经过少许配置将其简化为几行代码。java

阅读更多关于 Angular、TypeScript、Node.js/Java 、Spring 等技术文章,欢迎访问个人我的博客 —— 全栈修仙之路
1.1.2 Spring Boot

Spring Boot 是基于 Spring Framework,它为你的 Spring 应用程序提供了自动装配特性,它的设计目标是让你尽量快的上手应用程序的开发。如下是 Spring Boot 所拥有的一些特性:spring

  • 能够建立独立的 Spring 应用程序,而且基于 Maven 或 Gradle 插件,能够建立可执行的 JARs 和 WARs;
  • 内嵌 Tomcat 或 Jetty 等 Servlet 容器;
  • 提供自动配置的 "starter" 项目对象模型(POMS)以简化 Maven 配置;
  • 尽量自动配置 Spring 容器;
  • 提供一些常见的功能、如监控、WEB容器,健康,安全等功能;
  • 绝对没有代码生成,也不须要 XML 配置。
1.1.3 Spring Security

Spring Security 是一个可以为基于 Spring 的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组能够在 Spring 应用上下文中配置的 Bean,充分利用了 Spring IoC(Inversion of Control 控制反转),DI(Dependency Injection 依赖注入)和 AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减小了为企业系统安全控制编写大量重复代码的工做。数据库

Spring Security 拥有如下特性:编程

  • 对身份验证和受权的全面且可扩展的支持
  • 防护会话固定、点击劫持,跨站请求伪造等攻击
  • 支持 Servlet API 集成
  • 支持与 Spring Web MVC 集成

Spring、Spring Boot 和 Spring Security 三者的关系以下图所示:安全

spring-boot-security.jpg

1.2 Spring Security 集成

目前 Spring Security 5 支持与如下技术进行集成:架构

  • HTTP basic access authentication
  • LDAP system
  • OpenID identity providers
  • JAAS API
  • CAS Server
  • ESB Platform
  • ......
  • Your own authentication system

在进入 Spring Security 正题以前,咱们先来了解一下它的总体架构:框架

spring-security-arch.jpg

2、核心组件

2.1 SecurityContextHolder,SecurityContext 和 Authentication

最基本的对象是 SecurityContextHolder,它是咱们存储当前应用程序安全上下文的详细信息,其中包括当前使用应用程序的主体的详细信息。如当前操做的用户是谁,该用户是否已经被认证,他拥有哪些角色权限等。ide

默认状况下,SecurityContextHolder 使用 ThreadLocal 来存储这些详细信息,这意味着 Security Context 始终可用于同一执行线程中的方法,即便 Security Context 未做为这些方法的参数显式传递。spring-boot

获取当前用户的信息

由于身份信息与当前执行线程已绑定,因此可使用如下代码块在应用程序中获取当前已验证用户的用户名:

Object principal = SecurityContextHolder.getContext()
  .getAuthentication().getPrincipal();

if (principal instanceof UserDetails) {
  String username = ((UserDetails)principal).getUsername();
} else {
  String username = principal.toString();
}

调用 getContext() 返回的对象是 SecurityContext 接口的一个实例,对应 SecurityContext 接口定义以下:

// org/springframework/security/core/context/SecurityContext.java
public interface SecurityContext extends Serializable {
    Authentication getAuthentication();
    void setAuthentication(Authentication authentication);
}
Authentication

在 SecurityContext 接口中定义了 getAuthentication 和 setAuthentication 两个抽象方法,当调用 getAuthentication 方法后会返回一个 Authentication 类型的对象,这里的 Authentication 也是一个接口,它的定义以下:

// org/springframework/security/core/Authentication.java
public interface Authentication extends Principal, Serializable {
  // 权限信息列表,默认是GrantedAuthority接口的一些实现类,一般是表明权限信息的一系列字符串。
    Collection<? extends GrantedAuthority> getAuthorities();
  // 密码信息,用户输入的密码字符串,在认证事后一般会被移除,用于保障安全。
    Object getCredentials();
    Object getDetails();
  // 最重要的身份信息,大部分状况下返回的是UserDetails接口的实现类,也是框架中的经常使用接口之一。
    Object getPrincipal();
    boolean isAuthenticated();
    void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}

以上的 Authentication 接口是 spring-security-core jar 包中的接口,直接继承自 Principal 类,而 Principal 是位于 java.security 包中,由此可知 Authentication 是 spring security 中核心的接口。经过这个 Authentication 接口的实现类,咱们能够获得用户拥有的权限信息列表,密码,用户细节信息,用户身份信息,认证信息等。

2.2 小结

下面咱们来简单总结一下 SecurityContextHolder,SecurityContext 和 Authentication 这个三个对象之间的关系,SecurityContextHolder 用来保存 SecurityContext (安全上下文对象),经过调用 SecurityContext 对象中的方法,如 getAuthentication 方法,咱们能够方便地获取 Authentication 对象,利用该对象咱们能够进一步获取已认证用户的详细信息。

SecurityContextHolder,SecurityContext 和 Authentication 的详细定义以下所示:

security-context-holder.jpg

3、身份验证

3.1 Spring Security 中的身份验证是什么?

让咱们考虑一个每一个人都熟悉的标准身份验证方案:

  • 系统会提示用户使用用户名和密码登陆。
  • 系统验证用户名和密码是否正确。
  • 若验证经过则获取该用户的上下文信息(如权限列表)。
  • 为用户创建安全上下文。
  • 用户继续进行,可能执行某些操做,该操做可能受访问控制机制的保护,该访问控制机制根据当前安全上下文信息检查操做所需的权限。

前三项构成了身份验证进程,所以咱们将在 Spring Security 中查看这些内容。

  • 获取用户名和密码并将其组合到 UsernamePasswordAuthenticationToken 的实例中(咱们以前看到的Authentication 接口的实例)。
  • 令牌将传递给 AuthenticationManager 的实例以进行验证。
  • AuthenticationManager 在成功验证时返回彻底填充的 Authentication 实例。
  • SecurityContext 对象是经过调用 SecurityContextHolder.getContext().setAuthentication(…) 建立的,传入返回的身份验证 Authentication 对象。

3.2 Spring Security 身份验证流程示例

了解完上述的身份验证流程,咱们来看一个简单的示例:

AuthenticationManager 接口:

public interface AuthenticationManager {
  // 对传入的authentication对象进行认证
    Authentication authenticate(Authentication authentication)
            throws AuthenticationException;
}

SampleAuthenticationManager 类:

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");
   }
}

AuthenticationExample 类:

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 {
        // 使用用户输入的name和password建立request对象,这里的UsernamePasswordAuthenticationToken
        // 是前面提到的Authentication接口的实现类
        Authentication request = new UsernamePasswordAuthenticationToken(name, password);
        // 使用SampleAuthenticationManager实例,对request进行认证操做
        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());
  }
}

在以上代码中,咱们实现的 AuthenticationManager 将验证用户名和密码相同的任何用户。它为每一个用户分配一个角色。上面代码的验证过程是这样的:

Please enter your username:
semlinker
Please enter your password:
12345
Authentication failed: Bad Credentials
Please enter your username:
semlinker
Please enter your password:
semlinker
Successfully authenticated. Security context contains: org.springframework.security.authentication.UsernamePasswordAuthenticationToken@441d0230: Principal: semlinker; Credentials: [PROTECTED]; Authenticated: true; Details: null; Granted Authorities: ROLE_USER

4、 核心服务

4.1 AuthenticationManager,ProviderManager 和 AuthenticationProvider

AuthenticationManager(接口)是认证相关的核心接口,也是发起认证的出发点,由于在实际需求中,咱们可能会容许用户使用用户名 + 密码登陆,同时容许用户使用邮箱 + 密码,手机号码 + 密码登陆,甚至,可能容许用户使用指纹登陆,因此要求认证系统要支持多种认证方式。

Spring Security 中 AuthenticationManager 接口的默认实现是 ProviderManager,但它自己并不直接处理身份验证请求,它会委托给已配置的 AuthenticationProvider 列表,每一个列表依次被查询以查看它是否能够执行身份验证。每一个 Provider 验证程序将抛出异常或返回一个彻底填充的 Authentication 对象。

也就是说,Spring Security 中核心的认证入口始终只有一个:AuthenticationManager,不一样的认证方式:用户名 + 密码(UsernamePasswordAuthenticationToken),邮箱 + 密码,手机号码 + 密码登陆则对应了三个 AuthenticationProvider。

下面咱们来看一下 ProviderManager 的核心源码:

// spring-security-core-5.2.0.RELEASE-sources.jar
// org/springframework/security/authentication/ProviderManager.java
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;
        AuthenticationException parentException = null;
        Authentication result = null;
        Authentication parentResult = null;
        boolean debug = logger.isDebugEnabled();

    // 遍历providers列表,判断是否支持当前authentication对象的认证方式
        for (AuthenticationProvider provider : getProviders()) {
            if (!provider.supports(toTest)) {
                continue;
            }

            try {
        // 执行provider的认证方式并获取返回结果
                result = provider.authenticate(authentication);

                if (result != null) {
                    copyDetails(authentication, result);
                    break;
                }
            }
            catch (AccountStatusException | InternalAuthenticationServiceException e) {
                prepareException(e, authentication);
                throw e;
            } catch (AuthenticationException e) {
                lastException = e;
            }
        }

    // 若当前ProviderManager没法完成认证操做,且其包含父级认证器,则容许转交给父级认证器尝试进行认证
        if (result == null && parent != null) {
            try {
                result = parentResult = parent.authenticate(authentication);
            }
            catch (ProviderNotFoundException e) {
            }
            catch (AuthenticationException e) {
                lastException = parentException = e;
            }
        }

    
        if (result != null) {
            if (eraseCredentialsAfterAuthentication
                    && (result instanceof CredentialsContainer)) {
        // 完成认证,从authentication对象中移除私密数据
                ((CredentialsContainer) result).eraseCredentials();
            }
      
      // 若父级AuthenticationManager认证成功,则派发AuthenticationSuccessEvent事件
            if (parentResult == null) {
                eventPublisher.publishAuthenticationSuccess(result);
            }
            return result;
        }
    
    // 未认证成功,抛出ProviderNotFoundException异常
        if (lastException == null) {
            lastException = new ProviderNotFoundException(messages.getMessage(
                    "ProviderManager.providerNotFound",
                    new Object[] { toTest.getName() },
                    "No AuthenticationProvider found for {0}"));
        }
    
        if (parentException == null) {
            prepareException(lastException, authentication);
        }

        throw lastException;
    }
}

在 ProviderManager 进行认证的过程当中,会遍历 providers 列表,判断是否支持当前 authentication 对象的认证方式,若支持该认证方式时,就会调用所匹配 provider(AuthenticationProvider)对象的 authenticate 方法进行认证操做。若认证失败则返回 null,下一个 AuthenticationProvider 会继续尝试认证,若是全部认证器都没法认证成功,则 ProviderManager 会抛出一个 ProviderNotFoundException 异常。

4.2 DaoAuthenticationProvider

在 Spring Security 中较经常使用的 AuthenticationProvider 是 DaoAuthenticationProvider,这也是 Spring Security 最先支持的 AuthenticationProvider 之一。顾名思义,Dao 正是数据访问层的缩写,也暗示了这个身份认证器的实现思路。DaoAuthenticationProvider 类的内部结构以下:

dao-authentication-provider.jpg

在实际项目中,最多见的认证方式是使用用户名和密码。用户在登陆表单中提交了用户名和密码,而对于已注册的用户,在数据库中已保存了正确的用户名和密码,认证即是负责比对同一个用户名,提交的密码和数据库中所保存的密码是否相同即是了。

在 Spring Security 中,对于使用用户名和密码进行认证的场景,用户在登陆表单中提交的用户名和密码,被封装成了 UsernamePasswordAuthenticationToken,而根据用户名加载用户的任务则是交给了 UserDetailsService,在 DaoAuthenticationProvider 中,对应的方法就是 retrieveUser,虽然有两个参数,可是 retrieveUser 只有第一个参数起主要做用,返回一个 UserDetails。retrieveUser 方法的具体实现以下:

// spring-security-core-5.2.0.RELEASE-sources.jar
// org/springframework/security/authentication/dao/DaoAuthenticationProvider.java
protected final UserDetails retrieveUser(String username,
            UsernamePasswordAuthenticationToken authentication)
            throws AuthenticationException {
        prepareTimingAttackProtection();
        try {
            UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
            if (loadedUser == null) {
                throw new InternalAuthenticationServiceException(
                        "UserDetailsService returned null, which is an interface contract violation");
            }
            return loadedUser;
        }
        catch (UsernameNotFoundException ex) {
            mitigateAgainstTimingAttack(authentication);
            throw ex;
        }
        catch (InternalAuthenticationServiceException ex) {
            throw ex;
        }
        catch (Exception ex) {
            throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
        }
}

在 DaoAuthenticationProvider 类的 retrieveUser 方法中,会以传入的 username 做为参数,调用 UserDetailsService 对象的 loadUserByUsername 方法加载用户。

4.3 UserDetails 与 UserDetailsService

4.3.1 UserDetails 接口

在 DaoAuthenticationProvider 类中 retrieveUser 方法签名是这样的:

protected final UserDetails retrieveUser(String username,
    UsernamePasswordAuthenticationToken authentication) 
  throws AuthenticationException {
}

该方法返回 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 接口,它与 UserDetails 接口的定义以下:

user-details-vs-authentication.png

虽然 Authentication 与 UserDetails 很相似,但它们之间是有区别的。Authentication 的 getCredentials() 与 UserDetails 中的 getPassword() 须要被区分对待,前者是用户提交的密码凭证,后者是用户正确的密码,认证器其实就是对这二者进行比对。

此外 Authentication 中的 getAuthorities() 实际是由 UserDetails 的 getAuthorities() 传递而造成的。还记得 Authentication 接口中的 getUserDetails() 方法吗?其中的 UserDetails 用户详细信息就是通过了 provider (AuthenticationProvider) 认证以后被填充的。

4.3.2 UserDetailsService 接口

大多数身份验证提供程序都利用了 UserDetailsUserDetailsService 接口。UserDetailsService 接口的定义以下:

public interface UserDetailsService {
    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

在 UserDetailsService 接口中,只有一个 loadUserByUsername 方法,用于经过 username 来加载匹配的用户。当找不到 username 对应用户时,会抛出 UsernameNotFoundException 异常。UserDetailsService 和 AuthenticationProvider 二者的职责经常被人们搞混,记住一点便可,UserDetailsService 只负责从特定的地方(一般是数据库)加载用户信息,仅此而已。

UserDetailsService 常见的实现类有 JdbcDaoImpl,InMemoryUserDetailsManager,前者从数据库加载用户,后者从内存中加载用户,固然你也能够本身实现 UserDetailsService。

4.4 Spring Security Architecture

前面咱们已经介绍了 Spring Security 的核心组件(SecurityContextHolder,SecurityContext 和 Authentication)和核心服务(AuthenticationManager,ProviderManager 和 AuthenticationProvider),最后咱们再来回顾一下 Spring Security 总体架构:

spring-security-arch.jpg

5、参考资源

full-stack-logo

相关文章
相关标签/搜索