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 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 开始,当你使用基于命名空间的配置时,在加载应用程序上下文时会进行检查,如果登录页面似乎受到保护,则会记录一条警告消息。
我收到一个异常,提示“访问被拒绝(用户是匿名的);”。这是怎么回事?
这是一条调试级别的消息,当匿名用户首次尝试访问受保护的资源时会出现。
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 被标记为“安全”,因此不能在 HTTP 下使用。浏览器不会将 cookie 发送回服务器,任何会话状态(包括安全上下文信息)都会丢失。首先在 HTTP 中启动会话应该可以正常工作,因为会话 cookie 没有被标记为安全。然而,Spring Security 的 Session Fixation Protection 可能会干扰这一点,因为它会导致一个新的会话 ID cookie 被发送回用户的浏览器,通常带有安全标志。为了解决这个问题,你可以禁用会话固定保护。然而,在较新的 Servlet 容器中,你也可以配置会话 cookie 以从不使用安全标志。
在 HTTP 和 HTTPS 之间切换通常不是一个好主意,因为任何使用 HTTP 的应用程序都容易受到中间人攻击。为了真正安全,用户应该从 HTTPS 开始访问您的网站,并一直使用它直到他们注销。即使从通过 HTTP 访问的页面点击 HTTPS 链接也可能是有风险的。如果您需要更多的说服力,请查看像 sslstrip 这样的工具。
我没有在HTTP和HTTPS之间切换,但我的会话仍然丢失了。发生了什么?
会话通过交换会话 cookie 或在 URL 中添加 jsessionid
参数来维护(如果您使用 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>
即使我已将 create-session 属性设置为 never,Spring Security 仍然在某处创建了会话。这是什么问题?
这通常意味着用户的应用程序在某个地方创建了一个会话,但他们并没有意识到这一点。最常见的罪魁祸首是 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 操作等)中添加安全注解,它们似乎没有效果。为什么?
在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 是空的。为什么我看不到用户信息?
为什么我看不到用户信息?
如果你在匹配 URL 模式的 <intercept-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 有一个总体概述。还有一篇详细的博客文章《Spring Security 命名空间的背后》发布在 blog.springsource.com 上。如果你想了解全部细节,那么代码位于 Spring Security 3.0 发行版中的 spring-security-config
模块内。你可能应该先阅读标准 Spring Framework 参考文档中关于命名空间解析的章节。
“ROLE_” 代表什么,为什么我的角色名称需要它?
Spring Security 有一个基于投票者的架构,这意味着访问决策是由一系列 AccessDecisionVoter
实例做出的。投票者对“配置属性”进行操作,这些属性是为受保护资源(例如方法调用)指定的。使用这种方法,并非所有属性都与所有投票者相关,投票者需要知道何时应该忽略一个属性(弃权),以及何时应该根据属性值投票授予或拒绝访问。最常见的投票者是 RoleVoter
,默认情况下,每当它发现带有 ROLE_
前缀的属性时就会投票。它会简单地将属性(例如 ROLE_USER
)与当前用户被分配的权限名称进行比较。如果找到匹配项(他们有一个名为 ROLE_USER
的权限),它就会投票授予访问权限。否则,它会投票拒绝访问。
你可以通过设置 RoleVoter
的 rolePrefix
属性来更改前缀。如果你的应用程序只需要使用角色,并且不需要其他自定义投票器,可以将前缀设置为空字符串。在这种情况下,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包。依此类推。
对于第三方 jars,情况并不总是那么明显。一个好的起点是从预构建的示例应用程序的 WEB-INF/lib
目录中复制这些 jar。对于基本的应用程序,你可以从教程示例开始。如果你希望使用带有嵌入式测试服务器的 LDAP,请以 LDAP 示例作为起点。参考手册还包括 附录,其中列出了每个 Spring Security 模块的一级依赖项,并提供了一些关于它们是否可选以及何时需要的信息。
如果你使用 Maven 构建项目,将适当的 Spring Security 模块作为依赖项添加到你的 pom.xml
文件中会自动拉取框架所需的核心 jar 包。在 Spring Security 的 pom.xml
文件中标记为“可选”的任何依赖项,如果需要的话,必须添加到你自己的 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
,并从提供的 Authentication
令牌中提取信息,而不是实现 UserDetailsService
。
在标准的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认证)配置了两个独立的策略接口,一个用于执行认证,另一个用于加载用户权限,分别称为LdapAuthenticator
和LdapAuthoritiesPopulator
。DefaultLdapAuthoritiesPopulator
从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。
通常,你会将所需的功能添加到 BeanPostProcessor
的 postProcessBeforeInitialization
方法中。假设你想要自定义由 form-login
元素创建的 UsernamePasswordAuthenticationFilter
所使用的 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。