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 生成的值完全相同。
我的应用在尝试登录时陷入“无限循环”。这是怎么回事?
用户在使用无限循环和重定向到登录页面时遇到的常见问题,通常是由于意外将登录页面配置为“受保护”资源所致。请确保您的配置允许匿名访问登录页面。您可以通过 authorizeHttpRequests DSL 来实现这一点。
当你使用基于命名空间或DSL的配置时,会在加载应用程序上下文时进行检查,如果登录页面似乎受到保护,则会记录一条警告信息。
我收到一条异常信息,内容为“访问被拒绝(用户是匿名的)”。这是什么问题?
这是一条调试级别的消息,当匿名用户首次尝试访问受保护资源时触发。
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 "绑定"(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或在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 请求返回 403 Forbidden 错误,而 HTTP GET 请求正常,问题很可能与 CSRF 相关。解决方案是提供 CSRF 令牌,或者禁用 CSRF 保护(不建议后者)。
我使用 RequestDispatcher 将请求转发到另一个 URL,但我的安全约束没有被应用。
默认情况下,过滤器不会应用于转发(forwards)或包含(includes)。如果你确实希望安全过滤器应用于转发或包含,必须在 web.xml 文件中通过 <dispatcher> 元素显式配置,该元素是 <filter-mapping> 元素的子元素。
我已将 Spring Security 的 <global-method-security> 元素添加到我的应用上下文中,但是,如果我将安全注解添加到我的 Spring MVC 控制器 Bean(Strust 动作等)上,它们似乎没有生效。这是为什么?
在Spring Web应用程序中,用于存放DispatcherServlet相关Spring MVC bean的应用上下文通常与主应用上下文分离。该上下文通常定义在名为myapp-servlet.xml的文件中,其中myapp是在web.xml文件中为Spring DispatcherServlet指定的名称。一个应用程序可以包含多个DispatcherServlet实例,每个实例都拥有各自独立的应用上下文。这些"子"上下文中的bean对应用程序的其他部分不可见。而"父"应用上下文由您在web.xml文件中定义的ContextLoaderListener加载,并对所有子上下文可见。通常安全配置(包括<global-method-security>元素)会定义在这个父上下文中。因此,由于这些Web bean无法被DispatcherServlet上下文识别,对其方法设置的任何安全约束都不会生效。您需要将<global-method-security>声明移至Web上下文,或将需要受保护的bean移至主应用上下文中。
通常,我们建议在服务层而非单个Web控制器上应用方法安全。
我有一个用户,他确实已经通过了身份验证,但是,在某些请求中,当我尝试访问 SecurityContextHolder 时,Authentication 对象却是 null。为什么我看不到用户信息?
为什么我看不到用户信息?
如果你已通过在匹配URL模式的<intercept-url>元素中使用filters='none'属性将该请求排除在安全过滤器链之外,那么该请求的SecurityContextHolder将不会被填充。请检查调试日志以确认该请求是否通过了过滤器链。(你正在查看调试日志,对吗?)
使用 URL 属性时,authorize JSP 标签不遵循我的方法安全注解。为什么?
方法安全在使用 <sec:authorize> 的 url 属性时不会隐藏链接,因为我们无法轻易地逆向推导出哪个 URL 映射到哪个控制器端点。我们受到限制是因为控制器可以依赖请求头、当前用户和其他细节来决定调用哪个方法。
Spring Security 架构常见问题
本节解答常见的 Spring Security 架构问题:
如何知道类 X 属于哪个包?
定位类的最佳方法是在你的IDE中安装Spring Security源码。发行版包含了项目划分的每个模块的源码jar包。将这些jar包添加到你的项目源码路径中,然后你就可以直接导航到Spring Security的类(在Eclipse中使用Ctrl-Shift-T)。这也使得调试更加容易,并且你可以通过直接查看异常发生处的代码来排查问题,了解那里发生了什么。
命名空间元素如何映射到传统的Bean配置?
参考指南的命名空间附录中提供了命名空间所创建Bean的总体概述。此外,blog.springsource.com 上有一篇名为《Spring Security 命名空间背后》的详细博客文章。如果您希望了解全部细节,相关代码位于 Spring Security 3.0 发行版的 spring-security-config 模块中。建议您首先阅读标准 Spring Framework 参考文档中关于命名空间解析的章节。
"ROLE_" 是什么意思?
ROLE_ 是一种标识给定权限性质的方式。以 ROLE_ 为前缀的权限意味着该权限是一个角色,很可能源自 RBAC 授权模型。
使用前缀可以明确区分OAuth 2.0权限范围(使用SCOPE_)以及其他来源授予的权限。
您可以选择不为权限添加前缀。现代 Spring Security 授权组件允许您提供完整的权限名称,从而无需使用前缀。例如,authorizeHttpRequests 和 @PreAuthorize 允许您调用 hasAuthority 或 hasRole。
如何确定需要为应用程序添加哪些依赖项才能与 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构建项目,在pom.xml文件中添加相应的Spring Security模块依赖即可自动引入框架所需的核心jar包。但需注意:若需要使用Spring Security的pom.xml文件中标记为"optional"的依赖项,则必须手动将其添加至您自己的pom.xml文件中。
运行嵌入式 UnboundID LDAP 服务器需要哪些依赖项?
您需要在项目中添加以下依赖:
- Maven
- Gradle
<dependency>
<groupId>com.unboundid</groupId>
<artifactId>unboundid-ldapsdk</artifactId>
<version>7.0.1</version>
<scope>runtime</scope>
</dependency>
implementation 'com.unboundid:unboundid-ldapsdk:7.0.1'
运行嵌入式 ApacheDS LDAP 服务器需要哪些依赖项?
Spring Security 7 移除了对 Apache DS 的支持。请改用 UnboundID。
什么是 UserDetailsService,我需要它吗?
UserDetailsService 是一个用于加载特定用户账户数据的 DAO 接口。它的唯一功能就是加载这些数据,以供框架内的其他组件使用。它并不负责对用户进行身份验证。最常见的通过用户名和密码组合对用户进行身份验证的操作是由 DaoAuthenticationProvider 执行的,它被注入了一个 UserDetailsService,以便加载用户的密码(和其他数据),并与提交的值进行比较。请注意,如果您使用 LDAP,这种方法可能不适用。
若需自定义认证流程,您应当自行实现 AuthenticationProvider。可参考这篇博客文章中的示例,该示例演示了如何将 Spring Security 认证与 Google App Engine 集成。
常见操作指南问题
本节将解答关于 Spring Security 的常见操作问题:
我需要使用比用户名更多的信息进行登录。如何添加对额外登录字段(例如公司名称)的支持?
这个问题经常出现,你可以通过在线搜索找到更多信息。
提交的登录信息由 UsernamePasswordAuthenticationFilter 的一个实例处理。你需要自定义这个类来处理额外的数据字段。一种选择是使用你自己定制的认证令牌类(而不是标准的 UsernamePasswordAuthenticationToken)。另一种选择是将额外字段与用户名拼接(例如,使用 : 字符作为分隔符),并在 UsernamePasswordAuthenticationToken 的 username 属性中传递它们。
您还需要自定义实际的认证流程。例如,如果使用了自定义的认证令牌类,您将需要编写一个 AuthenticationProvider(或扩展标准的 DaoAuthenticationProvider)来处理它。如果您将字段进行了拼接,可以实现自己的 UserDetailsService 来拆分它们,并加载相应的用户数据进行认证。
如何为仅请求URL片段值不同的情况(例如 /thing1#thing2 和 /thing1#thing3)应用不同的 intercept-url 约束?
你无法这样做,因为片段(fragment)不会从浏览器传输到服务器。从服务器的角度来看,这些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() 将返回这个自定义对象。
如果你确实需要访问会话,必须通过自定义Web层来实现。
如何在 UserDetailsService 中访问用户的密码?
你不能(也不应该,即使你找到了方法)。你可能误解了它的用途。请参阅本FAQ前面章节中的“什么是UserDetailsService?”。
如何在应用程序中动态定义受保护的URL?
人们常常询问如何将受保护URL与安全元数据属性之间的映射存储在数据库中,而不是存储在应用程序上下文中。
首先,你应该问自己是否真的需要这样做。如果一个应用程序需要保证安全,那么它也需要基于既定的策略进行彻底的安全测试。在部署到生产环境之前,可能还需要进行审计和验收测试。一个注重安全的组织应当意识到,如果允许通过修改配置数据库中的一两行数据来在运行时更改安全设置,那么他们通过辛勤测试过程所获得的好处可能会瞬间化为乌有。如果你已经考虑到了这一点(也许是通过在应用程序中使用多层安全机制),Spring Security 允许你完全自定义安全元数据的来源。如果你选择这样做,你可以使其完全动态化。
方法安全和Web安全都受到AuthorizationManager实现的保护。对于Web安全,您可以提供自己的AuthorizationManager<RequestAuthorizationContext>实现,并通过过滤器链DSL进行配置,如下所示:
- Java
- Kotlin
@Component
public class DynamicAuthorizationManager implements AuthorizationManager<RequestAuthorizationContext> {
private final MyExternalAuthorizationService authz;
// ...
@Override
public AuthorizationResult authorize(Supplier<Authentication> authentication, RequestAuthorizationContext context) {
// query the external service
}
}
// ...
http
.authorizeHttpRequests((authorize) -> authorize.anyRequest().access(dynamicAuthorizationManager))
@Component
class DynamicAuthorizationManager : AuthorizationManager<RequestAuthorizationContext?> {
private val rules: MyAuthorizationRulesRepository? = null
// ...
override fun authorize(authentication: Supplier<Authentication?>?, context: RequestAuthorizationContext?): AuthorizationResult {
// look up rules from the database
}
}
// ...
http {
authorizeHttpRequests {
authorize(anyRequest, dynamicAuthorizationManager)
}
}
对于方法安全,你可以提供自己的 AuthorizationManager<MethodInvocation> 实现,并通过如下方式将其提供给 Spring AOP:
- Java
- Kotlin
@Component
public class DynamicAuthorizationManager implements AuthorizationManager<MethodInvocation> {
private final MyExternalAuthorizationService authz;
// ...
@Override
public AuthorizationResult authorize(Supplier<Authentication> authentication, MethodInvocation invocation) {
// query the external service
}
}
// ...
@Bean
static Advisor securedAuthorizationAdvisor(DynamicAuthorizationManager dynamicAuthorizationManager) {
return AuthorizationManagerBeforeMethodInterceptor.secured(dynamicAuthorizationManager)
}
@Component
class DynamicAuthorizationManager : AuthorizationManager<MethodInvocation?> {
private val authz: MyExternalAuthorizationService? = null
// ...
override fun authorize(authentication: Supplier<Authentication?>?, invocation: MethodInvocation?): AuthorizationResult {
// query the external service
}
}
companion object {
@Bean
fun securedAuthorizationAdvisor(dynamicAuthorizationManager: DynamicAuthorizationManager): Advisor {
return AuthorizationManagerBeforeMethodInterceptor.preAuthorize(dynamicAuthorizationManager)
}
}
如何通过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 的部分有详细说明。请注意,在这种情况下,您不能使用命名空间进行配置。同时,建议您查阅相关类和接口的 Javadoc。
我想修改由命名空间创建的 bean 的属性,但模式定义中不支持此操作。除了放弃使用命名空间,我还能做什么?
命名空间功能在设计上是有意限制的,因此它并不涵盖您使用普通Bean所能实现的所有操作。如果您想做一些简单的操作,比如修改一个Bean或注入不同的依赖项,可以通过在配置中添加一个BeanPostProcessor来实现。您可以在Spring参考手册中找到更多信息。为此,您需要了解一些关于哪些Bean被创建的知识,因此您也应该阅读前面问题中提到的关于命名空间如何映射到Spring Bean的博客文章。
通常,您会将所需功能添加到 BeanPostProcessor 的 postProcessBeforeInitialization 方法中。假设您想要自定义 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上调用它。