附录

1. 数据库表

HSQLDB 数据库提供了 DDL 语句。您可以将这些用作为您正在使用的数据库定义架构的指南。

1.1 用户

UserDetailsService (JdbcDaoImpl) 的标准 JDBC 实现需要表来加载用户的密码、帐户状态(启用或禁用)和权限列表(角色)。您将需要调整此架构以匹配您正在使用的数据库方言。

create table users(
username varchar_ignorecase(50) not null primary key,
password varchar_ignorecase(50) not null,
enabled boolean not null
);
create table authorities (
username varchar_ignorecase(50) not null,
authority varchar_ignorecase(50) not null,
constraint fk_authorities_users foreign key(username) references users(username)
);
create unique index ix_auth_username on authorities (username,authority);

1.1.1 对于 Oracle 数据库

CREATE TABLE USERS (
USERNAME NVARCHAR2(128) PRIMARY KEY,
PASSWORD NVARCHAR2(128) NOT NULL,
ENABLED CHAR(1) CHECK (ENABLED IN ('Y','N') ) NOT NULL
);
CREATE TABLE AUTHORITIES (
USERNAME NVARCHAR2(128) NOT NULL,
AUTHORITY NVARCHAR2(128) NOT NULL
);
ALTER TABLE AUTHORITIES ADD CONSTRAINT AUTHORITIES_UNIQUE UNIQUE (USERNAME, AUTHORITY);
ALTER TABLE AUTHORITIES ADD CONSTRAINT AUTHORITIES_FK1 FOREIGN KEY (USERNAME) REFERENCES USERS (USERNAME) ENABLE;

1.1.2 Group Authorities

create table groups (
id bigint generated by default as identity(start with 0) primary key,
group_name varchar_ignorecase(50) not null
);
create table group_authorities (
group_id bigint not null,
authority varchar(50) not null,
constraint fk_group_authorities_group foreign key(group_id) references groups(id)
);
create table group_members (
id bigint generated by default as identity(start with 0) primary key,
username varchar(50) not null,
group_id bigint not null,
constraint fk_group_members_group foreign key(group_id) references groups(id)
);

请记住,仅当您使用提供的 JDBC UserDetailsService 实现时才需要这些表。如果您自己编写或选择在没有 UserDetailsService 的情况下实现 AuthenticationProvider,那么只要满足接口契约,您就可以完全自由地存储数据。

1.2 Remember-Me

该表用于存储更安全的持久令牌记住我实现所使用的数据。如果您直接或通过命名空间使用 JdbcTokenRepositoryImpl,那么您将需要此表。请记住调整此架构以匹配您正在使用的数据库方言。

create table persistent_logins (
username varchar(64) not null,
series varchar(64) primary key,
token varchar(64) not null,
last_used timestamp not null
);

大部分情况,使用redis来保存,因为redis有失效时间。

1.3 ACL

这个模块用的比较少,所以这里省略。如果向看,请参考官网

1.4 OAuth 2.0 Client

OAuth2AuthorizedClientService (JdbcOAuth2AuthorizedClientService) 的 JDBC 实现需要用于持久化 OAuth2AuthorizedClient(s) 的表。您将需要调整此架构以匹配您正在使用的数据库方言。

CREATE TABLE oauth2_authorized_client (
client_registration_id varchar(100) NOT NULL,
principal_name varchar(200) NOT NULL,
access_token_type varchar(100) NOT NULL,
access_token_value blob NOT NULL,
access_token_issued_at timestamp NOT NULL,
access_token_expires_at timestamp NOT NULL,
access_token_scopes varchar(1000) DEFAULT NULL,
refresh_token_value blob DEFAULT NULL,
refresh_token_issued_at timestamp DEFAULT NULL,
created_at timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL,
PRIMARY KEY (client_registration_id, principal_name)
);

2. XML 命名空间

SpringBoot中用的很少了,详细可以参考官网

3. FAQ

FAQ 里面包含的问题感觉比较老了,可以忽略这一章节,因为在新的版本中,有新的功能体现出来了。

3.1 一般问题

  1. Spring Security 会照顾我所有的应用程序安全要求吗?
  2. 为什么不只使用 web.xml 安全性?
  3. 需要哪些 Java 和 Spring Framework 版本?
  4. 我是 Spring Security 的新手,我需要构建一个通过 HTTPS 支持 CAS 单点登录的应用程序,同时允许对某些 URL 进行本地基本身份验证,针对多个后端用户信息源(LDAP 和 JDBC)进行身份验证。我复制了一些我找到的配置文件,但它不起作用。

Spring Security 会照顾我所有的应用程序安全要求吗?

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

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

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

  1. Authentication: servlet 规范提供了一种身份验证方法。但是,您需要配置容器以执行身份验证,这通常需要编辑容器特定的“领域”设置。这使得配置不可移植,如果您需要编写一个实际的 Java 类来实现容器的身份验证接口,它就变得更加不可移植。使用 Spring Security,您可以实现完全的可移植性 - 一直到 WAR 级别。此外,Spring Security 提供了经过生产验证的身份验证提供程序和机制的选择,这意味着您可以在部署时切换身份验证方法。这对于编写需要在未知目标环境中工作的产品的软件供应商来说尤其有价值。

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

  3. Service layer and domain object security: servlet规范中对服务层安全性或域对象实例安全性的支持的缺失代表了多层应用程序的严重限制。通常,开发人员要么忽略这些要求,要么在他们的 MVC 控制器代码中实现安全逻辑(或者更糟糕的是,在页面内部)。这种方法有严重的缺点:

    • 关注点分离:授权是一个横切关注点,应该这样实现。实现授权代码的 MVC 控制器或视图使得控制器和授权逻辑的测试更加困难,更难以调试,并且经常会导致代码重复。
    • 支持富客户端和 Web 服务:如果最终必须支持其他客户端类型,则嵌入在 Web 层中的任何授权代码都是不可重用的。应该考虑到 Spring 远程导出器只导出服务层 bean(而不是 MVC 控制器)。因此,授权逻辑需要位于服务层以支持多种客户端类型。
    • 分层问题:通过 MVC 控制器或视图来实施有关服务层方法或域对象实例的授权决策是不正确架构设计。虽然可以将 Principal 传递给服务层以使其能够做出授权决定,但这样做会在每个服务层方法上引入一个额外的参数。一种更优雅的方法是使用 ThreadLocal 来保存 Principal,尽管这可能会增加开发时间,从而使简单地使用专用的安全框架变得更加经济(在成本效益的基础上)。
    • 授权码质量:人们常说 Web 框架“让做正确的事情更容易,做错误的事情更难”。安全框架是相同的,因为它们以抽象的方式设计用于广泛的目的。从头开始编写自己的授权代码并不能提供框架提供的“设计检查”,而且内部授权代码通常缺乏广泛部署、同行评审和新版本所带来的改进。

    对于简单的应用程序,servlet 规范的安全性可能就足够了。尽管考虑到 Web 容器可移植性、配置要求、有限的 Web 请求安全灵活性,和不存在的服务层和域对象实例安全性,很明显为什么开发人员经常寻找替代解决方案。

需要哪些 Java 和 Spring Framework 版本?

​ Spring Security 3.0 和 3.1 至少需要 JDK 1.5,并且至少需要 Spring 3.0.3。理想情况下,您应该使用最新版本以避免出现问题。

我是 Spring Security 的新手,我需要构建一个通过 HTTPS 支持 CAS 单点登录的应用程序,同时允许对某些 URL 进行本地基本身份验证,针对多个后端用户信息源(LDAP 和 JDBC)进行身份验证。我复制了一些我找到的配置文件,但它不起作用。

​ 有什么问题?

​ 或者替代一个替代的复杂场景……

​ 实际上,您需要了解您打算使用的技术,然后才能使用它们成功构建应用程序。安全性很复杂。使用登录表单和一些使用 Spring Security 命名空间的硬编码用户设置简单的配置相当简单。转向使用支持的 JDBC 数据库也很容易。但是,如果您尝试直接跳到像这样的复杂部署场景,您几乎肯定会感到沮丧。建立像 CAS 这样的系统所需的学习曲线有很大的飞跃,正确配置 LDAP 服务器并安装 SSL 证书。所以你需要一步一步来。

​ 从 Spring Security 的角度来看,您应该做的第一件事是遵循网站上的“入门”指南。这将带您完成一系列步骤来启动和运行,并了解框架的运行方式。如果您正在使用您不熟悉的其他技术,那么您应该做一些研究并尝试确保您可以在将它们组合到复杂系统之前单独使用它们。

3.2 常见问题

3.2.1 Authentication

  1. 当我尝试登录时,我收到一条错误消息,显示“凭据错误”。怎么了?
  2. 当我尝试登录时,我的应用程序进入“无限循环”,这是怎么回事?
  3. 我收到一条异常消息“访问被拒绝(用户是匿名的);”。怎么了?
  4. 为什么我退出应用程序后仍然可以看到安全页面?
  5. 我收到一条异常消息“在 SecurityContext 中找不到身份验证对象”。怎么了?
  6. 我无法让 LDAP 身份验证工作。

当我尝试登录时,我收到一条错误消息,显示“凭据错误”。怎么了?

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

​ 这也意味着如果您在论坛中提出这个问题,除非您提供额外的信息,否则您将不会得到答案。与任何问题一样,您应该检查调试日志的输出,注意任何异常堆栈跟踪和相关消息。在调试器中单步执行代码以查看身份验证失败的位置以及原因。编写一个测试用例,在应用程序之外执行您的身份验证配置。通常,失败是由于存储在数据库中的密码数据与用户输入的密码数据不同造成的。如果您使用散列密码,请确保存储在数据库中的值与应用程序中配置的 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 的“篡改数据”)。这与 Spring Security 无关,您应该配置您的应用程序或服务器以设置适当的 Cache-Control 响应 header。请注意,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 Security 将存储的密码与用户提交的密码进行比较。最常见的方法是使用 LDAP“绑定”,这是 LDAP 协议支持的操作之一。使用这种方法,Spring Security 通过尝试以用户身份对目录进行身份验证来验证密码。

LDAP 身份验证最常见的问题是缺乏对目录服务器树结构和配置的了解。这在不同的公司会有所不同,所以你必须自己去发现它。在向应用程序添加 Spring Security LDAP 配置之前,最好使用标准 Java LDAP 代码(不涉及 Spring Security)编写一个简单的测试,并确保您可以首先使其工作。例如,要对用户进行身份验证,您可以使用以下代码:

@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);
}

3.2.2 Session Management

  1. 我正在使用 Spring Security 的并发会话控制来防止用户一次登录多次。
  2. 当我通过 Spring Security 进行身份验证时,为什么会话 ID 会更改?
  3. 我正在使用 Tomcat(或其他一些 servlet 容器)并为我的登录页面启用了 HTTPS,然后切换回 HTTP。
  4. 我正在尝试使用并发会话控制支持,但它不会让我重新登录,即使我确定我已经注销并且没有超出允许的会话。
  5. Spring Security 通过将 create-session 属性设置为 never 来在某处创建会话,即使我已将其配置为不这样做。

会话管理问题是论坛问题的常见来源。如果您正在开发 Java Web 应用程序,您应该了解 servlet 容器和用户浏览器之间的会话是如何维护的。您还应该了解安全 cookie 和非安全 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 可能会干扰这一点,因为它会导致一个新的 session 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 会话注册表是很重要的。没有它,会话信息将不会从注册表中删除。

<listener>
<listener-class>org.springframework.security.web.session.HttpSessionEventPublisher</listener-class>
</listener>

3.2.3 各种各样的

  1. 执行 POST 时收到 403 Forbidden
  2. 我正在使用 RequestDispatcher 将请求转发到另一个 URL,但没有应用我的安全约束。
  3. 我已将 Spring Security 的 <global-method-security> 元素添加到我的应用程序上下文中,但如果我将安全注释添加到我的 Spring MVC 控制器 bean(Struts 操作等),那么它们似乎没有效果。
  4. 我有一个用户肯定已经过身份验证,但是当我在某些请求期间尝试访问 SecurityContextHolder 时,身份验证为空。
  5. 在使用 URL 属性时,授权 JSP 标记不检查我的方法安全注释。

执行 POST 时收到 403 Forbidden

如果 HTTP POST 返回 HTTP 403 Forbidden,但适用于 HTTP GET,则问题很可能与 CSRF 有关。提供 CSRF 令牌或禁用 CSRF 保护(不推荐)。

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

默认情况下,过滤器不应用于转发或包含。如果您真的希望将安全过滤器应用于转发和/或包含,那么您必须在 web.xml 中使用<dispatcher> element, a child element of <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,每个 DispatcherServlet 都有自己独立的应用程序上下文。这些“子”上下文中的 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 属性时,授权 JSP 标记不检查我的方法安全注释。

当使用 <sec:authorize> 中的 url 属性时,方法安全性不会隐藏链接,因为我们无法轻松地逆向工程将哪个 URL 映射到哪个控制器端点,因为控制器可以依赖 header,当前用户等来确定调用什么方法。

3.3 架构问题

  1. 我如何知道 X 在哪个包类中?
  2. 命名空间元素如何映射到传统的 bean 配置?
  3. “ROLE_”是什么意思,为什么我的角色名称需要它?
  4. 我如何知道要将哪些依赖项添加到我的应用程序以使用 Spring Security?
  5. 运行嵌入式 ApacheDS LDAP 服务器需要哪些依赖项?
  6. 什么是 UserDetailsService,我需要一个吗?

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

定位类的最佳方法是在 IDE 中安装 Spring Security 源代码。该发行版包括项目分成的每个模块的 source jar。将这些添加到您的项目源路径中,您可以直接导航到 Spring Security 类(Eclipse 中的 Ctrl-Shift-T)。这也使调试更容易,并允许您通过直接查看异常发生的代码来解决异常问题,以了解那里发生了什么。

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

在参考指南的命名空间附录中有一个关于命名空间创建的 bean 的一般概述。在 blog.springsource.com 上还有一篇名为“Behind the Spring Security Namespace”的详细博客文章。如果想了解完整的详细信息,那么代码位于 Spring Security 3.0 发行版中的 spring-security-config 模块中。您可能应该首先阅读标准 Spring Framework 参考文档中有关名称空间解析的章节。

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

Spring Security 具有基于投票者的架构,这意味着访问决策由一系列 AccessDecisionVoters 做出。投票者作用于为安全资源(例如方法调用)指定的“配置属性”。使用这种方法,并非所有属性都可能与所有选民相关,并且选民需要知道何时应忽略属性(弃权)以及何时应根据属性值投票授予或拒绝访问。最常见的投票者是 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。如果您使用安全命名空间配置,则需要 spring-security-config jar,对于 LDAP 支持,您需要 spring-security-ldap jar 等等。

如果您正在使用 maven 构建项目,那么将适当的 Spring Security 模块作为依赖项添加到您的 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 Security 身份验证与 Google App Engine 集成的示例,请参阅此博客文章。

3.4 常见的“Howto”

  1. 我需要使用更多信息 login in,而不仅仅是用户名。
  2. 如何应用不同的拦截 URL 约束,其中只有请求 URL 的片段值不同(例如 /foo#bar 和 /foo#blah?)
  3. 如何在 UserDetailsService 中访问用户的 IP 地址(或其他 Web 请求数据)?
  4. 如何从 UserDetailsService 访问 HttpSession?
  5. 如何在 UserDetailsService 中访问用户密码?
  6. 如何在应用程序中动态定义安全 URL?
  7. 如何针对 LDAP 进行身份验证但从数据库加载用户角色?
  8. 我想修改由命名空间创建的 bean 的属性,但模式中没有任何内容支持它。

我需要使用更多信息 login in,而不仅仅是用户名。

如何添加对额外登录字段(例如公司名称)的支持?

这个问题在 Spring Security 论坛中反复出现,因此您可以通过搜索档案(或通过 google)找到更多信息。

提交的登录信息由 UsernamePasswordAuthenticationFilter 的一个实例处理。您将需要自定义此类以处理额外的数据字段。一种选择是使用您自己定制的身份验证令牌类(而不是标准的 UsernamePasswordAuthenticationToken),另一种是简单地将额外字段与用户名连接起来(例如,使用“:”作为分隔符)并将它们传递到 UsernamePasswordAuthenticationToken 的用户名属性中。

您还需要自定义实际的身份验证过程。例如,如果您使用自定义身份验证令牌类,则必须编写一个 AuthenticationProvider 来处理它(或扩展标准 DaoAuthenticationProvider)。如果您已经连接了这些字段,您可以实现自己的 UserDetailsService 将它们拆分并加载适当的用户数据以进行身份验证。

如何应用不同的拦截 URL 约束,其中只有请求 URL 的片段值不同(例如 /foo#bar 和 /foo#blah?

你不能这样做,因为片段没有从浏览器传输到服务器。从服务器的角度来看,上面的 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 中访问用户密码?

你不能(也不应该)。你可能误解了它的目的。请参阅“什么是 UserDetailsService?”

如何在应用程序中动态定义安全 URL?

人们经常询问如何将安全 URL 和安全元数据属性之间的映射存储在数据库中,而不是存储在应用程序上下文中。

你应该问自己的第一件事是你是否真的需要这样做。如果应用程序需要保护,那么它还需要根据定义的策略彻底测试安全性。在推广到生产环境之前,它可能需要进行审计和验收测试。具有安全意识的组织应该意识到,如果允许在运行时通过更改配置数据库中的一两行来修改安全设置,他们勤奋测试过程的好处可能会立即消失(注释:这样做风险太大,因为以前测试的都白白的测试了,作为一个安全框架的作者,考虑的还是比较严谨)。如果您已经考虑到这一点(可能在应用程序中使用多层安全性),那么 Spring Security 允许您完全自定义安全元数据的来源。如果您愿意,您可以使其完全动态化。

方法和 Web 安全都受到 AbstractSecurityInterceptor 的子类的保护,该子类配置了一个 SecurityMetadataSource,它从中获取特定方法或过滤器调用的元数据。对于 Web 安全,拦截器类是 FilterSecurityInterceptor,它使用标记接口 FilterInvocationSecurityMetadataSource。它所操作的“安全对象”类型是 FilterInvocation。使用的默认实现(无论是在命名空间 <http> 中还是在显式配置拦截器时,都将 URL 模式列表及其相应的“配置属性”列表(ConfigAttribute 的实例)存储在内存映射中。

要从其他来源加载数据,您必须使用显式声明的安全过滤器链(通常是 Spring Security 的 FilterChainProxy)为了自定义 FilterSecurityInterceptor bean。你不能使用命名空间。然后,您将实现 FilterInvocationSecurityMetadataSource 以根据需要为特定的 FilterInvocation [1] 加载数据。一个非常基本的大纲看起来像这样:

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);
}
}

有关详细信息,请查看 DefaultFilterInvocationSecurityMetadataSource 的代码。

如何针对 LDAP 进行身份验证但从数据库加载用户角色?

LdapAuthenticationProvider bean(在 Spring Security 中处理普通的 LDAP 身份验证)配置了两个独立的策略接口,一种执行身份验证,另一种加载用户权限,分别称为 LdapAuthenticator 和 LdapAuthoritiesPopulator。DefaultLdapAuthoritiesPopulator 从 LDAP 目录加载用户权限,并具有各种配置参数以允许您指定应如何检索这些参数。

要改用 JDBC,您可以自己实现接口,使用适合您的模式的任何 SQL:

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));
}
});
}
}

然后将这种类型的 bean 添加到应用程序上下文中,并将其注入 LdapAuthenticationProvider。这在参考手册的 LDAP 章节中关于使用显式 Spring bean 配置 LDAP 的部分中进行了介绍。请注意,在这种情况下,您不能使用命名空间进行配置。您还应该查阅 Javadoc 以了解相关的类和接口。

我想修改由命名空间创建的 bean 的属性,但模式中没有任何内容支持它。

除了放弃使用命名空间,我还能做什么?

命名空间功能是有意限制的,因此它没有涵盖您可以使用普通 bean 执行的所有操作。如果你想做一些简单的事情,比如修改一个 bean,或者注入一个不同的依赖项,你可以通过在你的配置中添加一个 BeanPostProcessor 来做到这一点。更多信息可以在 Spring 参考手册中找到。为此,您需要了解创建了哪些 bean,因此您还应该阅读上述问题中关于命名空间如何映射到 Spring bean 的博客文章。

通常,您会将所需的功能添加到 BeanPostProcessor 的 postProcessBeforeInitialization 方法中。假设您要自定义 UsernamePasswordAuthenticationFilter 使用的 AuthenticationDetailsSource(由 form-login 元素创建)。您想从请求中提取名为 CUSTOM_HEADER 的特定标头,并在对用户进行身份验证时使用它。处理器类如下所示:

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;
}
}

然后,您将在应用程序上下文中注册此 bean。 Spring 将自动在应用程序上下文中定义的 bean 上调用它。

FilterInvocation 对象包含 HttpServletRequest,因此您可以获得 URL 或任何其他相关信息,您可以根据这些信息来决定返回的属性列表将包含什么。

4. 思考

4.1 是否在 URL 上设置权限?

Spring 的文档上写不要在 controller 层上进行权限空间,建议放在 service 上?

如果这么做呢? 在系统中,经常通过 URL 进行控制并与菜单一起控制,难道要进行一个新的权限分配方法。这样做符合要求不?

一、按照商品模块做初步划分:

  • 商品-admin:能对所有商品进行 add、update、del、search
  • 商品-owner : 只能对自己的商品进行处理
  • 商品-read: 可以对所有的商品进行查询,但是不能添加,也不能删除。

二、转化成 UrL 的页面,页面转换成菜单

  • 这样分配给用户,比较清晰。但是在内部,必须将每个页面上的内容与商品的权限分配做好处理。 这样的工作会很麻烦。