Spring Security 常见问题解答

一般问题

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

Spring Security 能否满足我所有应用安全需求?

Spring Security 为您的身份验证和授权需求提供了一个灵活的框架,但构建安全应用程序还有许多其他考虑因素超出了其范围。Web 应用程序容易受到各种攻击,您应该熟悉这些攻击,最好在开始开发之前就熟悉它们,以便您可以从一开始就考虑到它们来设计和编写代码。查看 OWASP 网站 以获取有关 Web 应用程序开发人员面临的主要问题以及您可以使用的应对措施的信息。

为什么不使用 web.xml 安全性?

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

  • 身份验证:Servlet 规范提供了一种身份验证方法。但是,您需要配置容器来执行身份验证,这通常需要编辑特定于容器的“领域”设置。这使得配置不可移植。此外,如果您需要编写一个实际的 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 框架版本?

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 的新手,需要构建一个支持 CAS 单点登录(通过 HTTPS)的应用程序,同时允许对某些 URL 进行本地基本身份验证,并针对多个后端用户信息源(LDAP 和 JDBC)进行身份验证。您已经复制了一些配置文件,但发现它不起作用。可能出了什么问题?

在成功使用这些技术构建应用程序之前,您需要了解您打算使用的技术。安全性很复杂。使用 Spring Security 的命名空间设置一个简单的配置(使用登录表单和一些使用硬编码的用户)相当简单。迁移到使用后端 JDBC 数据库也足够容易。但是,如果您尝试直接跳转到像这种情况这样的复杂部署场景,您几乎肯定会感到沮丧。设置 CAS 等系统、配置 LDAP 服务器和正确安装 SSL 证书所需的学习曲线有很大跳跃。因此,您需要一步一步地进行。

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

常见问题

本节介绍了人们在使用 Spring Security 时遇到的最常见问题。

当我尝试登录时,出现一条错误消息,提示“凭据错误”。出了什么问题?

这意味着身份验证失败。它没有说明原因,因为避免提供可能帮助攻击者猜测帐户名或密码的详细信息是一种良好的做法。

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

当我尝试登录时,我的应用程序进入“无限循环”。发生了什么事?

一个常见的用户问题是无限循环和重定向到登录页面,这是由于意外地将登录页面配置为“受保护”资源造成的。确保您的配置允许匿名访问登录页面,方法是将其从安全过滤器链中排除或将其标记为需要 ROLE_ANONYMOUS

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

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

我收到一条包含消息“访问被拒绝(用户是匿名的)”的异常。出了什么问题?

这是一条调试级别消息,当匿名用户首次尝试访问受保护资源时会发生。

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 请求永远不会被缓存。

我收到一条包含消息“在 SecurityContext 中未找到 Authentication 对象”的异常。出了什么问题?

以下清单显示了当匿名用户首次尝试访问受保护资源时发生的另一条调试级别消息。但是,此清单显示了当您的过滤器链配置中没有 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“绑定”,这是 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 未标记为安全。但是,Spring Security 的会话固定保护可能会干扰此操作,因为它会导致一个新的会话 ID Cookie 发送回用户的浏览器,通常带有安全标志。要解决此问题,您可以禁用会话固定保护。但是,在较新的 Servlet 容器中,您还可以配置会话 Cookie 以永远不使用安全标志。

在 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 令牌或禁用 CSRF 保护(后者不建议使用)。

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

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

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

在 Spring Web 应用程序中,保存调度程序 servlet 的 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。为什么我看不到用户信息?

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

如果您使用 <intercept-url> 元素(它匹配 URL 模式)中的 filters='none' 属性从安全过滤器链中排除了请求,则该请求的 SecurityContextHolder 不会被填充。检查调试日志以查看请求是否正在通过过滤器链。(您正在阅读调试日志,对吧?)

在使用 URL 属性时,authorize JSP 标签不尊重我的方法安全注解。为什么?

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

Spring Security 架构问题

本节介绍 Spring Security 架构的常见问题

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

定位类的最佳方法是在您的 IDE 中安装 Spring Security 源代码。发行版包含项目划分的每个模块的源 jar。将这些添加到您的项目源路径,然后您可以直接导航到 Spring Security 类(在 Eclipse 中为 Ctrl-Shift-T)。这也有助于简化调试,并允许您通过查看发生异常的代码来直接排除异常,以了解那里发生了什么。

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

参考指南的命名空间附录中概述了命名空间创建的 Bean 的一般情况。在blog.springsource.com上还有一篇名为“Spring Security 命名空间背后的故事”的详细博文。如果您想了解完整细节,则代码位于 Spring Security 3.0 发行版中的spring-security-config模块中。您可能应该首先阅读标准 Spring 框架参考文档中关于命名空间解析的章节。

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

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

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

如何知道要将哪些依赖项添加到我的应用程序中才能与 Spring Security 协同工作?

这取决于您正在使用哪些功能以及您正在开发哪种类型的应用程序。在 Spring Security 3.0 中,项目 jar 被划分为明确不同的功能区域,因此根据您的应用程序需求确定需要哪些 Spring Security jar 非常简单。所有应用程序都需要spring-security-corejar。如果您正在开发 Web 应用程序,则需要spring-security-webjar。如果您正在使用安全命名空间配置,则需要spring-security-configjar。对于 LDAP 支持,您需要spring-security-ldapjar。以此类推。

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

如果您使用 Maven 构建项目,则将相应的 Spring Security 模块作为依赖项添加到您的pom.xml文件中会自动引入框架所需的核心 jar。在 Spring Security pom.xml文件中标记为“可选”的任何 jar,如果您需要它们,则必须将其添加到您自己的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 请求数据)?

您无法做到(除非使用线程本地变量),因为提供给接口的唯一信息是用户名。您应该直接实现AuthenticationProvider而不是实现UserDetailsService,并从提供的Authentication令牌中提取信息。

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

如何在 UserDetailsService 中访问 HttpSession?

您无法做到,因为UserDetailsService不了解 servlet API。如果您想存储自定义用户数据,则应自定义返回的UserDetails对象。然后可以通过线程本地SecurityContextHolder在任何时候访问它。调用SecurityContextHolder.getContext().getAuthentication().getPrincipal()会返回此自定义对象。

如果您确实需要访问会话,则必须通过自定义 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,您可以根据架构使用任何合适的 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 的部分对此进行了介绍。请注意,在这种情况下,您不能使用命名空间进行配置。您还应该查阅security-api-url[Javadoc],了解相关的类和接口。

我想修改命名空间创建的 bean 的属性,但架构中没有任何内容支持它。在不放弃使用命名空间的情况下,我该怎么办?

命名空间功能有意被限制,因此它没有涵盖您可以使用普通 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 上调用它。