SpringSecuirty 核心设计分析

本贴最后更新于 2703 天前,其中的信息可能已经时过境迁

Spring Security核心设计

本文主要介绍Spring Security一个较为高层次结构设计,不涉及具体的配置及使用。 具体的配置及使用可参考官方文档。

基础概念

所谓安全

我们的安全存在多层次,多个多维度,在软件的维度可以简单划分为几个层次的安全:

  • 网络传输层
  • 操作系统层
  • 应用层

在管理层方面的安全更是形形式式,参考我们的日常的各种员工信息安全要求就可以知道

Spring Security其仅仅为应用层安全定制一些相关领域的解决方案。

身份认证 与 授权校验

认证 和 授权 在应用程序安全里是两个基本的抽象, 在Shiro,Spring Secuirty等框架里可以看见。

身份认证

认证指代通过某些手段,确定当事人的身份。

如,在WEB里 客户A在表单里填入客户的 用户名 及 密码, 发送到后台,后台通过用户名找到后台存储的密码信息,然后检查密码是否匹配,若匹配,则认为认证通过:正在操作浏览器的那个人正是我们的合法客户A

当然,以上这么一个校验过程仅仅是认证的其中一环,在一个健壮可靠的WEB表单认证过程中还会有很多的判断,如:

  • 若只允许在HTTPS协议下访问WEB,则使用HTTP的请求认证不通过
  • 若加入了跨站请求位置防御,则没有相关元信息的请求认证不通过
  • 若同一个用于只允许存在一个SESSION,那么试图在不同浏览器创建第二个SESSION的认证不通过
  • ......

以上种种的校验逻辑,都是认证的一环。因此实际上,一个健壮可靠的认证并不是我们简简单单写一个FILTER就可以的,认证包括很多方面的安全考虑,我们并不可能全都考虑到。

即使我们我们有充分的安全认识,避免重复造轮子也是提升效率的重要方面——Spring提供了很多集成的认证机制,并为其提供了开箱即用的实现,如:

  • 账号密码认证(之前提到的)
  • LDAP认证
  • X.509证书认证
  • BASIC认证
  • .....

授权校验

完成认证后,我们知道了访问者的身份,那么我们就可以确定其有什么权限,结合需要访问的资源的访问权限配置元信息,那么我们就可以知道该访问者有没有权限访问对应的资源。

判断一个确定了身份的访问者有没有权限访问某个资源的过程我们称之为 授权校验。

如,我们假定一个WEB方法

GET /systeminfo/cpu-status

是我们需要进行授权校验的资源,那么我们可以从配置中取得相关配置

hasRole("/systeminfo/**",ADMIN)

其表征符合ANT表达式 /systeminfo/** 的路径的访问者都必须有ADMIN权限

结合我们之前已经完成的认证,确定了用户,我们只要判断该用户有没有 ADMIN权限,即可判断该用户有没有访问该资源的权限。

在Spring Secuirty里面对于授权的过程中有三个核心的抽象:

  • 访问的资源
  • 访问的资源的权限控制元信息
  • 当前的认证用户

理论上包含了上述三个信息,我们就能正确的判断出一个资源能否被当前认证用户访问。

对于访问的资源,Spring Secuirt有这两个具体的实现:

  • WEB URL
  • JAVA METHOD

WEB URL我们在刚刚就已经给予说明了,JAVA METHOD顾名思义就是把JAVA方法视作资源,SpringSecurity对其进行授权的校验保护。

对应的JAVA METHOD对应的访问控制元信息肯定会与WEB URL的访问控制元信息有所不同,其具体的配置形式我们可以想象一下使用Spring切面相关配置中匹配方法的一些设置。

代码分析

认证

高层接口——AuthenticationManager

先看一下AuthenticationManager认证过程的伪代码

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

<span class="hljs-keyword" style="box-sizing: border-box; color: #333333; font-weight: bold;">while</span>(<span class="hljs-keyword" style="box-sizing: border-box; color: #333333; font-weight: bold;">true</span>) {
System.<span class="hljs-keyword" style="box-sizing: border-box; color: #333333; font-weight: bold;">out</span>.println(<span class="hljs-string" style="box-sizing: border-box; color: #dd1144;">"Please enter your username:"</span>);
String name = <span class="hljs-keyword" style="box-sizing: border-box; color: #333333; font-weight: bold;">in</span>.readLine();
System.<span class="hljs-keyword" style="box-sizing: border-box; color: #333333; font-weight: bold;">out</span>.println(<span class="hljs-string" style="box-sizing: border-box; color: #dd1144;">"Please enter your password:"</span>);
String password = <span class="hljs-keyword" style="box-sizing: border-box; color: #333333; font-weight: bold;">in</span>.readLine();
<span class="hljs-keyword" style="box-sizing: border-box; color: #333333; font-weight: bold;">try</span> {
	Authentication request = <span class="hljs-keyword" style="box-sizing: border-box; color: #333333; font-weight: bold;">new</span> UsernamePasswordAuthenticationToken(name, password);
	Authentication result = am.authenticate(request);
	SecurityContextHolder.getContext().setAuthentication(result);
	<span class="hljs-keyword" style="box-sizing: border-box; color: #333333; font-weight: bold;">break</span>;
} <span class="hljs-keyword" style="box-sizing: border-box; color: #333333; font-weight: bold;">catch</span>(AuthenticationException e) {
	System.<span class="hljs-keyword" style="box-sizing: border-box; color: #333333; font-weight: bold;">out</span>.println(<span class="hljs-string" style="box-sizing: border-box; color: #dd1144;">"Authentication failed: "</span> + e.getMessage());
}
}
System.<span class="hljs-keyword" style="box-sizing: border-box; color: #333333; font-weight: bold;">out</span>.println(<span class="hljs-string" style="box-sizing: border-box; color: #dd1144;">"Successfully authenticated. Security context contains: "</span> +
		SecurityContextHolder.getContext().getAuthentication());

}
}

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

AuthenticationManager是Spring Secuirty提供的一个高层抽象接口,其只有一个方法

Authentication authenticate(Authentication auth) throws AuthenticationException

根据其方法声明,我们可以看出,其用于认证,若认证失败,则会抛出AuthenticationException

我们再看一下Authentication的JAVA-DOC,我们可以知道这个Authentication有以下主要用途:

  • 保存用于认证的信息(getCredentials(),如账号密码,但默认情况下,认证成功后会被删除)
  • 标志当前请求是否已被认证(isAuthenticated())
  • 若已被认证,标志当前的访问者是谁,有什么权限(getPrincipal(),getAuthorities();)

若认证通过将会调用

SecurityContextHolder.getContext().setAuthentication(result);

SecurityContextHolder里面包含了一个ThreadLocal字段,其保存着SecurityContext对象。SpringSecurity通过这种形式,传递身份认证的相关信息。因此,无论在代码的什么地方,我们都能取到SpringSecuirty的安全上下文。

这里调用了setAuthetication(result),就是说把Authentication对象放到SecurityContext里。在SpringSecuirty的设计里,只要这么做了,就相当于认证成功了,后续相关的授权操作都会依赖于这个Authentiaction

所以我们在自行实现认证时,甚至于可以跳过实现AuthenticationManager,直接创建Authentication并放置于SecurityContext中。

上面的伪代码写main方法里,但实际上,这段代码只要迁移到我们特定的场景就是我们SpringSecuirty的实际认证逻辑。

如在WEB场景中,这段代码将加到WEB Filter中,以完成我们的认证。

当然,我们之前就提过,认证还有很多各方面的内容。如之前讲的HTTPS,跨站请求伪造等等。

具体实现——ProviderManager

ProviderManager是SpringSecurity唯一的一个原生AuthentiacationManager实现。

其主要的逻辑为依次调用ProviderManager里的AuthenticationProvider,直到其中一个AuthenticationProvider能够成功完成认证(某个Provider不能完成认证则轮到下一个Provider认证,但若某个Provider认为认证失败,则整个流程认证失败)

也就是说使用ProviderManager时,我们可以糅合多个认证机制,只要其中一个认证机制通过即可。 因此我们在实现自己的认证机制的时候,优先实现一个AuthenticationProvider整合到ProviderManager中。如果ProviderMangaer不能达到我们的逻辑要求时,才实行实现一个新的AuthenticationManager

AuthenticationProvier实例——DaoAuthentiacitonProvider

DaoAuthentiacitonProvider其实现了根据用户名密码认证的机制。 如我们的表单登录,使用用户和密码验证,使用的就是这个DaoAuthenticationProvider

在DaoAuthenticationProvider里需要注入一个UserDetailService接口,这个接口只需要完成一个方法

UserDetails loadUserByUserName(String userName) throws UserNameNotFoundException

这个接口是我们的业务程序的用户与SpringSecuirty的用户连接的桥梁,大多数情况下,需要我们自行实现本接口,以实现业务的用户到SpringSecurity用户的信息的装换。

授权校验

一个授权校验的逻辑类似如下

1. 检查用户有没有权限访问资源
2. 访问资源
3. 调整访问资源后获得的返回值

为了实现上面的3个步骤,SpringSecuirty对授权的几个模块进行了如下图的抽象

image

SecurityMetadataSource

本接口的一个关键方法为

Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArguementException

其传入参数为需要访问的资源。如一个Method对象、一个FilterInvocation对象。 根据传入的对象,本接口需要获取出与其相关的访问权限配置信息Collection<ConfigAttribute>

AccessDecisionManager

本接口的一个关键方法为

void decide(Authentication authentication, Object object,
		Collection<ConfigAttribute> configAttributes) throws AccessDeniedException,
		InsufficientAuthenticationException;

其传入 authentication(用户信息),访问的资源信息,及相关的授权访问元数据,根据这些信息,本接口的实现类将决定是否允许用户分文对应的资源,若允许则正常执行完成,若不通过则抛出授权异常。

具体实现类

本接口类有一个默认的抽象实现类AbstractAccessDecisionManager,其下面有3个具体实现类:

  • AffirmativeBased
  • UnanimousBased
  • ConsensusBased

这三个实现类都包含多个更细的授权控制逻辑单元——Voter。 而这三个的区别在于

  • AffermativeBased只需一个Voter通过,那么授权通过
  • UnanimousBased需要全部Voter通过,那么授权才通过
  • ConsensusBased需要过半数通过,那么授权才通过

正常情况下,我们需要实现自定义的授权逻辑的话,我们实现一个自定义的Voter,然后选择上述三种的其中一种策略即可。只有上述三种策略都不满足我们的授权认证逻辑时,我们才考虑自行实现AccessDecisionManager

AfterInvocationManager

本接口的一个关键方法为

Object decide(Authentication authentication, Object object,
		Collection<ConfigAttribute> attributes, Object returnedObject)
		throws AccessDeniedException;

其传入授权校验的所有信息以及访问资源后获得的返回值,然后根据具体情况对返回值进行适当改造后,作为最终的返回值。

具体实现类——AfterInvocationProviderMangaer

与之前的设计类似,其也包含多个更细小的改造逻辑单元AfterInvocationProvider。我们扩展此处的逻辑时优先实现AfterInvocationProvider

RunAsManager

该类的作用主要作用于后续调用需要切换调用身份(Authentication)才能进行的情况。例如本业务系统要调用另外一个业务系统,而本业务系统的用户与其他业务系统的用户结构不一致的情况

AbstractSecurityInterceptor

这个抽象类主要作用是整合了上面的这些组件,使其串联起来,关键的方法有:

protected InterceptorStatusToken beforeInvocation(Object object);
protected Object afterInvocation(InterceptorStatusToken token, Object returnedObject);

beforeInvocation里调用了SecurityMetadataSource及AccessDecisionManager的方法以完成了授权认证操作 afterInvocation里调用了AfterInvocationManager及RunAsManager的方法以完成了返回结果调整及用户授权调整的操作

具体实现类

该类的具体实现类如上图所示有三个,我们以FilterSecurityInterceptor为例。

FilterSecurityInterceptor实现的另外一个接口是Filter(severlet filter),SpringSecurity的各种认证Filter执行完后,最后执行的就是这一个FilterSecuirtyInterceptor的Filter方法。

该Filter方法的主要逻辑就是

调用beforeInvocation
实际执行资源访问
调用AfterInvocation

这样,我们就把整个认证和授权的逻辑结合起来了。

至此,Spring Security的主要骨架接口及实现已经介绍完毕。

  • 安全

    安全永远都不是一个小问题。

    200 引用 • 816 回帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...