Spring Security 常见问题解答

本站(springdoc.cn)中的内容来源于 spring.io ,原始版权归属于 spring.io。由 springdoc.cn 进行翻译,整理。可供个人学习、研究,未经许可,不得进行任何转载、商用或与之相关的行为。 商标声明:Spring 是 Pivotal Software, Inc. 在美国以及其他国家的商标。

该常见问题有以下几个部分。

普通问题

该常见问题回答了以下一般问题。

Spring Security能满足我所有的应用安全要求吗?

Spring Security为你提供了一个灵活的框架来满足你的认证和授权要求,但在构建一个安全的应用程序时,还有许多其他的考虑因素在其范围之外。Web应用程序容易受到各种攻击,你应该熟悉这些攻击,最好是在开始开发之前,这样你在设计和编码时就可以从一开始就考虑到这些攻击。请查看 OWASP web site,了解网络应用程序开发者面临的主要问题以及你可以用来对付它们的对策。

为什么不使用 web.xml Security?

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

  • Authentication: servlet规范提供了一种认证的方法。然而,你需要配置容器来执行认证,这通常需要编辑容器特定的 “realm” 设置。这就构成了一种不可移植的配置。另外,如果你需要编写一个实际的Java类来实现容器的认证接口,就会变得更加不可移植。使用Spring Security,你可以实现完全的可移植性—​直到WAR级别。此外,Spring Security还提供了经过生产验证的认证提供者和机制的选择,这意味着你可以在部署时切换你的认证方法。这对于编写需要在未知目标环境中工作的产品的软件供应商来说特别有价值。

  • Web request security: servlet规范提供了一种方法来保护你的请求URI。然而,这些URI只能用servlet规范自己的有限URI路径格式来表达。Spring Security提供了一种更全面的方法。例如,你可以使用Ant路径或正则表达式,你可以考虑URI的其他部分,而不仅仅是被请求的页面(例如,你可以考虑HTTP GET参数),你还可以实现自己的运行时配置数据源。这意味着你可以在你的Web应用的实际执行过程中动态地改变你的Web请求安全。

  • Service layer 和 domain object security: 在servlet规范中没有对服务层安全或域对象实例安全的支持,这对多层应用程序来说是严重的限制。通常情况下,开发者要么忽略这些要求,要么在他们的MVC控制器代码中实现安全逻辑(甚至更糟,在视图(View)中)。这种方法有很大的缺点。

    • 分离关注: 授权是一个跨领域的问题,应该这样来实现。实现授权代码的MVC控制器或视图使得测试控制器和授权逻辑都更加困难,更难调试,而且经常导致代码重复。

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

    • 分层问题: MVC控制器或视图是一个不正确的架构层,在其中实现有关服务层方法或域对象实例的授权决定。虽然可以将本金传递给服务层以使其做出授权决定,但这样做会在每个服务层方法上引入一个额外的参数。一个更优雅的方法是使用 ThreadLocal 来保存principal,尽管这可能会增加开发时间,以至于使用专门的安全框架会变得更经济(在成本效益的基础上)。

    • 授权代码的质量: 人们常说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进行本地基本认证(basic authentication),针对多个后端用户信息源(LDAP和JDBC)进行认证。你已经复制了一些配置文件,但发现它并不工作。可能有什么问题?

你需要了解你打算使用的技术,然后才能成功地用它们构建应用程序。安全问题很复杂。通过使用一个登录表单和一些硬编码的用户与Spring Security的命名空间来设置一个简单的配置是相当直接的。转而使用一个有后台的JDBC数据库也很容易。然而,如果你试图直接跳到像这种复杂的部署场景,你几乎肯定会感到沮丧。设置CAS等系统、配置LDAP服务器和正确安装SSL证书所需的学习曲线有一个很大的跳跃。所以你需要一步一步来。

从 Spring Security 的角度来看,你应该做的第一件事是遵循网站上的 "入门" 指南。这将带领你通过一系列的步骤来启动和运行,并对该框架的运作有一些了解。如果你使用你不熟悉的其他技术,你应该做一些研究,在把它们组合到一个复杂的系统中之前,尽量确保你可以独立地使用它们。

常见的问题

本节讨论人们在使用Spring Security时遇到的最常见的问题。

当我试图登录时,我得到一个错误信息,说 “Bad Credentials”。什么是错的?

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

这也意味着,如果你在网上问这个问题,除非你提供额外的信息,否则你不应该期望得到答案。与任何问题一样,你应该检查调试日志的输出,并注意任何异常堆栈跟踪和相关信息。你应该在调试器中浏览代码,看看哪里认证失败了,为什么?你还应该写一个测试用例,在应用程序之外演练你的认证配置。如果你使用哈希密码,请确保存储在数据库中的值与你应用程序中配置的 PasswordEncoder 产生的值完全相同。

当我试图登录时,我的应用程序进入了一个 "死循环"。这到底是怎么回事?

一个常见的用户问题是死循环和重定向到登录页面,这是因为不小心将登录页面配置为 “secured” 资源。确保你的配置允许匿名访问登录页面,可以从安全过滤器链中排除它,或者将它标记为需要 ROLE_ANONYMOUS

如果你的 AccessDecisionManager 包括一个 AuthenticatedVoter,你可以使用 IS_AUTHENTICATED_ANONYMOUSLY 属性。如果你使用标准命名空间的配置设置,这将自动可用。

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

我得到一个异常,信息是 "Access is denied (user is anonymous);"。这是怎么回事?

这是一个 debug 级别的信息,在匿名用户第一次尝试访问受保护的资源时发生。

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"。这是怎么回事?

下面的列表显示了另一个debug级别的消息,它在匿名用户第一次尝试访问受保护的资源时发生。然而,这个列表显示了当你的过滤器链配置中没有 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 authentication 不能正常工作。我的配置有什么问题?

请注意,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)
}

Session 管理

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

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

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

为什么当我通过Spring Security进行认证时,Session ID会发生变化?

在默认配置下,Spring Security 会在用户验证时改变会话ID。如果你使用的是Servlet 3.1或更新的容器,会话ID就会被简单地改变。如果你使用旧的容器,Spring Security 会使现有的会话无效,创建一个新的会话,并将会话数据转移到新的会话中。以这种方式改变会话标识符可以防止 “session-fixation” 攻击。你可以在网上和参考手册中找到更多关于这方面的信息。

我使用Tomcat(或其他一些servlet容器),并为我的登录页面启用了HTTPS,之后再切换到HTTP。这并不奏效。我最终还是回到了认证后的登录页面。

这是因为在HTTPS下创建的会话,如果会话cookie被标记为 “secure”,,随后就不能在HTTP下使用。浏览器不会将cookie送回服务器,而任何会话状态(包括安全上下文信息)都会丢失。首先在HTTP下开始一个会话应该是可行的,因为会话cookie没有被标记为安全。然而,Spring Security的 Session Fixation Protection 会干扰这一点,因为它导致一个新的会话ID cookie被送回用户的浏览器,通常带有 “secure” 标志。为了解决这个问题,你可以禁用会话固定保护。然而,在较新的Servlet容器中,你也可以将会话cookie配置为从不使用 “secure” 标志。

一般来说,在HTTP和HTTPS之间切换不是一个好主意,因为任何使用HTTP的应用程序都容易受到中间人攻击。为了真正的安全,用户应该以HTTPS开始访问你的网站,并继续使用,直到他们退出。即使从一个通过HTTP访问的页面点击HTTPS链接也有潜在的风险。如果你需要更多的说服力,请查看像 sslstrip 这样的工具。

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

会话是通过交换会话cookie或在URLs中添加 jsessionid 参数来维护的(如果你使用JSTL来输出URLs,或者你在URLs上调用 HttpServletResponse.encodeUrl(例如在重定向之前),这会自动发生。如果客户端禁用了cookie,而你没有重写URL以包括 jsessionid,那么会话就会丢失。请注意,出于安全考虑,使用cookies是首选,因为它不会在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,但我的安全约束没有被应用。

默认情况下,filter 不应用于 forward 或 include。如果你真的想让 security filter 应用于 forward 或 include。如果你真的想让,你必须在你的 web.xml 文件中通过使用 <dispatcher> 元素明确地配置这些filter,它是 <filter-mapping> 元素的一个子元素。

我已经把 Spring Security 的 <global-method-security> 元素添加到我的 application context 中,但是,如果我把security注解添加到我的Spring MVC Controller Bean(Struts Action等),它们似乎没有效果。这是为什么?

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

一般来说,我们建议在 service 层应用方法安全,而不是在单个 web controller 上。

我有一个肯定已被认证的用户,但是,当我在一些请求中试图访问 SecurityContextHolder 时,Authentication 为空。为什么我看不到用户的信息?

如果你在匹配 URL pattern 的 <intercept-url> 元素中使用 filters='none' 属性,将请求从 security filter 链中排除,那么 SecurityContextHolder 就不会为该请求进行填充。检查调试日志,看看该请求是否通过了过滤器链。(你在看调试日志,对吗?)

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

当使用 <sec:authorize> 中的 url 属性时,方法安全并没有隐藏链接,因为我们不能轻易地逆向设计什么URL映射到什么 controller 端点。我们受到了限制,因为 controller 可以依靠头header、当前用户和其他细节来决定调用什么方法。

Spring Security 架构问题

本节讨论常见的 Spring Security 架构问题。

我怎样才能知道某个类在哪个包里?

定位类的最好方法是在你的 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_" 是什么意思,为什么我的角色(Role)名称上需要它?

Spring Security 有一个基于投票者的架构,这意味着访问决定是由一系列的 AccessDecisionVoter 实例做出的。投票者对 “configuration attributes” 采取行动,这些属性是为安全资源(如方法调用)指定的。通过这种方法,并非所有的属性都可能与所有的投票者有关,投票者需要知道什么时候应该忽略一个属性(弃权),什么时候应该根据属性值来投票授予或拒绝访问。最常见的投票者是 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。如果你正在使用 security 命名空间配置,你需要 spring-security-config jar。对于LDAP支持,你需要 spring-security-ldap jar。以此类推。

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

如果你用Maven构建项目,在 pom.xml 文件中添加适当 的Spring Security 模块作为依赖项,会自动拉入框架所需的核心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 来分割它们,并为认证加载适当的用户数据。

我如何应用不同的 intercept-url 约束,其中只有请求的URL的fragment不同(如 /thing1#thing2 和 /thing1#thing3)?

你不能这样做,因为这个片段没有从浏览器传输到服务器。从服务器的角度来看,这些URL是相同的。这是GWT用户的一个普遍问题。

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

你不能这样做(如果不求助于类似线程本地变量(thread-local)的东西),因为提供给接口的唯一信息是用户名。你应该直接实现 AuthenticationProvider 并从提供的 Authentication token中提取信息,而不是实现 UserDetailsService

在标准的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?"

如何在一个应用程序中动态地定义 secured URL?

人们经常问及如何将secured URL和 security 元数据属性之间的映射存储在数据库中,而不是在 application context 中。

你应该问自己的第一件事是你是否真的需要这样做。如果一个应用程序需要安全,它还需要根据定义的政策对安全进行彻底的测试。在推广到生产环境之前,它可能需要进行审计和验收测试。一个有安全意识的组织应该意识到,如果让安全设置在运行时通过改变配置数据库中的一两行而被修改,那么他们勤奋的测试过程所带来的好处可能会立即被抹去。如果你已经考虑到了这一点(也许是通过在你的应用程序中使用多层安全),Spring Security 可以让你完全定制安全元数据的来源。如果你选择的话,你可以让它成为完全动态的。

方法和web安全都是由 AbstractSecurityInterceptor 的子类来保护的,它被配置了一个 SecurityMetadataSource,从中获取特定方法或过滤器调用的元数据。对于web安全,拦截器类是 FilterSecurityInterceptor,它使用 FilterInvocationSecurityMetadataSource 标记接口。它所操作的 “secured object” 类型是一个 FilterInvocation。默认的实现(在命名空间 <http> 和显式配置拦截器时都会使用)将URL pattern列表和它们相应的 “configuration attributes” 列表(ConfigAttribute 的实例)存储在一个内存Map中。

要从其他来源加载数据,你必须使用一个明确声明的 security 过滤器链(通常是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))
        }
    }
}

然后你可以在你的 application context 中添加这种类型的Bean,并将其注入 LdapAuthenticationProvider。这在参考手册的LDAP章节中关于通过使用显式Spring Bean配置LDAP的章节中有所涉及。注意,在这种情况下你不能使用命名空间进行配置。你还应该查阅 Javadoc 中的相关类和接口。

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

命名空间的功能是有意限制的,所以它并不包括你可以用普通Bean做的所有事情。如果你想做一些简单的事情,比如修改Bean或注入不同的依赖关系,你可以通过在配置中添加 BeanPostProcessor 来实现。你可以在 《Spring参考手册》中找到更多信息。要做到这一点,你需要知道一些关于创建哪些Bean的信息,所以你还应该阅读前面问题中提到的关于 命名空间如何映射到Spring Bean的博客文章。

通常,你会把你需要的功能添加到 BeanPostProcessorpostProcessBeforeInitialization 方法中。假设你想定制 UsernamePasswordAuthenticationFilter(由 form-login 元素创建)所使用的 AuthenticationDetailsSource。你想从请求中提取一个叫做 CUSTOM_HEADER 的特殊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
    }
}

然后你将在你的 application context 注册这个Bean。Spring 会自动在 application context 中定义的Bean上调用它。