# Spring 安全常见问题解答
# 一般性问题
# Will Spring Security take care of all my application security requirements?
Spring 安全性为你的身份验证和授权需求提供了一个非常灵活的框架,但是对于构建安全应用程序,还有许多其他考虑因素不在其范围内。Web 应用程序容易受到你应该熟悉的各种攻击,最好是在开始开发之前,这样你就可以从一开始就将它们设计和编写代码。查看OWASP 网站 (opens new window),了解有关 Web 应用程序开发人员面临的主要问题以及你可以针对他们使用的对策的信息。
# 为什么不直接使用 web.xml 安全性呢?
让我们假设你正在基于 Spring 开发一个 Enterprise 应用程序。通常需要解决四个安全问题:身份验证、Web 请求安全、服务层安全(即实现业务逻辑的方法)和域对象实例安全(即不同的域对象具有不同的权限)。考虑到这些典型的需求:
认证: Servlet 规范提供了一种身份验证方法。但是,你将需要配置容器以执行身份验证,这通常需要编辑特定于容器的“领域”设置。这使得配置不可移植,如果你需要编写一个实际的 爪哇 类来实现容器的身份验证接口,那么它就变得更加不可移植。有了 Spring 安全性,你就实现了完全的可移植性---直接到战争级别。 Spring 安全性还提供了经过生产验证的身份验证提供者和机制的选择,这意味着你可以在部署时切换身份验证方法。对于编写需要在未知目标环境中工作的产品的软件供应商来说,这一点尤其有价值。
Web 请求安全性: Servlet 规范提供了一种方法来保护你的请求 URI。然而,这些 URI 只能以 Servlet 规范自身的有限 URI 路径格式表示。 Spring 安全性提供了一种更全面的方法。例如,你可以使用 Ant 路径或正则表达式,你可以考虑 URI 的一部分,而不仅仅是所请求的页面(例如,你可以考虑 HTTP GET 参数),并且你可以实现自己的配置数据的运行时源。这意味着你的 Web 请求安全性可以在 WebApp 的实际执行过程中动态地进行更改。
服务层和域对象安全性: Servlet 规范中缺乏对服务层安全性或域对象实例安全性的支持,这代表了多层应用程序的严重限制。通常,开发人员要么忽略这些需求,要么在他们的 MVC 控制器代码中实现安全逻辑(甚至更糟的是,在视图中)。这种方法有严重的缺点:
*关注的分离:*授权是一个横切关注点,应该作为横切关注点来实现。实现授权代码的 MVC 控制器或视图使得对控制器和授权逻辑的测试更加困难,调试更加困难,并且通常会导致代码重复。
*对富客户机和 Web 服务的支持:*如果最终必须支持另一种客户机类型,则嵌入在 Web 层中的任何授权代码都是不可重用的。应该考虑的是 Spring Remoting Exports 只输出服务层 bean(而不是 MVC 控制器)。因为这样的授权逻辑需要位于服务层中,以支持多个客户机类型。
*分层问题:*MVC 控制器或视图仅仅是不正确的架构层,用于实现有关服务层方法或域对象实例的授权决策。虽然主体可以传递给服务层以使其能够做出授权决策,但这样做将在每个服务层方法上引入一个额外的参数。一种更优雅的方法是使用 ThreadLocal 来保存主体,尽管这可能会将开发时间增加到简单地使用专用安全框架会更经济(在成本效益的基础上)的程度。
*授权代码质量:*人们常说 Web 框架“使做正确的事情变得更容易,做错误的事情变得更难”。安全框架也是一样,因为它们是以抽象的方式设计的,目的很广泛。从零开始编写自己的授权代码并不能提供框架所提供的“设计检查”,而内部授权代码通常不会像广泛部署、同行评审和新版本那样得到改进。
对于简单的应用程序, Servlet 规范安全性可能就足够了。尽管在考虑 Web 容器可移植性、配置需求、有限的 Web 请求安全灵活性以及不存在的服务层和域对象实例安全性的情况下,开发人员经常寻找替代解决方案的原因是显而易见的。
# What 爪哇 and Spring Framework versions are required?
Spring 安全性 3.0 和 3.1 至少需要 JDK1.5,并且至少也需要 Spring 3.0.3。理想情况下,你应该使用最新的发布版本,以避免出现问题。
Spring Security2.0.x 至少需要 1.4 的 JDK 版本,并且是针对 Spring 2.0.x 构建的。它还应该与使用 Spring 2.5.x 的应用程序兼容。
# . I’ve copied some configuration files I found but it doesn’t work.
可能出什么问题了?
或者替代另一种复杂的场景…
实际上,在成功地使用它们构建应用程序之前,你需要了解打算使用的技术。安全是复杂的。使用登录表单设置一个简单的配置,而使用 Spring Security 的名称空间设置一些硬编码的用户是相当简单的。转向使用 Backed JDBC 数据库也很容易。但是,如果你尝试直接跳到这样一个复杂的部署场景,你几乎肯定会感到沮丧。在建立 CAS 等系统、配置 LDAP 服务器和正确安装 SSL 证书所需的学习曲线中,有一个很大的跳跃。因此,你需要一步一步地做事情。
从 Spring 安全的角度来看,你应该做的第一件事是遵循网站上的“入门”指南。这将需要你通过一系列步骤来启动和运行,并了解框架如何运行。如果你使用的是你 AREN 不熟悉的其他技术,那么你应该做一些研究,并尝试确保在将它们组合到一个复杂的系统中之前可以单独使用它们。
# 常见问题
认证
会话管理
杂项
# 当我尝试登录时,我会收到一条错误消息,上面写着“不良凭证”。怎么了?
这意味着身份验证失败。它没有说明原因,因为避免提供可能有助于攻击者猜测帐户名称或密码的详细信息是一种好的做法。
这也意味着,如果你在论坛上问这个问题,除非你提供额外的信息,否则你不会得到答案。对于任何问题,你都应该检查调试日志的输出,注意任何异常堆栈跟踪和相关消息。通过调试器中的代码,查看身份验证在哪里失败以及为什么失败。编写一个测试用例,在应用程序之外执行你的身份验证配置。通常情况下,故障是由于存储在数据库中的密码数据与用户输入的密码数据存在差异。如果使用散列密码,请确保存储在数据库中的值完全正确与应用程序中配置的PasswordEncoder
产生的值相同。
# 当我尝试登录时,我的应用程序进入了一个“死循环”,这是怎么回事?
无限循环和重定向到登录页面的常见用户问题是由于意外地将登录页面配置为“安全”资源而引起的。确保你的配置允许匿名访问登录页面,可以将其从安全筛选链中排除,也可以将其标记为需要角色 _Anonymous。
如果你的 AccessDecisionManager 包含一个身份验证投票器,则可以使用属性“is_Authenticated_Anonymouly”。如果你使用标准的名称空间配置设置,这将自动可用。
从 Spring Security2.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 安全性无关,你应该将你的应用程序或服务器配置为设置适当的Cache-Control
响应头。请注意,SSL 请求永远不会被缓存。
# 我得到了一个异常消息,即“在 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 安全性将存储的密码与用户提交的密码进行比较。最常见的方法是使用 LDAP“bind”,这是LDAP 协议 (opens new window)支持的操作之一。 Spring 使用这种方法,Security 通过尝试以用户身份对目录进行身份验证来验证密码。
LDAP 身份验证最常见的问题是缺乏对目录服务器树结构和配置的了解。这在不同的公司会有所不同,所以你必须自己去发现它。在向应用程序添加 Spring 安全性 LDAP 配置之前,使用标准的 爪哇 LDAP 代码编写一个简单的测试(不涉及 Spring 安全性)是一个好主意,并确保你可以首先让它工作。例如,要对用户进行身份验证,你可以使用以下代码:
Java
@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);
}
Kotlin
@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 安全性与维护会话或提供会话标识符无关。这完全由 Servlet 容器处理。
# I’m using Spring Security’s concurrent session control to prevent users from logging in more than once at a time.
当我在登录后打开另一个浏览器窗口时,它并不会阻止我再次登录。为什么我可以登录不止一次?
浏览器通常为每个浏览器实例维护一个会话。你不能同时进行两个单独的疗程。因此,如果你再次在另一个窗口或选项卡中登录,那么你只是在同一窗口中重新进行身份验证会话。服务器对选项卡、Windows 或浏览器实例一无所知。它只看到 HTTP 请求,并根据它们所包含的 JSessionID cookie 的值将这些请求与特定的会话绑定。当用户在会话期间进行身份验证时, Spring Security 的并发会话控件检查他们拥有的其他经过身份验证的会话的数量。如果已经用相同的方法对它们进行了身份验证,那么重新进行身份验证将不会有任何效果。
# Why does the session Id change when I authenticate through Spring Security?
使用默认配置, Spring Security 在用户进行身份验证时会更改会话ID。如果你使用的是 Servlet 3.1 或更新的容器,只需更改会话ID 即可。如果你使用的是旧的容器, Spring Security 会使现有的会话失效,创建一个新的会话,并将会话数据传输到新的会话。以这种方式更改会话标识符可防止“会话-fixing”攻击。你可以在网上和参考手册中找到更多有关此的信息。
# and have enabled HTTPS for my login page, switching back to HTTP afterwards.
它不起作用——我只是在进行了身份验证后回到了登录页面。
这是因为在 HTTPS 下创建的会话(会话cookie 被标记为“secure”)随后不能在 HTTP 下使用。浏览器不会将 cookie 发送回服务器,并且任何会话状态都将丢失(包括安全上下文信息)。首先在 HTTP 中启动会话应该可以工作,因为会话cookie 不会被标记为安全。然而, Spring 安全性的Session Fixation Protection (opens new window)可能会干扰这一点,因为它会导致新的会话ID cookie 被发送回用户的浏览器,通常带有安全标志。为了避免这种情况,你可以禁用会话固定保护,但是在较新的 Servlet 容器中,你还可以配置会话cookie,使其永远不使用安全标志。请注意,在 HTTP 和 HTTPS 之间切换通常不是一个好主意,因为任何使用 HTTP 的应用程序都容易受到中间人攻击。为了真正的安全,用户应该开始使用 HTTPS 访问你的网站,并继续使用它,直到他们退出。甚至从通过 HTTP 访问的页面中点击 HTTPS 链接也可能存在风险。如果你需要更多的说服力,请查看sslstrip (opens new window)之类的工具。
# 我没有在 HTTP 和 HTTPS 之间切换,但我的会话仍然迷路了。
会话可以通过交换会话cookie 或向 URL 添加jsessionid
参数来维护(如果你使用 JSTL 输出 URL,或者在 URL 上调用HttpServletResponse.encodeUrl
(例如,在重定向之前),这会自动发生。如果客户机禁用了 cookie,并且你没有重写 URL 以包含jsessionid
,那么会话将丢失。请注意,出于安全原因,Cookie 的使用是首选的,因为它不会公开 URL 中的会话信息。
# I’m trying to use the concurrent session-control support but it won’t let me log back in, even if I’m sure I’ve logged out and haven’t exceeded the allowed sessions.
确保已将侦听器添加到 web.xml 文件中。必须确保在会话被销毁时通知 Spring 安全会话登记册。没有它,会话信息将不会从注册表中删除。
<listener>
<listener-class>org.springframework.security.web.session.HttpSessionEventPublisher</listener-class>
</listener>
# Spring Security is creating a session somewhere, even though I’ve configured it not to, by setting the create-session attribute to never.
这通常意味着用户的应用程序正在某个地方创建一个会话,但他们 AREN 不知道这一点。最常见的罪魁祸首是 JSP。许多 AREN 的人并不知道 JSP 默认情况下会创建会话。要防止 JSP 创建会话,请将指令<%@ page session="false" %>
添加到页面的顶部。
如果你在确定在哪里创建了会话时遇到了困难,那么可以添加一些调试代码来跟踪位置。一种方法是将javax.servlet.http.HttpSessionListener
添加到应用程序中,该应用程序在sessionCreated
方法中调用Thread.dumpStack()
。
# 我在发帖时收到了一封 403 的禁止信。
如果 HTTP POST 返回了一个禁止的 HTTP403,但是对于 HTTPGET 有效,那么这个问题很可能与<gtr="61"/>有关。提供 CSRF 令牌或禁用 CSRF 保护(不建议使用)。
# 我正在使用 RequestDispatcher 将一个请求转发到另一个 URL,但是我的安全约束 AREN 没有被应用。
过滤器在默认情况下不应用于转发或包含。如果确实希望将安全过滤器应用于转发和/或包含,则必须使用 <dispatcher>元素(<filter-mapping>的子元素)在 web.xml 中显式地配置这些过滤器。
# then they don’t seem to have an effect.
在 Spring Web 应用程序中,为 Dispatcher Servlet 保存 Spring MVC bean 的应用程序上下文通常与主应用程序上下文分离。它通常在一个名为myapp-servlet.xml
的文件中定义,其中“myapp”是在web.xml
中分配给 Spring DispatcherServlet
的名称。一个应用程序可以有多个DispatcherServlet
s,每个都有自己独立的应用程序上下文。这些“子”上下文中的 bean 对应用程序的其余部分不可见。“父”应用程序上下文由你在web.xml
中定义的ContextLoaderListener
加载,并且对所有子上下文都是可见的。这个父上下文通常是你定义安全配置的地方,包括<global-method-security>
元素)。因此,不会强制执行应用于这些 Web bean 中的方法的任何安全约束,因为无法从DispatcherServlet
上下文中看到这些 bean。你需要将<global-method-security>
声明移动到 Web 上下文中,或者将你想要保护的 bean 移动到主应用程序上下文中。
通常,我们建议在服务层而不是在单个 Web 控制器上应用方法安全性。
# 我有一个用户肯定已经通过了身份验证,但是当我在某些请求中尝试访问 SecurityContextholder 时,身份验证是空的。
为什么我看不到用户信息?
如果你使用与 URL 模式匹配的<intercept-url>
元素中的属性filters='none'
从安全筛选链中排除了该请求,那么将不会为该请求填充SecurityContextHolder
。检查调试日志以查看请求是否正在通过筛选链。(你正在读取调试日志,对吗?)。
# 当使用 URL 属性时,Authorized JSP 标记不尊重我的方法安全注释。
当使用<sec:authorize>
中的url
属性时,方法安全性不会隐藏链接,因为我们无法轻松地反向工程将什么 URL 映射到什么控制器端点,因为控制器可以依靠头、当前用户等来确定调用什么方法。
# Spring Security Architecture Questions
# 我如何知道 X 类在哪个包中?
定位类的最佳方法是在 IDE 中安装 Spring 安全源。该分布包括项目被划分到的每个模块的源 JAR。将这些添加到项目源路径中,你就可以直接导航到 Spring 安全类(Ctrl-Shift-T
在 Eclipse 中)。这也使调试变得更容易,并允许你通过直接查看发生异常的代码来解决异常问题,从而了解发生了什么。
# How do the namespace elements map to conventional bean configurations?
在参考指南的命名空间附录中,对命名空间创建的 bean 有一个大致的概述。在blog.springsource.com (opens new window)上还有一篇名为“ Spring 安全命名空间背后”的详细博客文章。如果想知道完整的详细信息,那么代码在 Spring Security3.0 发行版中的spring-security-config
模块中。你可能应该先阅读标准 Spring 框架参考文档中有关名称空间解析的章节。
# “role_”是什么意思,为什么我需要它在我的角色名称?
Spring 安全性具有基于投票人的体系结构,这意味着访问决策是由一系列AccessDecisionVoter
s 作出的。投票者根据为安全资源(例如方法调用)指定的“配置属性”进行操作。使用这种方法,并不是所有属性都与所有投票者相关,投票者需要知道什么时候应该忽略一个属性(弃权),什么时候应该基于属性值投票授予或拒绝访问。最常见的投票者是RoleVoter
,在默认情况下,每当它发现带有“role_”前缀的属性时,它都会投票。它将属性(例如“role_user”)与当前用户被分配的权限的名称进行简单的比较。如果它找到一个匹配的(他们有一个名为“role_user”的权限),它将投票授予访问权限,否则将投票拒绝访问。
可以通过设置RoleVoter
的rolePrefix
属性来更改前缀。如果你只需要在应用程序中使用角色,而不需要其他自定义投票者,那么你可以将前缀设置为一个空白字符串,在这种情况下,RoleVoter
将把所有属性都视为角色。
# How do I know which dependencies to add to my application to work with Spring Security?
这将取决于你正在使用的功能以及正在开发的应用程序类型。在 Spring Security3.0 中,项目 JAR 被划分为明显不同的功能区域,因此很容易从应用程序需求中找出你需要的 Spring 安全性 JAR。所有应用程序都需要spring-security-core
jar。如果你正在开发一个 Web 应用程序,那么你需要spring-security-web
jar。如果使用安全名称空间配置,则需要spring-security-config
JAR,对于 LDAP 支持,则需要spring-security-ldap
JAR 等等。
对于第三方罐子来说,情况并不总是那么明显。一个很好的起点是从一个预构建的示例应用程序 WEB-INF/lib 目录中复制这些应用程序。对于基本的应用程序,你可以从教程示例开始。如果你希望在嵌入式测试服务器中使用 LDAP,那么可以使用 LDAP 示例作为起点。参考手册还包括附录 (opens new window)列出了每个 Spring 安全模块的第一级依赖关系,并提供了有关它们是否是可选的以及它们需要做什么的一些信息。
如果你使用 Maven 构建你的项目,那么将适当的 Spring 安全模块作为依赖项添加到 POM.xml 将自动获取框架所需的核心 JAR。在 Spring Security POM 文件中被标记为“可选的”的任何文件,如果你需要它们,都必须添加到你自己的 POM.xml 文件中。
# 运行嵌入式 ApacheDS LDAP 服务器需要哪些依赖项?
如果正在使用 Maven,则需要在 POM 依赖项中添加以下内容:
<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>
其他所需的罐子应以传递式方式拉入。
# 什么是 UserDetailsService,我需要一个吗?
UserDetailsService
是用于加载特定于用户帐户的数据的 DAO 接口。它没有其他功能来加载该数据,以供框架内的其他组件使用。它不负责对用户进行身份验证。使用用户名/密码组合对用户进行身份验证最常见的方法是DaoAuthenticationProvider
,它被注入UserDetailsService
,以允许它为用户加载密码(和其他数据),以便将其与提交的值进行比较。请注意,如果你正在使用 LDAP,这种方法可能行不通。。
如果你想定制身份验证过程,那么你应该自己实现AuthenticationProvider
。有关集成 Spring 安全性验证和 Google App Engine 的示例,请参见博客文章 (opens new window)。
# 共同的“howto”请求
# 我需要登录更多的信息,而不仅仅是用户名。
如何添加对额外登录字段(例如公司名称)的支持?
这个问题在 Spring 安全论坛中反复出现,因此你将通过搜索文档(或通过 Google)在那里找到更多信息。
提交的登录信息由UsernamePasswordAuthenticationFilter
实例处理。你将需要自定义这个类来处理额外的数据字段。一种选择是使用你自己定制的身份验证令牌类(而不是标准的UsernamePasswordAuthenticationToken
),另一种选择是简单地将额外的字段与用户名连接起来(例如,使用一个“:”作为分隔符),并将它们传递到UsernamePasswordAuthenticationToken
的用户名属性中。
你还需要自定义实际的身份验证过程。例如,如果你使用自定义身份验证令牌类,则必须编写AuthenticationProvider
来处理它(或扩展标准DaoAuthenticationProvider
)。如果你已将这些字段串联起来,则可以实现你自己的UserDetailsService
,它将它们拆分并加载适当的用户数据以进行身份验证。
# 在只有请求的 URL 的分段值不同(例如/foo#bar 和/foo#blah)的情况下,如何应用不同的截取 URL 约束?
你不能这样做,因为片段不会从浏览器传输到服务器。从服务器的角度来看,上面的 URL 是相同的。这是 GWT 用户的一个常见问题。
# 在用户详细服务中?
显然,你不能(在不求助于线程局部变量的情况下),因为提供给接口的唯一信息是用户名。与其实现UserDetailsService
,不如直接实现AuthenticationProvider
,并从提供的Authentication
令牌中提取信息。
在标准的 Web 设置中,Authentication
对象上的getDetails()
方法将返回WebAuthenticationDetails
的实例。如果需要其他信息,可以将自定义AuthenticationDetailsSource
插入到正在使用的身份验证筛选器中。如果你使用名称空间,例如使用<form-login>
元素,那么你应该删除该元素,并将其替换为<custom-filter>
声明,该声明指向显式配置的UsernamePasswordAuthenticationFilter
。
# 我如何从 UserDetailsService 访问 HttpSession?
你不能,因为UserDetailsService
没有意识到 Servlet API。如果你想存储自定义用户数据,那么你应该自定义返回的UserDetails
对象。然后可以在任何点通过线程本地SecurityContextHolder
访问此内容。调用SecurityContextHolder.getContext().getAuthentication().getPrincipal()
将返回此自定义对象。
如果你确实需要访问会话,那么它必须通过定制 Web 层来完成。
# 我如何在用户详细服务中访问用户的密码?
你不能(也不应该)。你可能误解了它的目的。见上文“什么是用户详细服务?”。
# 如何在应用程序中动态地定义安全的 URL?
人们经常询问如何在数据库中存储安全 URL 和安全元数据属性之间的映射,而不是在应用程序上下文中。
你应该问自己的第一件事是,你是否真的需要这么做。如果应用程序需要安全保护,那么它还需要基于已定义的策略对安全性进行彻底测试。在将其推出到生产环境之前,可能需要进行审核和验收测试。具有安全意识的组织应该意识到,通过允许在运行时更改配置数据库中的一两行来修改安全设置,可以立即消除其勤奋的测试过程的好处。 Spring 如果你已经考虑到了这一点(可能在你的应用程序中使用了多个安全层),那么安全性允许你完全自定义安全元数据的来源。如果你愿意的话,你可以让它完全充满活力。
方法和 Web 安全性都由AbstractSecurityInterceptor
的子类保护,该子类配置为SecurityMetadataSource
,它从该子类获得特定方法或过滤器调用的元数据。对于 Web 安全,拦截器类是FilterSecurityInterceptor
,它使用标记接口FilterInvocationSecurityMetadataSource
。它所操作的“安全对象”类型是FilterInvocation
。使用的默认实现(在名称空间<http>
中以及显式配置拦截器时)将 URL 模式列表及其相应的“配置属性”列表(ConfigAttribute
实例)存储在内存映射中。
要从替代源加载数据,必须使用显式声明的安全筛选链(通常是 Spring Security 的FilterChainProxy
),以便自定义FilterSecurityInterceptor
Bean。你不能使用名称空间。然后,你将实现FilterInvocationSecurityMetadataSource
来为特定的FilterInvocation
[1]任意加载数据。一个非常基本的大纲应该是这样的:
Java
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);
}
}
Kotlin
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
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));
}
});
}
}
Kotlin
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 获得相关的类和接口。
# I want to modify the property of a bean that is created by the namespace, but there is nothing in the schema to support it.
除了放弃名称空间的使用,我还能做什么呢?
名称空间功能是有意限制的,因此它不能涵盖使用普通 bean 所能做的所有事情。如果你想做一些简单的事情,比如修改一个 Bean,或者注入一个不同的依赖项,那么你可以通过在配置中添加一个BeanPostProcessor
来做到这一点。更多信息请参见Spring Reference Manual (opens new window)。为了做到这一点,你需要了解一些有关创建了哪些 bean 的信息,因此你还应该阅读how the namespace maps to Spring beans上的问题中的博客文章。
通常,你会将所需的功能添加到postProcessBeforeInitialization
的BeanPostProcessor
方法中。假设你希望自定义UsernamePasswordAuthenticationFilter
使用的AuthenticationDetailsSource
,(由form-login
元素创建)。你希望从请求中提取一个名为CUSTOM_HEADER
的特定标头,并在对用户进行身份验证时使用它。处理器类看起来是这样的:
Java
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;
}
}
Kotlin
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 上自动调用它。
1。FilterInvocation
对象包含HttpServletRequest
,因此你可以获得 URL 或任何其他相关信息,你可以根据返回的属性列表将包含什么来做出决定。
← WebSocket 安全 反应性应用 →