1.Spring Security 简介

  Java企业级开发生态丰富,无论你想做哪方面的功能,都有众多的框架和工具可供选择, 以至于SUN公司在早些年不得不制定了很多规范,这些规范在今天依然影响着我们的开发, 安全领域也是如此,然而,不同于其他领域,在Java企业级开发中,安全管理方面的框架非常少,一般来说,主要有三种方案:

  • Shiro
  • Spring Security
  • 开发者自己实现

  Shiro本身是一个老牌的安全管理框架,有着众多的优点,例如轻量、简单、易于集成、 可以在JavaSE环境中使用等。不过,在微服务时代,Shiro就显得力不从心了,在微服务面前, 它无法充分展示自己的优势。

  也有开发者选择自己实现安全管理,这一部分人不在少数,但是一个系统 的安全,不仅仅是登录和权限控制这么简单,我们还要考虑各种各样可能存在的网络攻击以及防御策略,从这个角度来说,开发者自己实现安全管理也并非是一件容易的事情,只有大公司才有足够的人力物力去支持这件事情。Spring Security作为Spring家族的一员,在和Spring家族的其他成员如Spring Boot、Spring Cloud等进行整合时,具有其他框架无可比拟的优势,同时对OAuth2有着良好的支持,再加 上 Spring Cloud 对 Spring Security 的不断加持(如推出 Spring Cloud Security),让 Spring Security 不知不觉中成为微服务项目的首选安全管理方案。

2.Spring Security 核心功能

  对于一个安全管理框架而言,无论是Shiro还是Spring Security,最核心的功能,无非就是如下两方面:

  • 认证
  • 授权

  通俗点说,认证就是身份验证(你是谁?),授权就是访问控制(你可以做什么?)

  2.1 认证

  Spring Security支持多种不同的认证方式,这些认证方式有的是Spring Security自己提供的认证功能,有的是第三方标准组织制订的。Spring Security集成的主流认证机制主要有如下几种:

  • 表单认证
  • OAuth2.0 认证
  • SAML2.0 认证
  • CAS 认证
  • RememberMe 自动认证
  • JAAS 认证
  • OpenlD 去中心化认证
  • Pre-Authentication Scenarios 认证口
  • X509认证
  • HTTP Basic 认证
  • HTTP Digest 认证

  作为一个开放的平台,Spring Security提供的认证机制不仅仅包括上面这些,我们还可以通过引入第三方依赖来支持更多的认证方式,同时,如果这些认证方式无法满足我们的需求, 我们也可以自定义认证逻辑,特别是当我们和一些“老破旧“的系统进行集成时,自定义认证逻辑就显得非常重要了。

  2.2 授权

  无论釆用了上面哪种认证方式,都不影响在Spring Security中使用授权功能。Spring Security支持基于URL的请求授权、支持方法访问授权、支持SpEL访问控制、支持域对象安全(ACL),同时也支持动态权限配置、支持RBAC权限模型等,总之,我们常见的权限管理需求,Spring Security基本上都是支持的。

 2.3 其他

  在认证和授权这两个核心功能之外,Spring Security还提供了很多安全管理的“周边功能”, 这也是一个非常重要的特色。

  大部分Java工程师都不是专业的Web安全工程师,自己开发的安全管理框架可能会存在 大大小小的安全漏洞,而Spring Security的强大之处在于,即使你不了解很多网络攻击,只要使用了 Spring Security,它会帮助我们自动防御很多网络攻击,例如CSRF攻击、会话固定攻击等,同时Spring Security还提供了 HTTP防火墙来拦截大量的非法请求。由此可见,研究Spring Securityr也是研究常见的网络攻击以及防御策略。

  对于大部分的Java项目而言,无论是从经济性还是安全性来考虑,使用Spring Security 无疑是最佳方案。

3. Spring Security 整体架构

3.1 认证和授权

  3.1.1 认证

  在 Spring Security 的架构设计中,认证(Authentication)和授权(Authorization)是分开的,无论使用什么样的认证方式,都不会影响授权,这是两个独立的存在,这种独立带来的好处之一,就是Spring Security可以非常方便地整合一些外部的认证方案。

  在Spring Security中,用户的认证信息主要由Authentication的实现类来保存, Authentication 接口定义如下:

public interface Authentication extends Principal, Serializable {
	Collection<? extends GrantedAuthority> getAuthorities();
	Object getCredentials();
	Object getDetails();
	Object getPrincipal();
	boolean isAuthenticated();
	void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}

这里接口中定义的方法如下:

  • getAuthorities方法:用来获取用户的权限。
  • getCredentials方法用来获取用户凭证,一般来说就是密码。
  • getDetails方法:用来获取用户携带的详细信息,可能是当前请求之类等。
  • getPrincipal方法:用来获取当前用户,例如是一个用户名或者一个用户对象。
  • isAuthenticated:当前用户是否认证成功。

  当用户使用用户名/密码登录或使用Remember-me登录时,都会对应一个不同的 Authentication 实例。Spring Security中的认证工作主要由AuthenticationManager接口来负责,下面来看一下该接口的定义:

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

  AuthenticationManager只有一个authenticate方法可以用来做认证,该方法有三个不同的返回值:

  • 返回Authentication,表示认证成功。
  • 抛出AuthenticationException异常,表示用户输入了无效的凭证。
  • 返回null,表示不能断定。

  AuthenticationManager 最主要的实现类是 ProviderManagerPioviderManager 管理了众多的 AuthenticationProvider 实例,AuthenticationProvider 有点类似于 AuthenticationManager,但是它多了一个supports方法用来判断是否支持给定的Authentication类型。

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

  由于Authentication拥有众多不同的实现类,这些不同的实现类又由不同的 AuthenticationProvider 来处理,所以 AuthenticationProvider 会有一个 supports 方法,用来判断当前的 AuthenticationProvider 是否支持对应的 Authentication。

  在一次完整的认证流程中,可能会同时存在多个AuthenticationProvider (例如,项目同时支持form表单登录和短信验证码登录),多个AuthenticationProvider统一由ProviderManager 来管理。同时,ProviderManager具有一个可选的parent,如果所有的AuthenticationProvider 都认证失败,那么就会调用parent进行认证。parent相当于一个备用认证方式,即各个 AuthenticationProvider都无法处理认证问题的时候,就由parent出场收拾残局。

  3.1.2 授权

  当完成认证后,接下来就是授权了。在Spring Security的授权体系中,有两个关键接口:

  • AccessDecisionManager
  • AccessDecisionVoter

  AccessDecisionVoter是一个投票器,投票器会检查用户是否具备应有的角色,进而投出赞成、反对或者弃权票;AccessDecisionManager则是一个决策器,来决定此次访问是否被允许。 AccessDecisionVoter 和 AccessDecisionManager 都有众多的实现类,在 AccessDecisionManager 中会挨个遍历AccessDecisionVoter,进而决定是否允许用户访问,因而AccessDecisionVoter 和 AccessDecisionManager 两者的关系类似于 AuthenticationProvider 和 ProviderManager 的关系。

  在Spring Security中,用户请求一个资源(通常是一个网络接口或者一个Java方法)所需要的角色会被封装成一个ConfigAttribute对象,在ConfigAttribute中只有一个getAttribute 方法,该方法返回一个String字符串,就是角色的名称。一般来说,角色名称都带有一个ROLE_ 前缀,投票器AccessDecisionVoter所做的事情,其实就是比较用户所具备的角色和请求某个资源所需的ConfigAttribute之间的关系。

3.2 Web安全

  在Spring Security中,认证、授权等功能都是基于过滤器来完成的。下表列出了 Spring Security中常见的过滤器,注意这里说的是否默认加载是指引入Spring Security依赖之后,开发者不做任何配置时,会自动加载的过滤器。

      

  

  开发者所见到的Spring Security提供的功能,都是通过这些过滤器来实现的,这些过滤器按照既定的优先级排列,最终形成一个过滤器链,开发者也可以自定义过滤器,并通过@Order 注解去调整自定义过滤器在过滤器链中的位置。

  需要注意的是,默认过滤器并不是直接放在Web项目的原生过滤器链中,而是通过一个FilterChainProxy 来统一管理。Spring Security 中的过滤器链通过 FilterChainProxy 嵌入到 Web 项目的原生过滤器链中,如下图所示。

  在Spring Security中,这样的过滤器链不仅仅只有一个,可能会有多个,如下图所示。 当存在多个过滤器链时,多个过滤器链之间要指定优先级,当请求到达后,会从 FilterChainProxy进行分发,先和哪个过滤器链匹配上,就用哪个过滤器链进行处理。当系统中存在多个不同的认证体系时,那么使用多个过滤器链就非常有效。

  

  FilterChainProxy 作为一个顶层管理者,将统一管理 Security Fliter FilterChainProxy 本身将通过Spring框架提供的DelegatingFilterProxy整合到原生过滤器链中,所以还可以做进一步的优化,如下图所示。

3.3 登录数据保存

  如果不使用Spring Security这一类的安全管理框架,大部分的开发者可能会将登录用户数据保存在Session中,事实上,Spring Security也是这么做的。但是,为了使用方便,Spring Security 在此基础上还做了一些改进,其中最主要的一个变化就是线程绑定,

  当用户登录成功后,Spring Security会将登录成功的用户信息保存到SecurityContextHolder 中。SecurityContextHolder 中的数据保存默认是通过ThreadLocal来实现的,使用ThreadLocal 创建的变量只能被当前线程访问,不能被其他线程访问和修改,也就是用户数据和请求线程绑定在一起。当登录请求处理完毕后,Spring Security会将SecurityContextHolder中的数据拿出来保存到Session中,同时将SecurityContextHolder中的数据清空。以后每当有请求到来时, Spring Security就会先从Session中取出用户登录数据,保存到SecurityContextHolder 中,方便在该请求的后续处理过程中使用,同时在请求结束时将SecurityContextHolder中的数据拿岀来保存到Session中,然后将SecurityContextHolder 中的数据清空。

  这一策略非常方便用户在Controller或者Service层获取当前登录用户数据,但是带来的另外一个问题就是,在子线程中想要获取用户登录数据就比较麻烦。Spring Security对此也提供了相应的解决方案,如果开发者使用@Async注解来开启异步任务的话,那么只需要添加如下配置,使用Spring Security提供的异步任务代理,就可以在异步任务中从SecurityContextHolder里边获取当前登录用户的信息:

@Configuration
public class ApplicationConfiguration extends AsyncConfigurerSupport {
    @Override
    public Executor getAsyncExecutor() {
        return new DelegatingSecurityContextExecutorService(Executors.newFixedThreadPool(5));
    }
}