Spring Security 常见问题解答

一般问题

本常见问题解答回答以下一般问题

Spring Security 能否处理我所有的应用程序安全需求?

Spring Security 为您的认证和授权需求提供了一个灵活的框架,但是构建安全应用程序还需要考虑许多其他超出其范围的问题。Web 应用容易受到各种攻击,您应该熟悉这些攻击,最好在开发开始之前就了解它们,以便从一开始就将它们考虑进设计和编码中。请访问 OWASP 网站,了解 Web 应用程序开发者面临的主要问题以及可用于对抗它们的对策。

为何不使用 web.xml 安全?

假设您正在开发一个基于 Spring 的企业应用程序。您通常需要解决四个安全问题:认证、Web 请求安全、服务层安全(实现业务逻辑的方法)和领域对象实例安全(不同的领域对象可以有不同的权限)。考虑到这些典型需求,我们有以下几点考虑

  • 认证:Servlet 规范提供了一种认证方法。但是,您需要配置容器来执行认证,这通常需要编辑特定于容器的“realm”设置。这会导致配置不可移植。此外,如果您需要编写实际的 Java 类来实现容器的认证接口,则会进一步降低可移植性。使用 Spring Security,您可以实现完全的可移植性,直至 WAR 级别。此外,Spring Security 提供了多种经过生产验证的认证提供者和机制,这意味着您可以在部署时切换认证方法。这对于编写需要在未知目标环境中运行的软件供应商来说尤其有价值。

  • Web 请求安全: Servlet 规范提供了一种保护请求 URI 的方法。但是,这些 URI 只能以 Servlet 规范自身有限的 URI 路径格式表达。Spring Security 提供了一种更全面的方法。例如,您可以使用 Ant 路径或正则表达式,您可以考虑 URI 中除了请求页面以外的部分(例如,您可以考虑 HTTP GET 参数),并且您可以实现自己的运行时配置数据源。这意味着您可以在 Web 应用程序实际执行期间动态更改 Web 请求的安全。

  • 服务层和领域对象安全: Servlet 规范缺乏对服务层安全或领域对象实例安全的支持,这对于多层应用程序来说是严重的限制。通常,开发者要么忽略这些需求,要么在其 MVC 控制器代码中(甚至更糟糕的是,在视图内部)实现安全逻辑。这种方法存在严重的缺点

    • 关注点分离: 授权是一个横切关注点,应该这样实现。在 MVC 控制器或视图中实现授权代码会使得测试控制器和授权逻辑更加困难,更难调试,并且经常导致代码重复。

    • 支持富客户端和 Web 服务: 如果最终需要支持额外的客户端类型,任何嵌入在 Web 层中的授权代码都是不可重用的。应该考虑到 Spring 远程导出器只导出服务层 bean(而不是 MVC 控制器)。因此,授权逻辑需要位于服务层,以支持多种客户端类型。

    • 分层问题: MVC 控制器或视图是实现涉及服务层方法或领域对象实例的授权决策的错误架构层。虽然可以将主体传递给服务层,使其能够做出授权决策,但这会在每个服务层方法上引入一个额外的参数。一种更优雅的方法是使用一个 ThreadLocal 来保存主体,尽管这可能会增加开发时间,使得使用专用的安全框架在成本效益基础上变得更加经济。

    • 授权代码质量: 人们常说 Web 框架“让做正确的事情更容易,让做错误的事情更难”。安全框架也是如此,因为它们以抽象的方式设计,适用于广泛的目的。从头开始编写自己的授权代码无法提供框架提供的“设计检查”,并且内部开发的授权代码通常缺乏通过广泛部署、同行评审和新版本而产生的改进。

对于简单的应用程序,Servlet 规范的安全可能就足够了。然而,当考虑到 Web 容器的可移植性、配置要求、有限的 Web 请求安全灵活性以及服务层和领域对象实例安全性的缺失时,开发者为何经常寻求替代解决方案就变得显而易见了。

需要哪些 Java 和 Spring Framework 版本?

Spring Security 3.0 和 3.1 需要至少 JDK 1.5,并且最低需要 Spring 3.0.3。理想情况下,您应该使用最新的发布版本以避免问题。

Spring Security 2.0.x 要求最低 JDK 版本为 1.4,并基于 Spring 2.0.x 构建。它也应与使用 Spring 2.5.x 的应用程序兼容。

我有一个复杂的场景。可能会有什么问题?

(本答案通过处理一个特定场景来回答一般复杂的场景。)

假设您是 Spring Security 的新手,需要构建一个应用程序,该应用程序支持通过 HTTPS 进行 CAS 单点登录,同时允许对特定 URL 进行本地基本认证,并针对多个后端用户信息源(LDAP 和 JDBC)进行认证。您复制了一些配置文件,但发现它不起作用。可能会有什么问题?

在成功构建使用这些技术的应用程序之前,您需要了解您打算使用的技术。安全很复杂。使用登录表单和 Spring Security 命名空间配置一些硬编码用户来设置一个简单的配置是相当直接的。切换到使用支持 JDBC 数据库也很容易。但是,如果您试图直接跳到像本例这样复杂的部署场景,几乎肯定会感到沮丧。设置 CAS 等系统、配置 LDAP 服务器以及正确安装 SSL 证书所需的学习曲线有很大的跳跃。所以您需要一步一步来。

从 Spring Security 的角度来看,您应该做的第一件事是遵循网站上的“入门”指南。这将引导您完成一系列步骤来启动并运行,并了解框架的运作方式。如果您使用了不熟悉的其他技术,您应该进行一些研究,并尝试确保您可以在独立使用它们之后再在复杂系统中组合它们。

常见问题

本节介绍使用 Spring Security 时最常见的问题

当我尝试登录时,收到错误消息“Bad Credentials”。这是怎么回事?

这意味着认证失败。它没有说明原因,因为不提供可能帮助攻击者猜测账户名或密码的详细信息是一个好的做法。

这也意味着,如果您在线询问这个问题,除非您提供额外信息,否则不应期望得到答案。与任何问题一样,您应该检查调试日志的输出,并注意任何异常堆栈跟踪和相关消息。您应该在调试器中单步执行代码,查看认证在哪里失败以及原因。您还应该编写一个测试用例,在应用程序外部测试您的认证配置。如果您使用哈希密码,请确保存储在数据库中的值与您应用程序中配置的 PasswordEncoder 生成的值完全相同。

当我尝试登录时,我的应用程序进入“死循环”。这是怎么回事?

用户常见的无限循环并重定向到登录页面的问题,通常是由于意外地将登录页面配置为“受保护”资源所致。请确保您的配置允许匿名访问登录页面,可以通过将其排除在安全过滤器链之外,或将其标记为需要 ROLE_ANONYMOUS

如果您的 AccessDecisionManager 包含一个 AuthenticatedVoter,您可以使用 IS_AUTHENTICATED_ANONYMOUSLY 属性。如果您使用标准命名空间配置设置,此属性会自动可用。

从 Spring Security 2.0.1 开始,当您使用基于命名空间的配置时,会在加载应用程序上下文时进行检查,如果您的登录页面看起来受到保护,则会记录警告消息。

我收到一条异常消息:“Access is denied (user is anonymous);”。这是怎么回事?

这是一条调试级别的消息,在匿名用户首次尝试访问受保护资源时出现。

DEBUG [ExceptionTranslationFilter] - Access is denied (user is anonymous); redirecting to authentication entry point
org.springframework.security.AccessDeniedException: Access is denied
at org.springframework.security.vote.AffirmativeBased.decide(AffirmativeBased.java:68)
at org.springframework.security.intercept.AbstractSecurityInterceptor.beforeInvocation(AbstractSecurityInterceptor.java:262)

这是正常的,不必担心。

为什么即使我已从应用程序中注销,仍然可以看到安全页面?

最常见的原因是您的浏览器缓存了该页面,您看到的是从浏览器缓存中检索的副本。通过检查浏览器是否实际发送了请求来验证这一点(检查您的服务器访问日志和调试日志,或使用合适的浏览器调试插件,例如 Firefox 的“Tamper Data”)。这与 Spring Security 无关,您应该配置您的应用程序或服务器来设置适当的 Cache-Control 响应头。请注意,SSL 请求从不被缓存。

我收到一条异常消息:“An Authentication object was not found in the SecurityContext”。这是怎么回事?

以下列表显示了匿名用户首次尝试访问受保护资源时出现的另一条调试级别消息。但是,此列表显示了当您的过滤器链配置中没有 AnonymousAuthenticationFilter 时会发生什么

DEBUG [ExceptionTranslationFilter] - Authentication exception occurred; redirecting to authentication entry point
org.springframework.security.AuthenticationCredentialsNotFoundException:
							An Authentication object was not found in the SecurityContext
at org.springframework.security.intercept.AbstractSecurityInterceptor.credentialsNotFound(AbstractSecurityInterceptor.java:342)
at org.springframework.security.intercept.AbstractSecurityInterceptor.beforeInvocation(AbstractSecurityInterceptor.java:254)

这是正常的,不必担心。

我无法使 LDAP 认证工作。我的配置有什么问题?

请注意,LDAP 目录的权限通常不允许您读取用户的密码。因此,通常无法使用什么是 UserDetailsService 以及我需要一个吗?,在该场景下 Spring Security 会将存储的密码与用户提交的密码进行比较。最常见的方法是使用 LDAP“绑定”(bind),这是 LDAP 协议支持的操作之一。通过这种方法,Spring Security 通过尝试作为用户向目录进行认证来验证密码。

LDAP 认证中最常见的问题是对目录服务器树结构和配置缺乏了解。这因公司而异,所以您必须自己弄清楚。在向应用程序添加 Spring Security LDAP 配置之前,您应该使用标准的 Java LDAP 代码(不涉及 Spring Security)编写一个简单测试,并确保它可以首先工作。例如,要认证用户,您可以使用以下代码

  • Java

  • Kotlin

@Test
public void ldapAuthenticationIsSuccessful() throws Exception {
		Hashtable<String,String> env = new Hashtable<String,String>();
		env.put(Context.SECURITY_AUTHENTICATION, "simple");
		env.put(Context.SECURITY_PRINCIPAL, "cn=joe,ou=users,dc=mycompany,dc=com");
		env.put(Context.PROVIDER_URL, "ldap://mycompany.com:389/dc=mycompany,dc=com");
		env.put(Context.SECURITY_CREDENTIALS, "joespassword");
		env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");

		InitialLdapContext ctx = new InitialLdapContext(env, null);

}
@Test
fun ldapAuthenticationIsSuccessful() {
    val env = Hashtable<String, String>()
    env[Context.SECURITY_AUTHENTICATION] = "simple"
    env[Context.SECURITY_PRINCIPAL] = "cn=joe,ou=users,dc=mycompany,dc=com"
    env[Context.PROVIDER_URL] = "ldap://mycompany.com:389/dc=mycompany,dc=com"
    env[Context.SECURITY_CREDENTIALS] = "joespassword"
    env[Context.INITIAL_CONTEXT_FACTORY] = "com.sun.jndi.ldap.LdapCtxFactory"
    val ctx = InitialLdapContext(env, null)
}

会话管理

会话管理问题是常见的问题来源。如果您正在开发 Java Web 应用程序,您应该了解会话如何在 Servlet 容器和用户浏览器之间维护。您还应该了解安全和非安全 cookie 之间的区别,以及使用 HTTP 和 HTTPS 以及在两者之间切换的影响。Spring Security 与维护会话或提供会话标识符无关。这完全由 Servlet 容器处理。

我正在使用 Spring Security 的并发会话控制来阻止用户同时多次登录。当我登录后打开另一个浏览器窗口时,它并没有阻止我再次登录。为什么我可以登录多次?

浏览器通常每个浏览器实例维护一个会话。您不能同时拥有两个独立的会话。因此,如果您在另一个窗口或标签页中再次登录,您只是在同一个会话中重新认证。服务器不知道关于标签页、窗口或浏览器实例的任何信息。它只看到 HTTP 请求,并根据这些请求中包含的 JSESSIONID cookie 的值将它们与特定会话关联。当用户在会话期间进行认证时,Spring Security 的并发会话控制会检查他们拥有的其他已认证会话的数量。如果他们已经使用同一会话进行了认证,则重新认证没有任何效果。

为什么通过 Spring Security 认证后,会话 ID 会改变?

在默认配置下,Spring Security 会在用户认证时更改会话 ID。如果您使用 Servlet 3.1 或更新的容器,会话 ID 会简单地更改。如果您使用旧版本容器,Spring Security 会使现有会话失效,创建一个新会话,并将会话数据转移到新会话。以这种方式更改会话标识符可以防止“会话固定”攻击。您可以在线或在参考手册中找到更多关于此的信息。

我使用 Tomcat(或其他 Servlet 容器),并为我的登录页面启用了 HTTPS,之后切换回 HTTP。它不工作。认证后我又回到了登录页面。

它不起作用 - 认证后我又回到了登录页面。

发生这种情况是因为在 HTTPS 下创建的会话(其会话 cookie 被标记为“secure”)随后无法在 HTTP 下使用。浏览器不会将 cookie 发送回服务器,并且任何会话状态(包括安全上下文信息)都会丢失。先在 HTTP 中启动会话应该可以工作,因为会话 cookie 未被标记为 secure。然而,Spring Security 的会话固定保护可能会干扰这一点,因为它会导致向用户浏览器发送新的会话 ID cookie,通常带有 secure 标志。要解决这个问题,您可以禁用会话固定保护。然而,在较新的 Servlet 容器中,您还可以配置会话 cookie 从不使用 secure 标志。

通常来说,在 HTTP 和 HTTPS 之间切换不是一个好主意,因为任何使用 HTTP 的应用程序都容易受到中间人攻击。为了真正安全,用户应该开始在 HTTPS 中访问您的网站,并一直使用它直到注销。即使从 HTTP 访问的页面点击 HTTPS 链接也存在潜在风险。如果您需要更多证据,请查看像 sslstrip 这样的工具。

我没有在 HTTP 和 HTTPS 之间切换,但我的会话仍然丢失了。发生了什么?

会话通过交换会话 cookie 或将 jsessionid 参数添加到 URL 来维护(如果您使用 JSTL 输出 URL 或对 URL 调用 HttpServletResponse.encodeUrl(例如在重定向之前),这会自动发生)。如果客户端禁用了 cookie,并且您没有重写 URL 以包含 jsessionid,则会话会丢失。请注意,出于安全原因,推荐使用 cookie,因为它不会在 URL 中暴露会话信息。

我正在尝试使用并发会话控制支持,但即使我确定已经注销并且没有超出允许的会话数,它也不让我重新登录。这是怎么回事?

确保您已将监听器添加到您的 web.xml 文件中。至关重要的是确保在会话销毁时通知 Spring Security 会话注册表。否则,会话信息不会从注册表中移除。以下示例在 web.xml 文件中添加了一个监听器

<listener>
		<listener-class>org.springframework.security.web.session.HttpSessionEventPublisher</listener-class>
</listener>

Spring Security 在某个地方创建了一个会话,即使我已通过将 create-session 属性设置为 never 来配置不创建会话。这是怎么回事?

这通常意味着用户的应用程序在某个地方创建了一个会话,而他们没有意识到。最常见的罪魁祸首是 JSP。很多人不知道 JSP 默认会创建会话。为了阻止 JSP 创建会话,请在页面顶部添加 <%@ page session="false" %> 指令。

如果您无法确定在哪里创建了会话,您可以添加一些调试代码来追踪位置。一种方法是向您的应用程序添加一个 javax.servlet.http.HttpSessionListener,它在 sessionCreated 方法中调用 Thread.dumpStack()

执行 POST 请求时收到 403 Forbidden。这是怎么回事?

如果对 HTTP POST 请求返回 HTTP 403 Forbidden 错误,但对 HTTP GET 请求有效,则问题很可能与 CSRF 相关。请提供 CSRF Token 或禁用 CSRF 保护(不推荐后者)。

我正在使用 RequestDispatcher 将请求转发到另一个 URL,但我的安全约束没有被应用。

默认情况下,过滤器不应用于转发或包含。如果您确实希望安全过滤器应用于转发或包含,则必须在 web.xml 文件中使用 <dispatcher> 元素(它是 <filter-mapping> 元素的子元素)显式配置它们。

我已将 Spring Security 的 <global-method-security> 元素添加到我的应用程序上下文中,但是,如果我将安全注解添加到我的 Spring MVC 控制器 bean(Struts action 等)中,它们似乎没有效果。为什么?

在 Spring Web 应用程序中,为 DispatcherServlet 持有 Spring MVC Bean 的应用程序上下文通常与主应用程序上下文是分开的。它通常定义在一个名为 myapp-servlet.xml 的文件中,其中 myapp 是在 web.xml 文件中分配给 Spring DispatcherServlet 的名称。一个应用程序可以有多个 DispatcherServlet 实例,每个实例都有自己独立的应用程序上下文。这些“子”上下文中的 Bean 对应用程序的其余部分是不可见的。“父”应用程序上下文是由你在 web.xml 文件中定义的 ContextLoaderListener 加载的,并且对所有子上下文可见。这个父上下文通常是你定义安全配置的地方,包括 <global-method-security> 元素。因此,应用于这些 Web Bean 方法的任何安全约束都不会被强制执行,因为这些 Bean 从 DispatcherServlet 上下文中是不可见的。你需要将 <global-method-security> 声明移到 Web 上下文,或者将你想保护的 Bean 移到主应用程序上下文中。

通常,我们建议在服务层而不是在单个 Web 控制器上应用方法安全。

我有一个用户已经确定认证通过,但是当我尝试在某些请求期间访问 SecurityContextHolder 时,Authentication 是 null。为什么我看不到用户信息?

为什么我看不到用户信息?

如果你通过在匹配 URL 模式的 <intercept-url> 元素中使用 filters='none' 属性将请求排除在安全过滤器链之外,那么 SecurityContextHolder 不会为该请求填充信息。检查调试日志,看看请求是否通过了过滤器链。(你在阅读调试日志,对吧?)

当使用 URL 属性时,authorize JSP Tag 不会遵守我的方法安全注解。为什么?

当在 <sec:authorize> 中使用 url 属性时,方法安全不会隐藏链接,因为我们无法轻易地反向工程哪个 URL 映射到哪个控制器端点。我们受限,因为控制器可能依赖于请求头、当前用户和其他细节来确定要调用哪个方法。

Spring Security 架构问题

本节回答常见的 Spring Security 架构问题

我如何知道类 X 在哪个包中?

定位类的最佳方法是在你的 IDE 中安装 Spring Security 源代码。发行版包含项目划分的每个模块的源代码 JAR 包。将它们添加到你的项目源路径中,然后你可以直接导航到 Spring Security 类(在 Eclipse 中使用 Ctrl-Shift-T)。这也使得调试更容易,并允许你通过直接查看代码发生异常的地方来排查异常,以了解发生了什么。

命名空间元素如何映射到传统的 Bean 配置?

在参考指南的命名空间附录中,对命名空间创建了哪些 Bean 有一个总体概述。在 blog.springsource.com 上还有一篇详细的博客文章,名为“Behind the Spring Security Namespace”。如果你想了解全部细节,代码位于 Spring Security 3.0 发行版中的 spring-security-config 模块中。你应该首先阅读标准 Spring Framework 参考文档中关于命名空间解析的章节。

“ROLE_”是什么意思,为什么我的角色名需要它?

Spring Security 具有基于投票者的架构,这意味着访问决策由一系列 AccessDecisionVoter 实例做出。投票者作用于“配置属性”,这些属性是为受保护的资源(例如方法调用)指定的。通过这种方法,并非所有属性都与所有投票者相关,并且投票者需要知道何时应该忽略某个属性(弃权)以及何时应该根据属性值投票授予或拒绝访问。最常见的投票者是 RoleVoter,它默认情况下在找到带有 ROLE_ 前缀的属性时投票。它将属性(例如 ROLE_USER)与当前用户已被分配的权限名称进行简单比较。如果找到匹配项(他们有一个名为 ROLE_USER 的权限),它就投票授予访问权。否则,它就投票拒绝访问。

你可以通过设置 RoleVoterrolePrefix 属性来更改前缀。如果你的应用程序只需要使用角色,并且不需要其他自定义投票者,你可以将前缀设置为空字符串。在这种情况下,RoleVoter 将所有属性视为角色。

我如何知道需要向我的应用程序添加哪些依赖才能使用 Spring Security?

这取决于你使用的功能和正在开发的应用程序类型。Spring Security 3.0 将项目 JAR 包划分为功能清晰不同的区域,因此很容易根据你的应用程序需求确定需要哪些 Spring Security JAR 包。所有应用程序都需要 spring-security-core JAR 包。如果你正在开发 Web 应用程序,则需要 spring-security-web JAR 包。如果你使用安全命名空间配置,则需要 spring-security-config JAR 包。对于 LDAP 支持,你需要 spring-security-ldap JAR 包,依此类推。

对于第三方 JAR 包,情况并非总是如此明显。一个好的起点是从预构建的样本应用程序的 WEB-INF/lib 目录中复制这些 JAR 包。对于一个基本应用程序,你可以从教程样本开始。对于一个基本应用程序,你可以从教程样本开始。如果你想使用嵌入式测试服务器的 LDAP,请使用 LDAP 样本作为起点。参考手册还包含一个 附录,列出了每个 Spring Security 模块的第一级依赖,并提供了一些关于它们是否是可选的以及何时需要的信息。

如果你使用 Maven 构建项目,将适当的 Spring Security 模块作为依赖添加到你的 pom.xml 文件中会自动拉取框架所需的核心 JAR 包。在 Spring Security 的 pom.xml 文件中标记为“optional”的任何依赖,如果你需要它们,则必须添加到你自己的 pom.xml 文件中。

运行嵌入式 ApacheDS LDAP 服务器需要哪些依赖?

如果你使用 Maven,你需要在你的 pom.xml 文件依赖项中添加以下内容

<dependency>
		<groupId>org.apache.directory.server</groupId>
		<artifactId>apacheds-core</artifactId>
		<version>1.5.5</version>
		<scope>runtime</scope>
</dependency>
<dependency>
		<groupId>org.apache.directory.server</groupId>
		<artifactId>apacheds-server-jndi</artifactId>
		<version>1.5.5</version>
		<scope>runtime</scope>
</dependency>

其他所需的 JAR 包应该通过传递依赖拉取。

什么是 UserDetailsService,我需要一个吗?

UserDetailsService 是一个用于加载特定于用户账户的数据的 DAO 接口。它除了加载这些数据供框架内的其他组件使用之外没有其他功能。它不负责认证用户。使用用户名和密码组合认证用户通常由 DaoAuthenticationProvider 执行,它被注入 UserDetailsService 以便加载用户的密码(和其他数据),并将其与提交的值进行比较。请注意,如果你使用 LDAP,这种方法可能不起作用

如果你想定制认证过程,你应该自己实现 AuthenticationProvider。请参阅这篇 博客文章,其中提供了一个将 Spring Security 认证与 Google App Engine 集成的示例。

常见操作指南问题

本节回答关于 Spring Security 的常见操作指南问题

我需要使用比用户名更多的信息进行登录。如何添加对额外登录字段(例如公司名称)的支持?

这个问题反复出现,因此你可以在线搜索找到更多信息。

提交的登录信息由 UsernamePasswordAuthenticationFilter 的实例处理。你需要定制此类以处理额外的字段。一种选择是使用你自己的自定义认证令牌类(而不是标准的 UsernamePasswordAuthenticationToken)。另一种选择是将额外字段与用户名连接起来(例如,使用 : 字符作为分隔符),并将它们作为 UsernamePasswordAuthenticationToken 的用户名属性传递。

你还需要定制实际的认证过程。例如,如果你使用自定义认证令牌类,则必须编写一个 AuthenticationProvider(或扩展标准的 DaoAuthenticationProvider)来处理它。如果你连接了字段,你可以实现你自己的 UserDetailsService 来将它们分开并加载适当的用户数据进行认证。

如何应用不同的拦截-URL 约束,其中只有请求 URL 的片段值不同(例如 /thing1#thing2 和 /thing1#thing3)?

你无法做到这一点,因为片段不会从浏览器传输到服务器。从服务器的角度来看,URL 是相同的。这是 GWT 用户常问的问题。

如何在 UserDetailsService 中访问用户的 IP 地址(或其他 Web 请求数据)?

你无法做到(除非借助于线程局部变量等),因为提供给接口的信息只有用户名。与其实现 UserDetailsService,不如直接实现 AuthenticationProvider 并从提供的 Authentication 令牌中提取信息。

在标准的 Web 设置中,Authentication 对象的 getDetails() 方法将返回一个 WebAuthenticationDetails 实例。如果你需要额外信息,可以将自定义的 AuthenticationDetailsSource 注入到你正在使用的认证过滤器中。例如,如果你正在使用命名空间,例如带有 <form-login> 元素,那么你应该移除此元素,并将其替换为指向显式配置的 UsernamePasswordAuthenticationFilter<custom-filter> 声明。

如何从 UserDetailsService 中访问 HttpSession?

你无法做到,因为 UserDetailsService 对 servlet API 一无所知。如果你想存储自定义用户数据,你应该定制返回的 UserDetails 对象。然后可以通过线程局部变量 SecurityContextHolder 在任何时候访问它。调用 SecurityContextHolder.getContext().getAuthentication().getPrincipal() 会返回这个自定义对象。

如果你确实需要访问 session,你必须通过定制 Web 层来实现。

如何在 UserDetailsService 中访问用户的密码?

你无法做到(而且不应该尝试,即使你找到了方法)。你可能误解了它的目的。请参阅 FAQ 中早先的 "什么是 UserDetailsService?"。

如何在应用程序中动态定义受保护的 URL?

人们经常询问如何将受保护 URL 和安全元数据属性之间的映射存储在数据库而不是应用程序上下文中。

你应该首先问自己是否真的需要这样做。如果一个应用程序需要安全,它还需要基于明确的策略进行彻底的安全测试。在部署到生产环境之前,可能需要进行审计和验收测试。具有安全意识的组织应该意识到,他们认真测试过程的好处可能会因允许通过更改配置数据库中的一两行来在运行时修改安全设置而瞬间消失。如果你已经考虑了这一点(也许通过在应用程序中使用多层安全),Spring Security 允许你完全定制安全元数据的来源。如果选择,你可以使其完全动态化。

方法安全和 Web 安全都由 AbstractSecurityInterceptor 的子类保护,它配置有一个 SecurityMetadataSource,从中获取特定方法或过滤器调用的元数据。对于 Web 安全,拦截器类是 FilterSecurityInterceptor,它使用 FilterInvocationSecurityMetadataSource 标记接口。它操作的“受保护对象”类型是 FilterInvocation。默认实现(在命名空间 <http> 和显式配置拦截器时都使用)将 URL 模式列表及其对应的“配置属性”列表(ConfigAttribute 实例)存储在内存映射中。

要从替代来源加载数据,你必须使用显式声明的安全过滤器链(通常是 Spring Security 的 FilterChainProxy)来定制 FilterSecurityInterceptor Bean。你不能使用命名空间。然后,你需要实现 FilterInvocationSecurityMetadataSource 以便为你希望的特定 FilterInvocation 加载数据。FilterInvocation 对象包含 HttpServletRequest,因此你可以获取 URL 或任何其他相关信息,以便根据返回的属性列表做出决策。基本轮廓可能看起来像以下示例

  • Java

  • Kotlin

	public class MyFilterSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {

		public List<ConfigAttribute> getAttributes(Object object) {
			FilterInvocation fi = (FilterInvocation) object;
				String url = fi.getRequestUrl();
				String httpMethod = fi.getRequest().getMethod();
				List<ConfigAttribute> attributes = new ArrayList<ConfigAttribute>();

				// Lookup your database (or other source) using this information and populate the
				// list of attributes

				return attributes;
		}

		public Collection<ConfigAttribute> getAllConfigAttributes() {
			return null;
		}

		public boolean supports(Class<?> clazz) {
			return FilterInvocation.class.isAssignableFrom(clazz);
		}
	}
class MyFilterSecurityMetadataSource : FilterInvocationSecurityMetadataSource {
    override fun getAttributes(securedObject: Any): List<ConfigAttribute> {
        val fi = securedObject as FilterInvocation
        val url = fi.requestUrl
        val httpMethod = fi.request.method

        // Lookup your database (or other source) using this information and populate the
        // list of attributes
        return ArrayList()
    }

    override fun getAllConfigAttributes(): Collection<ConfigAttribute>? {
        return null
    }

    override fun supports(clazz: Class<*>): Boolean {
        return FilterInvocation::class.java.isAssignableFrom(clazz)
    }
}

有关更多信息,请查看 DefaultFilterInvocationSecurityMetadataSource 的代码。

如何针对 LDAP 进行认证,但从数据库加载用户角色?

LdapAuthenticationProvider Bean(在 Spring Security 中处理正常的 LDAP 认证)配置有两个独立的策略接口,一个执行认证,一个加载用户权限,分别称为 LdapAuthenticatorLdapAuthoritiesPopulatorDefaultLdapAuthoritiesPopulator 从 LDAP 目录加载用户权限,并具有各种配置参数,让你指定如何检索这些权限。

要改用 JDBC,你可以根据你的 schema 实现接口,使用适当的 SQL

  • Java

  • Kotlin

public class MyAuthoritiesPopulator implements LdapAuthoritiesPopulator {
    @Autowired
    JdbcTemplate template;

    List<GrantedAuthority> getGrantedAuthorities(DirContextOperations userData, String username) {
        return template.query("select role from roles where username = ?",
                new String[] {username},
                new RowMapper<GrantedAuthority>() {
             /**
             *  We're assuming here that you're using the standard convention of using the role
             *  prefix "ROLE_" to mark attributes which are supported by Spring Security's RoleVoter.
             */
            @Override
            public GrantedAuthority mapRow(ResultSet rs, int rowNum) throws SQLException {
                return new SimpleGrantedAuthority("ROLE_" + rs.getString(1));
            }
        });
    }
}
class MyAuthoritiesPopulator : LdapAuthoritiesPopulator {
    @Autowired
    lateinit var template: JdbcTemplate

    override fun getGrantedAuthorities(userData: DirContextOperations, username: String): MutableList<GrantedAuthority?> {
        return template.query("select role from roles where username = ?",
            arrayOf(username)
        ) { rs, _ ->
            /**
             * We're assuming here that you're using the standard convention of using the role
             * prefix "ROLE_" to mark attributes which are supported by Spring Security's RoleVoter.
             */
            SimpleGrantedAuthority("ROLE_" + rs.getString(1))
        }
    }
}

然后,你需要将此类型的 Bean 添加到你的应用程序上下文,并将其注入到 LdapAuthenticationProvider 中。参考手册 LDAP 章中关于使用显式 Spring Bean 配置 LDAP 的部分对此进行了介绍。请注意,在这种情况下,你不能使用命名空间进行配置。你还应该查阅相关类和接口的 Javadoc

我想修改由命名空间创建的 Bean 的属性,但在 schema 中没有支持。除了放弃使用命名空间,我还能做什么?

命名空间功能故意被限制,因此它不包含所有你可以用普通 Bean 完成的操作。如果你想做一些简单的事情,例如修改 Bean 或注入不同的依赖,可以通过向配置添加 BeanPostProcessor 来实现。你可以在 Spring 参考手册 中找到更多信息。为此,你需要了解一些关于创建了哪些 Bean 的信息,因此你也应该阅读早先关于 命名空间如何映射到 Spring Bean 的问题中提到的博客文章。

通常,你会将所需的功能添加到 BeanPostProcessorpostProcessBeforeInitialization 方法中。假设你想定制 UsernamePasswordAuthenticationFilter(由 form-login 元素创建)使用的 AuthenticationDetailsSource。你想从请求中提取名为 CUSTOM_HEADER 的特定请求头,并在认证用户时使用它。处理器类将如下所示

  • Java

  • Kotlin

public class CustomBeanPostProcessor implements BeanPostProcessor {

		public Object postProcessAfterInitialization(Object bean, String name) {
				if (bean instanceof UsernamePasswordAuthenticationFilter) {
						System.out.println("********* Post-processing " + name);
						((UsernamePasswordAuthenticationFilter)bean).setAuthenticationDetailsSource(
										new AuthenticationDetailsSource() {
												public Object buildDetails(Object context) {
														return ((HttpServletRequest)context).getHeader("CUSTOM_HEADER");
												}
										});
				}
				return bean;
		}

		public Object postProcessBeforeInitialization(Object bean, String name) {
				return bean;
		}
}
class CustomBeanPostProcessor : BeanPostProcessor {
    override fun postProcessAfterInitialization(bean: Any, name: String): Any {
        if (bean is UsernamePasswordAuthenticationFilter) {
            println("********* Post-processing $name")
            bean.setAuthenticationDetailsSource(
                AuthenticationDetailsSource<HttpServletRequest, Any?> { context -> context.getHeader("CUSTOM_HEADER") })
        }
        return bean
    }

    override fun postProcessBeforeInitialization(bean: Any, name: String?): Any {
        return bean
    }
}

然后,你需要在应用程序上下文中注册此 Bean。Spring 会自动在应用程序上下文中定义的 Bean 上调用它。