8. 架构与实施

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

8.1 技术概述

8.1.1 运行时环境

Spring Security 3.0 需要 Java 5.0 Runtime Environment 或更高版本。由于 Spring Security 旨在以独立的方式运行,因此无需在 Java 运行时环境中放置任何特殊的配置文件。特别是,无需配置特殊的 Java 身份验证和授权服务(JAAS)策略文件或将 Spring Security 放置在公共 Classpath 位置。

同样,如果您使用的是 EJB 容器或 Servlet 容器,则无需在任何地方放置任何特殊的配置文件,也无需在服务器类加载器中包含 Spring Security。所有必需的文件将包含在您的应用程序中。

这种设计提供了最大的部署时间灵 Active,因为您只需将目标工件(JAR,WAR 或 EAR)从一个系统复制到另一个系统即可立即使用。

8.1.2 核心组件

在 Spring Security 3.0 中,spring-security-core jar 的内容被精简到最低限度。它不再包含与 Web 应用程序安全性,LDAP 或名称空间配置有关的任何代码。我们将在这里介绍您在核心模块中找到的一些 Java 类型。它们代表了框架的构建块,因此,如果您需要超越简单的名称空间配置,那么即使实际上不需要直接与它们进行交互,也必须了解它们的含义,这一点很重要。

SecurityContextHolder,SecurityContext 和身份验证对象

最基本的对象是SecurityContextHolder。我们在这里存储应用程序当前安全上下文的详细信息,其中包括当前使用该应用程序的主体的详细信息。默认情况下,SecurityContextHolder使用ThreadLocal来存储这些详细信息,这意味着安全上下文始终可用于同一执行线程中的方法,即使没有将安全上下文显式传递给这些方法的参数也是如此。如果在处理当前委托人的请求之后要清除线程,则以这种方式使用ThreadLocal是非常安全的。当然,Spring Security 会自动为您解决此问题,因此无需担心。

某些应用程序不完全适合使用ThreadLocal,因为它们使用线程的特定方式。例如,SwingClient 端可能希望 Java 虚拟机中的所有线程都使用相同的安全上下文。可以为SecurityContextHolder配置启动策略,以指定您希望如何存储上下文。对于独立应用程序,您将使用SecurityContextHolder.MODE_GLOBAL策略。其他应用程序可能希望让安全线程产生的线程也采用相同的安全身份。这是通过使用SecurityContextHolder.MODE_INHERITABLETHREADLOCAL实现的。您可以通过两种方式从默认SecurityContextHolder.MODE_THREADLOCAL更改模式。第一个是设置系统属性,第二个是在SecurityContextHolder上调用静态方法。大多数应用程序不需要更改默认值,但是如果需要更改,请查看 JavaDoc for 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对象获取主体。主体只是Object。大多数情况下,可以将其转换为UserDetails对象。 UserDetails是 Spring Security 中的核心接口。它代表一种原理,但以一种可扩展的和特定于应用程序的方式。将UserDetails视为您自己的用户数据库和SecurityContextHolder内部的 Spring Security 所需的适配器。作为您自己的用户数据库中某些内容的表示,通常您会将UserDetails强制转换为应用程序提供的原始对象,以便可以调用特定于业务的方法(例如getEmail()getEmployeeNumber()等)。

现在,您可能想知道,那么什么时候提供UserDetails对象?我怎么做?我以为您说的这个东西是声明性的,不需要编写任何 Java 代码-有什么用?简短的答案是有一个名为UserDetailsService的特殊接口。此接口上的唯一方法接受基于String的用户名参数,并返回UserDetails

UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;

这是在 Spring Security 中为用户加载信息的最常用方法,只要需要有关用户的信息,就会在整个框架中使用它。

成功认证后,将使用UserDetails来构建存储在SecurityContextHolder中的Authentication对象(有关此below的更多信息)。好消息是,我们提供了许多UserDetailsService实现,包括一个使用内存 Map(InMemoryDaoImpl)和另一个使用 JDBC(JdbcDaoImpl)的实现。但是,大多数用户倾向于编写自己的应用程序,而其实现往往只是位于代表其员工,Client 或应用程序其他用户的现有数据访问对象(DAO)之上。请记住,使用上述代码段可以始终从SecurityContextHolder获得UserDetailsService返回的任何优点。

Note

关于UserDetailsService常常会有一些困惑。它纯粹是用于用户数据的 DAO,除了将数据提供给框架内的其他组件外,不执行其他功能。特别是,它不会对用户进行身份验证,这是由AuthenticationManager完成的。在许多情况下,如果您需要自定义身份验证过程,则直接使用implement AuthenticationProvider更有意义。

GrantedAuthority

除委托人外,Authentication提供的另一个重要方法是getAuthorities()。此方法提供了GrantedAuthority个对象的数组。毫不奇怪,GrantedAuthority是授予委托人的权限。此类权限通常是“角色”,例如ROLE_ADMINISTRATORROLE_HR_SUPERVISOR。稍后将这些角色配置为 Web 授权,方法授权和域对象授权。 Spring Security 的其他部分能够解释这些权限,并希望它们存在。 GrantedAuthority对象通常由UserDetailsService加载。

通常,GrantedAuthority对象是应用程序范围的权限。它们不特定于给定的域对象。因此,您不太可能用GrantedAuthority代表对Employee对象号 54 的许可,因为如果有成千上万个这样的权限,您很快就会用完内存(或者至少导致应用程序花费很长时间)时间来认证用户)。当然,Spring Security 是专门为满足这一通用要求而设计的,但您可以为此目的使用项目的域对象安全性功能。

Summary

回顾一下,到目前为止,我们已经看到了 Spring Security 的主要组成部分:

  • SecurityContextHolder,以提供对SecurityContext的访问权限。

  • SecurityContext,以保存Authentication以及可能特定于请求的安全信息。

  • Authentication,以特定于 Spring Security 的方式表示主体。

  • GrantedAuthority,以反映授予主体的应用程序范围的权限。

  • UserDetails,以提供必要的信息以从应用程序的 DAO 或其他安全数据源构建 Authentication 对象。

  • UserDetailsService,以便在传入基于String的用户名(或证书 ID 等)时创建UserDetails

现在,您已经了解了这些重复使用的组件,下面让我们仔细看看身份验证的过程。

8.1.3 Authentication

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

Spring Security 中的身份验证是什么?

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

  • 提示用户使用用户名和密码登录。

  • 系统(成功)验证用户名的密码正确。

  • 获取该用户的上下文信息(他们的角色列表等)。

  • 为用户构建了安全上下文

  • 用户可能会 continue 执行某些操作,该操作可能会受到访问控制机制的保护,该访问控制机制会根据当前安全上下文信息检查该操作所需的权限。

前三项构成了身份验证过程,因此我们将看看它们在 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");
}
}

在这里,我们编写了一个 Servlets,要求用户 Importing 用户名和密码并执行上述 Sequences。我们在此处实现的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.springframew[emailprotected]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 Security 的身份验证系统的互操作性。例如,您可能正在使用容器 Management 的身份验证,该身份验证使当前用户可以从 ThreadLocal 或 JNDI 位置访问。或者,您可能在一家拥有遗留专有身份验证系统的公司工作,这是您无法控制的公司“标准”。在这种情况下,让 Spring Security 正常工作并仍然提供授权功能是很容易的。您所需要做的就是编写一个过滤器(或等效过滤器),该过滤器从某个位置读取第三方用户信息,构建特定于 Spring Security 的Authentication对象,并将其放入SecurityContextHolder。在这种情况下,您还需要考虑通常由内置身份验证基础结构自动处理的事情。例如,在将响应写入 Client 端脚注之前,您可能需要先创建到在请求之间缓存上下文的 HTTP 会话:[一旦提交响应,就无法创建会话。

如果您想知道AuthenticationManager在实际示例中是如何实现的,我们将在核心服务章节中进行介绍。

8.1.4 Web 应用程序中的身份验证

现在,让我们探讨一下在 Web 应用程序中使用 Spring Security 的情况(未启用web.xml安全性)。如何验证用户身份并构建安全上下文?

考虑典型的 Web 应用程序的身份验证过程:

  • 您访问主页,然后单击链接。

  • 向服务器发送请求,服务器确定您已请求受保护的资源。

  • 由于您目前尚未通过身份验证,因此服务器会发回响应,指示您必须进行身份验证。响应将是 HTTP 响应代码,或重定向到特定网页。

  • 根据身份验证机制,您的浏览器将重定向到特定网页,以便您可以填写表格,或者浏览器将以某种方式检索您的身份(通过 BASIC 身份验证对话框,cookie,X.509 证书等)。 )。

  • 浏览器将响应发送回服务器。这将是包含您填写的表单内容的 HTTP POST 或包含身份验证详细信息的 HTTPHeaders。

  • 接下来,服务器将决定所提供的凭据是否有效。如果有效,则将进行下一步。如果它们无效,通常会要求您的浏览器再试一次(因此您返回到上面的第二步)。

  • 您尝试引起身份验证过程的原始请求将被重试。希望您已获得足够授权的身份验证,以访问受保护的资源。如果您具有足够的访问权限,则请求将成功。否则,您将收到一个 HTTP 错误代码 403,表示“禁止”。

Spring Security 有不同的类负责上述大多数步骤。主要参与者(按照使用 Sequences)是ExceptionTranslationFilterAuthenticationEntryPoint和“身份验证机制”,它们负责调用上一节中看到的AuthenticationManager

ExceptionTranslationFilter

ExceptionTranslationFilter是 Spring Security 过滤器,负责检测引发的任何 Spring Security 异常。此类异常通常由AbstractSecurityInterceptor引发,该AbstractSecurityInterceptor是授权服务的主要提供者。我们将在下一节中讨论AbstractSecurityInterceptor,但是现在我们只需要知道它会产生 Java 异常,并且对 HTTP 或对主体进行身份验证一无所知。而是ExceptionTranslationFilter提供此服务,具体负责返回错误代码 403(如果主体已通过身份验证,因此仅缺少足够的访问权限-按照上面的第七步),或者启动AuthenticationEntryPoint(如果主体未通过身份验证并且因此我们需要开始第三步)。

AuthenticationEntryPoint

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

Authentication Mechanism

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

身份验证机制收到完全填充的Authentication对象后,它将认为请求有效,将Authentication放入SecurityContextHolder,并尝试重试原始请求(上述步骤 7)。另一方面,如果AuthenticationManager拒绝了该请求,则认证机制将要求用户代理重试(上述第二步)。

在请求之间存储 SecurityContext

根据应用程序的类型,可能需要制定一种策略来存储用户操作之间的安全上下文。在典型的 Web 应用程序中,用户登录一次,然后通过其会话 ID 进行标识。服务器缓存持续时间会话的主体信息。在 Spring Security 中,在请求之间存储SecurityContext的责任落在SecurityContextPersistenceFilter,默认情况下,该上下文将上下文存储为 HTTP 请求之间的HttpSession属性。它为每个请求将上下文恢复到SecurityContextHolder,并且至关重要的是,在请求完成时清除SecurityContextHolder。为了安全起见,您不应直接与HttpSession进行交互。这样做根本没有道理-始终使用SecurityContextHolder代替。

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

Note

在单个会话中接收并发请求的应用程序中,相同的SecurityContext实例将在线程之间共享。即使正在使用ThreadLocal,它也是从HttpSession中为每个线程检索的同一实例。如果您希望临时更改运行线程的上下文,则可能会产生影响。如果只使用SecurityContextHolder.getContext(),并在返回的上下文对象上调用setAuthentication(anAuthentication),则Authentication对象将在所有并发线程中更改,这些线程共享同一SecurityContext实例。您可以自定义SecurityContextPersistenceFilter的行为以为每个请求创建一个全新的SecurityContext,从而防止一个线程中的更改影响另一个线程。另外,您可以在临时更改上下文的位置创建一个新实例。方法SecurityContextHolder.createEmptyContext()始终返回新的上下文实例。

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

负责在 Spring Security 中做出访问控制决策的主要接口是AccessDecisionManager。它具有decide方法,该方法接受代表请求访问的主体的Authentication对象,“安全对象”(请参见下文)和适用于该对象的安全元数据属性的列表(例如访问权限所必需的角色的列表)被授予)。

安全和 AOP 建议

如果您熟悉 AOP,您会知道有不同类型的建议可用:之前,之后,抛出和周围。环绕建议非常有用,因为顾问可以选择是否 continue 进行方法调用,是否修改响应以及是否引发异常。 Spring Security 提供了有关方法调用以及 Web 请求的全面建议。我们使用 Spring 的标准 AOP 支持来获得方法调用的通用建议,并使用标准的 Filter 来实现 Web 请求的通用建议。

对于那些不熟悉 AOP 的人来说,要了解的关键是 Spring Security 可以帮助您保护方法调用以及 Web 请求。大多数人都对在其服务层上确保方法调用感兴趣。这是因为服务层是大多数业务逻辑驻留在当前 Java EE 应用程序中的地方。如果只需要在服务层中确保方法调用的安全,那么 Spring 的标准 AOP 就足够了。如果您需要直接保护域对象,则可能会发现 AspectJ 值得考虑。

您可以选择使用 AspectJ 或 Spring AOP 执行方法授权,也可以选择使用过滤器执行 Web 请求授权。您可以同时使用零,一,二或三种方法。主流用法是执行一些 Web 请求授权,并在服务层上执行一些 Spring AOP 方法调用授权。

安全对象和 AbstractSecurityInterceptor

那么,什么是“安全对象”? Spring Security 使用该术语来指代任何可以对其应用安全性(例如授权决策)的对象。最常见的示例是方法调用和 Web 请求。

每个受支持的安全对象类型都有其自己的拦截器类,该类是AbstractSecurityInterceptor的子类。重要的是,到AbstractSecurityInterceptor被调用时,如果主体已通过身份验证,则SecurityContextHolder将包含有效的Authentication

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

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

  • 将安全对象,当前Authentication和配置属性提交给AccessDecisionManager以进行授权决策

  • (可选)更改发生调用的Authentication

  • 允许进行安全对象调用(假设已授予访问权限)

  • 一旦调用返回,则调用AfterInvocationManager(如果已配置)。如果调用引发异常,则AfterInvocationManager将不会被调用。

什么是配置属性?

可以将“配置属性”视为对AbstractSecurityInterceptor使用的类具有特殊含义的字符串。它们由框架内的接口ConfigAttribute表示。它们可能是简单的角色名称,也可能具有更复杂的含义,具体取决于AccessDecisionManager实现的复杂程度。 AbstractSecurityInterceptor配置有SecurityMetadataSource,它用于查找安全对象的属性。通常,此配置对用户隐藏。配置属性将作为安全方法的 Comments 或安全 URL 的访问属性 Importing。例如,当我们在名称空间介绍中看到类似于<intercept-url pattern='/secure/**' access='ROLE_A,ROLE_B'/>的内容时,这表示配置属性ROLE_AROLE_B适用于与给定模式匹配的 Web 请求。实际上,使用默认的AccessDecisionManager配置,这意味着具有GrantedAuthority匹配这两个属性之一的任何人都将被允许访问。严格来说,它们只是属性,其解释取决于AccessDecisionManager的实现。前缀ROLE_的使用是标记,指示这些属性是角色,并且应由 Spring Security 的RoleVoter使用。仅在使用基于投票者的AccessDecisionManager时才有意义。我们将看到authorization chapter中如何实现AccessDecisionManager

RunAsManager

假设AccessDecisionManager决定允许该请求,则AbstractSecurityInterceptor通常只会 continue 处理该请求。话虽这么说,但在极少数情况下,用户可能希望将SecurityContext内的Authentication替换为另一个Authentication,这由AccessDecisionManager调用RunAsManager处理。在相当不常见的情况下,例如服务层方法需要调用远程系统并显示不同的标识时,这可能很有用。由于 Spring Security 会自动将安全身份从一台服务器传播到另一台服务器(假设您使用的是正确配置的 RMI 或 HttpInvoker 远程协议 Client 端),因此这可能很有用。

AfterInvocationManager

在安全对象调用 continue 进行之后,然后返回-这可能意味着方法调用完成或过滤器链进行了-AbstractSecurityInterceptor获得了处理调用的最后机会。在此阶段,AbstractSecurityInterceptor可能会修改返回对象。我们可能希望发生这种情况,因为无法在安全对象调用的“途中”做出授权决定。由于高度可插拔,AbstractSecurityInterceptor会将控制权传递给AfterInvocationManager,以根据需要实际修改对象。此类甚至可以完全替换对象,或者引发异常,也可以按照其选择的任何方式对其进行更改。调用后检查仅在调用成功的情况下执行。如果发生异常,将跳过其他检查。

AbstractSecurityInterceptor及其相关对象显示在图 8.1,“安全拦截器和“安全对象”模型”

图 8.1. 安全拦截器和“安全对象”模型

抽象安全拦截器

扩展安全对象模型

只有打算采用全新方法来拦截和授权请求的开发人员才需要直接使用安全对象。例如,有可能构建一个新的安全对象以保护对消息系统的呼叫。任何需要安全性并且还提供拦截呼叫的方式的东西(例如围绕建议语义的 AOP)都可以成为安全对象。话虽如此,大多数 Spring 应用程序将完全透明地使用当前支持的三种安全对象类型(AOPunionMethodInvocation,AspectJ JoinPoint和 Web 请求FilterInvocation)。

8.1.6 Localization

Spring Security 支持本地化最终用户可能会看到的异常消息。如果您的应用程序是为说英语的用户设计的,则您无需执行任何操作,因为默认情况下,所有安全消息都是英语的。如果您需要支持其他语言环境,则本节包含您需要了解的所有内容。

可以本地化所有异常消息,包括与身份验证失败和访问被拒绝(授权失败)有关的消息。专注于开发人员或系统部署人员的异常和日志消息(包括不正确的属性,违反接口 Contract,使用不正确的构造函数,启动时间验证,调试级别的日志记录)未本地化,而是在 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 的本地化支持来实际查找适当的消息。为了使它起作用,您必须确保来自传入请求的语言环境存储在 Spring 的org.springframework.context.i18n.LocaleContextHolder中。 Spring MVC 的DispatcherServlet自动为您的应用程序执行此操作,但是由于在此之前调用了 Spring Security 的过滤器,因此需要在调用过滤器之前将LocaleContextHolder设置为包含正确的Locale。您可以自己在过滤器中执行此操作(必须在web.xml中的 Spring Security 过滤器之前),也可以使用 Spring 的RequestContextFilter。请参阅 Spring Framework 文档以获取有关在 Spring 中使用本地化的更多详细信息。

“联系人”samples 应用程序被设置为使用本地化消息。

8.2 核心服务

现在,我们已经对 Spring Security 体系结构及其核心类进行了高级概述,让我们仔细研究一下一个或两个核心接口及其实现,尤其是AuthenticationManagerUserDetailsServiceAccessDecisionManager。这些在本文档的其余部分中会定期出现,因此了解它们的配置方式和操作方式非常重要。

8.2.1 AuthenticationManager,ProviderManager 和 AuthenticationProvider

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

Spring Security 中的默认实现称为ProviderManager,而不是处理身份验证请求本身,它委派给已配置的AuthenticationProvider s 列表,依次查询每个AuthenticationProvider s 以查看其是否可以执行认证。每个提供程序都将引发异常或返回完全填充的Authentication对象。还记得我们的好朋友UserDetailsUserDetailsService吗?如果没有,请返回上一章并刷新您的 Memory。验证身份验证请求的最常见方法是加载相应的UserDetails并对照用户 Importing 的密码检查加载的密码。这是DaoAuthenticationProvider使用的方法(请参见下文)。构建完整填充的Authentication对象(从成功的身份验证返回并存储在SecurityContext中)时,将使用已加载的UserDetails对象-尤其是其中包含的GrantedAuthority

如果使用名称空间,则在内部创建并维护ProviderManager的实例,然后使用名称空间身份验证提供程序元素将提供程序添加到该名称(请参见命名空间一章)。在这种情况下,您不应在应用程序上下文中声明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>

在上面的示例中,我们有三个提供程序。按照显示的 Sequences 尝试使用它们(使用List暗示),每个提供程序都可以尝试进行身份验证,或者仅返回null即可跳过身份验证。如果所有实现都返回 null,则ProviderManager将抛出ProviderNotFoundException。如果您想了解有关链接提供程序的更多信息,请参阅ProviderManager Javadoc。

诸如 Web 表单登录处理过滤器之类的身份验证机制将注入对ProviderManager的引用,并将调用该ProviderManager以处理其身份验证请求。您所需的提供程序有时可以与身份验证机制互换,而在其他时候,它们将取决于特定的身份验证机制。例如,DaoAuthenticationProviderLdapAuthenticationProvider与提交简单的用户名/密码身份验证请求的任何机制兼容,因此将与基于表单的登录名或 HTTP Basic 身份验证一起使用。另一方面,某些身份验证机制会创建只能由AuthenticationProvider的单一类型解释的身份验证请求对象。一个示例就是 JA-SIG CAS,它使用服务票证的概念,因此只能由CasAuthenticationProvider进行身份验证。您不必太担心这一点,因为如果您忘记注册合适的提供程序,则在尝试进行身份验证时,您只会收到ProviderNotFoundException

删除成功认证的凭据

默认情况下(从 Spring Security 3.1 开始),ProviderManager将尝试从Authentication对象中清除所有敏感的凭据信息,该信息由成功的身份验证请求返回。这样可以防止将密码之类的信息保留的时间过长。

例如,在使用用户对象的缓存来提高 Stateless 应用程序的性能时,这可能会导致问题。如果Authentication包含对缓存中某个对象(例如UserDetails实例)的引用,并且其凭据已删除,则将无法再针对缓存的值进行身份验证。如果使用缓存,则需要考虑到这一点。一个明显的解决方案是首先在高速缓存实现中或在AuthenticationProvider中创建对象的副本,后者创建返回的Authentication对象。或者,您可以禁用ProviderManagereraseCredentialsAfterAuthentication属性。有关更多信息,请参见 Javadoc。

DaoAuthenticationProvider

Spring Security 实现的最简单的AuthenticationProviderDaoAuthenticationProvider,也是框架最早支持的之一。它利用UserDetailsService(作为 DAO)来查找用户名,密码和GrantedAuthority。只需将UsernamePasswordAuthenticationToken中提交的密码与UserDetailsService加载的密码进行比较,即可对用户进行身份验证。配置提供程序非常简单:

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

PasswordEncoder是可选的。 PasswordEncoder提供对从已配置UserDetailsService返回的UserDetails对象中提供的密码的编码和解码。 below将对此进行更详细的讨论。

8.2.2 UserDetailsService 实现

如本参考指南前面所述,大多数身份验证提供程序都使用UserDetailsUserDetailsService接口。回想一下UserDetailsService的 Contract 是一种方法:

UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;

返回的UserDetails是提供获取器的接口,该获取器保证以非空方式提供身份验证信息,例如用户名,密码,授予的权限以及用户帐户是启用还是禁用。即使用户名和密码实际上并未用作身份验证决策的一部分,大多数身份验证提供程序也会使用UserDetailsService。他们可能仅将返回的UserDetails对象用于GrantedAuthority信息,因为某些其他系统(例如 LDAP 或 X.509 或 CAS 等)承担了实际验证凭据的责任。

鉴于UserDetailsService实施起来非常简单,因此用户应该使用自己选择的持久性策略轻松检索身份验证信息。话虽如此,Spring Security 确实包含一些有用的基本实现,我们将在下面进行介绍。

In-Memory Authentication

创建自定义UserDetailsService实现易于使用,该实现从所选的持久性引擎中提取信息,但是许多应用程序不需要这种复杂性。如果您正在构建原型应用程序或刚开始集成 Spring Security,而又不想 true 花费时间配置数据库或编写UserDetailsService实现,则尤其如此。对于这种情况,一个简单的选择是使用安全性namespace中的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]

For example

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

JdbcDaoImpl

Spring Security 还包括一个UserDetailsService,该UserDetailsService可以从 JDBC 数据源获取身份验证信息。在内部使用 Spring JDBC,因此避免了仅用于存储用户详细信息的功能齐全的对象关系 Map 器(ORM)的复杂性。如果您的应用程序确实使用了 ORM 工具,则您可能更愿意编写自定义UserDetailsService来重用您可能已经创建的 Map 文件。返回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来使用不同的关系数据库 Management 系统。您还可以使用从 JNDI 获得的全局数据源,就像其他任何 Spring 配置一样。

Authority Groups

默认情况下,JdbcDaoImpl假定单个权限直接 Map 到用户(请参见数据库架构附录),从而为单个用户加载权限。另一种方法是将权限划分为组,然后将组分配给用户。有些人喜欢使用这种方法来 Management 用户权限。有关如何启用组权限的更多信息,请参见JdbcDaoImpl Javadoc。组模式也包含在附录中。

8.2.3 密码编码

Spring Security 的PasswordEncoder接口用于执行密码的单向转换,以允许安全地存储密码。鉴于PasswordEncoder是一种单向转换,因此在密码转换需要采用两种方式(即存储用于向数据库进行身份验证的凭据)时并不适用。通常,PasswordEncoder用于存储需要与认证时用户提供的密码进行比较的密码。

Password History

多年来,用于存储密码的标准机制已经 Developing。最初,密码以纯文本格式存储。假定密码是安全的,因为数据存储密码已保存在访问它所需的凭据中。但是,恶意用户能够使用 SQL Injection 这样的攻击找到方法来获取用户名和密码的大型“数据转储”。随着越来越多的用户凭证成为公共安全 maven,我们意识到我们需要做更多的工作来保护用户密码。

然后鼓励开发人员在通过诸如 SHA-256 之类的单向哈希运行密码后存储密码。当用户尝试进行身份验证时,会将哈希密码与他们键入的密码的哈希进行比较。这意味着系统仅需要存储密码的单向哈希。如果发生违规,则仅暴露密码的一种单列哈希。由于散列是一种方式,计算出给定哈希值的密码很难计算,因此找出系统中的每个密码都不值得。为了击败这个新系统,恶意用户决定创建称为Rainbow Tables的查找表。他们无需每次都猜测每个密码,而是只计算一次密码并将其存储在查找表中。

为了减轻 Rainbow Tables 的有效性,鼓励开发人员使用加盐的密码。不仅将密码用作哈希函数的 Importing,还将为每个用户的密码生成随机字节(称为 salt)。盐和用户密码将通过散列函数运行,从而产生唯一的散列。盐将以明文形式与用户密码一起存储。然后,当用户尝试进行身份验证时,会将哈希密码与存储的盐的哈希值和他们键入的密码进行比较。唯一的盐意味着 Rainbow Tables 不再有效,因为每种盐和密码组合的哈希值都不同。

在现代,我们意识到密码哈希(例如 SHA-256)不再安全。原因是使用现代硬件,我们可以每秒执行数十亿次哈希计算。这意味着我们可以轻松地分别破解每个密码。

现在鼓励开发人员利用自适应单向功能来存储密码。具有自适应单向功能的密码验证有意占用大量资源(即 CPU,内存等)。自适应单向功能允许配置“工作因数”,该因数会随着硬件的改进而增加。建议将“工作因数”调整为大约 1 秒钟,以验证系统上的密码。这种权衡使攻击者难以破解密码,但代价却不那么高,这给您自己的系统增加了负担。 Spring Security 试图为“工作因素”提供一个良好的起点,但是鼓励用户为自己的系统自定义“工作因素”,因为不同系统之间的性能会有很大差异。应该使用的自适应单向函数的示例包括bcryptPBKDF2scryptArgon2

由于自适应单向功能有意占用大量资源,因此为每个请求验证用户名和密码都会大大降低应用程序的性能。 Spring Security(或任何其他库)无法执行任何操作来加快密码的验证速度,因为通过增加验证资源的强度来获得安全性。鼓励用户将长期凭证(即用户名和密码)交换为短期凭证(即会话,OAuth 令牌等)。可以快速验证短期凭证,而不会损失任何安全性。

DelegatingPasswordEncoder

在 Spring Security 5.0 之前,默认的PasswordEncoderNoOpPasswordEncoder,它需要纯文本密码。根据Password History部分,您可能希望默认的PasswordEncoder现在类似于BCryptPasswordEncoder。但是,这忽略了三个现实问题:

  • 有许多使用旧密码编码的应用程序无法轻松迁移

  • 密码存储的最佳做法将再次更改。

  • 作为一个框架,Spring Security 不能经常进行重大更改

相反,Spring Security 引入了DelegatingPasswordEncoder,它通过以下方式解决了所有问题:

  • 确保使用当前密码存储建议对密码进行编码

  • 允许以现代和旧式格式验证密码

  • 允许将来升级编码

您可以使用PasswordEncoderFactories轻松构造DelegatingPasswordEncoder的实例。

PasswordEncoder passwordEncoder =
    PasswordEncoderFactories.createDelegatingPasswordEncoder();

或者,您可以创建自己的自定义实例。例如:

String idForEncode = "bcrypt";
Map encoders = new HashMap<>();
encoders.put(idForEncode, new BCryptPasswordEncoder());
encoders.put("noop", NoOpPasswordEncoder.getInstance());
encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
encoders.put("scrypt", new SCryptPasswordEncoder());
encoders.put("sha256", new StandardPasswordEncoder());

PasswordEncoder passwordEncoder =
    new DelegatingPasswordEncoder(idForEncode, encoders);
密码存储格式

密码的一般格式为:

{id}encodedPassword

这样id是用于查找应使用哪个PasswordEncoder的标识符,而encodedPassword是所选PasswordEncoder的原始编码密码。 id必须在密码的开头,以{开头,以}结尾。如果找不到id,则id将为 null。例如,以下可能是使用不同id编码的密码列表。所有原始密码均为“密码”。

{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG (1)
{noop}password (2)
{pbkdf2}5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc (3)
{scrypt}$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc=  (4)
{sha256}97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0 (5)
  • (1) 第一个密码的PasswordEncoder id 为bcrypt,编码的密码为$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG。匹配时将委派给BCryptPasswordEncoder
  • (2) 第二个密码的PasswordEncoder id 为noop,编码的密码为password。匹配时将委派给NoOpPasswordEncoder
  • (3) 第三个密码的PasswordEncoder id 为pbkdf2,编码的密码为5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc。匹配时将委派给Pbkdf2PasswordEncoder
  • (4) 第四个密码的PasswordEncoder id 为scrypt,编码的密码为$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc=。匹配时,它将委派给SCryptPasswordEncoder
  • (5) 最终密码的PasswordEncoder id 为sha256,编码的密码为97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0。匹配时将委派给StandardPasswordEncoder

Note

一些用户可能会担心为潜在的黑客提供了存储格式。不必担心,因为密码的存储不依赖于算法是 Secret。此外,大多数格式很容易让攻击者在没有前缀的情况下弄清楚。例如,BCrypt 密码通常以$2a$开头。

Password Encoding

传递给构造函数的idForEncode确定将使用哪个PasswordEncoder编码密码。在上面构造的DelegatingPasswordEncoder中,这意味着将password编码的结果委托给BCryptPasswordEncoder并以{bcrypt}作为前缀。最终结果如下所示:

{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
Password Matching

匹配是根据{id}和构造器中提供的idPasswordEncoder的 Map 完成的。 名为“密码存储格式”的部分中的示例提供了如何完成此操作的示例。默认情况下,使用密码和未 Map 的id(包括空 ID)调用matches(CharSequence, String)的结果将导致IllegalArgumentException。可以使用DelegatingPasswordEncoder.setDefaultPasswordEncoderForMatches(PasswordEncoder)对此行为进行自定义。

通过使用id,我们可以匹配任何密码编码,但是使用最现代的密码编码对密码进行编码。这很重要,因为与加密不同,密码哈希被设计为没有简单的方法来恢复明文。由于无法恢复明文,因此很难迁移密码。虽然用户迁移NoOpPasswordEncoder很简单,但我们默认情况下选择包含它以使入门经验变得简单。

入门经验

如果您要编写演示或 samples,那么花一些时间来哈希用户密码会很麻烦。有一些方便的机制可以简化此过程,但这仍然不适合生产。

User user = User.withDefaultPasswordEncoder()
  .username("user")
  .password("password")
  .roles("user")
  .build();
System.out.println(user.getPassword());
// {bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG

如果要创建多个用户,则还可以重复使用该构建器。

UserBuilder users = User.withDefaultPasswordEncoder();
User user = users
  .username("user")
  .password("password")
  .roles("USER")
  .build();
User admin = users
  .username("admin")
  .password("password")
  .roles("USER","ADMIN")
  .build();

这会散列存储的密码,但是密码仍在内存和已编译的源代码中公开。因此,对于生产环境,仍不认为它是安全的。为了进行生产,您应该在外部对密码进行哈希处理。

Troubleshooting

当存储的密码之一没有名为“密码存储格式”的部分中所述的 id 时,会发生以下错误。

java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"
    at org.springframework.security.crypto.password.DelegatingPasswordEncoder$UnmappedIdPasswordEncoder.matches(DelegatingPasswordEncoder.java:233)
    at org.springframework.security.crypto.password.DelegatingPasswordEncoder.matches(DelegatingPasswordEncoder.java:196)

解决该错误的最简单方法是切换为显式提供您使用密码进行编码的PasswordEncoder。解决此问题的最简单方法是弄清楚密码的当前存储方式,并明确提供正确的PasswordEncoder。如果您是从 Spring Security 4.2.x 迁移的,则可以通过公开NoOpPasswordEncoder bean 来恢复到以前的行为。例如,如果您使用的是 Java 配置,则可以创建如下所示的配置:

Warning

恢复为NoOpPasswordEncoder被认为是不安全的。您应该改为使用DelegatingPasswordEncoder来支持安全密码编码。

@Bean
public static NoOpPasswordEncoder passwordEncoder() {
    return NoOpPasswordEncoder.getInstance();
}

如果您使用的是 XML 配置,则可以公开 ID 为passwordEncoderPasswordEncoder

<b:bean id="passwordEncoder"
        class="org.springframework.security.crypto.password.NoOpPasswordEncoder" factory-method="getInstance"/>

或者,您可以为所有密码加上正确的 ID 前缀,然后 continue 使用DelegatingPasswordEncoder。例如,如果您使用的是 BCrypt,则可以从以下方式迁移密码:

$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG

to

{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG

有关 Map 的完整列表,请参考PasswordEncoderFactories上的 Javadoc。

BCryptPasswordEncoder

BCryptPasswordEncoder实现使用广泛支持的bcrypt算法对密码进行哈希处理。为了使其更能抵御密码破解,bcrypt 故意降低了速度。与其他自适应单向功能一样,应将其调整为大约 1 秒钟才能验证系统上的密码。

// Create an encoder with strength 16
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(16);
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));

Pbkdf2PasswordEncoder

Pbkdf2PasswordEncoder实现使用PBKDF2算法对密码进行哈希处理。为了克服密码破解问题,PBKDF2 是一种故意缓慢的算法。与其他自适应单向功能一样,应将其调整为大约 1 秒钟才能验证系统上的密码。当需要 FIPS 认证时,此算法是不错的选择。

// Create an encoder with all the defaults
Pbkdf2PasswordEncoder encoder = new Pbkdf2PasswordEncoder();
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));

SCryptPasswordEncoder

SCryptPasswordEncoder实现使用scrypt算法对密码进行哈希处理。为了克服自定义硬件 scrypt 上的密码破解问题,这是一种故意缓慢的算法,需要大量内存。与其他自适应单向功能一样,应将其调整为大约 1 秒钟才能验证系统上的密码。

// Create an encoder with all the defaults
SCryptPasswordEncoder encoder = new SCryptPasswordEncoder();
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));

Other PasswordEncoders

还有很多其他PasswordEncoder实现完全是为了向后兼容而存在的。不推荐使用它们,以表明它们不再被视为安全。但是,由于很难迁移现有的旧系统,因此没有删除它们的计划。

8.2.4 Jackson 支持

Spring Security 已添加了 Jackson 支持来持久化与 Spring Security 相关的类。使用分布式会话(即会话复制,Spring Session 等)时,这可以提高序列化与 Spring Security 相关的类的性能。

要使用它,请将SecurityJackson2Modules.getModules(ClassLoader)注册为Jackson Modules

ObjectMapper mapper = new ObjectMapper();
ClassLoader loader = getClass().getClassLoader();
List<Module> modules = SecurityJackson2Modules.getModules(loader);
mapper.registerModules(modules);

// ... use ObjectMapper as normally ...
SecurityContext context = new SecurityContextImpl();
// ...
String json = mapper.writeValueAsString(context);