您的位置:首页 > 编程语言 > Java开发

Spring Security 文档翻译 - 架构与实现之一技术一览

2016-07-22 10:41 656 查看

架构与实现

你已经对安装和运行基于命名空间的配置的程序有些熟悉了,现在你希望知道更多关于这个框架是如何运作的知识。和大多数软件一样,Spring Security有一些核心的接口,类和概念上的抽象,这些被广泛使用在这个框架中。在这部分内容里,我们将对这些核心内容一探究竟并且看看在Spring Security中他们是如何一起工作来支持授权与访问控制功能的。

1.技术一览

1.1 运行环境

Spring Security 需要1.5以上的java运行环境。Spring Security的旨在以独立的方式来运行,没有必要放置任何特定的配置文件到你的Java运行时环境中。尤其是没有必要配置专门的Java鉴定与授权服务策略文件或者将Spring Security放置在公共的类路径中。

类似地,如果你正在使用EJB容器或者Servlet容器,没有必要将任何特殊的配置文件放置在任何位置,也不需要将Spring Security包含到服务器的类加载器中。

这种设计提供了最大化的开发灵活度,因此你可以简单的将你的目录JAR 等在不同的系统中复制,并且可以直接运行。

1.2 核心组件

在Spring Security 3.0中,“spring-security-core” jar 中已经剥离到最简单的程度。不再包含任何关联web应用安全,LDAP或者命名空间配置的代码。我们将会在后面预览一些core模块中的Java类。它们代表了这个框架的建筑基础,因此如果你需要不止学习简单的命名空间配置,那么理解他们是什么是非常重要的,除非你不直接和他们交互。

1.2.1 SecurityContextHolder, SecurityContext and Authentication对象

最基础的对象是“SecurityContextHolder”。这个对象里存储了应用当前的security 上下文信息,这些信息中包含了应用当前使用的主要的信息。“SecurityContextHolder”默认使用“ThreadLocal ”来存储这些信息,这意味着同一线程中执行的方法可以访问security 上下文,即使没有通过参数显式地传递给这些方法。如果在当前的请求处理完成之后记得清除线程,使用“ThreadLocal ”是安全的。当然,Spring Security会自动处理,我们没有必要担心这个。
有一些程序不是完全适用“ThreadLocal ”这种方式的,因为使用线程的特殊方式。例如,一个Swing客户端有可能需要一个JVM中所有的线程都使用同一个 security 上下文。“SecurityContextHolder ”可以在启动的时候配置一个策略来指定你想如何存储security上下文。对一独立的程序来说,你可以使用“SecurityContextHolder.MODE_GLOBAL ”策略。其他的程序有可能想让从安全的线程中产生的线程也使用同样的安全身份。可以使用“SecurityContextHolder.MODE_INHERITABLETHREADLOCAL”来达到这种目的。你可以通过两种方式来改变默认的“SecurityContextHolder.MODE_THREADLOCAL”。1.设置一个系统属性;2.调用“SecurityContextHolder” 的一个静态方法。大多数应用不需要改变默认的方式,如果需要的话可以参考“SecurityContextHolder ” Java Doc。

获取当前用户的信息
在“SecurityContextHolder ”内部,我们存储了当前与应用交互的基本信息。Spring Security 使用一个“Authentication ”对象来代表这些信息。我们一般不需要自己来创建“Authentication ”对象,但是用户来查询“Authentication ”对象是非常常见的。可以使用下面的代码块(在程序中任何地方)来获取当然被授权的用户名称。

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

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

通过调用getContext()来返回一个“SecurityContext ”接口的实例。这个对象被保存在thread-local存储中。接下来我们即将看到,大多数鉴权机制捆绑Spring Security返回“UserDetails ”的实例,这个实例是最基本的东西。

1.2.2 UserDetailsService

上面代码中更一个需要注意的是你能从“Authentication” 获取最基本的信息。这个最基本的信息仅仅是一个Object。通常我们能将它强转成“UserDetails ”对象。“UserDetails ”是一个Spring Security的核心接口。它代表了一个principal,但是是一种可扩展的应用可以定制的方式。可以把“UserDetails ”想像成我们数据库和Spring Security SecurityContextHolder需要的对象之间的适配器。作为一种我们数据库的某种代表,通常我们需要将“UserDetails ”强转成我们程序提供的原始对象,然后我们可以调用业务上定义的方法(比如'getEmail()','getEmployeeNumber()'等)。

现在我们可能会想知道,什么时候提供“UserDetails ”对象?怎么样提供?我认为你说的这个东西是已经申明了,并且我不需要写任何的Java代码-谁来提供?简单答案是有一个专门的“UserDetailsService ”接口。这个接口中只有一个接收字符串样式的用户名参数的方法,并且返回一个“UserDetails”:

UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;

在Spring Security中,这是最通用的方式去加载用户的信息,并且你将看到在框架中任何需要用户信息的时候被使用。

在鉴权成功的时候,UserDetails 被用来创建Authentication 对象,这个对象被存储在“SecurityContextHolder ”。好消息是框架中提供了一些“UserDetailsService” 的实现,其中一个使用内存中的map(InMemoryDaoImpl),另外一个使用JDBC(JdbcDaoImpl)。尽管如此,大多数用户还是希望创建它们自己的,在他们的实现中经常简单地使用DAO,这些代表了他们的employees,customers,或者应用中其他的用户。记住一个好处就是不管UserDetailsService 返回什么,我们都可以像上面的代码中那样从SecurityContextHolder 获取到。

我们经常会对UserDetailsService产生误解。它纯粹是一个获取用户数据的DAO 和执行为框架中其他组件提供这些用户数据的功能,除此之外不做其他事情。尤其是它不对用户进行鉴权,这个是由AuthenticationManager来做的。在很多情况下,如果需要自定义鉴权功能,可以直接实现“AuthenticationProvider” 。

1.2.3 授权

除了principal,“Authentication” 提供的另外一个重要的方法是“getAuthorities()” 。这个方法提供一组“GrantedAuthority” 对象。一个“GrantedAuthority” 对象是授权给principal的权限。这个权限通常是角色,比如“ROLE_ADMINISTRATOR” 或者“ROLE_HR_SUPERVISOR” 。这些角色是后来为了web授权,方法授权以及域对象授权而配置的(没懂是啥意思)。Spring Security的其他部分有能力拦截这些权限,并且期望他们被呈现。“GrantedAuthority” 通常由“UserDetailsService” 来加载。

通常“GrantedAuthority” 对象是应用侧的权限。他们不是指定给提供的域对象的。因此,你不要有使“GrantedAuthority” 代表“Employee”对象的权限,因为有成千上万的这样的权限,很可能会内存溢出(或者,至少会导致应用花费很时间来给用户鉴权)。当然Spring Security已经专门设计了来处理这个公共的需要,但是为了这个目的,域对象不要具有安全处理能力。

1.2.4 综述

只是简单回顾一下,我们目前接触到的主要元素为以下几点:

SecurityContextHolder,提供访问SecurityContext 的功能。

SecurityContext,持有Authentication 和提供可能的请求域的安全信息。

Authentication,代表了在Spring Security 特有方式中的principal。

GrantedAuthority,代表了给principal授予的应用端权限。

UserDetails,从DAO或者其他的数据提供构建认证信息必要的信息。

UserDetailsService,当提供用户名或者认证ID或者其他类似的信息时创建UserDetails。

1.3 认证

Spring Security 可以参与到许多不同的认证场景中。虽然我们建议使用Spring Security 来进行认证而且不要和已有的容器管理的认证进行集成,虽然这个功能也是支持的-跟自己专有的认证系统进行集成。

1.3.1 Spring Security中的认证是什么?

让我们来考虑一下一个大家都熟悉的标准认证场景。

一个用户想要通过用户名和密码登录。

系统校验用户名跟密码是匹配的。

获取到这个用户的信息(他的一些权限或其他的)。

为用户创建一个安全的环境。

用户收到响应,有可能会进行一些被访问控制机制保护的操作,这个机制会在当前的上下文环境中检查必要的操作权限。

最开始的三步构成了认证的过程,因此我们来看看Spring Security是怎么做的。

用户名跟密码被获取和组装到一个“UsernamePasswordAuthenticationToken(Authentication 的一个实例)” 的对象中。

令牌对象被传递到“AuthenticationManager” 实例中进行验证。

当认证成功之后,“AuthenticationManager” 返回一个完整的“Authentication”对象。

通过调用SecurityContextHolder.getContext().setAuthentication(...)将返回的认证对象传递进去建立安全上下文。

从那时起,用户被认为是已经认证通过了。下面看一段例子:

import org.springframework.security.authentication.*;
import org.springframework.security.core.*;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;

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 {
Authentication request = new UsernamePasswordAuthenticationToken(name, password);
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());
}
}

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” 将会对用户名跟密码一样的用户进行认证。并赋予一个角色给这个用户。输出大致是以下这样的:

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

注意我们不需要这样写代码,这个过程通常是在内部完成的,比如web认证的filter。当“SecurityContextHolder” 包含了完整的“Authentication ” 对象时,这个用户就被认证了。

1.3.2 直接设置SecurityContextHolder 的内容

实际上,Spring Security 并不关心你是如何将“Authentication” 对象设置到“SecurityContextHolder” 内部的。唯一关键的要求是“SecurityContextHolder ” 必须在“AbstractSecurityInterceptor (后面会说到)” 需要认证用户的操作时包含代表了principal 的“Authentication ”对象。

你可以(很多人这么做)写自己的过滤器或者MVC控制器来跟其他不是基于Spring Security的认证系统进行交互。比如,你可能正在用容器管理的认证系统,这些系统使来自ThreadLocal或者JNDI的用户可以使用。又或者你为使用遗留的专用认证系统的公司工作,这个系统是公司的,你有很少的控制权。在这种情况下,你很容易的就可以使用Spring Security,并且使其提供认证的能力。所有你需要做的工作是写一个filter(或其他类似的)来读取第三方的用户信息,创建一个Spring Security 专有的认证对象,然后将其设置到“SecurityContextHolder” 。在这种情况下,你也需要考虑一些平时内部认证系统自动做的事情。比如,你可能需要在返回给客户端之前先创建一个HTTP session来存储请求之间的上下文信息。

1.4 web应用中的认证

现在让我们探索一下当我们在web中使用Spring Security的解决方案。用户是如何认证和security上下文是如何创建的?

考虑一个典型的web应用认证的流程:

访问主页并点击一个链接。

向服务器发送一个请求,服务端确定你是不是之前访问过受保护的资源。

由于你之前没有认证,服务端会返回给你必须认证的消息。这个响应有可能是一个HTTP响应码,或者跳转到一个常规的网页。

你的浏览器将有可能会跳转到特定的网页以便你能填写表单,或者浏览器会以某种方式检索你的身份(通过一个基本的认证对话框,cookie,X.509证书等)。

浏览器会发送一个响应给服务器。有可能是包含了你填写的表单内容的HTTP POST,又或者是包含了你的认证信息的HTTP 头。

接下来服务端会判断提供的认证信息是不是有效的。如果有效,将会进入下一步。如果无效,你的浏览器通常会要求你重试(因此你又回到了上面的步骤)。

引起你要认证的原始请求将会重试。希望你能取得有效的权限来访问这些受保护的资源。如果你有有效的权限,这个请求便成功了。否则你会收到一个403的HTTP错误码,意思是你被禁止了。

Spring Security 有不同的类来负责上面描述的大部分步骤。最主要的部分是“ExceptionTranslationFilter”,“AuthenticationEntryPoint ” 和一个之前所说的负责调用AuthenticationManager的认证机制(按使用顺序排序)。

1.4.1 ExceptionTranslationFilter

“ExceptionTranslationFilter” 是一个Spring Security 过滤器,负责捕获任何抛出的Spring Security 异常。这些异常通常是由“AbstractSecurityInterceptor” 抛出的,这个是主要的认证服务的提供者。我们将在下一部分讨论“AbstractSecurityInterceptor” ,但是我们现在只需要知道它产生异常并且对HTTP 或者如何对principal进行授权一无所知。相反“ExceptionTranslationFilter” 提供这个服务,它专门负责返回403错误码(认证了但是没有权限访问)或者启动一个“AuthenticationEntryPoint(之前没有认证,去进行认证) ”。

1.4.2 AuthenticationEntryPoint

“AuthenticationEntryPoint” 负责上面步骤中的第三步。你可以想象到,每个web应用都会有一个默认的认证策略(这个也可以跟Spring Security其他的东西一样配置)。每个主要的认证系统都会有它自己的“AuthenticationEntryPoint”实现,通常都会执行上面描述的第3步中的某个操作。

1.4.3 认证机制

当你的浏览器通过HTTP表单或者HTTP header提交了你的认证信息,服务器上需要有个东西来收集这些信息。现在我们在上面所说的第6步中。在Spring Security里我们对收集认证信息的功能有个专门的称呼--认证机制。例子是基于表单的登录和基本认证方式。一旦从用户代理收集到身份认证信息,一个“Authentication”请求对象就会被创建并传递到“AuthenticationManager” 中。

1.4.4 请求之间的SecurityContext 存储

取决于系统的类型,可能需要有一个策略来存储用户操作之间的安全上下文。在一个典型的web应用中,一个用户只登录一次随后根据他们的session id来进行根踪。在session有效时间内,服务端会缓存principal 信息。在Spring Security中,在请求之间存储SecurityContext 的任务落在了“SecurityContextPersistenceFilter” 身上,它会默认将上下文对象作为一个属性存储HttpSession中。在每一次请求时它会将上下文对象存储到“SecurityContextHolder” 中,至关重要的是在请求完成时会清除“SecurityContextHolder” 。你不应该出于安全的目的去跟HttpSession 直接交互。也没有理由去这样做,通过“SecurityContextHolder” 就可以了。

许多其他的系统(比如无状态的RESTFull服务)不使用HTTP session,因些每个请求都会重新认证。然而将“SecurityContextPersistenceFilter” 包含在过滤器链中来保证SecurityContextHolder 在每个请求完成之后被清空仍然是很重要的。

在应用中如果单个session有并发请求,同一个“SecurityContext ”会在多个线程之间共享。尽管使用了“ThreadLocal” 但是在每个线程中从“HttpSession” 中获取到的都是同一个实例。这暗示着如果你想暂时改变一个线程正在运行的上下文。如果你只用了“SecurityContextHolder.getContext()” ,并且在返回的上下文对象中调用了setAuthentication(anAuthentication),然后“Authentication ” 对象会在所有共享同一个“SecurityContext” 的并发线程中发生改变。你可以自定义“SecurityContextPersistenceFilter” 的行为来让它为每个请求都创建新的“SecurityContext” 对象,从而阻止一个线程的修改影响其他的线程。还有一种可选的方式是你可以在你需要临时修改上下文对象的地方创建一个新的对象。“SecurityContextHolder.createEmptyContext()” 每次都会返回一个新的上下文对象。

1.5 访问控制

在Spring Security 负责访问控制的接口是"AccessDecisionManager" 。它有一个decide 方法,方法中持有一个代表principal 请求的“Authentication” 对象,这个对象是一个“安全对象” 并且有一组安全元数据属性(比如一组被授予访问的所需角色)。

1.5.1 安全和AOP 通知

如果你熟悉AOP,你应该知道有不同类型的通知:before, after, throws 和 around。一个 around 通知是非常有用的, 因为advisor 可以选择是否处理方法调用,修改响应结果和抛出异常。跟web 请求一样, Spring Security 为方法调用提供了一个around advice 。 我们可以通过Spring 标准的AOP支持来实现一个方法调用的around advice , 可以通过使用一个标准的filter 来实现一个web 请求的around advice 。

对于那些不熟悉AOP 的用户,关键点是明白Spring Security 可以帮助你像保护web请求一样保护方法调用。 大多数人对service层安全的方法调用感兴趣。 这是因为service层是现在大多数Java EE应用的业务逻辑层。 如果你仅仅需要保护service层的方法调用,Spring 标准的AOP已经足够了。如果你需要直接保护域对象,可以考虑一下AspectJ 。

你可以选择使用AspectJ 或 Spring AOP 来执行方法授权,或者选择使用过滤器来执行web请求授权。你也可以自由组合。 主流的使用方式是一些web请求授权与在service层使用Spring AOP 方法调用授权进行组合执行。

1.5.2 安全对象和 AbstractSecurityInterceptor

那么什么是“安全对象”?Spring Security 使用术语来引用任何可以被保护的对象(比如授权决策 )。 最常见的例子是方法调用和web 请求。

每个支持的安全对象类型都有自己的interceptor 类,这个些是"AbstractSecurityInterceptor" 的子类。重要的是当“AbstractSecurityInterceptor” 被调用的时候,如果principal 已经认证,"SecurityContextHolder" 将会包含一个有效的“Authentication ”。

"AbstractSecurityInterceptor" 提供了一致的流程来处理安全对象的请求,典型地是:

查找当前请求关联的“配置属性”

提交安全对象,当前"Authentication"和配置属性给"AccessDecisionManager"作授权决定

在调用的地方选择性的改变"Authentication"

允许安全对象调用执行(假设被授予可以访问)

一旦调用返回,如果配置了"AfterInvocationManager",则调用它。如果调用了一个例外,"AfterInvocationManager"不会被调用。

配置属性是什么?
一个“配置属性”可以认为是对被"AbstractSecurityInterceptor"使用了的类具有特殊意义的字符串。他们是由框架中"ConfigAttribute"接口表示的。它们可能是简单的角色名称或者其他更多复杂的意义,这取决于"AccessDecisionManager"实现有多复杂。"AbstractSecurityInterceptor"配置了一个用来查找安全对象的属性的"SecurityMetadataSource"。通常这个配置对用户是透明的。配置属性将作为安全方法的注解或者安全URL的访问属性进入。例如我们在命名空间指南中看到的
"<intercept-url pattern='/secure/**' access='ROLE_A,ROLE_B'/>",意思是赋予匹配到这个模式的web请求赋予"ROLE_A"和"ROLE_B"配置属性。实际上,在"AccessDecisionManager"默认的配置下,例子中的意思是有"AccessDecisionManager"的人满足其中一个属性就可以允许访问。严格地讲,它们只是属性,并且他们的解释依赖于"AccessDecisionManager"实现。"ROLE"前缀用来指出这些属性是角色,应该被Spring Security 的“RoleVoter”使用到。这只跟"AccessDecisionManager"使用了投票机制才有关。

RunAsManager
假设"AccessDecisionManager"决定允许这个请求,"AbstractSecurityInterceptor"通常是仅仅处理请求。之前提到过,在极少数情况下用户可能想替换掉"SecurityContext"中的"Authentication",它是由"AccessDecisionManager"调用"RunAsManager"处理的。这可能在合理的不正常情况下会有用,比如如果一个service层方法需要调用一个远程系统并且提供一个不同的标识。因为Spring Security 自动地从一个系统传递安全标识到另一个系统(假设你正在使用一个正确配置的RMI或者HttpInvoker 远程协议客户端),这有可能会有用。

AfterInvocationManager
随着安全对象调用的处理和返回-这意味着一个方法调用正在完成或者一个过滤器链正在处理-"AbstractSecurityInterceptor"有一个最后的机会来处理这个调用。在这个阶段"AbstractSecurityInterceptor"对修改返回结果有兴趣。我们可能希望这个会发生,因为对一个安全对象调用来说,一个授权对象不可能做一直在里面的决定(汗)。高度可拔插,"AbstractSecurityInterceptor"会在需要的情况下将控制权交给"AfterInvocationManager"来实实际修改返回值。这个类甚至可以完全替换掉返回值,或者抛出异常,或者不改变它。after-invocation 检查只会在方法调用成功的时候执行。如果有异常发生,附加的检查将会跳过。

可以在Security interceptors and the "secure object" model中找到"AbstractSecurityInterceptor"和相关的对象的信息。



继承安全对象模型
只有开发者考虑一个全新的拦截和授权请求方式时,才需要直接使用安全对象。例如,有可能要创建一个新的安全对象使消息系统中的电话变得安全。任何需要安全和提供拦截调用的方式(比如AOP 环绕通知语义)的可以被当成一个安全对象。之前已经说过,大多数的Spring 应用只需要简单透明地使用3个现在支持的安全对象类型(AOP "MethodInvocation",AspectJ "JoinPoint" 和web请求"FilterInvocation")。

1.6 本地化

Spring Security支持本地化的异常信息,终端用户可能会看到。如果你的应用是为英语用户设计的,你不需要做任何事,因为Spring Security消息默认使用英语。如果你需要支持其他地区,所有你需要知道的包含在这一节中。

所有的异常信息都可以本地化,包括与认证失败和访问被拒绝(授权失败)相关的信息。开发人员和系统开发人员关注的异常和日志信息(包括不正常的属性,接口契约验证,使用不正确的构造函数,启动时间验证,调试级别的日志)不能本地化,它们是用英语硬编码在Spring Security的代码中的。

查看"spring-security-core-xx.jar"你可以找到"org.springframework.security"包下面包含了一个"messages.properties"文件,还有一些常见语言的本地化版本。这些应该被你的“ApplicationContext” 引用,因为Spring Security 类实现Spring 的"MessageSourceAware"接口并且希望消息解析器能够在启动时注入到应用上下文中。通常所有你需要做的是在应用上下文中注册一个bean来引用这些消息。下面是一个例子:

<bean id="messageSource"
class="org.springframework.context.support.ReloadableResourceBundleMessageSource">
<property name="basename" value="classpath:org/springframework/security/messages"/>
</bean>

"messages.properties"的命名方式与标准的资源文件是一致的,代表Spring Security默认支持的语言。默认的语言是英语。

如果你想要自定义"messages.properties"文件,或者支持其他语言,你需要复制这个文件,相应地重命名,并且在上面提到的bean中注册。这个文件没有很多消息键,因此后面还需要再添加。如果你确定要执行这个文件的本地化,请考虑与你的团队合作,将它记录到JIRA任务中并且附加你的合适命名的本地化"messages.properties"文件。

Spring Security依赖于Spring 的本地化支持来查找对应的消息。为了能够正常工作,你不得不确认请求中的local信息存储在Spring 的"org.springframework.context.i18n.LocaleContextHolder"中。Spring MVC 的"DispatcherServlet"已经自动做了这个工作,但是由于Spring Security的过滤器在此之前调用,"LocaleContextHolder"需要在过滤器调用之前包含正确的"Locale"。可以自己写一个过滤器(必须在Spring Security的过滤器之前调用)或者使用"RequestContextFilter"。阅读Spring Framework 文档了解更多关于使用Spring 本地化的细节。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息