Spring Security 5.0.x 参考手册 【翻译自官方GIT-2018.06.12】

版本号:5.0.x 参考手册 【翻译自官方GIT - 2018.06.12】

Spring Security参考手册

Spring Security是一个强大且高度可定制的身份验证和访问控制框架。 这是保护基于Spring的应用程序的事实标准。

前言

Spring Security为基于Java EE的企业软件应用程序提供了全面的安全解决方案。 正如您在参考指南中发现的那样,我们试图为您提供一个有用且高度可配置的安全系统。

安全是一个不断变化的目标,追求全面的系统范围方法非常重要。 在安全圈中,我们鼓励您采用"layers of security",以便每个图层都尽可能保证安全,连续图层提供更高的安全性。 每个图层的"tighter"安全性,您的应用程序将更加健壮和安全。 在底层,您需要处理诸如传输安全和系统识别等问题,以缓解中间人攻击。 接下来,您通常会使用防火墙,可能是VPN或IP安全性,以确保只有经过授权的系统才能尝试连接。 在企业环境中,您可以部署一个DMZ,将面向公众的服务器与后端数据库和应用程序服务器分开。 您的操作系统也将扮演重要角色,解决诸如以无特权用户身份运行进程等问题并最大限度提高文件系统安全性。 操作系统通常也会配置自己的防火墙。 希望在这个过程中你会试图阻止对系统的拒绝服务和暴力攻击。 入侵检测系统对于监视和响应攻击也特别有用,因为这些系统能够采取保护措施,例如实时阻止侵入TCP / IP地址。 转移到更高层,您的Java虚拟机将有望配置为最大限度地减少授予不同Java类型的权限,然后您的应用程序将添加自己的问题特定于域的安全配置。 Spring Security使后面的这个领域 - 应用程序的安全性更容易。

当然,您需要正确处理上面提到的所有安全层,以及包含每个层的管理因素。 这些管理因素的非详尽清单将包括安全公告监控,修补,人员审查,审计,变更控制,工程管理系统,数据备份,灾难恢复,性能基准测试,负载监控,集中式日志记录,事件响应程序等。

由于Spring Security专注于帮助企业应用程序安全层,因此您会发现有多少不同的需求与业务问题域相同。 银行应用程序对电子商务应用程序有不同的需求。 电子商务应用程序对企业销售人员自动化工具有不同的需求。 这些自定义要求使应用程序安全性变得有趣,富有挑战性和有益。

请首先阅读入门。 这将向您介绍框架和基于命名空间的配置系统,您可以使用该系统快速启动和运行。 为了更好地理解Spring Security的工作原理以及您可能需要使用的某些类,请阅读体系结构和实现。 本指南的其余部分采用更传统的参考样式,旨在根据需要进行阅读。 我们还建议您尽可能多地阅读应用程序安全问题。 Spring Security不是解决所有安全问题的万能药。 从一开始,应用程序的设计就要考虑到安全性,这一点很重要。 试图改造它并不是一个好主意。 特别是,如果您正在构建Web应用程序,则应该意识到许多潜在的漏洞,例如跨站脚本,请求伪造和会话劫持,您应该从一开始就考虑这些漏洞。 OWASP网站(http://www.owasp.org/)维护着十大Web应用程序漏洞列表以及大量有用的参考信息。

我们希望您发现此参考指南很有用,我们欢迎您的反馈和suggestions

最后,欢迎来到Spring Security community

入门

本指南的后面部分提供了关于框架体系结构和实现类的深入讨论,您需要了解是否需要进行任何严格的自定义。 在这一部分中,我们将介绍Spring Security 4。 0,简要介绍一下该项目的历史,并对如何开始使用该框架稍微考虑一下。 特别是,我们将看看命名空间配置,与传统的Spring bean方法相比,它提供了一种更简单的保护应用程序的方法,您必须单独连接所有实现类。

我们还将看看可用的示例应用程序。 在阅读后面的章节之前,尝试运行这些内容并尝试一下这些内容 - 随着对框架理解的增加,您可以深入了解它们。 请同时查看http:// spring。 io / spring-security [项目网站],因为它有关于构建项目的有用信息,以及指向文章,视频和教程的链接。

介绍

什么是Spring Security?

Spring Security为基于Java EE的企业软件应用程序提供全面的安全服务。 特别强调支持使用Spring Framework构建的项目,Spring Framework是用于企业软件开发的领先Java EE解决方案。 如果您没有使用Spring开发企业应用程序,我们热烈鼓励您仔细研究它。 对Spring的一些熟悉 - 特别是依赖注入原则 - 将帮助您更轻松地熟悉Spring Security。

人们使用Spring Security的原因有很多,但大多数人在找到Java EE的Servlet规范或EJB规范的安全特性后,都没有深入了解典型企业应用场景所需的深度。 在提到这些标准的同时,重要的是要认识到它们在WAR或EAR级别不可移植。 因此,如果切换服务器环境,在新的目标环境中重新配置应用程序的安全性通常需要很多工作。 使用Spring Security克服了这些问题,并且还为您带来了许多其他有用的,可自定义的安全功能。

您可能知道应用程序安全性的两个主要方面是"authentication"和"authorization"(或"access-control")。 这是Spring Security的两大主要领域。 "Authentication"是建立委托人的过程是他们声称的("principal"通常是指可以在您的应用程序中执行操作的用户,设备或其他系统)。 "Authorization"指的是决定是否允许委托人在您的应用程序内执行操作的过程。 为了达到需要授权决定的地步,委托人的身份已经由认证过程确定。 这些概念很常见,并不完全针对Spring Security。

在认证级别,Spring Security支持多种认证模式。 这些认证模式大多由第三方提供,或者由相关标准组织(如互联网工程任务组)开发。 另外,Spring Security还提供了自己的一套认证功能。 具体而言,Spring Security目前支持与所有这些技术的认证集成:

  • HTTP BASIC验证头(基于IETF RFC的标准)

  • HTTP摘要式身份验证标头(基于IETF RFC的标准)

  • HTTP X.509客户端证书交换(基于IETF RFC的标准)

  • LDAP(跨平台认证需求的一种非常常见的方法,特别是在大型环境中)

  • 基于表单的身份验证(用于简单的用户界面需求)

  • OpenID身份验证

  • 基于预先建立的请求标头的认证(例如Computer Associates Siteminder)

  • Jasig中心身份验证服务(也称为CAS,它是一种流行的开源单点登录系统)

  • 远程方法调用(RMI)和HttpInvoker(Spring远程协议)的透明认证上下文传播

  • 自动"remember-me"身份验证(因此您可以勾选一个方框以避免在预定时间段内进行重新身份验证)

  • 匿名身份验证(允许每个未经身份验证的呼叫自动采用特定的安全身份)

  • 运行身份验证(如果一次调用应该使用不同的安全身份进行身份验证,这很有用)

  • Java认证和授权服务(JAAS)

  • Java EE容器认证(如果需要,您仍可以使用容器管理认证)

  • 的Kerberos

  • Java开源单一登录(JOSSO)*

  • OpenNMS网络管理平台*

  • AppFuse *

  • AndroMDA *

  • Mule ESB *

  • 直接Web请求(DWR)*

  • Grails *

  • 挂毯*

  • JTrac *

  • Jasypt *

  • 滚筒*

  • 弹性路径*

  • Atlassian人群*

  • 您自己的身份验证系统(请参阅下文)

(*表示由第三方提供)

许多独立软件供应商(ISV)都采用Spring Security,因为这种灵活的身份验证模型的选择非常重要。 这样做可以让他们快速地将他们的解决方案与他们最终客户需要的任何内容集成起来,而无需进行大量工程或要求客户改变其环境。 如果上述认证机制都不符合您的需求,Spring Security是一个开放平台,编写您自己的认证机制非常简单。 Spring Security的许多企业用户需要与不符合任何特定安全标准的"legacy"系统进行集成,而且Spring Security很高兴能够使用此类系统"play nicely"。

无论身份验证机制如何,Spring Security都提供了一套深层次的授权功能。 有三个主要的感兴趣领域:授权Web请求,授权是否可以调用方法并授权访问单个域对象实例。 为帮助您理解这些差异,请分别考虑Servlet规范Web模式安全性,EJB容器托管安全性和文件系统安全性中的授权功能。 Spring Security在所有这些重要领域提供了深入的功能,我们将在本参考指南的后面部分探讨这些功能。

历史

Spring Security于2003年末开始以"The Acegi Security System for Spring"开始。 Spring Developers的邮件列表上提出了一个问题,询问是否对基于Spring的安全实现给予了任何考虑。 当时Spring社区相对较小(特别是与今天的规模相比),而Spring的确只是从2003年初开始的SourceForge项目。 对这个问题的回应是,这是一个值得的领域,尽管目前缺乏时间阻碍了它的探索。

考虑到这一点,构建了一个简单的安全实现,而不是发布。 几个星期后,Spring社区的另一位成员询问了安全问题,并在当时向他们提供了这些代码。 接下来还有其他几个要求,到2004年1月,大约有20个人在使用这些代码。 这些先锋用户与其他人一起提出了一个建议SourceForge项目是有序的,该项目于2004年3月正式成立。

在那些早期,该项目没有任何自己的认证模块。 集装箱安全管理被用于认证过程,而Acegi Security则专注于授权。 这在一开始就很合适,但随着越来越多的用户请求额外的容器支持,容器特定身份验证领域接口的基本限制变得清晰起来。 还有一个相关的问题,即向容器的类路径添加新的JAR,这是最终用户混淆和错误配置的常见原因。

随后引入了Acegi安全特定的认证服务。 大约一年后,Acegi Security成为Spring Framework的正式子项目。 1。 0. 2006年5月发布了0最终版,经过两年半的积极应用于众多生产软件项目以及数百项改进和社区贡献。

Acegi Security于2007年底成为春季投资组合项目的官方项目,并更名为"Spring Security"。

今天,Spring Security拥有一个强大且活跃的开源社区。 在支持论坛上有数千条关于Spring Security的消息。 有一个积极的核心开发人员从事代码本身的工作,同时也是一个活跃的社区,他们也经常分享补丁和支持他们的同行。

发布编号

了解Spring Security发行版的工作原理非常有用,因为它可以帮助您确定迁移到项目未来版本所涉及的工作(或缺乏)。 每个版本使用一个标准的整数三元组:MAJOR.MINOR.PATCH。 目的是MAJOR版本不兼容,API的大规模升级。 MINOR版本应该在很大程度上保留与旧版次版本的源代码和二进制兼容性,认为可能会有一些设计更改和不兼容的更新。 PATCH级别应该完全兼容,前后颠倒,可能的例外是修改错误和缺陷的更改。

您受到更改影响的程度取决于您的代码的集成程度。 如果您正在进行大量定制,则与使用简单名称空间配置相比,您可能会受到更多的影响。

在推出新版本之前,您应该始终彻底测试您的应用程序。

获得Spring Security

你可以通过几种方式获得Spring Security。 您可以从主http:// spring下载打包发行版。 io / spring-security [Spring Security]页面中,从Maven Central存储库(或Spring Maven存储库中下载快照和里程碑版本)下载各个jar,或者,您可以自己从源代码构建项目。

使用Maven

最小的Spring Security Maven依赖关系集通常如下所示:

pom.xml
<dependencies>
<!-- ... other dependency elements ... -->
<dependency>
	<groupId>org.springframework.security</groupId>
	<artifactId>spring-security-web</artifactId>
	<version>{spring-security-version}</version>
</dependency>
<dependency>
	<groupId>org.springframework.security</groupId>
	<artifactId>spring-security-config</artifactId>
	<version>{spring-security-version}</version>
</dependency>
</dependencies>

如果您正在使用LDAP,OpenID等附加功能,则还需要包含相应的项目模块

Maven仓库

所有GA版本(即以.RELEASE结尾的版本)都将部署到Maven Central,因此您的POM中不需要声明其他Maven存储库。

如果您使用的是SNAPSHOT版本,则需要确保您已经定义了Spring Snapshot存储库,如下所示:

pom.xml
<repositories>
<!-- ... possibly other repository elements ... -->
<repository>
	<id>spring-snapshot</id>
	<name>Spring Snapshot Repository</name>
	<url>http://repo.spring.io/snapshot</url>
</repository>
</repositories>

如果您正在使用里程碑或候选版本,则需要确保您已经定义了Spring Milestone存储库,如下所示:

pom.xml
<repositories>
<!-- ... possibly other repository elements ... -->
<repository>
	<id>spring-milestone</id>
	<name>Spring Milestone Repository</name>
	<url>http://repo.spring.io/milestone</url>
</repository>
</repositories>
Spring框架

Spring Security针对Spring Framework {spring-version}构建,但应该可以在4.0.x中使用。 许多用户会遇到的问题是Spring Security的传递依赖关系解决了可能导致奇怪类路径问题的Spring Framework {spring-version}。

解决这个问题的一个(单调乏味的)方法是将所有Spring Framework模块包含在你的pom的 <dependencyManagement>部分中。 另一种方法是将spring-framework-bom包含在pom.xml<dependencyManagement>部分中,如下所示:

pom.xml
<dependencyManagement>
	<dependencies>
	<dependency>
		<groupId>org.springframework</groupId>
		<artifactId>spring-framework-bom</artifactId>
		<version>{spring-version}</version>
		<type>pom</type>
		<scope>import</scope>
	</dependency>
	</dependencies>
</dependencyManagement>

这将确保Spring Security的所有传递依赖使用Spring {spring-version}模块。

注意:此方法使用Maven的"bill of materials"(BOM)概念,并且仅在Maven 2.0.9+中提供。 有关如何解决依赖关系的更多详细信息,请参阅 Maven的介绍依赖机制文档

摇篮

最小的Spring Security Gradle依赖关系集通常如下所示:

build.gradle
dependencies {
	compile 'org.springframework.security:spring-security-web:{spring-security-version}'
	compile 'org.springframework.security:spring-security-config:{spring-security-version}'
}

如果您正在使用LDAP,OpenID等附加功能,则还需要包含相应的项目模块

Gradle存储库

所有GA版本(即以.RELEASE结尾的版本)都将部署到Maven Central,因此使用mavenCentral()存储库对于GA版本已足够。

build.gradle
repositories {
	mavenCentral()
}

如果您使用的是SNAPSHOT版本,则需要确保您已经定义了Spring Snapshot存储库,如下所示:

build.gradle
repositories {
	maven { url 'https://repo.spring.io/snapshot' }
}

如果您正在使用里程碑或候选版本,则需要确保您已经定义了Spring Milestone存储库,如下所示:

build.gradle
repositories {
	maven { url 'https://repo.spring.io/milestone' }
}
使用Spring 4.0.x和Gradle

默认情况下,Gradle将在解析传递版本时使用最新版本。 这意味着当使用Spring Framework {spring-version}运行Spring Security {spring-security-version}时,通常不需要额外的工作。 但是,有时可能会出现问题,因此最好使用 Gradle的ResolutionStrategy来缓解此问题,如下所示:

build.gradle
configurations.all {
	resolutionStrategy.eachDependency { DependencyResolveDetails details ->
		if (details.requested.group == 'org.springframework') {
			details.useVersion '{spring-version}'
		}
	}
}

这将确保Spring Security的所有传递依赖使用Spring {spring-version}模块。

注意:本例使用Gradle 1.9,但可能需要修改才能在将来的Gradle版本中使用,因为这是Gradle中的一项孵化功能。

项目模块

在Spring Security 3.0中,代码库被细分为独立的jar,这些jar更清楚地区分了不同的功能区域和第三方依赖关系。 如果您使用Maven构建项目,那么这些模块将添加到您的pom.xml中。 即使您不使用Maven,我们也建议您参考pom.xml文件以了解第三方依赖关系和版本。 或者,一个好主意是检查示例应用程序中包含的库。

核心 - spring-security-core.jar

包含核心认证和访问控制类和接口,远程处理支持和基本配置API。 由使用Spring Security的任何应用程序所要求。 支持独立应用程序,远程客户端,方法(服务层)安全性和JDBC用户配置。 包含顶级包:

  • org.springframework.security.core

  • org.springframework.security.access

  • org.springframework.security.authentication

  • org.springframework.security.provisioning

远程处理 - spring-security-remoting.jar

提供与Spring Remoting的集成。 除非你正在编写一个使用Spring Remoting的远程客户端,否则你不需要这个。 主包是org.springframework.security.remoting

Web - spring-security-web.jar

包含过滤器和相关的网络安全基础架构代码。 任何具有servlet API依赖性的东西。 如果您需要Spring Security Web认证服务和基于URL的访问控制,您将需要它。 主包是org.springframework.security.web

Config - spring-security-config.jar

包含安全名称空间解析代码和Java配置代码。 如果您使用Spring Security XML名称空间进行配置或Spring Security的Java配置支持,则需要它。 主包是org.springframework.security.config。 这些类别都不能直接用于应用程序。

LDAP - spring-security-ldap.jar

LDAP认证和供应代码。 如果您需要使用LDAP身份验证或管理LDAP用户条目,则为必需。 顶级包装是org.springframework.security.ldap

OAuth 2.0核心 - spring-security-oauth2-core.jar

spring-security-oauth2-core.jar包含为OAuth 2.0授权框架OpenID Connect Core 1.0提供支持的核心类和接口。 这是使用OAuth 2.0OpenID Connect Core 1.0的应用程序所必需的,例如客户端,资源服务器和授权服务器。 顶级包装是org.springframework.security.oauth2.core

OAuth 2.0客户端 - spring-security-oauth2-client.jar

spring-security-oauth2-client.jar是Spring Security对OAuth 2.0授权框架OpenID Connect Core 1.0的客户端支持。 由利用OAuth 2.0 Login和/或OAuth客户端支持的应用程序所需。 顶级包装是org.springframework.security.oauth2.client

OAuth 2.0 JOSE - spring-security-oauth2-jose.jar

spring-security-oauth2-jose.jar包含Spring Security对JOSE(Javascript对象签名和加密)框架的支持。 JOSE框架旨在提供一种安全地在各方之间传输声明的方法。 它由一系列规格构建而成:

  • JSON Web令牌(JWT)

  • JSON Web签名(JWS)

  • JSON Web加密(JWE)

  • JSON Web密钥(JWK)

它包含顶级软件包:

  • org.springframework.security.oauth2.jwt

  • org.springframework.security.oauth2.jose

ACL - spring-security-acl.jar

专门的域对象ACL实现。 用于将安全性应用于应用程序内的特定域对象实例。 顶级包装是org.springframework.security.acls

CAS - spring-security-cas.jar

Spring Security的CAS客户端集成。 如果您想使用CAS单点登录服务器的Spring Security Web认证。 顶级包装是org.springframework.security.cas

OpenID - spring-security-openid.jar

OpenID Web认证支持。 用于对外部OpenID服务器进行身份验证。 org.springframework.security.openid. 需要OpenID4Java。

测试 - spring-security-test.jar

支持使用Spring Security进行测试。

检出来源

由于Spring Security是一个开源项目,我们强烈建议您使用git来检查源代码。 这将使您可以完全访问所有示例应用程序,并且可以轻松构建项目的最新版本。 拥有项目的源代码对于调试也有很大的帮助。 异常堆栈跟踪不再是晦涩难解的黑盒问题,但您可以直接找到导致问题的线路并计算出发生的情况。 源代码是项目的最终文档,通常是查找实际内容的最简单方式。

要获得项目的源代码,请使用以下git命令:

git clone https://github.com/spring-projects/spring-security.git

这将使您可以访问本地计算机上的整个项目历史记录(包括所有版本和分支机构)。

Spring Security 5.1的新增功能

Spring Security 5.1提供了许多新功能。 以下是该版本的亮点。

新功能

  • 测试方法安全性

    • 支持在测试中设置SecurityContext时进行自定义 例如,@WithMockUser(setupBefore = TestExecutionEvent.TEST_EXECUTION)会在JUnit的@Before之后并且在执行测试之前设置一个用户。

    • @WithUserDetails现在可以与ReactiveUserDetailsService一起使用

  • [jackson] - 增加了对BadCredentialsException的支持

  • @ AuthenticationPrincipal

    • 支持在WebFlux中解析bean(已经在Spring MVC中受支持)

    • 支持解决WebFlux中的errorOnInvalidType(已经在Spring MVC中受支持)

样本和指南(从此处开始)

如果您想要开始使用Spring Security,最好的地方就是我们的示例应用程序。

。示例应用程序

来源说明指南

{gh-samples-url} / javaconfig / helloworld [Hello Spring Security]

演示如何将Spring Security与现有的使用基于Java的配置的应用程序集成。

你好春天安全指南

{gh-samples-url} / boot / helloworld [Hello Spring安全启动]

演示如何将Spring Security与现有的Spring Boot应用程序集成。

你好Spring安全启动指南

{gh-samples-url} / xml / helloworld [Hello Spring Security XML]

演示如何将Spring Security与使用基于XML的配置的现有应用程序集成。

Spring Security XML指南

{gh-samples-url} / javaconfig / hellomvc [Hello Spring MVC安全性]

演示如何将Spring Security与现有的Spring MVC应用程序集成。

Spring MVC安全指南

{gh-samples-url} / javaconfig / form [自定义登录表单]

演示如何创建自定义登录表单。

自定义登录表格指南

{gh-samples-url} / boot / oauth2login [OAuth 2.0登录]

演示如何将OAuth 2.0登录与OAuth 2.0或OpenID Connect 1.0 Provider集成。

OAuth 2.0登录指南

Java配置

在Spring 3.1中为Spring Framework添加了对 Java配置的一般支持。 自Spring Security 3.2以来,已经有了Spring Security Java Configuration支持,使用户无需使用任何XML即可轻松配置Spring Security。

如果您熟悉安全名称空间配置,那么您应该会发现它与Security Java Configuration支持之间的相似之处。

注:Spring Security提供了https://github.com/spring-projects/spring-security/tree/master/samples/javaconfig [示例应用程序],演示了如何使用Spring Security Java Configuration。

Hello Web安全Java配置

第一步是创建我们的Spring Security Java配置。 该配置会创建一个名为springSecurityFilterChain的Servlet过滤器,它负责应用程序中的所有安全性(保护应用程序URL,验证提交的用户名和密码,重定向到登录表单等)。 你可以在下面找到Spring Security Java Configuration的最基本的例子:

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.context.annotation.*;
import org.springframework.security.config.annotation.authentication.builders.*;
import org.springframework.security.config.annotation.web.configuration.*;

@EnableWebSecurity
public class WebSecurityConfig implements WebMvcConfigurer {

	@Bean
	public UserDetailsService userDetailsService() throws Exception {
		InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
		manager.createUser(User.withDefaultPasswordEncoder().username("user").password("password").roles("USER").build());
		return manager;
	}
}

这个配置真的没有太多,但它确实很多。 您可以在下面找到以下功能的摘要:

AbstractSecurityWebApplicationInitializer

下一步是向战争注册{{0}。 这可以在Servlet 3.0+环境中的 Spring的WebApplicationInitializer支持的Java配置中完成。 不出所料,Spring Security提供了一个基类AbstractSecurityWebApplicationInitializer,可确保springSecurityFilterChain为您注册。 我们使用AbstractSecurityWebApplicationInitializer的方式取决于我们是否已经使用Spring,或者Spring Security是我们应用程序中唯一的Spring组件。

不存在Spring的==== AbstractSecurityWebApplicationInitializer

如果您没有使用Spring或Spring MVC,则需要将WebSecurityConfig传递给超类,以确保获取配置。 你可以在下面找到一个例子:

import org.springframework.security.web.context.*;

public class SecurityWebApplicationInitializer
	extends AbstractSecurityWebApplicationInitializer {

	public SecurityWebApplicationInitializer() {
		super(WebSecurityConfig.class);
	}
}

SecurityWebApplicationInitializer将执行以下操作:

  • 为应用程序中的每个URL自动注册springSecurityFilterChain过滤器

  • 添加加载WebSecurityConfig的ContextLoaderListener。

具有Spring MVC的AbstractSecurityWebApplicationInitializer。====

如果我们在应用程序的其他地方使用Spring,我们可能已经有一个加载我们的Spring配置的WebApplicationInitializer。 如果我们使用以前的配置,我们会得到一个错误。 相反,我们应该使用现有的ApplicationContext注册Spring Security。 例如,如果我们使用Spring MVC,我们的SecurityWebApplicationInitializer将如下所示:

import org.springframework.security.web.context.*;

public class SecurityWebApplicationInitializer
	extends AbstractSecurityWebApplicationInitializer {

}

这只会为应用程序中的每个URL注册springSecurityFilterChain过滤器。 之后,我们将确保WebSecurityConfig在我们现有的ApplicationInitializer中加载。 例如,如果我们使用Spring MVC,它将被添加到getRootConfigClasses()

public class MvcWebApplicationInitializer extends
		AbstractAnnotationConfigDispatcherServletInitializer {

	@Override
	protected Class<?>[] getRootConfigClasses() {
		return new Class[] { WebSecurityConfig.class };
	}

	// ... other overrides ...
}

HttpSecurity

迄今为止,我们的WebSecurityConfig仅包含有关如何验证用户的信息。 Spring Security如何知道我们想要求所有用户进行身份验证? Spring Security如何知道我们想要支持基于表单的身份验证?原因是WebSecurityConfigurerAdapterconfigure(HttpSecurity http)方法中提供了一个默认配置,如下所示:

protected void configure(HttpSecurity http) throws Exception {
	http
		.authorizeRequests()
			.anyRequest().authenticated()
			.and()
		.formLogin()
			.and()
		.httpBasic();
}

上面的默认配置:

  • 确保对我们应用程序的任何请求都要求用户进行身份验证

  • 允许用户使用基于表单的登录进行身份验证

  • 允许用户使用HTTP基本身份验证进行身份验证

您会注意到这个配置与XML命名空间配置非常相似:

<http>
	<intercept-url pattern="/**" access="authenticated"/>
	<form-login />
	<http-basic />
</http>

关闭XML标签的Java配置等同于使用允许我们继续配置父项的and()方法表示。 如果你阅读代码,这也是有道理的。 我想配置授权请求并配置表单登录and__配置HTTP基本认证。

Java配置和表单登录

由于我们没有提及任何HTML文件或JSP,因此您可能想知道登录表单从何时被提示登录。 由于Spring Security的默认配置没有明确设置登录页面的URL,因此Spring Security会根据启用的功能自动生成一个URL,并使用处理提交的登录的URL的标准值,用户将默认的目标URL登录后发送到等等。

尽管自动生成的登录页面很方便快速启动和运行,但大多数应用程序都希望提供自己的登录页面。 为此,我们可以更新我们的配置,如下所示:

protected void configure(HttpSecurity http) throws Exception {
	http
		.authorizeRequests()
			.anyRequest().authenticated()
			.and()
		.formLogin()
			.loginPage("/login") 
			.permitAll();        
}

<1>更新后的配置指定登录页面的位置。 <2>我们必须授予所有用户(即未经身份验证的用户)访问我们的登录页面。formLogin().permitAll()方法允许所有用户访问与基于表单的登录相关的所有URL。

下面是一个使用JSP实现的用于当前配置的登录页面示例:

注意:下面的登录页面代表我们当前的配置。 如果某些默认设置不能满足我们的需求,我们可以轻松更新我们的配置。

<c:url value="/login" var="loginUrl"/>
<form action="${loginUrl}" method="post">       
	<c:if test="${param.error != null}">        
		<p>
			Invalid username and password.
		</p>
	</c:if>
	<c:if test="${param.logout != null}">       
		<p>
			You have been logged out.
		</p>
	</c:if>
	<p>
		<label for="username">Username</label>
		<input type="text" id="username" name="username"/>	
	</p>
	<p>
		<label for="password">Password</label>
		<input type="password" id="password" name="password"/>	
	</p>
	<input type="hidden"                        
		name="${_csrf.parameterName}"
		value="${_csrf.token}"/>
	<button type="submit" class="btn">Log in</button>
</form>

<1>发布到/login网址的邮件将尝试对用户进行身份验证 <2>如果查询参数error存在,则认证尝试失败 <3>如果查询参数logout存在,用户已成功注销 <4>用户名必须作为名为username的HTTP参数存在 <5>密码必须以名为password的HTTP参数 <6>我们必须包含CSRF令牌要了解更多信息,请阅读参考文献的跨网站请求伪造(CSRF)部分

授权请求

我们的示例只需要用户进行身份验证,并已为我们的应用程序中的每个URL完成此操作。 我们可以通过向我们的http.authorizeRequests()方法添加多个子项来为我们的网址指定自定义要求。 例如:

protected void configure(HttpSecurity http) throws Exception {
	http
		.authorizeRequests()                                                                
			.antMatchers("/resources/**", "/signup", "/about").permitAll()                  
			.antMatchers("/admin/**").hasRole("ADMIN")                                      
			.antMatchers("/db/**").access("hasRole('ADMIN') and hasRole('DBA')")            
			.anyRequest().authenticated()                                                   
			.and()
		// ...
		.formLogin();
}
 http.authorizeRequests()方法有多个孩子,每个匹配者按照他们声明的顺序考虑。 <2>我们指定了任何用户都可以访问的多个网址格式。 具体而言,如果网址以"/resources/"开头,等于"/signup"或等于"/about",任何用户都可以访问请求。 <3>任何以"/admin/"开头的网址将仅限于拥有角色"ROLE_ADMIN"的用户。 您会注意到,由于我们调用了hasRole方法,因此我们不需要指定"ROLE_"前缀。 <4>任何以"/db/"开头的网址都需要用户同时拥有"ROLE_ADMIN"和"ROLE_DBA"。 您会注意到,由于我们使用的是hasRole表达式,因此无需指定"ROLE_"前缀。 <5>任何尚未匹配的网址只要求用户进行身份验证

处理注销

使用WebSecurityConfigurerAdapter时,会自动应用注销功能。 默认情况下,访问网址/logout将通过以下方式登录用户:

  • 使HTTP会话无效

  • 清理已配置的任何RememberMe认证

  • 清除SecurityContextHolder

  • 重定向到/login?logout

但是,类似于配置登录功能,您还可以有多种选项来进一步自定义注销要求:

protected void configure(HttpSecurity http) throws Exception {
	http
		.logout()                                                                
			.logoutUrl("/my/logout")                                                 
			.logoutSuccessUrl("/my/index")                                           
			.logoutSuccessHandler(logoutSuccessHandler)                              
			.invalidateHttpSession(true)                                             
			.addLogoutHandler(logoutHandler)                                         
			.deleteCookies(cookieNamesToClear)                                       
			.and()
		...
}

<1>提供注销支持。 这在使用WebSecurityConfigurerAdapter时会自动应用。 <2>触发注销的URL(默认为/logout)。 如果启用CSRF保护(默认),则该请求也必须是POST。 有关更多信息,请参阅http://docs.spring.io/spring-security/site/docs/current/apidocs/ org / springframework / security / config / annotation / web / configurers / LogoutConfigurer.html#logoutUrl-java.lang.String- [JavaDoc]。 <3>发生注销后重定向到的URL。 默认值是/login?logout。 有关更多信息,请参阅http://docs.spring.io/spring-security/site/docs/current/apidocs/ org / springframework / security / config / annotation / web / configurers / LogoutConfigurer.html#logoutSuccessUrl-java.lang.String- [JavaDoc]。 <4>让我们指定一个自定义LogoutSuccessHandler。 如果指定,则logoutSuccessUrl()被忽略。 有关更多信息,请参阅http://docs.spring.io/spring-security/site/docs/current/apidocs/ org / springframework / security / config / annotation / web / configurers / LogoutConfigurer.html#logoutSuccessHandler-org.springframework.security.web.authentication.logout.LogoutSuccessHandler- [JavaDoc ]。 <5>指定在注销时是否使HttpSession无效。 这是默认的true。 配置封面下的{{0}。 有关更多信息,请参阅http://docs.spring.io/spring-security/site/docs/current/apidocs/ org / springframework / security / config / annotation / web / configurers / LogoutConfigurer.html#invalidateHttpSession-boolean- [JavaDoc]。 <6>添加一个LogoutHandler SecurityContextLogoutHandler默认添加为最后一个LogoutHandler。 <7>允许指定在注销成功时删除的cookies的名称。 这是明确添加CookieClearingLogoutHandler的快捷方式。

 

注销当然也可以使用XML名称空间表示法进行配置。 有关更多详细信息,请参阅Spring Security XML命名空间部分中logout element的文档。

通常,为了自定义注销功能,您可以添加 LogoutHandler 和/或 LogoutSuccessHandler 实现。 对于很多常见的场景,这些处理程序都是根据 涵盖了何时使用流利的API。

LogoutHandler

通常,LogoutHandler 实现指示能够参与注销处理的类。 预计它们将被调用来执行必要的清理。 因此他们应该 不会抛出异常。 提供了各种实现:

  • {安全API-URL}组织/ springframework的/安全/网络/认证/了rememberMe / PersistentTokenBasedRememberMeServices.html [对PersistentTokenBasedRememberMeServices]

  • {安全API-URL}组织/ springframework的/安全/网络/认证/了rememberMe / TokenBasedRememberMeServices.html [TokenBasedRememberMeServices]

  • {安全API-URL}组织/ springframework的/安全/网络/认证/注销/ CookieClearingLogoutHandler.html [CookieClearingLogoutHandler]

  • {安全API-URL}组织/ springframework的/安全/网络/ CSRF / CsrfLogoutHandler.html [CsrfLogoutHandler]

  • {安全API-URL}组织/ springframework的/安全/网络/认证/注销/ SecurityContextLogoutHandler.html [SecurityContextLogoutHandler]

有关详细信息,请参阅记住我的接口和实现

Fluent API不是直接提供LogoutHandler实现,而是提供了快捷方式,它们提供了相应的LogoutHandler实现。 例如。 deleteCookies()允许指定在注销成功时删除一个或多个Cookie的名称。 与添加CookieClearingLogoutHandler相比,这是一条捷径。

LogoutSuccessHandler

LogoutFilter成功注销后调用LogoutSuccessHandler,以处理例如 重定向或转发到适当的目的地。 请注意,界面与LogoutHandler几乎相同,但可能会引发异常。

提供了以下实现:

  • {安全API-URL}组织/ springframework的/安全/网络/认证/注销/ SimpleUrlLogoutSuccessHandler.html [SimpleUrlLogoutSuccessHandler]

  • HttpStatusReturningLogoutSuccessHandler

如上所述,您不需要直接指定SimpleUrlLogoutSuccessHandler。 相反,流利的API通过设置logoutSuccessUrl()来提供快捷方式。 这将设置封面下的SimpleUrlLogoutSuccessHandler。 提供的URL将在注销发生后重定向到。 默认值是/login?logout

REST API类型场景中的HttpStatusReturningLogoutSuccessHandler可能很有趣。 取而代之的是在成功注销后重定向到URL,这个LogoutSuccessHandler允许您提供一个简单的HTTP状态代码来返回。 如果未配置,则缺省情况下将返回状态代码200。

更多注销相关参考

WebFlux安全

Spring Security的WebFlux支持依赖于WebFilter,并且对于Spring WebFlux和Spring WebFlux.Fn也是如此。 你可以找到几个示例应用程序来演示下面的代码:

  • Hello WebFlux {gh-samples-url} / javaconfig / hellowebflux [hellowebflux]

  • Hello WebFlux.Fn {gh-samples-url} / javaconfig / hellowebfluxfn [hellowebfluxfn]

  • Hello WebFlux方法{gh-samples-url} / javaconfig / hellowebflux-method [hellowebflux-method]

最小的WebFlux安全配置

您可以在下面找到最低限度的WebFlux安全配置:

@EnableWebFluxSecurity
public class HelloWebfluxSecurityConfig {

	@Bean
	public MapReactiveUserDetailsService userDetailsService() {
		UserDetails user = User.withDefaultPasswordEncoder()
			.username("user")
			.password("user")
			.roles("USER")
			.build();
		return new MapReactiveUserDetailsService(user);
	}
}

此配置提供表单和http基本身份验证,设置授权以要求经过身份验证的用户访问任何页面,设置默认登录页面和默认注销页面,设置与安全性相关的HTTP标头,CSRF保护等。

明确的WebFlux安全配置

您可以在下面找到最小WebFlux安全配置的显式版本:

@EnableWebFluxSecurity
public class HelloWebfluxSecurityConfig {

	@Bean
	public MapReactiveUserDetailsService userDetailsService() {
		UserDetails user = User.withDefaultPasswordEncoder()
			.username("user")
			.password("user")
			.roles("USER")
			.build();
		return new MapReactiveUserDetailsService(user);
	}

	@Bean
	public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
		http
			.authorizeExchange()
				.anyExchange().authenticated()
				.and()
			.httpBasic().and()
			.formLogin();
		return http.build();
	}
}

该配置明确设置了与我们最小配置相同的所有内容。 从这里您可以轻松地对默认值进行更改。

OAuth 2.0登录

OAuth 2.0登录功能为应用程序提供了让用户通过在OAuth 2.0提供者处使用其现有帐户登录应用程序的功能(例如, GitHub)或OpenID Connect 1.0 Provider(例如Google)。 OAuth 2.0 Login实现了用例:"Login with Google"或"Login with GitHub"。

注意:通过使用https://tools.ietf.org/html/rfc6749#section-4.1[OAuth 2.0授权框架]和 OpenID Connect Core 1.0中指定的Authorization Code Grant实现OAuth 2.0登录。

Spring Boot 2.0示例

Spring Boot 2.0为OAuth 2.0登录带来了全面的自动配置功能。

本节介绍如何使用Google作为Authentication Provider配置{gh-samples-url} / boot / oauth2login [OAuth 2.0 Login sample],并涵盖以下主题:

初始设置

要使用Google的OAuth 2.0身份验证系统进行登录,您必须在Google API控制台中设置一个项目以获取OAuth 2.0凭据。

注意:用于身份验证的https://developers.google.com/identity/protocols/OpenIDConnect[Google' OAuth 2.0实现]符合 OpenID Connect 1.0规范,并且是 OpenID认证

按照https://developers.google.com/identity/protocols/OpenIDConnect[OpenID Connect]页上的说明操作,从"Setting up OAuth 2.0"部分开始。

完成"Obtain OAuth 2.0 credentials"指示后,您应该拥有一个新的OAuth客户端,其凭据由客户端ID和客户端密钥组成。

设置重定向URI

重定向URI是应用程序中的路径,用户的用户代理在与Google进行身份验证并授予对“同意”页面上的OAuth客户端created in the previous step的访问权之后,会将其重定向回。

在"Set a redirect URI"小节中,确保Authorized redirect URIs字段设置为http://localhost:8080/login/oauth2/code/google

提示:默认重定向URI模板为{baseUrl}/login/oauth2/code/{registrationId} registrationIdClientRegistration的唯一标识符。

配置application.yml

现在,您已经拥有了一个带有Google的新OAuth客户端,您需要配置该应用程序以使用OAuth客户端作为authentication flow。 要做到这一点:

  1. 转到application.yml并设置以下配置:

    spring:
      security:
        oauth2:
          client:
            registration:	
              google:	
                client-id: google-client-id
                client-secret: google-client-secret
    Example 1. OAuth客户端属性
     spring.security.oauth2.client.registration是OAuth客户端属性的基本属性前缀。 <2>基本属性前缀后面是ClientRegistration的ID,例如google。
  2. 用您之前创建的OAuth 2.0凭据替换client-idclient-secret属性中的值。

启动应用程序

启动Spring Boot 2.0示例并转至http://localhost:8080。 然后,您被重定向到默认的auto-generated登录页面,该页面显示Google的链接。

点击Google链接,然后重定向到Google进行身份验证。

在使用Google帐户凭证进行身份验证后,向您呈现的下一页是“同意”屏幕。 “同意”屏幕会要求您允许或拒绝访问之前创建的OAuth客户端。 点击Allow,授权OAuth客户端访问您的电子邮件地址和基本配置文件信息。

此时,OAuth客户端会从 UserInfo端点中检索您的电子邮件地址和基本配置文件信息,并建立经过验证的会话。

ClientRegistration

ClientRegistration是向OAuth 2.0或OpenID Connect 1.0 Provider注册的客户端的代表。

客户端注册保存信息,如客户端ID,客户端密码, 授权授权类型,重定向URI,范围,授权URI,令牌URI和其他详细信息。

ClientRegistration及其属性定义如下:

public final class ClientRegistration {
	private String registrationId;	
	private String clientId;	
	private String clientSecret;	
	private ClientAuthenticationMethod clientAuthenticationMethod;	
	private AuthorizationGrantType authorizationGrantType;	
	private String redirectUriTemplate;	
	private Set<String> scopes;	
	private ProviderDetails providerDetails;
	private String clientName;	

	public class ProviderDetails {
		private String authorizationUri;	
		private String tokenUri;	
		private UserInfoEndpoint userInfoEndpoint;
		private String jwkSetUri;	

		public class UserInfoEndpoint {
			private String uri;	
			private String userNameAttributeName;	

		}
	}
}
 registrationId:唯一标识ClientRegistration的标识。
 clientId:客户端标识符。
 clientSecret:客户端秘密。
 clientAuthenticationMethod:用于向提供者验证客户端的方法。 支持的值是basicpost
 authorizationGrantType:OAuth 2.0授权框架定义了四个https://tools.ietf.org/html/rfc6749#section-1.3 [授权]类型。 支持的值是authorization_code和隐式。
 redirectUriTemplate:客户端注册的重定向URI,Authorization Server重定向最终用户的用户代理 到最终用户已验证并授权访问客户端后。 默认的重定向URI模板是{baseUrl}/login/oauth2/code/{registrationId},它支持URI模板变量。
 scopes:客户端在授权请求流程中请求的范围,例如openid,电子邮件或配置文件。
 clientName:用于客户端的描述性名称。 该名称可用于某些情况下,例如在自动生成的登录页面中显示客户端的名称时。
 authorizationUri:授权服务器的授权端点URI。
 tokenUri:授权服务器的令牌端点URI。
 jwkSetUri:用于检索https://tools.ietf.org/html/rfc7517[JSON Web Key(JWK)]的URI从授权服务器设置, ,其中包含用于验证ID令牌的https://tools.ietf.org/html/rfc7515[JSON Web签名(JWS)]的加密密钥以及可选的UserInfo响应。
 (userInfoEndpoint)uri:UserInfo端点URI,用于访问经过身份验证的最终用户的声明/属性。
 userNameAttributeName:在UserInfo Response中返回的属性的名称,该名称引用最终用户的名称或标识符。

Spring Boot 2.0属性映射

下表概述了Spring Boot 2.0 OAuth客户端属性到ClientRegistration属性的映射。

Spring Boot 2.0ClientRegistration

spring.security.oauth2.client.registration.[registrationId]

registrationId

spring.security.oauth2.client.registration.[registrationId].client-id

clientId

spring.security.oauth2.client.registration.[registrationId].client-secret

clientSecret

spring.security.oauth2.client.registration.[registrationId].client-authentication-method

clientAuthenticationMethod

spring.security.oauth2.client.registration.[registrationId].authorization-grant-type

authorizationGrantType

spring.security.oauth2.client.registration.[registrationId].redirect-uri-template

redirectUriTemplate

spring.security.oauth2.client.registration.[registrationId].scope

scopes

spring.security.oauth2.client.registration.[registrationId].client-name

clientName

spring.security.oauth2.client.provider.[providerId].authorization-uri

providerDetails.authorizationUri

spring.security.oauth2.client.provider.[providerId].token-uri

providerDetails.tokenUri

spring.security.oauth2.client.provider.[providerId].jwk-set-uri

providerDetails.jwkSetUri

spring.security.oauth2.client.provider.[providerId].user-info-uri

providerDetails.userInfoEndpoint.uri

spring.security.oauth2.client.provider.[providerId].userNameAttribute

providerDetails.userInfoEndpoint.userNameAttributeName

ClientRegistrationRepository

ClientRegistrationRepository充当OAuth 2.0 / OpenID Connect 1.0 ClientRegistration(s)的存储库。

 客户端注册信息最终由关联的授权服务器存储和拥有。 该存储库提供了检索主要客户端注册信息的子集的能力, 它与授权服务器一起存储。

Spring Boot 2.0自动配置绑定spring.security.oauth2.client.registration.[registrationId]下的每个属性 到ClientRegistration的实例,然后在ClientRegistrationRepository内组合ClientRegistration个实例中的每个实例。

 ClientRegistrationRepository的默认实现是InMemoryClientRegistrationRepository

自动配置还将ClientRegistrationRepository注册为ApplicationContext中的@Bean 以便它可用于依赖注入,如果应用程序需要的话。

以下列表显示了一个示例:

@Controller
public class OAuth2LoginController {

	@Autowired
	private ClientRegistrationRepository clientRegistrationRepository;

	@RequestMapping("/")
	public String index() {
		ClientRegistration googleRegistration =
			this.clientRegistrationRepository.findByRegistrationId("google");

		...

		return "index";
	}
}

CommonOAuth2Provider

CommonOAuth2Provider为众多知名供应商预先定义了一组默认客户端属性:Google,GitHub,Facebook和Okta。

例如,authorization-uritoken-uriuser-info-uri不会经常更改提供者。 因此,提供默认值以减少所需的配置是有意义的。

如前所述,当我们configured a Google client时,只需要client-idclient-secret属性。

以下列表显示了一个示例:

spring:
  security:
    oauth2:
      client:
        registration:
          google:
            client-id: google-client-id
            client-secret: google-client-secret
 由于registrationIdgoogle)与CommonOAuth2Provider中的GOOGLE enum(不区分大小写)匹配,所以客户端属性的自动默认功能无缝工作。

对于您可能需要指定不同的registrationId的情况,例如google-login, 您仍然可以通过配置provider属性来利用客户端属性的自动默认功能。

以下列表显示了一个示例:

spring:
  security:
    oauth2:
      client:
        registration:
          google-login:	
            provider: google	
            client-id: google-client-id
            client-secret: google-client-secret
 registrationId设置为google-login
 provider属性设置为google,该属性将利用CommonOAuth2Provider.GOOGLE.getBuilder()中设置的客户端属性的自动默认设置。

配置自定义提供程序属性

有一些OAuth 2.0提供商支持多租户,这会为每个租户(或子域)生成不同的协议端点。

例如,向Okta注册的OAuth客户端分配给特定的子域,并拥有自己的协议端点。

对于这些情况,Spring Boot 2.0为配置自定义提供程序属性提供以下基本属性:spring.security.oauth2.client.provider.[providerId]

以下列表显示了一个示例:

spring:
  security:
    oauth2:
      client:
        registration:
          okta:
            client-id: okta-client-id
            client-secret: okta-client-secret
        provider:
          okta:	
            authorization-uri: https://your-subdomain.oktapreview.com/oauth2/v1/authorize
            token-uri: https://your-subdomain.oktapreview.com/oauth2/v1/token
            user-info-uri: https://your-subdomain.oktapreview.com/oauth2/v1/userinfo
            user-name-attribute: sub
            jwk-set-uri: https://your-subdomain.oktapreview.com/oauth2/v1/keys

<1>基本属性(spring.security.oauth2.client.provider.okta)允许自定义配置协议端点位置。

重写Spring Boot 2.0自动配置

用于OAuth客户端支持的Spring Boot 2.0自动配置类为OAuth2ClientAutoConfiguration

它执行以下任务:

  • 从配置的OAuth客户端属性中注册由ClientRegistration(s)组成的ClientRegistrationRepository @Bean

  • 提供WebSecurityConfigurerAdapter @Configuration并通过httpSecurity.oauth2Login()启用OAuth 2.0登录。

如果您需要根据您的特定要求覆盖自动配置,可以通过以下方式进行:

注册一个ClientRegistrationRepository @Bean

以下示例显示如何注册ClientRegistrationRepository @Bean

@Configuration
public class OAuth2LoginConfig {

	@Bean
	public ClientRegistrationRepository clientRegistrationRepository() {
		return new InMemoryClientRegistrationRepository(this.googleClientRegistration());
	}

	private ClientRegistration googleClientRegistration() {
		return ClientRegistration.withRegistrationId("google")
			.clientId("google-client-id")
			.clientSecret("google-client-secret")
			.clientAuthenticationMethod(ClientAuthenticationMethod.BASIC)
			.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
			.redirectUriTemplate("{baseUrl}/login/oauth2/code/{registrationId}")
			.scope("openid", "profile", "email", "address", "phone")
			.authorizationUri("https://accounts.google.com/o/oauth2/v2/auth")
			.tokenUri("https://www.googleapis.com/oauth2/v4/token")
			.userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo")
			.userNameAttributeName(IdTokenClaimNames.SUB)
			.jwkSetUri("https://www.googleapis.com/oauth2/v3/certs")
			.clientName("Google")
			.build();
	}
}
提供WebSecurityConfigurerAdapter

以下示例显示如何为WebSecurityConfigurerAdapter提供@EnableWebSecurity,并通过httpSecurity.oauth2Login()启用OAuth 2.0登录:

@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {

	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http
			.authorizeRequests()
				.anyRequest().authenticated()
				.and()
			.oauth2Login();
	}
}
完全覆盖自动配置

以下示例显示了如何通过注册ClientRegistrationRepository @Bean并提供WebSecurityConfigurerAdapter完全覆盖自动配置,两者均在前两节中进行了介绍。

@Configuration
public class OAuth2LoginConfig {

	@EnableWebSecurity
	public static class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {

		@Override
		protected void configure(HttpSecurity http) throws Exception {
			http
				.authorizeRequests()
					.anyRequest().authenticated()
					.and()
				.oauth2Login();
		}
	}

	@Bean
	public ClientRegistrationRepository clientRegistrationRepository() {
		return new InMemoryClientRegistrationRepository(this.googleClientRegistration());
	}

	private ClientRegistration googleClientRegistration() {
		return ClientRegistration.withRegistrationId("google")
			.clientId("google-client-id")
			.clientSecret("google-client-secret")
			.clientAuthenticationMethod(ClientAuthenticationMethod.BASIC)
			.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
			.redirectUriTemplate("{baseUrl}/login/oauth2/code/{registrationId}")
			.scope("openid", "profile", "email", "address", "phone")
			.authorizationUri("https://accounts.google.com/o/oauth2/v2/auth")
			.tokenUri("https://www.googleapis.com/oauth2/v4/token")
			.userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo")
			.userNameAttributeName(IdTokenClaimNames.SUB)
			.jwkSetUri("https://www.googleapis.com/oauth2/v3/certs")
			.clientName("Google")
			.build();
	}
}

没有Spring Boot 2.0的==== Java配置

如果您无法使用Spring Boot 2.0并希望在CommonOAuth2Provider中配置一个预定义提供程序(例如Google),请应用以下配置:

@Configuration
public class OAuth2LoginConfig {

	@EnableWebSecurity
	public static class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {

		@Override
		protected void configure(HttpSecurity http) throws Exception {
			http
				.authorizeRequests()
					.anyRequest().authenticated()
					.and()
				.oauth2Login();
		}
	}

	@Bean
	public ClientRegistrationRepository clientRegistrationRepository() {
		return new InMemoryClientRegistrationRepository(this.googleClientRegistration());
	}

	@Bean
	public OAuth2AuthorizedClientService authorizedClientService() {
		return new InMemoryOAuth2AuthorizedClientService(this.clientRegistrationRepository());
	}

	private ClientRegistration googleClientRegistration() {
		return CommonOAuth2Provider.GOOGLE.getBuilder("google")
			.clientId("google-client-id")
			.clientSecret("google-client-secret")
			.build();
	}
}

OAuth2AuthorizedClient / OAuth2AuthorizedClientService

OAuth2AuthorizedClient是授权客户端的代表。 当最终用户(资源所有者)授予客户端访问其受保护资源的权限时,客户端被认为是被授权的。

OAuth2AuthorizedClient用于将OAuth2AccessTokenClientRegistration(客户端)和资源所有者关联起来,该所有者是授予授权的Principal最终用户。

OAuth2AuthorizedClientService的主要作用是管理OAuth2AuthorizedClient个实例。 从开发人员的角度来看,它提供了查找与客户端关联的OAuth2AccessToken的功能,以便它可以用来发起对资源服务器的请求。

 Spring Boot 2.0自动配置在ApplicationContext中注册OAuth2AuthorizedClientService @Bean

开发人员还可以在ApplicationContext(覆盖Spring Boot 2.0自动配置)中注册OAuth2AuthorizedClientService@Bean,以便能够查找与特定相关的OAuth2AccessToken ClientRegistration(客户端)。

以下列表显示了一个示例:

@Controller
public class OAuth2LoginController {

	@Autowired
	private OAuth2AuthorizedClientService authorizedClientService;

	@RequestMapping("/userinfo")
	public String userinfo(OAuth2AuthenticationToken authentication) {
		// authentication.getAuthorizedClientRegistrationId() returns the
		// registrationId of the Client that was authorized during the Login flow
		OAuth2AuthorizedClient authorizedClient =
			this.authorizedClientService.loadAuthorizedClient(
				authentication.getAuthorizedClientRegistrationId(),
				authentication.getName());

		OAuth2AccessToken accessToken = authorizedClient.getAccessToken();

		...

		return "userinfo";
	}
}

认证

到目前为止,我们只看到了最基本的认证配置。 让我们来看看几个稍微更高级的配置认证选项。

内存认证

我们已经看到了为单个用户配置内存认证的例子。 以下是配置多个用户的示例:

@Bean
public UserDetailsService userDetailsService() throws Exception {
	// ensure the passwords are encoded properly
	UserBuilder users = User.withDefaultPasswordEncoder();
	InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
	manager.createUser(users.username("user").password("password").roles("USER").build());
	manager.createUser(users.username("admin").password("password").roles("USER","ADMIN").build());
	return manager;
}

JDBC身份验证

您可以找到更新以支持基于JDBC的身份验证。 以下示例假定您已经在应用程序中定义了DataSource jdbc-javaconfig示例提供了使用基于JDBC的身份验证的完整示例。

@Autowired
private DataSource dataSource;

@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
	// ensure the passwords are encoded properly
	UserBuilder users = User.withDefaultPasswordEncoder();
	auth
		.jdbcAuthentication()
			.dataSource(dataSource)
			.withDefaultSchema()
			.withUser(users.username("user").password("password").roles("USER"))
			.withUser(users.username("admin").password("password").roles("USER","ADMIN"));
}

LDAP身份验证

您可以找到更新以支持基于LDAP的身份验证。 ldap-javaconfig示例提供了使用基于LDAP的身份验证的完整示例。

@Autowired
private DataSource dataSource;

@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
	auth
		.ldapAuthentication()
			.userDnPatterns("uid={0},ou=people")
			.groupSearchBase("ou=groups");
}

上面的示例使用以下LDIF和嵌入式Apache DS LDAP实例。

users.ldif
dn: ou=groups,dc=springframework,dc=org
objectclass: top
objectclass: organizationalUnit
ou: groups

dn: ou=people,dc=springframework,dc=org
objectclass: top
objectclass: organizationalUnit
ou: people

dn: uid=admin,ou=people,dc=springframework,dc=org
objectclass: top
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
cn: Rod Johnson
sn: Johnson
uid: admin
userPassword: password

dn: uid=user,ou=people,dc=springframework,dc=org
objectclass: top
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
cn: Dianne Emu
sn: Emu
uid: user
userPassword: password

dn: cn=user,ou=groups,dc=springframework,dc=org
objectclass: top
objectclass: groupOfNames
cn: user
uniqueMember: uid=admin,ou=people,dc=springframework,dc=org
uniqueMember: uid=user,ou=people,dc=springframework,dc=org

dn: cn=admin,ou=groups,dc=springframework,dc=org
objectclass: top
objectclass: groupOfNames
cn: admin
uniqueMember: uid=admin,ou=people,dc=springframework,dc=org

的AuthenticationProvider

您可以通过将自定义AuthenticationProvider公开为bean来定义自定义身份验证。 例如,假设SpringAuthenticationProvider实现AuthenticationProvider,以下将自定义认证:

注:仅当AuthenticationManagerBuilder尚未填充时才会使用

@Bean
public SpringAuthenticationProvider springAuthenticationProvider() {
	return new SpringAuthenticationProvider();
}

的UserDetailsS​​ervice

您可以通过将自定义UserDetailsService公开为bean来定义自定义身份验证。 例如,假设SpringDataUserDetailsService实现UserDetailsService,以下将自定义认证:

注意:仅当AuthenticationManagerBuilder尚未填充且未定义AuthenticationProviderBean时才会使用此选项。

@Bean
public SpringDataUserDetailsService springDataUserDetailsService() {
	return new SpringDataUserDetailsService();
}

您还可以通过将PasswordEncoder公开为bean来自定义密码的编码方式。 例如,如果您使用bcrypt,则可以添加一个bean定义,如下所示:

@Bean
public BCryptPasswordEncoder passwordEncoder() {
	return new BCryptPasswordEncoder();
}

多个HttpSecurity

我们可以配置多个HttpSecurity实例,就像我们可以有多个<http>块一样。 关键是多次扩展WebSecurityConfigurationAdapter。 例如,以下是针对以/api/开头的URL进行不同配置的示例。

@EnableWebSecurity
public class MultiHttpSecurityConfig {
	@Bean                                                             
	public UserDetailsService userDetailsService() throws Exception {
		// ensure the passwords are encoded properly
		UserBuilder users = User.withDefaultPasswordEncoder();
		InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
		manager.createUser(users.username("user").password("password").roles("USER").build());
		manager.createUser(users.username("admin").password("password").roles("USER","ADMIN").build());
		return manager;
	}

	@Configuration
	@Order(1)                                                        
	public static class ApiWebSecurityConfigurationAdapter extends WebSecurityConfigurerAdapter {
		protected void configure(HttpSecurity http) throws Exception {
			http
				.antMatcher("/api/**")                               
				.authorizeRequests()
					.anyRequest().hasRole("ADMIN")
					.and()
				.httpBasic();
		}
	}

	@Configuration                                                   
	public static class FormLoginWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {

		@Override
		protected void configure(HttpSecurity http) throws Exception {
			http
				.authorizeRequests()
					.anyRequest().authenticated()
					.and()
				.formLogin();
		}
	}
}

<1>配置身份验证正常 <2>创建包含@OrderWebSecurityConfigurerAdapter的实例,以指定首先考虑哪个WebSecurityConfigurerAdapter。 <3> http.antMatcher声明,此HttpSecurity仅适用于以/api/开头的网址 <4>创建WebSecurityConfigurerAdapter的另一个实例。 如果网址不是以/api/开头,则会使用此配置。ApiWebSecurityConfigurationAdapter之后会考虑此配置,因为1@Order默认为最后一个值)后有@Order值。

方法安全性

从2.0版本开始,Spring Security已经大大改善了对服务层方法的安全性的支持。 它提供对JSR-250注释安全性的支持以及框架的原始@Secured注释。 从3.0开始,您还可以使用新的expression-based annotations。 您可以使用intercept-methods元素来装饰bean声明,也可以将安全性应用于单个bean,也可以使用AspectJ样式切入点在整个服务层中保护多个bean。

EnableGlobalMethodSecurity

我们可以在任何@Configuration实例上使用@EnableGlobalMethodSecurity注释来启用基于注释的安全性。 例如,以下内容将启用Spring Security的@Secured注释。

@EnableGlobalMethodSecurity(securedEnabled = true)
public class MethodSecurityConfig {
// ...
}

然后向方法(在类或接口上)添加注释将相应地限制对该方法的访问。 Spring Security的本地注释支持为该方法定义了一组属性。 这些将被传递给AccessDecisionManager以供它作出实际的决定:

public interface BankService {

@Secured("IS_AUTHENTICATED_ANONYMOUSLY")
public Account readAccount(Long id);

@Secured("IS_AUTHENTICATED_ANONYMOUSLY")
public Account[] findAccounts();

@Secured("ROLE_TELLER")
public Account post(Account account, double amount);
}

可以使用支持JSR-250注释

@EnableGlobalMethodSecurity(jsr250Enabled = true)
public class MethodSecurityConfig {
// ...
}

这些都是基于标准的,允许应用简单的基于角色的约束,但是没有Spring Security的本地注释的强大功能。 要使用新的基于表达式的语法,您可以使用

@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig {
// ...
}

和等效的Java代码将会是

public interface BankService {

@PreAuthorize("isAnonymous()")
public Account readAccount(Long id);

@PreAuthorize("isAnonymous()")
public Account[] findAccounts();

@PreAuthorize("hasAuthority('ROLE_TELLER')")
public Account post(Account account, double amount);
}

GlobalMethodSecurityConfiguration

有时您可能需要执行比@EnableGlobalMethodSecurity批注允许的操作更复杂的操作。 对于这些实例,您可以扩展GlobalMethodSecurityConfiguration,确保@EnableGlobalMethodSecurity注释存在于您的子类中。 例如,如果您想提供自定义MethodSecurityExpressionHandler,则可以使用以下配置:

@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
	@Override
	protected MethodSecurityExpressionHandler createExpressionHandler() {
		// ... create and return custom MethodSecurityExpressionHandler ...
		return expressionHandler;
	}
}

有关可覆盖的方法的其他信息,请参阅GlobalMethodSecurityConfiguration Javadoc。

EnableReactiveMethodSecurity

Spring Security通过使用ReactiveSecurityContextHolder设置的https://projectreactor.io/docs/core/release/reference/#context[Reactor’s Context]支持方法安全性。 例如,这将演示如何检索当前登录的用户的消息。

 

为此,该方法的返回类型必须是org.reactivestreams.Publisher(即Mono / Flux)。 这对于整合反应堆的Context是必要的。

Authentication authentication = new TestingAuthenticationToken("user", "password", "ROLE_USER");

Mono<String> messageByUsername = ReactiveSecurityContextHolder.getContext()
	.map(SecurityContext::getAuthentication)
	.map(Authentication::getName)
	.flatMap(this::findMessageByUsername)
	// In a WebFlux application the `subscriberContext` is automatically setup using `ReactorContextWebFilter`
	.subscriberContext(ReactiveSecurityContextHolder.withAuthentication(authentication));

StepVerifier.create(messageByUsername)
	.expectNext("Hi user")
	.verifyComplete();

this::findMessageByUsername定义为:

Mono<String> findMessageByUsername(String username) {
	return Mono.just("Hi " + username);
}

以下是在反应式应用程序中使用方法安全性时的最小方法安全配置。

@EnableReactiveMethodSecurity
public class SecurityConfig {
	@Bean
	public MapReactiveUserDetailsService userDetailsService() {
		User.UserBuilder userBuilder = User.withDefaultPasswordEncoder();
		UserDetails rob = userBuilder.username("rob").password("rob").roles("USER").build();
		UserDetails admin = userBuilder.username("admin").password("admin").roles("USER","ADMIN").build();
		return new MapReactiveUserDetailsService(rob, admin);
	}
}

考虑以下课程:

@Component
public class HelloWorldMessageService {
	@PreAuthorize("hasRole('ADMIN')")
	public Mono<String> findMessage() {
		return Mono.just("Hello World!");
	}
}

结合上述配置,@PreAuthorize("hasRole('ADMIN')")将确保findByMessage仅由具有角色ADMIN的用户调用。 请注意,标准方法安全性中的任何表达式都适用于@EnableReactiveMethodSecurity。 但是,目前我们只支持表达式的Booleanboolean的返回类型。 这意味着表达式不能阻止。

当与WebFlux安全进行集成时,Spring Security会根据经过验证的用户自动建立Reactor Context。

@EnableWebFluxSecurity
@EnableReactiveMethodSecurity
public class SecurityConfig {

	@Bean
	SecurityWebFilterChain springWebFilterChain(ServerHttpSecurity http) throws Exception {
		return http
			// Demonstrate that method security works
			// Best practice to use both for defense in depth
			.authorizeExchange()
				.anyExchange().permitAll()
				.and()
			.httpBasic().and()
			.build();
	}

	@Bean
	MapReactiveUserDetailsService userDetailsService() {
		User.UserBuilder userBuilder = User.withDefaultPasswordEncoder();
		UserDetails rob = userBuilder.username("rob").password("rob").roles("USER").build();
		UserDetails admin = userBuilder.username("admin").password("admin").roles("USER","ADMIN").build();
		return new MapReactiveUserDetailsService(rob, admin);
	}
}

您可以在{gh-samples-url} / javaconfig / hellowebflux-method [hellowebflux-method]中找到完整的示例

后处理配置的对象

Spring Security的Java配置不公开它配置的每个对象的每个属性。 这简化了大多数用户的配置。 最后,如果每个属性都暴露出来,用户可以使用标准的bean配置。

尽管没有直接公开每个属性的很好理由,但用户可能仍然需要更高级的配置选项。 为了解决这个问题,Spring Security引入了ObjectPostProcessor的概念,它可以用来修改或替换由Java配置创建的许多对象实例。 例如,如果您想在FilterSecurityInterceptor上配置filterSecurityPublishAuthorizationSuccess属性,则可以使用以下内容:

@Override
protected void configure(HttpSecurity http) throws Exception {
	http
		.authorizeRequests()
			.anyRequest().authenticated()
			.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
				public <O extends FilterSecurityInterceptor> O postProcess(
						O fsi) {
					fsi.setPublishAuthorizationSuccess(true);
					return fsi;
				}
			});
}

自定义DSL

您可以在Spring Security中提供您自己的定制DSL。 例如,你可能有这样的东西:

public class MyCustomDsl extends AbstractHttpConfigurer<MyCustomDsl, HttpSecurity> {
	private boolean flag;

	@Override
	public void init(H http) throws Exception {
		// any method that adds another configurer
		// must be done in the init method
		http.csrf().disable();
	}

	@Override
	public void configure(H http) throws Exception {
		ApplicationContext context = http.getSharedObject(ApplicationContext.class);

		// here we lookup from the ApplicationContext. You can also just create a new instance.
		MyFilter myFilter = context.getBean(MyFilter.class);
		myFilter.setFlag(flag);
		http.addFilterBefore(myFilter, UsernamePasswordAuthenticationFilter.class);
	}

	public MyCustomDsl flag(boolean value) {
		this.flag = value;
		return this;
	}

	public static MyCustomDsl customDsl() {
		return new MyCustomDsl();
	}
}

注意:这实际上是如何实现像HttpSecurity.authorizeRequests()这样的方法。

自定义DSL可以像这样使用:

@EnableWebSecurity
public class Config extends WebSecurityConfigurerAdapter {
	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http
			.apply(customDsl())
				.flag(true)
				.and()
			...;
	}
}

代码按以下顺序调用:

Config的configure方法中调用* 代码 在MyCustomDsl的init方法中调用* 代码 在MyCustomDsl的configure方法中调用* 代码

如果需要,可以使用SpringFactories默认WebSecurityConfiguerAdapter添加MyCustomDsl。 例如,您将在名为META-INF/spring.factories的类路径中创建资源,其中包含以下内容:

META-INF / spring.factories
org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer = sample.MyCustomDsl

希望禁用默认的用户可以明确地这样做。

@EnableWebSecurity
public class Config extends WebSecurityConfigurerAdapter {
	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http
			.apply(customDsl()).disable()
			...;
	}
}

安全名称空间配置

介绍

名称空间配置自Spring Framework 2.0以来已可用。 它允许您用传统的Spring bean应用程序上下文语法和其他XML模式中的元素进行补充。 您可以在Spring 参考文档中找到更多信息。 命名空间元素可以简单地用于配置单个Bean的更简洁的方式,或者更有力地定义更接近匹配问题域的替代配置语法,并隐藏用户的底层复杂性。 一个简单的元素可能会隐藏多个bean和处理步骤被添加到应用程序上下文的事实。 例如,将以下元素从安全性名称空间添加到应用程序上下文将启动一个嵌入式LDAP服务器,以在应用程序中测试使用情况:

<security:ldap-server />

这比连接相应的Apache Directory Server bean简单得多。 最常见的备选配置要求由ldap-server元素上的属性支持,用户不必担心需要创建哪些bean以及哪些bean属性名称。 脚注:[您可以在关于pass的章节中找到更多关于使用ldap-server元素的信息:specialcharacters,macros [LDAP身份验证]。]。 编辑应用程序上下文文件时使用好的XML编辑器应提供有关可用属性和元素的信息。 我们建议您尝试使用 Spring工具套件,因为它具有用于处理标准Spring名称空间的特殊功能。

要开始在您的应用程序上下文中使用安全性命名空间,您需要在您的类路径中包含spring-security-config jar。 然后,您需要做的就是将模式声明添加到您的应用程序上下文文件中:

<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:security="http://www.springframework.org/schema/security"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
		http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
		http://www.springframework.org/schema/security
		http://www.springframework.org/schema/security/spring-security.xsd">
	...
</beans>

在许多您将看到的示例中(以及示例应用程序中),我们经常会将"security"用作默认名称空间而不是"beans",这意味着我们可以忽略所有安全名称空间元素上的前缀,使内容更易于阅读。 如果将应用程序上下文划分为单独的文件并在其中一个文件中包含大部分安全配置,则可能还需要执行此操作。 您的安全应用程序上下文文件将会像这样开始

<beans:beans xmlns="http://www.springframework.org/schema/security"
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
		http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
		http://www.springframework.org/schema/security
		http://www.springframework.org/schema/security/spring-security.xsd">
	...
</beans:beans>

我们将假设这章从现在开始使用这个语法。

命名空间的设计

命名空间旨在捕获框架的最常见用途,并提供简化和简洁的语法来在应用程序中启用它们。 该设计基于框架内的大规模依赖关系,可分为以下几个方面:

  • Web / HTTP Security - 最复杂的部分。 设置用于应用框架认证机制的过滤器和相关服务bean,保护URL,呈现登录和错误页面等等。

  • 业务对象(方法)安全性_ - 保护服务层的选项。

  • AuthenticationManager - 处理来自框架其他部分的认证请求。

  • AccessDecisionManager - 为网络和方法安全提供访问决策。 一个默认的将被注册,但你也可以选择使用一个自定义的,使用普通的Spring bean语法声明。

  • AuthenticationProviders - 身份验证管理员验证用户身份的机制。 命名空间为几个标准选项提供了支持,同时也提供了添加使用传统语法声明的自定义bean的方法。

  • UserDetailsS​​ervice - 与身份验证提供程序密切相关,但其他bean通常也需要它。

我们将在下面的章节中看到如何配置这些。

安全名称空间配置入门

在本节中,我们将看看如何构建一个名称空间配置来使用框架的一些主要功能。 假设您最初希望尽快启动并运行,并将身份验证支持和访问控制添加到现有的Web应用程序,并带有一些测试登录名。 然后我们将看看如何切换到对数据库或其他安全存储库进行身份验证。 在后面的章节中,我们将介绍更高级的命名空间配置选项。

web.xml配置

您需要做的第一件事是将以下过滤声明添加到您的web.xml文件中:

<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>

<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

这为Spring Security Web基础结构提供了一个钩子。 DelegatingFilterProxy是一个Spring框架类,它委派一个过滤器实现,它在应用程序上下文中定义为一个Spring bean。 在这种情况下,bean被命名为"springSecurityFilterChain",它是由命名空间创建的用于处理Web安全性的内部基础设施bean。 请注意,您不应该自己使用这个bean名称。 将这些内容添加到您的web.xml后,即可开始编辑您的应用程序上下文文件。 Web安全服务使用<http>元素进行配置。

最小<http>配置

您只需启用网络安全即可

<http>
<intercept-url pattern="/**" access="hasRole('USER')" />
<form-login />
<logout />
</http>

其中说我们希望我们应用程序中的所有URL都受到保护,要求角色ROLE_USER能够访问它们,我们希望使用带有用户名和密码的表单登录应用程序,并且我们希望注册注销URL这将允许我们注销应用程序。 <http>元素是所有与Web相关的名称空间功能的父项。 <intercept-url>元素定义了一个pattern,它使用蚂蚁路径样式语法脚注与传入请求的URL匹配:[请参阅关于传递的部分:特殊字符,宏[请求匹配和HttpFirewall] Web应用程序基础结构一章,了解如何实际执行匹配的更多细节。]。 您也可以使用正则表达式匹配作为替代(有关更多详细信息,请参阅命名空间附录)。 access属性定义匹配给定模式的请求的访问要求。 通过默认配置,这通常是逗号分隔的角色列表,其中一个用户必须被允许发出请求。 前缀"ROLE_"是一个标记,表示应该与用户权限进行简单比较。 换句话说,应该使用正常的基于角色的检查。 Spring Security中的访问控制不限于使用简单角色(因此使用前缀来区分不同类型的安全属性)。 我们稍后会看到解释如何变化脚注:[{0}}属性中逗号分隔值的解释取决于pass的实现:specialcharacters,macros [AccessDecisionManager] 。 在Spring Security 3.0中,该属性也可以使用pass:specialcharacters,macros [EL expression]来填充。

 

您可以使用多个<intercept-url>元素为不同的网址集定义不同的访问权限要求,但会按列出的顺序对其进行评估,并使用第一个匹配项。 所以你必须把最具体的比赛放在最上面。 您还可以添加method属性,以将匹配限制为特定的HTTP方法(GETPOSTPUT)。

要添加一些用户,您可以直接在命名空间中定义一组测试数据:

<authentication-manager>
<authentication-provider>
	<user-service>
	<!-- Password is prefixed with {noop} to indicate to DelegatingPasswordEncoder that
	NoOpPasswordEncoder should be used. This is not safe for production, but makes reading
	in samples easier. Normally passwords should be hashed using BCrypt -->
	<user name="jimi" password="{noop}jimispassword" authorities="ROLE_USER, ROLE_ADMIN" />
	<user name="bob" password="{noop}bobspassword" authorities="ROLE_USER" />
	</user-service>
</authentication-provider>
</authentication-manager>

这是存储相同密码的安全方式的一个例子。 密码前缀为{bcrypt},用于指示支持任何已配置的PasswordEncoder进行匹配的DelegatingPasswordEncoder,并使用BCrypt对密码进行散列处理:

<authentication-manager>
<authentication-provider>
	<user-service>
	<user name="jimi" password="{bcrypt}$2a$10$ddEWZUl8aU0GdZPPpy7wbu82dvEw/pBpbRvDQRqA41y6mK1CoH00m"
			authorities="ROLE_USER, ROLE_ADMIN" />
	<user name="bob" password="{bcrypt}$2a$10$/elFpMBnAYYig6KRR5bvOOYeZr1ie1hSogJryg9qDlhza4oCw1Qka"
			authorities="ROLE_USER" />
	<user name="jimi" password="{noop}jimispassword" authorities="ROLE_USER, ROLE_ADMIN" />
	<user name="bob" password="{noop}bobspassword" authorities="ROLE_USER" />
	</user-service>
</authentication-provider>
</authentication-manager>

如果你熟悉框架的命名空间版本,你可能已经可以大概猜出这里发生了什么。 <http>元素负责创建一个FilterChainProxy和它使用的过滤器bean。 由于过滤器位置是预定义的,因此不正确的过滤器排序等常见问题不再是问题。

<authentication-provider>元素创建一个DaoAuthenticationProvider bean,<user-service>元素创建一个InMemoryDaoImpl。 所有authentication-provider元素必须是<authentication-manager>元素的子元素,它会创建一个ProviderManager并向其注册身份验证提供程序。 您可以找到有关在namespace appendix中创建的bean的更多详细信息。 如果您想开始了解框架中的重要类以及如何使用这些类,那么值得进行交叉检查,特别是如果您想稍后进行自定义的话。

上面的配置定义了两个用户,他们的密码和他们在应用程序中的角色(将用于访问控制)。 也可以使用user-service上的properties属性从标准属性文件加载用户信息。 有关文件格式的更多详细信息,请参阅in-memory authentication中的部分。 使用<authentication-provider>元素意味着用户信息将被认证管理器用于处理认证请求。 您可以有多个<authentication-provider>元素来定义不同的身份验证源,并且每个元素都会依次进行查询。

此时您应该能够启动您的应用程序,并且您将需要登录才能继续。 尝试一下,或尝试使用该项目附带的"tutorial"示例应用程序进行试验。

表单和基本登录选项

由于我们没有提及任何HTML文件或JSP,因此您可能想知道登录表单从何时被提示登录。 实际上,由于我们没有明确设置登录页面的URL,因此Spring Security会根据启用的功能自动生成一个URL,并使用处理提交的登录的URL的标准值,用户将默认的目标URL在登录后被发送到等等。 但是,命名空间提供了大量的支持来允许您自定义这些选项。 例如,如果您想提供自己的登录页面,则可以使用:

<http>
<intercept-url pattern="/login.jsp*" access="IS_AUTHENTICATED_ANONYMOUSLY"/>
<intercept-url pattern="/**" access="ROLE_USER" />
<form-login login-page='/login.jsp'/>
</http>

另请注意,我们已经添加了一个额外的intercept-url元素来说明登录页面的任何请求应该可供匿名用户使用脚注:[请参阅关于pass的章节:specialcharacters,macros [匿名身份验证]]以及关于值IS_AUTHENTICATED_ANONYMOUSLY如何处理的更多细节AuthenticatedVoter类。]。 否则,请求将与模式/ **匹配,并且无法访问登录页面本身! 这是一个常见的配置错误,并且会导致应用程序中出现无限循环。 如果您的登录页面似乎受到保护,Spring Security将在日志中发出警告。 通过为模式定义一个单独的http元素,也可以让所有匹配特定模式的请求完全绕过安全过滤器链,如下所示:

<http pattern="/css/**" security="none"/>
<http pattern="/login.jsp*" security="none"/>

<http use-expressions="false">
<intercept-url pattern="/**" access="ROLE_USER" />
<form-login login-page='/login.jsp'/>
</http>

从Spring Security 3.1开始,现在可以使用多个http元素为不同的请求模式定义单独的安全过滤器链配置。 如果从http元素中省略pattern属性,它将匹配所有请求。 创建一个不安全的模式就是这种语法的一个简单例子,其中模式映射到一个空的过滤器链脚注:[使用多个<http>元素是一个重要的特性,允许名称空间同时支持有状态和无状态例如,在同一个应用程序中的路径。 以前使用intercept-url元素上的属性filters="none"的语法与此更改不兼容,并且在3.1中不再支持。]。 我们将在Security Filter Chain一章中更详细地讨论这种新语法。

认识到这些不安全的请求将完全忽略任何Spring Security Web相关配置或诸如requires-channel之类的附加属性是非常重要的,因此您将无法访问当前用户的信息或在请求。 如果您仍希望应用安全筛选器链,请使用access='IS_AUTHENTICATED_ANONYMOUSLY'作为替代方案。

如果您想使用基本身份验证而不是表单登录,请将配置更改为

<http use-expressions="false">
<intercept-url pattern="/**" access="ROLE_USER" />
<http-basic />
</http>

然后,基本身份验证将优先,并在用户尝试访问受保护资源时用于提示登录。 如果您希望使用此配置,表单登录仍然可用,例如通过嵌入其他网页的登录表单。

设置默认的登录后目的地

如果尝试访问受保护资源时未提示表单登录,则default-target-url选项会发挥作用。 这是成功登录后用户将访问的网址,默认为"/"。 通过将always-use-default-target属性设置为"true",您还可以配置这些内容,以使用户always结束于此页面(无论登录名是"on-demand"还是明确选择登录) 。 如果您的应用程序始终要求用户从"home"页开始,这非常有用,例如:

<http pattern="/login.htm*" security="none"/>
<http use-expressions="false">
<intercept-url pattern='/**' access='ROLE_USER' />
<form-login login-page='/login.htm' default-target-url='/home.htm'
		always-use-default-target='true' />
</http>

为了更好地控制目标,您可以使用authentication-success-handler-ref属性作为default-target-url的替代选项。 被引用的bean应该是AuthenticationSuccessHandler的一个实例。 您可以在Core Filters章节以及命名空间附录中找到更多信息,以及有关如何在身份验证失败时自定义流的信息。

注销处理

logout元素通过导航到特定的URL添加了对注销的支持。 默认注销网址为/logout,但您可以使用logout-url属性将其设置为其他内容。 关于其他可用属性的更多信息可以在命名空间附录中找到。

使用其他身份验证提供程序

在实践中,您将需要一个更可扩展的用户信息来源,而不是添加到应用程序上下文文件中的几个名称。 您很可能希望将您的用户信息存储在数据库或LDAP服务器中。 LDAP命名空间配置在LDAP chapter中处理,所以我们不会在这里介绍它。 如果您在应用程序上下文中有一个名为"myUserDetailsService"的Spring Security {0}}的自定义实现,那么您可以使用这种方法进行身份验证

<authentication-manager>
	<authentication-provider user-service-ref='myUserDetailsService'/>
</authentication-manager>

如果你想使用数据库,那么你可以使用

<authentication-manager>
<authentication-provider>
	<jdbc-user-service data-source-ref="securityDataSource"/>
</authentication-provider>
</authentication-manager>

"securityDataSource"是应用程序上下文中DataSource bean的名称,指向包含标准Spring Security user data tables的数据库。 或者,您可以配置Spring Security JdbcDaoImpl bean并使用user-service-ref属性指向该bean:

<authentication-manager>
<authentication-provider user-service-ref='myUserDetailsService'/>
</authentication-manager>

<beans:bean id="myUserDetailsService"
	class="org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl">
<beans:property name="dataSource" ref="dataSource"/>
</beans:bean>

您还可以使用标准的AuthenticationProvider bean,如下所示

<authentication-manager>
	<authentication-provider ref='myAuthenticationProvider'/>
</authentication-manager>

其中myAuthenticationProvider是您的应用程序上下文中实现AuthenticationProvider的bean的名称。 您可以使用多个authentication-provider元素,在这种情况下,提供程序将按其声明的顺序进行查询。 有关如何使用名称空间配置Spring Security AuthenticationManager的更多信息,请参阅验证管理器和名称空间

添加密码编码器

应该始终使用为此目的而设计的安全哈希算法对密码进行编码(不是像SHA或MD5这样的标准算法)。 这由<password-encoder>元素支持。 使用bcrypt编码密码,原始身份验证提供程序配置将如下所示:

<beans:bean name="bcryptEncoder"
	class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"/>

<authentication-manager>
<authentication-provider>
	<password-encoder ref="bcryptEncoder"/>
	<user-service>
	<user name="jimi" password="$2a$10$ddEWZUl8aU0GdZPPpy7wbu82dvEw/pBpbRvDQRqA41y6mK1CoH00m"
			authorities="ROLE_USER, ROLE_ADMIN" />
	<user name="bob" password="$2a$10$/elFpMBnAYYig6KRR5bvOOYeZr1ie1hSogJryg9qDlhza4oCw1Qka"
			authorities="ROLE_USER" />
	</user-service>
</authentication-provider>
</authentication-manager>

在大多数情况下,bcrypt是一个不错的选择,除非你有一个遗留系统迫使你使用不同的算法。 如果您使用简单的哈希算法,或者更糟糕的是,存储纯文本密码,那么您应该考虑迁移到更安全的选项,如bcrypt。

高级网络功能

记住我的身份验证

有关remember-me命名空间配置的信息,请参阅单独的Remember-Me chapter

添加HTTP / HTTPS频道安全

如果您的应用程序同时支持HTTP和HTTPS,并且您要求只能通过HTTPS访问特定的URL,则可以使用<intercept-url>上的requires-channel属性直接支持此功能:

<http>
<intercept-url pattern="/secure/**" access="ROLE_USER" requires-channel="https"/>
<intercept-url pattern="/**" access="ROLE_USER" requires-channel="any"/>
...
</http>

使用此配置,如果用户尝试使用HTTP访问任何匹配"/secure/**"模式的内容,则会首先将其重定向到HTTPS URL脚注:[有关如何实现频道处理的更多详细信息,请参阅Javadoc对于ChannelProcessingFilter和相关类。]。 可用选项为"http","https"或"any"。 使用值"any"表示可以使用HTTP或HTTPS。

如果您的应用程序使用HTTP和/或HTTPS的非标准端口,则可以按如下方式指定端口映射列表:

<http>
...
<port-mappings>
	<port-mapping http="9080" https="9443"/>
</port-mappings>
</http>

请注意,为了确保安全,应用程序不应该使用HTTP或在HTTP和HTTPS之间切换。 它应该从HTTPS开始(用户输入HTTPS URL)并始终使用安全连接以避免任何可能的中间人攻击。

会话管理

检测超时

您可以配置Spring Security检测提交的无效会话ID并将用户重定向到适当的URL。 这是通过session-management元素实现的:

<http>
...
<session-management invalid-session-url="/invalidSession.htm" />
</http>

请注意,如果您使用此机制来检测会话超时,那么如果用户注销并重新登录而不关闭浏览器,它可能会错误地报告错误。 这是因为会话cookie在会话无效时不会被清除,即使用户已注销,也会重新提交。 您可以在注销时显式删除JSESSIONID cookie,例如在注销处理程序中使用以下语法:

<http>
<logout delete-cookies="JSESSIONID" />
</http>

不幸的是,这不能保证与每个servlet容器一起工作,所以你需要在你的环境中测试它

 

如果您在代理后运行应用程序,则可以通过配置代理服务器来删除会话cookie。 例如,使用Apache HTTPD的mod_headers,以下指令将通过在对注销请求的响应(假设应用程序部署在/tutorial路径下)中删除JSESSIONID cookie:

<LocationMatch "/tutorial/logout">
Header always set Set-Cookie "JSESSIONID=;Path=/tutorial;Expires=Thu, 01 Jan 1970 00:00:00 GMT"
</LocationMatch>
并发会话控制

如果您希望限制单个用户登录您的应用程序的能力,Spring Security通过以下简单添加支持这种开箱即用的功能。 首先,您需要将以下侦听器添加到您的web.xml文件中,以保持Spring Security关于会话生命周期事件的更新:

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

然后将以下行添加到您的应用程序上下文中:

<http>
...
<session-management>
	<concurrency-control max-sessions="1" />
</session-management>
</http>

这将防止用户多次登录 - 第二次登录会导致第一次登录失效。 通常情况下,您宁愿阻止第二次登录,在这种情况下您可以使用

<http>
...
<session-management>
	<concurrency-control max-sessions="1" error-if-maximum-exceeded="true" />
</session-management>
</http>

第二次登录将被拒绝。 通过"rejected",我们表示如果正在使用基于表单的登录,则用户将被发送到authentication-failure-url。 如果第二次认证是通过另一个非交互式机制(例如"remember-me")发生的,则会向客户端发送一个"unauthorized"(401)错误。 如果您想要使用错误页面,则可以将属性session-authentication-error-url添加到session-management元素。

如果您使用自定义身份验证过滤器进行基于表单的登录,则必须明确配置并发会话控制支持。 更多详细信息可以在Session Management chapter中找到。

会话固定攻击保护

会话固定攻击是潜在的风险,因为恶意攻击者可能通过访问站点创建会话,然后说服其他用户使用同一会话登录(通过向其发送包含会话标识符的链接作为参数,例如)。 Spring Security通过创建新会话或以其他方式更改用户登录时的会话ID来自动防止此问题。 如果您不需要此保护,或者与其他要求冲突,则可以使用<session-management>上的session-fixation-protection属性来控制行为,该属性有四个选项

  • none - 不要做任何事情。 原来的会议将被保留。

  • newSession - 创建新的"clean"会话,而不复制现有的会话数据(Spring Security相关的属性仍将被复制)。

  • migrateSession - 创建新会话并将所有现有会话属性复制到新会话。 这是Servlet 3.0或更旧版本容器的默认值。

  • changeSessionId - 不要创建新会话。 而是使用Servlet容器提供的会话固定保护(HttpServletRequest#changeSessionId())。 此选项仅在Servlet 3.1(Java EE 7)和更新的容器中可用。 在较旧的容器中指定它将导致异常。 这是Servlet 3.1和更新的容器中的默认值。

发生会话修复保护时,会导致在应用程序上下文中发布SessionFixationProtectionEvent。 如果您使用changeSessionId,则此保护将会also导致通知任何javax.servlet.http.HttpSessionIdListener,因此如果您的代码侦听这两个事件,请谨慎使用。 有关更多信息,请参阅Session Management一章。

OpenID支持

命名空间支持 OpenID的登录,而不是普通的基于表单的登录,或者除了普通的基于表单的登录外,还可以进行简单的更改:

<http>
<intercept-url pattern="/**" access="ROLE_USER" />
<openid-login />
</http>

然后,您应该向OpenID提供商(例如myopenid.com)注册您自己,并将用户信息添加到您的内存<user-service>中:

<user name="http://jimi.hendrix.myopenid.com/" authorities="ROLE_USER" />

您应该可以使用myopenid.com网站进行身份验证。 通过设置openid-login元素上的user-service-ref属性,还可以选择特定的UserDetailsService bean来使用OpenID。 有关更多信息,请参阅authentication providers上的上一节。 请注意,我们从上述用户配置中省略了密码属性,因为这组用户数据仅用于为用户加载权限。 随机密码将在内部生成,从而防止您在配置中的其他位置意外地将此用户数据用作身份验证源。

属性交换

支持OpenID 属性交换。 例如,以下配置将尝试从OpenID提供程序中检索电子邮件和全名,供应用程序使用:

<openid-login>
<attribute-exchange>
	<openid-attribute name="email" type="http://axschema.org/contact/email" required="true"/>
	<openid-attribute name="name" type="http://axschema.org/namePerson"/>
</attribute-exchange>
</openid-login>

每个OpenID属性的"type"是一个由特定模式确定的URI,在这种情况下为 http://axschema.org/。 如果必须为成功认证检索属性,则可以设置required属性。 支持的确切架构和属性取决于您的OpenID提供者。 属性值作为身份验证过程的一部分返回,并可以使用以下代码进行访问:

OpenIDAuthenticationToken token =
	(OpenIDAuthenticationToken)SecurityContextHolder.getContext().getAuthentication();
List<OpenIDAttribute> attributes = token.getAttributes();

OpenIDAttribute包含属性类型和检索值(或多值属性中的值)。 当我们查看technical overview章节中的核心Spring Security组件时,我们将更多地了解如何使用SecurityContextHolder类。 如果您希望使用多个身份提供程序,则还支持多个属性交换配置。 您可以使用每个identifier-matcher属性提供多个attribute-exchange个元素。 这包含一个正则表达式,它将与用户提供的OpenID标识符相匹配。 有关示例配置,请参阅代码库中的OpenID示例应用程序,为Google,Yahoo和MyOpenID提供程序提供不同的属性列表。

响应标题

有关如何自定义标题元素的其他信息,请参阅参考文献的安全性HTTP响应头部分。

添加您自己的过滤器

如果您以前使用过Spring Security,那么您会知道该框架维护一系列过滤器以应用其服务。 您可能希望将自己的过滤器添加到特定位置的堆栈中,或者使用当前没有命名空间配置选项(例如CAS)的Spring Security过滤器。 或者您可能想要使用标准名称空间过滤器的自定义版本,例如由<form-login>元素创建的UsernamePasswordAuthenticationFilter,以利用可用的一些额外配置选项显式的bean。 你怎么能用命名空间配置来做到这一点,因为过滤器链不直接暴露?

使用名称空间时,过滤器的顺序始终严格执行。 在创建应用程序上下文时,过滤器bean按名称空间处理代码进行排序,标准Spring Security过滤器在名称空间和知名位置都有一个别名。

 

在以前的版本中,在应用程序上下文的后处理期间,创建过滤器实例之后进行排序。 在版本3.0+中,现在在bean实例化之前,在bean元数据级完成排序。 这会影响您如何将自己的过滤器添加到堆栈,因为在解析<http>元素期间必须知道整个过滤器列表,因此语法在3.0中稍有变化。

[filter-stack]中显示了创建过滤器的过滤器,别名和命名空间元素/属性。 过滤器按照它们在过滤器链中出现的顺序列出。

。标准过滤器别名和排序

别名过滤器类名称空间元素或属性

CHANNEL_FILTER

ChannelProcessingFilter

http/intercept-url@requires-channel

SECURITY_CONTEXT_FILTER

SecurityContextPersistenceFilter

http

CONCURRENT_SESSION_FILTER

ConcurrentSessionFilter

session-management/concurrency-control

HEADERS_FILTER

HeaderWriterFilter

http/headers

CSRF_FILTER

CsrfFilter

http/csrf

LOGOUT_FILTER

LogoutFilter

http/logout

X509_FILTER

X509AuthenticationFilter

http/x509

PRE_AUTH_FILTER

AbstractPreAuthenticatedProcessingFilter子类

N / A

CAS_FILTER

CasAuthenticationFilter

N / A

FORM_LOGIN_FILTER

UsernamePasswordAuthenticationFilter

http/form-login

BASIC_AUTH_FILTER

BasicAuthenticationFilter

http/http-basic

SERVLET_API_SUPPORT_FILTER

SecurityContextHolderAwareRequestFilter

http/@servlet-api-provision

JAAS_API_SUPPORT_FILTER

JaasApiIntegrationFilter

http/@jaas-api-provision

REMEMBER_ME_FILTER

RememberMeAuthenticationFilter

http/remember-me

ANONYMOUS_FILTER

AnonymousAuthenticationFilter

http/anonymous

SESSION_MANAGEMENT_FILTER

SessionManagementFilter

session-management

EXCEPTION_TRANSLATION_FILTER

ExceptionTranslationFilter

http

FILTER_SECURITY_INTERCEPTOR

FilterSecurityInterceptor

http

SWITCH_USER_FILTER

SwitchUserFilter

N / A

您可以使用custom-filter元素和其中一个名称来指定您的过滤器应显示在的位置,您可以将自己的过滤器添加到堆栈中:

<http>
<custom-filter position="FORM_LOGIN_FILTER" ref="myFilter" />
</http>

<beans:bean id="myFilter" class="com.mycompany.MySpecialAuthenticationFilter"/>

如果您希望将过滤器插入堆栈中另一个过滤器之前或之后,也可以使用afterbefore属性。 名称"FIRST"和"LAST"可以与position属性一起使用,以指示您希望过滤器分别出现在整个堆栈之前或之后。

。避免过滤器位置冲突

 

如果您插入的自定义过滤器可能与名称空间创建的标准过滤器之一占据相同的位置,请务必不要错误地包含命名空间版本。 删除所有创建要替换其功能的过滤器的元素。

请注意,您无法替换使用<http>元素本身创建的过滤器 - SecurityContextPersistenceFilterExceptionTranslationFilterFilterSecurityInterceptor。 其他一些过滤器默认添加,但您可以禁用它们。 默认情况下会添加AnonymousAuthenticationFilter,除非您禁用session-fixation protection,否则SessionManagementFilter也会添加到过滤器链中。

如果要替换需要身份验证入口点的名称空间过滤器(即身份验证过程由未经身份验证的用户尝试访问受保护的资源触发),则还需要添加自定义入口点Bean。

设置自定义AuthenticationEntryPoint

如果您没有通过名称空间使用表单登录,OpenID或基本身份验证,则可能需要使用传统的bean语法定义身份验证过滤器和入口点,并将它们链接到名称空间中,就像我们刚刚看到的那样。 可以使用<http>元素上的entry-point-ref属性来设置相应的AuthenticationEntryPoint

CAS示例应用程序是使用包含此语法的名称空间的自定义Bean的一个很好的示例。 如果您不熟悉身份验证入口点,则会在technical overview一章中讨论它们。

方法安全性

从2.0版本开始,Spring Security已经大大改善了对服务层方法的安全性的支持。 它提供对JSR-250注释安全性的支持以及框架的原始@Secured注释。 从3.0开始,您还可以使用新的expression-based annotations。 您可以使用intercept-methods元素来装饰bean声明,也可以将安全性应用于单个bean,也可以使用AspectJ样式切入点在整个服务层中保护多个bean。

<global-method-security>元素

此元素用于在应用程序中启用基于注释的安全性(通过在元素上设置适当的属性),并将安全性切入点声明分组在一起,这些声明将应用于整个应用程序上下文中。 您应该只声​​明一个<global-method-security>元素。 以下声明将支持Spring Security的@Secured

<global-method-security secured-annotations="enabled" />

向方法(在类或接口上)添加注释会相应地限制对该方法的访问。 Spring Security的本地注释支持为该方法定义了一组属性。 这些将被传递给AccessDecisionManager,以便做出实际的决定:

public interface BankService {

@Secured("IS_AUTHENTICATED_ANONYMOUSLY")
public Account readAccount(Long id);

@Secured("IS_AUTHENTICATED_ANONYMOUSLY")
public Account[] findAccounts();

@Secured("ROLE_TELLER")
public Account post(Account account, double amount);
}

可以使用支持JSR-250注释

<global-method-security jsr250-annotations="enabled" />

这些都是基于标准的,允许应用简单的基于角色的约束,但是没有Spring Security的本地注释的强大功能。 要使用新的基于表达式的语法,您可以使用

<global-method-security pre-post-annotations="enabled" />

和等效的Java代码将会是

public interface BankService {

@PreAuthorize("isAnonymous()")
public Account readAccount(Long id);

@PreAuthorize("isAnonymous()")
public Account[] findAccounts();

@PreAuthorize("hasAuthority('ROLE_TELLER')")
public Account post(Account account, double amount);
}

如果您需要定义简单的规则,而不是根据用户的权限列表检查角色名称,则基于表达式的注释是一个不错的选择。

 

注释的方法只会对定义为Spring bean的实例(在启用了方法安全性的相同应用程序上下文中)保护。 如果你想确保那些不是由Spring创建的实例(例如使用new运算符),那么你需要使用AspectJ。

 

您可以在同一个应用程序中启用多种类型的注释,但只有一种类型应该用于任何界面或类,否则将无法很好地定义行为。 如果找到适用于特定方法的两个注释,则只会应用其中的一个注释。

使用protect-pointcut添加安全性切入点

protect-pointcut的使用特别强大,因为它允许您仅通过简单的声明就可以将安全性应用于许多bean。 考虑下面的例子:

<global-method-security>
<protect-pointcut expression="execution(* com.mycompany.*Service.*(..))"
	access="ROLE_USER"/>
</global-method-security>

这将保护应用程序上下文中声明的bean上的所有方法,这些方法的类位于com.mycompany包中,并且其类名以"Service"结尾。 只有具有ROLE_USER角色的用户才能够调用这些方法。 与URL匹配一样,最具体的匹配必须在切入点列表中排在第一位,因为将使用第一个匹配表达式。 安全注释优先于切入点。

默认AccessDecisionManager

本节假定您对Spring Security中访问控制的基础体系结构有一定的了解。 如果你不这样做,你可以跳过它并稍后回来,因为这部分内容只对那些需要进行一些定制以便使用不仅仅是基于角色的安全性的人非常相关。

当您使用名称空间配置时,会自动为您注册一个默认实例AccessDecisionManager,并将根据您在intercept-url中指定的访问属性为方法调用和Web URL访问做出访问决策。 }和protect-pointcut声明(如果您使用注释安全方法,则在注释中)。

默认策略是使用带有RoleVoterAuthenticatedVoterAffirmativeBased AccessDecisionManager。 您可以在authorization的章节中找到更多关于这些内容的信息。

自定义AccessDecisionManager

如果您需要使用更复杂的访问控制策略,则很容易为方法和网络安全设置替代方案。

对于方法安全性,您可以通过将global-method-security中的access-decision-manager-ref属性设置为应用程序上下文中相应的AccessDecisionManager bean的id来执行此操作:

<global-method-security access-decision-manager-ref="myAccessDecisionManagerBean">
...
</global-method-security>

Web安全的语法是相同的,但在http元素上:

<http access-decision-manager-ref="myAccessDecisionManagerBean">
...
</http>

验证管理器和名称空间

在Spring Security中提供认证服务的主界面是AuthenticationManager。 这通常是Spring Security的ProviderManager类的一个实例,如果您之前使用过该框架,您可能已经熟悉了它。 如果没有,稍后将在technical overview chapter中进行介绍。 这个bean实例是使用authentication-manager命名空间元素注册的。 如果您通过名称空间使用HTTP或方法安全性,则无法使用自定义AuthenticationManager,但这不应该成为问题,因为您可以完全控制所使用的AuthenticationProvider

您可能希望使用ProviderManager注册其他AuthenticationProvider Bean,您可以使用带有ref属性的<authentication-provider>元素进行此操作,其中属性的值是名称您要添加的提供程序bean的名称。 例如:

<authentication-manager>
<authentication-provider ref="casAuthenticationProvider"/>
</authentication-manager>

<bean id="casAuthenticationProvider"
	class="org.springframework.security.cas.authentication.CasAuthenticationProvider">
...
</bean>

另一个常见要求是上下文中的另一个bean可能需要对AuthenticationManager的引用。 您可以轻松地为AuthenticationManager注册别名,并在您的应用程序上下文的其他地方使用此名称。

<security:authentication-manager alias="authenticationManager">
...
</security:authentication-manager>

<bean id="customizedFormLoginFilter"
	class="com.somecompany.security.web.CustomFormLoginFilter">
<property name="authenticationManager" ref="authenticationManager"/>
...
</bean>

示例应用程序

有几个示例Web应用程序可用于该项目。 为避免下载量过大,分发zip文件中只包含"tutorial"和"contacts"个样本。 其他人可以直接从您可以获得的来源构建,如the introduction中所述。 您可以轻松地自行构建项目,并在项目网站 http://spring.io/spring-security/上提供更多信息。 本章中提到的所有路径都与项目源目录有关。

教程示例

教程示例是帮助您入门的一个很好的基本示例。 它始终使用简单的名称空间配置。 已编译的应用程序包含在分发zip文件中,可以部署到您的Web容器(spring-security-samples-tutorial-3.1.x.war)中。 form-based身份验证机制与常用的remember-me身份验证提供程序结合使用,以便使用Cookie自动记住登录。

我们建议您从教程示例开始,因为XML最小且易于遵循。 最重要的是,您可以轻松地将这一个XML文件(及其相应的web.xml条目)添加到您的现有应用程序中。 只有在实现这种基本集成时,我们才建议您尝试添加方法授权或域对象安全性。

通讯录

联系人范例是一个高级示例,它说明了除基本应用程序安全性以外,域对象访问控制列表(ACL)的更强大功能。 该应用程序提供了一个用户可以管理联系人简单数据库(域对象)的界面。

要进行部署,只需将Spring Security发行版中的WAR文件复制到您的容器的webapps目录中即可。 这场战争应该被称为spring-security-samples-contacts-3.1.x.war(附加版本号会因您使用的版本而异)。

启动容器后,检查应用程序是否可以加载。 访问http:// localhost:8080 / contacts(或适用于您的Web容器和您部署的WAR的URL)。

接下来,点击"Debug"。 系统会提示您进行身份验证,并在该页面上建议一系列用户名和密码。 只需对其中任何一个进行身份验证并查看生成的页面。 它应该包含类似于以下内容的成功消息:

Security Debug Information

Authentication object is of type:
org.springframework.security.authentication.UsernamePasswordAuthenticationToken

Authentication object as a String:

org.springframework.security.authentication.UsernamePasswordAuthenticationToken@1f127853:
Principal: org.springframework.security.core.userdetails.User@b07ed00: Username: rod; \
Password: [PROTECTED]; Enabled: true; AccountNonExpired: true;
credentialsNonExpired: true; AccountNonLocked: true; \
Granted Authorities: ROLE_SUPERVISOR, ROLE_USER; \
Password: [PROTECTED]; Authenticated: true; \
Details: org.springframework.security.web.authentication.WebAuthenticationDetails@0: \
RemoteIpAddress: 127.0.0.1; SessionId: 8fkp8t83ohar; \
Granted Authorities: ROLE_SUPERVISOR, ROLE_USER

Authentication object holds the following granted authorities:

ROLE_SUPERVISOR (getAuthority(): ROLE_SUPERVISOR)
ROLE_USER (getAuthority(): ROLE_USER)

Success! Your web filters appear to be properly configured!

成功收到上述消息后,请返回示例应用程序的主页并单击"Manage"。 然后您可以尝试应用程序。 请注意,只显示当前登录用户可用的联系人,并且只有具有ROLE_SUPERVISOR的用户才有权访问以删除其联系人。 在幕后,MethodSecurityInterceptor确保业务对象的安全。

该应用程序允许您修改与不同联系人关联的访问控制列表。 请务必通过查看应用程序上下文XML文件来尝试并了解它是如何工作的。

LDAP示例

LDAP示例应用程序提供基本配置,并使用传统Bean设置名称空间配置和等效配置,这两者都位于同一应用程序上下文文件中。 这意味着实际上在此应用程序中配置了两个相同的认证提供程序

OpenID示例

OpenID示例演示了如何使用名称空间来配置OpenID以及如何为Google,Yahoo和MyOpenID身份提供商设置 属性交换配置(如果您愿意,可以尝试添加其他人)。 它使用基于JQuery的 OpenID的选择项目来提供用户友好的登录页面,允许用户轻松选择提供者,而不是键入完整的OpenID标识符。

该应用程序不同于正常的身份验证方案,因为它允许任何用户访问该网站(只要其OpenID身份验证成功)。 您第一次登录时,您会收到"Welcome [your name]"“消息。 如果您注销并重新登录(使用相同的OpenID标识),则应该更改为"Welcome Back"。 这是通过使用自定义的UserDetailsService为任何用户分配标准角色并将标识内部存储在地图中来实现的。 显然,真正的应用程序会使用数据库。 看看源表格的更多信息。 这个类还考虑到不同的属性可能会从不同的提供者返回,并建立相应的用户名称。

CAS样本

CAS示例要求您同时运行CAS服务器和CAS客户端。 它不包含在发行版中,因此您应该按the introduction中所述检出项目代码。 您可以在sample/cas目录下找到相关文件。 其中还有一个Readme.txt文件,它解释了如何直接从源代码树运行服务器和客户端,并完成SSL支持。

JAAS示例

JAAS示例是如何在Spring Security中使用JAAS LoginModule的非常简单的示例。 如果用户名等于密码,则所提供的LoginModule将成功认证用户,否则会引发LoginException。 本例中使用的AuthorityGranter总是授予角色ROLE_USER。 示例应用程序还演示了如何通过将jaas-api-provision设置为等于"true",作为LoginModule返回的JAAS主体运行。

预认证示例

本示例应用程序演示了如何连接来自pre-authentication框架的bean以利用来自Java EE容器的登录信息。 用户名和角色是由容器设置的。

代码位于samples/preauth

Spring安全社区

问题跟踪

Spring Security使用JIRA来管理错误报告和增强请求。 如果您发现错误,请使用JIRA登录报告。 请勿将其登录到支持论坛,邮件列表或通过电子邮件发送给项目开发人员。 这种方法是临时的,我们更愿意使用更正式的流程来管理错误。

如果可能,请在您的问题报告中提供一个JUnit测试,以显示任何不正确的行为。 或者,更好的是,提供一个补丁来纠正问题。 同样,我们也欢迎增强功能在问题跟踪器中登录,但如果您包含相应的单元测试,我们只接受增强请求。 这对确保项目测试覆盖率得到充分维护是必要的。

您可以通过https://github.com/spring-projects/spring-security/issues访问问题跟踪器。

成为参与者

我们欢迎您参与Spring Security项目。 有很多贡献方式,包括阅读论坛并回答其他人提出的问题,编写新代码,改进现有代码,协助编写文档,开发示例或教程,或仅仅提出建议。

更多信息

欢迎关于Spring Security的问题和评论。 您可以在 http://spring.io/questions处的Stack Overflow网站上使用Spring,以与框架的其他用户讨论Spring Security。 如上所述,请记住使用JIRA进行错误报告。

体系结构和实现

熟悉设置和运行一些基于命名空间配置的应用程序后,您可能希望更深入地了解框架在命名空间外观背后的工作原理。 像大多数软件一样,Spring Security在整个框架中都有一些中心接口,类和概念抽象。 在参考指南的这一部分,我们将看看其中的一些内容,看看它们如何协同工作来支持Spring Security中的认证和访问控制。

技术概述

运行环境

Spring Security 3.0需要Java 5.0 Runtime Environment或更高版本。 由于Spring Security旨在以独立方式运行,因此不需要将任何特殊配置文件放入Java运行时环境中。 尤其是,不需要配置特殊的Java身份验证和授权服务(JAAS)策略文件,也不需要将Spring Security放入常见的类路径位置。

同样,如果您使用的是EJB容器或Servlet容器,则不需要在任何地方放置任何特殊配置文件,也不需要将Spring Security包含在服务器类加载器中。 所有必需的文件都将包含在您的应用程序中。

这种设计提供了最大的部署时间灵活性,因为您可以简单地将目标工件(不管是JAR,WAR还是EAR)从一个系统复制到另一个系统,并立即生效。

核心组件

在Spring Security 3.0中,spring-security-core jar的内容被精简到最低限度。 它不再包含任何与Web应用程序安全性,LDAP或命名空间配置相关的代码。 我们来看看你会在核心模块中找到的一些Java类型。 它们代表了框架的构建块,所以如果您需要超越简单的名称空间配置,那么了解它们的含义非常重要,即使您实际上不需要直接与它们交互。

SecurityContextHolder,SecurityContext和认证对象

最基本的对象是SecurityContextHolder。 这是我们存储应用程序当前安全上下文的详细信息的地方,其中包括当前使用该应用程序的主体的详细信息。 默认情况下,SecurityContextHolder使用ThreadLocal来存储这些细节,这意味着安全上下文始终可用于同一执行线程中的方法,即使安全上下文没有作为这些方法的论点。 以这种方式使用ThreadLocal是非常安全的,因为如果在处理当前委托人的请求之后谨慎清除线程,那么这是非常安全的。 当然,Spring Security会自动为您处理,因此您无需担心。

有些应用程序并不完全适合使用ThreadLocal,因为它们使用线程的具体方式。 例如,Swing客户端可能希望Java虚拟机中的所有线程使用相同的安全上下文。 可以在启动时使用策略配置SecurityContextHolder,以指定您希望如何存储上下文。 对于独立应用程序,您可以使用SecurityContextHolder.MODE_GLOBAL策略。 其他应用程序可能希望安全线程产生的线程也具有相同的安全身份。 这通过使用SecurityContextHolder.MODE_INHERITABLETHREADLOCAL来实现。 您可以通过两种方式从默认的SecurityContextHolder.MODE_THREADLOCAL更改模式。 第一个是设置系统属性,第二个是在SecurityContextHolder上调用静态方法。 大多数应用程序不需要改变默认设置,但是如果需要,请查看用于SecurityContextHolder的JavaDoc以了解更多信息。

获取有关当前用户的信息

SecurityContextHolder内部,我们存储当前与应用程序交互的委托人的详细信息。 Spring Security使用Authentication对象来表示这些信息。 您通常不需要自己创建Authentication对象,但用户查询Authentication对象相当常见。 您可以使用以下代码块 - 从应用程序中的任何位置 - 获取当前通过身份验证的用户的名称,例如:

Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();

if (principal instanceof UserDetails) {
String username = ((UserDetails)principal).getUsername();
} else {
String username = principal.toString();
}

通过调用getContext()返回的对象是SecurityContext接口的一个实例。 这是保存在线程本地存储中的对象。 正如我们将在下面看到的,Spring Security中的大多数身份验证机制均返回一个UserDetails实例作为主体。

UserDetailsS​​ervice。==== 从上面的代码片段中要注意的另一项是您可以从Authentication对象获取主体。 校长只是一个Object。 大多数情况下,这可以转换为UserDetails对象。 UserDetails是Spring Security的核心接口。 它代表一个委托人,但以一种可扩展的和特定于应用程序的方式。 可以将UserDetails看作您自己的用户数据库与Spring Security在SecurityContextHolder中需要的适配器之间的适配器。 作为您自己的用户数据库中的某些东西的表示形式,您经常会将UserDetails转换为您的应用程序提供的原始对象,因此您可以调用业务特定的方法(如getEmail(),{{2 }} 等等)。

现在您可能想知道,那么何时提供UserDetails对象?我怎么做?我以为你说这件事是声明式的,我不需要编写任何Java代码 - 什么给了?简短的回答是,有一个称为UserDetailsService的特殊界面。 此接口上的唯一方法接受基于String的用户名参数并返回UserDetails

UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;

这是在Spring Security中为用户加载信息的最常见方法,只要需要用户信息,您就会看到它在整个框架中使用。

在成功认证时,UserDetails用于构建存储在SecurityContextHolder中的Authentication对象(更多位于此below)。 好消息是,我们提供了许多UserDetailsService实现,包括使用内存映射(InMemoryDaoImpl)的另一个实现,以及另一个使用JDBC(JdbcDaoImpl)的实现。 不过,大多数用户倾向于编写他们自己的代码,而他们的实现通常只是坐在代表其员工,客户或应用程序其他用户的现有数据访问对象(DAO)之上。 请记住,使用上述代码片段可始终从SecurityContextHolder获得任何UserDetailsService回报。

 

UserDetailsService通常存在一些混淆。 它纯粹是用于用户数据的DAO,除了将该数据提供给框架内的其他组件外,不执行其他功能。 特别是,__不会对用户进行身份验证,这是由AuthenticationManager完成的。 在许多情况下,如果您需要自定义身份验证过程,则implement AuthenticationProvider更直接。

的GrantedAuthority

除了委托人之外,Authentication提供的另一个重要方法是getAuthorities()。 此方法提供了一组GrantedAuthority对象。 毫不奇怪,GrantedAuthority是授予校长的权威。 此类权限通常为"roles",如ROLE_ADMINISTRATORROLE_HR_SUPERVISOR。 稍后将为Web授权,方法授权和域对象授权配置这些角色。 Spring Security的其他部门有能力解释这些权威,并希望他们出席。 GrantedAuthority对象通常由UserDetailsService加载。

通常,GrantedAuthority对象是应用程序范围的权限。 它们不是特定于给定域对象的。 因此,您不可能有GrantedAuthority代表对{54}的对象Employee的许可权,因为如果有成千上万的此类权限,您会很快耗尽内存(至少,导致应用程序花费很长时间来验证用户)。 当然,Spring Security的设计明确是为了处理这个常见的需求,但是你应该使用项目的域对象安全功能来达到这个目的。

摘要

回想一下,到目前为止,Spring Security的主要构建块是:

  • SecurityContextHolder,以提供对SecurityContext的访问。

  • SecurityContext来保存Authentication和可能的特定于请求的安全信息。

  • Authentication,以特定于Spring Security的方式表示主体。

  • GrantedAuthority,以反映授予主体的应用程序范围的权限。

  • UserDetails,从应用程序的DAO或其他安全数据源提供必要的信息来构建Authentication对象。

  • UserDetailsService,以基于String的用户名(或证书ID等)传递时创建UserDetails

现在您已经了解了这些重复使用的组件,让我们仔细看看认证过程。

认证

Spring Security可以参与许多不同的认证环境。 尽管我们建议人们使用Spring Security进行身份验证,并且不会与现有的容器管理身份验证集成,但仍然支持 - 与您自己的专有身份验证系统集成。

Spring Security中的认证是什么?

我们来考虑一下每个人都熟悉的标准身份验证方案。

  1. 提示用户使用用户名和密码登录。

  2. 系统(成功)验证密码对用户名是否正确。

  3. 获取该用户的上下文信息(其角色列表等)。

  4. 为用户建立安全上下文

  5. 用户可能会继续执行某些操作,该操作可能受访问控制机制保护,访问控制机制会针对当前安全上下文信息检查操作所需的权限。

前三项构成了认证过程,因此我们将在Spring Security中看看这些是如何发生的。

  1. 获取用户名和密码并将其合并到UsernamePasswordAuthenticationTokenAuthentication界面的一个实例,我们之前看到)的一个实例中。

  2. 将令牌传递给AuthenticationManager的实例进行验证。

  3. AuthenticationManager在成功验证时返回完全填充的Authentication实例。

  4. 通过调用SecurityContextHolder.getContext().setAuthentication(…​)传入返回的认证对象来建立安全上下文。

从那时起,用户被认为是被认证的。 我们来看一些代码作为例子。

import org.springframework.security.authentication.*;
import org.springframework.security.core.*;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;

public class AuthenticationExample {
private static AuthenticationManager am = new SampleAuthenticationManager();

public static void main(String[] args) throws Exception {
	BufferedReader in = new BufferedReader(new InputStreamReader(System.in));

	while(true) {
	System.out.println("Please enter your username:");
	String name = in.readLine();
	System.out.println("Please enter your password:");
	String password = in.readLine();
	try {
		Authentication request = new UsernamePasswordAuthenticationToken(name, password);
		Authentication result = am.authenticate(request);
		SecurityContextHolder.getContext().setAuthentication(result);
		break;
	} catch(AuthenticationException e) {
		System.out.println("Authentication failed: " + e.getMessage());
	}
	}
	System.out.println("Successfully authenticated. Security context contains: " +
			SecurityContextHolder.getContext().getAuthentication());
}
}

class SampleAuthenticationManager implements AuthenticationManager {
static final List<GrantedAuthority> AUTHORITIES = new ArrayList<GrantedAuthority>();

static {
	AUTHORITIES.add(new SimpleGrantedAuthority("ROLE_USER"));
}

public Authentication authenticate(Authentication auth) throws AuthenticationException {
	if (auth.getName().equals(auth.getCredentials())) {
	return new UsernamePasswordAuthenticationToken(auth.getName(),
		auth.getCredentials(), AUTHORITIES);
	}
	throw new BadCredentialsException("Bad Credentials");
}
}

这里我们写了一个小程序,要求用户输入用户名和密码并执行上述顺序。 我们在此处实施的AuthenticationManager将验证任何用户名和密码相同的用户。 它为每个用户分配一个角色。 从上面的输出将会是这样的:

Please enter your username:
bob
Please enter your password:
password
Authentication failed: Bad Credentials
Please enter your username:
bob
Please enter your password:
bob
Successfully authenticated. Security context contains: \
org.springframework.security.authentication.UsernamePasswordAuthenticationToken@441d0230: \
Principal: bob; Password: [PROTECTED]; \
Authenticated: true; Details: null; \
Granted Authorities: ROLE_USER

请注意,您通常不需要编写任何代码。 该过程通常在内部发生,例如在Web认证过滤器中。 我们刚刚在这里包含了代码,以表明在Spring Security中实际构成认证的问题有一个相当简单的答案。 当SecurityContextHolder包含完全填充的Authentication对象时,会对用户进行身份验证。

直接设置SecurityContextHolder内容

实际上,Spring Security并不介意如何将Authentication对象放入SecurityContextHolder中。 唯一关键的要求是SecurityContextHolder包含表示AbstractSecurityInterceptor之前的委托人{我们稍后会看到更多信息}的Authentication需要授权用户操作。

您可以(以及许多用户)编写自己的过滤器或MVC控制器,以提供与基于Spring Security的身份验证系统的互操作性。 例如,您可能正在使用容器管理的身份验证,它使当前用户可以从ThreadLocal或JNDI位置获得。 或者,您可能会为拥有旧版专有认证系统的公司工作,该系统是您无法控制的公司"standard"。 在这种情况下,Spring Security很容易运行,并且仍然提供授权功能。 您只需编写一个过滤器(或相当的),从一个位置读取第三方用户信息,构建一个特定于Spring Security的Authentication对象,并将其放入SecurityContextHolder中。 在这种情况下,您还需要考虑通常由内置身份验证基础结构自动处理的事情。 例如,在您将响应写入客户端脚注之前,您可能需要先向cache the context between requests创建一个HTTP会话:[在响应提交后无法创建会话。

如果您想知道如何在实际示例中实现AuthenticationManager,我们将在core services chapter中查看该示例。

Web应用程序中的=== 身份验证 现在我们来探讨一下在Web应用程序中使用Spring Security的情况(未启用web.xml安全性)。 用户如何进行身份验证并建立安全上下文?

考虑一个典型的Web应用程序的身份验证过程:

  1. 您访问主页,然后点击链接。

  2. 请求发送到服务器,并且服务器确定您已请求受保护的资源。

  3. 由于您目前没有进行身份验证,因此服务器会发回一个响应,指出您必须进行身份验证。 响应可能是HTTP响应代码,也可能是重定向到特定网页。

  4. 根据验证机制,您的浏览器将重定向到特定的网页,以便您填写表单,或者浏览器以某种方式检索您的身份(通过BASIC身份验证对话框,cookie, X.509证书等)。

  5. 浏览器将向服务器发送回应。 这将是一个HTTP POST,其中包含您填写的表单的内容,或者包含您的验证细节的HTTP标头。

  6. 接下来,服务器将决定提交的凭证是否有效。 如果它们有效,下一步就会发生。 如果它们无效,通常你的浏览器会被要求重试(所以你回到上面的第二步)。

  7. 您为导致身份验证过程所做的原始请求将被重试。 希望您已通过足够的授权机构进行身份验证,以访问受保护的资源。 如果您有足够的访问权限,请求将会成功。 否则,您将收到HTTP错误代码403,表示"forbidden"。

Spring Security具有不同的类,负责上述大多数步骤。 主要参与者(按其使用顺序)是ExceptionTranslationFilterAuthenticationEntryPoint和"authentication mechanism",负责调用我们在其中看到的AuthenticationManager前一节。

的ExceptionTranslationFilter

ExceptionTranslationFilter是一个Spring Security过滤器,负责检测抛出的任何Spring Security异常。 这种例外通常会由授权服务的主要提供者AbstractSecurityInterceptor抛出。 我们将在下一节讨论AbstractSecurityInterceptor,但现在我们只需要知道它会产生Java异常,并且对HTTP没有任何认识,或者如何去认证主体。 相反,ExceptionTranslationFilter提供此服务,具体负责返回错误代码403(如果主体已通过身份验证,并因此缺少足够的访问权限 - 按上述步骤7),或启动AuthenticationEntryPoint(如果委托人未经认证,因此我们需要开始第三步)。

的AuthenticationEntryPoint

AuthenticationEntryPoint负责上面列表中的第三步。 正如你可以想象的那样,每个Web应用程序都会有一个默认的身份验证策略(当然,这可以像Spring Security中的其他任何东西一样配置,但现在让我们保持简单)。 每个主要认证系统都有自己的AuthenticationEntryPoint实现,通常执行步骤3中描述的操作之一。

认证机制

一旦您的浏览器提交了您的身份验证凭据(无论是作为HTTP表单发布还是HTTP标头),服务器上都需要有"collects"这些身份验证详细信息。 到目前为止,我们正在上面的列表中的第六步。 在Spring Security中,我们为从用户代理(通常是Web浏览器)收集认证详细信息的功能有一个特殊名称,将其称为"authentication mechanism"。 示例是基于表单的登录和基本身份验证。 一旦从用户代理收集了认证详细信息,就会构建Authentication "request"对象,然后将其呈现给AuthenticationManager

在认证机制接收到完全填充的Authentication对象后,它将认为请求有效,将Authentication放入SecurityContextHolder,并重试原始请求(步骤7以上)。 另一方面,如果AuthenticationManager拒绝了请求,认证机制将要求用户代理重试(上面的第二步)。

在请求之间存储SecurityContext

根据应用程序的类型,可能需要制定一个策略来存储用户操作之间的安全上下文。 在典型的Web应用程序中,用户登录一次,随后通过其会话ID进行标识。 服务器缓存持续时间会话的主要信息。 在Spring Security中,存储请求之间SecurityContext的责任归属于SecurityContextPersistenceFilter,缺省情况下,该请求将上下文存储为HTTP请求之间的HttpSession属性。 它将每个请求的上下文恢复到SecurityContextHolder,并且在请求完成时关键地清除SecurityContextHolder。 出于安全目的,您不应直接与HttpSession进行交互。 没有理由这么做 - 总是使用SecurityContextHolder

许多其他类型的应用程序(例如,无状态的RESTful Web服务)不使用HTTP会话,并将在每个请求中重新进行身份验证。 但是,{{0​​}}包含在链中以确保SecurityContextHolder在每个请求之后被清除仍然很重要。

 

在单个会话中接收并发请求的应用程序中,同一个SecurityContext实例将在线程之间共享。 即使正在使用ThreadLocal,它也是从每个线程的HttpSession中检索的实例。 如果您想临时更改线程正在运行的上下文,这会产生影响。 如果您只是使用SecurityContextHolder.getContext()并在返回的上下文对象上调用setAuthentication(anAuthentication),则Authentication对象将在共享相同SecurityContext实例的all个并发线程中更改。 您可以自定义SecurityContextPersistenceFilter的行为,为每个请求创建一个全新的SecurityContext,以防止一个线程中的更改影响另一个线程。 或者,您可以在临时更改上下文的位置创建新实例。 方法SecurityContextHolder.createEmptyContext()总是返回一个新的上下文实例。

Spring Security中的=== 访问控制(授权) 负责在Spring Security中制定访问控制决策的主界面是AccessDecisionManager。 它有一个decide方法,它需要一个表示请求主体访问的Authentication对象,一个"secure object"(见下文)以及适用于该对象的安全元数据属性列表(例如a授予访问权限所需的角色列表)。

安全和AOP建议

如果你熟悉AOP,你会意识到有不同类型的建议可用:在之前,之后,投掷和周围。 周围的建议非常有用,因为顾问可以选择是否继续进行方法调用,是否修改响应以及是否抛出异常。 Spring Security为方法调用和Web请求提供了一个周围的建议。 我们使用Spring的标准AOP支持为方法调用提供了全面的建议,并且我们使用标准Filter实现了对Web请求的全面建议。

对于那些不熟悉AOP的人来说,理解的关键是Spring Security可以帮助您保护方法调用以及Web请求。 大多数人都对保护其服务层上的方法调用感兴趣。 这是因为服务层是大多数业务逻辑驻留在当代Java EE应用程序中的地方。 如果你只需要在服务层保证方法调用的安全,Spring的标准AOP就足够了。 如果您需要直接保护域对象,您可能会发现AspectJ值得考虑。

您可以选择使用AspectJ或Spring AOP执行方法授权,也可以选择使用过滤器执行Web请求授权。 您可以一起使用零个,一个,两个或三个这些方法。 主流使用模式是执行一些Web请求授权,再加上服务层上的一些Spring AOP方法调用授权。

安全对象和AbstractSecurityInterceptor

那么,无论如何,is是"secure object"? Spring Security使用这个术语来指代可以有安全性的任何对象(例如授权决定)。 最常见的例子是方法调用和Web请求。

每个受支持的安全对象类型都有其自己的拦截器类,它是AbstractSecurityInterceptor的子类。 重要的是,在AbstractSecurityInterceptor被调用的时候,如果委托人已被认证,则SecurityContextHolder将包含有效的Authentication

AbstractSecurityInterceptor为处理安全对象请求提供了一致的工作流程,通常为:

  1. 查找与当前请求关联的"configuration attributes"

  2. 将安全对象,当前Authentication和配置属性提交给AccessDecisionManager以获得授权决定

  3. 可以选择更改调用发生的Authentication

  4. 允许继续进行安全对象调用(假设授予访问权限)

  5. 调用返回后调用AfterInvocationManager(如果已配置)。 如果调用引发异常,则不会调用AfterInvocationManager

什么是配置属性?

可以将"configuration attribute"看作对AbstractSecurityInterceptor使用的类具有特殊含义的字符串。 它们由框架中的接口ConfigAttribute表示。 它们可能是简单的角色名称,或者具有更复杂的含义,具体取决于AccessDecisionManager实施的复杂程度。 AbstractSecurityInterceptor配置有一个SecurityMetadataSource,用于查找安全对象的属性。 通常这个配置对用户是隐藏的。 配置属性将作为安全方法的注释或安全URL上的访问属性输入。 例如,当我们在名称空间介绍中看到类似<intercept-url pattern='/secure/**' access='ROLE_A,ROLE_B'/>的内容时,这表示配置属性ROLE_AROLE_B适用于匹配给定模式的Web请求。 实际上,使用默认的AccessDecisionManager配置,这意味着任何匹配这两个属性的GrantedAuthority都将被允许访问。 严格来说,它们只是属性,解释依赖于AccessDecisionManager实现。 使用前缀ROLE_是一个标记,用于表示这些属性是角色,并且应该由Spring Security的RoleVoter使用。 这仅在使用基于选民的AccessDecisionManager时才有用。 我们将看到如何在authorization chapter中实现AccessDecisionManager

RunAsManager

假设AccessDecisionManager决定允许请求,则AbstractSecurityInterceptor通常只会继续处理请求。 尽管如此,在极少数情况下,用户可能想用Authentication替换SecurityContext中的AuthenticationAccessDecisionManager处理{{2} }。 在合理的异常情况下,这可能非常有用,例如,如果服务层方法需要调用远程系统并呈现不同的身份。 由于Spring Security自动将安全身份从一台服务器传播到另一台服务器(假设您使用正确配置的RMI或HttpInvoker远程协议客户端),这可能很有用。

AfterInvocationManager

遵循安全对象调用流程,然后返回 - 这可能意味着方法调用完成或过滤器链处理 -  AbstractSecurityInterceptor最终有机会处理调用。 在这个阶段,AbstractSecurityInterceptor对可能修改返回对象感兴趣。 我们可能希望发生这种情况,因为无法对安全对象调用授权决定"on the way in"。 由于具有高度可插入性,AbstractSecurityInterceptor会将控制权交给AfterInvocationManager,以根据需要实际修改对象。 这个类甚至可以完全替换对象,或者抛出异常,或者不以任何方式改变它。 只有调用成功时才会执行调用后检查。 如果发生异常,则额外的检查将被跳过。

AbstractSecurityInterceptor及其相关对象显示在[abstract-security-interceptor]

。安全拦截器和"secure object"模型 image::images/security-interception.png[抽象安全拦截器]

扩展安全对象模型

只有开发人员想要采用全新的截取和授权请求的方式,才需要直接使用安全对象。 例如,可以构建一个新的安全对象来保护对消息系统的调用。 任何需要安全性并且还提供拦截呼叫的方式(如围绕通知语义的AOP)都可以被制作成安全对象​​。 话虽如此,大多数Spring应用程序将完全透明地使用目前支持的三种安全对象类型(AOP联盟MethodInvocation,AspectJ JoinPoint和Web请求FilterInvocation)。

本地化

Spring Security支持终端用户可能看到的异常消息的本地化。 如果您的应用程序是为讲英语的用户设计的,则默认情况下,您不需要执行任何操作,所有安全信息都是英文的。 如果您需要支持其他语言环境,则需要了解的所有内容都包含在本节中。

所有异常消息都可以本地化,包括与认证失败和访问被拒绝有关的消息(授权失败)。 专注于开发人员或系统部署人员的异常和日志消息(包括不正确的属性,接口契约违规,使用不正确的构造函数,启动时间验证,调试级别日志记录)未本地化,而是在Spring Security代码中以英文硬编码。

spring-security-core-xx.jar中发送邮件,您会发现一个org.springframework.security包,其中包含messages.properties文件,以及一些常用语言的本地化版本。 这应该被你的ApplicationContext引用,因为Spring Security类实现了Spring的MessageSourceAware接口,并期望消息解析器在应用程序上下文启动时被依赖注入。 通常,您只需在应用程序上下文中注册一个bean来引用这些消息。 一个例子如下所示:

<bean id="messageSource"
	class="org.springframework.context.support.ReloadableResourceBundleMessageSource">
<property name="basename" value="classpath:org/springframework/security/messages"/>
</bean>

messages.properties按照标准资源包进行命名,并表示Spring Security消息支持的默认语言。 这个默认文件是英文的。

如果您想自定义messages.properties文件或支持其他语言,则应复制该文件并相应地对其重命名,并在上面的bean定义中注册它。 此文件中没有大量的消息密钥,因此本地化不应被视为主要举措。 如果您确实执行了此文件的本地化,请考虑通过记录JIRA任务并附上适当命名的messages.properties本地化版本与社区分享您的工作。

Spring Security依靠Spring的本地化支持来实际查找适当的消息。 为了达到此目的,您必须确保传入请求中的语言环境存储在Spring的org.springframework.context.i18n.LocaleContextHolder中。 Spring MVC的DispatcherServlet会自动为您的应用程序执行此操作,但由于在此之前调用了Spring Security的过滤器,因此需要在调用过滤器之前将LocaleContextHolder设置为包含正确的Locale。 您可以自己在过滤器中执行此操作(它必须位于web.xml中的Spring Security过滤器之前),也可以使用Spring的RequestContextFilter。 有关在Spring中使用本地化的更多详细信息,请参阅Spring Framework文档。

"contacts"示例应用程序设置为使用本地化的消息。

核心服务

现在我们对Spring Security体系结构及其核心类进行高级概述,让我们仔细研究一个或两个核心接口及其实现,特别是AuthenticationManagerUserDetailsServiceAccessDecisionManager。 这些文件会在本文档的其余部分定期出现,因此了解它们如何配置以及如何操作非常重要。

AuthenticationManager,ProviderManager和AuthenticationProvider

AuthenticationManager仅仅是一个接口,所以实现可以是我们选择的任何东西,但实际上它是如何工作的?如果我们需要检查多个身份验证数据库或不同的身份验证服务(如数据库和LDAP服务器)的组合,该怎么办?

Spring Security中的默认实现被称为ProviderManager,而不是自己处理认证请求,它委托给配置的AuthenticationProvider列表,每个AuthenticationProvider查询是否可以执行认证。 每个提供程序将引发异常或返回完全填充的Authentication对象。 请记住我们的好朋友UserDetailsUserDetailsService?如果不是,请回到上一章并刷新你的记忆。 验证身份验证请求的最常见方法是加载相应的UserDetails,并检查加载的密码与用户输入的密码。 这是DaoAuthenticationProvider使用的方法(见下文)。 装载的UserDetails对象 - 特别是其包含的GrantedAuthority- 将用于构建完整填充的Authentication对象,该对象从成功的身份验证中返回并存储在{{3} }。

如果您使用的是命名空间,则ProviderManager的实例将在内部创建和维护,并且您可以使用命名空间身份验证提供程序元素向它添加提供程序(请参阅the namespace chapter)。 在这种情况下,您不应该在应用程序上下文中声明一个ProviderManager bean。 但是,如果您不使用名称空间,那么您会声明它如下所示:

<bean id="authenticationManager"
		class="org.springframework.security.authentication.ProviderManager">
	<constructor-arg>
		<list>
			<ref local="daoAuthenticationProvider"/>
			<ref local="anonymousAuthenticationProvider"/>
			<ref local="ldapAuthenticationProvider"/>
		</list>
	</constructor-arg>
</bean>

在上面的例子中,我们有三个提供者。 它们按所示顺序(使用List)进行尝试,每个提供程序都可以尝试进行身份验证,或者通过简单地返回null来跳过身份验证。 如果所有实现都返回null,则ProviderManager将抛出ProviderNotFoundException。 如果您有兴趣详细了解链接提供商,请参阅ProviderManager Javadoc。

身份验证机制(例如Web表单登录处理过滤器)会引用ProviderManager,并会调用它来处理其身份验证请求。 您需要的提供者有时可以与认证机制互换,而在其他时候,他们将依赖于特定的认证机制。 例如,DaoAuthenticationProviderLdapAuthenticationProvider与任何提交简单的用户名/密码认证请求的机制兼容,因此可以与基于表单的登录或HTTP基本认证一起使用。 另一方面,一些认证机制会创建一个认证请求对象,该对象只能由单一类型的AuthenticationProvider来解释。 例如,JA-SIG CAS使用服务票据的概念,因此只能通过CasAuthenticationProvider进行身份验证。 您不必太在意这一点,因为如果您忘记注册合适的提供商,则只需在尝试进行身份验证时收到ProviderNotFoundException

成功验证时擦除凭证

默认情况下(从Spring Security 3.1开始),ProviderManager将尝试清除成功认证请求返回的Authentication对象中的任何敏感凭据信息。 这可以防止密码等信息被保留超过必要的时间。

例如,当您使用用户对象缓存时,这可能会导致问题,以提高无状态应用程序的性能。 如果Authentication包含对缓存中对象的引用(例如UserDetails实例),并且已删除其凭据,则无法再对缓存值进行身份验证。 如果您使用缓存,则需要考虑这一点。 一个明显的解决方案是先创建对象的副本,无论是在缓存实现中还是在创建返回的Authentication对象的AuthenticationProvider中。 或者,您可以禁用ProviderManager上的eraseCredentialsAfterAuthentication属性。 有关更多信息,请参阅Javadoc。

DaoAuthenticationProvider还可以为

Spring Security最简单的AuthenticationProviderDaoAuthenticationProvider,它也是该框架最早支持的之一。 它利用UserDetailsService(作为DAO)来查找用户名,密码和GrantedAuthority。 它仅通过将UsernamePasswordAuthenticationToken中提交的密码与UserDetailsService加载的密码进行比较来对用户进行身份验证。 配置提供者非常简单:

<bean id="daoAuthenticationProvider"
	class="org.springframework.security.authentication.dao.DaoAuthenticationProvider">
<property name="userDetailsService" ref="inMemoryDaoImpl"/>
<property name="passwordEncoder" ref="passwordEncoder"/>
</bean>

PasswordEncoder是可选的。 PasswordEncoder提供从配置的UserDetailsService返回的UserDetails对象中显示的密码的编码和解码。 这将在below中进行更详细的讨论。

UserDetailsS​​ervice实现

如本参考指南前面所述,大多数身份验证提供程序利用UserDetailsUserDetailsService接口。 回想一下,UserDetailsService的合同是单一方法:

UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;

返回的UserDetails是一个提供getter的接口,用于保证认证信息非空值的提供,如用户名,密码,授予的权限以及用户帐户是启用还是禁用。 即使用户名和密码实际上未用作身份验证决策的一部分,大多数身份验证提供程序也会使用UserDetailsService。 他们可能会将UserDetails对象仅用于其GrantedAuthority信息,因为其他系统(如LDAP或X.509或CAS等)承担了实际验证凭据的责任。

鉴于UserDetailsService的实现非常简单,用户应该很容易使用自己选择的持久性策略来检索认证信息。 话虽如此,Spring Security确实包含了一些有用的基础实现,我们将在下面进行介绍。

内存认证

易于使用创建自定义的UserDetailsService实现,从所选持久性引擎中提取信息,但许多应用程序不需要这种复杂性。 如果您正在构建原型应用程序,或者刚刚开始集成Spring Security,尤其当您不想花时间配置数据库或编写UserDetailsService实现时,情况尤其如此。 对于这种情况,一个简单的选择是使用安全性namespace中的user-service元素:

<user-service id="userDetailsService">
<!-- Password is prefixed with {noop} to indicate to DelegatingPasswordEncoder that
NoOpPasswordEncoder should be used. This is not safe for production, but makes reading
in samples easier. Normally passwords should be hashed using BCrypt -->
<user name="jimi" password="{noop}jimispassword" authorities="ROLE_USER, ROLE_ADMIN" />
<user name="bob" password="{noop}bobspassword" authorities="ROLE_USER" />
</user-service>

这也支持使用外部属性文件:

<user-service id="userDetailsService" properties="users.properties"/>

属性文件应该包含表单中的条目

username=password,grantedAuthority[,grantedAuthority][,enabled|disabled]

例如

jimi=jimispassword,ROLE_USER,ROLE_ADMIN,enabled
bob=bobspassword,ROLE_USER,enabled

JdbcDaoImpl

Spring Security还包含一个可以从JDBC数据源获取认证信息的UserDetailsService。 使用内部Spring JDBC,因此避免了用于存储用户详细信息的全功能对象关系映射程序(ORM)的复杂性。 如果您的应用程序确实使用了ORM工具,那么您可能更愿意编写一个自定义UserDetailsService来重用您可能已经创建的映射文件。 返回JdbcDaoImpl,示例配置如下所示:

<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="org.hsqldb.jdbcDriver"/>
<property name="url" value="jdbc:hsqldb:hsql://localhost:9001"/>
<property name="username" value="sa"/>
<property name="password" value=""/>
</bean>

<bean id="userDetailsService"
	class="org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl">
<property name="dataSource" ref="dataSource"/>
</bean>

您可以通过修改上面显示的DriverManagerDataSource来使用不同的关系数据库管理系统。 您也可以像使用其他Spring配置一样使用从JNDI获取的全局数据源。

权限组

默认情况下,假设权限直接映射到用户(请参阅database schema appendix),JdbcDaoImpl为单个用户加载权限。 另一种方法是将权限分为组并将组分配给用户。 有些人更喜欢这种方法作为管理用户权利的手段。 有关如何启用组权限的更多信息,请参阅JdbcDaoImpl Javadoc。 组模式也包含在附录中。

密码编码

Spring Security的PasswordEncoder界面用于执行密码的单向转换,以便安全地存储密码。 鉴于PasswordEncoder是单向转换,当密码转换需要两种方式(即存储用于向数据库进行身份验证的凭据)时,它并不适用。 通常PasswordEncoder用于存储需要在验证时与用户提供的密码进行比较的密码。

密码历史记录

多年来,用于存储密码的标准机制已经发展。 开始时密码以纯文本形式存储。 密码被认为是安全的,因为数据存储密码被保存在所需的凭据中以便访问它。 但是,恶意用户能够通过SQL注入等攻击方式找到获取大量"data dumps"用户名和密码的方法。 随着越来越多的用户凭据成为公安专家意识到我们需要做更多的事情来保护用户的密码。

然后鼓励开发人员在通过单向散列(如SHA-256)运行密码后存储密码。 当用户尝试认证时,散列密码将与他们键入的密码的散列进行比较。 这意味着系统只需要存储密码的单向散列。 如果发生了违规,那么只有密码的单向散列被暴露。 由于哈希是一种方法,并且在计算上很难猜测给定散列的密码,所以在系统中找出每个密码是不值得的。 为了击败这个新系统,恶意用户决定创建名为https://en.wikipedia.org/wiki/Rainbow_table [Rainbow Tables]的查找表。 他们不是每次都在猜测每个密码,而是一次计算密码并将其存储在查找表中。

为了降低彩虹表的有效性,鼓励开发者使用咸味密码。 而不是仅使用密码作为散列函数的输入,将为每个用户的密码生成随机字节(称为salt)。 盐和用户的密码将通过产生唯一散列的散列函数运行。 盐将以明文形式与用户密码一起存储。 然后当用户尝试认证时,散列密码将与存储的盐的哈希以及他们输入的密码进行比较。 独特的盐意味着Rainbow Tables不再有效,因为每种盐和密码组合的散列值都不相同。

在现代,我们意识到密码哈希(如SHA-256)不再安全。 原因是,使用现代硬件,我们可以每秒执行数十亿次哈希计算。 这意味着我们可以轻松破解每个密码。

现在鼓励开发人员利用自适应单向函数来存储密码。 使用自适应单向函数对密码进行验证是有意识的资源(即CPU,内存等)密集型。 自适应单向函数允许配置随着硬件变得越来越好的"work factor"。 建议"work factor"调整大约1秒钟以验证系统上的密码。 这种权衡是为了让攻击者难以破解密码,但并不那么昂贵,它会给你自己的系统带来过大的负担。 Spring Security试图为"work factor"提供良好的起点,但鼓励用户为自己的系统自定义"work factor",因为不同系统的性能会有很大差异。 应该使用的自适应单向函数的例子包括 bcrypt PBKDF2 scrypt 和https://en.wikipedia.org/wiki/Argon2[Argon2]。

由于自适应单向函数是故意耗费资源的,因此验证每个请求的用户名和密码将显着降低应用程序的性能。 Spring Security(或任何其他库)没有什么能够加速验证密码,因为通过使验证资源密集而获得安全性。 鼓励用户交换短期凭证(即会话,OAuth令牌等)的长期凭证(即用户名和密码)。 短期凭证可以快速验证而不会有任何安全性损失。

DelegatingPasswordEncoder

在Spring Security 5.0之前,默认的PasswordEncoderNoOpPasswordEncoder,它需要纯文本密码。 根据{{​​0}}部分,您可能预计现在默认的PasswordEncoderBCryptPasswordEncoder类似。 但是,这忽略了三个现实世界的问题:

  • 许多使用旧密码编码的应用程序无法轻松迁移

  • 密码存储的最佳做法将再次发生变化。

  • 作为一个框架,Spring Security不能经常发生重大更改

相反,Spring Security引入了DelegatingPasswordEncoder,它解决了所有的问题:

  • 确保使用当前密码存储建议对密码进行编码

  • 允许以现代和传统格式验证密码

  • 允许将来升级编码

您可以使用PasswordEncoderFactories轻松构建DelegatingPasswordEncoder的实例。

PasswordEncoder passwordEncoder =
    PasswordEncoderFactories.createDelegatingPasswordEncoder();

或者,您可以创建自己的自定义实例。例如:

String idForEncode = "bcrypt";
Map encoders = new HashMap<>();
encoders.put(idForEncode, new BCryptPasswordEncoder());
encoders.put("noop", NoOpPasswordEncoder.getInstance());
encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
encoders.put("scrypt", new SCryptPasswordEncoder());
encoders.put("sha256", new StandardPasswordEncoder());

PasswordEncoder passwordEncoder =
    new DelegatingPasswordEncoder(idForEncode, encoders);
密码存储格式

密码的一般格式是:

{id}encodedPassword

这样id是一个标识符,用于查找应该使用哪一个PasswordEncoderencodedPassword是所选PasswordEncoder的原始编码密码。 id必须位于密码的开头,以{开头并以}结尾。 如果无法找到id,则id将为空。 例如,以下可能是使用不同id编码的密码列表。 所有原始密码均为"password"。

{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG 
{noop}password 
{pbkdf2}5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc 
{scrypt}$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc=  
{sha256}97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0 

<1>第一个密码的PasswordEncoder标识为bcrypt,编码密码为$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG。 匹配时,它将委托给BCryptPasswordEncoder <2>第二个密码的PasswordEncoder ID为noop,并且编码密码为password。 匹配时,它将委托给NoOpPasswordEncoder <3>第三个密码的PasswordEncoder标识为pbkdf2,编码密码为5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc。 匹配时,它将委托给Pbkdf2PasswordEncoder <4>第四个密码的PasswordEncoder编号为scrypt,编码密码为$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc= 匹配时,它将委托给SCryptPasswordEncoder <5>最终密码的PasswordEncoder ID为sha256,encodedPassword为97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0。 匹配时,它将委托给StandardPasswordEncoder

 

一些用户可能会担心存储格式是为潜在的黑客提供的。 这不是一个问题,因为密码的存储不依赖于算法是一个秘密。 此外,大多数格式都很容易让攻击者找出没有前缀的地方。 例如,BCrypt密码通常以$2a$开头。

密码编码

传递给构造函数的idForEncode将决定使用哪一个PasswordEncoder来对密码进行编码。 在我们上面构建的DelegatingPasswordEncoder中,这意味着编码password的结果将委派给BCryptPasswordEncoder并以{bcrypt}为前缀。 最终结果如下所示:

{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
密码匹配

匹配是基于{id}以及构造函数中提供的idPasswordEncoder的映射完成的。 我们在[Password Storage Format]中的示例提供了一个如何完成的示例。 默认情况下,使用密码和未映射的id调用matches(CharSequence, String)的结果(包括空id)将导致IllegalArgumentException。 这种行为可以使用DelegatingPasswordEncoder.setDefaultPasswordEncoderForMatches(PasswordEncoder)进行自定义。

通过使用id,我们可以匹配任何密码编码,但使用最现代的密码编码对密码进行编码。 这很重要,因为与加密不同,密码哈希的设计使得没有简单的方法来恢复明文。 由于无法恢复明文,因此难以迁移密码。 尽管用户迁移NoOpPasswordEncoder非常简单,但我们默认选择将其包含在内以简化入门体验。

入门体验

如果您正在制作演示或样本,花时间散列用户的密码会有点麻烦。 有便利的机制可以使这更容易,但这仍然不适用于生产。

User user = User.withDefaultPasswordEncoder()
  .username("user")
  .password("password")
  .roles("user")
  .build();
System.out.println(user.getPassword());
// {bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG

如果您正在创建多个用户,则还可以重新使用该构建器。

UserBuilder users = User.withDefaultPasswordEncoder();
User user = users
  .username("user")
  .password("password")
  .roles("USER")
  .build();
User admin = users
  .username("admin")
  .password("password")
  .roles("USER","ADMIN")
  .build();

这确实散列了存储的密码,但密码仍在内存和编译后的源代码中公开。 因此,对于生产环境来说,它仍然不被认为是安全的。 对于生产,你应该在外部散列你的密码。

故障处理

如{0}}中所述,存储的其中一个密码没有标识时会发生以下错误。

java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"
	at org.springframework.security.crypto.password.DelegatingPasswordEncoder$UnmappedIdPasswordEncoder.matches(DelegatingPasswordEncoder.java:233)
	at org.springframework.security.crypto.password.DelegatingPasswordEncoder.matches(DelegatingPasswordEncoder.java:196)

解决错误的最简单方法是切换为明确提供密码编码的PasswordEncoder。 解决问题的最简单方法是确定您的密码当前如何存储,并明确提供正确的PasswordEncoder。 如果您正在从Spring Security 4.2.x迁移,您可以通过公开NoOpPasswordEncoderbean来恢复到以前的行为。 例如,如果您正在使用Java配置,则可以创建如下所示的配置:

 

恢复到NoOpPasswordEncoder不被认为是安全的。 您应该转而使用DelegatingPasswordEncoder来支持安全的密码编码。

@Bean
public static NoOpPasswordEncoder passwordEncoder() {
    return NoOpPasswordEncoder.getInstance();
}

如果您使用的是XML配置,则可以公开一个ID为passwordEncoder的{​​{0}}:

<b:bean id="passwordEncoder"
        class="org.springframework.security.crypto.password.NoOpPasswordEncoder" factory-method="getInstance"/>

或者,您可以使用正确的ID为所有密码加上前缀,并继续使用DelegatingPasswordEncoder。 例如,如果您使用的是BCrypt,则可以将密码从以下类似的位置迁移:

$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG

{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG

BCryptPasswordEncoder

BCryptPasswordEncoder实施使用广泛支持的https://en.wikipedia.org/wiki/Bcrypt[bcrypt]算法对密码进行哈希处理。 为了使它对密码破解更具抵抗性,bcrypt故意缓慢。 与其他自适应单向函数一样,应该调整大约1秒以验证系统上的密码。

// Create an encoder with strength 16
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(16);
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));

Pbkdf2PasswordEncoder

Pbkdf2PasswordEncoder实施使用https://en.wikipedia.org/wiki/PBKDF2[PBKDF2]算法对密码进行哈希处理。 为了破解密码破解PBKDF2是一个故意缓慢的算法。 与其他自适应单向函数一样,应该调整大约1秒以验证系统上的密码。 当需要FIPS认证时,此算法是一个不错的选择。

// Create an encoder with all the defaults
Pbkdf2PasswordEncoder encoder = new Pbkdf2PasswordEncoder();
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));

SCryptPasswordEncoder

SCryptPasswordEncoder实施使用https://en.wikipedia.org/wiki/Scrypt[scrypt]算法对密码进行哈希处理。 为了击败定制硬件上的密码破解scrypt是一个故意缓慢的算法,需要大量的内存。 与其他自适应单向函数一样,应该调整大约1秒以验证系统上的密码。

// Create an encoder with all the defaults
SCryptPasswordEncoder encoder = new SCryptPasswordEncoder();
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));

其他PasswordEncoders。==

有大量其他PasswordEncoder实现完全为了向后兼容而存在。 他们都被弃用,表明他们不再被认为是安全的。 但是,由于难以迁移现有的遗留系统,因此没有计划将其删除。

杰克逊支持。===

Spring Security已经增加了Jackson Support来坚持Spring Security相关的类。 这可以提高在处理分布式会话(即会话复制,Spring会话等)时序列化Spring Security相关类的性能。

要使用它,请将JacksonJacksonModules.getModules(ClassLoader)注册为 杰克逊模块

ObjectMapper mapper = new ObjectMapper();
ClassLoader loader = getClass().getClassLoader();
List<Module> modules = SecurityJackson2Modules.getModules(loader);
mapper.registerModules(modules);

// ... use ObjectMapper as normally ...
SecurityContext context = new SecurityContextImpl();
// ...
String json = mapper.writeValueAsString(context);

测试

本节介绍Spring Security提供的测试支持。

 

要使用Spring Security测试支持,您必须将spring-security-test-{spring-security-version}.jar作为项目的依赖项。

测试方法安全性

本节演示如何使用Spring Security的测试支持来测试基于安全性的方法。 我们首先介绍需要用户进行身份验证才能访问的MessageService

public class HelloMessageService implements MessageService {

	@PreAuthorize("authenticated")
	public String getMessage() {
		Authentication authentication = SecurityContextHolder.getContext()
															.getAuthentication();
		return "Hello " + authentication;
	}
}

getMessage的结果是一个表示当前Spring Security Authentication的"Hello"的字符串。 输出示例如下所示。

Hello org.springframework.security.authentication.UsernamePasswordAuthenticationToken@ca25360: Principal: org.springframework.security.core.userdetails.User@36ebcb: Username: user; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: ROLE_USER; Credentials: [PROTECTED]; Authenticated: true; Details: null; Granted Authorities: ROLE_USER

安全测试设置

在我们使用Spring Security Test支持之前,我们必须执行一些设置。下面是一个例子:

@RunWith(SpringJUnit4ClassRunner.class) 
@ContextConfiguration 
public class WithMockUserTests {

这是如何设置Spring Security Test的基本示例。亮点是:

 @RunWith指示spring-test模块创建一个ApplicationContext。这与使用现有的Spring Test支持没有区别。有关更多信息,请参阅 Spring参考
 @ContextConfiguration指示spring-test用于创建ApplicationContext的配置。由于没有指定配置,因此将尝试默认配置位置。这与使用现有的Spring Test支持没有区别。有关更多信息,请参阅 Spring参考

注意:Spring Security使用WithSecurityContextTestExecutionListener挂钩到Spring Test支持中,这将确保我们的测试使用正确的用户运行。 它通过在运行我们的测试之前填充SecurityContextHolder来完成此操作。 测试完成后,它会清除SecurityContextHolder。 如果您只需要Spring Security相关支持,则可以用@SecurityTestExecutionListeners替换@ContextConfiguration

请记住,我们已将@PreAuthorize注释添加到我们的HelloMessageService,因此它需要经过身份验证的用户才能调用它。 如果我们进行了以下测试,我们预计将通过以下测试:

@Test(expected = AuthenticationCredentialsNotFoundException.class)
public void getMessageUnauthenticated() {
	messageService.getMessage();
}

@ WithMockUser

问题是"How could we most easily run the test as a specific user?" 答案是使用@WithMockUser。 以下测试将以具有用户名"user",密码"password"和角色"ROLE_USER"的用户身份运行。

@Test
@WithMockUser
public void getMessageWithMockUser() {
String message = messageService.getMessage();
...
}

具体如下:

  • 使用用户名"user"的用户不必存在,因为我们正在嘲笑用户

  • SecurityContext中填充的Authentication类型为UsernamePasswordAuthenticationToken

  • Authentication的主体是Spring Security的User对象

  • User将使用用户名"user",密码"password"和一个名为"ROLE_USER"的GrantedAuthority

我们的例子很好,因为我们可以利用很多默认值。 如果我们想用其他用户名运行测试,该怎么办? 以下测试将使用用户名"customUser"运行。同样,用户不需要实际存在。

@Test
@WithMockUser("customUsername")
public void getMessageWithMockUserCustomUsername() {
	String message = messageService.getMessage();
...
}

我们也可以轻松定制角色。 例如,将使用用户名"admin"和角色"ROLE_USER"和"ROLE_ADMIN"来调用此测试。

@Test
@WithMockUser(username="admin",roles={"USER","ADMIN"})
public void getMessageWithMockUserCustomUser() {
	String message = messageService.getMessage();
	...
}

如果我们不希望该值自动以ROLE_开头,我们可以使用权限属性。 例如,此测试将使用用户名"admin"和权限"USER"和"ADMIN"进行调用。

@Test
@WithMockUser(username = "admin", authorities = { "ADMIN", "USER" })
public void getMessageWithMockUserCustomAuthorities() {
	String message = messageService.getMessage();
	...
}

当然,将注释放在每种测试方法上可能有点乏味。 相反,我们可以在类级别放置注释,并且每个测试都将使用指定的用户。 例如,以下内容将针对用户使用用户名"admin",密码"password"以及角色"ROLE_USER"和"ROLE_ADMIN"运行每项测试。

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
@WithMockUser(username="admin",roles={"USER","ADMIN"})
public class WithMockUserTests {

默认情况下,在TestExecutionListener.beforeTestMethod事件期间设置SecurityContext。 这与JUnit的@Before之前发生的情况相同。 您可以将此更改为在JUnit的@Before之后但在调用测试方法之前的TestExecutionListener.beforeTestExecution事件期间发生。

@WithMockUser(setupBefore = TestExecutionEvent.TEST_EXECUTION)

@ WithAnonymousUser

使用@WithAnonymousUser允许以匿名用户身份运行。 当您希望使用特定用户运行大多数测试,但希望以匿名用户身份运行一些测试时,这样做尤其方便。 例如,以下内容将使用@WithMockUser和匿名用户作为匿名用户与MockUser1和withMockUser2一起运行。

@RunWith(SpringJUnit4ClassRunner.class)
@WithMockUser
public class WithUserClassLevelAuthenticationTests {

	@Test
	public void withMockUser1() {
	}

	@Test
	public void withMockUser2() {
	}

	@Test
	@WithAnonymousUser
	public void anonymous() throws Exception {
		// override default to run as anonymous user
	}
}

默认情况下,在TestExecutionListener.beforeTestMethod事件期间设置SecurityContext。 这与JUnit的@Before之前发生的情况相同。 您可以将此更改为在JUnit的@Before之后但在调用测试方法之前的TestExecutionListener.beforeTestExecution事件期间发生。

@WithAnonymousUser(setupBefore = TestExecutionEvent.TEST_EXECUTION)

@ WithUserDetails

虽然@WithMockUser是一种非常方便的入门方式,但它可能无法在所有情况下使用。 例如,应用程序期望Authentication主体具有特定类型是很常见的。 这样做是为了让应用程序可以将主体引用为自定义类型,并减少Spring Security上的耦合。

自定义主体通常由自定义UserDetailsService返回,该自定义返回实现UserDetails和自定义类型的对象。 对于这种情况,使用自定义UserDetailsService创建测试用户非常有用。 这正是@WithUserDetails所做的。

假设我们将UserDetailsService作为bean公开,则将使用UsernamePasswordAuthenticationToken类型的Authentication和使用用户名从UserDetailsService返回的主体调用以下测试的"user"。

@Test
@WithUserDetails
public void getMessageWithUserDetails() {
	String message = messageService.getMessage();
	...
}

我们还可以自定义用于从我们的UserDetailsService查找用户的用户名。 例如,此测试将使用用户名为"customUsername"的{​​{0}}返回的主体执行。

@Test
@WithUserDetails("customUsername")
public void getMessageWithUserDetailsCustomUsername() {
	String message = messageService.getMessage();
	...
}

我们还可以提供一个显式的bean名称来查找UserDetailsService。 例如,此测试将使用bean名称为"myUserDetailsService"的UserDetailsService查找"customUsername"的用户名。

@Test
@WithUserDetails(value="customUsername", userDetailsServiceBeanName="myUserDetailsService")
public void getMessageWithUserDetailsServiceBeanName() {
	String message = messageService.getMessage();
	...
}

@WithMockUser一样,我们也可以将我们的注释放在课程级别,以便每个测试都使用同一个用户。 但与@WithMockUser不同,@WithUserDetails要求用户存在。

默认情况下,在TestExecutionListener.beforeTestMethod事件期间设置SecurityContext。 这与JUnit的@Before之前发生的情况相同。 您可以将此更改为在JUnit的@Before之后但在调用测试方法之前的TestExecutionListener.beforeTestExecution事件期间发生。

@WithUserDetails(setupBefore = TestExecutionEvent.TEST_EXECUTION)

@ WithSecurityContext

我们已经看到,如果我们不使用自定义Authentication主体,@WithMockUser是一个很好的选择。 接下来我们发现@WithUserDetails允许我们使用自定义UserDetailsService创建我们的Authentication主体,但需要用户存在。 我们现在将看到一个允许最大灵活性的选项。

我们可以创建自己的注释,使用@WithSecurityContext创建任何我们想要的SecurityContext。 例如,我们可能会创建一个名为@WithMockCustomUser的注释,如下所示:

@Retention(RetentionPolicy.RUNTIME)
@WithSecurityContext(factory = WithMockCustomUserSecurityContextFactory.class)
public @interface WithMockCustomUser {

	String username() default "rob";

	String name() default "Rob Winch";
}

您可以看到@WithMockCustomUser使用@WithSecurityContext注释进行了注释。 这是Spring Security Test支持的信号,我们打算为该测试创建一个SecurityContext @WithSecurityContext注释要求我们指定一个SecurityContextFactory,它会根据我们的@WithMockCustomUser注释创建一个新的SecurityContext。 您可以在下面找到我们的WithMockCustomUserSecurityContextFactory实施:

public class WithMockCustomUserSecurityContextFactory
	implements WithSecurityContextFactory<WithMockCustomUser> {
	@Override
	public SecurityContext createSecurityContext(WithMockCustomUser customUser) {
		SecurityContext context = SecurityContextHolder.createEmptyContext();

		CustomUserDetails principal =
			new CustomUserDetails(customUser.name(), customUser.username());
		Authentication auth =
			new UsernamePasswordAuthenticationToken(principal, "password", principal.getAuthorities());
		context.setAuthentication(auth);
		return context;
	}
}

我们现在可以使用新注释标注测试类或测试方法,并且Spring Security的WithSecurityContextTestExecutionListener将确保我们的SecurityContext适当地填充。

创建自己的WithSecurityContextFactory实现时,很高兴知道它们可以使用标准的Spring注释进行注释。 例如,WithUserDetailsSecurityContextFactory使用@Autowired注释来获取UserDetailsService

final class WithUserDetailsSecurityContextFactory
	implements WithSecurityContextFactory<WithUserDetails> {

	private UserDetailsService userDetailsService;

	@Autowired
	public WithUserDetailsSecurityContextFactory(UserDetailsService userDetailsService) {
		this.userDetailsService = userDetailsService;
	}

	public SecurityContext createSecurityContext(WithUserDetails withUser) {
		String username = withUser.value();
		Assert.hasLength(username, "value() must be non-empty String");
		UserDetails principal = userDetailsService.loadUserByUsername(username);
		Authentication authentication = new UsernamePasswordAuthenticationToken(principal, principal.getPassword(), principal.getAuthorities());
		SecurityContext context = SecurityContextHolder.createEmptyContext();
		context.setAuthentication(authentication);
		return context;
	}
}

默认情况下,在TestExecutionListener.beforeTestMethod事件期间设置SecurityContext。 这与JUnit的@Before之前发生的情况相同。 您可以将此更改为在JUnit的@Before之后但在调用测试方法之前的TestExecutionListener.beforeTestExecution事件期间发生。

@WithSecurityContext(setupBefore = TestExecutionEvent.TEST_EXECUTION)

测试元注释

如果您经常在测试中重复使用同一用户,那么不得不重复指定属性。 例如,如果有许多与使用用户名"admin"和角色ROLE_USERROLE_ADMIN的管理用户相关的测试,您必须编写这些测试:

@WithMockUser(username="admin",roles={"USER","ADMIN"})

我们可以使用元注释,而不是随处重复。 例如,我们可以创建一个名为WithMockAdmin的元注释:

@Retention(RetentionPolicy.RUNTIME)
@WithMockUser(value="rob",roles="ADMIN")
public @interface WithMockAdmin { }

现在我们可以像@WithMockUser一样使用@WithMockAdmin

元注释可与上述任何测试注释一起使用。 例如,这意味着我们也可以为@WithUserDetails("admin")创建元注释。

Spring MVC测试集成

Spring Security提供与 Spring MVC测试的全面集成

设置MockMvc和Spring Security

为了在Spring MVC测试中使用Spring Security,有必要将Spring Security FilterChainProxy添加为Filter。 还需要添加Spring Security的TestSecurityContextHolderPostProcessor以支持Running as a User in Spring MVC Test with Annotations。 这可以使用Spring Security的SecurityMockMvcConfigurers.springSecurity()来完成。 例如:

注意:Spring Security的测试支持需要spring-test-4.1.3.RELEASE或更高版本。

import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.*;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
@WebAppConfiguration
public class CsrfShowcaseTests {

	@Autowired
	private WebApplicationContext context;

	private MockMvc mvc;

	@Before
	public void setup() {
		mvc = MockMvcBuilders
				.webAppContextSetup(context)
				.apply(springSecurity()) 
				.build();
	}

...
 SecurityMockMvcConfigurers.springSecurity()将执行我们需要将Spring Security与Spring MVC Test集成的所有初始设置

SecurityMockMvcRequestPostProcessors

Spring MVC Test提供了一个方便的接口,称为RequestPostProcessor,可用于修改请求。 Spring Security提供了许多使得测试更容易的RequestPostProcessor实现。 为了使用Spring Security的RequestPostProcessor实现,请确保使用以下静态导入:

import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.*;

使用CSRF保护进行测试

在测试任何非安全的HTTP方法并使用Spring Security的CSRF保护时,您必须确保在请求中包含有效的CSRF令牌。 使用以下命令将有效的CSRF令牌指定为请求参数:

mvc
	.perform(post("/").with(csrf()))

如果你喜欢,你可以在标题中包含CSRF令牌:

mvc
	.perform(post("/").with(csrf().asHeader()))

您也可以使用以下测试来提供无效的CSRF令牌:

mvc
	.perform(post("/").with(csrf().useInvalidToken()))

在Spring MVC测试中以用户身份运行测试

通常需要以特定用户身份运行测试。 有两种填充用户的简单方法:

以RequestPostProcessor的形式在Spring MVC测试中以用户身份运行

有多种选项可将用户关联到当前的HttpServletRequest。 例如,以下内容将以用户名"user",密码"password"和角色"ROLE_USER"作为用户(不需要存在)运行:

 

该支持通过将用户与HttpServletRequest关联起作用。 要将请求与SecurityContextHolder关联,您需要确保SecurityContextPersistenceFilterMockMvc实例关联。 有几种方法可以做到这一点:

  • 调用apply(springSecurity())

  • 将Spring Security的FilterChainProxy添加到MockMvc

  • 手动添加SecurityContextPersistenceFilterMockMvc实例可能在使用MockMvcBuilders.standaloneSetup时有意义

mvc
	.perform(get("/").with(user("user")))

您可以轻松进行自定义。 例如,以下内容将以用户名"admin",密码"pass"和角色"ROLE_USER"和"ROLE_ADMIN"作为用户(不需要存在)运行。

mvc
	.perform(get("/admin").with(user("admin").password("pass").roles("USER","ADMIN")))

如果您有要使用的自定义UserDetails,则也可以轻松指定。 例如,以下内容将使用指定的UserDetails(不需要存在)与具有指定UserDetails的主体的UsernamePasswordAuthenticationToken一起运行:

mvc
	.perform(get("/").with(user(userDetails)))

您可以使用以下方式以匿名用户身份运行:

mvc
	.perform(get("/").with(anonymous()))

如果您使用默认用户运行并希望以匿名用户的身份执行一些请求,则此功能特别有用。

如果您想要自定义Authentication(不需要存在),则可以使用以下方法执行此操作:

mvc
	.perform(get("/").with(authentication(authentication)))

您甚至可以使用以下方式自定义SecurityContext

mvc
	.perform(get("/").with(securityContext(securityContext)))

我们还可以确保使用MockMvcBuilders的默认请求以每个请求的特定用户身份运行。 例如,以下内容将以用户名"admin",密码"password"和角色"ROLE_ADMIN"作为用户(不需要存在)运行:

mvc = MockMvcBuilders
		.webAppContextSetup(context)
		.defaultRequest(get("/").with(user("user").roles("ADMIN")))
		.apply(springSecurity())
		.build();

如果你发现你在许多测试中使用同一个用户,建议将用户移到一个方法。 例如,您可以在名为CustomSecurityMockMvcRequestPostProcessors的类中指定以下内容:

public static RequestPostProcessor rob() {
	return user("rob").roles("ADMIN");
}

现在您可以在SecurityMockMvcRequestPostProcessors上执行静态导入,并在您的测试中使用它:

import static sample.CustomSecurityMockMvcRequestPostProcessors.*;

...

mvc
	.perform(get("/").with(rob()))

作为用户在Spring MVC测试中使用注解运行=====

作为使用RequestPostProcessor创建用户的替代方法,您可以使用[Testing Method Security]中描述的注释。 例如,以下内容将使用用户名"user",密码"password"和角色"ROLE_USER"进行测试:

@Test
@WithMockUser
public void requestProtectedUrlWithUser() throws Exception {
mvc
		.perform(get("/"))
		...
}

或者,以下内容将使用用户名"user",密码"password"和角色"ROLE_ADMIN"进行测试:

@Test
@WithMockUser(roles="ADMIN")
public void requestProtectedUrlWithUser() throws Exception {
mvc
		.perform(get("/"))
		...
}

测试HTTP基本身份验证

虽然始终可以使用HTTP Basic进行身份验证,但记住头名称,格式和编码值有点繁琐。 现在可以使用Spring Security的httpBasic RequestPostProcessor完成。 例如,下面的代码片段:

mvc
	.perform(get("/").with(httpBasic("user","password")))

将尝试使用HTTP Basic通过确保在HTTP请求上填充以下标头,以用户名"user"和密码"password"对用户进行身份验证:

Authorization: Basic dXNlcjpwYXNzd29yZA==

SecurityMockMvcRequestBuilders

Spring MVC Test还提供了一个RequestBuilder接口,可用于创建测试中使用的MockHttpServletRequest。 Spring Security提供了几个可用于简化测试的RequestBuilder实现。 为了使用Spring Security的RequestBuilder实现,请确保使用以下静态导入:

import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.*;

测试基于表单的身份验证

使用Spring Security的测试支持,您可以轻松创建一个请求来测试基于表单的身份验证。 例如,以下内容将使用用户名"user",密码"password"和有效的CSRF令牌向"/login"提交POST:

mvc
	.perform(formLogin())

定制请求很容易。 例如,以下内容将使用用户名"admin",密码"pass"和有效的CSRF令牌向"/auth"提交POST:

mvc
	.perform(formLogin("/auth").user("admin").password("pass"))

我们也可以自定义包含用户名和密码的参数名称。 例如,上述请求被修改为在HTTP参数"u"上包含用户名,在HTTP参数"p"中包含密码。

mvc
	.perform(formLogin("/auth").user("u","admin").password("p","pass"))

测试注销

虽然使用标准的Spring MVC测试相当简单,但您可以使用Spring Security的测试支持来简化测试注销。 例如,以下内容将使用有效的CSRF令牌向"/logout"提交POST:

mvc
	.perform(logout())

您还可以自定义发布到的URL。 例如,下面的代码段将使用有效的CSRF令牌向"/signout"提交POST:

mvc
	.perform(logout("/signout"))

SecurityMockMvcResultMatchers

有时需要对请求进行各种与安全相关的断言。 为了适应这种需求,Spring Security Test支持实现了Spring MVC Test的ResultMatcher接口。 为了使用Spring Security的ResultMatcher实现,请确保使用以下静态导入:

import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.*;

未经身份验证的声明

有时,声明没有经过认证的用户与MockMvc调用的结果关联可能很有价值。 例如,您可能想要测试提交无效的用户名和密码,并验证没有用户通过身份验证。 使用Spring Security的测试支持,您可以使用如下所示轻松完成此操作:

mvc
	.perform(formLogin().password("invalid"))
	.andExpect(unauthenticated());

已验证的声明

通常我们必须断言经过身份验证的用户存在。 例如,我们可能想验证我们是否成功验证。 我们可以通过以下代码片段验证基于表单的登录是否成功:

mvc
	.perform(formLogin())
	.andExpect(authenticated());

如果我们想要声明用户的角色,我们可以改进我们以前的代码,如下所示:

mvc
	.perform(formLogin().user("admin"))
	.andExpect(authenticated().withRoles("USER","ADMIN"));

或者,我们可以验证用户名:

mvc
	.perform(formLogin().user("admin"))
	.andExpect(authenticated().withUsername("admin"));

我们也可以结合这些断言:

mvc
	.perform(formLogin().user("admin").roles("USER","ADMIN"))
	.andExpect(authenticated().withUsername("admin"));

我们也可以对认证进行任意的断言

mvc
	.perform(formLogin())
	.andExpect(authenticated().withAuthentication(auth ->
		assertThat(auth).isInstanceOf(UsernamePasswordAuthenticationToken.class)));

WebFlux支持

反应式方法安全性

例如,我们可以使用与测试方法安全性中所做的相同的设置和注释,从EnableReactiveMethodSecurity测试我们的示例。 以下是我们可以做的一个最小样本:

@RunWith(SpringRunner.class)
@ContextConfiguration(classes = HelloWebfluxMethodApplication.class)
public class HelloWorldMessageServiceTests {
	@Autowired
	HelloWorldMessageService messages;

	@Test
	public void messagesWhenNotAuthenticatedThenDenied() {
		StepVerifier.create(this.messages.findMessage())
			.expectError(AccessDeniedException.class)
			.verify();
	}

	@Test
	@WithMockUser
	public void messagesWhenUserThenDenied() {
		StepVerifier.create(this.messages.findMessage())
			.expectError(AccessDeniedException.class)
			.verify();
	}

	@Test
	@WithMockUser(roles = "ADMIN")
	public void messagesWhenAdminThenOk() {
		StepVerifier.create(this.messages.findMessage())
			.expectNext("Hello World!")
			.verifyComplete();
	}
}

WebTestClientSupport

Spring Security提供了与WebTestClient的集成。 基本设置如下所示:

@RunWith(SpringRunner.class)
@ContextConfiguration(classes = HelloWebfluxMethodApplication.class)
public class HelloWebfluxMethodApplicationTests {
	@Autowired
	ApplicationContext context;

	WebTestClient rest;

	@Before
	public void setup() {
		this.rest = WebTestClient
			.bindToApplicationContext(this.context)
			// add Spring Security test Support
			.apply(springSecurity())
			.configureClient()
			.filter(basicAuthentication())
			.build();
	}
	// ...
}

认证

将Spring Security支持应用到WebTestClient后,我们可以使用注释或mutateWith支持。 例如:

@Test
public void messageWhenNotAuthenticated() throws Exception {
	this.rest
		.get()
		.uri("/message")
		.exchange()
		.expectStatus().isUnauthorized();
}

// --- WithMockUser ---

@Test
@WithMockUser
public void messageWhenWithMockUserThenForbidden() throws Exception {
	this.rest
		.get()
		.uri("/message")
		.exchange()
		.expectStatus().isEqualTo(HttpStatus.FORBIDDEN);
}

@Test
@WithMockUser(roles = "ADMIN")
public void messageWhenWithMockAdminThenOk() throws Exception {
	this.rest
		.get()
		.uri("/message")
		.exchange()
		.expectStatus().isOk()
		.expectBody(String.class).isEqualTo("Hello World!");
}

// --- mutateWith mockUser ---

@Test
public void messageWhenMutateWithMockUserThenForbidden() throws Exception {
	this.rest
		.mutateWith(mockUser())
		.get()
		.uri("/message")
		.exchange()
		.expectStatus().isEqualTo(HttpStatus.FORBIDDEN);
}

@Test
public void messageWhenMutateWithMockAdminThenOk() throws Exception {
	this.rest
		.mutateWith(mockUser().roles("ADMIN"))
		.get()
		.uri("/message")
		.exchange()
		.expectStatus().isOk()
		.expectBody(String.class).isEqualTo("Hello World!");
}

CSRF支持

Spring Security还为WebTestClient提供CSRF测试支持。 例如:

this.rest
	// provide a valid CSRF token
	.mutateWith(csrf())
	.post()
	.uri("/login")
	...

Web应用程序安全性

大多数Spring Security用户将在使用HTTP和Servlet API的应用程序中使用框架。 在这一部分中,我们将看看Spring Security如何为应用程序的Web层提供身份验证和访问控制功能。 我们将看看名称空间的外观,并查看实际组装哪些类和接口以提供Web层安全性。 在某些情况下,有必要使用传统的bean配置来完全控制配置,所以我们还将看到如何在没有命名空间的情况下直接配置这些类。

安全过滤器链

Spring Security的Web基础架构完全基于标准的servlet过滤器。 它不在内部使用servlet或任何其他基于servlet的框架(如Spring MVC),因此它没有与任何特定Web技术的强大链接。 它处理HttpServletRequestHttpServletResponse,并不关心请求是来自浏览器,Web服务客户端,HttpInvoker还是AJAX应用程序。

Spring Security在内部维护一个过滤器链,其中每个过滤器都有特定的责任,并且根据需要哪些服务来添加或从配置中删除过滤器。 过滤器的排序很重要,因为它们之间存在依赖关系。 如果您一直在使用namespace configuration,那么过滤器会自动为您配置,而且您不必明确定义任何Spring bean,但在这里可能有时想要完全控制安全过滤器链,无论是因为您正在使用名称空间不支持的功能,或者您正在使用您自己的定制版本的类。

的DelegatingFilterProxy

使用servlet过滤器时,显然需要在web.xml中声明它们,否则它们将被servlet容器忽略。 在Spring Security中,过滤器类也是在应用程序上下文中定义的Spring bean,因此可以利用Spring丰富的依赖注入工具和生命周期接口。 Spring的DelegatingFilterProxy提供了web.xml和应用程序上下文之间的链接。

使用DelegatingFilterProxy时,您会在web.xml文件中看到类似这样的内容:

<filter>
<filter-name>myFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>

<filter-mapping>
<filter-name>myFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

请注意,过滤器实际上是DelegatingFilterProxy,而不是实际实现过滤器逻辑的类。 DelegatingFilterProxyFilter的方法委托给从Spring应用程序上下文获得的bean。 这使得bean可以从Spring Web应用程序上下文生命周期支持和配置灵活性中受益。 该bean必须实现javax.servlet.Filter,并且它必须具有与filter-name元素中相同的名称。 阅读Javadoc的DelegatingFilterProxy了解更多信息

的FilterChainProxy

Spring Security的Web基础架构只能通过委派给FilterChainProxy的实例来使用。 安全过滤器不应该单独使用。 理论上,您可以在应用程序上下文文件中声明您需要的每个Spring Security过滤器bean,并为每个过滤器添加一个DelegatingFilterProxy条目到web.xml,确保它们的顺序正确,但这会是如果你有很多的过滤器,会很麻烦并且很快就会把web.xml文件搞乱。FilterChainProxy让我们为web.xml添加单个条目,并完全处理应用程序上下文文件以管理我们的Web安全性Bean。 它使用DelegatingFilterProxy进行连线,就像上面的示例一样,但将filter-name设置为bean名称"filterChainProxy"。 过滤器链然后在应用程序上下文中用相同的bean名称声明。 这是一个例子:

<bean id="filterChainProxy" class="org.springframework.security.web.FilterChainProxy">
<constructor-arg>
	<list>
	<sec:filter-chain pattern="/restful/**" filters="
		securityContextPersistenceFilterWithASCFalse,
		basicAuthenticationFilter,
		exceptionTranslationFilter,
		filterSecurityInterceptor" />
	<sec:filter-chain pattern="/**" filters="
		securityContextPersistenceFilterWithASCTrue,
		formLoginFilter,
		exceptionTranslationFilter,
		filterSecurityInterceptor" />
	</list>
</constructor-arg>
</bean>

名称空间元素filter-chain用于方便地设置应用程序中所需的安全筛选器链。 脚注:[请注意,您需要将安全性名称空间包含在应用程序上下文XML文件中才能使用此语法。 使用filter-chain-map的较早语法仍然受支持,但不赞成使用构造函数参数注入。]。 它将特定的URL模式映射到根据filters元素中指定的bean名称构建的过滤器列表,并将它们组合到SecurityFilterChain类型的bean中。 pattern属性采用Ant路径,最具体的URI应首先显示脚注:[可以使用request-matcher-ref属性指定一个RequestMatcher实例,使其更强大匹配]。 在运行时,FilterChainProxy将找到与当前Web请求匹配的第一个URI模式,并将filters属性指定的筛选器列表应用于该请求。 过滤器将按照它们定义的顺序调用,因此您可以完全控制应用于特定URL的过滤器链。

您可能已经注意到我们已经在过滤器链中声明了两个SecurityContextPersistenceFilterASCallowSessionCreation的缩写,属性为SecurityContextPersistenceFilter)。 由于Web服务永远不会在将来的请求中显示jsessionid,因此为这些用户代理创建HttpSession将会是浪费。 如果您有一个需要最大可伸缩性的高容量应用程序,我们建议您使用上述方法。 对于较小的应用程序,使用单个SecurityContextPersistenceFilter(默认allowSessionCreationtrue)可能就足够了。

请注意,FilterChainProxy不会在其配置的过滤器上调用标准过滤器生命周期方法。 我们建议您使用Spring的应用程序上下文生命周期接口作为替代方案,就像您对其他任何Spring bean一样。

当我们查看如何使用namespace configuration设置网络安全时,我们使用了名称为"springSecurityFilterChain"的DelegatingFilterProxy。 您现在应该能够看到这是由命名空间创建的FilterChainProxy的名称。

绕过滤链

您可以使用属性filters = "none"作为提供过滤器bean列表的替代方法。 这将完全忽略来自安全过滤器链的请求模式。 请注意,与此路径相匹配的任何内容都不会应用认证或授权服务,并且可以自由访问。 如果您想在请求期间使用SecurityContext内容的内容,则它必须通过安全筛选器链。 否则,SecurityContextHolder将不会被填充,并且内容将为空。

过滤器排序

过滤器在链中定义的顺序非常重要。 无论您实际使用哪些过滤器,顺序应如下所示:

  • ChannelProcessingFilter,因为它可能需要重定向到不同的协议

  • SecurityContextPersistenceFilter,因此可以在Web请求的开头SecurityContextHolder中设置SecurityContext,并且可以复制对SecurityContext所做的任何更改到Web请求结束时的HttpSession(准备好用于下一个Web请求)

  • ConcurrentSessionFilter,因为它使用SecurityContextHolder功能,需要更新SessionRegistry以反映来自主体的持续请求

  • 认证处理机制 - UsernamePasswordAuthenticationFilterCasAuthenticationFilterBasicAuthenticationFilter等 - 以便可以修改SecurityContextHolder以包含有效的Authentication请求令牌

  • SecurityContextHolderAwareRequestFilter,如果您使用它将感知Spring安全的HttpServletRequestWrapper安装到您的servlet容器中

  • JaasApiIntegrationFilter,如果JaasAuthenticationTokenSecurityContextHolder中,则会将FilterChain处理为JaasAuthenticationToken中的{{5}

  • RememberMeAuthenticationFilter,这样如果没有更早的认证处理机制更新SecurityContextHolder,并且请求提供了一个允许记住我服务的cookie,则会记住一个合适的记录{{3} }对象将放在那里

  • AnonymousAuthenticationFilter,这样如果没有更早的认证处理机制更新SecurityContextHolder,则会在其中放置匿名Authentication对象

  • ExceptionTranslationFilter,以捕获任何Spring安全性异常,以便可以返回HTTP错误响应或者启动适当的AuthenticationEntryPoint

  • FilterSecurityInterceptor,以保护网络URI并在访问被拒绝时引发异常

请求匹配和HttpFirewall

Spring Security有几个区域,您定义的模式会根据传入的请求进行测试,以决定如何处理请求。 这发生在FilterChainProxy决定哪个过滤器链应该通过请求并且FilterSecurityInterceptor决定哪个安全约束条件适用于请求时发生。 了解机制是什么以及在针对您定义的模式进行测试时使用的URL值非常重要。

Servlet规范定义了HttpServletRequest的几个属性,这些属性可以通过getter方法访问,我们可能需要匹配。 这些是contextPathservletPathpathInfoqueryString。 Spring Security只对保护应用程序中的路径感兴趣,所以contextPath被忽略。 不幸的是,servlet规范没有准确定义servletPathpathInfo的值将为特定请求URI包含的内容。 例如,URL中的每个路径段都可能包含参数,如 RFC 2396中所定义 脚注:[您可能在浏览器不支持cookies并且jsessionid参数在分号后追加了URL时看到了这一点。 但是,RFC允许在URL的任何路径段中存在这些参数]。 规范没有明确说明这些是否应该包含在servletPathpathInfo中,并且不同的servlet容器之间的行为会有所不同。 当应用程序部署在未从这些值中剥离路径参数的容器中时,攻击者可能会将它们添加到请求的URL中,以使模式匹配成功或意外失败。 脚注:[原始值将在请求离开FilterChainProxy后返回,因此仍然可用于应用程序。]。 传入URL中的其他变体也是可能的。 例如,它可能包含路径遍历序列(如/../)或多个正斜杠(//),这也可能导致模式匹配失败。 有些容器在执行servlet映射之前将这些归一化,但其他容器则不会。 为防止出现类似问题,FilterChainProxy使用HttpFirewall策略检查并包装请求。 未规范化的请求默认会自动被拒绝,路径参数和重复的斜杠会被删除以达到匹配的目的。 脚注:[因此,例如,原始请求路径/secure;hack=1/somefile.html;hack=2将作为/secure/somefile.html返回。]。 因此,必须使用FilterChainProxy来管理安全过滤器链。 请注意,容器对servletPathpathInfo值进行解码,因此您的应用程序不应该包含任何包含分号的有效路径,因为这些部分将被删除以达到匹配目的。

如上所述,默认策略是使用Ant风格路径进行匹配,这对于大多数用户来说可能是最佳选择。 该策略在类AntPathRequestMatcher中实现,该类使用Spring的AntPathMatcher对模式与连接的servletPathpathInfo执行不区分大小写的匹配,忽略{{4 }}。

如果由于某种原因,您需要更强大的匹配策略,则可以使用正则表达式。 战略实施是RegexRequestMatcher。 有关更多信息,请参阅此类的Javadoc。

实际上,我们建议您在服务层使用方法安全性,以控制对应用程序的访问,而不完全依赖于在Web应用程序级别定义的安全约束。 URL变化,很难考虑到应用程序可能支持的所有可能的URL以及请求可能被操纵的方式。 你应该试着限制自己使用一些简单易懂的简单蚂蚁路径。 始终尝试使用"deny-by-default"方法,并在其中定义最后一个捕获所有通配符(/ or )并拒绝访问。

在服务层定义的安全性更健壮,更难绕过,所以你应该总是利用Spring Security的方法安全选项。

通过拒绝HTTP响应头中的新行字符,HttpFirewall还可以阻止https://www.owasp.org/index.php/HTTP_Response_Splitting [HTTP响应拆分]。

默认情况下使用StrictHttpFirewall。 该实施拒绝看起来是恶意的请求。 如果它对您的需求太严格,那么您可以自定义哪些类型的请求被拒绝。 但是,您知道这会打开您的应用程序以应对攻击,这一点很重要。 例如,如果您希望利用Spring MVC的Matrix变量,则可以在XML中使用以下配置:

<b:bean id="httpFirewall"
      class="org.springframework.security.web.firewall.StrictHttpFirewall"
      p:allowSemicolon="true"/>

<http-firewall ref="httpFirewall"/>

Java配置通过公开StrictHttpFirewall bean可以实现同样的效果。

@Bean
public StrictHttpFirewall httpFirewall() {
    StrictHttpFirewall firewall = new StrictHttpFirewall();
    firewall.setAllowSemicolon(true);
    return firewall;
}

StrictHttpFirewall提供了有效的HTTP方法白名单,可以通过https://www.owasp.org/index.php/Cross_Site_Tracing [跨站点跟踪(XST)]和https://www.owasp .org / index.php / Test_HTTP_Methods_(OTG-CONFIG-006)[HTTP动词篡改]。 默认的有效方法是"DELETE","GET","HEAD","OPTIONS","PATCH","POST"和"PUT"。 如果您的应用程序需要修改有效的方法,您可以配置一个自定义的StrictHttpFirewall bean。 例如,以下内容仅允许使用HTTP "GET"和"POST"方法:

<b:bean id="httpFirewall"
      class="org.springframework.security.web.firewall.StrictHttpFirewall"
      p:allowedHttpMethods="GET,HEAD"/>

<http-firewall ref="httpFirewall"/>

Java配置通过公开StrictHttpFirewall bean可以实现同样的效果。

@Bean
public StrictHttpFirewall httpFirewall() {
    StrictHttpFirewall firewall = new StrictHttpFirewall();
    firewall.setAllowedHttpMethods(Arrays.asList("GET", "POST"));
    return firewall;
}
 

如果您使用的是new MockHttpServletRequest(),则它当前将创建一个HTTP方法为空字符串""。 这是一个无效的HTTP方法,将被Spring Security拒绝。 您可以通过用new MockHttpServletRequest("GET", "")替换它来解决此问题。 请参阅https://jira.spring.io/browse/SPR-16851[SPR_16851]以了解有关改善此问题的问题。

如果您必须允许任何HTTP方法(不推荐),则可以使用StrictHttpFirewall.setUnsafeAllowAnyHttpMethod(true)。 这将完全禁用HTTP方法的验证。

与其他基于过滤器的框架一起使用

如果您使用的其他框架也是基于过滤器的,那么您需要确保Spring Security过滤器是第一位的。 这使得SecurityContextHolder能够及时填充以供其他过滤器使用。 例子是使用SiteMesh来装饰你的网页或像Wicket这样的网络框架,它使用过滤器来处理它的请求。

高级命名空间配置

正如我们前面在命名空间章节中看到的那样,可以使用多个http元素为不同的URL模式定义不同的安全配置。 每个元素在内部FilterChainProxy和应该映射到它的URL模式内创建一个过滤器链。 元素将按照它们声明的顺序添加,因此必须首先声明最具体的模式。 下面是另一个例子,对于上述类似的情况,应用程序同时支持无状态的RESTful API以及用户使用表单登录的普通Web应用程序。

<!-- Stateless RESTful service using Basic authentication -->
<http pattern="/restful/**" create-session="stateless">
<intercept-url pattern='/**' access="hasRole('REMOTE')" />
<http-basic />
</http>

<!-- Empty filter chain for the login page -->
<http pattern="/login.htm*" security="none"/>

<!-- Additional filter chain for normal users, matching all other requests -->
<http>
<intercept-url pattern='/**' access="hasRole('USER')" />
<form-login login-page='/login.htm' default-target-url="/home.htm"/>
<logout />
</http>

核心安全筛选器

在使用Spring Security的Web应用程序中总是会使用一些关键过滤器,所以我们先看看这些及其支持类和接口。 我们不会涵盖每个功能,因此如果您想要获得完整的图片,请务必查看它们的Javadoc。

FilterSecurityInterceptor

在讨论access-control in general时,我们已经简要地看到了FilterSecurityInterceptor,并且我们已经将<intercept-url>元素的名称空间用于其内部进行配置。 现在我们将看到如何显式配置它以与FilterChainProxy一起使用,以及其伴随过滤器ExceptionTranslationFilter。 典型配置示例如下所示:

<bean id="filterSecurityInterceptor"
	class="org.springframework.security.web.access.intercept.FilterSecurityInterceptor">
<property name="authenticationManager" ref="authenticationManager"/>
<property name="accessDecisionManager" ref="accessDecisionManager"/>
<property name="securityMetadataSource">
	<security:filter-security-metadata-source>
	<security:intercept-url pattern="/secure/super/**" access="ROLE_WE_DONT_HAVE"/>
	<security:intercept-url pattern="/secure/**" access="ROLE_SUPERVISOR,ROLE_TELLER"/>
	</security:filter-security-metadata-source>
</property>
</bean>

FilterSecurityInterceptor负责处理HTTP资源的安全性。 它需要引用AuthenticationManagerAccessDecisionManager。 它还提供了适用于不同HTTP URL请求的配置属性。 请参阅技术介绍中的the original discussion on these

FilterSecurityInterceptor可以通过两种方式配置配置属性。 上面显示的第一个使用<filter-security-metadata-source>命名空间元素。 这与命名空间章节中的<http>元素相似,但<intercept-url>子元素仅使用patternaccess属性。 逗号用于分隔适用于每个HTTP URL的不同配置属性。 第二种选择是编写自己的SecurityMetadataSource,但这超出了本文的范围。 无论使用何种方法,SecurityMetadataSource都负责返回包含与单个安全HTTP URL关联的所有配置属性的List<ConfigAttribute>

应该注意的是FilterSecurityInterceptor.setSecurityMetadataSource()方法实际上期望FilterInvocationSecurityMetadataSource的一个实例。 这是一个标记接口,其子类SecurityMetadataSource。 它仅表示SecurityMetadataSource理解FilterInvocation。 为了简单起见,我们将继续将FilterInvocationSecurityMetadataSource称为SecurityMetadataSource,因为区分与大多数用户的关系不大。

通过命名空间语法创建的SecurityMetadataSource通过将请求URL与配置的pattern属性进行匹配来获取特定FilterInvocation的配置属性。 这与命名空间配置的行为方式相同。 默认情况下,将所有表达式视为Apache Ant路径,正则表达式也支持更复杂的情况。 request-matcher属性用于指定正在使用的模式的类型。 在相同的定义中混合表达式语法是不可能的。 作为一个例子,使用正则表达式而不是Ant路径的前面的配置将被写成如下:

<bean id="filterInvocationInterceptor"
	class="org.springframework.security.web.access.intercept.FilterSecurityInterceptor">
<property name="authenticationManager" ref="authenticationManager"/>
<property name="accessDecisionManager" ref="accessDecisionManager"/>
<property name="runAsManager" ref="runAsManager"/>
<property name="securityMetadataSource">
	<security:filter-security-metadata-source request-matcher="regex">
	<security:intercept-url pattern="\A/secure/super/.*\Z" access="ROLE_WE_DONT_HAVE"/>
	<security:intercept-url pattern="\A/secure/.*\" access="ROLE_SUPERVISOR,ROLE_TELLER"/>
	</security:filter-security-metadata-source>
</property>
</bean>

模式总是按照它们定义的顺序进行评估。 因此,重要的是更多的特定模式在列表中定义得比较不具体的模式更高。 这反映在我们上面的示例中,其中更具体的/secure/super/模式显示高于不太具体的/secure/模式。 如果它们相反,则/secure/模式将始终匹配,并且永远不会评估/secure/super/模式。

的ExceptionTranslationFilter

ExceptionTranslationFilter位于安全筛选器堆栈中FilterSecurityInterceptor的上方。 它本身并不执行任何实际的安全性实施,但处理安全拦截器引发的异常并提供合适的HTTP响应。

<bean id="exceptionTranslationFilter"
class="org.springframework.security.web.access.ExceptionTranslationFilter">
<property name="authenticationEntryPoint" ref="authenticationEntryPoint"/>
<property name="accessDeniedHandler" ref="accessDeniedHandler"/>
</bean>

<bean id="authenticationEntryPoint"
class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint">
<property name="loginFormUrl" value="/login.jsp"/>
</bean>

<bean id="accessDeniedHandler"
	class="org.springframework.security.web.access.AccessDeniedHandlerImpl">
<property name="errorPage" value="/accessDenied.htm"/>
</bean>

的AuthenticationEntryPoint

如果用户请求安全的HTTP资源,但它们未通过身份验证,则将调用AuthenticationEntryPoint。 一个合适的AuthenticationExceptionAccessDeniedException将被进一步向下调用堆栈的安全拦截器引发,触发入口点上的commence方法。 这可以向用户提供适当的响应,以便开始认证。 我们这里使用的是LoginUrlAuthenticationEntryPoint,它将请求重定向到不同的URL(通常是登录页面)。 所使用的实际实现将取决于您希望在应用程序中使用的身份验证机制。

AccessDeniedHandler

如果用户已经通过身份验证并且他们尝试访问受保护的资源,会发生什么情况?在正常使用情况下,不应该发生这种情况,因为应用程序工作流程应限制为用户有权访问的操作。 例如,到管理页面的HTML链接可能对没有管理员角色的用户隐藏。 尽管如此,您不能依赖隐藏链接的安全性,因为用户总是可能直接输入URL来尝试绕过限制。 或者他们可能会修改一个RESTful URL来更改一些参数值。 您的应用程序必须受到保护以免出现这些情况,否则它肯定会变得不安全 您通常会使用简单的Web层安全性将约束应用于基本URL,并在您的服务层接口上使用更具体的基于方法的安全性来真正确定允许的内容。

如果AccessDeniedException被抛出并且用户已经被认证,那么这意味着已经尝试了他们没有足够权限的操作。 在这种情况下,ExceptionTranslationFilter将调用第二个策略AccessDeniedHandler。 默认情况下,使用AccessDeniedHandlerImpl,仅向客户端发送403(禁止)响应。 或者,您可以显式配置一个实例(如上例),并设置一个错误页面URL,它将把请求转发给脚注:[ 我们使用forward,以便SecurityContextHolder仍然包含主体的细节,这对于向用户显示可能是有用的。 在Spring Security的旧版本中,我们依靠servlet容器来处理缺少这种有用上下文信息的403错误消息。 ]. 这可以是简单的"access denied"页面,如JSP,也可以是更复杂的处理程序,如MVC控制器。 当然,你可以自己实现接口并使用你自己的实现。

当您使用命名空间来配置应用程序时,也可以提供自定义的AccessDeniedHandler。 有关更多详情,请参阅the namespace appendix

SavedRequest和RequestCache接口

ExceptionTranslationFilter职责的另一个责任是在调用AuthenticationEntryPoint之前保存当前请求。 这允许在用户通过身份验证后恢复请求(请参阅之前的web authentication概述)。 一个典型的例子是用户使用表单登录,然后通过默认SavedRequestAwareAuthenticationSuccessHandler(请参阅below)重定向到原始URL。

RequestCache封装了存储和检索HttpServletRequest实例所需的功能。 默认情况下使用HttpSessionRequestCache,将请求存储在HttpSession中。 当用户被重定向到原始URL时,RequestCacheFilter实际上可以恢复缓存中保存的请求。

在正常情况下,您不需要修改任何此功能,但保存的请求处理是"best-effort"方法,并且可能存在默认配置无法处理的情况。 这些接口的使用使它可以从Spring Security 3.0开始完全插入。

SecurityContextPersistenceFilter

我们在Technical Overview一章中介绍了此重要过滤器的用途,因此您可能需要重新阅读该部分。 我们先来看看如何将它配置为与FilterChainProxy一起使用。 基本配置只需要bean本身

<bean id="securityContextPersistenceFilter"
class="org.springframework.security.web.context.SecurityContextPersistenceFilter"/>

正如我们以前所见,这个过滤器有两个主要任务。 它负责存储HTTP请求之间的SecurityContext内容,并在请求完成时清除SecurityContextHolder。 清除存储上下文的ThreadLocal是非常重要的,因为否则可能会将一个线程替换到servlet容器的线程池中,同时为特定用户提供安全上下文。 此线程可能会在稍后阶段使用,并使用错误的凭据执行操作。

SecurityContextRepository

从Spring Security 3.0开始,加载和存储安全上下文的工作现在被委派给一个单独的策略接口:

public interface SecurityContextRepository {

SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder);

void saveContext(SecurityContext context, HttpServletRequest request,
		HttpServletResponse response);
}

HttpRequestResponseHolder只是传入请求和响应对象的容器,允许实现用包装类替换它们。 返回的内容将被传递给过滤器链。

默认实现是HttpSessionSecurityContextRepository,它将安全性上下文存储为HttpSession属性脚注:[在Spring Security 2.0及更早版本中,此过滤器被称为HttpSessionContextIntegrationFilter并执行所有存储上下文是由过滤器本身执行的。 如果您熟悉此课程,则可以在HttpSessionSecurityContextRepository上找到大多数可用的配置选项。]。 此实现的最重要的配置参数是allowSessionCreation属性,该属性默认为true,因此如果类需要存储经过身份验证的用户的安全上下文,则允许该类创建会话除非进行认证并且安全上下文的内容已经改变,否则不会创建一个)。 如果您不想创建会话,则可以将此属性设置为false

<bean id="securityContextPersistenceFilter"
	class="org.springframework.security.web.context.SecurityContextPersistenceFilter">
<property name='securityContextRepository'>
	<bean class='org.springframework.security.web.context.HttpSessionSecurityContextRepository'>
	<property name='allowSessionCreation' value='false' />
	</bean>
</property>
</bean>

或者,您可以提供一个NullSecurityContextRepository实例,一个 空对象实现,它将阻止安全上下文被存储,即使在请求期间已经创建了一个会话。

UsernamePasswordAuthenticationFilter

现在我们已经看到Spring Security web配置中总是存在三种主要的过滤器。 这些也是由名称空间<http>元素自动创建的三个元素,不能用替代项替代。 现在唯一缺少的是一种实际的身份验证机制,它允许用户进行身份验证。 这个过滤器是最常用的认证过滤器,也是最经常定制脚注的过滤器:[由于历史原因,在Spring Security 3.0之前,此过滤器被称为AuthenticationProcessingFilter,入口点被称为AuthenticationProcessingFilterEntryPoint 。 由于框架现在支持许多不同形式的认证,因此它们在3.0中都被赋予了更具体的名称。]。 它还提供名称空间中<form-login>元素使用的实现。 配置它需要三个阶段。

  • 使用登录页面的网址配置LoginUrlAuthenticationEntryPoint,就像我们上面所做的一样,并将其设置在ExceptionTranslationFilter上。

  • 实现登录页面(使用JSP或MVC控制器)。

  • 在应用程序上下文中配置UsernamePasswordAuthenticationFilter的实例

  • 将过滤器bean添加到您的过滤器链代理中(确保您注意订单)。

登录表单只包含usernamepassword个输入字段,并发布到由过滤器监控的网址(默认情况下为/login)。 基本的过滤器配置如下所示:

<bean id="authenticationFilter" class=
"org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter">
<property name="authenticationManager" ref="authenticationManager"/>
</bean>

认证成功和失败的应用程序流程

过滤器会调用配置的AuthenticationManager来处理每个认证请求。 成功认证或认证失败后的目的地分别由AuthenticationSuccessHandlerAuthenticationFailureHandler策略接口控制。 该过滤器的属性允许您设置这些属性,以便您可以完全自定义行为脚注:[在3.0之前的版本中,此时的应用程序流已经演变为一个阶段,由此类和策略插件。 该决定是为了重构代码以使这两个策略完全负责。]。 提供了一些标准实现,如SimpleUrlAuthenticationSuccessHandlerSavedRequestAwareAuthenticationSuccessHandlerSimpleUrlAuthenticationFailureHandlerExceptionMappingAuthenticationFailureHandlerDelegatingAuthenticationFailureHandler。 查看这些类的Javadoc,并了解AbstractAuthenticationProcessingFilter以了解它们如何工作以及支持的功能。

如果认证成功,则生成的Authentication对象将被放置到SecurityContextHolder中。 然后会调用配置的AuthenticationSuccessHandler将用户重定向或转发到适当的目标。 默认情况下使用SavedRequestAwareAuthenticationSuccessHandler,这意味着用户在被要求登录之前将被重定向到他们要求的原始目的地。

 

ExceptionTranslationFilter缓存用户所做的原始请求。 当用户进行身份验证时,请求处理程序使用此缓存请求来获取原始URL并将其重定向到它。 原始请求然后被重建并用作替代。

如果认证失败,则会调用配置的AuthenticationFailureHandler

Servlet API集成

本节介绍Spring Security如何与Servlet API集成。 servletapi-xml示例应用程序演示了每种方法的用法。

Servlet 2.5+集成

HttpServletRequest.getRemoteUser()

HttpServletRequest.getRemoteUser()将返回通常为当前用户名的SecurityContextHolder.getContext().getAuthentication().getName()的结果。 如果您想在应用程序中显示当前用户名,这会很有用。 此外,检查这是否为空可用于指示用户是否已通过身份验证或匿名。 知道用户是否被认证对于确定是否应该显示某些UI元素是有用的(即,仅当用户被认证时才显示注销链接)。

HttpServletRequest.getUserPrincipal()

HttpServletRequest.getUserPrincipal()将返回SecurityContextHolder.getContext().getAuthentication()的结果。 这意味着在使用基于用户名和密码的身份验证时,它是Authentication,通常是UsernamePasswordAuthenticationToken的实例。 如果您需要有关用户的其他信息,这可能很有用。 例如,您可能已创建一个自定义UserDetailsService,返回包含用户姓名和姓氏的自定义UserDetails。 您可以通过以下方式获取此信息:

Authentication auth = httpServletRequest.getUserPrincipal();
// assume integrated custom UserDetails called MyCustomUserDetails
// by default, typically instance of UserDetails
MyCustomUserDetails userDetails = (MyCustomUserDetails) auth.getPrincipal();
String firstName = userDetails.getFirstName();
String lastName = userDetails.getLastName();
 

应该指出,在整个应用程序中执行如此多的逻辑通常是不好的做法。 相反,应该集中它来减少Spring Security和Servlet API的耦合。

HttpServletRequest.isUserInRole(字符串)

HttpServletRequest.isUserInRole(字符串)将确定SecurityContextHolder.getContext().getAuthentication().getAuthorities()是否包含传递给isUserInRole(String)的角色的GrantedAuthority。 通常用户不应将"ROLE_"前缀传入此方法,因为它会自动添加。 例如,如果您要确定当前用户是否具有权限"ROLE_ADMIN",则可以使用以下内容:

boolean isAdmin = httpServletRequest.isUserInRole("ADMIN");

这对确定是否显示某些UI组件可能很有用。 例如,只有当前用户是管理员时,才可以显示管理员链接。

Servlet 3+集成

以下部分描述Spring Security集成的Servlet 3方法。

HttpServletRequest.authenticate(HttpServletRequest的,HttpServletResponse的)

HttpServletRequest.authenticate(HttpServletRequest的,HttpServletResponse的)方法可用于确保用户通过身份验证。 如果它们未通过身份验证,则配置的AuthenticationEntryPoint将用于请求用户进行身份验证(即重定向到登录页面)。

HttpServletRequest.login(字符串,字符串)

HttpServletRequest.login(字符串,字符串)方法可用于使用当前AuthenticationManager对用户进行身份验证。 例如,以下内容会尝试使用用户名"user"和密码"password"进行身份验证:

try {
httpServletRequest.login("user","password");
} catch(ServletException e) {
// fail to authenticate
}
 

如果您希望Spring Security处理失败的身份验证尝试,则不需要捕获ServletException。

HttpServletRequest.logout()

可以使用 HttpServletRequest.logout()方法记录当前用户。

通常这意味着SecurityContextHolder将被清除,HttpSession将失效,任何"Remember Me"认证将被清除,等等。 但是,配置的LogoutHandler实现将根据您的Spring Security配置而有所不同。 请注意,在调用HttpServletRequest.logout()之后,您仍然负责编写响应。 通常这会涉及重定向到欢迎页面。

AsyncContext.start(可运行)

确保您的凭据将传播到新线程的 AsynchContext.start(可运行)方法。 使用Spring Security的并发支持,Spring Security会覆盖AsyncContext.start(Runnable),以确保在处理Runnable时使用当前的SecurityContext。 例如,以下内容会输出当前用户的身份验证:

final AsyncContext async = httpServletRequest.startAsync();
async.start(new Runnable() {
	public void run() {
		Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
		try {
			final HttpServletResponse asyncResponse = (HttpServletResponse) async.getResponse();
			asyncResponse.setStatus(HttpServletResponse.SC_OK);
			asyncResponse.getWriter().write(String.valueOf(authentication));
			async.complete();
		} catch(Exception e) {
			throw new RuntimeException(e);
		}
	}
});

异步Servlet支持

如果您正在使用基于Java的配置,则可以开始使用了。 如果您使用的是XML配置,则需要进行一些更新。 第一步是确保您已更新web.xml以至少使用3.0架构,如下所示:

<web-app xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0">

</web-app>

接下来,您需要确保您的springSecurityFilterChain已设置为处理异步请求。

<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>
	org.springframework.web.filter.DelegatingFilterProxy
</filter-class>
<async-supported>true</async-supported>
</filter>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
<dispatcher>REQUEST</dispatcher>
<dispatcher>ASYNC</dispatcher>
</filter-mapping>

而已! 现在,Spring Security将确保您的SecurityContext也在异步请求上传播。

那么它是怎样工作的?如果你不是真的感兴趣,可以跳过本节的其余部分,否则请继续阅读。 其中大部分都是内置到Servlet规范中的,但是有一点调整,Spring Security确实能够正确地处理异步请求。 在Spring Security 3.2之前,只要提交HttpServletResponse,SecurityContextHolder中的SecurityContext就会自动保存。 这可能会导致异步环境中的问题。 例如,请考虑以下几点:

httpServletRequest.startAsync();
new Thread("AsyncThread") {
	@Override
	public void run() {
		try {
			// Do work
			TimeUnit.SECONDS.sleep(1);

			// Write to and commit the httpServletResponse
			httpServletResponse.getOutputStream().flush();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}.start();

问题是这个Thread对于Spring Security来说是不知道的,所以SecurityContext不会传播给它。 这意味着当我们提交HttpServletResponse时,没有SecuriytContext。 当Spring Security在提交HttpServletResponse时自动保存SecurityContext时,它会丢失我们的登录用户。

从3.2版本开始,Spring Security足够聪明,只要HttpServletRequest.startAsync()被调用,就不会再自动保存SecurityContext来提交HttpServletResponse。

Servlet 3.1+集成

以下部分描述Spring Security集成的Servlet 3.1方法。

的HttpServletRequest#changeSessionId()

HttpServletRequest.changeSessionId()是在Servlet 3.1及更高版本中防止Session Fixation攻击的默认方法。

基本和摘要式身份验证

基本和摘要式身份验证是在Web应用程序中流行的备用身份验证机制。 基本身份验证通常用于在每个请求上传递其凭据的无状态客户端。 将它与基于表单的身份验证结合使用是非常常见的,其中通过基于浏览器的用户界面和作为Web服务来使用应用程序。 但是,基本身份验证将密码作为纯文本传输,因此它应该只能用于加密的传输层(如HTTPS)。

BasicAuthenticationFilter一样

BasicAuthenticationFilter负责处理HTTP标头中显示的基本认证凭证。 这可以用于验证Spring Remoting协议(例如Hessian和Burlap)以及普通浏览器用户代理(如Firefox和Internet Explorer)所做的调用。 控制HTTP基本认证的标准由RFC 1945第11节定义,BasicAuthenticationFilter符合本RFC。 基本身份验证是一种非常有吸引力的身份验证方法,因为它在用户代理中非常广泛地部署,并且实现非常简单(它只是用户名:密码的Base64编码,在HTTP标头中指定)。

配置

要实施HTTP基本认证,您需要将BasicAuthenticationFilter添加到您的过滤器链中。 应用程序上下文应包含BasicAuthenticationFilter及其所需的协作者:

<bean id="basicAuthenticationFilter"
class="org.springframework.security.web.authentication.www.BasicAuthenticationFilter">
<property name="authenticationManager" ref="authenticationManager"/>
<property name="authenticationEntryPoint" ref="authenticationEntryPoint"/>
</bean>

<bean id="authenticationEntryPoint"
class="org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint">
<property name="realmName" value="Name Of Your Realm"/>
</bean>

配置的AuthenticationManager处理每个认证请求。 如果认证失败,则配置的AuthenticationEntryPoint将用于重试认证过程。 通常,您将结合BasicAuthenticationEntryPoint使用过滤器,该过滤器会返回带有合适标头的401响应,以重试HTTP基本身份验证。 如果认证成功,则像往常一样将生成的Authentication对象放入SecurityContextHolder

如果认证事件成功,或者由于HTTP标头不包含支持的认证请求而未尝试认证,则过滤器链将照常继续。 过滤器链将被中断的唯一时间是身份验证失败并调用AuthenticationEntryPoint

DigestAuthenticationFilter

DigestAuthenticationFilter能够处理HTTP头中提供的摘要式身份验证凭证。 摘要式身份验证尝试解决许多基本身份验证的弱点,特别是通过确保证书永远不会以明文形式在整个线路上发送。 许多用户代理支持摘要式身份验证,包括Mozilla Firefox和Internet Explorer。 控制HTTP摘要认证的标准由RFC 2617定义,该RFC 2617更新RFC 2069规定的早期版本的摘要认证标准。 大多数用户代理实现RFC 2617。 Spring Security的DigestAuthenticationFilter与RFC 2617规定的“auth”保护质量(qop)兼容,后者也提供与RFC 2069的向后兼容性。 如果您需要使用未加密的HTTP(即无TLS / HTTPS)并希望最大限度地提高身份验证过程的安全性,则摘要式身份验证是一种更具吸引力的选项。 事实上,如RFC 2518第17.1节所述,摘要式身份验证是WebDAV协议的强制性要求。

 

您不应该在现代应用程序中使用摘要,因为它不被认为是安全的。 最明显的问题是,您必须以明文,加密或MD5格式存储您的密码。 所有这些存储格式都被认为是不安全的。 相反,您应该使用单向自适应密码哈希(即bCrypt,PBKDF2,SCrypt等)。

摘要式身份验证的核心是"nonce"。 这是服务器生成的值。 Spring Security的随机数采用以下格式:

base64(expirationTime + ":" + md5Hex(expirationTime + ":" + key))
expirationTime:   The date and time when the nonce expires, expressed in milliseconds
key:              A private key to prevent modification of the nonce token

DigestAuthenticatonEntryPoint具有一个属性,用于指定用于生成随机标记的key以及用于确定到期时间的默认值nonceValiditySeconds(默认值300,等于5分钟)。 Whist的nonce有效,摘要是通过连接各种字符串来计算的,包括用户名,密码,nonce,被请求的URI,客户端生成的nonce(仅仅是用户代理生成每个请求的随机值),域名等,然后执行MD5哈希。 服务器和用户代理都执行此摘要计算,如果他们不同意所包含的值(例如密码),则会生成不同的哈希码。 在Spring Security实现中,如果服务器生成的nonce仅仅过期(但摘要本来是有效的),DigestAuthenticationEntryPoint将发送一个"stale=true"头。 这告诉用户代理不需要打扰用户(因为密码和用户名等是正确的),而只是使用新的随机数再次尝试。

DigestAuthenticationEntryPoint的{​​{0}}参数的适当值取决于您的应用程序。 非常安全的应用程序应该注意,可以使用截取的身份验证标头模拟主体,直到达到现时包含的expirationTime。 这是选择适当设置时的关键原则,但对于非常安全的应用程序而言,首先不会在TLS / HTTPS上运行,这种情况并不常见。

由于Digest身份验证的实现比较复杂,因此通常会出现用户代理问题。 例如,Internet Explorer无法在同一个会话的后续请求中显示“opaque”令牌。 Spring Security过滤器因此将所有状态信息封装到“nonce”令牌中。 在我们的测试中,Spring Security的实现可以在Mozilla Firefox和Internet Explorer中可靠地工作,正确处理nonce超时等。

配置

现在我们已经回顾了理论,让我们看看如何使用它。 要实现HTTP摘要认证,有必要在过滤器链中定义DigestAuthenticationFilter。 应用程序上下文将需要定义DigestAuthenticationFilter及其所需的协作者:

<bean id="digestFilter" class=
	"org.springframework.security.web.authentication.www.DigestAuthenticationFilter">
<property name="userDetailsService" ref="jdbcDaoImpl"/>
<property name="authenticationEntryPoint" ref="digestEntryPoint"/>
<property name="userCache" ref="userCache"/>
</bean>

<bean id="digestEntryPoint" class=
	"org.springframework.security.web.authentication.www.DigestAuthenticationEntryPoint">
<property name="realmName" value="Contacts Realm via Digest Authentication"/>
<property name="key" value="acegi"/>
<property name="nonceValiditySeconds" value="10"/>
</bean>

配置的UserDetailsService是必需的,因为DigestAuthenticationFilter必须能够直接访问用户的明文密码。 如果在DAO脚注中使用编码密码,摘要式身份验证将不起作用:[如果DigestAuthenticationFilter.passwordAlreadyEncoded设置为{{},则可以使用HEX格式(MD5(用户名:realm:password) 1}}。 但是,其他密码编码不适用于摘要式身份验证。]。 DAO协作者和UserCache通常直接与DaoAuthenticationProvider共享。 authenticationEntryPoint属性必须为DigestAuthenticationEntryPoint,以便DigestAuthenticationFilter可以获取正确的realmNamekey以进行摘要计算。

BasicAuthenticationFilter类似,如果认证成功,Authentication请求令牌将被放入SecurityContextHolder中。 如果身份验证事件成功,或者由于HTTP头未包含摘要身份验证请求而未尝试身份验证,则过滤器链将照常继续。 过滤器链将被中断的唯一时间是如果验证失败并且调用AuthenticationEntryPoint,如前段所述。

摘要式身份验证RFC提供了一系列附加功能以进一步提高安全性。 例如,可以在每个请求中更改随机数。 尽管如此,Spring Security实现的目的是尽量减少实现的复杂性(以及可能出现的无疑的用户代理不兼容),并避免存储服务器端状态。 如果您希望更详细地了解这些功能,请您参阅RFC 2617。 据我们所知,Spring Security的实现符合RFC的最低标准。

记住我的身份验证

概述

记住我或永久登录身份验证是指网站能够记住会话之间的主体身份。 这通常通过向浏览器发送cookie来完成,在将来的会话中检测到cookie并导致自动登录。 Spring Security为这些操作提供了必要的钩子,并且有两个具体的记住我的实现。 一个使用散列来保存基于cookie的令牌的安全性,另一个使用数据库或其他持久存储机制来存储生成的令牌。

请注意,这两种实现都需要UserDetailsService。 如果您使用不使用UserDetailsService的身份验证提供程序(例如,LDAP提供程序),那么它将不起作用,除非您的应用程序上下文中还有一个UserDetailsService bean。

简单的基于哈希的令牌方法

这种方法使用散列来实现有用的记忆我策略。 本质上,cookie在成功交互式验证后发送到浏览器,cookie的组成如下:

base64(username + ":" + expirationTime + ":" +
md5Hex(username + ":" + expirationTime + ":" password + ":" + key))

username:          As identifiable to the UserDetailsService
password:          That matches the one in the retrieved UserDetails
expirationTime:    The date and time when the remember-me token expires, expressed in milliseconds
key:               A private key to prevent modification of the remember-me token

因此,记住我记号仅在指定的时间段内有效,并且前提是用户名,密码和密钥不会更改。 值得注意的是,这存在潜在的安全问题,因为捕获的记忆我令牌将可用于任何用户代理直到令牌过期。 这与摘要式身份验证相同。 如果委托人知道令牌已被捕获,他们可以轻松更改其密码,并立即使有问题的所有令牌令牌失效。 如果需要更重要的安全性,则应使用下一节中介绍的方法。 或者,记住我服务应该根本不用。

如果您熟悉namespace configuration一章中讨论的主题,则只需添加<remember-me>元素即可启用记事本身份验证:

<http>
...
<remember-me key="myAppKey"/>
</http>

通常会自动选择UserDetailsService。 如果您的应用程序环境中有多个应用程序,则需要指定哪一个应与user-service-ref属性一起使用,其中值是您的UserDetailsService bean的名称。

持久令牌方法

此方法基于文章 http://jaspan.com/improved_persistent_login_cookie_best_practice进行了一些小修改脚注:[基本上,用户名不包含在cookie中,以防止不必要地公开有效的登录名。 本文的评论部分对此进行了讨论。]。 要在命名空间配置中使用这种方法,您需要提供一个数据源参考:

<http>
...
<remember-me data-source-ref="someDataSource"/>
</http>

数据库应该包含使用以下SQL(或等价物)创建的persistent_logins表:

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

记住我的接口和实现

Remember-me与UsernamePasswordAuthenticationFilter一起使用,并通过AbstractAuthenticationProcessingFilter超类中的钩子实现。 它也用于BasicAuthenticationFilter。 挂钩将在适当的时候调用具体的RememberMeServices。 界面如下所示:

Authentication autoLogin(HttpServletRequest request, HttpServletResponse response);

void loginFail(HttpServletRequest request, HttpServletResponse response);

void loginSuccess(HttpServletRequest request, HttpServletResponse response,
	Authentication successfulAuthentication);

请参阅Javadoc,详细讨论这些方法的用途,但请注意,在此阶段AbstractAuthenticationProcessingFilter仅调用loginFail()loginSuccess()方法。 每当SecurityContextHolder不包含Authentication时,autoLogin()方法由RememberMeAuthenticationFilter调用。 因此,该接口为潜在的记忆我实现提供了充分的与身份验证相关的事件通知,并在候选Web请求可能包含Cookie并希望被记住时委托实现。 这种设计允许任何数量的记忆我实施策略。 我们在上面看到Spring Security提供了两个实现。 我们将依次查看这些内容。

TokenBasedRememberMeServices

此实现支持简单的基于哈希的令牌方法中介绍的更简单的方法。 TokenBasedRememberMeServices生成RememberMeAuthenticationToken,由RememberMeAuthenticationProvider处理。 此身份验证提供程序与TokenBasedRememberMeServices共享一个key。 另外,TokenBasedRememberMeServices需要一个UserDetailsS​​ervice,通过它可以检索用户名和密码以进行签名比较,并生成RememberMeAuthenticationToken以包含正确的GrantedAuthority。 某些注销命令应由应用程序提供,如果用户请求该命令,则会使Cookie无效。TokenBasedRememberMeServices还实现了Spring Security的LogoutHandler界面,因此可以与LogoutFilter一起使用以自动清除cookie。

在应用程序上下文中启用Remember-me服务所需的bean如下所示:

<bean id="rememberMeFilter" class=
"org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter">
<property name="rememberMeServices" ref="rememberMeServices"/>
<property name="authenticationManager" ref="theAuthenticationManager" />
</bean>

<bean id="rememberMeServices" class=
"org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices">
<property name="userDetailsService" ref="myUserDetailsService"/>
<property name="key" value="springRocks"/>
</bean>

<bean id="rememberMeAuthenticationProvider" class=
"org.springframework.security.authentication.RememberMeAuthenticationProvider">
<property name="key" value="springRocks"/>
</bean>

不要忘记将您的RememberMeServices实施添加到您的UsernamePasswordAuthenticationFilter.setRememberMeServices()属性,将RememberMeAuthenticationProvider添加到您的AuthenticationManager.setProviders()列表中,并将RememberMeAuthenticationFilter添加到您的{{5 }}(通常在你的UsernamePasswordAuthenticationFilter之后)。

对PersistentTokenBasedRememberMeServices

此类可以与TokenBasedRememberMeServices相同的方式使用,但还需要使用PersistentTokenRepository配置以存储令牌。 有两种标准实现。

仅用于测试的*  InMemoryTokenRepositoryImpl。 *  JdbcTokenRepositoryImpl将令牌存储在数据库中。

数据库模式在持久令牌方法中进行了描述。

跨网站请求伪造(CSRF)

本节讨论Spring Security的  跨站点请求伪造(CSRF)支持。

CSRF攻击

在我们讨论Spring Security如何保护应用程序免受CSRF攻击之前,我们将解释什么是CSRF攻击。 我们来看一个具体的例子来更好地理解。

假设您银行的网站提供了一个表格,允许从当前登录用户转账到另一个银行账户。 例如,HTTP请求可能如下所示:

POST /transfer HTTP/1.1
Host: bank.example.com
Cookie: JSESSIONID=randomid; Domain=bank.example.com; Secure; HttpOnly
Content-Type: application/x-www-form-urlencoded

amount=100.00&routingNumber=1234&account=9876

现在假装你认证你的银行网站,然后在没有注销的情况下访问一个恶意网站。 邪恶的网站包含一个HTML页面,其格式如下:

<form action="https://bank.example.com/transfer" method="post">
<input type="hidden"
	name="amount"
	value="100.00"/>
<input type="hidden"
	name="routingNumber"
	value="evilsRoutingNumber"/>
<input type="hidden"
	name="account"
	value="evilsAccountNumber"/>
<input type="submit"
	value="Win Money!"/>
</form>

你喜欢赢钱,所以你点击提交按钮。 在这个过程中,你无意间将100美元转让给恶意用户。 发生这种情况的原因是,虽然恶意网站无法看到您的Cookie,但与您的银行相关的Cookie仍会与请求一起发送。

最糟糕的是,整个过程可能已经使用JavaScript进行自动化。 这意味着你甚至不需要点击按钮。 那么我们如何保护自己免受这种攻击呢?

同步器令牌模式

问题是来自银行网站的HTTP请求和来自恶意网站的请求完全相同。 这意味着无法拒绝来自恶意网站的请求,并允许来自银行网站的请求。 为了防止CSRF攻击,我们需要确保恶意网站无法提供请求中的内容。

一种解决方案是使用https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)_Prevention_Cheat_Sheet#General_Recommendation:_Synchronizer_Token_Pattern[Synchronizer Token Pattern]。 此解决方案是为了确保除了我们的会话cookie之外,每个请求还需要一个随机生成的令牌作为HTTP参数。 提交请求时,服务器必须查找参数的期望值,并将其与请求中的实际值进行比较。 如果这些值不匹配,则请求将失败。

我们可以放宽期望,只需要更新状态的每个HTTP请求的令牌。 这可以安全地完成,因为相同的来源策略确保恶意网站无法读取响应。 此外,我们不希望在HTTP GET中包含随机标记,因为这会导致令牌泄漏。

我们来看看我们的例子将如何改变。 假设随机生成的令牌存在于名为_csrf的HTTP参数中。 例如,转账请求看起来像这样:

POST /transfer HTTP/1.1
Host: bank.example.com
Cookie: JSESSIONID=randomid; Domain=bank.example.com; Secure; HttpOnly
Content-Type: application/x-www-form-urlencoded

amount=100.00&routingNumber=1234&account=9876&_csrf=<secure-random>

你会注意到我们添加了一个随机值的_csrf参数。 现在,恶意网站将无法猜测_csrf参数(必须在恶意网站上明确提供)的正确值,并且当服务器将实际令牌与预期令牌进行比较时,传输将失败。

何时使用CSRF保护

什么时候应该使用CSRF保护?我们的建议是针对普通用户可以通过浏览器处理的任何请求使用CSRF保护。 如果您只创建非浏览器客户端使用的服务,则可能需要禁用CSRF保护。

CSRF保护和JSON

一个常见的问题是"do I need to protect JSON requests made by javascript?"简短的答案是,这取决于。 但是,您必须非常小心,因为存在会影响JSON请求的CSRF漏洞利用。 例如,恶意用户可以创建 使用以下格式的JSON CSRF

<form action="https://bank.example.com/transfer" method="post" enctype="text/plain">
<input name='{"amount":100,"routingNumber":"evilsRoutingNumber","account":"evilsAccountNumber", "ignore_me":"' value='test"}' type='hidden'>
<input type="submit"
	value="Win Money!"/>
</form>

这将产生以下JSON结构

{ "amount": 100,
"routingNumber": "evilsRoutingNumber",
"account": "evilsAccountNumber",
"ignore_me": "=test"
}

如果应用程序未验证Content-Type,那么它将暴露于此漏洞利用。 根据设置,验证Content-Type的Spring MVC应用程序仍然可以通过更新URL后缀来利用".json"来结束,如下所示:

<form action="https://bank.example.com/transfer.json" method="post" enctype="text/plain">
<input name='{"amount":100,"routingNumber":"evilsRoutingNumber","account":"evilsAccountNumber", "ignore_me":"' value='test"}' type='hidden'>
<input type="submit"
	value="Win Money!"/>
</form>

CSRF和无状态浏览器应用程序

如果我的应用程序是无状态的呢?这并不一定意味着你受到保护。 事实上,如果用户不需要在Web浏览器中针对特定请求执行任何操作,那么他们可能仍然容易受到CSRF攻击。

例如,考虑一个应用程序使用包含所有状态的自定义cookie来进行身份验证,而不是JSESSIONID。 当CSRF攻击发生时,自定义cookie将与请求一起发送,其方式与前面示例中发送的JSESSIONID cookie相同。

使用基本身份验证的用户也容易受到CSRF攻击,因为浏览器将自动在任何请求中包含用户名密码,方式与JSESSIONID cookie在前一示例中发送的方式相同。

使用Spring Security CSRF保护

那么使用Spring Security来保护我们的站点免受CSRF攻击需要采取哪些步骤?下面概述了使用Spring Security的CSRF保护的步骤:

使用适当的HTTP动词

防止CSRF攻击的第一步是确保您的网站使用正确的HTTP动词。 具体来说,在Spring Security的CSRF支持可以使用之前,您需要确定您的应用程序正在使用PATCH,POST,PUT和/或DELETE来修改状态。

这不是Spring Security支持的限制,而是正确的CSRF预防的一般要求。 原因是在HTTP GET中包含私人信息会导致信息泄露。 有关使用POST而不是GET获取敏感信息的一般指导,请参阅 RFC 2616第15.1.3节对URI中的敏感信息进行编码

配置CSRF保护

下一步是在您的应用程序中包含Spring Security的CSRF保护。 有些框架通过对用户的会话进行无效处理来处理无效的CSRF令牌,但这会导致its own problems。 相反,默认情况下,Spring Security的CSRF保护将导致HTTP 403访问被拒绝。 这可以通过配置AccessDeniedHandler以不同方式处理InvalidCsrfTokenException来定制。

从Spring Security 4.0开始,默认情况下使用XML配置启用CSRF保护。 如果您想禁用CSRF保护,则可以在下面看到相应的XML配置。

<http>
	<!-- ... -->
	<csrf disabled="true"/>
</http>

Java Configuration默认启用CSRF保护。 如果您想禁用CSRF,则可以在下面看到相应的Java配置。 有关如何配置CSRF保护的其他自定义,请参阅csrf()的Javadoc。

@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {

	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http
			.csrf().disable();
	}
}

包含CSRF令牌

表单提交

最后一步是确保在所有PATCH,POST,PUT和DELETE方法中包含CSRF标记。 解决此问题的一种方法是使用_csrf请求属性来获取当前的CsrfToken。 下面显示了使用JSP进行此操作的示例:

<c:url var="logoutUrl" value="/logout"/>
<form action="${logoutUrl}"
	method="post">
<input type="submit"
	value="Log out" />
<input type="hidden"
	name="${_csrf.parameterName}"
	value="${_csrf.token}"/>
</form>

更简单的方法是使用Spring Security JSP标记库中的the csrfInput tag

 

如果您使用Spring MVC <form:form>标记或 Thymeleaf 2.1+并且正在使用@EnableWebSecurity,则CsrfToken会自动包含在您的使用CsrfRequestDataValueProcessor中。

Ajax和JSON请求

如果您使用的是JSON,则无法在HTTP参数中提交CSRF令牌。 相反,您可以在HTTP头中提交令牌。 一个典型的模式是将CSRF令牌包含在元标记中。 下面显示了一个JSP示例:

<html>
<head>
	<meta name="_csrf" content="${_csrf.token}"/>
	<!-- default header name is X-CSRF-TOKEN -->
	<meta name="_csrf_header" content="${_csrf.headerName}"/>
	<!-- ... -->
</head>
<!-- ... -->

您可以使用Spring Security JSP标记库中的更简单的csrfMetaTags tag,而不是手动创建元标记。

然后,您可以将令牌包含在所有Ajax请求中。 如果您使用jQuery,可以使用以下方法完成此操作:

$(function () {
var token = $("meta[name='_csrf']").attr("content");
var header = $("meta[name='_csrf_header']").attr("content");
$(document).ajaxSend(function(e, xhr, options) {
	xhr.setRequestHeader(header, token);
});
});

作为jQuery的替代方案,我们建议使用 cujoJS的 rest.js. rest.js模块为以RESTful方式处理HTTP请求和响应提供了高级支持。 核心功能是通过将拦截器链接到客户端来根据需要上下文化HTTP客户端添加行为的能力。

var client = rest.chain(csrf, {
token: $("meta[name='_csrf']").attr("content"),
name: $("meta[name='_csrf_header']").attr("content")
});

配置的客户端可以与需要向CSRF保护的资源发出请求的应用程序的任何组件共享。 rest.js和jQuery之间的一个重要区别是,只有使用配置的客户端发出的请求才会包含CSRF令牌,而对于jQuery,其中all请求将包含该令牌。 限定哪些请求接收令牌的能力有助于防止将CSRF令牌泄露给第三方。 有关rest.js的更多信息,请参阅https://github.com/cujojs/rest/tree/master/docs[rest.js参考文档]。

CookieCsrfTokenRepository

可能会有用户想要将CsrfToken保存在Cookie中的情况。 默认情况下,CookieCsrfTokenRepository将写入名为XSRF-TOKEN的Cookie,并从名为X-XSRF-TOKEN的标头或HTTP参数_csrf中读取。 这些默认值来自https://docs.angularjs.org/api/ng/service/$http#cross-site-request-forgery-xsrf-protection[AngularJS]

您可以使用以下方式在XML中配置CookieCsrfTokenRepository

<http>
	<!-- ... -->
	<csrf token-repository-ref="tokenRepository"/>
</http>
<b:bean id="tokenRepository"
	class="org.springframework.security.web.csrf.CookieCsrfTokenRepository"
	p:cookieHttpOnly="false"/>
 

示例显式设置cookieHttpOnly=false。 这是允许JavaScript(即AngularJS)读取它的必要条件。 如果您不需要直接使用JavaScript读取cookie,则建议省略cookieHttpOnly=false以提高安全性。

您可以使用以下方法在Java配置中配置CookieCsrfTokenRepository

@EnableWebSecurity
public class WebSecurityConfig extends
		WebSecurityConfigurerAdapter {

	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http
			.csrf()
				.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
	}
}
 

示例显式设置cookieHttpOnly=false。 这是允许JavaScript(即AngularJS)读取它的必要条件。 如果您不需要直接使用JavaScript读取cookie的功能,建议您省略cookieHttpOnly=false(改为使用new CookieCsrfTokenRepository())以提高安全性。

CSRF警告

实施CSRF时有一些注意事项。

超时

一个问题是预期的CSRF令牌存储在HttpSession中,因此一旦HttpSession过期,您配置的AccessDeniedHandler将收到InvalidCsrfTokenException。 如果您使用默认的AccessDeniedHandler,浏览器将获得HTTP 403并显示错误消息。

 

有人可能会问,为什么默认情况下CsrfToken没有存储在cookie中。 这是因为有一些已知漏洞可以通过其他域设置标题(即指定cookie)。 这与Ruby on Rails 当标题X-Requested-With存在时不再跳过CSRF检查的原因相同。 有关如何执行漏洞利用的详细信息,请参阅 这个webappsec.org线程。 另一个缺点是,通过消除状态(即超时),如果令牌受到威胁,则无法强制终止令牌。

缓解处于超时状态的活动用户的一个简单方法是使用一些JavaScript让用户知道他们的会话即将过期。 用户可以点击一个按钮继续并刷新会话。

或者,指定自定义AccessDeniedHandler可让您以任何方式处理InvalidCsrfTokenException。 有关如何自定义AccessDeniedHandler的示例,请参阅xml和https://github.com/spring-projects/spring-security/blob/3.2.0.RC1/提供的链接config / src / test / groovy / org / springframework / security / config / annotation / web / configurers / NamespaceHttpAccessDeniedHandlerTests.groovy#L64 [Java配置]。

最后,可以将应用程序配置为使用不会过期的CookieCsrfTokenRepository。 如前所述,这不像使用会话那样安全,但在很多情况下可以足够好。

登录

为了防止 伪造登录请求,登录表单也应该受到保护,以防止CSRF攻击。 由于CsrfToken存储在HttpSession中,这意味着一旦访问CsrfToken令牌属性,就会创建一个HttpSession。 虽然这在RESTful /无状态架构中听起来很糟糕,但现实是状态对于实现实际安全性是必需的。 没有国家,如果令牌受到损害,我们就无能为力。 实际上,CSRF令牌的规模很小,对我们的架构应该有微不足道的影响。

保护登录表单的常用技术是在表单提交之前使用JavaScript函数获取有效的CSRF标记。 通过这样做,不需要考虑会话超时(在前面的章节中讨论过),因为会话是在表单提交之前创建的(假设没有配置CookieCsrfTokenRepository),因此用户可以停留在登录页面上,并在需要时提交用户名/密码。 为了实现这一点,您可以利用Spring Security提供的CsrfTokenArgumentResolver,并公开如here中描述的端点。

注销

添加CSRF将更新LogoutFilter以仅使用HTTP POST。 这可确保注销需要CSRF令牌,并且恶意用户无法强制注销用户。

一种方法是使用表单注销。 如果你真的想要一个链接,你可以使用JavaScript来让链接执行一个POST(即可能在一个隐藏的窗体上)。 对于禁用JavaScript的浏览器,您可以选择使链接将用户带到注销确认页面,该页面将执行POST。

如果你真的想在注销时使用HTTP GET,你可以这样做,但请记住这通常不被推荐。 例如,以下Java配置将执行注销,并使用任何HTTP方法请求URL /注销:

@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {

	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http
			.logout()
				.logoutRequestMatcher(new AntPathRequestMatcher("/logout"));
	}
}

多部分(文件上传)

有多种方法可以对多部分/表单数据使用CSRF保护。 每个选项都有其折衷。

 

在将Spring Security的CSRF保护与多部分文件上传集成之前,请确保您可以先不使用CSRF保护。 有关在Spring参考的 17.10 Spring的多部分(文件上传)支持部分和 MultipartFilter javadoc中可找到有关使用Spring的多部分表单的更多信息。

在Spring Security之前放置MultipartFilter

第一种选择是确保在MultipartFilter在Spring Security过滤器之前被指定。 在Spring Security过滤器之前指定MultipartFilter意味着没有授权调用MultipartFilter,这意味着任何人都可以在您的服务器上放置临时文件。 但是,只有授权用户才能提交由您的应用程序处理的文件。 一般来说,这是推荐的方法,因为临时文件上传应该对大多数服务器产生可忽略的影响。

为确保在使用java配置的Spring Security过滤器之前指定MultipartFilter,用户可以覆盖beforeSpringSecurityFilterChain,如下所示:

public class SecurityApplicationInitializer extends AbstractSecurityWebApplicationInitializer {

	@Override
	protected void beforeSpringSecurityFilterChain(ServletContext servletContext) {
		insertFilters(servletContext, new MultipartFilter());
	}
}

为确保在配置XML配置的Spring Security过滤器之前指定MultipartFilter,用户可以确保MultipartFilter的<filter-mapping>元素位于web.xml中的springSecurityFilterChain之前,如下所示:

<filter>
	<filter-name>MultipartFilter</filter-name>
	<filter-class>org.springframework.web.multipart.support.MultipartFilter</filter-class>
</filter>
<filter>
	<filter-name>springSecurityFilterChain</filter-name>
	<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
	<filter-name>MultipartFilter</filter-name>
	<url-pattern>/*</url-pattern>
</filter-mapping>
<filter-mapping>
	<filter-name>springSecurityFilterChain</filter-name>
	<url-pattern>/*</url-pattern>
</filter-mapping>
包含CSRF令牌

如果允许未经授权的用户上传临时文件是不可接受的,另一种方法是将MultipartFilter放置在Spring Security筛选器之后,并将CSRF作为查询参数包含在表单的action属性中。 下面显示了一个jsp的例子

<form action="./upload?${_csrf.parameterName}=${_csrf.token}" method="post" enctype="multipart/form-data">

这种方法的缺点是查询参数可能泄漏。 更为常见的是,将敏感数据放在主体或标题中以确保其不泄漏是最佳做法。 其他信息可以在 RFC 2616第15.1.3节对URI中的敏感信息进行编码中找到。

HiddenHttpMethodFilter

HiddenHttpMethodFilter应放置在Spring Security过滤器之前。 总的来说,这是事实,但在防范CSRF攻击时可能会产生额外的影响。

请注意,HiddenHttpMethodFilter只覆盖POST上的HTTP方法,所以这实际上不会导致任何实际问题。 但是,确保在Spring Security过滤器之前放置它仍然是最佳实践。

覆盖默认值

Spring Security的目标是提供保护用户免受攻击的默认设置。 这并不意味着你被迫接受所有的默认值。

例如,您可以提供一个自定义CsrfTokenRepository来覆盖CsrfToken的存储方式。

您还可以指定一个自定义的RequestMatcher来确定哪些请求受到CSRF保护(即您可能不在意是否利用了注销)。 简而言之,如果Spring Security的CSRF保护行为不符合您的要求,您可以自定义行为。 有关如何使用XML和CsrfConfigurer javadoc制作这些自定义项的详细信息,请参阅<csrf>文档以获取有关如何在使用Java配置时进行这些自定义项的详细信息。

CORS

Spring Framework提供了 为CORS提供一流的支持。 必须在Spring Security之前处理CORS,因为预执行请求不会包含任何Cookie(即JSESSIONID)。 如果请求中不包含任何cookie并且Spring Security是第一个,则该请求将确定用户未被认证(因为请求中没有cookie)并拒绝它。

确保首先处理CORS的最简单方法是使用CorsFilter。 用户可以通过使用以下命令提供CorsConfigurationSource来将CorsFilter与Spring Security集成在一起:

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http
			// by default uses a Bean by the name of corsConfigurationSource
			.cors().and()
			...
	}

	@Bean
	CorsConfigurationSource corsConfigurationSource() {
		CorsConfiguration configuration = new CorsConfiguration();
		configuration.setAllowedOrigins(Arrays.asList("https://example.com"));
		configuration.setAllowedMethods(Arrays.asList("GET","POST"));
		UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
		source.registerCorsConfiguration("/**", configuration);
		return source;
	}
}

或以XML格式

<http>
	<cors configuration-source-ref="corsSource"/>
	...
</http>
<b:bean id="corsSource" class="org.springframework.web.cors.UrlBasedCorsConfigurationSource">
	...
</b:bean>

如果您使用Spring MVC的CORS支持,则可以省略指定CorsConfigurationSource,Spring Security将利用为Spring MVC提供的CORS配置。

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http
			// if Spring MVC is on classpath and no CorsConfigurationSource is provided,
			// Spring Security will use CORS configuration provided to Spring MVC
			.cors().and()
			...
	}
}

或以XML格式

<http>
	<!-- Default to Spring MVC's CORS configuration -->
	<cors />
	...
</http>

安全性HTTP响应头

本节讨论Spring Security支持将各种安全头添加到响应中。

默认安全标题

Spring Security允​​许用户轻松地注入默认安全头以帮助保护他们的应用程序。 Spring Security的默认设置是包含以下标题:

Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Content-Type-Options: nosniff
Strict-Transport-Security: max-age=31536000 ; includeSubDomains
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block

注意:仅在HTTPS请求上添加严格传输安全性

有关每个标题的更多详细信息,请参阅相应章节:

虽然这些标题都被认为是最佳实践,但应该指出,并非所有客户都使用标题,因此鼓励进行其他测试。

您可以自定义特定的标题。 例如,假设您希望您的HTTP响应标头如下所示:

Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block

具体来说,您需要使用以下自定义设置的所有默认标题:

您可以使用以下Java配置轻松完成此操作:

@EnableWebSecurity
public class WebSecurityConfig extends
		WebSecurityConfigurerAdapter {

	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http
			// ...
			.headers()
				.frameOptions().sameOrigin()
				.httpStrictTransportSecurity().disable();
	}
}

或者,如果您使用的是Spring Security XML Configuration,则可以使用以下内容:

<http>
	<!-- ... -->

	<headers>
		<frame-options policy="SAMEORIGIN" />
		<hsts disable="true"/>
	</headers>
</http>

如果您不希望添加默认设置并希望明确控制应该使用的内容,则可以禁用默认设置。 下面提供了一个基于Java和XML的配置示例:

如果您使用的是Spring Security的Java配置,以下内容仅会添加Cache Control

@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
	http
	// ...
	.headers()
		// do not use any default headers unless explicitly listed
		.defaultsDisabled()
		.cacheControl();
}
}

以下XML仅会添加Cache Control

<http>
	<!-- ... -->

	<headers defaults-disabled="true">
		<cache-control/>
	</headers>
</http>

如有必要,可以使用以下Java配置禁用所有HTTP安全响应标头:

@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
	http
	// ...
	.headers().disable();
}
}

如有必要,可以使用下面的XML配置禁用所有HTTP安全响应标头:

<http>
	<!-- ... -->

	<headers disabled="true" />
</http>

缓存控制

在过去,Spring Security要求您为您的Web应用程序提供自己的缓存控制。 这在当时似乎是合理的,但浏览器缓存已经发展到包括用于安全连接的缓存。 这意味着用户可以查看已认证的页面并注销,然后恶意用户可以使用浏览器历史记录查看缓存的页面。 为了帮助缓解这个问题,Spring Security增加了缓存控制支持,它将在响应中插入以下头文件。

Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0

简单地添加没有子元素的<headers>元素将自动添加缓存控制和其他一些保护。 但是,如果您只需要缓存控制,则可以使用带有<cache-control>元素和headers@defaults-disabled属性的Spring Security的XML名称空间启用此功能。

<http>
	<!-- ... -->

	<headers defaults-disable="true">
		<cache-control />
	</headers>
</http>

同样,您可以通过以下方式仅启用Java配置中的缓存控制:

@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
	http
	// ...
	.headers()
		.defaultsDisabled()
		.cacheControl();
}
}

如果您真的想缓存特定的响应,您的应用程序可以选择性地调用 HttpServletResponse.setHeader(字符串,字符串)来覆盖Spring Security设置的标头。 这对于确保像CSS,JavaScript和图像等正确缓存很有用。

在使用Spring Web MVC时,通常在您的配置中完成。 例如,以下配置将确保为所有资源设置缓存标头:

@EnableWebMvc
public class WebMvcConfiguration implements WebMvcConfigurer {

	@Override
	public void addResourceHandlers(ResourceHandlerRegistry registry) {
		registry
			.addResourceHandler("/resources/**")
			.addResourceLocations("/resources/")
			.setCachePeriod(31556926);
	}

	// ...
}

内容类型选项

历史上,浏览器(包括Internet Explorer)会尝试使用 内容嗅探来猜测请求的内容类型。 这允许浏览器通过猜测未指定内容类型的资源上的内容类型来改善用户体验。 例如,如果浏览器遇到没有指定内容类型的JavaScript文件,它将能够猜测内容类型并执行它。

 

在允许上传内容时,应该做很多其他事情(即,仅在不同域中显示文档,确保设置Content-Type标头,清理文档等)。 但是,这些措施超出了Spring Security提供的范围。 指出禁用内容嗅探时,还必须指出内容类型以使事情正常工作。

内容嗅探的问题在于,它允许恶意用户使用polyglots(即作为多种内容类型有效的文件)来执行XSS攻击。 例如,某些网站可能允许用户向网站提交有效的postscript文档并查看它。 恶意用户可能会创建 postscript文件也是一个有效的JavaScript文件并使用它执行XSS攻击。

内容嗅探可以通过将以下标题添加到我们的响应来禁用:

X-Content-Type-Options: nosniff

就像缓存控制元素一样,在没有子元素的情况下使用<headers>元素时,默认添加nosniff指令。 但是,如果您想更多地控制添加哪些标题,则可以使用<content-type-options>元素和headers@defaults-disabled属性,如下所示:

<http>
	<!-- ... -->

	<headers defaults-disabled="true">
		<content-type-options />
	</headers>
</http>

默认情况下,Spring Security Java配置会添加X-Content-Type-Options标头。 如果您想更好地控制标题,可以使用以下内容显式指定内容类型选项:

@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
	http
	// ...
	.headers()
		.defaultsDisabled()
		.contentTypeOptions();
}
}

HTTP严格传输安全性(HSTS)

当您输入银行网站时,您是输入mybank.example.com还是输入https://mybank.example.com []?如果您省略https协议,则可能会受 中间人攻击的影响。 即使网站执行https://mybank.example.com的重定向,恶意用户也可能会拦截最初的HTTP请求并操纵响应(即重定向到https://mibank.example.com并窃取其凭据)。

许多用户省略了https协议,这就是 HTTP严格传输安全性(HSTS)创建的原因。 一旦mybank.example.com作为 HSTS主机添加,浏览器可以提前知道任何对mybank.example.com的请求应该被解释为https://mybank.example.com。 这大大降低了发生中间人攻击的可能性。

 

根据{{​​0}},HSTS标头仅被注入到HTTPS响应中。 为了使浏览器确认标题,浏览器必须首先相信签署用于建立连接的SSL证书的CA(而不仅仅是SSL证书)。

将站点标记为HSTS主机的一种方法是将主机预加载到浏览器中。 另一种方法是将"Strict-Transport-Security"标头添加到响应中。 例如,以下内容将指示浏览器将域名视为一年的HSTS主机(一年大约有31536000秒):

Strict-Transport-Security: max-age=31536000 ; includeSubDomains

可选的includeSubDomains指令指示Spring Security将子域(即secure.mybank.example.com)也视为HSTS域。

与其他标题一样,Spring Security默认添加HSTS。 您可以使用<hsts>元素自定义HSTS标头,如下所示:

<http>
	<!-- ... -->

	<headers>
		<hsts
			include-subdomains="true"
			max-age-seconds="31536000" />
	</headers>
</http>

同样,您可以只启用带有Java配置的HSTS头文件:

@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
	http
	// ...
	.headers()
		.httpStrictTransportSecurity()
			.includeSubdomains(true)
			.maxAgeSeconds(31536000);
}
}

HTTP公钥锁定(HPKP)

HTTP Public Key Pinning(HPKP)是一项安全功能,它告诉Web客户端将特定的加密公钥与特定的Web服务器相关联,以防止伪造的证书对中间人(MITM)的攻击。

为了确保在TLS会话中使用的服务器公钥的真实性,该公钥被封装到通常由证书颁发机构(CA)签署的X.509证书中。 Web客户端(如浏览器)信任很多这些CA,它们都可以为任意域名创建证书。 如果攻击者能够危害单个CA,他们可以对各种TLS连接执行MITM攻击。 HPKP可以通过告诉客户端哪个公钥属于某个Web服务器来规避HTTPS协议的这种威胁。 HPKP是首次使用信托(TOFU)技术。 Web服务器第一次通过特殊的HTTP头告诉客户端公钥属于它时,客户端将这些信息存储一段给定的时间。 当客户端再次访问服务器时,它需要一个包含指纹已经通过HPKP知道的公钥的证书。 如果服务器提供未知的公钥,则客户端应向用户提供警告。

 

由于用户代理需要根据SSL证书链验证引脚,所以HPKP头只能注入到HTTPS响应中。

为您的站点启用此功能非常简单,只需在通过HTTPS访问您的站点时返回Public-Key-Pins HTTP标头即可。 例如,以下内容将指示用户代理仅向指定的URI报告引脚验证失败(通过https://tools.ietf.org/html/rfc7469#section-2.1.4[report-uri]指令)2个引脚:

Public-Key-Pins-Report-Only: max-age=5184000 ; pin-sha256="d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM=" ; pin-sha256="E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g=" ; report-uri="http://example.net/pkp-report" ; includeSubDomains

pin validation failure report是标准的JSON结构,可以通过Web应用程序自己的API或公开托管的HPKP报告服务来捕获,如https://report-uri.io/[REPORT-URI]。

可选的includeSubDomains指令指示浏览器也使用给定引脚验证子域。

与其他标题相反,Spring Security默认不添加HPKP。 您可以使用<hpkp>元素自定义HPKP标题,如下所示:

<http>
	<!-- ... -->

	<headers>
		<hpkp
			include-subdomains="true"
			report-uri="http://example.net/pkp-report">
			<pins>
					<pin algorithm="sha256">d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM=</pin>
					<pin algorithm="sha256">E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g=</pin>
			</pins>
		</hpkp>
	</headers>
</http>

同样,您可以使用Java配置启用HPKP标题:

@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {

		@Override
		protected void configure(HttpSecurity http) throws Exception {
				http
				// ...
				.headers()
						.httpPublicKeyPinning()
								.includeSubdomains(true)
								.reportUri("http://example.net/pkp-report")
								.addSha256Pins("d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM=", "E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g=";
		}
}

X框-选项

允许将您的网站添加到框架可能是一个安全问题。 例如,使用聪明的CSS样式的用户可能会被欺骗点击他们不想要的东西( 视频演示)。 例如,登录他们银行的用户可能会点击一个按钮,授予对其他用户的访问权限。 这种攻击被称为 点击劫持

 

另一种处理点击劫持的现代方法是使用内容安全策略(CSP)

有多种方法可以缓解点击劫持攻击。 例如,为了保护传统浏览器免受点击劫持攻击,您可以使用https://www.owasp.org/index.php/Clickjacking_Defense_Cheat_Sheet#Best-for-now_Legacy_Browser_Frame_Breaking_Script [框架破解代码]。 虽然不完美,但对于传统浏览器来说,破解代码是最好的选择。

处理点击劫持的更现代的方法是使用https://developer.mozilla.org/en-US/docs/HTTP/X-Frame-Options[X-Frame-Options]标头:

X-Frame-Options: DENY

X-Frame-Options响应头指示浏览器阻止响应中的任何站点在帧中呈现。 默认情况下,Spring Security会禁用iframe中的渲染。

您可以使用frame-options元素自定义X-Frame-Options。 例如,以下内容将指示Spring Security使用允许同一域内的iframe的"X-Frame-Options: SAMEORIGIN":

<http>
	<!-- ... -->

	<headers>
		<frame-options
		policy="SAMEORIGIN" />
	</headers>
</http>

同样,您可以使用以下方法自定义框架选项以在Java配置中使用相同的源:

@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
	http
	// ...
	.headers()
		.frameOptions()
			.sameOrigin();
}
}

X-​​XSS-保护

有些浏览器支持过滤https://www.owasp.org/index.php/Testing_for_Reflected_Cross_site_scripting_(OWASP-DV-001)[reflected XSS attacks]。 这绝不是万无一失的,但有助于XSS保护。

默认情况下,过滤通常处于启用状态,因此添加标头通常会确保启用它并指示浏览器在检测到XSS攻击时该执行什么操作。 例如,该过滤器可能会尝试以最小侵入方式更改内容,以继续呈现所有内容。 有时候,这种类型的替换可能会变成 XSS漏洞本身。 相反,最好是阻止内容而不是尝试修复它。 为此,我们可以添加以下标题:

X-XSS-Protection: 1; mode=block

该标题默认包含在内。 但是,如果我们想要,我们可以定制它。 例如:

<http>
	<!-- ... -->

	<headers>
		<xss-protection block="false"/>
	</headers>
</http>

同样,您可以使用以下方法在Java配置中自定义XSS保护:

@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
	http
	// ...
	.headers()
		.xssProtection()
			.block(false);
}
}

内容安全策略(CSP)

https://www.w3.org/TR/CSP2/ [内容安全策略(CSP)]是Web应用程序可以利用的机制来缓解内容注入漏洞,例如跨站点脚本(XSS)。 CSP是一种声明性策略,为Web应用程序作者声明并最终通知客户端(用户代理)有关Web应用程序预期从中加载资源的来源提供了便利。

 

内容安全策略不是为了解决所有内容注入漏洞。 相反,可以利用CSP来帮助减少内容注入攻击造成的危害。 作为第一道防线,Web应用程序作者应验证其输入并对其输出进行编码。

Web应用程序可以通过在响应中包含以下HTTP标头之一来使用CSP:

  • Content-Security-Policy

  • Content-Security-Policy-Report-Only

这些标头中的每一个都用作将security policy交付给客户端的机制。 安全策略包含一组security policy directives(例如script-srcobject-src),每个负责声明特定资源表示的限制。

例如,Web应用程序可以声明它希望通过在响应中包含以下标题来从特定的可信来源加载脚本:

Content-Security-Policy: script-src https://trustedscripts.example.com

尝试从其他来源加载脚本,而不是在script-src指令中声明的脚本将被用户代理阻止。 此外,如果在安全策略中声明了https://www.w3.org/TR/CSP2/#directive-report-uri[report-uri]指令,则该违规将由用户代理报告到声明的URL。

例如,如果Web应用程序违反了声明的安全策略,则以下响应标头将指示用户代理将违规报告发送到策略的report-uri指令中指定的URL。

Content-Security-Policy: script-src https://trustedscripts.example.com; report-uri /csp-report-endpoint/

Violation reports是标准的JSON结构,可以通过Web应用程序自己的API或公开托管的CSP违规报告服务来捕获,如https://report-uri.io/[REPORT-URI]。

Content-Security-Policy-Report-Only标头为Web应用程序作者和管理员提供了监控安全策略的功能,而不是强制执行它们。 此标题通常用于为网站试验和/或开发安全策略。 当策略被认为有效时,可以通过使用Content-Security-Policy标头字段来强制实施。

给定以下响应头,该策略声明脚本可以从两个可能来源之一加载。

Content-Security-Policy-Report-Only: script-src 'self' https://trustedscripts.example.com; report-uri /csp-report-endpoint/

如果站点违反了此策略,则通过尝试从evil.com加载脚本,用户代理将向report-uri指令指定的声明URL发送违规报告,但仍允许加载违规资源。

配置内容安全策略

默认情况下需要注意Spring Security does not add内容安全策略。 Web应用程序作者必须声明安全策略以强制执行和/或监视受保护的资源。

例如,鉴于以下安全策略:

script-src 'self' https://trustedscripts.example.com; object-src https://trustedplugins.example.com; report-uri /csp-report-endpoint/

您可以使用带<content-security-policy>元素的XML配置来启用CSP头,如下所示:

<http>
	<!-- ... -->

	<headers>
		<content-security-policy
			policy-directives="script-src 'self' https://trustedscripts.example.com; object-src https://trustedplugins.example.com; report-uri /csp-report-endpoint/" />
	</headers>
</http>

要启用CSP report-only_头,请按如下所示配置元素:

<http>
	<!-- ... -->

	<headers>
		<content-security-policy
			policy-directives="script-src 'self' https://trustedscripts.example.com; object-src https://trustedplugins.example.com; report-uri /csp-report-endpoint/"
			report-only="true" />
	</headers>
</http>

同样,您可以使用Java配置启用CSP头,如下所示:

@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
	http
	// ...
	.headers()
		.contentSecurityPolicy("script-src 'self' https://trustedscripts.example.com; object-src https://trustedplugins.example.com; report-uri /csp-report-endpoint/");
}
}

要启用CSP 'report-only'头,请提供以下Java配置:

@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
	http
	// ...
	.headers()
		.contentSecurityPolicy("script-src 'self' https://trustedscripts.example.com; object-src https://trustedplugins.example.com; report-uri /csp-report-endpoint/")
		.reportOnly();
}
}
其他资源

将内容安全策略应用于Web应用程序通常是一项不重要的任务。 以下资源可能会为您的网站制定有效的安全策略提供进一步的帮助。

推荐人政策

Referrer Policy是Web应用程序可以利用的机制来管理引用者字段,其中包含最后一个 用户所在的页面。

Spring Security的方法是使用https://www.w3.org/TR/referrer-policy/[Referrer Policy]标头,它提供了不同的https://www.w3.org/TR/referrer-policy/#referrer-政策[政策]:

Referrer-Policy: same-origin

Referrer-Policy响应头指示浏览器让目的地知道用户以前的来源。

配置Referrer策略

Spring Security doesn’t add默认引用策略标头。

您可以使用带<referrer-policy>元素的XML配置启用Referrer-Policy标头,如下所示:

<http>
	<!-- ... -->

	<headers>
		<referrer-policy policy="same-origin" />
	</headers>
</http>

同样,您可以使用Java配置启用Referrer Policy标头,如下所示:

@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
	http
	// ...
	.headers()
		.referrerPolicy(ReferrerPolicy.SAME_ORIGIN);
}
}

自定义标题

Spring Security具有一些机制,可以方便地将更常见的安全性标题添加到应用程序中。 不过,它也提供挂钩来启用添加自定义标题。

静态标头

您可能有时希望将自定义安全标头插入到您的应用程序中,但不支持开箱即用。 例如,给定以下自定义安全标头:

X-Custom-Security-Header: header-value

使用XML名称空间时,可以使用<header>元素将这些头添加到响应中,如下所示:

<http>
	<!-- ... -->

	<headers>
		<header name="X-Custom-Security-Header" value="header-value"/>
	</headers>
</http>

同样,可以使用Java Configuration将头添加到响应中,如下所示:

@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
	http
	// ...
	.headers()
		.addHeaderWriter(new StaticHeadersWriter("X-Custom-Security-Header","header-value"));
}
}

标题作者

如果名称空间或Java配置不支持所需的标题,则可以创建自定义HeadersWriter实例,甚至可以提供HeadersWriter的自定义实现。

我们来看看使用XFrameOptionsHeaderWriter的自定义实例的示例。 也许你想允许为相同的来源制作内容。 通过将policy属性设置为"SAMEORIGIN",很容易支持这一点,但让我们来看看使用ref属性的更明确的示例。

<http>
	<!-- ... -->

	<headers>
		<header ref="frameOptionsWriter"/>
	</headers>
</http>
<!-- Requires the c-namespace.
See http://docs.spring.io/spring/docs/current/spring-framework-reference/htmlsingle/#beans-c-namespace
-->
<beans:bean id="frameOptionsWriter"
	class="org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter"
	c:frameOptionsMode="SAMEORIGIN"/>

我们还可以通过Java配置将内容的框架限制在相同的原始位置:

@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
	http
	// ...
	.headers()
		.addHeaderWriter(new XFrameOptionsHeaderWriter(XFrameOptionsMode.SAMEORIGIN));
}
}

DelegatingRequestMatcherHeaderWriter

有时您可能只想为某些请求编写头文件。 例如,也许你只想保护你的登录页面不被陷害。 您可以使用DelegatingRequestMatcherHeaderWriter来执行此操作。 在使用XML名称空间配置时,可以通过以下方式完成此操作:

<http>
	<!-- ... -->

	<headers>
		<frame-options disabled="true"/>
		<header ref="headerWriter"/>
	</headers>
</http>

<beans:bean id="headerWriter"
	class="org.springframework.security.web.header.writers.DelegatingRequestMatcherHeaderWriter">
	<beans:constructor-arg>
		<bean class="org.springframework.security.web.util.matcher.AntPathRequestMatcher"
			c:pattern="/login"/>
	</beans:constructor-arg>
	<beans:constructor-arg>
		<beans:bean
			class="org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter"/>
	</beans:constructor-arg>
</beans:bean>

我们还可以使用java配置防止内容成帧到登录页面:

@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
	RequestMatcher matcher = new AntPathRequestMatcher("/login");
	DelegatingRequestMatcherHeaderWriter headerWriter =
		new DelegatingRequestMatcherHeaderWriter(matcher,new XFrameOptionsHeaderWriter());
	http
	// ...
	.headers()
		.frameOptions().disabled()
		.addHeaderWriter(headerWriter);
}
}

会话管理

HTTP会话相关功能由过滤器委派的SessionManagementFilterSessionAuthenticationStrategy接口组合处理。 典型用法包括会话固定保护攻击预防,会话超时检测以及限制已验证用户同时打开的会话数。

SessionManagementFilter

SessionManagementFilter根据SecurityContextHolder的当前内容检查SecurityContextRepository的内容,以确定用户在当前请求期间是否已通过身份验证,通常是通过非交互式身份验证机制作为预认证或记住我的脚注:[SessionManagementFilter不会检测到身份验证后执行重定向(例如表单登录)的身份验证,因为筛选器在身份验证请求期间不会被调用。 会话管理功能必须在这些情况下单独处理。 ]. 如果存储库包含安全上下文,则该过滤器不执行任何操作。 如果没有,并且线程本地SecurityContext包含(非匿名)Authentication对象,则筛选器会假定它们已由堆栈中的前一个筛选器进行了身份验证。 然后它将调用配置的SessionAuthenticationStrategy

如果用户当前未通过身份验证,则筛选器将检查是否请求了无效的会话ID(例如由于超时),并且将调用配置的InvalidSessionStrategy(如果已设置)。 最常见的行为就是重定向到一个固定的URL,并将其封装在标准实现SimpleRedirectInvalidSessionStrategy中。 在通过命名空间as described earlier配置无效会话URL时,也会使用后者。

SessionAuthenticationStrategy

SessionManagementFilterAbstractAuthenticationProcessingFilter都使用SessionAuthenticationStrategy,所以如果您使用的是自定义的表单登录类,则需要将其注入到这两者中。 在这种情况下,组合命名空间和自定义Bean的典型配置可能如下所示:

<http>
<custom-filter position="FORM_LOGIN_FILTER" ref="myAuthFilter" />
<session-management session-authentication-strategy-ref="sas"/>
</http>

<beans:bean id="myAuthFilter" class=
"org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter">
	<beans:property name="sessionAuthenticationStrategy" ref="sas" />
	...
</beans:bean>

<beans:bean id="sas" class=
"org.springframework.security.web.authentication.session.SessionFixationProtectionStrategy" />

请注意,如果要在实现HttpSessionBindingListener的会话中存储Bean,包括Spring会话范围的bean,则使用默认值SessionFixationProtectionStrategy可能会导致问题。 有关更多信息,请参阅此类的Javadoc。

并发控制

Spring Security能够防止委托人同时对同一应用程序进行超过指定次数的身份验证。 许多独立软件开发商利用这一点来强制执行许可,而网络管理员喜欢这种功能,因为它有助于防止人们共享登录名。 例如,您可以阻止用户"Batman"从两个不同的会话登录到Web应用程序。 您可以使其以前的登录失效,或者在他们尝试再次登录时报告错误,从而阻止第二次登录。 请注意,如果您使用的是第二种方法,那么未明确注销的用户(例如,刚刚关闭浏览器的用户)将无法再次登录,直到其原始会话过期。

并发控制由名称空间支持,因此请检查较早的名称空间章节以获取最简单的配置。 有时候你需要定制一些东西。

该实现使用SessionAuthenticationStrategy的专用版本,称为ConcurrentSessionControlAuthenticationStrategy

 

以前,并发身份验证检查由ProviderManager进行,可以使用ConcurrentSessionController注入。 后者会检查用户是否尝试超出允许的会话数量。 但是,这种方法需要事先创建HTTP会话,这是不可取的。 在Spring Security 3中,用户首先通过AuthenticationManager进行身份验证,并且一旦他们成功通过身份验证,将创建一个会话并检查是否允许他们打开另一个会话。

要使用并发会话支持,您需要将以下内容添加到web.xml

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

另外,您需要将ConcurrentSessionFilter添加到您的FilterChainProxy ConcurrentSessionFilter需要两个构造函数参数sessionRegistry,它通常指向SessionRegistryImpl的实例,而sessionInformationExpiredStrategy定义了在会话过期时应用的策略。 使用命名空间创建FilterChainProxy和其他默认bean的配置可能如下所示:

<http>
<custom-filter position="CONCURRENT_SESSION_FILTER" ref="concurrencyFilter" />
<custom-filter position="FORM_LOGIN_FILTER" ref="myAuthFilter" />

<session-management session-authentication-strategy-ref="sas"/>
</http>

<beans:bean id="redirectSessionInformationExpiredStrategy"
class="org.springframework.security.web.session.SimpleRedirectSessionInformationExpiredStrategy">
<beans:constructor-arg name="invalidSessionUrl" value="/session-expired.htm" />
</beans:bean>

<beans:bean id="concurrencyFilter"
class="org.springframework.security.web.session.ConcurrentSessionFilter">
<beans:constructor-arg name="sessionRegistry" ref="sessionRegistry" />
<beans:constructor-arg name="sessionInformationExpiredStrategy" ref="redirectSessionInformationExpiredStrategy" />
</beans:bean>

<beans:bean id="myAuthFilter" class=
"org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter">
<beans:property name="sessionAuthenticationStrategy" ref="sas" />
<beans:property name="authenticationManager" ref="authenticationManager" />
</beans:bean>

<beans:bean id="sas" class="org.springframework.security.web.authentication.session.CompositeSessionAuthenticationStrategy">
<beans:constructor-arg>
	<beans:list>
	<beans:bean class="org.springframework.security.web.authentication.session.ConcurrentSessionControlAuthenticationStrategy">
		<beans:constructor-arg ref="sessionRegistry"/>
		<beans:property name="maximumSessions" value="1" />
		<beans:property name="exceptionIfMaximumExceeded" value="true" />
	</beans:bean>
	<beans:bean class="org.springframework.security.web.authentication.session.SessionFixationProtectionStrategy">
	</beans:bean>
	<beans:bean class="org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy">
		<beans:constructor-arg ref="sessionRegistry"/>
	</beans:bean>
	</beans:list>
</beans:constructor-arg>
</beans:bean>

<beans:bean id="sessionRegistry"
	class="org.springframework.security.core.session.SessionRegistryImpl" />

每当HttpSession开始或结束时,将侦听器添加到web.xml,都会将ApplicationEvent发布到Spring ApplicationContext。 这很重要,因为它允许在会话结束时通知SessionRegistryImpl。 如果没有它,即使用户退出其他会话或超时,用户将永远无法再次重新登录。

查询当前通过身份验证的用户及其会话的SessionRegistry

通过命名空间或使用普通bean设置并发控制具有有用的副作用,即为您提供对可在应用程序中直接使用的SessionRegistry的引用,因此即使您不想限制用户可能拥有的会话数量,无论如何,建立基础架构可能都是值得的。 您可以将maximumSession属性设置为-1以允许无限制的会话。 如果您使用的是名称空间,则可以使用session-registry-alias属性为内部创建的SessionRegistry设置别名,从而为您自己的bean注入引用。

getAllPrincipals()方法为您提供当前已通过身份验证的用户列表。 您可以通过调用getAllSessions(Object principal, boolean includeExpiredSessions)方法列出用户的会话,该方法返回SessionInformation对象列表。 您也可以通过调用SessionInformation实例上的expireNow()来过期用户的会话。 当用户返回到应用程序时,将阻止他们继续进行。 例如,您可以在管理应用程序中找到这些方法。 查看Javadoc以获取更多信息。

匿名身份验证

概述

通常认为采用"deny-by-default"的安全做法非常好,您可以在其中明确指定允许的内容并拒绝其他所有内容。 定义未经身份验证的用户可访问的内容也是类似的情况,特别是对于Web应用程序。 许多网站要求用户必须通过除少数网址以外的其他任何验证(例如,家庭和登录页面)。 在这种情况下,为这些特定的URL定义访问配置属性是最容易的,而不是针对每个安全资源。 换句话说,有时很高兴地说默认情况下ROLE_SOMETHING是必需的,并且只允许某些例外情况,例如登录,注销和应用程序的主页。 您也可以完全忽略过滤器链中的这些页面,从而绕过访问控制检查,但这可能因其他原因而不受欢迎,特别是如果页面对经过身份验证的用户的行为不同。

这就是我们所说的匿名认证。 请注意,"anonymously authenticated"的用户与未经身份验证的用户之间没有真正的概念区别。 Spring Security的匿名身份验证只是为您提供一种更方便的配置访问控制属性的方式。 例如,调用诸如getCallerPrincipal的servlet API调用,即使实际上存在SecurityContextHolder中的匿名认证对象,仍将返回null。

在其他情况下,匿名身份验证很有用,例如审计拦截器查询SecurityContextHolder以确定哪个主体负责给定操作。 如果他们知道SecurityContextHolder始终包含Authentication对象,并且从不null,则可以更强健地编写类。

配置

当使用HTTP配置Spring Security 3.0时,会自动提供匿名认证支持,并且可以使用<anonymous>元素进行自定义(或禁用)。 除非您使用传统的bean配置,否则不需要配置这里描述的bean。

三个类共同提供匿名身份验证功能。 AnonymousAuthenticationTokenAuthentication的实现,并存储适用于匿名主体的GrantedAuthority。 有一个对应的AnonymousAuthenticationProvider,链接到ProviderManager,以便接受AnonymousAuthenticationToken。 最后,还有一个AnonymousAuthenticationFilter,它在普通身份验证机制之后被链接,并且如果没有现有的Authentication,则会自动向SecurityContextHolder添加AnonymousAuthenticationToken。 筛选器和身份验证提供程序的定义如下所示:

<bean id="anonymousAuthFilter"
	class="org.springframework.security.web.authentication.AnonymousAuthenticationFilter">
<property name="key" value="foobar"/>
<property name="userAttribute" value="anonymousUser,ROLE_ANONYMOUS"/>
</bean>

<bean id="anonymousAuthenticationProvider"
	class="org.springframework.security.authentication.AnonymousAuthenticationProvider">
<property name="key" value="foobar"/>
</bean>

key在过滤器和身份验证提供程序之间共享,以便前者创建的令牌可以被后面的脚注接受:[{ 不应将key属性的使用视为提供任何实际安全性。 这仅仅是一个记账练习。 如果您在验证客户端可能构建Authentication对象(例如使用RMI调用)的情况下共享包含AnonymousAuthenticationProvider的{​​{0}},则恶意客户端可以提交自己创建的AnonymousAuthenticationToken(使用所选的用户名和权限列表)。 如果key是可猜测的或可以发现的,那么该令牌将被匿名提供者接受。 这对正常使用来说并不是问题,但如果您使用RMI,则最好使用自定义ProviderManager,省略匿名提供程序,而不是共享您用于HTTP身份验证机制的那个。 ]. userAttributeusernameInTheAuthenticationToken,grantedAuthority[,grantedAuthority]的形式表示。 这与在InMemoryDaoImpl的{​​{0}}属性的等号之后使用的语法相同。

如前所述,匿名身份验证的好处是所有的URI模式都可以应用于它们。 例如:

<bean id="filterSecurityInterceptor"
	class="org.springframework.security.web.access.intercept.FilterSecurityInterceptor">
<property name="authenticationManager" ref="authenticationManager"/>
<property name="accessDecisionManager" ref="httpRequestAccessDecisionManager"/>
<property name="securityMetadata">
	<security:filter-security-metadata-source>
	<security:intercept-url pattern='/index.jsp' access='ROLE_ANONYMOUS,ROLE_USER'/>
	<security:intercept-url pattern='/hello.htm' access='ROLE_ANONYMOUS,ROLE_USER'/>
	<security:intercept-url pattern='/logoff.jsp' access='ROLE_ANONYMOUS,ROLE_USER'/>
	<security:intercept-url pattern='/login.jsp' access='ROLE_ANONYMOUS,ROLE_USER'/>
	<security:intercept-url pattern='/**' access='ROLE_USER'/>
	</security:filter-security-metadata-source>" +
</property>
</bean>

AuthenticationTrustResolver

舍弃匿名身份验证讨论的是AuthenticationTrustResolver接口及其相应的AuthenticationTrustResolverImpl实现。 该接口提供了isAnonymous(Authentication)方法,它允许感兴趣的类考虑这种特殊类型的身份验证状态。ExceptionTranslationFilter在处理AccessDeniedException时使用此接口。 如果AccessDeniedException被抛出,并且身份验证是匿名类型,而不是抛出403(禁止)响应,则过滤器将改为开始AuthenticationEntryPoint,以便委托人可以正确进行身份验证。 这是一个必要的区别,否则委托人将始终被视为"authenticated",永远不会有机会通过表单,基本,摘要或其他正常的身份验证机制进行登录。

您经常会看到上述拦截器配置中的ROLE_ANONYMOUS属性被IS_AUTHENTICATED_ANONYMOUSLY替代,这在定义访问控制时实际上是同样的事情。 这是使用我们将在authorization chapter中看到的AuthenticatedVoter的一个示例。 它使用AuthenticationTrustResolver来处理这个特定的配置属性并授予匿名用户访问权限。 AuthenticatedVoter方法更强大,因为它允许区分匿名,记住我和完全认证的用户。 如果您不需要此功能,那么您可以坚持使用ROLE_ANONYMOUS,这将由Spring Security的标准RoleVoter处理。

WebSocket安全性

Spring Security 4增加了对保护 Spring的WebSocket支持的支持。 本节介绍如何使用Spring Security的WebSocket支持。

注意:您可以在samples / javaconfig / chat中找到完整的WebSocket安全工作示例。

。直接JSR-356支持

Spring Security没有提供直接的JSR-356支持,因为这样做没有多大价值。 这是因为格式未知,所以 小泉可以做到保证未知格式。 另外,JSR-356不提供拦截消息的方法,所以安全性会相当侵入。

WebSocket配置

Spring Security 4.0通过Spring消息抽象为WebSockets引入了授权支持。 要使用Java配置配置授权,只需扩展AbstractSecurityWebSocketMessageBrokerConfigurer并配置MessageSecurityMetadataSourceRegistry即可。 例如:

@Configuration
public class WebSocketSecurityConfig
      extends AbstractSecurityWebSocketMessageBrokerConfigurer {  

    protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
        messages
                .simpDestMatchers("/user/*").authenticated() 
    }
}

这将确保:

<1>任何入站CONNECT消息都需要一个有效的CSRF令牌来执行Same Origin Policy <2> SecurityContextHolder由simpUser标头属性中的用户填充,用于任何入站请求。 <3>我们的邮件需要获得适当的授权。具体来说,任何以"/user/"开头的入站邮件都需要ROLE_USER。有关授权的更多详细信息,请参阅WebSocket授权

Spring Security还为保护WebSocket提供XML Namespace支持。 可比的基于XML的配置如下所示:

<websocket-message-broker>  
    
    <intercept-message pattern="/user/**" access="hasRole('USER')" />
</websocket-message-broker>

这将确保:

<1>任何入站CONNECT消息都需要一个有效的CSRF令牌来执行Same Origin Policy <2> SecurityContextHolder由simpUser标头属性中的用户填充,用于任何入站请求。 <3>我们的邮件需要获得适当的授权。具体来说,任何以"/user/"开头的入站邮件都需要ROLE_USER。有关授权的更多详细信息,请参阅WebSocket授权

WebSocket身份验证

当WebSocket连接建立时,WebSocket重复使用与HTTP请求中相同的认证信息。 这意味着HttpServletRequest上的Principal将被移交给WebSockets。 如果您使用的是Spring Security,则HttpServletRequest上的Principal会自动覆盖。

更具体地说,为了确保用户已经对WebSocket应用程序进行了身份验证,所有必需的是确保您设置Spring Security来验证您的基于HTTP的Web应用程序。

WebSocket授权

Spring Security 4.0通过Spring消息抽象为WebSockets引入了授权支持。 要使用Java配置配置授权,只需扩展AbstractSecurityWebSocketMessageBrokerConfigurer并配置MessageSecurityMetadataSourceRegistry即可。 例如:

@Configuration
public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {

    @Override
    protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
        messages
                .nullDestMatcher().authenticated() 
                .simpSubscribeDestMatchers("/user/queue/errors").permitAll() 
                .simpDestMatchers("/app/**").hasRole("USER") 
                .simpSubscribeDestMatchers("/user/**", "/topic/friends/*").hasRole("USER") 
                .simpTypeMatchers(MESSAGE, SUBSCRIBE).denyAll() 
                .anyMessage().denyAll(); 

    }
}

这将确保:

<1>没有目标的任何消息(即消息类型为MESSAGE或SUBSCRIBE以外的任何消息)都需要用户进行身份验证 <2>任何人都可以订阅/ user / queue / errors <3>具有以"/app/"开头的目标的任何消息都将要求用户拥有角色ROLE_USER <4>任何类型为SUBSCRIBE的以"/user/"或"/topic/friends/"开头的消息都需要ROLE_USER <5>任何类型为MESSAGE或SUBSCRIBE的消息都会被拒绝。由于6,我们不需要这一步,但它说明了如何匹配特定的消息类型。 <6>其他消息被拒绝。这是确保您不会错过任何消息的好主意。

Spring Security还为保护WebSocket提供XML Namespace支持。 可比的基于XML的配置如下所示:

<websocket-message-broker>
    
    <intercept-message type="CONNECT" access="permitAll" />
    <intercept-message type="UNSUBSCRIBE" access="permitAll" />
    <intercept-message type="DISCONNECT" access="permitAll" />

    <intercept-message pattern="/user/queue/errors" type="SUBSCRIBE" access="permitAll" /> 
    <intercept-message pattern="/app/**" access="hasRole('USER')" />      

    
    <intercept-message pattern="/user/**" access="hasRole('USER')" />
    <intercept-message pattern="/topic/friends/*" access="hasRole('USER')" />

    
    <intercept-message type="MESSAGE" access="denyAll" />
    <intercept-message type="SUBSCRIBE" access="denyAll" />

    <intercept-message pattern="/**" access="denyAll" /> 
</websocket-message-broker>

这将确保:

<1>类型为CONNECT,UNSUBSCRIBE或DISCONNECT的任何消息都需要用户进行身份验证 <2>任何人都可以订阅/ user / queue / errors <3>具有以"/app/"开头的目标的任何消息都将要求用户拥有角色ROLE_USER <4>任何类型为SUBSCRIBE的以"/user/"或"/topic/friends/"开头的消息都需要ROLE_USER <5>任何类型为MESSAGE或SUBSCRIBE的消息都会被拒绝。由于6,我们不需要这一步,但它说明了如何匹配特定的消息类型。 <6>任何其他带有目的地的邮件都会被拒绝。这是确保您不会错过任何消息的好主意。

WebSocket授权说明

为了正确保护你的应用程序,理解Spring的WebSocket支持是很重要的。

消息类型的===== WebSocket授权

了解SUBSCRIBE和MESSAGE消息类型之间的区别以及它在Spring中的工作方式非常重要。

考虑一个聊天应用程序。

  • 系统可以通过"/topic/system/notifications"的目的地向所有用户发送通知MESSAGE

  • 客户可以通过SUBSCRIBE向"/topic/system/notifications"收到通知。

虽然我们希望客户能够订阅"/topic/system/notifications",但我们不希望让他们向该目的地发送MESSAGE。 如果我们允许发送MESSAGE到"/topic/system/notifications",则客户端可以直接向该端点发送消息并模拟系统。

一般而言,应用程序通常会拒绝任何发送到以 经纪人前缀开头的消息(即"/topic/"或"/queue/")的消息。

目的地上的===== WebSocket授权

了解目的地如何转变也很重要。

考虑一个聊天应用程序。

  • 用户可以通过向"/app/chat"的目的地发送消息来向特定用户发送消息。

  • 应用程序看到该消息,确保将"from"属性指定为当前用户(我们不能信任客户端)。

  • 然后,应用程序使用SimpMessageSendingOperations.convertAndSendToUser("toUser", "/queue/messages", message)将消息发送给收件人。

  • 邮件转到"/queue/user/messages-<sessionid>"的目的地

通过上面的应用程序,我们希望允许我们的客户端收听转换为"/queue/user/messages-<sessionid>"的{​​{0}}。 但是,我们不希望客户端能够收听"/queue/*",因为这可以让客户端看到每个用户的消息。

一般来说,应用程序通常拒绝发送给以 经纪人前缀开头的消息(即"/topic/"或"/queue/")的任何SUBSCRIBE。 当然,我们可能会提供例外来解释诸如此类的事情

出站邮件

Spring包含标题为 消息流的部分,它描述了消息如何流经系统。 值得注意的是,Spring Security只保护clientInboundChannel。 Spring Security不会尝试保护clientOutboundChannel

最重要的原因是性能。 对于每一条消息,通常会有更多消息传出。 我们鼓励确保订阅端点,而不是保护出站消息。

实施相同的原产地政策

强调浏览器不强制WebSocket连接的 同源政策是非常重要的。 这是一个非常重要的考虑因素。

为什么同源?

考虑以下情况。 用户访问bank.com并向其帐户进行身份验证。 同一用户在其浏览器中打开另一个选项卡并访问evil.com。 同源策略确保evil.com无法读取或写入bank.com数据。

使用WebSockets相同的来源策略不适用。 事实上,除非bank.com明确禁止它,否则evil.com可以代表用户读取和写入数据。 这意味着用户可以通过webSocket进行任何操作(即转账资金),evil.com可以代表该用户进行操作。

由于SockJS试图模拟WebSockets,它也绕过了同源策略。 这意味着开发人员在使用SockJS时需要明确地保护他们的应用程序免受外部域的攻击

Spring WebSocket允许的起源

幸运的是,自Spring 4.1.5以来,Spring的WebSocket和SockJS支持限制了对 当前域的访问。 Spring Security增加了额外的保护层来提供 深度防守

将CSRF添加到Stomp标题

默认情况下,Spring Security需要任何CONNECT消息类型中的CSRF token。 这确保只有可访问CSRF令牌的站点才能连接。 由于只有Same Origin可以访问CSRF令牌,因此不允许外部域进行连接。

通常,我们需要将CSRF令牌包含在HTTP标头或HTTP参数中。 但是,SockJS不允许使用这些选项。 相反,我们必须在Stomp标头中包含令牌

通过访问名为_csrf的请求属性,应用程序可以obtain a CSRF token。 例如,以下将允许访问JSP中的CsrfToken

var headerName = "${_csrf.headerName}";
var token = "${_csrf.token}";

如果您使用静态HTML,则可以在REST端点上公开CsrfToken。 例如,以下内容将显示URL / csrf上的CsrfToken

@RestController
public class CsrfController {

    @RequestMapping("/csrf")
    public CsrfToken csrf(CsrfToken token) {
        return token;
    }
}

JavaScript可以对端点进行REST调用,并使用响应填充headerName和令牌。

我们现在可以在Stomp客户端中包含令牌。 例如:

...
var headers = {};
headers[headerName] = token;
stompClient.connect(headers, function(frame) {
  ...

}

在WebSockets中禁用CSRF

如果您想允许其他域访问您的网站,可以禁用Spring Security的保护。 例如,在Java配置中,您可以使用以下内容:

@Configuration
public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {

    ...

    @Override
    protected boolean sameOriginDisabled() {
        return true;
    }
}

使用SockJS

SockJS提供后备传输以支持旧版浏览器。 在使用后备选项时,我们需要放松一些安全约束,以允许SockJS与Spring Security合作。

SockJS和框架选项

SockJS可以使用https://github.com/sockjs/sockjs-client/tree/v0.3.4[transport利用iframe]。 默认情况下,Spring Security将deny站点框起来以防止点击劫持攻击。 为了允许基于SockJS框架的传输工作,我们需要配置Spring Security以允许相同的源来构造内容。

您可以使用frame-options元素自定义X-Frame-Options。 例如,以下内容将指示Spring Security使用允许同一域内的iframe的"X-Frame-Options: SAMEORIGIN":

<http>
    <!-- ... -->

    <headers>
        <frame-options
          policy="SAMEORIGIN" />
    </headers>
</http>

同样,您可以使用以下方法自定义框架选项以在Java配置中使用相同的源:

@EnableWebSecurity
public class WebSecurityConfig extends
   WebSecurityConfigurerAdapter {

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http
      // ...
      .headers()
        .frameOptions()
            .sameOrigin();
  }
}

SockJS&Relaxing CSRF

对于任何基于HTTP的传输,SockJS在CONNECT消息上使用POST。 通常,我们需要将CSRF令牌包含在HTTP标头或HTTP参数中。 但是,SockJS不允许使用这些选项。 相反,我们必须按照将CSRF添加到Stomp标题中所述将标记包含在Stomp标头中。

这也意味着我们需要通过Web层来放松我们的CSRF保护。 具体而言,我们希望为我们的连接网址禁用CSRF保护。 我们不希望为每个网址禁用CSRF保护。 否则我们的网站将容易受到CSRF攻击。

我们可以通过提供CSRF RequestMatcher轻松实现此目的。 我们的Java配置使得这非常简单。 例如,如果我们的端点是"/chat",我们可以仅使用以下配置为仅以"/chat/"开头的URL禁用CSRF保护:

@Configuration
@EnableWebSecurity
public class WebSecurityConfig
    extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http
            .csrf()
                // ignore our stomp endpoints since they are protected using Stomp headers
                .ignoringAntMatchers("/chat/**")
                .and()
            .headers()
                // allow same origin to frame our site to support iframe SockJS
                .frameOptions().sameOrigin()
                .and()
            .authorizeRequests()

            ...

如果我们使用基于XML的配置,我们可以使用csrf@request-matcher-ref。 例如:

<http ...>
    <csrf request-matcher-ref="csrfMatcher"/>

    <headers>
        <frame-options policy="SAMEORIGIN"/>
    </headers>

    ...
</http>

<b:bean id="csrfMatcher"
    class="AndRequestMatcher">
    <b:constructor-arg value="#{T(org.springframework.security.web.csrf.CsrfFilter).DEFAULT_CSRF_MATCHER}"/>
    <b:constructor-arg>
        <b:bean class="org.springframework.security.web.util.matcher.NegatedRequestMatcher">
          <b:bean class="org.springframework.security.web.util.matcher.AntPathRequestMatcher">
            <b:constructor-arg value="/chat/**"/>
          </b:bean>
        </b:bean>
    </b:constructor-arg>
</b:bean>

授权

Spring Security中的高级授权功能是其受欢迎最引人注目的原因之一。 无论您选择如何进行身份验证 - 无论是使用Spring Security提供的机制和提供程序,还是与容器或其他非Spring Security身份验证机构集成 - 您都会发现授权服务可以在您的应用程序中以一致且简单的方式使用办法。

在本部分中,我们将探讨第I部分中介绍的各种AbstractSecurityInterceptor实现。 然后,我们继续探讨如何通过使用域访问控制列表来微调授权。

授权体系结构

当局

正如我们在technical overview中看到的,所有Authentication实现都存储GrantedAuthority对象列表。 这些代表已经授予校长的权力。 GrantedAuthority对象被AuthenticationManager插入到Authentication对象中,并在被授权决定时被AccessDecisionManager读取。

GrantedAuthority只有一个方法的接口:

String getAuthority();

这种方法允许 AccessDecisionManager来获得GrantedAuthority的精确String表示。 通过将表示作为String返回,大多数AccessDecisionManager都可以轻松"read" GrantedAuthority。 如果GrantedAuthority无法精确地表示为String,则认为GrantedAuthority "complex",getAuthority()必须返回null

"complex" GrantedAuthority的示例将是一个存储适用于不同客户帐号的操作和权限阈值列表的实现。 将GrantedAuthority复杂表示为String很困难,因此getAuthority()方法应返回null。 这将向任何AccessDecisionManager表明它需要专门支持GrantedAuthority实施以了解其内容。

Spring Security包含一个具体的GrantedAuthority实现SimpleGrantedAuthority。 这允许任何用户指定的String转换为GrantedAuthority。 安全体系结构中包含的所有AuthenticationProvider都使用SimpleGrantedAuthority来填充Authentication对象。

预调用处理

正如我们在Technical Overview章节中看到的那样,Spring Security提供了拦截器来控制对安全对象的访问,例如方法调用或Web请求。 关于是否允许继续调用的预调用决定由AccessDecisionManager完成。

AccessDecisionManager

AccessDecisionManagerAbstractSecurityInterceptor调用,并负责做出最终的访问控制决策。AccessDecisionManager接口包含三种方法:

void decide(Authentication authentication, Object secureObject,
	Collection<ConfigAttribute> attrs) throws AccessDeniedException;

boolean supports(ConfigAttribute attribute);

boolean supports(Class clazz);

AccessDecisionManagerdecide方法传递所需的所有相关信息,以作出授权决定。 特别是,传递安全Object使得可以检查包含在实际安全对象调用中的那些参数。 例如,我们假设安全对象是MethodInvocation。 很容易为Customer参数查询MethodInvocation,然后在AccessDecisionManager中实施某种安全逻辑,以确保允许委托人对该客户进行操作。 如果访问被拒绝,实现预计会抛出AccessDeniedException

supports(ConfigAttribute)方法在启动时由AbstractSecurityInterceptor调用,以确定AccessDecisionManager是否可以处理传递的ConfigAttribute supports(Class)方法由安全拦截器实现调用,以确保配置的AccessDecisionManager支持安全拦截器将呈现的安全对象的类型。

基于投票的AccessDecisionManager实现

虽然用户可以实现自己的AccessDecisionManager来控制授权的所有方面,但Spring Security包括基于投票的多个AccessDecisionManager实现。 [authz-access-voting]展示了相关的类。

。投票决策经理 image::images/access-decision-voting.png[]

使用这种方法,在授权决策中轮询一系列AccessDecisionVoter实现。 然后AccessDecisionManager根据对投票的评估决定是否抛出AccessDeniedException

AccessDecisionVoter界面有三种方法:

int vote(Authentication authentication, Object object, Collection<ConfigAttribute> attrs);

boolean supports(ConfigAttribute attribute);

boolean supports(Class clazz);

具体实现返回一个int,可能的值反映在AccessDecisionVoter静态字段ACCESS_ABSTAINACCESS_DENIEDACCESS_GRANTED中。 如果投票实施对授权决定没有意见,则投票实施将返回ACCESS_ABSTAIN。 如果确实有意见,则必须返回ACCESS_DENIEDACCESS_GRANTED

Spring Security提供了三个具体的AccessDecisionManager来计票。 ConsensusBased实施将根据非弃权票的共识授予或拒绝访问。 提供属性是为了在票数相等的情况下控制行为或者如果所有票都弃权。 如果收到一个或多个ACCESS_GRANTED票(即如果至少有一次赞成票,拒绝投票将被忽略),则AffirmativeBased实施将授予访问权限。 与ConsensusBased实现一样,如果所有选民都弃权,则会有一个参数控制行为。 UnanimousBased提供商期望获得一致的ACCESS_GRANTED票,以便授予访问权限,而忽略弃权。 如果有任何ACCESS_DENIED投票,它将拒绝访问。 与其他实现一样,如果所有选民都弃权,则会有一个参数来控制行为。

可以实现一个自定义的AccessDecisionManager来区别投票。 例如,来自特定AccessDecisionVoter的投票可能会获得额外的权重,而来自特定选民的拒绝投票可能会产生否决权。

的RoleVoter

Spring Security提供的最常用的AccessDecisionVoter是简单的RoleVoter,它将配置属性视为简单的角色名称,如果用户已分配角色,则投票授予访问权限。

如果任何ConfigAttribute以前缀ROLE_开头,它会投票。 如果存在GrantedAuthority(通过getAuthority()方法返回String表示)完全等于一个或多个以ConfigAttributes开头的{{}},则它将投票授予访问权限{ {4}}。 如果从ROLE_开始的任何ConfigAttribute都没有完全匹配,则RoleVoter将投票拒绝访问。 如果没有以ROLE_开头的ConfigAttribute,则投票者将弃权。

AuthenticatedVoter使用

我们隐式看到的另一个选民是AuthenticatedVoter,它可以用来区分匿名,完全认证和记住我认证的用户。 许多网站允许在记住我身份验证的情况下进行某些有限的访问,但需要用户通过登录才能确认其身份以获得完整访问权限。

当我们使用属性IS_AUTHENTICATED_ANONYMOUSLY授予匿名访问权时,此属性正在由AuthenticatedVoter处理。 有关更多信息,请参阅此类的Javadoc。

自定义选民

显然,你也可以实现一个自定义的AccessDecisionVoter,你可以把任何你想要的访问控制逻辑放在里面。 它可能特定于您的应用程序(与业务逻辑相关),也可能实现一些安全管理逻辑。 例如,您会在Spring网站上找到一个 博客文章,其中介绍了如何使用投票者实时拒绝其账户被暂停的用户访问。

调用处理后的=== 在继续进行安全对象调用之前,AccessDecisionManagerAbstractSecurityInterceptor调用,但某些应用程序需要修改安全对象调用实际返回的对象的方法。 虽然您可以轻松实现您自己的AOP关注点来实现此目的,但Spring Security提供了一个方便的钩子,它具有几个与ACL功能集成的具体实现。

[authz-after-invocation]演示Spring Security的AfterInvocationManager及其具体实现。

。调用实施后 image::images/after-invocation.png[]

与Spring Security的许多其他部分一样,AfterInvocationManager有一个具体实现AfterInvocationProviderManager,用于轮询AfterInvocationProvider的列表。 每个AfterInvocationProvider都可以修改返回对象或抛出AccessDeniedException。 事实上,多个提供者可以修改该对象,因为前一个提供者的结果被传递给列表中的下一个。

请注意,如果您使用的是AfterInvocationManager,则仍然需要允许MethodSecurityInterceptor的{​​{2}}允许进行操作的配置属性。 如果您使用的是典型的Spring Security包含的AccessDecisionManager实现,如果没有为特定的安全方法调用定义配置属性,则会导致每个AccessDecisionVoter放弃投票。 反过来,如果AccessDecisionManager属性“allowIfAllAbstainDecisions”为false,则会抛出AccessDeniedException。 您可以通过以下两种方式避免此潜在问题:(i)将“allowIfAllAbstainDecisions”设置为true(尽管通常不建议这样做),或者(ii)确保至少有一个配置属性{{2 }}将投票授予访问权限。 后者(推荐)方法通常通过ROLE_USERROLE_AUTHENTICATED配置属性来实现。

分层角色

这是一个常见的要求,即应用程序中的特定角色应该自动实现其他角色。 例如,在一个具有"admin"和"user"角色概念的应用程序中,您可能希望管理员能够完成普通用户所能做的一切。 要做到这一点,您可以确保所有管理员用户都被分配了"user"角色。 或者,您可以修改每个需要"user"角色的访问约束,以包含"admin"角色。 如果您的应用程序中有许多不同的角色,这可能会变得非常复杂。

使用角色层次结构允许您配置哪些角色(或权限)应该包括其他角色。 Spring Security的RoleVoter RoleHierarchyVoter的扩展版本配置了RoleHierarchy,从中获得用户分配的所有"reachable authorities"。 典型的配置可能如下所示:

<bean id="roleVoter" class="org.springframework.security.access.vote.RoleHierarchyVoter">
	<constructor-arg ref="roleHierarchy" />
</bean>
<bean id="roleHierarchy"
		class="org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl">
	<property name="hierarchy">
		<value>
			ROLE_ADMIN > ROLE_STAFF
			ROLE_STAFF > ROLE_USER
			ROLE_USER > ROLE_GUEST
		</value>
	</property>
</bean>

在这里,我们在层次ROLE_ADMIN ⇒ ROLE_STAFF ⇒ ROLE_USER ⇒ ROLE_GUEST中有四个角色。 使用ROLE_ADMIN进行身份验证的用户在针对使用上述RoleHierarchyVoter配置的AccessDecisionManager进行安全约束评估时,表现得好像具有全部四种角色一样。 >符号可以被认为是"includes"的含义。

角色层次结构为简化应用程序的访问控制配置数据和/或减少需要分配给用户的权限数量提供了一种便捷方式。 对于更复杂的需求,您可能希望定义应用程序需要的特定访问权限与分配给用户的角色之间的逻辑映射,并在加载用户信息时在两者之间进行转换。

安全对象实现

AOP联盟(MethodInvocation)安全拦截器

在Spring Security 2.0之前,安全MethodInvocation需要相当多的锅炉配置。 现在推荐的方法安全方法是使用namespace configuration。 这样方法安全基础结构bean就会自动为您配置,因此您并不需要了解实现类。 我们只是简单介绍一下这里涉及的类。

使用保证MethodInvocation的{​​{0}}实施的方法安全性。 根据配置方法,拦截器可能特定于单个bean或在多个bean之间共享。 拦截器使用MethodSecurityMetadataSource实例来获取适用于特定方法调用的配置属性。MapBasedMethodSecurityMetadataSource用于存储由方法名称(可以通配符)键入的配置属性,并且在应用上下文中使用<intercept-methods><protect-point>元素定义属性时将在内部使用。 其他实现将用于处理基于注释的配置。

显式MethodSecurityInterceptor配置

您当然可以在应用程序上下文中直接配置MethodSecurityIterceptor,以便与Spring AOP的代理机制一起使用:

<bean id="bankManagerSecurity" class=
	"org.springframework.security.access.intercept.aopalliance.MethodSecurityInterceptor">
<property name="authenticationManager" ref="authenticationManager"/>
<property name="accessDecisionManager" ref="accessDecisionManager"/>
<property name="afterInvocationManager" ref="afterInvocationManager"/>
<property name="securityMetadataSource">
	<sec:method-security-metadata-source>
	<sec:protect method="com.mycompany.BankManager.delete*" access="ROLE_SUPERVISOR"/>
	<sec:protect method="com.mycompany.BankManager.getBalance" access="ROLE_TELLER,ROLE_SUPERVISOR"/>
	</sec:method-security-metadata-source>
</property>
</bean>

AspectJ(JoinPoint)安全拦截器

AspectJ安全拦截器与上一节讨论的AOP联盟安全拦截器非常相似。 事实上,我们只会讨论本节的不同之处。

AspectJ拦截器被命名为AspectJSecurityInterceptor。 与依靠Spring应用程序上下文通过代理编织安全拦截器的AOP Alliance安全拦截器不同,通过AspectJ编译器编译AspectJSecurityInterceptor。 在同一个应用程序中使用两种类型的安全拦截器并不少见,AspectJSecurityInterceptor用于域对象实例安全性,而AOP联盟MethodSecurityInterceptor用于服务层安全性。

我们首先考虑如何在Spring应用程序上下文中配置AspectJSecurityInterceptor

<bean id="bankManagerSecurity" class=
	"org.springframework.security.access.intercept.aspectj.AspectJMethodSecurityInterceptor">
<property name="authenticationManager" ref="authenticationManager"/>
<property name="accessDecisionManager" ref="accessDecisionManager"/>
<property name="afterInvocationManager" ref="afterInvocationManager"/>
<property name="securityMetadataSource">
	<sec:method-security-metadata-source>
	<sec:protect method="com.mycompany.BankManager.delete*" access="ROLE_SUPERVISOR"/>
	<sec:protect method="com.mycompany.BankManager.getBalance" access="ROLE_TELLER,ROLE_SUPERVISOR"/>
	</sec:method-security-metadata-source>
</property>
</bean>

如您所见,除了课程名称外,AspectJSecurityInterceptor与AOP Alliance安全拦截器完全相同。 事实上,两个拦截器可以共享securityMetadataSource,因为SecurityMetadataSourcejava.lang.reflect.Method协同工作,而不是AOP库特定的类。 当然,您的访问决策可以访问相关的AOP库特定的调用(例如MethodInvocationJoinPoint),因此在进行访问决策时可以考虑一系列的附加标准(如方法参数) 。

接下来,您需要定义一个AspectJ aspect。 例如:

package org.springframework.security.samples.aspectj;

import org.springframework.security.access.intercept.aspectj.AspectJSecurityInterceptor;
import org.springframework.security.access.intercept.aspectj.AspectJCallback;
import org.springframework.beans.factory.InitializingBean;

public aspect DomainObjectInstanceSecurityAspect implements InitializingBean {

	private AspectJSecurityInterceptor securityInterceptor;

	pointcut domainObjectInstanceExecution(): target(PersistableEntity)
		&& execution(public * *(..)) && !within(DomainObjectInstanceSecurityAspect);

	Object around(): domainObjectInstanceExecution() {
		if (this.securityInterceptor == null) {
			return proceed();
		}

		AspectJCallback callback = new AspectJCallback() {
			public Object proceedWithObject() {
				return proceed();
			}
		};

		return this.securityInterceptor.invoke(thisJoinPoint, callback);
	}

	public AspectJSecurityInterceptor getSecurityInterceptor() {
		return securityInterceptor;
	}

	public void setSecurityInterceptor(AspectJSecurityInterceptor securityInterceptor) {
		this.securityInterceptor = securityInterceptor;
	}

	public void afterPropertiesSet() throws Exception {
		if (this.securityInterceptor == null)
			throw new IllegalArgumentException("securityInterceptor required");
		}
	}
}

在上面的示例中,安全拦截器将应用于PersistableEntity的每个实例,这是一个未显示的抽象类(您可以使用任何其他类或pointcut表达式)。 对于那些好奇的人来说,需要AspectJCallback,因为proceed();语句仅在around()正文中具有特殊含义。 AspectJSecurityInterceptor在需要目标对象继续时调用这个匿名AspectJCallback类。

您将需要配置Spring以加载该方面并将其与AspectJSecurityInterceptor连接起来。 下面显示了实现这一点的bean声明:

<bean id="domainObjectInstanceSecurityAspect"
	class="security.samples.aspectj.DomainObjectInstanceSecurityAspect"
	factory-method="aspectOf">
<property name="securityInterceptor" ref="bankManagerSecurity"/>
</bean>

而已! 现在,您可以使用您认为合适的任何方式(例如new Person();)从应用程序中的任何位置创建bean,并且它们将应用安全拦截器。

基于表达式的访问控制

Spring Security 3.0引入了使用Spring EL表达式作为授权机制的能力,以及简单使用以前见过的配置属性和访问决策选项。 基于表达式的访问控制建立在相同的体系结构上,但允许将复杂的布尔逻辑封装在单个表达式中。

概述

Spring Security使用Spring EL进行表达式支持,如果您有兴趣更深入地理解该主题,您应该看看它是如何工作的。 作为评估上下文的一部分,表达式使用"root object"进行评估。 Spring Security使用Web和方法安全性的特定类作为根对象,以便提供内置表达式和对当前主体等值的访问。

常用内置表达式

表达式根对象的基类是SecurityExpressionRoot。 这提供了可用于Web和方法安全性的一些常用表达式。

。常见的内置表达式

表达式描述

hasRole([role])

如果当前主体具有指定的角色,则返回true。 默认情况下,如果提供的角色不以“ROLE_”开头,它将被添加。 这可以通过修改DefaultWebSecurityExpressionHandler上的defaultRolePrefix进行自定义。

hasAnyRole([role1,role2])

如果当前主体具有任何提供的角色(作为逗号分隔的字符串列表),则返回true。 默认情况下,如果提供的角色不以“ROLE_”开头,它将被添加。 这可以通过修改DefaultWebSecurityExpressionHandler上的defaultRolePrefix进行自定义。

hasAuthority([authority])

如果当前委托人具有指定的权限,则返回true

hasAnyAuthority([authority1,authority2])

如果当前主体具有任何提供的角色(作为逗号分隔的字符串列表),则返回true;

principal

允许直接访问表示当前用户的主体对象

authentication

允许直接访问从SecurityContext获取的当前Authentication对象

permitAll

始终评估为true

denyAll

始终评估为false

isAnonymous()

如果当前主体是匿名用户,则返回true

isRememberMe()

如果当前主体是记事本用户,则返回true

isAuthenticated()

如果用户不是匿名的,则返回true

isFullyAuthenticated()

如果用户不是匿名用户或记住我用户,则返回true

hasPermission(Object target, Object permission)

如果用户有权访问为给定权限提供的目标,则返回true。 例如,hasPermission(domainObject, 'read')

hasPermission(Object targetId, String targetType, Object permission)

如果用户有权访问为给定权限提供的目标,则返回true。 例如,hasPermission(1, 'com.example.domain.Message', 'read')

网络安全表达式

要使用表达式保护单个网址,您首先需要将<http>元素中的use-expressions属性设置为true。 然后,Spring Security将期望<intercept-url>元素的access属性包含Spring EL表达式。 表达式应评估为布尔值,定义是否允许访问。 例如:

<http>
	<intercept-url pattern="/admin*"
		access="hasRole('admin') and hasIpAddress('192.168.1.0/24')"/>
	...
</http>

这里我们已经定义应用程序的"admin"区域(由URL模式定义)应该只对拥有授予权限"admin"并且其IP地址与本地子网匹配的用户可用。 我们已经在上一节中看到了内置的hasRole表达式。 表达式hasIpAddress是特定于Web安全性的附加内置表达式。 它由WebSecurityExpressionRoot类定义,其中的一个实例在评估Web访问表达式时用作表达式根对象。 该对象还直接公开名为request的{​​{0}}对象,以便您可以直接在表达式中调用请求。 如果正在使用表达式,则WebExpressionVoter将被添加到名称空间所使用的AccessDecisionManager。 因此,如果您不使用名称空间并想使用表达式,则必须将其中一个添加到您的配置中。

在Web安全表达式中引用Beans

如果你想扩展可用的表达式,你可以很容易地引用你公开的任何Spring Bean。 例如,假设您的名称为webSecurity的Bean包含以下方法签名:

public class WebSecurity {
		public boolean check(Authentication authentication, HttpServletRequest request) {
				...
		}
}

你可以参考使用的方法:

<http>
	<intercept-url pattern="/user/**"
		access="@webSecurity.check(authentication,request)"/>
	...
</http>

或者在Java配置中

http
		.authorizeRequests()
				.antMatchers("/user/**").access("@webSecurity.check(authentication,request)")
				...

Web安全表达式中的==== 路径变量

有时能够在URL中引用路径变量是很好的。 例如,考虑一个REST式应用程序,该应用程序通过URL路径以/user/{userId}的格式通过id查找用户。

通过将其放入模式中,您可以轻松地引用路径变量。 例如,如果您有名为webSecurity的Bean包含以下方法签名:

public class WebSecurity {
		public boolean checkUserId(Authentication authentication, int id) {
				...
		}
}

你可以参考使用的方法:

<http>
	<intercept-url pattern="/user/{userId}/**"
		access="@webSecurity.checkUserId(authentication,#userId)"/>
	...
</http>

或者在Java配置中

http
		.authorizeRequests()
				.antMatchers("/user/{userId}/**").access("@webSecurity.checkUserId(authentication,#userId)")
				...

在这两种配置中,匹配的URL将传入路径变量(并将其转换为checkUserId方法)。 例如,如果网址为/user/123/resource,则传入的ID为123

方法安全表达式

方法安全性比简单的允许或拒绝规则复杂一点。 Spring Security 3.0引入了一些新的注释,以便全面支持表达式的使用。

@ Pre和@Post注释

有四个注释支持表达式属性以允许调用前和调用后授权检查,并支持对提交的集合参数或返回值进行过滤。 他们是@PreAuthorize@PreFilter@PostAuthorize@PostFilter。 通过global-method-security命名空间元素启用它们的使用:

<global-method-security pre-post-annotations="enabled"/>

使用@PreAuthorize和@PostAuthorize的===== 访问控制 最明显有用的注释是@PreAuthorize,它决定一个方法是否可以被实际调用。 例如(来自"Contacts"示例应用程序)

@PreAuthorize("hasRole('USER')")
public void create(Contact contact);

这意味着只有具有角色"ROLE_USER"的用户才能访问。 显然,使用传统配置和简单配置属性来实现所需角色可以轻松实现同样的目的。 但是关于:

@PreAuthorize("hasPermission(#contact, 'admin')")
public void deletePermission(Contact contact, Sid recipient, Permission permission);

这里我们实际上使用方法参数作为表达式的一部分来决定当前用户是否具有给定联系人的"admin"权限。 内置的hasPermission()表达式通过应用程序上下文链接到Spring Security ACL模块中,因为我们将see below。 您可以按名称访问任何方法参数作为表达式变量。

Spring Security可以通过多种方式来解决方法参数。 Spring Security使用DefaultSecurityParameterNameDiscoverer来发现参数名称。 默认情况下,对于整个方法尝试以下选项。

  • 如果Spring Security的@P注释出现在该方法的单个参数上,则会使用该值。 这对于在JDK 8之前使用JDK编译的接口非常有用,它不包含有关参数名称的任何信息。 例如:

    import org.springframework.security.access.method.P;
    
    ...
    
    @PreAuthorize("#c.name == authentication.name")
    public void doSomething(@P("c") Contact contact);

    在幕后,使用AnnotationParameterNameDiscoverer实现的这种使用可以被自定义为支持任何指定注释的value属性。

  • 如果Spring Data的@Param注释至少存在于该方法的一个参数中,则将使用该值。 这对于在JDK 8之前使用JDK编译的接口非常有用,它不包含有关参数名称的任何信息。 例如:

    import org.springframework.data.repository.query.Param;
    
    ...
    
    @PreAuthorize("#n == authentication.name")
    Contact findContactByName(@Param("n") String name);

    在幕后,使用AnnotationParameterNameDiscoverer实现的这种使用可以被自定义为支持任何指定注释的value属性。

  • 如果使用JDK 8使用-parameters参数编译源文件,并且正在使用Spring 4+,则使用标准的JDK反射API来发现参数名称。 这适用于类和接口。

  • 最后,如果代码是使用调试符号编译的,则参数名称将使用调试符号发现。 这对接口不起作用,因为它们没有关于参数名称的调试信息。 对于接口,必须使用注释或JDK 8方法。

[[EL-预后注释-SPEL]]

在表达式中可以使用任何Spring-EL功能,因此您也可以访问参数的属性。 例如,如果您希望特定的方法只允许访问其用户名与联系人相匹配的用户,则可以编写

@PreAuthorize("#contact.name == authentication.name")
public void doSomething(Contact contact);

在这里,我们正在访问另一个内置表达式authentication,它是存储在安全上下文中的Authentication。 您还可以使用表达式principal直接访问其"principal"属性。 该值通常是UserDetails实例,因此您可以使用表达式principal.usernameprincipal.enabled

[[EL-预后注释-交]]

通常情况下,您可能希望在调用该方法后执行访问控制检查。 这可以使用@PostAuthorize注释来实现。 要从方法访问返回值,请使用表达式中的内置名称returnObject

使用@PreFilter和@PostFilter进行过滤

正如您可能已经知道的那样,Spring Security支持对集合和数组进行过滤,现在可以使用表达式来实现这一点。 这通常是对方法的返回值执行的。 例如:

@PreAuthorize("hasRole('USER')")
@PostFilter("hasPermission(filterObject, 'read') or hasPermission(filterObject, 'admin')")
public List<Contact> getAll();

当使用@PostFilter注释时,Spring Security遍历返回的集合并移除提供的表达式为false的所有元素。 名称filterObject引用集合中的当前对象。 您也可以在方法调用之前使用@PreFilter进行过滤,尽管这是一种不太常见的要求。 语法相同,但如果有多个参数是集合类型,则必须使用此批注的filterTarget属性通过名称选择一个参数。

请注意,过滤显然不能替代您的数据检索查询。 如果您要过滤大量集合并删除很多条目,那么这可能效率不高。

内置表达式

有一些特定于方法安全的内置表达式,我们已经在上面看到了这些内置表达式。 filterTargetreturnValue值非常简单,但使用hasPermission()表达式需要仔细观察。

PermissionEvaluator接口

hasPermission()表达式被委派给PermissionEvaluator的一个实例。 它旨在桥接表达式系统和Spring Security的ACL系统,允许您根据抽象权限指定域对象的授权约束。 它对ACL模块没有明确的依赖关系,所以如果需要的话,你可以将它交换出来用于替代实现。 界面有两种方法:

boolean hasPermission(Authentication authentication, Object targetDomainObject,
							Object permission);

boolean hasPermission(Authentication authentication, Serializable targetId,
							String targetType, Object permission);

它直接映射到表达式的可用版本,但不提供第一个参数(Authentication对象)。 第一种用于已经加载访问控制的域对象的情况。 然后,如果当前用户具有该对象的给定权限,表达式将返回true。 第二个版本用于未加载对象但标识符已知的情况。 还需要域对象的抽象"type"说明符,以允许加载正确的ACL权限。 传统上,这是对象的Java类,但不一定要与加载权限的方式一致。

要使用hasPermission()表达式,您必须在应用程序上下文中显式配置PermissionEvaluator。 这看起来像这样:

<security:global-method-security pre-post-annotations="enabled">
<security:expression-handler ref="expressionHandler"/>
</security:global-method-security>

<bean id="expressionHandler" class=
"org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler">
	<property name="permissionEvaluator" ref="myPermissionEvaluator"/>
</bean>

myPermissionEvaluator是实现PermissionEvaluator的bean。 通常这将是来自名为AclPermissionEvaluator的ACL模块的实现。 有关更多详细信息,请参阅"Contacts"示例应用程序配置。

方法安全元注释

您可以使用元注释来进行方法安全性,以使您的代码更具可读性。 如果您发现在整个代码库中重复相同的复杂表达式,这一点尤其方便。 例如,请考虑以下几点:

@PreAuthorize("#contact.name == authentication.name")

我们可以创建一个可用来代替的元注释,而不是随处重复。

@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("#contact.name == authentication.name")
public @interface ContactPermission {}

元注释可用于任何Spring Security方法安全注释。 为了保持与规范兼容,JSR-250注释不支持元注释。

其他主题= 在本部分中,我们将介绍需要了解以前章节的功能以及框架的一些更先进和较少使用的功能。

域对象安全性(ACL)

概述

复杂的应用程序通常会发现需要定义访问权限,而不仅仅是在Web请求或方法调用级别。 相反,安全决策需要包括谁(Authentication),哪里(MethodInvocation)和什么(SomeDomainObject)。 换句话说,授权决策还需要考虑方法调用的实际域对象实例主题。

想象一下,你正在为宠物诊所设计一个应用程序。 您的基于Spring的应用程序将有两个主要用户组:宠物诊所的员工以及宠物诊所的客户。 员工可以访问所有数据,而客户只能看到他们自己的客户记录。 为了让它更有趣,您的客户可以允许其他用户查看他们的客户记录,例如他们的"puppy preschool"指导或本地"Pony Club"的总裁。 使用Spring Security作为基础,您可以使用以下几种方法:

  • 编写您的业务方法以强化安全性。 您可以咨询Customer域对象实例中的集合以确定哪些用户有权访问。 通过使用SecurityContextHolder.getContext().getAuthentication(),您可以访问Authentication对象。

  • 编写一个AccessDecisionVoter以强制存储在Authentication对象中的GrantedAuthority[]的安全性。 这意味着您的AuthenticationManager需要使用代表主体有权访问的每个Customer域对象实例的自定义GrantedAuthority[]填充Authentication

  • 编写一个AccessDecisionVoter来强制执行安全性,并直接打开目标Customer域对象。 这意味着你的选民需要访问一个DAO,它允许它检索Customer对象。 然后,它将访问Customer对象的已批准用户集合并作出适当的决定。

这些方法中的每一种都是完全合法的。 但是,第一次将您的授权检查与您的业务代码耦合在一起。 其中的主要问题包括单元测试的难度增加以及在其他地方重新使用Customer授权逻辑更加困难。 从Authentication对象获取GrantedAuthority[]也很好,但不会扩展到大量的Customer。 如果用户可能能够访问5,000个Customer(在这种情况下不太可能,但想象一下,如果它是大型Pony Club的受欢迎兽医!)所需的内存消耗量以及构建{{1 }}对象将是不可取的。 最后的方法,直接从外部代码打开Customer,可能是三者中最好的。 它实现了关注点的分离,并且不会滥用内存或CPU周期,但它仍然效率低下,AccessDecisionVoter和最终的业务方法本身都会执行对DAO的调用,负责检索{{1 }}对象。 每个方法调用两次访问显然是不可取的。 另外,列出的每种方法都需要从头开始编写自己的访问控制列表(ACL)持久性和业务逻辑。

幸运的是,还有另一种选择,我们将在下面讨论。

重要概念

Spring Security的ACL服务在spring-security-acl-xxx.jar中提供。 您需要将此JAR添加到您的类路径中以使用Spring Security的域对象实例安全功能。

Spring Security的域对象实例安全功能以访问控制列表(ACL)的概念为中心。 系统中的每个域对象实例都有自己的ACL,ACL记录了谁可以使用该域对象的详细信息。 考虑到这一点,Spring Security为您的应用程序提供了三个与ACL相关的主要功能:

  • 有效检索所有域对象的ACL条目(并修改这些ACL)的方法

  • 在调用方法之前,确保给定委托人被允许与您的对象一起工作

  • 确保给定主体的方法可以在调用方法后与您的对象(或返回的对象)一起工作

正如第一个要点所指出的,Spring Security ACL模块的主要功能之一是提供了一种检索ACL的高性能方法。 此ACL存储库功能非常重要,因为系统中的每个域对象实例都可能有多个访问控制条目,并且每个ACL可能以树状结构从其他ACL继承(这由Spring支持的开箱即用安全性,并且非常常用)。 Spring Security的ACL功能经过精心设计,可提供ACL的高性能检索,以及可插拔缓存,死锁最小化数据库更新,独立于ORM框架(我们直接使用JDBC),适当的封装和透明的数据库更新。

鉴于数据库是ACL模块操作的核心,我们来研究实现中默认使用的四个主表。 下表按照典型的Spring Security ACL部署中的大小顺序列出,其中最后一行列出的表格最多:

  • ACL_SID允许我们唯一标识系统中的任何主体或权限("SID"代表"security identity")。 唯一的列是ID,SID的文本表示,以及用于指示文本表示是指主体名称还是GrantedAuthority的标志。 因此,每个唯一主体或GrantedAuthority都有一行。 当在接收权限的上下文中使用时,SID通常称为"recipient"。

  • ACL_CLASS允许我们唯一标识系统中的任何域对象类。 唯一的列是ID和Java类名。 因此,对于我们希望存储ACL权限的每个唯一类,都有一行。

  • ACL_OBJECT_IDENTITY存储系统中每个唯一域对象实例的信息。 列包括ID,ACL_CLASS表的外键,唯一标识符,因此我们知道哪些ACL_CLASS实例正在提供信息,父项,ACL_SID表的外键以表示域对象实例的所有者,以及我们是否允许ACL条目从任何父级ACL继承。 对于我们存储ACL权限的每个域对象实例,我们都有一行。

  • 最后,ACL_ENTRY存储分配给每个收件人的个人权限。 列包括ACL_OBJECT_IDENTITY的外键,收件人(即ACL_SID的外键),是否将进行审计以及表示实际权限被授予或拒绝的整数位掩码。 对于每个收到与域对象一起使用权限的收件人,我们都有一行。

如最后一段所述,ACL系统使用整数位掩码。 不用担心,您不需要意识到使用ACL系统时移位的更多细节,但足以说我们有32位可以打开或关闭。 这些位中的每一个都表示权限,默认情况下会读取权限(位0),写入(位1),创建(位2),删除(位3)和管理权(位4)。 如果您希望使用其他权限,则很容易实现您自己的Permission实例,而ACL框架的其余部分将在不知道您的扩展的情况下运行。

理解系统中域对象的数量与我们选择使用整数位掩码的事实完全没有关系。 虽然您有32位可用于权限,但您可能拥有数十亿个域对象实例(这将意味着ACL_OBJECT_IDENTITY中的数十亿行,很可能是ACL_ENTRY)。 我们提出这一点是因为我们发现,有时候人们错误地认为他们需要一点点为每个潜在的领域对象,情况并非如此。

现在我们已经提供了ACL系统的基本概述,以及它在表结构中的外观,让我们来探索关键接口。 关键接口是:

  • Acl:每个域对象都有且仅有一个Acl对象,它在内部保存AccessControlEntry,并且知道Acl的所有者。 Acl不直接引用域对象,而是引用ObjectIdentity Acl存储在ACL_OBJECT_IDENTITY表中。

  • AccessControlEntryAcl包含多个AccessControlEntry,在框架中通常缩写为ACE。 每个ACE指的是PermissionSidAcl的特定元组。 ACE也可以是授予或不授予,并包含审计设置。 ACE存储在ACL_ENTRY表中。

  • Permission:许可表示特定的不可变位掩码,并为位掩码和输出信息提供便利功能。 上面介绍的基本权限(比特0到4)包含在BasePermission类中。

  • Sid:ACL模块需要引用主体和GrantedAuthority[]。 间接级别由Sid接口提供,该接口是"security identity"的缩写。 常用的类包括PrincipalSid(用于表示Authentication对象内的主体)和GrantedAuthoritySid。 安全标识信息存储在ACL_SID表中​​。

  • ObjectIdentity:每个域对象在ACL模块内由ObjectIdentity表示。 默认实现称为ObjectIdentityImpl

  • AclService:检索适用于给定ObjectIdentityAcl。 在包含的实现(JdbcAclService)中,检索操作被委托给LookupStrategy LookupStrategy为检索ACL信息提供了一种高度优化的策略,使用批量检索(BasicLookupStrategy),并支持利用实体化视图,分层查询和类似以性能为中心的非ANSI SQL功能的自定义实现。

  • MutableAclService:允许修改Acl以提供持久性。 如果你不希望的话,使用这个接口并不重要。

请注意,我们开箱即用的AclService和相关的数据库类都使用ANSI SQL。 因此这应该适用于所有主要数据库。 在撰写本文时,系统已经成功通过Hypersonic SQL,PostgreSQL,Microsoft SQL Server和Oracle测试。

Spring Security附带两个样本,演示ACL模块。 第一个是联系人示例,另一个是文档管理系统(DMS)示例。 我们建议看看这些例子。

入门

要开始使用Spring Security的ACL功能,您需要将ACL信息存储在某处。 这需要使用Spring来实例化DataSource。 然后将DataSource注入到JdbcMutableAclServiceBasicLookupStrategy实例中。 后者提供了高性能的ACL检索功能,前者提供了增强功能。 有关示例配置,请参阅Spring Security附带的示例之一。 您还需要使用最后一节中列出的四个ACL特定的表来填充数据库(有关适当的SQL语句,请参阅ACL示例)。

一旦创建了所需的模式并实例化JdbcMutableAclService,接下来需要确保您的域模型支持与Spring Security ACL包的互操作性。 希望ObjectIdentityImpl将被证明是足够的,因为它提供了很多方法来使用它。 大多数人将拥有包含public Serializable getId()方法的域对象。 如果返回类型很长,或者与长整型(例如int)兼容,您会发现不需要进一步考虑ObjectIdentity问题。 ACL模块的很多部分都依赖于长标识符。 如果你不使用long(或int,byte等),那么你很有可能需要重新实现一些类。 我们不打算在Spring Security的ACL模块中支持非长标识符,因为longs已经与所有数据库序列(最常见的标识符数据类型)兼容,并且具有足够的长度以容纳所有常见的使用场景。

以下代码片段显示了如何创建Acl或修改现有的Acl

// Prepare the information we'd like in our access control entry (ACE)
ObjectIdentity oi = new ObjectIdentityImpl(Foo.class, new Long(44));
Sid sid = new PrincipalSid("Samantha");
Permission p = BasePermission.ADMINISTRATION;

// Create or update the relevant ACL
MutableAcl acl = null;
try {
acl = (MutableAcl) aclService.readAclById(oi);
} catch (NotFoundException nfe) {
acl = aclService.createAcl(oi);
}

// Now grant some permissions via an access control entry (ACE)
acl.insertAce(acl.getEntries().length, p, sid, true);
aclService.updateAcl(acl);

在上面的示例中,我们检索与标识号为44的"Foo"域对象关联的ACL。 然后,我们添加一个ACE,使得名为"Samantha"的主体可以"administer"该对象。 代码片段相对不言自明,除了insertAce方法外。 insertAce方法的第一个参数是确定Acl中的哪个位置将插入新条目。 在上面的示例中,我们只是将新ACE放在现有ACE的末尾。 最后一个参数是布尔值,表示ACE是否授予或拒绝。 大多数时候它会授予(true),但是如果拒绝(false),则权限将被有效阻止。

作为DAO或存储库操作的一部分,Spring Security不提供任何特殊集成来自动创建,更新或删除ACL。 相反,你将需要为你的单个域对象编写如上所示的代码。 值得考虑在服务层使用AOP来自动将ACL信息与服务层操作集成。 过去我们发现这是一个非常有效的方法。

一旦您使用上述技术将一些ACL信息存储在数据库中,下一步就是将ACL信息实际用作授权决策逻辑的一部分。 你在这里有很多选择。 您可以编写自己的AccessDecisionVoterAfterInvocationProvider,分别在方法调用之前或之后触发。 这些类将使用AclService来检索相关的ACL,然后调用Acl.isGranted(Permission[] permission, Sid[] sids, boolean administrativeMode)来决定是否授予或拒绝权限。 或者,您可以使用我们的AclEntryVoterAclEntryAfterInvocationProviderAclEntryAfterInvocationCollectionFilteringProvider类。 所有这些类都提供了一种基于声明的方法来在运行时评估ACL信息,从而不需要编写任何代码。 请参阅示例应用程序以了解如何使用这些类。

预认证方案

在某些情况下,您希望使用Spring Security进行授权,但在访问应用程序之前,用户已被某个外部系统可靠地认证。 我们将这些情况称为"pre-authenticated"情景。 示例包括X.509,Siteminder和运行该应用程序的Java EE容器的身份验证。 在使用预认证时,Spring Security必须

  • 识别发出请求的用户。

  • 获取用户权限。

细节将取决于外部认证机制。 在X.509的情况下,用户可以通过他们的证书信息来标识,或者在Siteminder的情况下通过HTTP请求标头来标识用户。 如果依赖容器身份验证,则将通过调用传入HTTP请求上的getUserPrincipal()方法来识别用户。 在某些情况下,外部机制可能会为用户提供角色/权限信息,但在其他情况下,必须从单独的来源获取权限,例如UserDetailsService

预认证框架类

由于大多数预认证机制遵循相同的模式,因此Spring Security具有一组类,它们为实现预认证的认证提供程序提供了一个内部框架。 这消除了重复,并允许以结构化的方式添加新的实现,而无需从头开始编写所有内容。 如果您想使用X.509 authentication之类的东西,则不需要了解这些类,因为它已经有一个名称空间配置选项,该选项使用起来更简单并且易于使用。 如果您需要使用显式的bean配置或正在计划编写自己的实现,那么了解提供的实现如何工作将会很有用。 您会在org.springframework.security.web.authentication.preauth下找到课程。 我们只是在这里提供一个大纲,所以你应该在适当的地方咨询Javadoc和源代码。

AbstractPreAuthenticatedProcessingFilter

该类将检查安全上下文的当前内容,如果为空,它将尝试从HTTP请求中提取用户信息并将其提交给AuthenticationManager。 子类覆盖以下方法来获取此信息:

protected abstract Object getPreAuthenticatedPrincipal(HttpServletRequest request);

protected abstract Object getPreAuthenticatedCredentials(HttpServletRequest request);

调用这些后,过滤器将创建一个包含返回数据的PreAuthenticatedAuthenticationToken并提交它进行验证。 在"authentication"这里,我们实际上只是意味着可能会加载用户权限的进一步处理,但遵循标准的Spring Security身份验证体系结构。

与其他Spring Security身份验证过滤器一样,身份验证前过滤器也具有authenticationDetailsSource属性,默认情况下,该属性将创建一个WebAuthenticationDetails对象以将其他信息(如会话标识符和始发IP地址)存储在{{ 2}} Authentication对象的属性。 在可以从预认证机制获取用户角色信息的情况下,数据也存储在此属性中,并且具体实现了GrantedAuthoritiesContainer接口。 这使验证提供者能够读取外部分配给用户的权限。 接下来我们将看一个具体的例子。

J2eeBasedPreAuthenticatedWebAuthenticationDetailsS​​ource

如果过滤器配置了一个作为该类实例的authenticationDetailsSource,则通过为每个预定义的"mappable roles"调用isUserInRole(String role)方法来获得权限信息。 该类从配置的MappableAttributesRetriever中获取这些信息。 可能的实现包括在应用程序上下文中对列表进行硬编码,并从web.xml文件中的<security-role>信息中读取角色信息。 预认证示例应用程序使用后一种方法。

还有一个阶段使用配置的Attributes2GrantedAuthoritiesMapper将角色(或属性)映射到Spring Security GrantedAuthority对象。 默认情况下,只会为名称添加通常的ROLE_前缀,但它可以让您完全控制行为。

PreAuthenticatedAuthenticationProvider

预先认证的提供者要比为用户加载UserDetails对象做更多的事情。 它通过委托给AuthenticationUserDetailsService来完成。 后者与标准UserDetailsService类似,但采用Authentication对象而不仅仅是用户名:

public interface AuthenticationUserDetailsService {
	UserDetails loadUserDetails(Authentication token) throws UsernameNotFoundException;
}

此接口也可能有其他用途,但通过预认证,它允许访问打包在Authentication对象中的权限,如我们在前一节中看到的那样。 PreAuthenticatedGrantedAuthoritiesUserDetailsService类执行此操作。 或者,它可以通过UserDetailsByNameServiceWrapper实施委托给标准UserDetailsService

Http403ForbiddenEntryPoint

technical overview一章讨论了AuthenticationEntryPoint。 通常,它负责启动未经身份验证的用户(当他们尝试访问受保护的资源时)的身份验证过程,但在预先验证的情况下,这不适用。 如果您未将预认证与其他认证机制结合使用,那么您只会将ExceptionTranslationFilter配置为具有此类的实例。 如果用户被AbstractPreAuthenticatedProcessingFilter拒绝,则会被调用,从而导致空身份验证。 如果被调用,它总是返回一个403 - 禁止的响应代码。

具体实现

own chapter中涵盖了X.509认证。 在这里,我们将看看一些为其他预认证方案提供支持的类。

请求标头认证(Siteminder)

外部认证系统可以通过在HTTP请求上设置特定标头来向应用程序提供信息。 一个众所周知的例子是Siteminder,它在名为SM_USER的标头中传递用户名。 该机制由RequestHeaderAuthenticationFilter类简单地从头中提取用户名来支持。 它默认使用名称SM_USER作为标题名称。 查看Javadoc了解更多详情。

 

请注意,使用这样的系统时,框架完全不执行身份验证检查,而且extremely非常重要,因为外部系统已正确配置并保护对应用程序的所有访问。 如果攻击者能够在未检测到原始请求的情况下伪造标头,那么他们可能会选择他们希望的任何用户名。

Siteminder示例配置

使用此过滤器的典型配置如下所示:

<security:http>
<!-- Additional http configuration omitted -->
<security:custom-filter position="PRE_AUTH_FILTER" ref="siteminderFilter" />
</security:http>

<bean id="siteminderFilter" class="org.springframework.security.web.authentication.preauth.RequestHeaderAuthenticationFilter">
<property name="principalRequestHeader" value="SM_USER"/>
<property name="authenticationManager" ref="authenticationManager" />
</bean>

<bean id="preauthAuthProvider" class="org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider">
<property name="preAuthenticatedUserDetailsService">
	<bean id="userDetailsServiceWrapper"
		class="org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper">
	<property name="userDetailsService" ref="userDetailsService"/>
	</bean>
</property>
</bean>

<security:authentication-manager alias="authenticationManager">
<security:authentication-provider ref="preauthAuthProvider" />
</security:authentication-manager>

我们在此假设security namespace正用于配置。 还假定您已将UserDetailsService(称为"userDetailsService")添加到您的配置中以加载用户的角色。

Java EE容器认证

J2eePreAuthenticatedProcessingFilter将从HttpServletRequestuserPrincipal属性中提取用户名。 如上所述,J2eeBasedPreAuthenticatedWebAuthenticationDetailsS​​ource中使用此过滤器通常会与使用Java EE角色相结合。

在使用这种方法的代码库中有一个示例应用程序,因此如果您有兴趣,可以从github获取代码并查看应用程序上下文文件。 该代码位于samples/xml/preauth目录中。

LDAP身份验证

概述

LDAP通常被组织用作用户信息和身份验证服务的中央存储库。 它也可以用来存储应用程序用户的角色信息。

LDAP服务器如何配置有很多不同的情况,所以Spring Security的LDAP提供程序是完全可配置的。 它使用单独的策略接口进行身份验证和角色检索,并提供可配置为处理各种情况的默认实现。

在尝试将其用于Spring Security之前,您应该熟悉LDAP。 以下链接提供了有关概念的良好介绍,并提供了使用免费LDAP服务器OpenLDAP设置目录的指南: http://www.zytrax.com/books/ldap/。 熟悉用于从Java访问LDAP的JNDI API也可能有用。 我们不在LDAP提供程序中使用任何第三方LDAP库(Mozilla,JLDAP等),但广泛使用Spring LDAP,因此如果您计划添加自己的自定义项,对该项目的某些熟悉可能会很有用。

使用LDAP身份验证时,确保正确配置LDAP连接池非常重要。 如果您不熟悉如何操作,可以参考 Java LDAP文档

在Spring Security中使用LDAP

Spring Security中的LDAP认证大致可以分为以下几个阶段。

  • 从登录名获取唯一的LDAP "Distinguished Name"或DN。 这通常意味着在目录中执行搜索,除非事先知道用户名到DN的确切映射。 因此,用户在登录时可能会输入名称"joe",但用于向LDAP进行身份验证的实际名称将是完整的DN,例如uid=joe,ou=users,dc=spring,dc=io

  • 以该用户的身份"binding"对用户进行身份验证,或者对DN的目录条目中的password属性执行用户密码的远程"compare"操作。

  • 加载用户权限列表。

例外情况是LDAP目录仅用于在本地检索用户信息并进行身份验证。 这可能是不可能的,因为目录通常设置为对用户密码等属性的读访问权限有限。

我们将在下面看一些配置方案。 有关可用配置选项的完整信息,请参阅安全名称空间模式(您的XML编辑器中应该提供哪些信息)。

配置LDAP服务器

您需要做的第一件事是配置服务器进行身份验证。 这是使用安全名称空间中的<ldap-server>元素完成的。 可以使用url属性将其配置为指向外部LDAP服务器:

<ldap-server url="ldap://springframework.org:389/dc=springframework,dc=org" />

使用嵌入式测试服务器

<ldap-server>元素也可用于创建嵌入式服务器,这对于测试和演示可能非常有用。 在这种情况下,您可以不使用url属性来使用它:

<ldap-server root="dc=springframework,dc=org"/>

这里我们已经指定目录的根目录DIT应该是"dc=springframework,dc=org",这是默认值。 使用这种方式,命名空间解析器将创建一个嵌入式Apache Directory服务器,并扫描类路径以查找将尝试加载到服务器的任何LDIF文件。 您可以使用定义要加载的LDIF资源的ldif属性来自定义此行为:

<ldap-server ldif="classpath:users.ldif" />

这使得启动和运行LDAP变得容易很多,因为使用外部服务器可能会很不方便。 它还将用户从连接Apache Directory服务器所需的复杂bean配置中隔离开来。 使用普通的Spring Beans,配置会更加混乱。 您必须拥有必要的Apache目录依赖关系jar供您的应用程序使用。 这些可以从LDAP示例应用程序获得。

使用绑定验证

这是最常见的LDAP身份验证方案。

<ldap-authentication-provider user-dn-pattern="uid={0},ou=people"/>

这个简单的例子将通过用所提供的模式中的用户登录名替换用户并获得用户的DN,并尝试将该用户与登录密码绑定。 如果您的所有用户都存储在目录中的单个节点下,这是可以的。 如果您想要配置LDAP搜索过滤器来查找用户,则可以使用以下内容:

<ldap-authentication-provider user-search-filter="(uid={0})"
	user-search-base="ou=people"/>

如果与上述服务器定义一起使用,则会使用user-search-filter属性的值作为过滤器在DN ou=people,dc=springframework,dc=org下执行搜索。 同样,用户登录名将替换过滤器名称中的参数,因此它将搜索uid属性等于用户名的条目。 如果未提供user-search-base,则将从根目录执行搜索。

加载授权

如何从LDAP目录中的组加载权限是由以下属性控制的。

  • group-search-base. 定义应在其下执行组搜索的目录树的部分。

  • group-role-attribute. 包含组条目定义的权限名称的属性。 默认为cn

  • group-search-filter. 用于搜索组成员身份的过滤器。 默认值为uniqueMember={0},对应于groupOfUniqueNamesLDAP类注脚:[请注意,这与使用member={0}的底层DefaultLdapAuthoritiesPopulator的默认配置不同)。 在这种情况下,替换参数是用户的完整专有名称。 如果您想过滤登录名称,则可以使用参数{1}

所以如果我们使用下面的配置

<ldap-authentication-provider user-dn-pattern="uid={0},ou=people"
		group-search-base="ou=groups" />

并以用户"ben"身份成功进行身份验证,则后续加载权限会在目录条目ou=groups,dc=springframework,dc=org下执行搜索,查找包含值uid=ben,ou=people,dc=springframework,dc=org的属性uniqueMember的条目。 默认情况下,机构名称的前缀是ROLE_。 您可以使用role-prefix属性进行更改。 如果您不需要任何前缀,请使用role-prefix="none"。 有关加载权限的更多信息,请参阅DefaultLdapAuthoritiesPopulator类的Javadoc。

实现类

我们上面使用的命名空间配置选项很容易使用,比使用Spring bean明确得多。 有些情况下,您可能需要知道如何直接在应用程序上下文中配置Spring Security LDAP。 例如,您可能希望自定义某些类的行为。 如果您很高兴使用命名空间配置,那么您可以跳过本节和下一节。

主要的LDAP提供者类LdapAuthenticationProvider实际上并没有多大作用,而是将工作委托给其他两个bean,LdapAuthenticatorLdapAuthoritiesPopulator,它们负责验证用户并检索用户的GrantedAuthority分别。

LdapAuthenticator实现

认证者还负责检索任何所需的用户属性。 这是因为对属性的权限可能取决于正在使用的身份验证的类型。 例如,如果作为用户进行绑定,则可能需要使用用户自己的权限读取它们。

目前Spring Security提供了两种身份验证策略:

  • 直接向LDAP服务器认证("bind"认证)。

  • 密码比较,其中用户提供的密码与存储库中存储的密码进行比较。 这可以通过检索密码属性的值并在本地进行检查或通过执行LDAP "compare"操作来完成,其中将提供的密码传递给服务器进行比较,并且实际密码值永远不会被检索。

通用功能

在可以通过任一策略对用户进行身份验证之前,必须从提供给应用程序的登录名中获取专有名称(DN)。 这可以通过简单的模式匹配(通过设置setUserDnPatterns数组属性)或通过设置userSearch属性来完成。 对于DN模式匹配方法,使用标准的Java模式格式,并且登录名将替换参数{0}。 该模式应该与配置的SpringSecurityContextSource将绑定到的DN相关(有关此信息的更多信息,请参阅connecting to the LDAP server中的部分)。 例如,如果您使用URL为ldap://monkeymachine.co.uk/dc=springframework,dc=org的LDAP服务器,并且模式为uid={0},ou=greatapes,则登录名"gorilla"将映射到DN uid=gorilla,ou=greatapes,dc=springframework,dc=org。 每个配置的DN模式将依次尝试,直到找到匹配项。 有关使用搜索的信息,请参阅下面的search objects部分。 也可以使用两种方法的组合 - 首先检查模式,如果找不到匹配的DN,则将使用搜索。

认证者

org.springframework.security.ldap.authentication中的类BindAuthenticator实现了绑定认证策略。 它只是试图绑定为用户。

PasswordComparisonAuthenticator

PasswordComparisonAuthenticator实施密码比较认证策略。

连接到LDAP服务器

上面讨论的bean必须能够连接到服务器。 他们都必须提供SpringSecurityContextSource,这是Spring LDAP的ContextSource的扩展。 除非您有特殊要求,否则您通常会配置一个DefaultSpringSecurityContextSource bean,该bean可以使用您的LDAP服务器的URL进行配置,并且可以使用默认使用的"manager"用户的用户名和密码绑定到服务器时(而不是匿名绑定)。 有关更多信息,请阅读此类的Javadoc和Spring LDAP的AbstractContextSource

LDAP搜索对象

通常比简单的DN匹配更复杂的策略需要在目录中定位用户条目。 这可以封装在可以提供给验证器实现的LdapUserSearch实例中,例如,允许它们定位用户。 提供的实现是FilterBasedLdapUserSearch

FilterBasedLdapUserSearch中

这个bean使用LDAP过滤器来匹配目录中的用户对象。 该过程在Javadoc中对 JDK DirContext类中的相应搜索方法进行了解释。 如上所述,搜索过滤器可以提供参数。 对于这个类,唯一有效的参数是{0},它将被用户的登录名取代。

LdapAuthoritiesPopulator在

在成功验证用户之后,LdapAuthenticationProvider将尝试通过调用配置的LdapAuthoritiesPopulator Bean来为用户加载一组权限。 DefaultLdapAuthoritiesPopulator是一个实现,它将通过在目录中搜索用户所属的组来加载权限(通常这些将是目录中的groupOfNamesgroupOfUniqueNames条目)。 有关它如何工作的更多细节,请咨询本课程的Javadoc。

如果您只想使用LDAP进行身份验证,但是从不同的来源(例如数据库)加载权限,则可以提供您自己的此接口实现,然后插入。

Spring Bean配置

使用我们在此讨论的一些bean的典型配置可能如下所示:

<bean id="contextSource"
		class="org.springframework.security.ldap.DefaultSpringSecurityContextSource">
<constructor-arg value="ldap://monkeymachine:389/dc=springframework,dc=org"/>
<property name="userDn" value="cn=manager,dc=springframework,dc=org"/>
<property name="password" value="password"/>
</bean>

<bean id="ldapAuthProvider"
	class="org.springframework.security.ldap.authentication.LdapAuthenticationProvider">
<constructor-arg>
<bean class="org.springframework.security.ldap.authentication.BindAuthenticator">
	<constructor-arg ref="contextSource"/>
	<property name="userDnPatterns">
	<list><value>uid={0},ou=people</value></list>
	</property>
</bean>
</constructor-arg>
<constructor-arg>
<bean
	class="org.springframework.security.ldap.userdetails.DefaultLdapAuthoritiesPopulator">
	<constructor-arg ref="contextSource"/>
	<constructor-arg value="ou=groups"/>
	<property name="groupRoleAttribute" value="ou"/>
</bean>
</constructor-arg>
</bean>

这将设置提供者访问URL为ldap://monkeymachine:389/dc=springframework,dc=org的LDAP服务器。 尝试使用DN uid=<user-login-name>,ou=people,dc=springframework,dc=org进行绑定来执行身份验证。 验证成功后,角色将通过使用默认过滤器(member=<user’s-DN>)在DN ou=groups,dc=springframework,dc=org下搜索来分配给用户。 角色名称将取自每个匹配的"ou"属性。

要配置使用过滤器(uid=<user-login-name>)而不是DN模式(或除此之外)的用户搜索对象,您可以配置以下bean

<bean id="userSearch"
	class="org.springframework.security.ldap.search.FilterBasedLdapUserSearch">
<constructor-arg index="0" value=""/>
<constructor-arg index="1" value="(uid={0})"/>
<constructor-arg index="2" ref="contextSource" />
</bean>

并通过设置BindAuthenticator bean的userSearch属性来使用它。 然后认证者会在尝试以该用户身份进行绑定之前调用搜索对象以获取正确的用户DN。

LDAP属性和自定义UserDetails

使用LdapAuthenticationProvider进行身份验证的最终结果与使用标准UserDetailsService接口的普通Spring Security身份验证相同。 UserDetails对象被创建并存储在返回的Authentication对象中。 与使用UserDetailsService一样,常见的要求是能够自定义此实现并添加额外的属性。 使用LDAP时,这些通常是来自用户条目的属性。 UserDetails对象的创建由提供者的UserDetailsContextMapper策略控制,该策略负责将用户对象映射到LDAP上下文数据和从LDAP上下文数据映射用户对象:

public interface UserDetailsContextMapper {

UserDetails mapUserFromContext(DirContextOperations ctx, String username,
		Collection<GrantedAuthority> authorities);

void mapUserToContext(UserDetails user, DirContextAdapter ctx);
}

只有第一种方法与认证有关。 如果您提供此接口的实现并将其注入到LdapAuthenticationProvider中,则您可以精确控制如何创建UserDetails对象。 第一个参数是Spring LDAP的DirContextOperations实例,它允许您访问在验证期间加载的LDAP属性。 username参数是用于认证的名称,最后一个参数是配置的LdapAuthoritiesPopulator为用户加载的权限的集合。

根据您使用的身份验证类型,上下文数据加载的方式略有不同。 使用BindAuthenticator,绑定操作返回的上下文将用于读取属性,否则将使用从配置的ContextSource中获取的标准上下文读取数据(当搜索配置为定位用户,这将是搜索对象返回的数据)。

Active Directory身份验证

Active Directory支持其自身的非标准认证选项,并且正常使用模式不适合标准的LdapAuthenticationProvider。 通常使用域用户名(格式为user@domain)执行身份验证,而不是使用LDAP专有名称。 为了简化这个过程,Spring Security 3.1有一个为典型Active Directory设置定制的身份验证提供程序。

ActiveDirectoryLdapAuthenticationProvider

配置ActiveDirectoryLdapAuthenticationProvider非常简单。 您只需提供域名和提供服务器脚注地址的LDAP URL:[也可以使用DNS查找来获取服务器的IP地址。 目前尚未支持,但希望能在未来的版本中。]。 一个示例配置将如下所示:

<bean id="adAuthenticationProvider"
class="org.springframework.security.ldap.authentication.ad.ActiveDirectoryLdapAuthenticationProvider">
	<constructor-arg value="mydomain.com" />
	<constructor-arg value="ldap://adserver.mydomain.com/" />
</bean>
}

请注意,为了定义服务器位置,不需要指定单独的ContextSource - 该bean是完全独立的。 例如,名为"Sharon"的用户可以通过输入用户名sharon或完整的Active Directory userPrincipalName进行身份验证,即sharon@mydomain.com。 然后定位用户的目录条目,并返回可用于定制创建的UserDetails对象(a UserDetailsContextMapper)的属性(如上所述)以实现此目的。 与目录的所有交互都与用户本身的身份一致。 没有"manager"用户的概念。

默认情况下,用户权限是从用户条目的memberOf属性值中获取的。 分配给用户的权限可以再次使用UserDetailsContextMapper进行自定义。 您还可以在提供程序实例中注入GrantedAuthoritiesMapper以控制最终位于Authentication对象中的权限。

Active Directory错误代码

默认情况下,失败的结果将导致标准的Spring Security BadCredentialsException。 如果将属性convertSubErrorCodesToExceptions设置为true,则将解析异常消息以尝试解压出特定于Active Directory的错误代码并引发更具体的异常。 查看Javadoc课程获取更多信息。

OAuth 2.0登录 - 高级配置

HttpSecurity.oauth2Login()为定制OAuth 2.0登录提供了许多配置选项。 主要配置选项分组到他们的协议端点对应部分。

例如,oauth2Login().authorizationEndpoint()允许配置Authorization Endpoint, 而oauth2Login().tokenEndpoint()允许配置Token Endpoint

以下代码显示了一个示例:

@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {

	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http
			.oauth2Login()
				.authorizationEndpoint()
					...
				.redirectionEndpoint()
					...
				.tokenEndpoint()
					...
				.userInfoEndpoint()
					...
	}
}

oauth2Login() DSL的主要目标是与规范中定义的命名保持一致。

OAuth 2.0授权框架如下定义https://tools.ietf.org/html/rfc6749#section-3[Protocol Endpoints]:

授权过程使用两个授权服务器端点(HTTP资源):

  • 授权端点:客户端用于通过用户代理重定向从资源所有者获取授权。

  • 令牌端点:客户端用来为访问令牌交换授权授权,通常使用客户端验证。

以及一个客户端端点:

  • 重定向端点:授权服务器用于返回响应 通过资源所有者用户代理向客户端提供授权凭证。

OpenID Connect Core 1.0规范定义了 UserInfo端点,如下所示:

UserInfo端点是一个OAuth 2.0保护资源,用于返回有关经过身份验证的最终用户的声明。 为了获得有关最终用户的请求声明,客户端向UserInfo端点发出请求 通过使用通过OpenID Connect Authentication获取的访问令牌。 这些声明通常由包含声明的名称/值对集合的JSON对象表示。

以下代码显示了可用于oauth2Login() DSL的完整配置选项:

@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {

	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http
			.oauth2Login()
				.clientRegistrationRepository(this.clientRegistrationRepository())
				.authorizedClientService(this.authorizedClientService())
				.loginPage("/login")
				.authorizationEndpoint()
					.baseUri(this.authorizationRequestBaseUri())
					.authorizationRequestRepository(this.authorizationRequestRepository())
					.and()
				.redirectionEndpoint()
					.baseUri(this.authorizationResponseBaseUri())
					.and()
				.tokenEndpoint()
					.accessTokenResponseClient(this.accessTokenResponseClient())
					.and()
				.userInfoEndpoint()
					.userAuthoritiesMapper(this.userAuthoritiesMapper())
					.userService(this.oauth2UserService())
					.oidcUserService(this.oidcUserService())
					.customUserType(GitHubOAuth2User.class, "github");
	}
}

下面的章节会详细介绍每个可用的配置选项:

OAuth 2.0登录页面

默认情况下,OAuth 2.0登录页面由DefaultLoginPageGeneratingFilter自动生成。 默认登录页面显示每个配置的OAuth客户端及其ClientRegistration.clientName 作为能够启动授权请求(或OAuth 2.0登录)的链接。

每个OAuth客户端的链接目标缺省为以下内容:

OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI + "/{registrationId}"

以下行显示一个示例:

<a href="/oauth2/authorization/google">Google</a>

要覆盖默认的登录页面, 配置oauth2Login().loginPage()和(可选)oauth2Login().authorizationEndpoint().baseUri()

以下列表显示了一个示例:

@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {

	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http
			.oauth2Login()
				.loginPage("/login/oauth2")
				...
				.authorizationEndpoint()
					.baseUri("/login/oauth2/authorization")
					....
	}
}
 您需要提供带有@RequestMapping("/login/oauth2")的{​​{0}},以便呈现自定义登录页面。
 

如前所述,配置oauth2Login().authorizationEndpoint().baseUri()是可选的。 但是,如果您选择自定义,请确保每个OAuth客户端的链接都与authorizationEndpoint().baseUri()匹配。

以下行显示一个示例:

<a href="/login/oauth2/authorization/google">Google</a>

授权端点

AuthorizationRequestRepository

AuthorizationRequestRepository负责持续OAuth2AuthorizationRequest 从授权请求发起到授权响应的时间 (回调)。

 OAuth2AuthorizationRequest用于关联和验证授权响应。

AuthorizationRequestRepository的默认实现是HttpSessionOAuth2AuthorizationRequestRepository, 将OAuth2AuthorizationRequest存储在HttpSession中。

如果您想提供AuthorizationRequestRepository的自定义实施 将OAuth2AuthorizationRequest的属性存储在Cookie中, 按以下示例中所示进行配置:

@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {

	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http
			.oauth2Login()
				.authorizationEndpoint()
					.authorizationRequestRepository(this.cookieAuthorizationRequestRepository())
					...
	}

	private AuthorizationRequestRepository<OAuth2AuthorizationRequest> cookieAuthorizationRequestRepository() {
		return new HttpCookieOAuth2AuthorizationRequestRepository();
	}
}

重定向端点

授权服务器使用重定向端点来返回授权响应 (包含授权证书)通过资源所有者用户代理发送给客户端。

 OAuth 2.0登录利用授权代码授权。 因此,授权凭证是授权代码。

默认的授权响应baseUri(重定向端点)为/login/oauth2/code/*,其在OAuth2LoginAuthenticationFilter.DEFAULT_FILTER_PROCESSES_URI中定义。

如果您想自定义授权响应baseUri,请按照以下示例中所示进行配置:

@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {

	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http
			.oauth2Login()
				.redirectionEndpoint()
					.baseUri("/login/oauth2/callback/*")
					....
	}
}
 

您还需要确保ClientRegistration.redirectUriTemplate与自定义授权响应baseUri匹配。

以下列表显示了一个示例:

return CommonOAuth2Provider.GOOGLE.getBuilder("google")
	.clientId("google-client-id")
	.clientSecret("google-client-secret")
	.redirectUriTemplate("{baseUrl}/login/oauth2/callback/{registrationId}")
	.build();

令牌端点

OAuth2AccessTokenResponseClient

OAuth2AccessTokenResponseClient负责交换授权授权凭证 获取授权服务器的令牌端点上的访问令牌凭证。

OAuth2AccessTokenResponseClient的默认实现是NimbusAuthorizationCodeTokenResponseClient, 它在Token端点交换访问令牌的授权代码。

 NimbusAuthorizationCodeTokenResponseClient在内部使用https://connect2id.com/products/nimbus-oauth-openid-connect-sdk[Nimbus OAuth 2.0 SDK]。

如果您想提供OAuth2AccessTokenResponseClient的自定义实施 它使用Spring Framework 5反应WebClient来发起对Token端点的请求, 按以下示例中所示进行配置:

@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {

	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http
			.oauth2Login()
				.tokenEndpoint()
					.accessTokenResponseClient(this.accessTokenResponseClient())
					...
	}

	private OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient() {
		return new SpringWebClientAuthorizationCodeTokenResponseClient();
	}
}

UserInfo端点

UserInfo端点包含许多配置选项,如以下小节所述:

映射用户权限

在用户成功通过OAuth 2.0 Provider进行身份验证后, 可以将OAuth2User.getAuthorities()(或OidcUser.getAuthorities())映射到一组新GrantedAuthority个实例, 在完成身份验证时将提供给OAuth2AuthenticationToken

 OAuth2AuthenticationToken.getAuthorities()用于授权请求,如hasRole('USER')hasRole('ADMIN')

映射用户权限时有几个选项可供选择:

使用GrantedAuthoritiesMapper

提供GrantedAuthoritiesMapper的实现并按照以下示例中所示进行配置:

@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {

	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http
			.oauth2Login()
				.userInfoEndpoint()
					.userAuthoritiesMapper(this.userAuthoritiesMapper())
					...
	}

	private GrantedAuthoritiesMapper userAuthoritiesMapper() {
		return (authorities) -> {
			Set<GrantedAuthority> mappedAuthorities = new HashSet<>();

			authorities.forEach(authority -> {
				if (OidcUserAuthority.class.isInstance(authority)) {
					OidcUserAuthority oidcUserAuthority = (OidcUserAuthority)authority;

					OidcIdToken idToken = oidcUserAuthority.getIdToken();
					OidcUserInfo userInfo = oidcUserAuthority.getUserInfo();

					// Map the claims found in idToken and/or userInfo
					// to one or more GrantedAuthority's and add it to mappedAuthorities

				} else if (OAuth2UserAuthority.class.isInstance(authority)) {
					OAuth2UserAuthority oauth2UserAuthority = (OAuth2UserAuthority)authority;

					Map<String, Object> userAttributes = oauth2UserAuthority.getAttributes();

					// Map the attributes found in userAttributes
					// to one or more GrantedAuthority's and add it to mappedAuthorities

				}
			});

			return mappedAuthorities;
		};
	}
}

或者,您可以注册GrantedAuthoritiesMapper @Bean以使其自动应用于配置,如以下示例所示:

@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {

	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http.oauth2Login();
	}

	@Bean
	public GrantedAuthoritiesMapper userAuthoritiesMapper() {
		...
	}
}
使用OAuth2UserService的基于代表团的策略

与使用GrantedAuthoritiesMapper相比,此策略更先进,但它也更灵活 因为它使您可以访问OAuth2UserRequestOAuth2User(使用OAuth 2.0 UserService时) 或OidcUserRequestOidcUser(使用OpenID Connect 1.0 UserService时)。

OAuth2UserRequest(和OidcUserRequest)可让您访问关联的OAuth2AccessToken, 这在delegator需要获取权限信息的情况下非常有用 从受保护的资源中映射用户的自定义权限。

以下示例显示如何使用OpenID Connect 1.0 UserService实施和配置基于委派的策略:

@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {

	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http
			.oauth2Login()
				.userInfoEndpoint()
					.oidcUserService(this.oidcUserService())
					...
	}

	private OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService() {
		final OidcUserService delegate = new OidcUserService();

		return (userRequest) -> {
			// Delegate to the default implementation for loading a user
			OidcUser oidcUser = delegate.loadUser(userRequest);

			OAuth2AccessToken accessToken = userRequest.getAccessToken();
			Set<GrantedAuthority> mappedAuthorities = new HashSet<>();

			// TODO
			// 1) Fetch the authority information from the protected resource using accessToken
			// 2) Map the authority information to one or more GrantedAuthority's and add it to mappedAuthorities

			// 3) Create a copy of oidcUser but use the mappedAuthorities instead
			oidcUser = new DefaultOidcUser(mappedAuthorities, oidcUser.getIdToken(), oidcUser.getUserInfo());

			return oidcUser;
		};
	}
}

配置自定义OAuth2User

CustomUserTypesOAuth2UserServiceOAuth2UserService的实现 为自定义OAuth2User类型提供支持。

如果默认实现(DefaultOAuth2User)不符合您的需求, 您可以定义自己的OAuth2User实现。

以下代码演示了如何为GitHub注册自定义OAuth2User类型:

@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {

	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http
			.oauth2Login()
				.userInfoEndpoint()
					.customUserType(GitHubOAuth2User.class, "github")
					...
	}
}

以下代码显示了GitHub的自定义OAuth2User类型的示例:

public class GitHubOAuth2User implements OAuth2User {
	private List<GrantedAuthority> authorities =
		AuthorityUtils.createAuthorityList("ROLE_USER");
	private Map<String, Object> attributes;
	private String id;
	private String name;
	private String login;
	private String email;

	@Override
	public Collection<? extends GrantedAuthority> getAuthorities() {
		return this.authorities;
	}

	@Override
	public Map<String, Object> getAttributes() {
		if (this.attributes == null) {
			this.attributes = new HashMap<>();
			this.attributes.put("id", this.getId());
			this.attributes.put("name", this.getName());
			this.attributes.put("login", this.getLogin());
			this.attributes.put("email", this.getEmail());
		}
		return attributes;
	}

	public String getId() {
		return this.id;
	}

	public void setId(String id) {
		this.id = id;
	}

	@Override
	public String getName() {
		return this.name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public String getLogin() {
		return this.login;
	}

	public void setLogin(String login) {
		this.login = login;
	}

	public String getEmail() {
		return this.email;
	}

	public void setEmail(String email) {
		this.email = email;
	}
}
 idnameloginemail是在GitHub的UserInfo Response中返回的属性。 有关从UserInfo端点返回的详细信息,请参阅API文档 对于https://developer.github.com/v3/users/#get-the-authenticated-user ["Get the authenticated user"]。

OAuth 2.0 UserService

DefaultOAuth2UserServiceOAuth2UserService的实现 支持标准的OAuth 2.0 Provider。

 OAuth2UserService获取用户属性 最终用户(资源所有者)从UserInfo端点(通过使用 在授权流程中授予客户端的访问令牌) 并以OAuth2User的形式返回AuthenticatedPrincipal

如果默认实现不适合您的需求,则可以定义您自己的OAuth2UserService实现 对于标准的OAuth 2.0 Provider。

以下配置演示了如何配置自定义OAuth2UserService

@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {

	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http
			.oauth2Login()
				.userInfoEndpoint()
					.userService(this.oauth2UserService())
					...
	}

	private OAuth2UserService<OAuth2UserRequest, OAuth2User> oauth2UserService() {
		return new CustomOAuth2UserService();
	}
}

OpenID Connect 1.0 UserService

OidcUserServiceOAuth2UserService的实现 支持OpenID Connect 1.0 Provider的。

 OAuth2UserService负责获取用户属性 最终用户(资源所有者)从UserInfo端点(通过使用 在授权流程中授予客户端的访问令牌) 并以OidcUser的形式返回AuthenticatedPrincipal

如果默认实现不适合您的需求,则可以定义您自己的OAuth2UserService实现 适用于OpenID Connect 1.0 Provider。

以下配置演示了如何配置自定义OpenID Connect 1.0 OAuth2UserService

@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {

	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http
			.oauth2Login()
				.userInfoEndpoint()
					.oidcUserService(this.oidcUserService())
					...
	}

	private OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService() {
		return new CustomOidcUserService();
	}
}

JSP标记库

Spring Security有自己的taglib,它为访问安全信息和在JSP中应用安全约束提供了基本的支持。

声明Taglib

要使用任何标签,您必须在JSP中声明安全taglib:

<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %>

授权标签

该标签用于确定是否应评估其内容。 在Spring Security 3.0中,它可以以两种方式使用脚注:[ Spring Security 2.0的遗留选项也受到支持,但不鼓励。 ]. 第一种方法使用在标签的access属性中指定的web-security expression。 表达式评估将委派给应用程序上下文中定义的SecurityExpressionHandler<FilterInvocation>(您应该在<http>名称空间配置中启用Web表达式以确保此服务可用)。 所以,例如,你可能有

<sec:authorize access="hasRole('supervisor')">

This content will only be visible to users who have the "supervisor" authority in their list of <tt>GrantedAuthority</tt>s.

</sec:authorize>

当与Spring Security的PermissionEvaluator结合使用时,该标签也可以用来检查权限。 例如:

<sec:authorize access="hasPermission(#domain,'read') or hasPermission(#domain,'write')">

This content will only be visible to users who have read or write permission to the Object found as a request attribute named "domain".

</sec:authorize>

一个常见的要求是只显示特定的链接,如果用户实际上被允许点击它。 我们如何预先确定是否允许某些事情?该标签也可以在另一种模式下运行,该模式允许您将特定的URL定义为属性。 如果用户被允许调用该URL,那么标签主体将被评估,否则它将被跳过。 所以你可能有类似的东西

<sec:authorize url="/admin">

This content will only be visible to users who are authorized to send requests to the "/admin" URL.

</sec:authorize>

要使用此标记,您的应用程序上下文中还必须有WebInvocationPrivilegeEvaluator的实例。 如果您使用的是名称空间,则会自动注册。 这是DefaultWebInvocationPrivilegeEvaluator的一个实例,它为提供的URL创建一个虚拟Web请求,并调用安全拦截器来查看请求是成功还是失败。 这允许您委派到您在<http>命名空间配置中使用intercept-url声明定义的访问控制设置,并且不必在JSP中复制信息(如所需的角色)。 这种方法还可以与提供HTTP方法的method属性相结合,以获得更具体的匹配。

通过将var属性设置为变量名称,可以将评估标记的布尔结果(无论是授予还是拒绝访问)存储在页面上下文范围变量中,从而避免需要复制和重新评估条件页面中的其他点。

禁用标签授权以进行测试

在页面中隐藏未授权用户的链接并不妨碍他们访问URL。 例如,他们可以直接将其输入到浏览器中。 作为测试过程的一部分,您可能想要揭示隐藏区域,以检查链接是否真正在后端得到保护。 如果您将系统属性spring.security.disableUISecurity设置为true,则authorize标签仍会运行,但不会隐藏其内容。 默认情况下,它也会用<span class="securityHiddenUI">…​</span>标签包围内容。 这使您可以显示具有特定CSS样式(例如不同背景颜色)的"hidden"内容。 例如,尝试运行启用了此属性的"tutorial"示例应用程序。

如果要从默认的span标签更改周围文本(或使用空字符串将其完全删除),还可以设置属性spring.security.securedUIPrefixspring.security.securedUISuffix

认证标签

该标签允许访问存储在安全上下文中的当前Authentication对象。 它直接在JSP中呈现对象的属性。 因此,例如,如果Authentication的{​​{0}}属性是Spring Security的UserDetails对象的实例,则使用<sec:authentication property="principal.username" />将呈现当前用户的名称。

当然,没有必要为这类事情使用JSP标签,而有些人更愿意在视图中尽可能少地使用逻辑。 您可以访问MVC控制器中的Authentication对象(通过调用SecurityContextHolder.getContext().getAuthentication())并将数据直接添加到您的模型中,以供视图渲染。

accesscontrollist标记

该标签只有在与Spring Security的ACL模块一起使用时才有效。 它检查指定域对象的必需权限的逗号分隔列表。 如果当前用户拥有所有这些权限,则标签正文将被评估。 如果他们不这样做,它将被跳过。 一个例子可能是

小心:一般来说,这个标签应被视为弃用。 请改用授权标签

<sec:accesscontrollist hasPermission="1,2" domainObject="${someObject}">

This will be shown if the user has all of the permissions represented by the values "1" or "2" on the given object.

</sec:accesscontrollist>

权限被传递给应用程序上下文中定义的PermissionFactory,并将它们转换为ACL Permission实例,因此它们可以是工厂支持的任何格式 - 它们不必是整数,它们可以是READWRITE之类的字符串。 如果未找到PermissionFactory,则将使用DefaultPermissionFactory的实例。 来自应用程序上下文的AclService将用于加载提供对象的Acl实例。 Acl将被调用所需的权限来检查是否所有这些都被授予。

这个标签也支持var属性,就像authorize标签一样。

csrfInput标记

如果启用了CSRF保护,则此标记会为CSRF保护令牌插入一个隐藏表单字段,其中包含正确的名称和值。 如果CSRF保护未启用,则此标记不输出任何内容。

通常,Spring Security会为您使用的任何<form:form>标记自动插入CSRF表单字段,但如果由于某种原因您无法使用<form:form>,则csrfInput是一个方便的替代品。

您应该将此标记置于HTML <form></form>块内,您通常会在其中放置其他输入字段。 不要将此标签放置在Spring <form:form></form:form>块内。 Spring Security自动处理Spring窗体。

	<form method="post" action="/do/something">
		<sec:csrfInput />
		Name:<br />
		<input type="text" name="name" />
		...
	</form>

csrfMetaTags标记

如果启用CSRF保护,则此标记会插入包含CSRF保护令牌表单字段和标头名称以及CSRF保护标记值的元标记。 这些元标记对于在应用程序中使用JavaScript中的CSRF保护很有用。

您应该将csrfMetaTags放置在HTML <head></head>块内,您通常会在其中放置其他元标记。 一旦你使用这个标签,你可以使用JavaScript轻松访问表单字段名称,标题名称和标记值。 在这个例子中使用JQuery使任务更容易。

<!DOCTYPE html>
<html>
	<head>
		<title>CSRF Protected JavaScript Page</title>
		<meta name="description" content="This is the description for this page" />
		<sec:csrfMetaTags />
		<script type="text/javascript" language="javascript">

			var csrfParameter = $("meta[name='_csrf_parameter']").attr("content");
			var csrfHeader = $("meta[name='_csrf_header']").attr("content");
			var csrfToken = $("meta[name='_csrf']").attr("content");

			// using XMLHttpRequest directly to send an x-www-form-urlencoded request
			var ajax = new XMLHttpRequest();
			ajax.open("POST", "http://www.example.org/do/something", true);
			ajax.setRequestHeader("Content-Type", "application/x-www-form-urlencoded data");
			ajax.send(csrfParameter + "=" + csrfToken + "&name=John&...");

			// using XMLHttpRequest directly to send a non-x-www-form-urlencoded request
			var ajax = new XMLHttpRequest();
			ajax.open("POST", "http://www.example.org/do/something", true);
			ajax.setRequestHeader(csrfHeader, csrfToken);
			ajax.send("...");

			// using JQuery to send an x-www-form-urlencoded request
			var data = {};
			data[csrfParameter] = csrfToken;
			data["name"] = "John";
			...
			$.ajax({
				url: "http://www.example.org/do/something",
				type: "POST",
				data: data,
				...
			});

			// using JQuery to send a non-x-www-form-urlencoded request
			var headers = {};
			headers[csrfHeader] = csrfToken;
			$.ajax({
				url: "http://www.example.org/do/something",
				type: "POST",
				headers: headers,
				...
			});

		<script>
	</head>
	<body>
		...
	</body>
</html>

如果未启用CSRF保护,则csrfMetaTags不输出任何内容。

Java认证和授权服务(JAAS)提供程序

概述

Spring Security提供了一个能够将身份验证请求委托给Java身份验证和授权服务(JAAS)的软件包。 这个包在下面详细讨论。

AbstractJaasAuthenticationProvider

AbstractJaasAuthenticationProvider是提供的JAAS AuthenticationProvider实现的基础。 子类必须实现一个创建LoginContext的方法。 AbstractJaasAuthenticationProvider有许多可以注入的依赖关系,下面将对此进行讨论。

JAAS CallbackHandler

大多数JAAS LoginModule都需要进行某种回调。 这些回调通常用于从用户获取用户名和密码。

在Spring Security部署中,Spring Security负责此用户交互(通过身份验证机制)。 因此,在认证请求委派给JAAS时,Spring Security的认证机制将已经完全填充了Authentication对象,其中包含JAAS LoginModule所需的全部信息。

因此,Spring Security的JAAS包提供了两个默认回调处理程序JaasNameCallbackHandlerJaasPasswordCallbackHandler。 每个回调处理程序都实现JaasAuthenticationCallbackHandler。 在大多数情况下,这些回调处理程序可以在不理解内部机制的情况下使用。

对于需要完全控制回调行为的用户,内部AbstractJaasAuthenticationProviderInternalCallbackHandler包装这些JaasAuthenticationCallbackHandler InternalCallbackHandler是实际实现JAAS普通CallbackHandler接口的类。 无论何时使用JAAS LoginModule,它都会传递一个配置应用程序上下文的列表InternalCallbackHandler。 如果LoginModule请求对InternalCallbackHandler进行回调,则回调将被传递给被封装的JaasAuthenticationCallbackHandler

JAAS AuthorityGranter ==== JAAS与校长合作。 甚至"roles"在JAAS中都被表示为主体。 另一方面,Spring Security使用Authentication对象。 每个Authentication对象包含一个主体和多个GrantedAuthority。 为了促进这些不同概念之间的映射,Spring Security的JAAS包中包含一个AuthorityGranter接口。

AuthorityGranter负责检查JAAS校长并返回一组String,代表分配给校长的当局。 对于每个返回的授权字符串,AbstractJaasAuthenticationProvider创建一个JaasGrantedAuthority(它实现了Spring Security的GrantedAuthority接口),该接口包含授权字符串和AuthorityGranter传递的JAAS主体。AbstractJaasAuthenticationProvider通过首先使用JAAS LoginModule成功验证用户的凭证,然后访问它返回的LoginContext来获取JAAS主体。 对LoginContext.getSubject().getPrincipals()进行调用,并将每个结果主体传递给针对AbstractJaasAuthenticationProvider.setAuthorityGranters(List)属性定义的每个AuthorityGranter

考虑到每个JAAS主体都具有特定于实现的含义,Spring Security不包括任何生产AuthorityGranter。 但是,单元测试中有一个TestAuthorityGranter,它演示了一个简单的AuthorityGranter实现。

DefaultJaasAuthenticationProvider

DefaultJaasAuthenticationProvider允许将JAAS Configuration对象作为依赖注入到该对象中。 然后使用注入的JAAS Configuration创建一个LoginContext。 这意味着DefaultJaasAuthenticationProvider没有像JaasAuthenticationProvider那样绑定Configuration的任何特定实现。

InMemoryConfiguration

为了便于Configuration注入DefaultJaasAuthenticationProvider,提供了名为InMemoryConfiguration的默认内存中实现。 实现构造函数接受Map,其中每个键表示登录配置名称,值表示AppConfigurationEntryArrayInMemoryConfiguration还支持AppConfigurationEntry对象的默认Array,如果未在所提供的Map内找到映射,则将使用该对象。 有关详细信息,请参阅InMemoryConfiguration的类级别javadoc。

DefaultJaasAuthenticationProvider示例配置

尽管InMemoryConfiguration的Spring配置可能比标准JAAS配置文件更详细,但与DefaultJaasAuthenticationProvider结合使用会比JaasAuthenticationProvider更灵活,因为它不依赖于默认的{{3 }}实现。

下面提供了使用InMemoryConfiguration的{​​{0}}的示例配置。 请注意,Configuration的自定义实现也可以很容易地注入到DefaultJaasAuthenticationProvider中。

<bean id="jaasAuthProvider"
class="org.springframework.security.authentication.jaas.DefaultJaasAuthenticationProvider">
<property name="configuration">
<bean class="org.springframework.security.authentication.jaas.memory.InMemoryConfiguration">
<constructor-arg>
	<map>
	<!--
	SPRINGSECURITY is the default loginContextName
	for AbstractJaasAuthenticationProvider
	-->
	<entry key="SPRINGSECURITY">
	<array>
	<bean class="javax.security.auth.login.AppConfigurationEntry">
		<constructor-arg value="sample.SampleLoginModule" />
		<constructor-arg>
		<util:constant static-field=
			"javax.security.auth.login.AppConfigurationEntry$LoginModuleControlFlag.REQUIRED"/>
		</constructor-arg>
		<constructor-arg>
		<map></map>
		</constructor-arg>
		</bean>
	</array>
	</entry>
	</map>
	</constructor-arg>
</bean>
</property>
<property name="authorityGranters">
<list>
	<!-- You will need to write your own implementation of AuthorityGranter -->
	<bean class="org.springframework.security.authentication.jaas.TestAuthorityGranter"/>
</list>
</property>
</bean>

JaasAuthenticationProvider

JaasAuthenticationProvider假定默认Configuration  通过ConfigFile的一个实例。 这个假设是为了试图更新Configuration而做出的。 JaasAuthenticationProvider然后使用默认的Configuration创建LoginContext

假设我们有一个JAAS登录配置文件/WEB-INF/login.conf,其中包含以下内容:

JAASTest {
	sample.SampleLoginModule required;
};

像所有Spring Security bean一样,JaasAuthenticationProvider通过应用程序上下文进行配置。 以下定义将对应于上述JAAS登录配置文件:

<bean id="jaasAuthenticationProvider"
class="org.springframework.security.authentication.jaas.JaasAuthenticationProvider">
<property name="loginConfig" value="/WEB-INF/login.conf"/>
<property name="loginContextName" value="JAASTest"/>
<property name="callbackHandlers">
<list>
<bean
	class="org.springframework.security.authentication.jaas.JaasNameCallbackHandler"/>
<bean
	class="org.springframework.security.authentication.jaas.JaasPasswordCallbackHandler"/>
</list>
</property>
<property name="authorityGranters">
	<list>
	<bean class="org.springframework.security.authentication.jaas.TestAuthorityGranter"/>
	</list>
</property>
</bean>

作为主题运行=== 如果已配置,则JaasApiIntegrationFilter将尝试以JaasAuthenticationToken上的Subject身份运行。 这意味着可以使用以下方式访问Subject

Subject subject = Subject.getSubject(AccessController.getContext());

这种集成可以使用jaas-api-provision属性轻松配置。 当与依赖JAAS主题被填充的遗留或外部API集成时,此功能非常有用。

CAS认证

概述

JA-SIG在企业范围内生成一个称为CAS的单一登录系统。 与其他举措不同,JA-SIG的中央身份验证服务是开源的,广泛使用,易于理解,独立于平台,并且支持代理功能。 Spring Security完全支持CAS,并提供了从Spring Security的单应用部署到由企业级CAS服务器保护的多应用部署的简单迁移路径。

你可以在http://www.ja-sig.org/cas上了解更多关于CAS的知识。 您还需要访问此网站以下载CAS服务器文件。

CAS的工作原理

虽然CAS网站包含详细介绍CAS体系结构的文档,但我们在Spring Security的上下文中再次提供总体概述。 Spring Security 3.x支持CAS 3。 在撰写本文时,CAS服务器版本为3.4。

您的企业中某处需要安装CAS服务器。 CAS服务器只是一个标准的WAR文件,因此设置服务器没有任何困难。 在WAR文件中,您将自定义向用户显示的页面上的登录和其他单点登录。

部署CAS 3.4服务器时,还需要在CAS中包含的deployerConfigContext.xml中指定AuthenticationHandlerAuthenticationHandler有一个简单的方法,它返回一个布尔值,以确定给定的一组凭证是否有效。 您的AuthenticationHandler实现需要链接到某种类型的后端身份验证存储库,例如LDAP服务器或数据库。 CAS本身包含许多AuthenticationHandler来协助完成此操作。 当您下载并部署服务器war文件时,它将被设置为成功验证输入与其用户名相匹配的密码的用户,这对测试非常有用。

除CAS服务器本身之外,其他关键角色当然是在整个企业中部署的安全Web应用程序。 这些Web应用程序被称为"services"。 有三种类型的服务。 那些对服务票据进行身份验证的服务,可以获得代理票据的服务和对代理票据进行身份验证的服务。 验证代理票据的方式不同,因为代理列表必须经过验证,并且通常可以重新使用代理票证。

Spring Security和CAS交互序列

Web浏览器,CAS服务器和Spring安全保护服务之间的基本交互如下所示:

  • 网络用户正在浏览服务的公共页面。 不涉及CAS或Spring Security。

  • 用户最终会请求一个安全的页面或者它使用的其中一个安全的页面。 Spring Security的ExceptionTranslationFilter将检测AccessDeniedExceptionAuthenticationException

  • 由于用户的Authentication对象(或缺少)导致AuthenticationException,因此ExceptionTranslationFilter将调用配置的AuthenticationEntryPoint。 如果使用CAS,这将是CasAuthenticationEntryPoint类。

  • CasAuthenticationEntryPoint将用户的浏览器重定向到CAS服务器。 它还会指示一个service参数,它是Spring Security服务(您的应用程序)的回调URL。 例如,浏览器重定向到的URL可能是https://my.company.com/cas/login?service=https%3A%2F%2Fserver3.company.com%2Fwebapp%2Flogin/cas。

  • 用户的浏览器重定向到CAS后,系统会提示他们输入用户名和密码。 如果用户提交了一个表明他们以前登录过的会话cookie,他们将不会再被提示重新登录(这个过程有一个例外,我们将在后面介绍)。 如果使用CAS 3.0,CAS将使用PasswordHandler(或AuthenticationHandler)来决定用户名和密码是否有效。

  • 成功登录后,CAS会将用户的浏览器重定向回原始服务。 它还将包含一个ticket参数,该参数是代表"service ticket"的不透明字符串。 继续我们前面的例子,浏览器重定向到的URL可能是https://server3.company.com/webapp/login/cas?ticket=ST-0-ER94xMJmn6pha35CQRoZ。

  • 回到服务Web应用程序,CasAuthenticationFilter始终监听/login/cas的请求(这是可配置的,但我们将使用此介绍中的默认值)。 处理过滤器将构建表示服务票证的UsernamePasswordAuthenticationToken。 委托人将等于CasAuthenticationFilter.CAS_STATEFUL_IDENTIFIER,而凭证将是服务票证不透明值。 此认证请求将交给配置的AuthenticationManager

  • AuthenticationManager的实现将是ProviderManager,而ProviderManager将依次配置CasAuthenticationProvider CasAuthenticationProvider仅响应包含特定于CAS的主体(如CasAuthenticationFilter.CAS_STATEFUL_IDENTIFIER)和CasAuthenticationToken(稍后讨论)的UsernamePasswordAuthenticationToken

  • CasAuthenticationProvider将使用TicketValidator实施验证服务票证。 这通常是Cas20ServiceTicketValidator,它是包含在CAS客户端库中的类之一。 如果应用程序需要验证代理票证,则使用Cas20ProxyTicketValidator TicketValidator向CAS服务器发出HTTPS请求以验证服务票据。 它也可能包含一个代理回调URL,它包含在此示例中:https://my.company.com/cas/proxyValidate?service=https%3A%2F%2Fserver3.company.com%2Fwebapp%2Flogin/cas&ticket= ST-0-ER94xMJmn6pha35CQRoZ&pgtUrl = HTTPS://server3.company.com/webapp/login/cas/proxyreceptor。

  • 返回CAS服务器,将收到验证请求。 如果所提供的服务票据与发行票据的服务URL相匹配,则CAS将以XML表示用户名的肯定响应。 如果任何代理参与了身份验证(如下所述),则代理列表也会包含在XML响应中。

  • [可选]如果对CAS验证服务的请求包含代理回调URL(位于pgtUrl参数中),CAS将在XML响应中包含一个pgtIou字符串。 此pgtIou表示代理授予凭单IOU。 然后,CAS服务器将创建自己的HTTPS连接回到pgtUrl。 这是为了相互认证CAS服务器和声称的服务URL。 HTTPS连接将用于向原始Web应用程序发送授予票证的代理。 例如,https://server3.company.com/webapp/login/cas/proxyreceptor?pgtIou=PGTIOU-0-R0zlgrl4pdAQwBvJWO3vnNpevwqStbSGcq3vKB2SqSFFRnjPHt&pgtId=PGT-1-si9YkkHLrtACBo64rmsi3v2nf7cpCResXg5MpESZFArbaZiOKH。

  • Cas20TicketValidator将解析从CAS服务器收到的XML。 它将返回到CasAuthenticationProvider a TicketResponse,其中包括用户名(必填),代理列表(如果有任何参与)和代理授予票证IOU(如果请求代理回调)。

  • 下一步CasAuthenticationProvider将调用配置的CasProxyDecider CasProxyDecider指示服务是否接受TicketResponse中的代理列表。 Spring Security提供了几个实现:RejectProxyTicketsAcceptAnyCasProxyNamedCasProxyDecider。 这些名称在很大程度上不言自明,除了NamedCasProxyDecider允许提供List个可信代理之外。 接下来*  CasAuthenticationProvider将请求AuthenticationUserDetailsService加载适用于Assertion中包含的用户的GrantedAuthority个对象。

  • 如果没有问题,CasAuthenticationProvider会构建CasAuthenticationToken,其中包含TicketResponseGrantedAuthority中包含的详细信息。

  • 然后控件返回到CasAuthenticationFilter,这将创建的CasAuthenticationToken放置在安全上下文中。

  • 用户的浏览器被重定向到导致AuthenticationException(或取决于配置的custom destination)的原始页面。

很高兴你仍然在这里! 现在让我们看看这是如何配置的

CAS客户端的配置

由于Spring Security的原因,CAS的Web应用程序变得非常简单。 假设你已经知道了使用Spring Security的基础知识,所以下面不再介绍。 我们假设正在使用基于名称空间的配置,并根据需要添加CAS bean。 每节都建立在前一节的基础上。 完整的CAS sample application可以在Spring Security示例中找到。

服务票据验证

本节介绍如何设置Spring Security来验证服务票证。 通常这都是一个Web应用程序需要的。 您将需要向应用程序上下文添加一个ServiceProperties bean。 这代表您的CAS服务:

<bean id="serviceProperties"
	class="org.springframework.security.cas.ServiceProperties">
<property name="service"
	value="https://localhost:8443/cas-sample/login/cas"/>
<property name="sendRenew" value="false"/>
</bean>

service必须等于由CasAuthenticationFilter监控的网址。 sendRenew默认为false,但如果您的应用程序特别敏感,则应将其设置为true。 该参数的作用是告诉CAS登录服务,登录时的单一登录是不可接受的。 相反,用户需要重新输入用户名和密码才能访问该服务。

应配置以下bean以启动CAS认证过程(假设您使用的是名称空间配置):

<security:http entry-point-ref="casEntryPoint">
...
<security:custom-filter position="CAS_FILTER" ref="casFilter" />
</security:http>

<bean id="casFilter"
	class="org.springframework.security.cas.web.CasAuthenticationFilter">
<property name="authenticationManager" ref="authenticationManager"/>
</bean>

<bean id="casEntryPoint"
	class="org.springframework.security.cas.web.CasAuthenticationEntryPoint">
<property name="loginUrl" value="https://localhost:9443/cas/login"/>
<property name="serviceProperties" ref="serviceProperties"/>
</bean>

要使CAS运行,ExceptionTranslationFilter必须将其authenticationEntryPoint属性设置为CasAuthenticationEntryPoint bean。 这可以使用entry-point-ref轻松完成,如上例所示。CasAuthenticationEntryPoint必须引用上面讨论的ServiceProperties bean,它提供了企业CAS登录服务器的URL。 这是用户的浏览器将被重定向的地方。

CasAuthenticationFilterUsernamePasswordAuthenticationFilter(用于基于表单的登录)具有非常相似的属性。 您可以使用这些属性来自定义诸如认证成功和失败的行为。

接下来,您需要添加一个CasAuthenticationProvider及其合作者:

<security:authentication-manager alias="authenticationManager">
<security:authentication-provider ref="casAuthenticationProvider" />
</security:authentication-manager>

<bean id="casAuthenticationProvider"
	class="org.springframework.security.cas.authentication.CasAuthenticationProvider">
<property name="authenticationUserDetailsService">
	<bean class="org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper">
	<constructor-arg ref="userService" />
	</bean>
</property>
<property name="serviceProperties" ref="serviceProperties" />
<property name="ticketValidator">
	<bean class="org.jasig.cas.client.validation.Cas20ServiceTicketValidator">
	<constructor-arg index="0" value="https://localhost:9443/cas" />
	</bean>
</property>
<property name="key" value="an_id_for_this_auth_provider_only"/>
</bean>

<security:user-service id="userService">
<!-- Password is prefixed with {noop} to indicate to DelegatingPasswordEncoder that
NoOpPasswordEncoder should be used.
This is not safe for production, but makes reading
in samples easier.
Normally passwords should be hashed using BCrypt -->
<security:user name="joe" password="{noop}joe" authorities="ROLE_USER" />
...
</security:user-service>

CasAuthenticationProvider使用UserDetailsService实例为用户加载权限,一旦它们通过了CAS的认证。 我们在这里展示了一个简单的内存设置。 请注意,CasAuthenticationProvider实际上并未使用密码进行身份验证,但它确实使用了权限。

如果您重新参考How CAS Works部分,这些bean都是不言自明的。

这完成了CAS的最基本配置。 如果您没有犯任何错误,您的Web应用程序应该在CAS单一登录框架内愉快地工作。 Spring Security的其他部分不需要担心CAS处理身份验证的事实。 在下面的章节中,我们将讨论一些(可选的)更高级的配置。

单次注销

CAS协议支持Single Logout,可以很容易地添加到Spring Security配置中。 以下是处理Single Logout的Spring Security配置的更新

<security:http entry-point-ref="casEntryPoint">
...
<security:logout logout-success-url="/cas-logout.jsp"/>
<security:custom-filter ref="requestSingleLogoutFilter" before="LOGOUT_FILTER"/>
<security:custom-filter ref="singleLogoutFilter" before="CAS_FILTER"/>
</security:http>

<!-- This filter handles a Single Logout Request from the CAS Server -->
<bean id="singleLogoutFilter" class="org.jasig.cas.client.session.SingleSignOutFilter"/>

<!-- This filter redirects to the CAS Server to signal Single Logout should be performed -->
<bean id="requestSingleLogoutFilter"
	class="org.springframework.security.web.authentication.logout.LogoutFilter">
<constructor-arg value="https://localhost:9443/cas/logout"/>
<constructor-arg>
	<bean class=
		"org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler"/>
</constructor-arg>
<property name="filterProcessesUrl" value="/logout/cas"/>
</bean>

logout元素将用户从本地应用程序中注销,但不会终止与CAS服务器或任何其他已登录的应用程序的会话。requestSingleLogoutFilter过滤器将允许请求/spring_security_cas_logout的URL将应用程序重定向到配置的CAS服务器注销URL。 然后,CAS服务器将向所有登录的服务发送单一注销请求。 singleLogoutFilter通过在静态Map中查找HttpSession处理单一注销请求,然后使其无效。

这可能会令人困惑,为什么需要logout元素和singleLogoutFilter。 由于SingleSignOutFilter仅将HttpSession存储在静态Map中以便对其调用invalidate,因此首先在本地注销是最佳做法。 使用上面的配置,注销流程将是:

  • 用户请求/logout将用户登录到本地应用程序,并将用户发送到注销成功页面。

  • 注销成功页面/cas-logout.jsp应指示用户单击指向/logout/cas的链接,以注销所有应用程序。

  • 当用户点击链接时,用户被重定向到CAS单一注销URL(https:// localhost:9443 / cas / logout)。

  • 在CAS服务器端,CAS单一注销URL然后向所有CAS服务提交单一注销请求。 在CAS服务方面,JASIG的SingleSignOutFilter通过使原始会话无效来处理注销请求。

下一步是将以下内容添加到您的web.xml中

<filter>
<filter-name>characterEncodingFilter</filter-name>
<filter-class>
	org.springframework.web.filter.CharacterEncodingFilter
</filter-class>
<init-param>
	<param-name>encoding</param-name>
	<param-value>UTF-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>characterEncodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<listener>
<listener-class>
	org.jasig.cas.client.session.SingleSignOutHttpSessionListener
</listener-class>
</listener>

在使用SingleSignOutFilter时,您可能会遇到一些编码问题。 因此,建议在使用SingleSignOutFilter时添加CharacterEncodingFilter以确保字符编码正确。 有关详细信息,请再次参阅JASIG的文档。SingleSignOutHttpSessionListener确保当HttpSession过期时,用于单次注销的映射将被删除。

使用CAS对无状态服务进行身份验证

本节介绍如何使用CAS对服务进行身份验证。 换句话说,本节讨论如何设置使用CAS认证的服务的客户端。 下一节将介绍如何设置无状态服务以使用CAS进行身份验证。

配置CAS以获取代理授予票证

为了向无状态服务进行身份验证,应用程序需要获取代理授予票证(PGT)。 本节介绍如何配置Spring Security以获取PGT构建基于[Service Ticket Authentication]配置。

第一步是在您的Spring Security配置中包含ProxyGrantingTicketStorage。 这用于存储由CasAuthenticationFilter获得的PGT,以便它们可用于获取代理票证。 示例配置如下所示

<!--
NOTE: In a real application you should not use an in memory implementation.
You will also want to ensure to clean up expired tickets by calling
ProxyGrantingTicketStorage.cleanup()
-->
<bean id="pgtStorage" class="org.jasig.cas.client.proxy.ProxyGrantingTicketStorageImpl"/>

下一步是更新CasAuthenticationProvider以获取代理票证。 为此,请将Cas20ServiceTicketValidator替换为Cas20ProxyTicketValidator。 应将proxyCallbackUrl设置为该应用程序将收到PGT的URL。 最后,配置还应引用ProxyGrantingTicketStorage,以便它可以使用PGT来获取代理票证。 您可以在下面找到一个配置更改的示例。

<bean id="casAuthenticationProvider"
	class="org.springframework.security.cas.authentication.CasAuthenticationProvider">
...
<property name="ticketValidator">
	<bean class="org.jasig.cas.client.validation.Cas20ProxyTicketValidator">
	<constructor-arg value="https://localhost:9443/cas"/>
		<property name="proxyCallbackUrl"
		value="https://localhost:8443/cas-sample/login/cas/proxyreceptor"/>
	<property name="proxyGrantingTicketStorage" ref="pgtStorage"/>
	</bean>
</property>
</bean>

最后一步是更新CasAuthenticationFilter以接受PGT并将它们存储在ProxyGrantingTicketStorage中。proxyReceptorUrlCas20ProxyTicketValidatorproxyCallbackUrl匹配非常重要。 示例配置如下所示。

<bean id="casFilter"
		class="org.springframework.security.cas.web.CasAuthenticationFilter">
	...
	<property name="proxyGrantingTicketStorage" ref="pgtStorage"/>
	<property name="proxyReceptorUrl" value="/login/cas/proxyreceptor"/>
</bean>
使用代理票证调用无状态服务

现在Spring Security获得PGT,您可以使用它们来创建可用于向无状态服务进行身份验证的代理票证。 CAS sample applicationProxyTicketSampleServlet中包含一个工作示例。 示例代码可以在下面找到:

protected void doGet(HttpServletRequest request, HttpServletResponse response)
	throws ServletException, IOException {
// NOTE: The CasAuthenticationToken can also be obtained using
// SecurityContextHolder.getContext().getAuthentication()
final CasAuthenticationToken token = (CasAuthenticationToken) request.getUserPrincipal();
// proxyTicket could be reused to make calls to the CAS service even if the
// target url differs
final String proxyTicket = token.getAssertion().getPrincipal().getProxyTicketFor(targetUrl);

// Make a remote call using the proxy ticket
final String serviceUrl = targetUrl+"?ticket="+URLEncoder.encode(proxyTicket, "UTF-8");
String proxyResponse = CommonUtils.getResponseFromServer(serviceUrl, "UTF-8");
...
}

代理券验证

CasAuthenticationProvider区分有状态客户和无状态客户。 有状态的客户端被认为是提交给CasAuthenticationFilter的{​​{0}}的任何客户端。 无状态客户端是指向filterProcessUrl以外的网址向CasAuthenticationFilter提交身份验证请求的任何客户端。

因为远程协议无法在HttpSession的上下文中呈现自己,所以不可能依赖于在请求之间的会话中存储安全上下文的默认实践。 此外,由于CAS服务器在TicketValidator验证后使服务器失效,因此在后续请求中呈现相同的代理服务器故障将不起作用。

一个显而易见的选择是远程协议客户端根本不使用CAS。 但是,这将消除CAS的许多理想功能。 作为中间地带,CasAuthenticationProvider使用StatelessTicketCache。 这仅用于使用等于CasAuthenticationFilter.CAS_STATELESS_IDENTIFIER的主体的无状态客户端。 发生什么情况是CasAuthenticationProvider将结果CasAuthenticationToken存储在StatelessTicketCache中,并以代理票据为关键字。 因此,远程协议客户端可以呈现相同的代理票证,CasAuthenticationProvider不需要联系CAS服务器进行验证(除第一个请求外)。 一旦通过身份验证,代理票证就可以用于原始目标服务以外的URL。

本部分构建在前面的部分以适应代理票证认证。 第一步是指定验证所有工件,如下所示。

<bean id="serviceProperties"
	class="org.springframework.security.cas.ServiceProperties">
...
<property name="authenticateAllArtifacts" value="true"/>
</bean>

下一步是为CasAuthenticationFilter指定servicePropertiesauthenticationDetailsSourceserviceProperties属性指示CasAuthenticationFilter尝试验证所有工件,而不是只验证filterProcessUrl中的工件。 ServiceAuthenticationDetailsSource创建一个ServiceAuthenticationDetails,以确保基于HttpServletRequest的当前URL在验证票证时用作服务URL。 可以通过注入返回自定义ServiceAuthenticationDetails的自定义AuthenticationDetailsSource来定制生成服务URL的方法。

<bean id="casFilter"
	class="org.springframework.security.cas.web.CasAuthenticationFilter">
...
<property name="serviceProperties" ref="serviceProperties"/>
<property name="authenticationDetailsSource">
	<bean class=
	"org.springframework.security.cas.web.authentication.ServiceAuthenticationDetailsSource">
	<constructor-arg ref="serviceProperties"/>
	</bean>
</property>
</bean>

您还需要更新CasAuthenticationProvider以处理代理票证。 为此,请将Cas20ServiceTicketValidator替换为Cas20ProxyTicketValidator。 您需要配置statelessTicketCache以及您想要接受的代理。 您可以在下面找到接受所有代理所需更新的示例。

<bean id="casAuthenticationProvider"
	class="org.springframework.security.cas.authentication.CasAuthenticationProvider">
...
<property name="ticketValidator">
	<bean class="org.jasig.cas.client.validation.Cas20ProxyTicketValidator">
	<constructor-arg value="https://localhost:9443/cas"/>
	<property name="acceptAnyProxy" value="true"/>
	</bean>
</property>
<property name="statelessTicketCache">
	<bean class="org.springframework.security.cas.authentication.EhCacheBasedTicketCache">
	<property name="cache">
		<bean class="net.sf.ehcache.Cache"
			init-method="initialise" destroy-method="dispose">
		<constructor-arg value="casTickets"/>
		<constructor-arg value="50"/>
		<constructor-arg value="true"/>
		<constructor-arg value="false"/>
		<constructor-arg value="3600"/>
		<constructor-arg value="900"/>
		</bean>
	</property>
	</bean>
</property>
</bean>

X.509身份验证

概述

X.509证书认证的最常见用途是验证使用SSL时服务器的身份,最常见的情况是从浏览器使用HTTPS。 浏览器将自动检查由服务器提供的证书是否由其维护的可信证书颁发机构列表中的一个颁发(即数字签名)。

您也可以在"mutual authentication"上使用SSL;服务器将作为SSL握手的一部分向客户端请求有效的证书。 服务器将通过检查其证书是否由可接受的权限进行签名来验证客户端。 如果提供了有效的证书,它可以通过应用程序中的servlet API获取。 Spring Security X.509模块使用过滤器提取证书。 它将证书映射到应用程序用户,并加载该用户的授权权限以供标准Spring Security基础结构使用。

在尝试将它用于Spring Security之前,您应该熟悉使用证书并为您的servlet容器设置客户端身份验证。 大部分工作是创建和安装合适的证书和密钥。 例如,如果您使用Tomcat,请阅读 http://tomcat.apache.org/tomcat-6.0-doc/ssl-howto.html中的说明。 在使用Spring Security进行试用之前,务必使用它

将X.509身份验证添加到您的Web应用程序中

启用X.509客户端身份验证非常简单。 只需将<x509/>元素添加到您的http安全性名称空间配置中即可。

<http>
...
	<x509 subject-principal-regex="CN=(.*?)," user-service-ref="userService"/>;
</http>

该元素有两个可选属性:

  • subject-principal-regex. 正则表达式用于从证书的主题名称中提取用户名。 上面显示了默认值。 这是传递给UserDetailsService的用户名,用于为用户加载权限。

  • user-service-ref. 这是与X.509一起使用的UserDetailsService的bean ID。 如果在应用程序上下文中只定义了一个,则不需要。

subject-principal-regex应该包含一个组。 例如,默认表达式"CN=(.*?),"与公共名称字段匹配。 因此,如果证书中的主题名称是"CN=Jimi Hendrix, OU=…​",则会给出用户名"Jimi Hendrix"。 这些匹配不区分大小写。 因此,"emailAddress=(.?),"将匹配"EMAILADDRESS=jimi@hendrix.org,CN=…​",并提供用户名"jimi@hendrix.org"。 如果客户端提交证书并且成功提取了有效用户名,则安全上下文中应该有一个有效的Authentication对象。 如果没有找到证书,或者找不到相应的用户,那么安全上下文将保持空白。 这意味着您可以使用其他选项(如基于表单的登录)轻松使用X.509身份验证。

在Tomcat中设置SSL

在Spring Security项目的samples/certificate目录中有一些预先生成的证书。 如果您不想生成自己的文件,您可以使用这些文件来启用SSL进行测试。 文件server.jks包含服务器证书,私钥和颁发证书颁发机构证书。 还有一些来自示例应用程序的用户的客户端证书文件。 您可以在浏览器中安装这些以启用SSL客户端身份验证。

要运行带有SSL支持的tomcat,请将server.jks文件放入tomcat conf目录,并将以下连接器添加到server.xml文件

<Connector port="8443" protocol="HTTP/1.1" SSLEnabled="true" scheme="https" secure="true"
			clientAuth="true" sslProtocol="TLS"
			keystoreFile="${catalina.home}/conf/server.jks"
			keystoreType="JKS" keystorePass="password"
			truststoreFile="${catalina.home}/conf/server.jks"
			truststoreType="JKS" truststorePass="password"
/>

如果即使客户端没有提供证书,仍然希望SSL连接成功,也可以将clientAuth设置为want。 除非使用非X.509身份验证机制(例如表单身份验证),否则未提供证书的客户将无法访问由Spring Security保护的任何对象。

运行身份验证替换

概述

AbstractSecurityInterceptor能够在安全对象回调阶段暂时替换SecurityContextSecurityContextHolder中的Authentication对象。 只有在AuthenticationManagerAccessDecisionManager成功处理原始Authentication对象时才会发生这种情况。 RunAsManager将指示应在SecurityInterceptorCallback期间使用的替换Authentication对象(如果有)。

通过在安全对象回调阶段暂时替换Authentication对象,受保护的调用将能够调用需要不同身份验证和授权凭证的其他对象。 它也能够对特定的GrantedAuthority对象执行任何内部安全检查。 由于Spring Security提供了许多帮助类,可以根据SecurityContextHolder的内容自动配置远程协议,因此这些运行替代在调用远程Web服务时特别有用

配置

Spring Security提供了一个RunAsManager接口:

Authentication buildRunAs(Authentication authentication, Object object,
	List<ConfigAttribute> config);

boolean supports(ConfigAttribute attribute);

boolean supports(Class clazz);

第一种方法返回应在方法调用期间替换现有的Authentication对象的Authentication对象。 如果该方法返回null,则表示不应该进行替换。 第二种方法被AbstractSecurityInterceptor用作配置属性启动验证的一部分。 supports(Class)方法由安全拦截器实现调用,以确保配置的RunAsManager支持安全拦截器将呈现的安全对象的类型。

Spring Security提供了一个RunAsManager的具体实现。 如果ConfigAttributeRUN_AS_开头,则RunAsManagerImpl类将返回替换RunAsUserToken。 如果找到任何此类ConfigAttribute,则替换RunAsUserToken将包含与原始Authentication对象相同的委托人,凭据和授予的权限,并为每个Authentication对象创建一个新的{{3} {4}} ConfigAttribute。 每个新的SimpleGrantedAuthority都会以ROLE_开头,后面跟RUN_AS ConfigAttribute。 例如,RUN_AS_SERVER将导致包含ROLE_RUN_AS_SERVER授权的替换RunAsUserToken

替代RunAsUserToken与其他Authentication对象一样。 它需要通过AuthenticationManager进行身份验证,可能通过委派给合适的AuthenticationProvider RunAsImplAuthenticationProvider执行此类身份验证。 它只是接受任何提供的RunAsUserToken有效。

为确保恶意代码不会创建RunAsUserToken并将其呈现为RunAsImplAuthenticationProvider保证接受,密钥的散列值将存储在所有生成的令牌中。 使用相同的键在bean上下文中创建RunAsManagerImplRunAsImplAuthenticationProvider

<bean id="runAsManager"
	class="org.springframework.security.access.intercept.RunAsManagerImpl">
<property name="key" value="my_run_as_password"/>
</bean>

<bean id="runAsAuthenticationProvider"
	class="org.springframework.security.access.intercept.RunAsImplAuthenticationProvider">
<property name="key" value="my_run_as_password"/>
</bean>

通过使用相同的密钥,每个RunAsUserToken都可以验证它是由批准的RunAsManagerImpl创建的。 出于安全原因,RunAsUserToken在创建之后是不可变的

Spring Security加密模块

介绍

Spring Security Crypto模块为对称加密,密钥生成和密码编码提供支持。 该代码作为核心模块的一部分进行分发,但不依赖于任何其他Spring Security(或Spring)代码。

加密器

Encryptors类提供了构造对称加密器的工厂方法。 使用这个类,您可以创建ByteEncryptors以原始字节[]形式加密数据。 您还可以构造TextEncryptors来加密文本字符串。 加密器是线程安全的。

BytesEncryptor

使用Encryptors.standard工厂方法构建一个"standard" BytesEncryptor:

Encryptors.standard("password", "salt");

"standard"加密方法是使用PKCS#5的PBKDF2(基于密码的密钥推导函数#2)的256位AES。 此方法需要Java 6。 用于生成SecretKey的密码应保存在安全的地方,不要共享。 salt用于防止在加密数据被泄露的情况下对字典进行字典攻击。 一个16字节的随机初始化向量也被应用,因此每个加密的消息都是唯一的。

提供的salt应该是以十六进制编码的字符串形式,是随机的,并且长度至少为8个字节。 这种盐可以使用KeyGenerator生成:

String salt = KeyGenerators.string().generateKey(); // generates a random 8-byte salt that is then hex-encoded

TextEncryptor

使用Encryptors.text工厂方法构建标准的TextEncryptor:

Encryptors.text("password", "salt");

TextEncryptor使用标准的BytesEncryptor来加密文本数据。 加密结果以十六进制编码字符串的形式返回,以便于在文件系统或数据库中存储。

使用Encryptors.queryableText工厂方法构建一个"queryable" TextEncryptor:

Encryptors.queryableText("password", "salt");

可查询TextEncryptor和标准TextEncryptor之间的区别与初始化向量(iv)处理有关。 在可查询的TextEncryptor#encrypt操作中使用的iv是共享的,或者是常量,并且不是随机生成的。 这意味着多次加密的相同文本将始终产生相同的加密结果。 这不太安全,但对于需要查询的加密数据来说是必需的。 可查询加密文本的一个例子是一个OAuth apiKey。

密钥生成器

KeyGenerators类提供了许多便捷工厂方法来构建不同类型的密钥生成器。 使用这个类,你可以创建一个BytesKeyGenerator来生成byte []键。 你也可以构造一个StringKeyGenerator来生成字符串键。 KeyGenerators是线程安全的。

BytesKeyGenerator

使用KeyGenerators.secureRandom工厂方法生成由SecureRandom实例支持的BytesKeyGenerator:

BytesKeyGenerator generator = KeyGenerators.secureRandom();
byte[] key = generator.generateKey();

默认密钥长度是8个字节。 还有一个可以控制密钥长度的KeyGenerators.secureRandom变体:

KeyGenerators.secureRandom(16);

使用KeyGenerators.shared工厂方法构造一个BytesKeyGenerator,它总是在每次调用时返回相同的键:

KeyGenerators.shared(16);

StringKeyGenerator

使用KeyGenerators.string工厂方法构造一个8字节的SecureRandom KeyGenerator,它将每个键的字符串编码为一个字符串:

KeyGenerators.string();

密码编码

spring-security-crypto模块的密码包提供对密码编码的支持。 PasswordEncoder是中央服务界面,具有以下签名:

public interface PasswordEncoder {

String encode(String rawPassword);

boolean matches(String rawPassword, String encodedPassword);
}

如果rawPassword一旦编码,就等于encodedPassword,则匹配方法返回true。 此方法旨在支持基于密码的身份验证方案。

BCryptPasswordEncoder实现使用广泛支持的"bcrypt"算法对密码进行哈希处理。 Bcrypt使用一个随机的16字节盐值,并且是故意缓慢的算法,以阻止密码破解者。 它所做的工作量可以使用值为4到31的"strength"参数进行调整。 值越高,计算散列的工作就越多。 默认值是10。 您可以在已部署的系统中更改此值,而不会影响现有密码,因为该值也存储在已编码的散列中。

// Create an encoder with strength 16
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(16);
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));

Pbkdf2PasswordEncoder实现使用PBKDF2算法来散列密码。 为了破解密码破解PBKDF2是一个故意缓慢的算法,应该调整大约0.5秒来验证系统上的密码。

// Create an encoder with all the defaults
Pbkdf2PasswordEncoder encoder = new Pbkdf2PasswordEncoder();
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));

并发支持

在大多数环境中,安全性是按照Thread存储的。 这意味着在新的Thread上完成工作时,SecurityContext将丢失。 Spring Security提供了一些基础设施来帮助用户更容易。 Spring Security在多线程环境中为Spring Security提供低级抽象。 事实上,这就是Spring Security与AsyncContext.start(可运行)Spring MVC异步集成集成的基础。

DelegatingSecurityContextRunnable

Spring Security的并发支持中最基本的构建块之一是DelegatingSecurityContextRunnable。 它包装委托Runnable,以便为委托初始化具有指定SecurityContextSecurityContextHolder。 然后调用委托Runnable,确保事后清除{{0}。DelegatingSecurityContextRunnable看起来像这样:

public void run() {
try {
	SecurityContextHolder.setContext(securityContext);
	delegate.run();
} finally {
	SecurityContextHolder.clearContext();
}
}

虽然非常简单,但它可以将SecurityContext从一个线程转移到另一个线程。 这很重要,因为在大多数情况下,SecurityContextHolder是以每个线程为基础进行操作的。 例如,您可能使用了Spring Security的<global-method-security>支持来保护您的某项服务。 您现在可以轻松地将当前Thread的{​​{0}}传输到调用安全服务的Thread。 下面是您可以如何做到这一点的一个例子:

Runnable originalRunnable = new Runnable() {
public void run() {
	// invoke secured service
}
};

SecurityContext context = SecurityContextHolder.getContext();
DelegatingSecurityContextRunnable wrappedRunnable =
	new DelegatingSecurityContextRunnable(originalRunnable, context);

new Thread(wrappedRunnable).start();

上面的代码执行以下步骤:

  • 创建一个将调用我们的安全服务的Runnable。 注意它不知道Spring Security

  • 获得我们希望从SecurityContextHolder使用的SecurityContext并初始化DelegatingSecurityContextRunnable

  • 使用DelegatingSecurityContextRunnable创建一个线程

  • 开始我们创建的主题

由于从SecurityContextHolder创建带有SecurityContext的{​​{0}}是很常见的,因此它有一个快捷构造器。 以下代码与上面的代码相同:

Runnable originalRunnable = new Runnable() {
public void run() {
	// invoke secured service
}
};

DelegatingSecurityContextRunnable wrappedRunnable =
	new DelegatingSecurityContextRunnable(originalRunnable);

new Thread(wrappedRunnable).start();

我们所使用的代码很容易使用,但它仍然需要我们使用Spring Security的知识。 在下一节中,我们将看看如何利用DelegatingSecurityContextExecutor来隐藏我们使用Spring Security的事实。

DelegatingSecurityContextExecutor

在上一节中,我们发现很容易使用DelegatingSecurityContextRunnable,但它并不理想,因为我们必须了解Spring Security才能使用它。 我们来看看DelegatingSecurityContextExecutor如何将我们的代码屏蔽掉我们使用Spring Security的任何知识。

DelegatingSecurityContextExecutor的设计与DelegatingSecurityContextRunnable非常相似,只是它接受委托Executor而不是委托Runnable。 你可以在下面看到一个如何使用它的例子:

SecurityContext context = SecurityContextHolder.createEmptyContext();
Authentication authentication =
	new UsernamePasswordAuthenticationToken("user","doesnotmatter", AuthorityUtils.createAuthorityList("ROLE_USER"));
context.setAuthentication(authentication);

SimpleAsyncTaskExecutor delegateExecutor =
	new SimpleAsyncTaskExecutor();
DelegatingSecurityContextExecutor executor =
	new DelegatingSecurityContextExecutor(delegateExecutor, context);

Runnable originalRunnable = new Runnable() {
public void run() {
	// invoke secured service
}
};

executor.execute(originalRunnable);

该代码执行以下步骤:

  • 创建SecurityContext用于我们的DelegatingSecurityContextExecutor。 请注意,在这个例子中,我们只需手工创建SecurityContext。 但是,无论我们在何处或如何获得SecurityContext(即,如果需要,我们可以从SecurityContextHolder获得),都无关紧要。

  • 创建一个负责执行提交的Runnable的委托执行程序

  • 最后,我们创建一个DelegatingSecurityContextExecutor,它负责封装用DelegatingSecurityContextRunnable传递给execute方法的任何Runnable。 然后它将封装的Runnable传递给delegateExecutor。 在这种情况下,将为提交给我们的DelegatingSecurityContextExecutor的每个Runnable使用相同的SecurityContext。 如果我们正在运行需要由具有提升权限的用户运行的后台任务,这很好。

  • 此时,您可能会问自己"How does this shield my code of any knowledge of Spring Security?"而不是在我们自己的代码中创建SecurityContextDelegatingSecurityContextExecutor,我们可以注入一个已经初始化的{{4 }}。

@Autowired
private Executor executor; // becomes an instance of our DelegatingSecurityContextExecutor

public void submitRunnable() {
Runnable originalRunnable = new Runnable() {
	public void run() {
	// invoke secured service
	}
};
executor.execute(originalRunnable);
}

现在我们的代码不知道SecurityContext正在传播到Thread,然后originalRunnable被执行,然后SecurityContextHolder被清除。 在这个例子中,正在使用同一个用户来执行每个线程。 如果我们想在我们调用executor.execute(Runnable)时(即当前登录的用户)使用来自SecurityContextHolder的用户来处理originalRunnable,该怎么办? 这可以通过从我们的DelegatingSecurityContextExecutor构造函数中删除SecurityContext参数来完成。 例如:

SimpleAsyncTaskExecutor delegateExecutor = new SimpleAsyncTaskExecutor();
DelegatingSecurityContextExecutor executor =
	new DelegatingSecurityContextExecutor(delegateExecutor);

现在,executor.execute(Runnable)随时执行,SecurityContext首先由SecurityContextHolder获得,然后SecurityContext用于创建我们的DelegatingSecurityContextRunnable。 这意味着我们使用与调用executor.execute(Runnable)代码相同的用户来执行我们的Runnable

Spring安全性并发类

请参阅Javadoc以获取与Java并发API和Spring Task抽象的其他集成。 一旦你理解了前面的代码,它们就不言自明了。

  • DelegatingSecurityContextCallable

  • DelegatingSecurityContextExecutor

  • DelegatingSecurityContextExecutorService

  • DelegatingSecurityContextRunnable

  • DelegatingSecurityContextScheduledExecutorService

  • DelegatingSecurityContextSchedulingTaskExecutor

  • DelegatingSecurityContextAsyncTaskExecutor

  • DelegatingSecurityContextTaskExecutor

Spring MVC集成

Spring Security提供了许多与Spring MVC的可选集成。 本节将更详细地介绍集成。

@ EnableWebMvcSecurity

注意:从Spring Security 4.0开始,@EnableWebMvcSecurity已被弃用。 替换是@EnableWebSecurity,它将确定基于类路径添加Spring MVC功能。

为了使Spring Security与Spring MVC集成,将@EnableWebSecurity注释添加到您的配置中。

注意:Spring Security使用Spring MVC的https://docs.spring.io/spring/docs/5.0.0.RELEASE/spring-framework-reference/web.html#mvc-config-customize[WebMvcConfigurer]提供配置。 这意味着如果您使用更高级的选项,如直接与WebMvcConfigurationSupport进行集成,则需要手动提供Spring Security配置。

MvcRequestMatcher

Spring Security提供了与Spring MVC如何在具有MvcRequestMatcher的URL上匹配的深度集成。 这有助于确保您的安全规则与用于处理您的请求的逻辑相匹配。

为了使用MvcRequestMatcher,您必须将Spring Security配置放置在与您的DispatcherServlet相同的ApplicationContext中。 这是非常必要的,因为Spring Security的MvcRequestMatcher期望名称为mvcHandlerMappingIntrospectorHandlerMappingIntrospector bean将被用于执行匹配的Spring MVC配置注册。

对于web.xml,这意味着您应该将配置置于DispatcherServlet.xml中。

<listener>
  <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

<!-- All Spring Configuration (both MVC and Security) are in /WEB-INF/spring/ -->
<context-param>
  <param-name>contextConfigLocation</param-name>
  <param-value>/WEB-INF/spring/*.xml</param-value>
</context-param>

<servlet>
  <servlet-name>spring</servlet-name>
  <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
  <!-- Load from the ContextLoaderListener -->
  <init-param>
    <param-name>contextConfigLocation</param-name>
    <param-value></param-value>
  </init-param>
</servlet>

<servlet-mapping>
  <servlet-name>spring</servlet-name>
  <url-pattern>/</url-pattern>
</servlet-mapping>

放置在DispatcherServlet ApplicationContext中的WebSecurityConfiguration下方。

public class SecurityInitializer extends
    AbstractAnnotationConfigDispatcherServletInitializer {

  @Override
  protected Class<?>[] getRootConfigClasses() {
    return null;
  }

  @Override
  protected Class<?>[] getServletConfigClasses() {
    return new Class[] { RootConfiguration.class,
        WebMvcConfiguration.class };
  }

  @Override
  protected String[] getServletMappings() {
    return new String[] { "/" };
  }
}
 

始终建议通过匹配HttpServletRequest和方法安全性来提供授权规则。

通过在HttpServletRequest上进行匹配提供授权规则是很好的,因为它在代码路径的早期发生并有助于减少https://en.wikipedia.org/wiki/Attack_surface[attack表面]。 方法安全性可确保如果有人绕过了Web授权规则,则您的应用程序仍处于安全状态。 这就是所谓的https://en.wikipedia.org/wiki/Defense_in_depth_(computing)[深度防御]

考虑一个映射如下的控制器:

@RequestMapping("/admin")
public String admin() {

如果我们想限制管理员用户访问此控制器方法,开发人员可以通过将HttpServletRequest与以下内容进行匹配来提供授权规则:

protected configure(HttpSecurity http) throws Exception {
	http
		.authorizeRequests()
			.antMatchers("/admin").hasRole("ADMIN");
}

或以XML格式

<http>
	<intercept-url pattern="/admin" access="hasRole('ADMIN')"/>
</http>

无论哪种配置,网址/admin都需要经过身份验证的用户成为管理员用户。 但是,根据我们的Spring MVC配置,URL /admin.html也将映射到我们的admin()方法。 另外,根据我们的Spring MVC配置,URL /admin/也会映射到我们的admin()方法。

问题是我们的安全规则仅保护/admin。 我们可以为Spring MVC的所有排列添加额外的规则,但这将是非常冗长和乏味的。

相反,我们可以利用Spring Security的MvcRequestMatcher。 以下配置将通过使用Spring MVC匹配URL来保护Spring MVC将匹配的相同URL。

protected configure(HttpSecurity http) throws Exception {
	http
		.authorizeRequests()
			.mvcMatchers("/admin").hasRole("ADMIN");
}

或以XML格式

<http request-matcher="mvc">
	<intercept-url pattern="/admin" access="hasRole('ADMIN')"/>
</http>

@ AuthenticationPrincipal

Spring Security提供了AuthenticationPrincipalArgumentResolver,它可以自动解析当前Authentication.getPrincipal()的Spring MVC参数。 通过使用@EnableWebSecurity,您将自动将它添加到您的Spring MVC配置中。 如果你使用基于XML的配置,你必须自己添加它。 例如:

<mvc:annotation-driven>
		<mvc:argument-resolvers>
				<bean class="org.springframework.security.web.method.annotation.AuthenticationPrincipalArgumentResolver" />
		</mvc:argument-resolvers>
</mvc:annotation-driven>

一旦AuthenticationPrincipalArgumentResolver被正确配置,您就可以在Spring MVC层完全脱离Spring Security。

考虑一种情况,即自定义UserDetailsService返回实现UserDetailsObject和您自己的CustomUser Object}。可以使用以下代码访问当前已认证用户的CustomUser

@RequestMapping("/messages/inbox")
public ModelAndView findMessagesForUser() {
	Authentication authentication =
	SecurityContextHolder.getContext().getAuthentication();
	CustomUser custom = (CustomUser) authentication == null ? null : authentication.getPrincipal();

	// .. find messages for this user and return them ...
}

从Spring Security 3.2开始,我们可以通过添加注释来更直接地解决争论。例如:

import org.springframework.security.core.annotation.AuthenticationPrincipal;

// ...

@RequestMapping("/messages/inbox")
public ModelAndView findMessagesForUser(@AuthenticationPrincipal CustomUser customUser) {

	// .. find messages for this user and return them ...
}

有时可能需要以某种方式改变校长。 例如,如果CustomUser需要是最终的,则无法扩展。 在这种情况下,UserDetailsService可能会返回实现UserDetailsObject,并提供一个名为getCustomUser的方法来访问CustomUser。 例如,它可能看起来像:

public class CustomUserUserDetails extends User {
		// ...
		public CustomUser getCustomUser() {
				return customUser;
		}
}

然后,我们可以使用使用Authentication.getPrincipal()的https://docs.spring.io/spring/docs/current/spring-framework-reference/html/expressions.html[SpEL表达式]来访问CustomUser。作为根对象:

import org.springframework.security.core.annotation.AuthenticationPrincipal;

// ...

@RequestMapping("/messages/inbox")
public ModelAndView findMessagesForUser(@AuthenticationPrincipal(expression = "customUser") CustomUser customUser) {

	// .. find messags for this user and return them ...
}

我们也可以在我们的SpEL表达式中引用Beans。 例如,如果我们使用JPA来管理用户,并且我们想修改并保存当前用户的属性,可以使用以下内容。

import org.springframework.security.core.annotation.AuthenticationPrincipal;

// ...

@PutMapping("/users/self")
public ModelAndView updateName(@AuthenticationPrincipal(expression = "@jpaEntityManager.merge(#this)") CustomUser attachedCustomUser,
		@RequestParam String firstName) {

	// change the firstName on an attached instance which will be persisted to the database
	attachedCustomUser.setFirstName(firstName);

	// ...
}

我们可以通过将@AuthenticationPrincipal作为我们自己注释中的元注释来进一步消除对Spring Security的依赖。 下面我们演示如何在名为@CurrentUser的注释中执行此操作。

注意:认识到为了消除对Spring Security的依赖是很重要的,消费应用程序会创建@CurrentUser。 这一步并非严格要求,但有助于将您对Spring Security的依赖隔离到更加中心的位置。

@Target({ElementType.PARAMETER, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@AuthenticationPrincipal
public @interface CurrentUser {}

现在@CurrentUser已经被指定,我们可以用它来发信号来解决我们当前认证用户的CustomUser。 我们还将Spring Security的依赖性分离为单个文件。

@RequestMapping("/messages/inbox")
public ModelAndView findMessagesForUser(@CurrentUser CustomUser customUser) {

	// .. find messages for this user and return them ...
}

Spring MVC异步集成

Spring Web MVC 3.2+对 异步请求处理提供了很好的支持。 无需额外配置,Spring Security会自动将SecurityContext设置为执行控制器返回的CallableThread。 例如,以下方法会自动将CallableCallable创建时可用的SecurityContext一起执行:

@RequestMapping(method=RequestMethod.POST)
public Callable<String> processUpload(final MultipartFile file) {

return new Callable<String>() {
	public Object call() throws Exception {
	// ...
	return "someView";
	}
};
}
 将SecurityContext分配给Callable的

从技术上讲,Spring Security与WebAsyncManager集成。 用于处理Callable的{​​{0}}是调用SecurityContextHolder时存在的SecurityContext

没有与控制器返回的DeferredResult的自动集成。 这是因为DeferredResult由用户处理,因此无法自动与它集成。 但是,您仍然可以使用Concurrency Support来提供与Spring Security的透明集成。

Spring MVC和CSRF集成

自动令牌包含

Spring Security将在使用 Spring MVC表单标签的表单中自动include the CSRF Token。 例如,以下JSP:

<jsp:root xmlns:jsp="http://java.sun.com/JSP/Page"
	xmlns:c="http://java.sun.com/jsp/jstl/core"
	xmlns:form="http://www.springframework.org/tags/form" version="2.0">
	<jsp:directive.page language="java" contentType="text/html" />
<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
	<!-- ... -->

	<c:url var="logoutUrl" value="/logout"/>
	<form:form action="${logoutUrl}"
		method="post">
	<input type="submit"
		value="Log out" />
	<input type="hidden"
		name="${_csrf.parameterName}"
		value="${_csrf.token}"/>
	</form:form>

	<!-- ... -->
</html>
</jsp:root>

将输出类似于以下内容的HTML:

<!-- ... -->

<form action="/context/logout" method="post">
<input type="submit" value="Log out"/>
<input type="hidden" name="_csrf" value="f81d4fae-7dec-11d0-a765-00a0c91e6bf6"/>
</form>

<!-- ... -->

解析CsrfToken

Spring Security提供了CsrfTokenArgumentResolver,它可以自动解析当前CsrfToken的Spring MVC参数。 通过使用@EnableWebSecurity,您将自动将它添加到您的Spring MVC配置中。 如果你使用基于XML的配置,你必须自己添加它。

CsrfTokenArgumentResolver配置正确后,您可以将CsrfToken公开给您的基于静态HTML的应用程序。

@RestController
public class CsrfController {

	@RequestMapping("/csrf")
	public CsrfToken csrf(CsrfToken token) {
		return token;
	}
}

保持CsrfToken与其他域的秘密非常重要。 这意味着如果您使用的是https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS[Cross Origin Sharing(CORS)],则应NOT公开CsrfToken到任何外部域名。

春季数据集成

Spring Security提供了Spring Data集成,允许在查询中引用当前用户。 这不仅有用,而且必须将用户包含在查询中以支持分页结果,因为之后过滤结果不会扩展。

Spring Data&Spring安全配置

要使用此支持,请提供一个SecurityEvaluationContextExtension类型的bean。 在Java配置中,这看起来像:

@Bean
public SecurityEvaluationContextExtension securityEvaluationContextExtension() {
	return new SecurityEvaluationContextExtension();
}

在XML配置中,这看起来像:

<bean class="org.springframework.security.data.repository.query.SecurityEvaluationContextExtension"/>

@Query中的== 安全表达式

现在Spring Security可以用在您的查询中。 例如:

@Repository
public interface MessageRepository extends PagingAndSortingRepository<Message,Long> {
	@Query("select m from Message m where m.to.id = ?#{ principal?.id }")
	Page<Message> findInbox(Pageable pageable);
}

这将检查Authentication.getPrincipal().getId()是否等于Message的收件人。 请注意,此示例假定您已将主体自定义为具有id属性的对象。 通过公开SecurityEvaluationContextExtension bean,所有Common Security Expressions在查询中都可用。

附录

安全数据库架构

框架使用了各种数据库模式,本附录为他们提供了一个单一的参考点。 您只需提供所需功能区域的表格。

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

用户架构=== UserDetailsServiceJdbcDaoImpl)的标准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);

组权限

Spring Security 2.0在JdbcDaoImpl中引入了对组权限的支持。 如果组已启用,则表结构如下。 您将需要调整此架构以匹配您正在使用的数据库方言。

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,那么只要满足界面合同,就可以完全自由地存储数据。

持久登录(记住我)架构

此表用于存储更安全的persistent token记住我实现所使用的数据。 如果您直接或通过名称空间使用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
);

ACL架构

Spring Security ACL实现使用了四个表。

  1. acl_sid存储由ACL系统识别的安全身份。 这些可以是适用于多个校长的独特校长或权威机构。

  2. acl_class定义ACL应用于的域对象类型。 class列存储对象的Java类名称。

  3. acl_object_identity存储特定域对象的对象标识定义。

  4. acl_entry存储适用于特定对象标识和安全标识的ACL权限。

假定数据库将自动生成每个身份的主键。 JdbcMutableAclService必须能够在acl_sidacl_class表中创建新行时检索这些行。 它有两个属性,用于定义检索这些值classIdentityQuerysidIdentityQuery所需的SQL。 这两个都默认为call identity()

ACL工件JAR包含用于在HyperSQL(HSQLDB),PostgreSQL,MySQL / MariaDB,Microsoft SQL Server和Oracle数据库中创建ACL模式的文件。 这些模式也在以下部分中进行了演示。

的HyperSQL

缺省模式与框架内单元测试中使用的嵌入式HSQLDB数据库一起使用。

create table acl_sid(
	id bigint generated by default as identity(start with 100) not null primary key,
	principal boolean not null,
	sid varchar_ignorecase(100) not null,
	constraint unique_uk_1 unique(sid,principal)
);

create table acl_class(
	id bigint generated by default as identity(start with 100) not null primary key,
	class varchar_ignorecase(100) not null,
	constraint unique_uk_2 unique(class)
);

create table acl_object_identity(
	id bigint generated by default as identity(start with 100) not null primary key,
	object_id_class bigint not null,
	object_id_identity varchar_ignorecase(36) not null,
	parent_object bigint,
	owner_sid bigint,
	entries_inheriting boolean not null,
	constraint unique_uk_3 unique(object_id_class,object_id_identity),
	constraint foreign_fk_1 foreign key(parent_object)references acl_object_identity(id),
	constraint foreign_fk_2 foreign key(object_id_class)references acl_class(id),
	constraint foreign_fk_3 foreign key(owner_sid)references acl_sid(id)
);

create table acl_entry(
	id bigint generated by default as identity(start with 100) not null primary key,
	acl_object_identity bigint not null,
	ace_order int not null,
	sid bigint not null,
	mask integer not null,
	granting boolean not null,
	audit_success boolean not null,
	audit_failure boolean not null,
	constraint unique_uk_4 unique(acl_object_identity,ace_order),
	constraint foreign_fk_4 foreign key(acl_object_identity) references acl_object_identity(id),
	constraint foreign_fk_5 foreign key(sid) references acl_sid(id)
);

的PostgreSQL

create table acl_sid(
	id bigserial not null primary key,
	principal boolean not null,
	sid varchar(100) not null,
	constraint unique_uk_1 unique(sid,principal)
);

create table acl_class(
	id bigserial not null primary key,
	class varchar(100) not null,
	constraint unique_uk_2 unique(class)
);

create table acl_object_identity(
	id bigserial primary key,
	object_id_class bigint not null,
	object_id_identity varchar(36) not null,
	parent_object bigint,
	owner_sid bigint,
	entries_inheriting boolean not null,
	constraint unique_uk_3 unique(object_id_class,object_id_identity),
	constraint foreign_fk_1 foreign key(parent_object)references acl_object_identity(id),
	constraint foreign_fk_2 foreign key(object_id_class)references acl_class(id),
	constraint foreign_fk_3 foreign key(owner_sid)references acl_sid(id)
);

create table acl_entry(
	id bigserial primary key,
	acl_object_identity bigint not null,
	ace_order int not null,
	sid bigint not null,
	mask integer not null,
	granting boolean not null,
	audit_success boolean not null,
	audit_failure boolean not null,
	constraint unique_uk_4 unique(acl_object_identity,ace_order),
	constraint foreign_fk_4 foreign key(acl_object_identity) references acl_object_identity(id),
	constraint foreign_fk_5 foreign key(sid) references acl_sid(id)
);

您必须分别将JdbcMutableAclServiceclassIdentityQuerysidIdentityQuery属性设置为以下值:

  • select currval(pg_get_serial_sequence('acl_class', 'id'))

  • select currval(pg_get_serial_sequence('acl_sid', 'id'))

MySQL和MariaDB

CREATE TABLE acl_sid (
	id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
	principal BOOLEAN NOT NULL,
	sid VARCHAR(100) NOT NULL,
	UNIQUE KEY unique_acl_sid (sid, principal)
) ENGINE=InnoDB;

CREATE TABLE acl_class (
	id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
	class VARCHAR(100) NOT NULL,
	UNIQUE KEY uk_acl_class (class)
) ENGINE=InnoDB;

CREATE TABLE acl_object_identity (
	id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
	object_id_class BIGINT UNSIGNED NOT NULL,
	object_id_identity VARCHAR(36) NOT NULL,
	parent_object BIGINT UNSIGNED,
	owner_sid BIGINT UNSIGNED,
	entries_inheriting BOOLEAN NOT NULL,
	UNIQUE KEY uk_acl_object_identity (object_id_class, object_id_identity),
	CONSTRAINT fk_acl_object_identity_parent FOREIGN KEY (parent_object) REFERENCES acl_object_identity (id),
	CONSTRAINT fk_acl_object_identity_class FOREIGN KEY (object_id_class) REFERENCES acl_class (id),
	CONSTRAINT fk_acl_object_identity_owner FOREIGN KEY (owner_sid) REFERENCES acl_sid (id)
) ENGINE=InnoDB;

CREATE TABLE acl_entry (
	id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
	acl_object_identity BIGINT UNSIGNED NOT NULL,
	ace_order INTEGER NOT NULL,
	sid BIGINT UNSIGNED NOT NULL,
	mask INTEGER UNSIGNED NOT NULL,
	granting BOOLEAN NOT NULL,
	audit_success BOOLEAN NOT NULL,
	audit_failure BOOLEAN NOT NULL,
	UNIQUE KEY unique_acl_entry (acl_object_identity, ace_order),
	CONSTRAINT fk_acl_entry_object FOREIGN KEY (acl_object_identity) REFERENCES acl_object_identity (id),
	CONSTRAINT fk_acl_entry_acl FOREIGN KEY (sid) REFERENCES acl_sid (id)
) ENGINE=InnoDB;

Microsoft SQL Server

CREATE TABLE acl_sid (
	id BIGINT NOT NULL IDENTITY PRIMARY KEY,
	principal BIT NOT NULL,
	sid VARCHAR(100) NOT NULL,
	CONSTRAINT unique_acl_sid UNIQUE (sid, principal)
);

CREATE TABLE acl_class (
	id BIGINT NOT NULL IDENTITY PRIMARY KEY,
	class VARCHAR(100) NOT NULL,
	CONSTRAINT uk_acl_class UNIQUE (class)
);

CREATE TABLE acl_object_identity (
	id BIGINT NOT NULL IDENTITY PRIMARY KEY,
	object_id_class BIGINT NOT NULL,
	object_id_identity VARCHAR(36) NOT NULL,
	parent_object BIGINT,
	owner_sid BIGINT,
	entries_inheriting BIT NOT NULL,
	CONSTRAINT uk_acl_object_identity UNIQUE (object_id_class, object_id_identity),
	CONSTRAINT fk_acl_object_identity_parent FOREIGN KEY (parent_object) REFERENCES acl_object_identity (id),
	CONSTRAINT fk_acl_object_identity_class FOREIGN KEY (object_id_class) REFERENCES acl_class (id),
	CONSTRAINT fk_acl_object_identity_owner FOREIGN KEY (owner_sid) REFERENCES acl_sid (id)
);

CREATE TABLE acl_entry (
	id BIGINT NOT NULL IDENTITY PRIMARY KEY,
	acl_object_identity BIGINT NOT NULL,
	ace_order INTEGER NOT NULL,
	sid BIGINT NOT NULL,
	mask INTEGER NOT NULL,
	granting BIT NOT NULL,
	audit_success BIT NOT NULL,
	audit_failure BIT NOT NULL,
	CONSTRAINT unique_acl_entry UNIQUE (acl_object_identity, ace_order),
	CONSTRAINT fk_acl_entry_object FOREIGN KEY (acl_object_identity) REFERENCES acl_object_identity (id),
	CONSTRAINT fk_acl_entry_acl FOREIGN KEY (sid) REFERENCES acl_sid (id)
);

Oracle数据库

CREATE TABLE acl_sid (
	id NUMBER(38) NOT NULL PRIMARY KEY,
	principal NUMBER(1) NOT NULL CHECK (principal in (0, 1)),
	sid NVARCHAR2(100) NOT NULL,
	CONSTRAINT unique_acl_sid UNIQUE (sid, principal)
);
CREATE SEQUENCE acl_sid_sequence START WITH 1 INCREMENT BY 1 NOMAXVALUE;
CREATE OR REPLACE TRIGGER acl_sid_id_trigger
	BEFORE INSERT ON acl_sid
	FOR EACH ROW
BEGIN
	SELECT acl_sid_sequence.nextval INTO :new.id FROM dual;
END;

CREATE TABLE acl_class (
	id NUMBER(38) NOT NULL PRIMARY KEY,
	class NVARCHAR2(100) NOT NULL,
	CONSTRAINT uk_acl_class UNIQUE (class)
);
CREATE SEQUENCE acl_class_sequence START WITH 1 INCREMENT BY 1 NOMAXVALUE;
CREATE OR REPLACE TRIGGER acl_class_id_trigger
	BEFORE INSERT ON acl_class
	FOR EACH ROW
BEGIN
	SELECT acl_class_sequence.nextval INTO :new.id FROM dual;
END;

CREATE TABLE acl_object_identity (
	id NUMBER(38) NOT NULL PRIMARY KEY,
	object_id_class NUMBER(38) NOT NULL,
	object_id_identity NVARCHAR2(36) NOT NULL,
	parent_object NUMBER(38),
	owner_sid NUMBER(38),
	entries_inheriting NUMBER(1) NOT NULL CHECK (entries_inheriting in (0, 1)),
	CONSTRAINT uk_acl_object_identity UNIQUE (object_id_class, object_id_identity),
	CONSTRAINT fk_acl_object_identity_parent FOREIGN KEY (parent_object) REFERENCES acl_object_identity (id),
	CONSTRAINT fk_acl_object_identity_class FOREIGN KEY (object_id_class) REFERENCES acl_class (id),
	CONSTRAINT fk_acl_object_identity_owner FOREIGN KEY (owner_sid) REFERENCES acl_sid (id)
);
CREATE SEQUENCE acl_object_identity_sequence START WITH 1 INCREMENT BY 1 NOMAXVALUE;
CREATE OR REPLACE TRIGGER acl_object_identity_id_trigger
	BEFORE INSERT ON acl_object_identity
	FOR EACH ROW
BEGIN
	SELECT acl_object_identity_sequence.nextval INTO :new.id FROM dual;
END;

CREATE TABLE acl_entry (
	id NUMBER(38) NOT NULL PRIMARY KEY,
	acl_object_identity NUMBER(38) NOT NULL,
	ace_order INTEGER NOT NULL,
	sid NUMBER(38) NOT NULL,
	mask INTEGER NOT NULL,
	granting NUMBER(1) NOT NULL CHECK (granting in (0, 1)),
	audit_success NUMBER(1) NOT NULL CHECK (audit_success in (0, 1)),
	audit_failure NUMBER(1) NOT NULL CHECK (audit_failure in (0, 1)),
	CONSTRAINT unique_acl_entry UNIQUE (acl_object_identity, ace_order),
	CONSTRAINT fk_acl_entry_object FOREIGN KEY (acl_object_identity) REFERENCES acl_object_identity (id),
	CONSTRAINT fk_acl_entry_acl FOREIGN KEY (sid) REFERENCES acl_sid (id)
);
CREATE SEQUENCE acl_entry_sequence START WITH 1 INCREMENT BY 1 NOMAXVALUE;
CREATE OR REPLACE TRIGGER acl_entry_id_trigger
	BEFORE INSERT ON acl_entry
	FOR EACH ROW
BEGIN
	SELECT acl_entry_sequence.nextval INTO :new.id FROM dual;
END;

安全名称空间

本附录提供了对安全性命名空间中可用元素的引用以及他们创建的底层bean的信息(假定了解各个类以及它们如何一起工作 - 您可以在项目Javadoc和本文档中的其他地方找到更多信息)。 如果您之前没有使用过命名空间,请阅读命名空间配置上的introductory chapter,因为这是对此处信息的补充。 建议在编辑基于模式的配置时使用高质量的XML编辑器,因为这将提供关于哪些元素和属性可用的上下文信息以及解释其用途的注释。 命名空间是用 RELAX NG压缩格式编写的,后来转换为XSD模式。 如果您熟悉这种格式,您可能希望查看https://raw.githubusercontent.com/spring-projects/spring-security/master/config/src/main/resources/org/springframework/security/config/ spring-security-4.1.rnc [模式文件]直接。

Web应用程序安全性

<debug>

启用S​​pring Security调试基础架构。 这将提供可读的(多行)调试信息来监视进入安全过滤器的请求。 这可能包括敏感信息,如请求参数或标题,并且只能在开发环境中使用。

<http>

如果您在应用程序中使用<http>元素,则会创建一个名为"springSecurityFilterChain"的FilterChainProxy bean,并使用元素内的配置在内部构建过滤器链 FilterChainProxy. 从Spring Security 3.1开始,可以使用额外的http元素添加额外的过滤器链脚注:[请参阅pass:specialcharacters,宏[introductory chapter]了解如何设置来自{{2 }}]。 一些核心过滤器总是在过滤器链中创建,而其他核心过滤器将根据存在的属性和子元素添加到堆栈中。 标准过滤器的位置是固定的(请参阅 命名空间简介中的the filter order table),当用户必须显式配置过滤器链时,用以前版本的框架消除常见的错误来源 FilterChainProxy bean。 当然,如果您需要完全控制配置,您仍然可以这样做。

所有需要引用AuthenticationManager的过滤器将自动注入由命名空间配置创建的内部实例(有关AuthenticationManager的更多信息,请参阅introductory chapter)。

每个<http>命名空间块始终创建一个SecurityContextPersistenceFilter,一个ExceptionTranslationFilter和一个FilterSecurityInterceptor。 这些是固定的,不能用替代品替代。

<http>属性

<http>元素上的属性控制核心过滤器的一些属性。

  • access-decision-manager-ref 可选属性,指定应用于授权HTTP请求的AccessDecisionManager实现的ID。 默认情况下,AffirmativeBased实现用于RoleVoterAuthenticatedVoter

  • authentication-manager-ref 对由此http元素创建的FilterChain使用的AuthenticationManager的引用。

  • auto-config 自动注册登录表单,BASIC认证,注销服务。 如果设置为"true",则会添加所有这些功能(尽管您仍然可以通过提供相应的元素来自定义每个功能的配置)。 如果未指定,则默认为"false"。 不建议使用此属性。 使用显式配置元素来避免混淆。

  • create-session 控制Spring安全类创建HTTP会话的热切程度。 选项包括:

    • always - 如果Spring Security不存在,Spring会主动创建一个会话。

    • ifRequired - Spring Security只会在需要时创建一个会话(默认值)。

    • never - Spring Security永远不会创建一个会话,但如果应用程序会使用一个会话。

    • stateless - Spring Security不会创建会话并忽略会话以获取Spring Authentication

  • disable-url-rewriting 防止将会话ID附加到应用程序中的URL上。 如果此属性设置为true,则客户端必须使用Cookie。 默认值是true

  • entry-point-ref 通常,所使用的AuthenticationEntryPoint将根据已配置的认证机制进行设置。 此属性允许通过定义将启动身份验证过程的自定义AuthenticationEntryPoint bean来覆盖此行为。

  • jaas-api-provision 如果可用,请将请求作为从JaasAuthenticationToken获取的Subject来运行,该JaasAuthenticationToken通过将JaasApiIntegrationFilter bean添加到堆栈来实现。 默认为false

  • name 一个bean标识符,用于在上下文中的其他地方引用bean。

  • once-per-request 对应于FilterSecurityInterceptor的{​​{0}}属性。 默认为true

  • pattern 为http元素定义模式控制将通过其定义的过滤器列表进行过滤的请求。 解释取决于配置的request-matcher。 如果未定义模式,则所有请求都将匹配,因此应首先声明最具体的模式。

  • realm 设置用于基本身份验证的领域名称(如果启用)。 对应BasicAuthenticationEntryPoint上的realmName属性。

  • request-matcher 定义FilterChainProxy中使用的RequestMatcher策略和intercept-url创建的bean匹配传入请求。 对于Spring MVC,ant,正则表达式和不区分大小写的正则表达式,选项分别为mvcantregexciRegex。 使用其patternmethodservlet-path属性为每个intercept-url元素创建单独的实例。 使用AntPathRequestMatcher匹配Ant路径,使用RegexRequestMatcher匹配正则表达式,并使用匹配MvcRequestMatcher的Spring MVC路径。 有关这些类的详细信息,请参阅Javadoc以了解如何执行匹配。 Ant路径是默认策略。

  • request-matcher-ref 对实现RequestMatcher的bean的引用,它将确定是否应使用此FilterChain。 这是pattern的更强大替代方案。

  • security 通过将此属性设置为none,可将请求模式映射到空的过滤器链。 没有安全性将被应用,Spring Security的功能都不可用。

  • security-context-repository-ref 允许在SecurityContextPersistenceFilter中注入自定义SecurityContextRepository

  • servlet-api-provision 提供HttpServletRequest安全方法的版本,例如isUserInRole()getPrincipal(),这些方法通过在堆栈中添加一个SecurityContextHolderAwareRequestFilter bean来实现。 默认为true

<access-denied-handler>

此元素允许您使用error-page属性为ExceptionTranslationFilter使用的默认AccessDeniedHandler设置errorPage属性,或者使用{{4 }}属性。 这在ExceptionTranslationFilter的部分有更详细的讨论。

<access-denied-handler>的{​​{0}}父元素

<access-denied-handler>属性
  • error-page 访问被拒绝的页面,认证用户将被重定向到,如果他们请求他们没有权限访问的页面。

  • ref 定义对AccessDeniedHandler类型的Spring bean的引用。

<cors>

该元素允许配置CorsFilter。 如果未指定CorsFilterCorsConfigurationSource,并且Spring MVC位于类路径中,则HandlerMappingIntrospector将用作CorsConfigurationSource

<cors>属性

<cors>元素上的属性控制标题元素。

  • ref 可选属性,指定CorsFilter的bean名称。

  • cors-configuration-source-ref 可选属性,指定要注入到由XML命名空间创建的CorsFilter中的CorsConfigurationSource的bean名称。

<cors>的{​​{0}}父元素

<headers>

该元素允许配置附加(安全)头与响应一起发送。 它可以轻松配置多个标题,还可以通过header元素设置自定义标题。 其他信息可以在参考文献的Security Headers部分找到。

  • Cache-ControlPragmaExpires - 可以使用cache-control元素进行设置。 这可确保浏览器不会缓存受保护的页面。

  • Strict-Transport-Security - 可以使用hsts元素进行设置。 这可确保浏览器自动为未来的请求请求HTTPS。

  • X-Frame-Options - 可以使用frame-options元素进行设置。 X-框架,选项标头可用于防止点击劫持攻击。

  • X-XSS-Protection - 可以使用xss-protection元素进行设置。 浏览器可以使用 X-XSS-保护标题进行基本控制。

  • X-Content-Type-Options - 可以使用content-type-options元素进行设置。 X-Content-Type的选项标题阻止Internet Explorer从MIME中嗅探到已声明的内容类型的响应。 在下载扩展程序时,这也适用于Google Chrome。

  • Public-Key-PinningPublic-Key-Pinning-Report-Only - 可以使用hpkp元素进行设置。 这使得HTTPS网站能够抵御攻击者使用错误发布或其他欺诈性证书的冒充行为。

  • Content-Security-PolicyContent-Security-Policy-Report-Only - 可以使用content-security-policy元素进行设置。 https://www.w3.org/TR/CSP2/ [内容安全策略(CSP)]是Web应用程序可以利用的机制来缓解内容注入漏洞,例如跨站点脚本(XSS)。

  • Referrer-Policy - 可以使用referrer-policy元素进行设置,https://www.w3.org/TR/referrer-policy/[Referrer-Policy]是一种Web应用程序可以利用来管理引荐来源字段,其中包含用户所在的最后一页。

<headers>属性

<headers>元素上的属性控制标题元素。

  • defaults-disabled 可选属性,用于指定禁用默认Spring Security的HTTP响应标头。 默认值为false(包含默认标题)。

  • disabled 可选属性,指定禁用Spring Security的HTTP响应标头。 默认值为false(标题已启用)。

<headers>的{​​{0}}父元素

<headers>的{​​{0}}子元素

<cache-control>

添加Cache-ControlPragmaExpires标头,以确保浏览器不会缓存受保护的页面。

<cache-control>属性
  • disabled 指定是否应禁用缓存控制。 默认为false。

<cache-control>的{​​{0}}父元素

<hsts>

启用后,将 严格,运输和安全标头添加到任何安全请求的响应中。 这允许服务器指示浏览器为将来的请求自动使用HTTPS。

<hsts>属性
  • disabled 指定是否应禁用Strict-Transport-Security。 默认为false。

  • include-sub-domains 指定是否应包含子域。 默认为true。

  • max-age-seconds 指定主机应被视为已知HSTS主机的最长时间。 默认一年。

  • request-matcher-ref RequestMatcher实例用于确定是否应设置标题。 如果HttpServletRequest.isSecure()为true,则默认为。

<hsts>的{​​{0}}父元素

<hpkp>

启用后,将针对任何安全请求的https://tools.ietf.org/html/rfc7469[Public Key Pinning Extension for HTTP]标头添加到响应中。 这使得HTTPS网站能够抵御攻击者使用错误发布或其他欺诈性证书的冒充行为。

<hpkp>属性
  • disabled 指定是否应禁用HTTP公钥关联(HPKP)。 默认为true。

  • include-sub-domains 指定是否应包含子域。 默认为false。

  • max-age-seconds 设置Public-Key-Pins标头的max-age指令的值。 默认60天。

  • report-only 指定浏览器是否应报告引脚验证失败。 默认为true。

  • report-uri 指定浏览器应报告引脚验证失败的URI。

<hpkp>的{​​{0}}父元素

<pins>

引脚列表

<pins>的{​​{0}}子元素

<pin>

使用base64编码的SPKI指纹作为值并将加密散列算法用作属性来指定针脚

<pin>属性
  • algorithm 密码散列算法。 默认是SHA256。

<pin>的{​​{0}}父元素

<content-security-policy>

启用后,将https://www.w3.org/TR/CSP2/ [内容安全策略(CSP)]标头添加到响应中。 CSP是Web应用程序可以利用的机制来缓解内容注入漏洞,例如跨站点脚本(XSS)。

<content-security-policy>属性
  • policy-directives Content-Security-Policy头的安全策略指令或者仅report-only设置为true,则使用Content-Security-Policy-Report-Only头。

  • report-only 设置为true,仅启用仅用于报告策略违规的Content-Security-Policy-Report-Only标头。 默认为false。

<content-security-policy>的{​​{0}}父元素

<referrer-policy>

启用后,将https://www.w3.org/TR/referrer-policy/[Referrer Policy]标头添加到响应中。

<referrer-policy>属性
  • policy Referrer-Policy标头的策略。 默认"no-referrer"。

<referrer-policy>的{​​{0}}父元素

<frame-options>

启用后,将 X-Frame-Options标题添加到响应中,这允许较新的浏览器执行一些安全检查并防止 点击劫持攻击。

<frame-options>属性
  • disabled 如果禁用,则不包括X-Frame-Options标头。 默认为false。

  • policy

    • DENY无论站点尝试这样做,页面都不能显示在框架中。 这是指定frame-options-policy时的默认值。

    • SAMEORIGIN该页面只能以与页面本身相同的原点显示在框架中

    • ALLOW-FROM origin页面只能显示在指定原点的框架中。

    换句话说,如果你指定了DENY,那么当从其他站点加载时,不仅会尝试在一个框架中加载页面,当从同一站点加载时,尝试这样做将失败。 另一方面,如果指定了SAMEORIGIN,只要网站将其包含在一个框架中,就可以使用该框架中的页面,即可与该页面提供的页面相同。

  • strategy 选择使用ALLOW-FROM策略时要使用的AllowFromStrategy

    • static使用单个静态ALLOW-FROM值。 该值可以通过value属性进行设置。

    • regexp使用regelur表达式来验证传入的请求,以及它们是否被允许。 正则表达式可以通过value属性设置。 用于检索要验证的值的请求参数可以使用from-parameter指定。

    • whitelist包含允许的域的逗号分隔列表。 逗号分隔列表可以通过value属性设置。 用于检索要验证的值的请求参数可以使用from-parameter指定。

  • ref 也可以使用自定义AllowFromStrategy,而不是使用其中一种预定义的策略。 这个bean的引用可以通过这个ref属性来指定。

  • value 使用ALLOW-FROM时使用的值是strategy

  • from-parameter 指定ALLOW-FROM策略使用正则表达式或白名单时要使用的请求参数的名称。

<frame-options>的{​​{0}}父元素

<xss-protection>

 X-XSS-Protection头添加到回复中,以帮助防范 反射/ Type-1跨站点脚本(XSS)次攻击。 这绝对不能完全保护XSS攻击!

<xss-protection>属性
  • xss-protection-block 当true和xss-protection-enabled为true时,将mode = block添加到标题。 这向浏览器表明该页面不应该被加载。 当false和xss-protection-enabled为真时,当检测到反射攻击时,页面仍将呈现,但响应将被修改以防止攻击。 请注意,有时会绕过此模式,这通常会使页面更加受欢迎。

<xss-protection>的{​​{0}}父元素

<content-type-options>

将值为nosniff的X-Content-Type-Options标头添加到响应中。 禁用MIME嗅探适用于IE8 +和Chrome扩展程序。

<content-type-options>属性
  • disabled 指定是否应禁用内容类型选项。 默认为false。

<content-type-options>的{​​{0}}父元素

<header>

将其他标题添加到响应中,需要指定名称和值。

<header-attributes>属性
  • header-name 标题的name

  • value 要添加的标题的value

  • ref 引用HeaderWriter界面的自定义实现。

<header>的{​​{0}}父元素

<anonymous>

AnonymousAuthenticationFilter添加到堆栈并添加AnonymousAuthenticationProvider。 如果您使用IS_AUTHENTICATED_ANONYMOUSLY属性,则为必需。

<anonymous>的{​​{0}}父元素

<anonymous>属性
  • enabled 通过默认名称空间设置,匿名"authentication"设施将自动启用。 您可以使用此属性禁用它。

  • granted-authority 应该分配给匿名请求的授予权限。 通常这是用来分配匿名请求的特定角色,随后可以用于授权决策。 如果未设置,则默认为ROLE_ANONYMOUS

  • key 提供者和过滤器之间共享的关键。 这通常不需要设置。 如果未设置,它将默认为安全随机生成的值。 这意味着设置此值可以改善使用匿名功能时的启动时间,因为安全随机值可能需要一段时间才能生成。

  • username 应分配给匿名请求的用户名。 这样可以识别委托人,这对于记录和审计可能很重要。 如果未设置,则默认为anonymousUser

<csrf>

该元素将为应用程序添加 跨站请求伪造(CSRF)保护。 它还将默认的RequestCache更新为仅在成功验证后重播"GET"请求。 其他信息可以在参考文献的Cross Site Request Forgery (CSRF)部分找到。

<csrf>的{​​{0}}父元素

<csrf>属性
  • disabled 可选属性,指定禁用Spring Security的CSRF保护。 默认值为false(启用CSRF保护)。 强烈建议启用CSRF保护。

  • token-repository-ref 使用CsrfTokenRepository。 默认值是HttpSessionCsrfTokenRepository

  • request-matcher-ref 要用来确定是否应用CSRF的RequestMatcher实例。 默认值是除"GET","TRACE","HEAD","OPTIONS"以外的任何HTTP方法。

<custom-filter>

该元素用于向过滤器链添加过滤器。 它不会创建任何额外的bean,但用​​于选择已在应用程序上下文中定义的javax.servlet.Filter类型的bean,并将其添加到由Spring Security维护的过滤器链中的特定位置。 完整的详细信息可以在namespace chapter中找到。

<custom-filter>的{​​{0}}父元素

<custom-filter>属性
  • after 紧随其后的过滤器应放置在链中。 只有希望将自己的过滤器组合到安全过滤器链中并具有标准Spring Security过滤器知识的高级用户才需要此功能。 筛选器名称映射到特定的Spring Security实现筛选器。

  • before 自定义过滤器应该放置在链中之前的过滤器

  • position 自定义过滤器应放置在链中的显式位置。 如果您要更换标准过滤器,请使用此选项

  • ref 定义对实现Filter的Spring bean的引用。

<expression-handler>

定义如果启用基于表达式的访问控制,将使用的SecurityExpressionHandler实例。 如果不提供,将使用默认实现(不支持ACL)。

<expression-handler>的{​​{0}}父元素

<expression-handler>属性
  • ref 定义对实现SecurityExpressionHandler的Spring bean的引用。

<form-login>

用于将UsernamePasswordAuthenticationFilter添加到过滤器堆栈,并将LoginUrlAuthenticationEntryPoint添加到应用程序上下文以根据需要提供身份验证。 这将始终优先于其他命名空间创建的入口点。 如果未提供任何属性,则会在URL "/login"脚注中自动生成登录页面:[ 此功能实际上只是为了方便而提供的,并不适用于生产(视图技术将被选择并可用于呈现定制登录页面)。 类DefaultLoginPageGeneratingFilter负责呈现登录页面,并在必要时为常规表单登录和/或OpenID提供登录表单。 ]行为可以使用<form-login> Attributes进行自定义。

<form-login>的{​​{0}}父元素

<form-login>属性
  • always-use-default-target 如果设置为true,则用户始终以default-target-url给定的值开始,而不管它们是如何到达登录页面的。 映射到UsernamePasswordAuthenticationFilter的{​​{0}}属性。 默认值是false

  • authentication-details-source-ref 引用将由认证过滤器使用的AuthenticationDetailsSource

  • authentication-failure-handler-ref 可以用作authentication-failure-url的替代方法,让您在验​​证失败后完全控制导航流。 该值应该是应用程序上下文中的AuthenticationFailureHandler bean的名称。

  • authentication-failure-url 映射到UsernamePasswordAuthenticationFilter的{​​{0}}属性。 定义浏览器在登录失败时将被重定向到的URL。 默认为/login?error,将由自动登录页面生成器自动处理,并用错误消息重新呈现登录页面。

  • authentication-success-handler-ref 这可以用作default-target-urlalways-use-default-target的替代方案,让您在成功验证后完全控制导航流程。 该值应该是应用程序上下文中的AuthenticationSuccessHandler bean的名称。 默认情况下,使用SavedRequestAwareAuthenticationSuccessHandler的实现,并使用default-target-url注入。

  • default-target-url 映射到UsernamePasswordAuthenticationFilter的{​​{0}}属性。 如果未设置,则默认值为"/"(应用程序根目录)。 如果用户在尝试访问受保护的资源时没有被要求登录,并将其带到最初请求的URL,则用户将在登录后进入该URL。

  • login-page 应该用于呈现登录页面的URL。 映射到LoginUrlAuthenticationEntryPoint的{​​{0}}属性。 默认为"/login"。

  • login-processing-url 映射到UsernamePasswordAuthenticationFilter的{​​{0}}属性。 默认值是"/login"。

  • password-parameter 包含密码的请求参数的名称。 默认为"password"。

  • username-parameter 包含用户名的请求参数的名称。 默认为"username"。

  • authentication-success-forward-url 将ForwardAuthenticationSuccessHandler映射到UsernamePasswordAuthenticationFilterauthenticationSuccessHandler属性。

  • authentication-failure-forward-url 将ForwardAuthenticationFailureHandler映射到UsernamePasswordAuthenticationFilterauthenticationFailureHandler属性。

<http-basic>

向配置添加BasicAuthenticationFilterBasicAuthenticationEntryPoint。 如果未启用基于表单的登录,后者将仅用作配置入口点。

<http-basic>的{​​{0}}父元素

<http-basic>属性
  • authentication-details-source-ref 引用将由认证过滤器使用的AuthenticationDetailsSource

  • entry-point-ref 设置BasicAuthenticationFilter使用的AuthenticationEntryPoint

<http-firewall>元素

这是一个顶级元素,可用于将HttpFirewall的自定义实现注入到名称空间创建的FilterChainProxy中。 默认实现应该适用于大多数应用程序。

<http-firewall>属性
  • ref 定义对实现HttpFirewall的Spring bean的引用。

<intercept-url>

此元素用于定义应用程序感兴趣的一组URL模式并配置应该如何处理它们。 它用于构造FilterSecurityInterceptor使用的FilterInvocationSecurityMetadataSource。 例如,如果特定的URL需要通过HTTPS访问,它还负责配置ChannelProcessingFilter。 当针对传入请求匹配指定模式时,匹配按声明元素的顺序完成。 所以最具体的模式应该是第一位的,最普通的模式应该是最后一位。

<intercept-url>的{​​{0}}父元素

<intercept-url>属性
  • access 列出将存储在定义的URL模式/方法组合的FilterInvocationSecurityMetadataSource中的访问属性。 这应该是安全配置属性(如角色名称)的以逗号分隔的列表。

  • filters 只能取值"none"。 这将导致任何匹配请求完全绕过Spring Security过滤器链。 <http>配置的其余部分都不会对请求产生任何影响,并且在其持续时间内不会有可用的安全上下文。 请求期间访问受保护的方法将失败。

注意:此属性对filter-security-metadata-source无效

  • method HTTP方法将与模式和servlet路径(可选)结合使用以匹配传入的请求。 如果省略,任何方法都会匹配。 如果使用和不使用方法指定了相同的模式,则特定于方法的匹配将优先。

  • pattern 定义URL路径的模式。 内容将取决于来自包含http元素的request-matcher属性,所以默认为ant路径语法。

  • request-matcher-ref 对RequestMatcher的引用,将用于确定是否使用此<intercept-url>

  • requires-channel 可以是"http"或"https",具体取决于是否应分别通过HTTP或HTTPS访问特定的URL模式。 或者,当没有偏好时,可以使用值"any"。 如果此属性出现在任何<intercept-url>元素上,则ChannelProcessingFilter将被添加到过滤器堆栈,并将其附加依赖项添加到应用程序上下文中。

如果添加了<port-mappings>配置,SecureChannelProcessorInsecureChannelProcessor bean将用于确定用于重定向到HTTP / HTTPS的端口。

注意:此属性对filter-security-metadata-source无效

  • servlet-path 将与模式和HTTP方法结合使用以匹配传入请求的servlet路径。 此属性仅适用于request-matcher为'mvc'时。 此外,该值仅在以下两种用例中需要:1)ServletContext中注册的HttpServlet中有2个或更多HttpServlet映射的映射以'/'开头,且映射不同; 2)该模式以注册HttpServlet路径的相同值开始,不包括默认(根)HttpServlet '/'

注意:此属性对filter-security-metadata-source无效

<jee>

将J2eePreAuthenticatedProcessingFilter添加到过滤器链以提供与容器验证的集成。

<jee>的{​​{0}}父元素

<jee>属性
  • mappable-roles 在传入的HttpServletRequest中查找的逗号分隔列表。

  • user-service-ref 对用户服务(或UserDetailsS​​ervice bean)Id的引用

<logout>

LogoutFilter添加到过滤器堆栈。 这是使用SecurityContextLogoutHandler配置的。

<logout>的{​​{0}}父元素

<logout>属性
  • delete-cookies 用户注销时应删除的Cookie的名称的逗号分隔列表。

  • invalidate-session 映射到SecurityContextLogoutHandler的{​​{0}}。 默认为"true",所以会话将在注销时失效。

  • logout-success-url 注销后用户将访问的目标网址。 默认为<form-login-login-page> /?注销(即/ login?注销)

    设置此属性将为SessionManagementFilter注入配置了属性值的SimpleRedirectInvalidSessionStrategy。 当提交无效的会话ID时,该策略将被调用,重定向到配置的URL。

  • logout-url 将导致注销(即将由过滤器处理)的URL。 默认为"/logout"。

  • success-handler-ref 可用于提供LogoutSuccessHandler的实例,该实例将在注销后调用以控制导航。

<openid-login>

<form-login>类似,具有相同的属性。 login-processing-url的默认值是"/login/openid"。 一个OpenIDAuthenticationFilterOpenIDAuthenticationProvider将被注册。 后者需要引用UserDetailsService。 再次,这可以使用user-service-ref属性由id指定,或者将自动定位到应用程序上下文中。

<openid-login>的{​​{0}}父元素

<openid-login>属性
  • always-use-default-target 登录后是否应始终将用户重定向到default-target-url。

  • authentication-details-source-ref 引用将由认证过滤器使用的AuthenticationDetailsS​​ource

  • authentication-failure-handler-ref 引用一个AuthenticationFailureHandler bean,它应该用来处理失败的认证请求。 不应该与authentication-failure-url结合使用,因为实现应始终处理导航到后续目标

  • authentication-failure-url 登录失败页面的URL。 如果未指定登录失败URL,则Spring Security将自动在/ login?login_error中创建失败登录URL,并在请求时使用相应的筛选器呈现该登录失败URL。

  • authentication-success-forward-url 将ForwardAuthenticationSuccessHandler映射到UsernamePasswordAuthenticationFilterauthenticationSuccessHandler属性。

  • authentication-failure-forward-url 将ForwardAuthenticationFailureHandler映射到UsernamePasswordAuthenticationFilterauthenticationFailureHandler属性。

  • authentication-success-handler-ref 引用应该用于处理成功认证请求的AuthenticationSuccessHandler bean。 不应与default-target-url(或always-use-default-target)结合使用,因为实施应始终处理导航到后续目标

  • default-target-url 如果用户以前的操作无法恢复,则在成功验证后将重定向到的URL。 这通常发生在用户访问登录页面而未首先请求触发认证的安全操作时。 如果未指定,则默认为应用程序的根目录。

  • login-page 登录页面的URL。 如果没有指定登录URL,Spring Security将自动在/ login上创建一个登录URL,并在请求时提供相应的过滤器来呈现该登录URL。

  • login-processing-url 登录表单发布到的URL。 如果未指定,则默认为/ login。

  • password-parameter 包含密码的请求参数的名称。 默认为"password"。

  • user-service-ref 对用户服务(或UserDetailsS​​ervice bean)Id的引用

  • username-parameter 包含用户名的请求参数的名称。 默认为"username"。

<openid-login>的{​​{0}}子元素 * attribute-exchange

<attribute-exchange>

attribute-exchange元素定义应从身份提供商请求的属性列表。 可以在名称空间配置章节的OpenID Support部分中找到示例。 可以使用多于一个,在这种情况下,每个都必须具有identifier-match属性,其中包含与提供的OpenID标识符相匹配的正则表达式。 这允许从不同的提供者(谷歌,雅虎等)获取不同的属性列表。

<attribute-exchange>的{​​{0}}父元素

<attribute-exchange>属性
  • identifier-match 一个正则表达式,在决定在认证过程中使用哪种属性交换配置时,将与所称身份进行比较。

<attribute-exchange>的{​​{0}}子元素

<openid-attribute>

制作OpenID AX时使用的属性  获取请求

<openid-attribute>的{​​{0}}父元素

<openid-attribute>属性
  • count 指定您希望返回的属性数量。 例如,返回3封电子邮件。 默认值是1。

  • name 指定您希望返回的属性的名称。 例如,电子邮件。

  • required 指定OP是否需要此属性,但如果OP未返回属性,则不会出错。 默认为false。

  • type 指定属性类型。 例如,http://axschema.org/contact/email。 查看您的OP的文档以获取有效的属性类型。

<port-mappings>

默认情况下,{{}}的实例将被添加到配置中,以用于重定向到安全和不安全的URL。 此元素可以选择用于覆盖该类定义的默认映射。 每个子<port-mapping>元素定义一对HTTP:HTTPS端口。 默认映射是80:443和8080:8443。 覆盖这些的示例可以在namespace introduction中找到。

<port-mappings>的{​​{0}}父元素