9、Spring Security 5.2.2文档翻译-架构和实现

一旦您熟悉了基于名称空间配置的应用程序的设置和运行,您可能希望进一步了解框架在名称空间背后的实际工作方式。与大多数软件一样,Spring Security具有某些在整个框架中通常使用的中心接口、类和概念抽象。在参考指南的这一部分中,我们将研究其中的一些,并了解它们如何协同工作以支持Spring Security中的身份验证和访问控制。

1、 技术概览

1.1、运行时环境

Spring Security 5.2.2.RELESE 需要Java 8或更高的运行时环境。由于Spring Security旨在以自包含的方式进行操作,因此不需要将任何特殊的配置文件放入Java运行时环境中。特别是,不需要配置特殊的Java身份验证和授权服务(JAAS)策略文件,也不需要将Spring Security放入公共类路径位置。类似地,如果您使用EJB容器或Servlet容器,则不需要在任何地方放置任何特殊配置文件,也不需要在服务器类加载器中包含Spring安全性。所有需要的文件都将包含在您的应用程序中。这种设计提供了最大的部署时间灵活性,因为您可以简单地将目标工件(不管是JAR、WAR还是EAR)从一个系统复制到另一个系统,它将立即工作。

1.2、核心组件

从Spring Security 3.0开始,Spring-Security-core.jar的内容被简化到最少。它不再包含任何与web应用程序安全性、LDAP或名称空间配置相关的代码。我们将在这里看一看在核心模块中找到的一些Java类型。它们表示框架的构建块,因此如果您需要超越简单的名称空间配置,那么理解它们是什么是很重要的,即使您实际上不需要直接与它们交互。

SecurityContextHolder, SecurityContext and Authentication Objects

最基本的对象是SecurityContextHolder。这里存储应用程序当前安全上下文的详细信息,其中包括当前使用应用程序的被认证对象的详细信息。默认情况下,SecurityContextHolder使用一个ThreadLocal来存储这些细节,这意味着安全上下文始终对同一执行线程中的方法可用,即使安全上下文没有作为参数显式地传递给这些方法。如果在当前被认证对象的请求被处理后清除线程,那么以这种方式使用ThreadLocal是非常安全的。当然,Spring Security会自动为您处理这些问题,所以不必担心。

有些应用程序并不完全适合使用ThreadLocal,因为它们使用线程的特定方式。例如,Swing客户机可能希望Java虚拟机中的所有线程使用相同的安全上下文。SecurityContextHolder可以在启动时配置一个策略来指定您希望如何存储上下文。对于一个独立的应用程序,您将使用SecurityContextHolder.MODE_GLOBAL策略。其他应用程序可能希望安全线程派生的线程也具有相同的安全标识。这是通过使用SecurityContextHolder.MODE_INHERITABLETHREADLOCAL来实现的。您可以以两种方式改变默认的SecurityContextHolder.MODE_THREADLOCAL。第一个是设置一个系统属性,第二个是调用SecurityContextHolder中的一个静态方法。大多数应用程序不需要更改默认设置,但是如果需要更改,请查看JavaDoc 中 SecurityContextHolder以了解更多信息。

获取当前用户的信息

在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接口的一个实例。这是保存在线程本地存储中的对象。我们将在下面看到,Spring Security中的大多数身份验证机制都将UserDetails的实例作为主体返回。

The UserDetailsService

从上面的代码片段中需要注意的另一项是,您可以从Authentication对象获得一个被认证对象。被认证对象只是一个对象。大多数情况下,这可以转换为UserDetails对象。

UserDetails是Spring Security中的一个核心接口。它表示一个被认证对象,但是是以一种可扩展的和特定于应用程序的方式。将UserDetails看作是您自己的用户数据库和SecurityContextHolder中的Spring安全性需求之间的适配器。作为来自您自己的用户数据库的内容的表示,您经常会将UserDetails转换为应用程序提供的原始对象,因此您可以调用特定于业务的方法(如getEmail(),getEmployeeNumber()等)。

现在您可能想知道,我什么时候提供UserDetails对象?怎么做呢?我以为你说这个东西是声明性的,我不需要写任何Java代码,这是为什么呢?简而言之,有一个称为UserDetailsService的特殊接口。此接口上的惟一方法接受基于字符串的username参数并返回UserDetails。

UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;

这是Spring Security中为用户加载信息的最常用方法,只要需要用户的信息,您就可以在整个框架中看到它的使用。

成功验证后,UserDetails用于构建存储在SecurityContextHolder中验证对象(更多信息见下面)。好消息是,我们提供了许多UserDetailsService实现,其中一个使用内存映射(InMemoryDaoImpl),另一个使用JDBC (JdbcDaoImpl)。但是,大多数用户都倾向于编写自己的实现,他们的实现通常只是位于表示其雇员、客户或应用程序的其他用户的现有数据访问对象(DAO)之上。请记住,无论您的UserDetailsService返回什么,都可以使用上面的代码片段从securitycontext中获得。

UserDetailsService经常会引起一些混淆。它纯粹是一个用于用户数据的DAO,除了向框架内的其他组件提供数据外,不执行任何其他功能。特别是,它不会对用户进行身份验证,这是由AuthenticationManager完成的。在许多情况下,如果需要自定义身份验证过程,直接实现AuthenticationProvider更有意义。

GrantedAuthority

除了被认证对象之外,身份验证提供的另一个重要方法是getAuthority()。此方法提供GrantedAuthority 对象数组。GrantedAuthority是授予被认证对象的权力,这并不奇怪。这些权限通常是“角色”,例如ROLE_ADMINISTRATOR或ROLE_HR_SUPERVISOR。稍后将为web授权、方法授权和域对象授权配置这些角色。Spring Security的其他部分能够解释这些权限,并期待它们的出现。GrantedAuthority 通常由UserDetailsService加载。

通常,GrantedAuthority对象是应用程序范围的权限。它们并不特定于给定的域对象。因此,您可能没有GrantedAuthority来表示对Employee对象编号54的权限,因为如果有成千上万个这样的权限,您将很快耗尽内存(或者,至少会导致应用程序需要很长时间来验证用户)。当然,Spring Security是专门为处理这一常见需求而设计的,但是您应该使用项目的域对象安全功能来实现这一目的。

总结

简单回顾一下,到目前为止我们看到的Spring安全性的主要构建块是:

  • SecurityContextHolder,提供对SecurityContext的访问。
  • SecurityContext,保存Authentication和特定于请求的安全信息。
  • Authentication,特定的Spring Security的方式表示被认证对象。
  • GrantedAuthority,以反映授予被认证对象的应用程序范围的权限。
  • UserDetails,提供从应用程序的DAOs或其他安全数据源构建身份验证对象所需的信息。
  • UserDetailsService,在传入基于字符串的用户名(或证书ID等)时创建用户详细信息。

既然您已经对这些重复使用的组件有了一定的了解,现在让我们进一步了解一下身份验证的过程。

1.3、Authentication

Spring Security可以参与许多不同的身份验证环境。虽然我们建议人们使用Spring Security进行身份验证,而不与现有的容器管理身份验证集成,但它仍然受到支持—就像与您自己的专有身份验证系统集成一样。

什么是Spring安全中的身份验证?

让我们考虑一个大家都熟悉的标准身份验证场景。

  1. 提示用户使用用户名和密码登录。
  2. 系统(成功地)验证用户名的密码正确。
  3. 获取该用户的上下文信息(角色列表等)。
  4. 为用户建立安全上下文。
  5. 用户继续执行某些操作,这些操作可能受到访问控制机制的保护,该机制根据当前安全上下文信息检查操作所需的权限。

前四项构成了身份验证过程,因此我们将了解这些内容在Spring Security中是如何发生的。

  1. 获取用户名和密码并将其组合为UsernamePasswordAuthenticationToken实例(Authentication接口的实例,如前所述)。
  2. 令牌被传递给AuthenticationManager实例进行验证。
  3. 成功验证后,AuthenticationManager返回一个完全填充的 Authentication实例。
  4. 通过调用securitycontext . getcontext (). setauthentication(…)来建立安全上下文,并传入返回的authentication对象。
    从那时起,用户就被认为是经过身份验证的。让我们以一些代码为例。
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身份验证过滤器中。我们在这里包含这些代码是为了说明在Spring Security中究竟是什么构成身份验证的问题有一个非常简单的答案。当SecurityContextHolder包含一个完全填充的Authentication 对象时,将对用户进行身份验证。

直接设置SecurityContextHolder的内容

事实上,Spring Security并不介意您如何将Authentication对象放入SecurityContextHolder中。
惟一的关键要求是SecurityContextHolder包含一个Authentication,它表示AbstractSecurityInterceptor(稍后我们将详细介绍)需要授权用户操作之前的被认证对象。

您可以(许多用户也这样做)编写自己的过滤器或MVC控制器,以提供与不基于Spring安全性的身份验证系统的互操作性。例如,您可能正在使用容器管理的身份验证,它使当前用户可以从ThreadLocal或JNDI位置访问。或者,您可能在一家拥有遗留的专有身份验证系统的公司工作,这是一种您几乎无法控制的企业“标准”。在这种情况下,很容易让Spring Security正常工作,同时仍然提供授权功能。您所需要做的就是编写一个过滤器(或等效的过滤器),从某个位置读取第三方用户信息,构建一个Spring特定于安全的身份验证对象,并将其放入SecurityContextHolder中。在这种情况下,您还需要考虑通常由内置的身份验证基础结构自动处理的事情。例如,在将响应写入客户机之前,您可能需要预先创建一个HTTP会话来缓存请求之间的上下文。(一旦提交了响应,就不可能创建会话。)

如果您想知道如何在真实世界的示例中实现AuthenticationManager,我们将在核心服务一章中介绍。

1.4 在web应用中认证

现在让我们研究一下在web应用程序中使用Spring安全性的情况(没有启用web.xml安全性)。如何验证用户身份并建立安全上下文?

考虑一个典型的web应用程序的身份验证过程:

  1. 你访问主页,然后点击一个链接。
  2. 一个请求被发送到服务器,服务器决定您请求的是受保护的资源。
  3. 由于目前还没有进行身份验证,服务器将发回一个响应,指示您必须进行身份验证。响应可以是HTTP响应代码,也可以是指向特定web页面的重定向。
  4. 根据身份验证机制,您的浏览器将重定向到特定的web页面,以便您填写表单,或者浏览器将以某种方式检索您的身份。(通过一个基本的认证对话框,一个cookie,一个X.509证书等。)
  5. 浏览器将向服务器发回响应。这将是一个包含您填写的表单内容的HTTP POST,或者是一个包含您的身份验证详细信息的HTTP头。
  6. 接下来,服务器将决定所提供的凭据是否有效。如果它们是有效的,下一步就会发生。如果它们是无效的,通常您的浏览器将被要求再次尝试(因此您返回到上面的步骤2)。
  7. 接下来,服务器将决定所提供的凭据是否有效。如果它们是有效的,下一步就会发生。如果它们是无效的,通常您的浏览器将被要求再次尝试(因此您返回到上面的步骤2)。

Spring Security有不同的类负责上述大多数步骤。主要的参与者(按使用的顺序)是ExceptionTranslationFilter、一个AuthenticationEntryPoint和一个“身份验证机制”,后者负责调用AuthenticationManager,我们在前面的小节中已经看到了。

ExceptionTranslationFilter

ExceptionTranslationFilter是一个Spring安全过滤器,它负责检测抛出的任何Spring安全异常。此类异常通常由AbstractSecurityInterceptor抛出,它是授权服务的主要提供者。我们将在下一节中讨论AbstractSecurityInterceptor,但是现在我们只需要知道它会产生Java异常,并且不知道HTTP或如何对被认证对象进行身份验证。相反ExceptionTranslationFilter提供这种服务,具体负责返回错误代码403(如果被认证对象已经过身份验证的,因此只是缺乏足够的访问——按步骤7),或启动一个AuthenticationEntryPoint(如果被认证对象还没有经过身份验证的,因此我们要开始第三步)。

AuthenticationEntryPoint

AuthenticationEntryPoint负责上述列表中的第三步。可以想象,每个web应用程序都有一个默认的身份验证策略(可以像Spring Security中的几乎所有东西一样进行配置,但是现在让我们保持简单)。每个主要的身份验证系统都有自己的AuthenticationEntryPoint实现,它通常执行步骤3中描述的操作之一。

Authentication Mechanism

一旦您的浏览器提交了您的身份验证凭证(作为HTTP表单post或HTTP头文件),服务器上就需要有一些东西来“收集”这些身份验证细节。到目前为止,我们处于上述列表的第六步。在Spring Security中,我们为从用户代理(通常是web浏览器)收集身份验证细节的功能提供了一个特殊的名称,将其称为“身份验证机制”。示例有基于表单的登录和基本身份验证。从用户代理收集身份验证细节之后,将构建Authentication“请求”对象,然后将其提交给AuthenticationManager。

在请求之间存储SecurityContext

根据应用程序的类型,可能需要一种策略来存储用户操作之间的安全上下文。在典型的web应用程序中,用户登录一次,然后通过其会话Id进行标识。服务器在会话期间缓存被认证对象的信息。在Spring Security中,在请求之间存储SecurityContext的责任落在SecurityContextPersistenceFilter上,它默认将上下文存储为HTTP请求之间的HttpSession属性。它将每个请求的上下文恢复到SecurityContextHolder,并且,最重要的是,在请求完成时清除SecurityContextHolder。出于安全目的,您不应该直接与HttpSession交互,这样做是没有理由的,所以使用SecurityContextHolder来代替。

许多其他类型的应用程序(例如,无状态rest式web服务)不使用HTTP会话,而是对每个请求重新进行身份验证。但是,在链中包含SecurityContextPersistenceFilter仍然很重要,以确保在每个请求之后清除SecurityContextHolder。

在一个在单个会话中接收并发请求的应用程序中,同一个SecurityContext实例将在线程之间共享。即使使用了ThreadLocal,它也是每个线程从HttpSession检索到的同一个实例。如果您希望临时更改线程运行的上下文,这将产生影响。如果您只是使用SecurityContext.getcontext(),并在返回的上下文对象上调用setAuthentication(anAuthentication),那么在所有共享同一个SecurityContext实例的并发线程中,Authentication对象都会发生变化。您可以自定义SecurityContextPersistenceFilter的行为,为每个请求创建一个全新的SecurityContext,从而防止一个线程中的更改影响到另一个线程。或者,您可以在临时更改上下文的地方创建一个新实例。方法SecurityContext . createEmptyContext()总是返回一个新的上下文实例。

1.5 Spring Security中的访问控制(授权)

在Spring Security中,负责访问控制决策的主接口是AccessDecisionManager。它有一个decide方法,该方法接受一个表示请求访问的被认证的Authentication对象、一个“secure object”(见下面)和一个应用于该对象的安全元数据属性列表(如授予访问所需的角色列表)。

Security and AOP Advice

如果熟悉AOP,就会知道有不同类型的advice可用:before、after、throw和around。around advice非常有用,因为advisor工具可以选择是否继续方法调用,是否修改响应,是否抛出异常。Spring Security为方法调用和web请求提供了一个around advice。我们使用Spring的标准AOP支持实现了方法调用的around advice,我们使用标准过滤器实现了web请求的around advice。

对于那些不熟悉AOP的人来说,要理解的关键点是Spring Security 可以帮助您保护方法调用和web请求。大多数人感兴趣的是保护其服务层上的方法调用。这是因为服务层是当前一代Java EE应用程序中大部分业务逻辑所在的地方。如果只需要在服务层中保护方法调用,Spring的标准AOP就足够了。如果您需要直接保护域对象,您可能会发现AspectJ是值得考虑的。可以选择使用AspectJ或Spring AOP执行方法授权,也可以选择使用过滤器执行web请求授权。您可以同时使用这些方法中的零个、一个、两个或三个。主流的使用模式是执行一些web请求授权,同时在服务层上执行一些Spring AOP方法调用授权。

Secure Objects and the AbstractSecurityInterceptor

那么什么是“secure object”呢?
Spring Security使用这个术语来指任何可以应用安全性(例如授权决策)的对象。最常见的例子是方法调用和web请求。

每种受支持的安全对象类型都有自己的拦截器类,它是AbstractSecurityInterceptor的子类。重要的是,在调用AbstractSecurityInterceptor时,如果被认证对象已经过身份验证,SecurityContext将包含有效的Authentication。

AbstractSecurityInterceptor为处理安全对象请求提供了一致的工作流,通常是:

  1. 查找与当前请求相关联的“配置属性”
  2. 为授权决策向AccessDecisionManager提交安全对象、当前身份验证和配置属性。
  3. 可以选择更改调用的Authentication
  4. 允许继续进行安全对象调用(假设授予了访问权)
  5. 一旦调用返回,则调用AfterInvocationManager(如果已配置)。如果调用引发异常,AfterInvocationManager将不会被调用。

什么是配置属性?

可以将“配置属性”视为对AbstractSecurityInterceptor使用的类具有特殊意义的字符串。它们由框架中的接口ConfigAttribute表示。它们可能是简单的角色名,也可能有更复杂的含义,这取决于AccessDecisionManager实现的复杂程度。AbstractSecurityInterceptor配置了一个SecurityMetadataSource,它用来查找安全对象的属性。通常这种配置对用户是隐藏的。配置属性将作为安全方法上的注解或安全url上的访问属性输入。例如,当我们在名称空间介绍中看到类似<intercept-url pattern=’/secure/**’ access=‘ROLE_A,ROLE_B’/>这样的内容时,这意味着配置属性ROLE_A和ROLE_B适用于匹配给定模式的web请求。在实践中,使用默认的AccessDecisionManager配置,这意味着任何具有匹配这两个属性之一的授权权限的人都将被允许访问。严格地说,它们只是属性,其解释依赖于AccessDecisionManager实现。前缀ROLE_的使用表明这些属性是角色,应该由Spring Security的RoleVoter使用。这仅在使用voter-based的AccessDecisionManager时相关。我们将在authorization chapter中看到AccessDecisionManager是如何实现的。

RunAsManager

假设AccessDecisionManager决定允许请求,AbstractSecurityInterceptor通常只处理请求。话虽如此,在极少数情况下,用户可能希望用不同的Authentication替换SecurityContext中的Authentication,这是由调用RunAsManager的AccessDecisionManager处理的。这在相当不寻常的情况下可能有用,比如服务层方法需要调用远程系统并提供不同的标识。因为Spring Security会自动将安全标识从一个服务器传播到另一个服务器(假设您使用的是正确配置的RMI或HttpInvoker remoting协议客户机),所以这可能很有用。

AfterInvocationManager

在完成安全对象调用并返回之后(这可能意味着完成方法调用或执行过滤器链),AbstractSecurityInterceptor将获得最后一次处理调用的机会。在这个阶段,AbstractSecurityInterceptor可能对修改返回对象感兴趣。我们可能希望发生这种情况,因为无法“在进入”安全对象调用的过程中做出授权决策。由于可高度插拔,AbstractSecurityInterceptor将控制权传递给AfterInvocationManager,以便在需要时实际修改对象。这个类甚至可以完全替换对象,或者抛出异常,或者不根据自己的选择以任何方式更改它。只有在调用成功时,才会执行调用后检查。如果发生异常,将跳过其他检查。AbstractSecurityInterceptor及其相关对象如图9.1所示
Figure 9.1. Security interceptors and the "secure object" model

扩展安全对象模型

只有考虑使用一种全新的拦截和授权请求的方法的开发人员才需要直接使用安全对象。例如,可以构建一个新的安全对象来保护对消息传递系统的调用。何需要安全性并提供拦截调用的方法(如围around advice语义的AOP)的东西都可以成为安全对象。尽管如此,大多数Spring应用程序将完全透明地使用当前支持的三种安全对象类型(AOP Alliance MethodInvocation、AspectJ JoinPoint和web请求FilterInvocation)。

2、核心服务

现在我们已经大致了解了Spring安全体系结构及其核心类,接下来让我们进一步了解一两个核心接口及其实现,特别是AuthenticationManager、UserDetailsService和AccessDecisionManager。这些内容经常出现在本文档的其余部分中,因此了解它们的配置和操作方式非常重要。

2.1、The AuthenticationManager, ProviderManager and AuthenticationProvider

AuthenticationManager只是一个接口,所以我们可以选择任何实现,但它在实践中是如何工作的呢?如果需要检查多个身份验证数据库或不同身份验证服务(如数据库和LDAP服务器)的组合,该怎么办?

Spring Security中的默认实现称为ProviderManager,它不处理身份验证请求本身,而是将其委托给一个已配置的AuthenticationProviders列表,依次查询每个提供者,以查看是否可以执行身份验证。每个提供者要么抛出一个异常,要么返回一个完全填充的Authentication对象。还记得我们的好朋友UserDetails和UserDetailsService吗?如果不记得,回顾前一章。验证身份验证请求最常用的方法是加载相应的UserDetails并根据用户输入的密码检查已加载的密码。这是DaoAuthenticationProvider使用的方法(参见下面)。加载的UserDetails对象——特别是它包含的GrantedAuthoritys——将在构建完全填充的Authentication对象时使用,该对象从成功的身份验证返回并存储在SecurityContext中。

如果使用名称空间,将在内部创建和维护ProviderManager的实例,并通过使用名称空间身份验证提供者元素向其添加提供者(请参阅 the namespace chapter)。在这种情况下,不应该在应用程序上下文中声明ProviderManager bean。但是,如果您没有使用名称空间,那么您可以这样声明它:

<bean id="authenticationManager"
        class="org.springframework.security.authentication.ProviderManager">
    <constructor-arg>
        <list>
            <ref local="daoAuthenticationProvider"/>
            <ref local="anonymousAuthenticationProvider"/>
            <ref local="ldapAuthenticationProvider"/>
        </list>
    </constructor-arg>
</bean>

在上面的例子中,我们有三个提供者。它们按照显示的顺序进行尝试(这是使用列表所暗示的),每个提供者都可以尝试身份验证,或者通过简单地返回null来跳过身份验证。如果所有实现都返回null, ProviderManager将抛出ProviderNotFoundException。如果您有兴趣了解关于链接提供程序的更多信息,请参考ProviderManager Javadoc。

身份验证机制(如web表单登录处理筛选器)被注入到ProviderManager的引用,并将调用它来处理身份验证请求。您需要的提供者有时可以与身份验证机制互换,而在其他时候则依赖于特定的身份验证机制。例如,DaoAuthenticationProvider和LdapAuthenticationProvider与提交简单用户名/密码身份验证请求的任何机制都是兼容的,因此可以与基于表单的登录或HTTP基本身份验证工作。另一方面,一些身份验证机制创建一个身份验证请求对象,该对象只能由单一类型的AuthenticationProvider解释。这方面的一个例子是JA-SIG CAS,它使用服务票据的概念,因此只能通过CasAuthenticationProvider进行身份验证。您不必太关心这个问题,因为如果忘记注册合适的提供者,那么在尝试进行身份验证时,您只会收到ProviderNotFoundException。

成功验证时擦除凭据

默认情况下(从Spring Security 3.1开始),ProviderManager将尝试从成功的身份验证请求返回的Authentication对象中清除任何敏感的凭据信息。这可以防止像密码这样的信息被保留超过必要的时间。

这可能会在您使用用户对象的缓存时造成问题,例如,在无状态应用程序中提高性能。如果身份验证包含对缓存中的对象的引用(例如UserDetails实例),并且该对象的凭据已被删除,那么将无法根据缓存的值进行身份验证。如果使用缓存,则需要考虑这一点。一个明显的解决方案是,首先在缓存实现中或在创建返回的Authentication对象的AuthenticationProvider中复制对象。或者,您可以在ProviderManager上禁用eraseCredentialsAfterAuthentication属性。有关更多信息,请参见Javadoc。

DaoAuthenticationProvider

Spring Security实现的最简单的AuthenticationProvider是DaoAuthenticationProvider,它也是该框架最早支持的一个。它利用UserDetailsService(作为一个DAO)来查找用户名、密码和授权权限。它只需将UsernamePasswordAuthenticationToken中提交的密码与UserDetailsService加载的密码进行比较,就可以对用户进行身份验证。配置提供者非常简单:

<bean id="daoAuthenticationProvider"
    class="org.springframework.security.authentication.dao.DaoAuthenticationProvider">
<property name="userDetailsService" ref="inMemoryDaoImpl"/>
<property name="passwordEncoder" ref="passwordEncoder"/>
</bean>

编码器是可选的。PasswordEncoder提供密码的编码和解码,这些密码出现在从配置的UserDetailsService返回的UserDetails对象中。下面将对此进行更详细的讨论

2.2、 UserDetailsService实现

正如本参考指南前面所提到的,大多数身份验证提供者都利用了UserDetails和UserDetailsService接口。回想一下UserDetailsService的契约是一个单一的方法:

UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;

返回的UserDetails是一个接口,它提供了一些getters,这些getters保证提供非空的身份验证信息,比如用户名、密码、授予的权限以及用户帐户是否启用或禁用。大多数身份验证提供者将使用UserDetailsService,即使用户名和密码实际上并不是身份验证决策的一部分。他们可能只使用返回的UserDetails对象的授权信息,因为其他一些系统(如LDAP或X.509或CAS等)承担了实际验证凭证的责任。

由于UserDetailsService的实现非常简单,用户可以很容易地使用自己选择的持久性策略来检索身份验证信息。尽管如此,Spring Security确实包含了一些有用的基本实现,我们将在下面介绍。

内存中的身份验证

创建一个自定义UserDetailsService实现很容易使用,它可以从选择的持久性引擎中提取信息,但是许多应用程序不需要这样的复杂性。如果您正在构建一个原型应用程序,或者刚刚开始集成Spring Security,而又不想花时间配置数据库或编写UserDetailsService实现,那么这一点尤其重要。对于这种情况,一个简单的选择是使用来自安全名称空间的user-service元素:

<user-service id="userDetailsService">
<!-- Password is prefixed with {noop} to indicate to DelegatingPasswordEncoder that
NoOpPasswordEncoder should be used. This is not safe for production, but makes reading
in samples easier. Normally passwords should be hashed using BCrypt -->
<user name="jimi" password="{noop}jimispassword" authorities="ROLE_USER, ROLE_ADMIN" />
<user name="bob" password="{noop}bobspassword" authorities="ROLE_USER" />
</user-service>

这也支持使用外部属性文件:

<user-service id="userDetailsService" properties="users.properties"/>

属性文件应该包含表单中的条目:

username=password,grantedAuthority[,grantedAuthority][,enabled|disabled]

例如:

jimi=jimispassword,ROLE_USER,ROLE_ADMIN,enabled
bob=bobspassword,ROLE_USER,enabled

JdbcDaoImpl

Spring Security还包括一个UserDetailsService,它可以从JDBC数据源获取身份验证信息。在内部使用了Spring JDBC,因此它避免了全功能对象关系映射器(ORM)的复杂性,只用于存储用户细节。如果您的应用程序确实使用ORM工具,那么您可能更愿意编写一个自定义UserDetailsService来重用您可能已经创建的映射文件。返回到JdbcDaoImpl,示例配置如下所示:

<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="org.hsqldb.jdbcDriver"/>
<property name="url" value="jdbc:hsqldb:hsql://localhost:9001"/>
<property name="username" value="sa"/>
<property name="password" value=""/>
</bean>

<bean id="userDetailsService"
    class="org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl">
<property name="dataSource" ref="dataSource"/>
</bean>

您可以通过修改上面显示的DriverManagerDataSource来使用不同的关系数据库管理系统。与其他任何Spring配置一样,您也可以使用从JNDI获得的全局数据源。

Authority Groups

默认情况下,JdbcDaoImpl加载单个用户的权限,并假设这些权限是直接映射到用户组(请参阅database schema appendix)。另一种方法是将权限划分为组并将组分配给用户。有些人喜欢将此方法作为管理用户权限的一种方法。有关如何启用组权限的更多信息,请参阅JdbcDaoImpl Javadoc。附录中还包含了组模式。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章