走近科学之探秘 Spring Security 核心组件

1. SecurityContextHolder 类

走近科学之探秘 Spring Security 核心组件
90843905be494acaa86be634d130f40b.png

SecurityContextHolder 顾名思义,他是一个 holder,用于持有的是安全上下文(security context)的信息。SecurityContextHolder 记录如下信息:当前操作的用户是谁,该用户是否已经被认证,他拥有哪些角色或权限等等。

Tomcat 建立会话的流程

在典型的 web 应用程序中,用户登录一次,然后由其会话 ID 标识。服务器缓存持续时间会话的主体信息。有人可能对 Tomcat 建立会话的流程还不熟悉,这里稍微整理一下。

当客户一般是认证成功后,在调用 request.getSession() 方法后,Tomcat 会创建一个 HttpSesion 对象,存入一个 ConcurrentHashMap,Key 是 SessionId,Value 就是 HttpSession。然后请求完成后,在返回的报文中添加 Set-Cookie:JSESSIONID=xxx,然后客户端浏览器会保存这个 Cookie。当浏览器再次访问这个服务器的时候,都会带上这个 Cookie。Tomcat 接收到这个请求后,根据 JSESSIONID 把对应的 HttpSession 对象取出来,放入 HttpSerlvetRequest 对象里面。

  1. 这些处理都在 Spring Security 的拦截链之前完成。
  2. Tomcat 中 HttpSesion 的默认过期时间为 30 分钟。
  3. 如无特殊处理,Cookie JSESSIONID 会在浏览器关闭的时候清除。
  4. HttpSession 会一直存在服务端,实际上是存在运行内存中。除非 Session 过期 或者 Tomcat 奔溃 或者 服务器奔溃,否则会话信息不会消失。

Spring Security 会话存储流程

走近科学之探秘 Spring Security 核心组件
L3Byb3h5L2.png

SecurityContext 对象实际存储于 Tomcat HttpSession 中的一个 key 中,名为 “SPRING_SECURITY_CONTEXT”。

在 Spring Security 中,在请求之间存储 SecurityContext 的责任落在 SecurityContextPersistenceFilter 上,默认情况下,该上下文将上下文存储为 HTTP 请求之间的HttpSession 属性。SecurityContextPersistenceFilter 是 Security 的拦截器,而且是拦截链中的第一个拦截器,请求来临时它会从 HttpSession 中把 SecurityContext 取出来,然后放入 SecurityContextHolder。在所有拦截器都处理完成后,再把 SecurityContext 存入 HttpSession,并清除 SecurityContextHolder 内的引用。

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException 
{
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;

        if (request.getAttribute(FILTER_APPLIED) != null) {
            // ensure that filter is only applied once per request
            chain.doFilter(request, response);
            return;
        }

        final boolean debug = logger.isDebugEnabled();

        request.setAttribute(FILTER_APPLIED, Boolean.TRUE);

        if (forceEagerSessionCreation) {
            HttpSession session = request.getSession();

            if (debug && session.isNew()) {
                logger.debug("Eagerly created session: " + session.getId());
            }
        }

        HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request,
                response);
        // 利用HttpSecurityContextRepository从HttpSesion中获取SecurityContext对象
        // 如果没有HttpSession,即浏览器第一次访问服务器,还没有产生会话。
        // 它会创建一个空的SecurityContext对象
        SecurityContext contextBeforeChainExecution = repo.loadContext(holder);

        try {
            // 把SecurityContext放入到SecurityContextHolder中
            SecurityContextHolder.setContext(contextBeforeChainExecution);
            // 执行拦截链,这个链会逐层向下执行
            chain.doFilter(holder.getRequest(), holder.getResponse());

        }
        finally { 
            // 当拦截器都执行完的时候把当前线程对应的SecurityContext从SecurityContextHolder中取出来
            SecurityContext contextAfterChainExecution = SecurityContextHolder
                    .getContext();
            // Crucial removal of SecurityContextHolder contents - do this before anything
            // else.
            SecurityContextHolder.clearContext();
            // 利用HttpSecurityContextRepository把SecurityContext写入HttpSession
            repo.saveContext(contextAfterChainExecution, holder.getRequest(),
                    holder.getResponse());
            request.removeAttribute(FILTER_APPLIED);

            if (debug) {
                logger.debug("SecurityContextHolder now cleared, as request processing completed");
            }
        }
    }

SecurityContextHolder 可以用来设置和获取 SecurityContext。它主要是给框架内部使用的,可以利用它获取当前用户的 SecurityContext 进行请求检查,和访问控制等。

SecurityContextHolder 存储策略

  1. 存储在线程中。
  2. 存储在线程中,但子线程可以获取到父线程中的 SecurityContext。
  3. 在所有线程中都相同。

SecurityContextHolder 默认使用 ThreadLocal 策略来存储认证信息。看到 ThreadLocal 也就意味着,这是一种与线程绑定的策略。在 web 环境下,Spring Security 在用户登录时自动绑定认证信息到当前线程,在用户退出时,自动清除当前线程的认证信息。

SecurityContextHolder 采用策略模式,根据 strategyName 字段创建不同的 SecurityContextHolderStrategy 对象。

public class SecurityContextHolder {
 // 三种存储策略
 public static final String MODE_THREADLOCAL = "MODE_THREADLOCAL";
 public static final String MODE_INHERITABLETHREADLOCAL = "MODE_INHERITABLETHREADLOCAL";
 public static final String MODE_GLOBAL = "MODE_GLOBAL";
 
 public static final String SYSTEM_PROPERTY = "spring.security.strategy";
 
 // System.getProperty() 从JVM中获取配置的属性SYSTEM_PROPERTY
 // 获取不到 strategyName = null
 private static String strategyName = System.getProperty(SYSTEM_PROPERTY);
 private static SecurityContextHolderStrategy strategy;
 private static int initializeCount = 0;
 
 // 随着类的加载而加载
 static {
  initialize();
 }

 ...
 
 // 初始化
 private static void initialize() {
  if (!StringUtils.hasText(strategyName)) {
   // 设置默认策略
   strategyName = MODE_THREADLOCAL;
  }
  
  // 根据strategyName字段创建对应的SecurityContextHolderStrategy对象
  if (strategyName.equals(MODE_THREADLOCAL)) {
   strategy = new ThreadLocalSecurityContextHolderStrategy();
  }
  else if (strategyName.equals(MODE_INHERITABLETHREADLOCAL)) {
   strategy = new InheritableThreadLocalSecurityContextHolderStrategy();
  }
  else if (strategyName.equals(MODE_GLOBAL)) {
   strategy = new GlobalSecurityContextHolderStrategy();
  }
  else {
   // 自定义策略
   ...
  }

  initializeCount++;
 }

 // 
 public static void setContext(SecurityContext context) {
  strategy.setContext(context);
 }

 // 可以设置新的存储策略
 public static void setStrategyName(String strategyName) {
  SecurityContextHolder.strategyName = strategyName;
  // 修改strategyName后需要重新执行initialize创建新的SecurityContextHolderStrategy对象
  initialize();
 }
 ...
}

2. SecurityContext 接口

SecurityContext 安全上下文,用户通过 Spring Security 的校验之后,验证信息存储在 SecurityContext 中,SecurityContext 接口只定义了两个方法,实际上其主要作用就是设置、获取 Authentication 对象。


public interface SecurityContext extends Serializable {
 Authentication getAuthentication();
 void setAuthentication(Authentication authentication);
}

3. Authentication 接口

Authentication 直译过来是“认证”的意思,在 Spring Security 中 Authentication 用来表示当前用户是谁,一般来讲你可以理解为 authentication 就是一组用户名密码信息。

Authentication 内容

  • principal:用于标识用户当通过 username 和 password 认证用户时,principal 通常是一个 UserDetails 的实现类对象。
  • credentials:通常是密码,在很多场景,如果用户已经被认证,那么此项将被清除以防止密码泄露。
  • authorities:用户具有的权限或角色。
public interface Authentication extends PrincipalSerializable {

 // 权限信息列表,默认是GrantedAuthority接口的一些实现类,通常是代表权限信息的一系列字符串。
 Collection<? extends GrantedAuthority> getAuthorities();
 
 // 密码信息,用户输入的密码字符串,在认证过后通常会被移除,用于保障安全
 Object getCredentials();
 
 // 细节信息,web应用中的实现接口通常为 WebAuthenticationDetails,
        // 它记录了访问者的ip地址和sessionId的值。
 Object getDetails();
 
 // 最重要的身份信息,大部分情况下返回的是UserDetails接口的实现类,也是框架中的常用接口之一。
 Object getPrincipal();
 
 boolean isAuthenticated();
 
 void setAuthenticated(boolean var1) throws IllegalArgumentException;
}

4. GrantedAuthority 接口

GrantedAuthority 是在 Authentication 的接口中使用集合存储权限。

Collection<? extends GrantedAuthority> getAuthorities();

可以看到权限集合存放的元素是 GrantedAuthority 的实现类,也可以使用 String。

该接口表示了当前用户所拥有的权限(或者角色)信息。这些信息有授权负责对象 AccessDecisionManager 来使用,并决定最终用户是否可以访问某资源。

5. UserDetails 接口

这个接口规范了用户详细信息所拥有的字段,譬如用户名、密码、账号是否过期、是否锁定等。在 Spring Security 中,获取当前登录的用户的信息,一般情况是需要在这个接口上面进行扩展,用来对接自己系统的用户。

public interface UserDetails extends Serializable {
 Collection<? extends GrantedAuthority> getAuthorities();
 String getPassword();
 String getUsername();
 // 用户账户是否过去,过期的用户不能被认证
 boolean isAccountNonExpired();
  // 用户是否被lock,lock的用户不能被认证
 boolean isAccountNonLocked();
  // 用户的credentials (password)是否过期,国企的不能认证成功
 boolean isCredentialsNonExpired();
  // 用户是enabled或者disabled,diabled的用户不能被认证
 boolean isEnabled();
}

6. UserDetailsService 接口

这个接口非常重要,一般情况我们都是通过扩展这个接口来显示获取我们的用户信息,用户登录时传递的用户名和密码也是通过这里这查找出来的用户名和密码进行校验。

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

但是真正的校验不在这里,而是由 AuthenticationManager 以及 AuthenticationProvider 负责的,需要强调的是,如果用户不存在,不应返回 NULL,而要抛出异常 UsernameNotFoundException。

7. AuthenticationManager 接口

AuthenticationManager 是认证相关的核心接口,也是发起认证的出发点,因为在实际需求中,我们可能会允许用户使用用户名+密码登录,同时允许用户使用邮箱+密码,手机号码+密码登录,甚至,可能允许用户使用指纹登录。

public interface AuthenticationManager {
 Authentication authenticate(Authentication authentication) throws AuthenticationException;
}

所以说 AuthenticationManager 一般不直接认证,AuthenticationManager 接口的常用实现类 ProviderManager 内部会维护一个 List<AuthenticationProvider> 列表,存放多种认证方式,实际上这是委派器模式的应用(Delegate)。

核心的认证入口始终只有一个:AuthenticationManager,不同的认证方式有:用户名+密码,邮箱+密码,手机号码+密码登录,分别对应了三个 AuthenticationProvider。

走近科学之探秘 Spring Security 核心组件
d4896ab8d74540c98b3ee97c0a36adf9.png

8. AuthenticationProvider 接口

AuthenticationProvider 接口最常用的一个实现便是 DaoAuthenticationProvider。

public interface AuthenticationProvider {
 Authentication authenticate(Authentication authentication) throws AuthenticationException;
 boolean supports(Class<?> authentication);
}

顾名思义,Dao 正是数据访问层的缩写,也暗示了这个身份认证器的实现思路。主要作用:它获取用户提交的用户名和密码,比对其正确性,如果正确,返回一个数据库中的用户信息(假设用户信息被保存在数据库中)。

9. AuthenticationProvider 和 UserDetailsService 关系

  1. UserDetails 接口代表了最详细的用户信息,这个接口包含了一些必要的用户信息字段,我们一般都需要对它进行必要的扩展。它和 Authentication 接口很类似,比如它们都拥有 username,authorities。
  2. Authentication 的 getCredentials() 与 UserDetails 中的 getPassword() 需要被区分对待,前者是用户提交的密码凭证,后者是用户正确的密码,认证器其实就是对这两者的比对。
  3. Authentication 中的 getAuthorities() 实际是由 UserDetails 的 getAuthorities() 传递而形成的。还记得 Authentication 接口中的 getUserDetails() 方法吗?其中的 UserDetails 用户详细信息便是经过了 AuthenticationProvider 之后被填充的。
  4. UserDetailsService 和 AuthenticationProvider 两者的职责常常被人们搞混,UserDetailsService 只负责从特定的地方加载用户信息,可以是数据库、Redis缓存、接口等

10. Spring Security是如何完成身份认证的?

  1. 用户名和密码被过滤器获取到,封装成 Authentication,通常情况下是 UsernamePasswordAuthenticationToken 实现类。
  2. AuthenticationManager 身份管理器负责验证这个 Authentication。
  3. 认证成功后,AuthenticationManager 身份管理器返回一个被填充满了信息的(包括上面提到的权限信息,身份信息,细节信息,但密码通常会被移除)Authentication 实例。
  4. SecurityContextHolder 安全上下文容器将第 3 步填充了信息的 Authentication,通过 SecurityContextHolder.getContext().setAuthentication() 方法,设置到其中。


原文始发于微信公众号(白菜说技术):走近科学之探秘 Spring Security 核心组件

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/39908.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!