Spring Security是一个提供身份验证,授权和保护以防止常见攻击的框架。 凭借对命令式和响应式应用程序的一流支持,它是用于保护基于Spring的应用程序的实际标准。
前言
1.先决条件
Spring Security需要Java 8或更高版本的运行时环境。
由于Spring Security旨在以独立的方式运行,因此您无需在Java Runtime Environment中放置任何特殊的配置文件。 特别是,您无需配置特殊的Java身份验证和授权服务(JAAS)策略文件,也无需将Spring Security放置在公共类路径位置。
同样,如果使用EJB容器或Servlet容器,则无需在任何地方放置任何特殊的配置文件,也无需在服务器类加载器中包含Spring Security。 所有必需的文件都包含在您的应用程序中。
这种设计提供了最大的部署时间灵活性,因为您可以将目标工件(JAR,WAR或EAR)从一个系统复制到另一个系统,并且可以立即使用。
2.Spring Security社区
欢迎来到Spring Security社区! 本节讨论如何充分利用我们庞大的社区。
2.1. 获得帮助
如果您需要有关Spring Security的帮助,我们将在这里为您提供帮助。 以下是获得帮助的一些最佳方法:
-
通读本文档。
-
尝试我们许多 示例应用程序之一 。
-
使用 标签 在 https://stackoverflow.com 上 提问
spring-security。 -
在 https://github.com/spring-projects/spring-security/issues 报告错误和增强请求
2.2. 参与其中
我们欢迎您参与Spring Security项目。 有很多贡献的方法,包括回答有关StackOverflow的问题,编写新代码,改进现有代码,协助编写文档,开发示例或教程,报告错误或仅提出建议。 有关更多信息,请参见我们的 贡献 文档。
2.3. 源代码
您可以在GitHub上找到Spring Security的源代码, 网址 为 https://github.com/spring-projects/spring-security/
2.4. Apache 2许可证
Spring Security是在 Apache 2.0许可 下发行的开源软件 。
2.5. 社交媒体
您可以 在Twitter上 关注 @SpringSecurity 和 Spring Security团队 ,以获取最新消息。 您还可以关注 @SpringCentral 以了解整个Spring产品组合的最新信息。
3. Spring Security 5.2的新功能
Spring Security 5.2提供了许多新功能。 以下是该版本的重点内容。
3.1. Servlet
-
在HTTP安全DSL中 添加了 嵌套的构建器 支持
-
OAuth 2.0客户端
-
引入 OAuth2AuthorizedClientManager / OAuth2AuthorizedClientProvider
-
添加了 AuthorizedClientServiceOAuth2AuthorizedClientManager ,它能够在HttpServletRequest上下文之外进行操作
-
PKCE对 公共客户的支持
-
支持 资源所有者密码凭证 授予
-
支持 通过NimbusJwtDecoder 使用 对称密钥 进行ID令牌验证
-
将 随机数 添加 到OpenID Connect身份验证请求中
-
OpenID Connect RP启动的注销
-
更新的 文档
-
-
OAuth 2.0资源服务器
3.3. 核心
-
引入 RSocket 支持
-
引入 SAML服务提供商 支持
-
为方法参数 引入 @CurrentSecurityContext
-
将 密钥资料 转换 为密钥实例
-
支持 Clear-Site-Data 标头
-
添加了 nohttp 来构建
-
JDK 12 支持
-
支持 消息表达式中的 路径变量
-
配置类是无代理的,并且支持 proxyBeanMethods = false
-
支持不同 BCrypt编码 之间的升级
-
支持不同 SCrypt编码 之间的升级
4.获得Spring Security
本节讨论了有关获取Spring Security二进制文件所需的所有知识。 有关 如何获取 源代码的信息 , 请参见 源代码。
4.1. 发行编号
Spring Security版本的格式为MAJOR.MINOR.PATCH,使得:
-
主要版本可能包含重大更改。 通常,这样做是为了提供改进的安全性以匹配现代安全性实践。
-
MINOR版本包含增强功能,但被视为被动更新
-
PATCH级别应该是完全兼容的,向前和向后兼容,但可能存在修正错误的更改除外。
4.2. 与Maven结合使用
与大多数开源项目一样,Spring Security将其依赖项部署为Maven工件。 本节中的主题提供有关使用Maven时如何使用Spring Security的详细信息。
4.2.1. Maven的春季靴
Spring Boot提供了一个
spring-boot-starter-security
启动程序,
该
启动程序将与Spring Security相关的依赖项聚合在一起。
使用启动器的最简单且首选的方法是
通过IDE集成(
Eclipse
,
IntelliJ
,
NetBeans
)或通过
https://start.spring.io
使用
Spring Initializr
。
另外,您可以手动添加启动器,如以下示例所示:
<dependencies>
<!-- ... other dependency elements ... -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>
由于Spring Boot提供了Maven BOM来管理依赖版本,因此您无需指定版本。 如果您希望覆盖Spring Security版本,可以通过提供Maven属性来实现,如以下示例所示:
<properties>
<!-- ... -->
<spring-security.version>5.2.0.RELEASE</spring-security.version>
</dependencies>
由于Spring Security仅在主要版本中进行重大更改,因此可以将较新版本的Spring Security与Spring Boot一起使用是安全的。 但是,有时您可能还需要更新Spring Framework的版本。 您可以通过添加Maven属性来执行此操作,如以下示例所示:
<properties>
<!-- ... -->
<spring.version>5.2.0.RELEASE</spring.version>
</dependencies>
如果您使用其他功能(例如LDAP,OpenID和其他功能),则还需要包括适当的 项目模块 。
4.2.2. 没有Spring Boot的Maven
当您在没有Spring Boot的情况下使用Spring Security时,首选方法是使用Spring Security的BOM,以确保在整个项目中使用一致的Spring Security版本。 以下示例显示了如何执行此操作:
<dependencyManagement>
<dependencies>
<!-- ... other dependency elements ... -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-bom</artifactId>
<version>5.2.0.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
最小的Spring Security Maven依赖关系集通常如下所示:
<dependencies>
<!-- ... other dependency elements ... -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
</dependency>
</dependencies>
如果您使用其他功能(例如LDAP,OpenID和其他功能),则还需要包括适当的 项目模块 。
Spring Security是基于Spring Framework 5.2.0.RELEASE构建的,但通常应与任何较新版本的Spring Framework 5.x一起使用。
Spring Security的可传递依赖项解决了Spring Framework 5.2.0.RELEASE的事实,这可能会引起奇怪的类路径问题,这使许多用户不满。
解决此问题的最简单方法是使用
您
spring-framework-bom
的
<dependencyManagement>
部分中的,
pom.xml
如以下示例所示:
<dependencyManagement>
<dependencies>
<!-- ... other dependency elements ... -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-framework-bom</artifactId>
<version>5.2.0.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
前面的示例确保Spring Security的所有传递依赖项都使用Spring 5.2.0.RELEASE模块。
| 这种方法使用Maven的“物料清单”(BOM)概念,并且仅在Maven 2.0.9+中可用。 有关如何解决依赖关系的其他详细信息,请参见 Maven的依赖机制简介文档 。 |
4.2.3. Maven仓库
所有GA版本(即以.RELEASE结尾的版本)均已部署到Maven Central,因此无需在pom中声明其他Maven存储库。
如果使用SNAPSHOT版本,则需要确保定义了Spring Snapshot存储库,如以下示例所示:
<repositories>
<!-- ... possibly other repository elements ... -->
<repository>
<id>spring-snapshot</id>
<name>Spring Snapshot Repository</name>
<url>https://repo.spring.io/snapshot</url>
</repository>
</repositories>
如果使用里程碑版本或候选版本,则需要确保定义了Spring Milestone存储库,如以下示例所示:
<repositories>
<!-- ... possibly other repository elements ... -->
<repository>
<id>spring-milestone</id>
<name>Spring Milestone Repository</name>
<url>https://repo.spring.io/milestone</url>
</repository>
</repositories>
4.3. 摇篮
作为大多数开源项目,Spring Security将其依赖项部署为Maven工件,从而提供了一流的Gradle支持。 以下主题详细介绍了使用Gradle时如何使用Spring Security。
4.3.1. 带有Gradle的Spring Boot
Spring Boot提供了一个
spring-boot-starter-security
启动程序,
该
启动程序将与Spring Security相关的依赖项聚合在一起。
使用启动器的最简单且首选的方法是
通过IDE集成(
Eclipse
,
IntelliJ
,
NetBeans
)或通过
https://start.spring.io
使用
Spring Initializr
。
另外,您可以手动添加启动器,如以下示例所示:
dependencies {
compile "org.springframework.boot:spring-boot-starter-security"
}
由于Spring Boot提供了Maven BOM来管理依赖版本,因此您无需指定版本。 如果您希望覆盖Spring Security版本,可以通过提供Gradle属性来实现,如以下示例所示:
ext['spring-security.version']='5.2.0.RELEASE'
由于Spring Security仅在主要版本中进行重大更改,因此可以将较新版本的Spring Security与Spring Boot一起使用是安全的。 但是,有时您可能还需要更新Spring Framework的版本。 您可以通过添加Gradle属性来执行此操作,如以下示例所示:
ext['spring.version']='5.2.0.RELEASE'
如果您使用其他功能(例如LDAP,OpenID和其他功能),则还需要包括适当的 项目模块 。
4.3.2. 没有Spring Boot的Gradle
当您在没有Spring Boot的情况下使用Spring Security时,首选方法是使用Spring Security的BOM,以确保在整个项目中使用一致的Spring Security版本。 您可以使用 Dependency Management Plugin 来做到这一点 ,如以下示例所示:
plugins {
id "io.spring.dependency-management" version "1.0.6.RELEASE"
}
dependencyManagement {
imports {
mavenBom 'org.springframework.security:spring-security-bom:5.2.0.RELEASE'
}
}
最小的Spring Security Maven依赖关系集通常如下所示:
dependencies {
compile "org.springframework.security:spring-security-web"
compile "org.springframework.security:spring-security-config"
}
如果您使用其他功能(例如LDAP,OpenID和其他功能),则还需要包括适当的 项目模块 。
Spring Security是基于Spring Framework 5.2.0.RELEASE构建的,但通常应与任何较新版本的Spring Framework 5.x一起使用。
{JB}许多用户可能会误以为Spring Security的可传递依赖项解决了Spring Framework 5.2.0.RELEASE,这会引起奇怪的类路径问题。
解决此问题的最简单方法是
spring-framework-bom
在的
<dependencyManagement>
部分中
使用
pom.xml
。
您可以使用
Dependency Management Plugin
来做到这一点
,如以下示例所示:
plugins {
id "io.spring.dependency-management" version "1.0.6.RELEASE"
}
dependencyManagement {
imports {
mavenBom 'org.springframework:spring-framework-bom:5.2.0.RELEASE'
}
}
前面的示例确保Spring Security的所有传递依赖项都使用Spring 5.2.0.RELEASE模块。
4.3.3. Gradle仓库
所有GA版本(即以.RELEASE结尾的版本)均已部署到Maven Central,因此使用mavenCentral()存储库足以满足GA版本的要求。 以下示例显示了如何执行此操作:
repositories {
mavenCentral()
}
如果使用SNAPSHOT版本,则需要确保已定义Spring Snapshot存储库,如以下示例所示:
repositories {
maven { url 'https://repo.spring.io/snapshot' }
}
如果使用里程碑版本或候选版本,则需要确保定义了Spring Milestone存储库,如以下示例所示:
repositories {
maven { url 'https://repo.spring.io/milestone' }
}
5.项目模块
在Spring Security 3.0中,代码库被细分为单独的jar,这些jar更清楚地区分了不同的功能区域和第三方依赖项。
如果使用Maven构建项目,则应将这些模块添加到中
pom.xml
。
即使您不使用Maven,我们也建议您查阅
pom.xml
文件以了解第三方依赖关系和版本。
另一个好主意是检查示例应用程序中包含的库。
5.1.
核心-
spring-security-core.jar
该模块包含核心身份验证和访问控制类和接口,远程支持和基本配置API。 使用Spring Security的任何应用程序都需要它。 它支持独立的应用程序,远程客户端,方法(服务层)安全性和JDBC用户配置。 它包含以下顶级程序包:
-
org.springframework.security.core -
org.springframework.security.access -
org.springframework.security.authentication -
org.springframework.security.provisioning
5.2.
远程处理—
spring-security-remoting.jar
该模块提供了与Spring Remoting的集成。
除非您要编写使用Spring Remoting的远程客户端,否则您不需要这样做。
主要包装是
org.springframework.security.remoting
。
5.3.
Web-
spring-security-web.jar
该模块包含过滤器和相关的Web安全基础结构代码。
它包含任何与Servlet API相关的内容。
如果需要Spring Security Web认证服务和基于URL的访问控制,则需要它。
主要包装是
org.springframework.security.web
。
5.4.
配置-
spring-security-config.jar
该模块包含安全名称空间解析代码和Java配置代码。
如果将Spring Security XML名称空间用于配置或Spring Security的Java Configuration支持,则需要它。
主要包装是
org.springframework.security.config
。
这些类都不打算直接在应用程序中使用。
5.5.
LDAP-
spring-security-ldap.jar
该模块提供LDAP身份验证和供应代码。
如果您需要使用LDAP认证或管理LDAP用户条目,则为必填项。
顶级软件包是
org.springframework.security.ldap
。
5.6.
OAuth 2.0核心-
spring-security-oauth2-core.jar
spring-security-oauth2-core.jar
包含为OAuth 2.0授权框架和OpenID Connect Core 1.0提供支持的核心类和接口。
使用OAuth 2.0或OpenID Connect Core 1.0的应用程序(例如客户端,资源服务器和授权服务器)需要它。
顶级软件包是
org.springframework.security.oauth2.core
。
5.7.
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登录或OAuth客户端支持的应用程序需要使用它。
顶级软件包是
org.springframework.security.oauth2.client
。
5.8.
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
5.9。
OAuth 2.0资源服务器-
spring-security-oauth2-resource-server.jar
spring-security-oauth2-resource-server.jar
包含Spring Security对OAuth 2.0资源服务器的支持。
它用于通过OAuth 2.0承载令牌保护API。
顶级软件包是
org.springframework.security.oauth2.server.resource
。
5.10。
ACL-
spring-security-acl.jar
该模块包含专门的域对象ACL实现。
它用于将安全性应用于应用程序中的特定域对象实例。
顶级软件包是
org.springframework.security.acls
。
5.11.
CAS —
spring-security-cas.jar
该模块包含Spring Security的CAS客户端集成。
如果要对CAS单点登录服务器使用Spring Security Web认证,则应该使用它。
顶级软件包是
org.springframework.security.cas
。
6.样品
Spring Security包含许多 示例 应用程序。
Servlet应用
Filter
。
这意味着它可以与在Servlet容器中运行的任何应用程序一起使用。
更具体地说,您无需在基于Servlet的应用程序中使用Spring即可利用Spring Security。
7.你好Spring Security
本节介绍了使用 Spring Boot , Java Configuration 或 XML Configuration 的最小Spring Security应用程序 。
7.1. 你好Spring Security(启动)
本节介绍了如何将Spring Security与Spring Boot结合使用的最小设置。 有关如何将Spring Security与Java配置一起使用,请参阅 Hello Spring Security(Java配置) 。 有关如何将Spring Security与XML配置一起使用,请参阅 Hello Spring Security(XML) 。
| 完整的应用程序可以在 samples / boot / helloworld中找到 |
7.1.2. 启动Hello Spring安全启动
现在,您可以
使用Maven插件的
目标
来
运行Spring Boot应用程序
run
。
以下示例显示了如何执行此操作(以及执行此操作的输出的开头):
$ ./mvn spring-boot:run
...
INFO 23689 --- [ restartedMain] .s.s.UserDetailsServiceAutoConfiguration :
Using generated security password: 8e557245-73e2-4286-969a-ff57fe326336
...
7.1.3. Spring Boot自动配置
Spring Boot自动:
-
启用Spring Security的默认配置,该默认配置将Servlet创建
Filter为名为的BeanspringSecurityFilterChain。 此bean负责应用程序内的所有安全性(保护应用程序URL,验证提交的用户名和密码,重定向到登录表单等)。 -
UserDetailsService用用户名 创建一个 bean,user并将其随机生成的密码记录到控制台。 -
为每个请求向以Servlet容器
Filter命名的bean 注册springSecurityFilterChain。
Spring Boot的配置不多,但功能很多。 功能摘要如下:
-
要求经过身份验证的用户才能与应用程序进行任何交互
-
为您生成一个默认的登录表单
-
让用户名
user和密码登录到控制台,以使用基于表单的身份验证进行身份验证(在前面的示例中,密码为8e557245-73e2-4286-969a-ff57fe326336) -
使用BCrypt保护密码存储
-
让用户注销
-
CSRF攻击 预防
-
会话固定 保护
-
安全标题集成
-
HTTP严格传输安全性 用于安全请求
-
缓存控制(以后可以由您的应用程序覆盖,以允许缓存您的静态资源)
-
X-Frame-Options集成有助于防止 Clickjacking
-
-
与以下Servlet API方法集成:
7.2. Hello Spring Security(Java配置)
本节介绍如何在Java配置中使用Spring Security。 有关如何将Spring Security与XML配置一起使用,请参阅 Hello Spring Security(XML) 。 有关如何在Spring Boot配置中使用Spring Security的信息,请参见 Hello Spring Security(引导) 。
| 您可以在 samples / javaconfig / helloworld中 找到完整的应用程序 。 |
7.2.2.
最小
@EnableWebSecurity
配置
第一步是创建我们的Spring Security Java配置。
该配置将创建一个servlet
Filter
(称为
springSecurityFilterChain
),该
servlet
负责应用程序中的所有安全功能(保护应用程序URL,验证提交的用户名和密码,重定向到登录表单等)。
以下示例显示了Spring Security Java配置的最基本示例:
import org.springframework.context.annotation.*;
import org.springframework.security.config.annotation.web.configuration.*;
import org.springframework.security.core.userdetails.*;
import org.springframework.security.provisioning.*;
@EnableWebSecurity
public class WebSecurityConfig {
// @formatter:off
@Bean
public UserDetailsService userDetailsService() {
UserDetails user = User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("USER")
.build();
return new InMemoryUserDetailsManager(user);
}
// @formatter:on
}
此配置的确没有太多,但是却做了很多。 功能摘要如下:
-
要求经过身份验证的用户才能与应用程序进行任何交互
-
为您生成一个默认的登录表单
-
允许用户使用 基于表单的身份验证 的用户名
user和密码进行password身份验证 -
使用BCrypt保护密码存储
-
让用户注销
-
CSRF攻击 预防
-
会话固定 保护
-
安全标题集成
-
HTTP严格传输安全性 用于安全请求
-
缓存控制(以后可以由您的应用程序覆盖,以允许缓存您的静态资源)
-
X-Frame-Options集成有助于防止 Clickjacking
-
-
与以下Servlet API方法集成:
7.2.3.
使用
AbstractSecurityWebApplicationInitializer
下一步是向
springSecurityFilterChain
战争
登记
。
Spring Security提供了一个
AbstractSecurityWebApplicationInitializer
利用
Spring的WebApplicationInitializer支持
的基类(
)
。
以下示例显示了示例配置:
import org.springframework.security.web.context.*;
public class SecurityInitializer
extends AbstractSecurityWebApplicationInitializer {
public SecurityInitializer() {
super(WebSecurityConfig.class);
}
}
在
SecurityInitializer
做以下的事情:
-
添加
ContextLoaderListener会加载WebSecurityConfig。 -
查找
Filter名为 类型的BeanspringSecurityFilterChain并将其注册以处理应用程序中的每个URL。
|
如果要与Spring MVC应用程序集成,请确保配置,
例子21. MvcInitializer.java
|
7.3. Hello Spring安全性(XML)
本节介绍如何在XML配置中使用Spring Security。 有关如何将Spring Security与Java配置一起使用,请参阅 Hello Spring Security(Java配置) 。 有关如何在Spring Boot配置中使用Spring Security的信息,请参见 Hello Spring Security(引导) 。
7.3.2.
最小
<http>
配置
在本节中,我们讨论如何将Spring Security与XML配置一起使用。
| 完整的应用程序可以在 samples / xml / helloworld中找到 |
第一步是创建我们的Spring Security XML配置。
该配置将创建一个Servlet
Filter
(称为
springSecurityFilterChain
),该
Servlet
负责应用程序中的所有安全性(保护应用程序URL,验证提交的用户名和密码,重定向到登录表单等)。
以下示例显示了Spring Security XML配置的最基本示例:
<b:beans xmlns="http://www.springframework.org/schema/security"
xmlns:b="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/security https://www.springframework.org/schema/security/spring-security.xsd">
<http />
<user-service>
<user name="user" password="{noop}password" authorities="ROLE_USER" />
</user-service>
</b:beans>
此配置的确没有太多,但是却做了很多。 功能摘要如下:
-
要求经过身份验证的用户才能与应用程序进行任何交互
-
为您生成一个默认的登录表单
-
允许用户使用 基于表单的身份验证 的用户名
user和密码进行password身份验证 -
使用BCrypt保护密码存储
-
让用户注销
-
CSRF攻击 预防
-
会话固定 保护
-
安全标题集成
-
HTTP严格传输安全性 用于安全请求
-
缓存控制(以后可以由您的应用程序覆盖,以允许缓存您的静态资源)
-
X-Frame-Options集成有助于防止 Clickjacking
-
-
与以下Servlet API方法集成:
7.3.3.
web.xml
组态
下一步是确保已读入我们的安全性配置。为此,我们需要确保a
ContextLoaderListener
已注册并且
a
contextConfigLocation
包括配置。
以下示例显示了如何执行此操作:
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0" 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">
<!--
Loads the Spring configurations from contextConfigLocation
-->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!--
The locations of the Spring Configuration. In this case, all configuration is
in /WEB-INF/spring/
-->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
/WEB-INF/spring/*.xml
</param-value>
</context-param>
<!--
DelegatingFilterProxy looks for a Spring bean by the name of filter (springSecurityFilterChain) and delegates
all work to that Bean. This is how the Servlet Container can a Spring Bean to act as a Servlet Filter.
-->
<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>
</web-app>
|
如果您与现有的Spring MVC应用程序集成,请确保配置,
src / main / webapp / WEB-INF / web.xml
|
8.架构与实现
一旦熟悉了设置和运行一些基于名称空间配置的应用程序,您可能希望对框架在名称空间外观背后的实际工作方式有更多的了解。 像大多数软件一样,Spring Security具有某些中心接口,类和概念抽象,它们在整个框架中都经常使用。 在 Reference指南的这一部分中,我们将研究其中的一些,并了解它们如何协同工作以在Spring Security中支持身份验证和访问控制。
8.1. 技术概述
8.1.1. 运行环境
Spring Security 5.2.0.RELEASE需要Java 8 Runtime Environment或更高版本。 由于Spring Security旨在以独立的方式运行,因此无需在Java运行时环境中放置任何特殊的配置文件。 特别是,不需要配置特殊的Java身份验证和授权服务(JAAS)策略文件或将Spring Security放置在公共类路径位置。
同样,如果您使用的是EJB容器或Servlet容器,则无需在任何地方放置任何特殊的配置文件,也无需在服务器类加载器中包含Spring Security。 所有必需的文件将包含在您的应用程序中。
这种设计提供了最大的部署时间灵活性,因为您只需将目标工件(JAR,WAR或EAR)从一个系统复制到另一个系统即可立即使用。
8.1.2. 核心组件
从Spring Security 3.0开始,将
spring-security-core
jar
的内容
减少到最低限度。
它不再包含与Web应用程序安全性,LDAP或名称空间配置有关的任何代码。
我们将在这里看一下您将在核心模块中找到的一些Java类型。
它们代表了框架的构建块,因此,如果您需要超越简单的名称空间配置,那么即使实际上不需要直接与它们进行交互,也必须了解它们的含义,这一点很重要。
SecurityContextHolder,SecurityContext和身份验证对象
最基本的对象是
SecurityContextHolder
。
我们在这里存储应用程序当前安全上下文的详细信息,其中包括当前使用该应用程序的主体的详细信息。
默认情况下,会
SecurityContextHolder
使用
ThreadLocal
来存储这些详细信息,这意味着即使没有将安全性上下文作为这些方法的参数显式传递,安全性上下文也始终可用于同一执行线程中的方法。
ThreadLocal
如果在处理当前委托人的请求之后要清除线程,则以这种方式
使用a
是非常安全的。
当然,Spring Security会自动为您解决此问题,因此无需担心。
由于某些应用程序使用
ThreadLocal
线程的特定方式,因此它们
并不完全适合使用
。
例如,Swing客户端可能希望Java虚拟机中的所有线程都使用相同的安全上下文。
SecurityContextHolder
可以在启动时配置策略,以指定希望如何存储上下文。
对于独立应用程序,您将使用该
SecurityContextHolder.MODE_GLOBAL
策略。
其他应用程序可能希望让安全线程产生的线程也采用相同的安全身份。
这是通过使用实现的
SecurityContextHolder.MODE_INHERITABLETHREADLOCAL
。
您可以通过
SecurityContextHolder.MODE_THREADLOCAL
两种方式
从默认更改模式
。
第一个是设置系统属性,第二个是在上调用静态方法
SecurityContextHolder
。
大多数应用程序都不需要更改默认值,但是如果需要更改,请查看JavaDoc
SecurityContextHolder
了解更多。
获取有关当前用户的信息
在内部,
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
作为主体。
UserDetailsService
上述代码片段中需要注意的另一项内容是,您可以从
Authentication
对象中
获取主体
。
委托人只是一个
Object
。
大多数情况下,可以将其转换为
UserDetails
对象。
UserDetails
是Spring Security中的核心接口。
它代表一个原理,但以一种可扩展的和特定于应用程序的方式。
可以将其
UserDetails
看作是您自己的用户数据库和Spring Security内部需要什么之间的适配器
SecurityContextHolder
。
作为您自己的用户数据库中某些内容的表示形式,通常您会将强制
UserDetails
转换为应用程序提供的原始对象,以便可以调用特定于业务的方法(例如
getEmail()
,
getEmployeeNumber()
等等)。
现在,您可能想知道,那么什么时候提供
UserDetails
对象?
我怎么做?
我以为您说的这个东西是声明性的,我不需要编写任何Java代码-有什么用?
简短的答案是有一个名为的特殊接口
UserDetailsService
。
此接口上的唯一方法接受一个
String
基于用户名的参数,并返回
UserDetails
:
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
这是在Spring Security中为用户加载信息的最常用方法,只要需要有关用户的信息,就会在整个框架中使用它。
成功认证后,
UserDetails
将用于构建
Authentication
存储在中的对象
SecurityContextHolder
(有关更多信息,请
参见
下文
)。
好消息是,我们提供了许多
UserDetailsService
实现,包括一个使用内存映射(
InMemoryDaoImpl
)的实现,另一个使用JDBC(
JdbcDaoImpl
)的实现。
但是,大多数用户倾向于编写自己的应用程序,而其实现往往只是位于代表其员工,客户或应用程序其他用户的现有数据访问对象(DAO)之上。
请记住,您的
UserDetailsService
收益始终可以通过
SecurityContextHolder
使用上面的代码片段
获得
的好处
。
|
经常会有一些困惑
|
授予的权限
除了主体,提供的另一个重要方法
Authentication
是
getAuthorities()
。
此方法提供了一个
GrantedAuthority
对象
数组
。
GrantedAuthority
毫不奇怪,
A
是授予委托人的权限。
此类权限通常是“角色”,例如
ROLE_ADMINISTRATOR
或
ROLE_HR_SUPERVISOR
。
稍后将这些角色配置为Web授权,方法授权和域对象授权。
Spring Security的其他部分能够解释这些权限,并希望它们存在。
GrantedAuthority
对象通常由加载
UserDetailsService
。
通常,
GrantedAuthority
对象是应用程序范围的权限。
它们不特定于给定的域对象。
因此,您不可能
GrantedAuthority
代表
Employee
对象号54
的权限
,因为如果有成千上万个这样的权限,您很快就会用完内存(或者至少导致应用程序花费很长时间来完成)。验证用户身份)。
当然,Spring Security是专门为满足这一通用需求而设计的,但您可以为此目的使用项目的域对象安全性功能。
摘要
回顾一下,到目前为止,我们已经看到了Spring Security的主要组成部分:
-
SecurityContextHolder,提供对的访问SecurityContext。 -
SecurityContext,以保存Authentication(可能还有特定 于 请求的)安全信息。 -
Authentication,以特定于Spring Security的方式表示主体。 -
GrantedAuthority,以反映授予主体的应用程序范围的权限。 -
UserDetails,以提供必要的信息以从应用程序的DAO或其他安全数据源构建Authentication对象。 -
UserDetailsService,以UserDetails在传入String基于-的用户名(或证书ID等) 时 创建一个 。
现在,您已经了解了这些重复使用的组件,下面让我们仔细看看身份验证的过程。
8.1.3. 认证方式
Spring Security可以参与许多不同的身份验证环境。 虽然我们建议人们使用Spring Security进行身份验证,而不是与现有的容器管理的身份验证集成,但是仍然支持它-与您自己的专有身份验证系统集成一样。
Spring Security中的身份验证是什么?
让我们考虑一个大家都熟悉的标准身份验证方案。
-
提示用户使用用户名和密码登录。
-
系统(成功)验证用户名的密码正确。
-
获取该用户的上下文信息(他们的角色列表等)。
-
为用户建立了安全上下文
-
用户可能会继续执行某些操作,该操作可能会受到访问控制机制的保护,该访问控制机制会根据当前安全上下文信息检查该操作所需的权限。
前四项构成了身份验证过程,因此我们将看看它们在Spring Security中是如何发生的。
-
获取用户名和密码,并将其组合到的一个实例
UsernamePasswordAuthenticationToken(Authentication接口 的实例 ,我们之前已经看到)。 -
令牌会传递到的实例
AuthenticationManager进行验证。 -
成功认证后 ,
AuthenticationManager返回一个完全填充的Authentication实例。 -
通过调用
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
包含一个
Authentication
代表主体的
AbstractSecurityInterceptor
。
您可以(而且很多用户都可以)编写自己的过滤器或MVC控制器,以提供与不基于Spring Security的身份验证系统的互操作性。
例如,您可能正在使用容器管理的身份验证,该身份验证使当前用户可以从ThreadLocal或JNDI位置访问。
或者,您可能在拥有传统专有身份验证系统的公司工作,这是您无法控制的公司“标准”。
在这种情况下,让Spring Security正常工作并仍然提供授权功能是很容易的。
您所需要做的就是编写一个过滤器(或等效过滤器),该过滤器从某个位置读取第三方用户信息,构建特定于Spring Security的
Authentication
对象,然后将其放入
SecurityContextHolder
。
在这种情况下,您还需要考虑通常由内置身份验证基础结构自动处理的事情。
例如,您可能需要先创建HTTP会话来
缓存请求之间的上下文
,然后再将响应写入客户端
[ 1 ]
。
如果您想知道
AuthenticationManager
在实际示例中
如何
实现,我们将在
核心服务一章中进行介绍
。
8.1.4. Web应用程序中的身份验证
现在,让我们探讨一下在Web应用程序中使用Spring Security(未
web.xml
启用安全性)的情况。
如何认证用户并建立安全上下文?
考虑典型的Web应用程序的身份验证过程:
-
您访问主页,然后单击链接。
-
请求发送到服务器,服务器确定您已请求受保护的资源。
-
由于您目前尚未通过身份验证,因此服务器会发回响应,指示您必须进行身份验证。 响应将是HTTP响应代码,或重定向到特定网页。
-
根据身份验证机制,您的浏览器将重定向到特定网页,以便您可以填写表格,或者浏览器将以某种方式检索您的身份(通过BASIC身份验证对话框,cookie,X.509证书等)。 )。
-
浏览器将响应发送回服务器。 这将是包含您填写的表单内容的HTTP POST或包含身份验证详细信息的HTTP标头。
-
接下来,服务器将决定所提供的凭据是否有效。 如果有效,则将进行下一步。 如果它们无效,通常会要求您的浏览器再试一次(因此您返回到上面的第二步)。
-
您尝试引起身份验证过程的原始请求将被重试。 希望您已获得足够授权的身份验证,以访问受保护的资源。 如果您具有足够的访问权限,则请求将成功。 否则,您将收到一个HTTP错误代码403,表示“禁止”。
Spring Security具有负责上述大多数步骤的不同类。
主要参与者(按照使用顺序)是
ExceptionTranslationFilter
,
AuthenticationEntryPoint
和和“身份验证机制”,它们负责调用
AuthenticationManager
上一节中看到的。
ExceptionTranslationFilter
ExceptionTranslationFilter
是一个Spring Security过滤器,它负责检测抛出的任何Spring Security异常。
此类异常通常由
AbstractSecurityInterceptor
授权服务的主要提供者
抛出
。
我们将
AbstractSecurityInterceptor
在下一节中
讨论
,但是现在我们只需要知道它会产生Java异常,并且对HTTP或对主体进行身份验证一无所知。
取而代之的是,
ExceptionTranslationFilter
提供此服务,具体负责返回错误代码403(如果主体已通过身份验证,因此仅缺少足够的访问权限-按照上述第七步),或启动
AuthenticationEntryPoint
(如果主体未通过身份验证,因此我们需要开始第三步)。
认证入口点
该
AuthenticationEntryPoint
负责第三步在上面的列表中。
可以想象,每个Web应用程序将具有默认的身份验证策略(嗯,可以像配置Spring Security中的所有其他功能一样配置它,但是现在让我们保持简单)。
每个主要认证系统都有其自己的
AuthenticationEntryPoint
实现,该实现通常执行步骤3中所述的操作之一。
认证机制
一旦您的浏览器提交了身份验证凭据(作为HTTP表单帖子或HTTP标头),服务器上就需要有一些东西来“收集”这些身份验证详细信息。
到目前为止,我们位于上述列表的第六步。
在Spring Security中,我们有一个特殊的名称,用于从用户代理(通常是Web浏览器)收集身份验证详细信息的功能,将其称为“身份验证机制”。
示例是基于表单的登录和基本身份验证。
从用户代理收集到身份验证详细信息后,
Authentication
便会构建
一个
“请求”对象,然后将其呈现给
AuthenticationManager
。
身份验证机制收到完满的
Authentication
对象后,它将认为请求有效,将
Authentication
放入
SecurityContextHolder
,并导致重试原始请求(上述步骤7)。
另一方面,如果
AuthenticationManager
拒绝了请求,则认证机制将要求用户代理重试(上面的第二步)。
在请求之间存储SecurityContext
根据应用程序的类型,可能需要制定一种策略来存储用户操作之间的安全上下文。
在典型的Web应用程序中,用户登录一次,然后通过其会话ID进行标识。
服务器缓存持续时间会话的主体信息。
在Spring Security中,存储
SecurityContext
请求之间
的责任
落在
SecurityContextPersistenceFilter
,默认情况下,将上下文存储为
HttpSession
HTTP请求之间
的
属性。
它将上下文还原到
SecurityContextHolder
每个请求,并且至关重要的是,清除
SecurityContextHolder
请求何时完成。
HttpSession
为了安全起见,
您不应该直接与
进行
互动
。
这样做根本没有道理-始终使用
SecurityContextHolder
替代项。
许多其他类型的应用程序(例如,无状态RESTful Web服务)不使用HTTP会话,并且将在每个请求上重新进行身份验证。
但是,仍然必须
SecurityContextPersistenceFilter
确保链中包含,以确保
SecurityContextHolder
在每次请求后清除。
|
在单个会话中接收并发请求的应用程序中,
|
8.1.5. Spring Security中的访问控制(授权)
在Spring Security中负责做出访问控制决策的主要接口是
AccessDecisionManager
。
它具有一种
decide
方法,
该
方法采用
Authentication
代表请求访问的主体
的
对象,“安全对象”(请参见下文)和适用于该对象的安全元数据属性列表(例如授予访问权限所需的角色列表) )。
安全和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
那么,什么 是 “安全对象”? Spring Security使用该术语来指代任何可以对其应用安全性(例如授权决策)的对象。 最常见的示例是方法调用和Web请求。
每个受支持的安全对象类型都有其自己的拦截器类,该类是的子类
AbstractSecurityInterceptor
。
重要的
AbstractSecurityInterceptor
是,在调用时,
如果主体已通过身份验证
,
SecurityContextHolder
则将包含有效
Authentication
的内容。
AbstractSecurityInterceptor
提供用于处理安全对象请求的一致工作流,通常是:
-
查找与当前请求关联的“配置属性”
-
将安全对象,当前
Authentication属性和配置属性提交AccessDecisionManager给授权决策 -
(可选)更改
Authentication调用 的 依据 -
允许进行安全对象调用(假设已授予访问权限)
-
调用
AfterInvocationManager如果配置,一旦调用返回。 如果调用引发了异常,AfterInvocationManager则不会调用。
什么是配置属性?
可以将“配置属性”视为一个String,它对所使用的类具有特殊的含义
AbstractSecurityInterceptor
。
它们由
ConfigAttribute
框架内的
接口表示
。
它们可以是简单的角色名称,也可以具有更复杂的含义,具体取决于实现的复杂程度
AccessDecisionManager
。
使用
AbstractSecurityInterceptor
配置了
SecurityMetadataSource
,它使用来查找安全对象的属性。
通常,此配置将对用户隐藏。
配置属性将作为安全方法的注释或安全URL的访问属性输入。
例如,当我们
<intercept-url pattern='/secure/**' access='ROLE_A,ROLE_B'/>
在名称空间介绍中
看到类似内容
时,这表示配置属性
ROLE_A
和
ROLE_B
适用于与给定模式匹配的Web请求。
实际上,使用默认
AccessDecisionManager
配置,这意味着
GrantedAuthority
将允许具有这两个属性之一匹配的
任何人
访问。
严格来说,它们只是属性,其解释取决于
AccessDecisionManager
实现。
前缀的使用
ROLE_
是一个标记,以指示这些属性是角色,并且应由Spring Security的消费
RoleVoter
。
这仅
AccessDecisionManager
在使用
基于投票者的情况下才有意义
。
我们将
AccessDecisionManager
在
授权一章中
了解如何
实现
。
RunAsManager
假设
AccessDecisionManager
决定允许该请求,
AbstractSecurityInterceptor
通常将继续进行该请求。
话虽这么说,在极少数情况下,用户可能希望
用不同的
替换
Authentication
内部的
,这由
调用a
处理
。
在相当不常见的情况下,例如服务层方法需要调用远程系统并显示不同的标识时,这可能很有用。
因为Spring Security会自动将安全身份从一台服务器传播到另一台服务器(假设您使用的是正确配置的RMI或HttpInvoker远程协议客户端),所以这可能很有用。
SecurityContext
Authentication
AccessDecisionManager
RunAsManager
AfterInvocationManager
在安全对象调用继续进行之后,然后返回-这可能意味着方法调用完成或过滤器链继续进行-
AbstractSecurityInterceptor
最终有机会处理该调用。
在此阶段,
AbstractSecurityInterceptor
有兴趣修改返回对象。
我们可能希望发生这种情况,因为无法在安全对象调用的“途中”做出授权决定。
由于高度可插拔,因此
AbstractSecurityInterceptor
将控制权传递给,
AfterInvocationManager
以便在需要时实际修改对象。
此类甚至可以完全替换对象,或者引发异常,也可以按照其选择的任何方式对其进行更改。
调用后检查仅在调用成功的情况下执行。
如果发生异常,将跳过其他检查。
AbstractSecurityInterceptor
安全拦截器和“安全对象”模型
中显示了它及其相关对象
8.2. 核心服务
现在,我们对Spring Security的架构和核心类的高度概括,让我们来仔细看看一个或两个核心接口及其实现的,特别是
AuthenticationManager
,
UserDetailsService
和
AccessDecisionManager
。
这些在本文档的其余部分中会定期出现,因此了解它们的配置方式和操作方式非常重要。
8.2.1. AuthenticationManager,ProviderManager和AuthenticationProvider
该
AuthenticationManager
只是一个接口,这样的实现可以让我们选择,但它是如何在实践中运作?
如果我们需要检查多个身份验证数据库或不同身份验证服务(例如数据库和LDAP服务器)的组合,该怎么办?
调用Spring Security中的默认实现,
ProviderManager
而不是处理身份验证请求本身,它委托给已配置的
AuthenticationProvider
s
列表,
依次查询每个s,以查看其是否可以执行身份验证。
每个提供程序都将引发异常或返回完全填充的
Authentication
对象。
记得我们的好朋友,
UserDetails
和
UserDetailsService
?
如果没有,请返回上一章并刷新您的记忆。
验证身份验证请求的最常见方法是加载相应
UserDetails
的密码,并对照用户输入的密码检查已加载的密码。
这是
DaoAuthenticationProvider
(见下文)
使用的方法
。
加载的
UserDetails
对象-尤其是
GrantedAuthority
包含的对象-构建
Authentication
成功通过身份验证返回并存储在中的
完全填充
对象
时将使用
它
SecurityContext
。
如果使用名称空间,
ProviderManager
则会在内部创建并维护
的实例
,然后使用名称空间身份验证提供程序元素将提供程序添加到该名称空间(请参阅
“名称空间”一章
)。
在这种情况下,您不应
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
。
如果您想了解有关链接提供程序的更多信息,请 Reference
ProviderManager
Javadoc。
诸如Web表单登录处理过滤器之类的身份验证机制注入了对的引用
ProviderManager
,并将调用它来处理其身份验证请求。
您所需的提供程序有时可以与身份验证机制互换,而在其他时候,它们将取决于特定的身份验证机制。
例如,
DaoAuthenticationProvider
并且
LdapAuthenticationProvider
与提交简单的用户名/密码身份验证请求的任何机制兼容,因此将与基于表单的登录名或HTTP Basic身份验证一起使用。
另一方面,某些身份验证机制会创建一个身份验证请求对象,该对象只能由一种类型的
AuthenticationProvider
。
一个示例就是JA-SIG CAS,它使用服务票证的概念,因此只能由进行身份验证
CasAuthenticationProvider
。
您不必太担心这一点,因为如果您忘记注册合适的提供程序,则
ProviderNotFoundException
在尝试进行身份验证时
只会收到一个
。
清除成功认证的凭据
默认情况下(从Spring Security 3.1开始),
ProviderManager
它将尝试从
Authentication
成功的身份验证请求返回
的
对象中
清除所有敏感的凭据信息
。
这样可以防止将密码之类的信息保留的时间过长。
例如,在使用用户对象的缓存来提高无状态应用程序的性能时,这可能会导致问题。
如果
Authentication
包含对缓存中某个对象(例如
UserDetails
实例)
的
引用,并且已删除其凭据,则将无法再针对缓存的值进行身份验证。
如果使用缓存,则需要考虑到这一点。
一个明显的解决方案是先在高速缓存实现中或在
AuthenticationProvider
创建返回
Authentication
对象的对象
中创建
对象的副本
。
或者,您可以在禁用该
eraseCredentialsAfterAuthentication
属性
ProviderManager
。
有关更多信息,请参见Javadoc。
DaoAuthenticationProvider
AuthenticationProvider
Spring Security实现
的最简单的方法
是
DaoAuthenticationProvider
,也是框架最早支持的方法之一。
它利用
UserDetailsService
(作为DAO)查找用户名,密码和
GrantedAuthority
s。
只需将比较提交的密码
UsernamePasswordAuthenticationToken
与加载
的密码,即可对用户进行身份验证
UserDetailsService
。
配置提供程序非常简单:
<bean id="daoAuthenticationProvider"
class="org.springframework.security.authentication.dao.DaoAuthenticationProvider">
<property name="userDetailsService" ref="inMemoryDaoImpl"/>
<property name="passwordEncoder" ref="passwordEncoder"/>
</bean>
该
PasswordEncoder
是可选的。
A
PasswordEncoder
提供对
UserDetails
从配置的返回
的
对象中
提供的密码进行编码和解码
UserDetailsService
。
这将在
下面
更详细地讨论
。
8.2.2. UserDetailsService实施
如本 Reference指南前面所述,大多数身份验证提供程序都利用
UserDetails
和
UserDetailsService
接口。
回想一下,合同
UserDetailsService
是一种方法:
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
返回的
UserDetails
是一个接口,该接口提供了获取器,该获取器保证以非空方式提供身份验证信息,例如用户名,密码,已授予的权限以及用户帐户是启用还是禁用。
即使用
UserDetailsService
户名和密码实际上没有用作身份验证决策的一部分,
大多数身份验证提供程序也会使用
。
他们可能
UserDetails
仅
将返回的
对象用作其
GrantedAuthority
信息,因为某些其他系统(例如LDAP或X.509或CAS等)承担了实际验证凭据的责任。
给定的实现
UserDetailsService
是如此简单,因此用户应该使用自己选择的持久性策略轻松检索身份验证信息。
话虽如此,Spring Security确实包含一些有用的基本实现,我们将在下面进行介绍。
内存中身份验证
创建自定义
UserDetailsService
实现从选择的持久性引擎中提取信息
很容易使用
,但是许多应用程序不需要这种复杂性。
如果您正在构建原型应用程序或刚开始集成Spring Security,而又不想真正花费时间配置数据库或编写
UserDetailsService
实现,
则尤其如此
。
对于这种情况,一个简单的选择是使用
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还包括一个
UserDetailsService
可从JDBC数据源获取身份验证信息的。
在内部使用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
上面显示的内容
使用不同的关系数据库管理系统
。
您还可以使用从JNDI获得的全局数据源,就像其他任何Spring配置一样。
权威团体
默认情况下,
JdbcDaoImpl
假设权限直接映射到用户,则为单个用户加载权限(请参阅
数据库架构附录
)。
另一种方法是将权限划分为组,然后将组分配给用户。
有些人喜欢使用这种方法来管理用户权限。
有关
JdbcDaoImpl
如何启用组权限的更多信息,
请参见
Javadoc。
组模式也包含在附录中。
9.认证
9.1. 内存中身份验证
我们已经看到了为单个用户配置内存中身份验证的示例。 下面是配置多个用户的示例:
@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;
}
9.2. 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"));
}
9.3. LDAP验证
9.3.1. 总览
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连接池。 如果您不熟悉该操作,可以 Reference Java LDAP文档 。
9.3.2. 将LDAP与Spring Security结合使用
Spring Security中的LDAP认证可以大致分为以下几个阶段。
-
从登录名获取唯一的LDAP“专有名称”或DN。 除非事先知道用户名到DN的确切映射,否则这通常意味着在目录中执行搜索。 因此,用户在登录时可能会输入名称“ joe”,但是用于验证LDAP的实际名称将是完整DN,例如
uid=joe,ou=users,dc=spring,dc=io。 -
通过“绑定”该用户或通过对该用户的密码与DN目录条目中的password属性进行远程“比较”操作来对用户进行身份验证。
-
加载用户的权限列表。
例外是仅使用LDAP目录检索用户信息并在本地对其进行身份验证。 这可能是不可能的,因为目录经常被设置为具有诸如用户密码之类的属性的有限读取访问权限。
我们将在下面查看一些配置方案。 有关可用配置选项的完整信息,请查阅安全名称空间模式(XML编辑器中应提供该信息)。
9.4. 配置LDAP服务器
您需要做的第一件事是配置要对其进行身份验证的服务器。
这是使用
<ldap-server>
安全名称空间中
的
元素
完成的
。
可以使用以下
url
属性
将其配置为指向外部LDAP服务器
:
<ldap-server url="ldap://springframework.org:389/dc=springframework,dc=org" />
spring-security
提供与
嵌入式ldap服务器的
集成
apacheds
并
unboundid
作为嵌入式ldap服务器的
集成
。
您可以使用中的属性
mode
在
它们之间进行选择
ldap-server
。
|
9.4.1. 使用嵌入式测试服务器
该
<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 Directory依赖项jar,供应用程序使用。 这些可以从LDAP示例应用程序获得。
9.4.2. 使用绑定身份验证
这是最常见的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"/>
如果与上述服务器定义一起使用,这将
ou=people,dc=springframework,dc=org
使用
user-search-filter
属性
的值
作为过滤器
在DN下执行搜索
。
再次用用户登录名代替过滤器名称中的参数,因此它将搜索
uid
属性等于用户名
的条目
。
如果
user-search-base
未提供,将从根目录执行搜索。
9.4.3. 正在加载当局
通过以下属性控制如何从LDAP目录中的组加载权限。
-
group-search-base。 定义目录树的一部分,应在该部分下执行组搜索。 -
group-role-attribute。 该属性包含组条目定义的权限名称。 默认为cn -
group-search-filter。 用于搜索组成员身份的过滤器。 默认值为uniqueMember={0},对应于groupOfUniqueNamesLDAP类 [ 2 ] 。 在这种情况下,替换参数是用户的完整专有名称。{1}如果要过滤登录名,可以使用 该参数 。
因此,如果我们使用以下配置
<ldap-authentication-provider user-dn-pattern="uid={0},ou=people"
group-search-base="ou=groups" />
并成功验证为用户“ ben”,随后的权限加载将在目录条目下执行搜索
ou=groups,dc=springframework,dc=org
,以查找包含
uniqueMember
具有value
属性的条目
uid=ben,ou=people,dc=springframework,dc=org
。
默认情况下,权限名称将带有前缀
ROLE_
。
您可以使用
role-prefix
属性
更改此设置
。
如果您不需要任何前缀,请使用
role-prefix="none"
。
有关加载权限的更多信息,请参见
DefaultLdapAuthoritiesPopulator
该类
的Javadoc
。
9.5. 实现类
我们上面使用的名称空间配置选项比明确使用Spring Bean简单易用,而且简洁得多。 在某些情况下,您可能需要了解如何在应用程序上下文中直接配置Spring Security LDAP。 例如,您可能希望自定义某些类的行为。 如果您对使用名称空间配置感到满意,则可以跳过本节和下一节。
最主要的LDAP提供器类是
LdapAuthenticationProvider
,实际上并没有做太多,而是代理的工作,其他两个bean,一个
LdapAuthenticator
和
LdapAuthoritiesPopulator
它负责用户认证和检索用户的组
GrantedAuthority
分别秒。
9.5.1. LdapAuthenticator的实现
验证者还负责检索任何必需的用户属性。 这是因为对属性的权限可能取决于所使用的身份验证类型。 例如,如果以用户身份进行绑定,则可能有必要使用用户自己的权限来读取它们。
Spring Security目前提供两种身份验证策略:
-
直接对LDAP服务器进行身份验证(“绑定”身份验证)。
-
密码比较,将用户提供的密码与存储库中存储的密码进行比较。 可以通过检索password属性的值并在本地对其进行检查来完成此操作,也可以通过执行LDAP“比较”操作来完成,在该操作中,将提供的密码传递给服务器进行比较,并且永远不会检索到真实的密码值。
通用功能
在可能(通过任何一种策略)对用户进行身份验证之前,必须从提供给应用程序的登录名中获得专有名称(DN)。
这可以通过简单的模式匹配(通过设置
setUserDnPatterns
array属性)或通过设置
userSearch
属性来完成。
对于DN模式匹配方法,将使用标准Java模式格式,并将登录名替换为parameter
{0}
。
该模式应相对于配置文件
SpringSecurityContextSource
将绑定
到的DN
(
有关此信息,
请参阅有关
连接LDAP服务器
的部分
)。
例如,如果您使用带有URL的LDAP服务器
ldap://monkeymachine.co.uk/dc=springframework,dc=org
,并且具有pattern
uid={0},ou=greatapes
,则登录名“ gorilla”将映射到DN。
uid=gorilla,ou=greatapes,dc=springframework,dc=org
。
依次尝试每个已配置的DN模式,直到找到匹配项。
有关使用搜索的信息,请参见
下面
有关
搜索对象
的部分
。
也可以使用两种方法的组合-首先检查模式,如果找不到匹配的DN,将使用搜索。
9.5.2. 连接到LDAP服务器
上面讨论的Bean必须能够连接到服务器。
它们都必须提供a
SpringSecurityContextSource
,它是Spring LDAP的扩展
ContextSource
。
除非有特殊要求,否则通常将配置一个
DefaultSpringSecurityContextSource
bean,可以使用LDAP服务器的URL以及(可选)使用“ manager”用户的用户名和密码来配置bean,绑定到服务器时默认使用该用户名和密码(代替匿名绑定)。
有关更多信息,请阅读此类的Javadoc和Spring LDAP的Javadoc
AbstractContextSource
。
9.5.3. LDAP搜索对象
在目录中定位用户条目时,通常需要比简单的DN匹配更复杂的策略。
可以将其封装在一个
LdapUserSearch
实例中,
该
实例可以提供给身份验证器实现,例如,以允许它们定位用户。
提供的实现为
FilterBasedLdapUserSearch
。
FilterBasedLdapUserSearch
该bean使用LDAP过滤器来匹配目录中的用户对象。
Javadoc中针对
JDK DirContext类
的相应搜索方法说明了该过程
。
如此处所述,可以为搜索过滤器提供参数。
对于此类,唯一有效的参数是
{0}
它将用用户的登录名替换。
9.5.4. LdapAuthoritiesPopulator
成功验证用户身份后,
LdapAuthenticationProvider
将通过调用配置的
LdapAuthoritiesPopulator
bean
尝试为用户加载一组权限
。
的
DefaultLdapAuthoritiesPopulator
是,这将通过搜索组的目录,其中所述用户是其成员(通常这些将加载当局的实现
groupOfNames
或
groupOfUniqueNames
目录中的条目)。
有关此类的更多详细信息,请查阅Javadoc。
如果您只想使用LDAP进行身份验证,但是从其他来源(例如数据库)加载授权,则可以提供自己的接口实现,然后注入该接口。
9.5.5. 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服务器
ldap://monkeymachine:389/dc=springframework,dc=org
。
身份验证将通过尝试与DN绑定来执行
uid=<user-login-name>,ou=people,dc=springframework,dc=org
。
身份验证成功后,将通过
ou=groups,dc=springframework,dc=org
使用默认过滤器
在DN下搜索来将角色分配给用户
(member=<user’s-DN>)
。
角色名称将从每个匹配项的“ 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。
9.5.6. LDAP属性和自定义的用户详细信息
使用身份验证的最终结果与
LdapAuthenticationProvider
使用标准
UserDetailsService
接口
的常规Spring Security身份验证相同
。
UserDetails
创建
一个
对象并将其存储在返回的
Authentication
对象中。
与使用一样
UserDetailsService
,一个共同的要求是能够自定义此实现并添加额外的属性。
使用LDAP时,这些通常是用户条目中的属性。
UserDetails
对象
的创建
由提供者的
UserDetailsContextMapper
策略
控制,该
策略负责将用户对象与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
,将从bind操作返回的上下文用于读取属性,否则将使用从配置中获得的标准上下文读取数据
ContextSource
(当
配置
了搜索以定位用户时,这将是由搜索对象)。
9.6. Active Directory验证
Active Directory支持其自己的非标准身份验证选项,并且正常使用模式不太适合标准
LdapAuthenticationProvider
。
通常,身份验证是使用域用户名(以形式
user@domain
)而不是使用LDAP可分辨名称来执行的。
为了简化此操作,Spring Security 3.1具有一个身份验证提供程序,该身份验证提供程序是针对典型的Active Directory设置而定制的。
9.6.1. ActiveDirectoryLdapAuthenticationProvider
配置
ActiveDirectoryLdapAuthenticationProvider
非常简单。
您只需要提供域名和提供服务器地址的LDAP URL
[ 3 ]
。
配置示例如下所示:
<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
对象中使用(
UserDetailsContextMapper
如上所述,可以为此目的注入
a
)。
与目录的所有交互都以用户本身的身份进行。
没有“经理”用户的概念。
默认情况下,从
memberOf
用户条目
的
属性值
获得用户权限
。
可以再次使用来定制分配给用户的权限
UserDetailsContextMapper
。
您还可以将a
GrantedAuthoritiesMapper
注入提供程序实例中,以控制最终在
Authentication
对象
中获得的权限
。
9.7. LDAP Java配置
您可以找到更新以支持基于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实例。
dn:ou = groups,dc = springframework,dc = org 对象类:顶部 对象类:organizationalUnit ou:团体 dn:ou = people,dc = springframework,dc = org 对象类:顶部 对象类:organizationalUnit ou:人 dn:uid = admin,ou = people,dc = springframework,dc = org 对象类:顶部 对象类:人 对象类:organizationalPerson 对象类:inetOrgPerson cn:罗德·约翰逊 sn:约翰逊 uid:管理员 userPassword:密码 dn:uid = user,ou = people,dc = springframework,dc = org 对象类:顶部 对象类:人 对象类:organizationalPerson 对象类:inetOrgPerson cn:Dianne Emu sn:mu uid:用户 userPassword:密码 dn:cn = user,ou = groups,dc = springframework,dc = org 对象类:顶部 对象类:groupOfNames cn:用户 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 对象类:顶部 对象类:groupOfNames cn:管理员 uniqueMember:uid = admin,ou = people,dc = springframework,dc = org
9.8. 身份验证提供者
9.8.1. AuthenticationProvider Java配置
您可以通过将定制暴露
AuthenticationProvider
为Bean
来定义定制身份验证
。
例如,以下代码将在
SpringAuthenticationProvider
实现的情况
下自定义身份验证
AuthenticationProvider
:
仅在
AuthenticationManagerBuilder
尚未填充时使用
|
@Bean
public SpringAuthenticationProvider springAuthenticationProvider() {
return new SpringAuthenticationProvider();
}
9.8.2. AuthenticationProvider XML配置
实际上,与添加到应用程序上下文文件中的几个名称相比,您将需要更具扩展性的用户信息源。
您很可能希望将用户信息存储在数据库或LDAP服务器之类的文件中。
LDAP名称空间配置将在
LDAP一章中介绍
,因此在此不再赘述。
如果您
UserDetailsService
在应用程序上下文中
具有Spring Security的自定义实现(
称为“ myUserDetailsService”),则可以使用对此进行身份验证
<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
用户数据表
的数据库
。
另外,您可以配置Spring Security
JdbcDaoImpl
bean并使用
user-service-ref
属性
指向它
:
<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
在您的应用程序上下文中实现的Bean的名称
在哪里
AuthenticationProvider
。
您可以使用多个
authentication-provider
元素,在这种情况下,将按照声明它们的顺序查询提供者。
有关
如何
使用命名空间配置
Spring Security的更多信息,
请参见
认证管理器和命名
AuthenticationManager
空间。
9.9。 UserDetailsService
您可以通过将定制暴露
UserDetailsService
为Bean
来定义定制身份验证
。
例如,以下代码将在
SpringDataUserDetailsService
实现的情况
下自定义身份验证
UserDetailsService
:
仅当
AuthenticationManagerBuilder
尚未填充且未
AuthenticationProviderBean
定义
no时使用
。
|
@Bean
public SpringDataUserDetailsService springDataUserDetailsService() {
return new SpringDataUserDetailsService();
}
您还可以通过将a
PasswordEncoder
作为bean
公开来自定义密码的编码方式
。
例如,如果使用bcrypt,则可以添加如下所示的bean定义:
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
9.10。 密码编码
Spring Security的
PasswordEncoder
界面用于执行密码的单向转换,以允许安全地存储密码。
给出的
PasswordEncoder
是一种单向转换,而密码转换需要采用两种方式(即存储用于对数据库进行身份验证的凭据)时,则无意使用。
通常
PasswordEncoder
用于在认证时存储需要与用户提供的密码进行比较的密码。
9.10.1. 密码记录
多年来,用于存储密码的标准机制已经发展。 最初,密码以纯文本格式存储。 假定密码是安全的,因为数据存储密码已保存在访问它所需的凭据中。 但是,恶意用户能够使用SQL Injection这样的攻击找到方法来获取用户名和密码的大型“数据转储”。 随着越来越多的用户凭证成为公共安全专家,我们意识到我们需要做更多的工作来保护用户密码。
然后鼓励开发人员在通过诸如SHA-256之类的单向哈希运行密码后存储密码。 当用户尝试进行身份验证时,会将散列的密码与他们键入的密码的散列进行比较。 这意味着系统仅需要存储密码的单向哈希。 如果发生违规,则仅暴露密码的一种哈希方式。 由于散列是一种方式,计算出给定哈希值的密码很难计算,因此找出系统中的每个密码都不值得。 为了击败这个新系统,恶意用户决定创建称为 Rainbow Tables的 查找 表 。 他们不必每次都猜测每个密码,而是计算一次密码并将其存储在查找表中。
为了减轻Rainbow Tables的有效性,鼓励开发人员使用加盐的密码。 不仅将密码用作哈希函数的输入,还将为每个用户的密码生成随机字节(称为salt)。 盐和用户的密码将通过散列函数运行,从而产生唯一的散列。 盐将以明文形式与用户密码一起存储。 然后,当用户尝试进行身份验证时,会将哈希密码与存储的盐的哈希值和他们键入的密码进行比较。 唯一的盐意味着Rainbow Tables不再有效,因为每种盐和密码组合的哈希值都不同。
在现代,我们意识到加密哈希(例如SHA-256)不再安全。 原因是使用现代硬件,我们可以每秒执行数十亿次哈希计算。 这意味着我们可以轻松地分别破解每个密码。
现在鼓励开发人员利用自适应单向功能来存储密码。 带有自适应单向功能的密码验证有意占用大量资源(即CPU,内存等)。 自适应单向功能允许配置“工作因数”,该因数会随着硬件的改进而增加。 建议将“工作因数”调整为大约1秒钟,以验证系统上的密码。 这种权衡使攻击者难以破解密码,但代价却不高,这给您自己的系统带来了沉重负担。 Spring Security试图为“工作因素”提供一个良好的起点,但是鼓励用户为自己的系统自定义“工作因素”,因为不同系统之间的性能会有很大差异。 bcrypt , PBKDF2 , scrypt 和 Argon2 。
由于自适应单向功能有意占用大量资源,因此为每个请求验证用户名和密码都会大大降低应用程序的性能。 Spring Security(或任何其他库)无法采取任何措施来加快密码的验证速度,因为通过增加验证资源的强度来获得安全性。 鼓励用户将长期凭证(即用户名和密码)交换为短期凭证(即会话,OAuth令牌等)。 可以快速验证短期凭证,而不会损失任何安全性。
9.10.2. DelegatingPasswordEncoder
在此之前的Spring Security 5.0的默认
PasswordEncoder
是
NoOpPasswordEncoder
其所需的明文密码。
根据“
密码历史记录”
部分,您可能希望现在的默认值
PasswordEncoder
是
BCryptPasswordEncoder
。
但是,这忽略了三个现实问题:
-
有许多使用旧密码编码的应用程序无法轻松迁移
-
密码存储的最佳做法将再次更改。
-
作为一个框架,Spring Security不能经常进行重大更改
相反,Spring Security引入了
DelegatingPasswordEncoder
通过以下方法解决所有问题的方法:
-
确保使用当前密码存储建议对密码进行编码
-
允许以现代和旧式格式验证密码
-
允许将来升级编码
您可以轻松地构建的一个实例
DelegatingPasswordEncoder
使用
PasswordEncoderFactories
。
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
的标识符是用于查找
PasswordEncoder
应使用
的标识符,
并且
encodedPassword
是所选的原始编码密码
PasswordEncoder
。
在
id
必须在密码的开始,开始
{
和结束
}
。
如果
id
找不到,
id
则将为null。
例如,以下可能是使用different编码的密码列表
id
。
所有原始密码均为“密码”。
{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG (1)
{noop}password (2)
{pbkdf2}5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc (3)
{scrypt}$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc= (4)
{sha256}97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0 (5)
| 1个 |
第一个密码的
PasswordEncoder
ID为
bcrypt
,编码
密码
为
$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
。
匹配时将委托给
BCryptPasswordEncoder |
| 2 |
第二个密码的
PasswordEncoder
ID为
noop
,编码
密码
为
password
。
匹配时将委托给
NoOpPasswordEncoder |
| 3 |
第三个密码的
PasswordEncoder
ID为
pbkdf2
,编码
密码
为
5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc
。
匹配时将委托给
Pbkdf2PasswordEncoder |
| 4 |
第四个密码的
PasswordEncoder
ID为
scrypt
,编码的
密码
为
,
$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc=
匹配时将委派给
SCryptPasswordEncoder |
| 5 |
最终密码的
PasswordEncoder
ID为
sha256
,编码
密码
为
97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0
。
匹配时将委托给
StandardPasswordEncoder |
|
一些用户可能会担心为潜在的黑客提供了存储格式。
不必担心,因为密码的存储不依赖于算法是秘密。
此外,大多数格式很容易让攻击者弄清楚没有前缀的情况。
例如,BCrypt密码通常以开头
|
密码编码
的
idForEncode
传递到构造确定哪个
PasswordEncoder
将用于编码的密码。
在
DelegatingPasswordEncoder
上面
的
构造中,这意味着编码结果
password
将委托给
BCryptPasswordEncoder
并以开头
{bcrypt}
。
最终结果如下所示:
{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
密码匹配
匹配是基于
{id}
和
id
到
PasswordEncoder
构造函数中提供的
的映射
来完成的
。
我们的“
密码存储格式”
示例提供了一个有效的示例。
默认情况下,
matches(CharSequence, String)
使用密码和
id
未映射(包括空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();
这会散列存储的密码,但是密码仍在内存和已编译的源代码中公开。 因此,对于生产环境它仍然不被认为是安全的。 对于生产,您应该在外部对密码进行哈希处理。
故障排除
当所存储的密码之一没有如“ 密码存储格式”中 所述的id时,会发生以下错误 。
java.lang.IllegalArgumentException:没有为id“ null”映射的PasswordEncoder
在org.springframework.security.crypto.password.DelegatingPasswordEncoder $ UnmappedIdPasswordEncoder.matches(DelegatingPasswordEncoder.java:233)处
在org.springframework.security.crypto.password.DelegatingPasswordEncoder.matches(DelegatingPasswordEncoder.java:196)
解决该错误的最简单方法是切换为显式提供
PasswordEncoder
密码进行编码的方式。
解决此问题的最简单方法是弄清楚密码的当前存储方式,并明确提供正确的密码
PasswordEncoder
。
如果您是从Spring Security 4.2.x迁移的,则可以通过公开一个
NoOpPasswordEncoder
bean
恢复到以前的行为
。
例如,如果您使用的是Java配置,则可以创建如下所示的配置:
|
还原为
|
@Bean
public static NoOpPasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
如果您使用的是XML配置,则可以公开
PasswordEncoder
带有id的
passwordEncoder
:
<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
有关映射的完整列表,请参阅 PasswordEncoderFactories 上的Javadoc 。
9.10.3. BCryptPasswordEncoder
该
BCryptPasswordEncoder
实现使用广泛支持的
bcrypt
算法对密码进行哈希处理。
为了使其更能抵抗密码破解,bcrypt故意降低了速度。
与其他自适应单向功能一样,应将其调整为大约1秒钟,以验证系统上的密码。
// Create an encoder with strength 16
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(16);
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));
9.10.4. Argon2PasswordEncoder
该
Argon2PasswordEncoder
实现使用
Argon2
算法对密码进行哈希处理。
Argon2是“
密码哈希竞赛”
的获胜者
。
为了克服自定义硬件上的密码破解问题,Argon2是一种故意慢速的算法,需要大量内存。
与其他自适应单向功能一样,应将其调整为大约1秒钟,以验证系统上的密码。
如果
Argon2PasswordEncoder
需要BouncyCastle,则为
当前实现
。
// Create an encoder with all the defaults
Argon2PasswordEncoder encoder = new Argon2PasswordEncoder();
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));
9.10.5. Pbkdf2PasswordEncoder
该
Pbkdf2PasswordEncoder
实现使用
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));
9.10.6. SCryptPasswordEncoder
该
SCryptPasswordEncoder
实现使用
scrypt
算法对密码进行哈希处理。
为了克服自定义硬件scrypt上的密码破解问题,它是一种故意缓慢的算法,需要大量内存。
与其他自适应单向功能一样,应将其调整为大约1秒钟,以验证系统上的密码。
// Create an encoder with all the defaults
SCryptPasswordEncoder encoder = new SCryptPasswordEncoder();
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));
9.10.7. 其他密码编码器
存在大量其他
PasswordEncoder
实现完全是为了向后兼容的实现。
它们均已弃用,以表明它们不再被视为安全。
但是,由于很难迁移现有的旧系统,因此没有删除它们的计划。
9.10.8. 密码编码器XML配置
密码应始终使用为此目的而设计的安全哈希算法(而不是诸如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。
9.11. 身份验证管理器和命名空间
Spring Security提供身份验证服务的主要接口是
AuthenticationManager
。
这通常是Spring Security的
ProviderManager
类
的实例,
如果您以前使用过该框架,则可能已经熟悉。
否则,将在后面的“
技术概述”一章中进行介绍
。
使用
authentication-manager
命名空间元素
注册Bean实例
。
AuthenticationManager
如果通过名称空间使用HTTP或方法安全性,
则不能使用自定义
,但这不会成为问题,因为您可以完全控制
AuthenticationProvider
所使用的。
您可能要向注册其他
AuthenticationProvider
bean,
ProviderManager
然后可以使用
<authentication-provider>
带有
ref
属性
的
元素
来执行此操作
,其中属性的值是要添加的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>
9.12. 会话管理
与HTTP会话相关的功能由
SessionManagementFilter
和
SessionAuthenticationStrategy
过滤器委托给
的
接口
的组合来处理
。
典型的用法包括防止会话固定保护攻击,检测会话超时以及限制已认证用户可以同时打开多少个会话。
9.12.1. 检测超时
您可以配置Spring Security来检测提交的无效会话ID,并将用户重定向到适当的URL。
这是通过以下
session-management
元素
实现的
:
<http>
...
<session-management invalid-session-url="/invalidSession.htm" />
</http>
请注意,如果使用此机制来检测会话超时,则在用户注销然后重新登录而不关闭浏览器的情况下,它可能会错误地报告错误。 这是因为在使会话无效时不会清除会话cookie,即使用户已注销,会话cookie也会重新提交。 您可能能够在注销时显式删除JSESSIONID cookie,例如通过在注销处理程序中使用以下语法:
<http>
<logout delete-cookies="JSESSIONID" />
</http>
不幸的是,不能保证它可以与每个servlet容器一起使用,因此您需要在您的环境中对其进行测试
===如果您正在代理后运行应用程序,则还可以通过配置代理服务器来删除会话cookie。
例如,使用Apache HTTPD的mod_headers,以下指令将
JSESSIONID
通过在注销请求的响应
中使
cookie过期
来删除该
cookie(假设该应用程序已部署在path下
/tutorial
):
|
<LocationMatch "/tutorial/logout">
Header always set Set-Cookie "JSESSIONID=;Path=/tutorial;Expires=Thu, 01 Jan 1970 00:00:00 GMT"
</LocationMatch>
===
9.12.2. 并发会话控制
如果您希望限制单个用户登录应用程序的能力,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>
然后,第二次登录将被拒绝。
“拒绝”是指如果用户
authentication-failure-url
正在使用基于表单的登录名,则
该用户将被发送到该页面
。
如果第二次身份验证是通过另一个非交互机制(例如“ remember-me”)进行的,则“未授权”(401)错误将发送给客户端。
相反,如果要使用错误页面,则可以将属性添加
session-authentication-error-url
到
session-management
元素。
如果使用定制的身份验证筛选器进行基于表单的登录,则必须显式配置并发会话控制支持。 更多细节可以在 会话管理一章中找到 。
9.12.3. 会话固定攻击防护
会话固定
攻击是一种潜在的风险,恶意攻击者有可能通过访问站点来创建会话,然后诱使另一个用户以相同的会话登录(通过向他们发送包含会话标识符作为参数的链接,以便例)。
Spring Security通过创建新会话或在用户登录时更改会话ID来自动防御这种情况。如果您不需要此保护,或者与其他要求冲突,则可以使用上的
session-fixation-protection
属性
控制行为
<session-management>
,有四个选项
-
none-什么也不要做。 原始会话将保留。 -
newSession-创建一个新的“干净”会话,而不复制现有会话数据(仍将复制与Spring Security相关的属性)。 -
migrateSession-创建一个新会话,并将所有现有会话属性复制到新会话。 这是Servlet 3.0或更早版本的容器中的默认值。 -
changeSessionId-不要创建新的会话。 相反,请使用Servlet容器(HttpServletRequest#changeSessionId()) 提供的会话固定保护 。 此选项仅在Servlet 3.1(Java EE 7)和更高版本的容器中可用。 在较旧的容器中指定它会导致异常。 这是Servlet 3.1和更高版本容器中的默认设置。
发生会话固定保护时,将导致在
SessionFixationProtectionEvent
应用程序上下文中
发布会话固定保护
。
如果使用
changeSessionId
,这种保护会
也
导致任何
javax.servlet.http.HttpSessionIdListener
被通知S,所以如果你的代码侦听这两个事件谨慎使用。
有关
其他信息,
请参见“
会话管理”
一章。
9.12.4. SessionManagementFilter
该
SessionManagementFilter
检查的内容
SecurityContextRepository
针对的当前内容
SecurityContextHolder
,以确定是否用户已经在当前请求期间被认证,通常由非交互式认证机制,如预认证或记住-ME
[ 4 ]
。
如果存储库包含安全上下文,则过滤器不执行任何操作。
如果不是,并且线程局部
SecurityContext
包含一个(非匿名)
Authentication
对象,则过滤器将假定它们已由堆栈中的先前过滤器验证。
然后它将调用configure
SessionAuthenticationStrategy
。
9.12.5. 会话认证策略
SessionAuthenticationStrategy
被两个
SessionManagementFilter
和
AbstractAuthenticationProcessingFilter
,因此,如果您使用的是自定义表单登录类,例如,您需要将其注入到这两个。
在这种情况下,将名称空间和自定义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" />
请注意,
SessionFixationProtectionStrategy
如果您在实现的会话中存储Bean
HttpSessionBindingListener
(包括Spring会话范围的Bean)
,则使用默认值
可能会导致问题
。
有关更多信息,请参见Javadoc。
9.12.6. 并发控制
Spring Security可以防止主体同时向同一应用程序进行身份验证超过指定次数。 许多ISV都利用此功能来实施许可,而Web管理员喜欢此功能,因为它有助于防止人们共享登录名。 例如,您可以阻止用户“蝙蝠侠”从两个不同的会话登录到Web应用程序。 您可以使他们的先前登录到期,也可以在他们再次尝试登录时报告错误,从而阻止第二次登录。 请注意,如果您使用第二种方法,则未明确注销的用户(例如,刚刚关闭浏览器的用户)将无法再次登录,直到他们的原始会话期满为止。
名称空间支持并发控制,因此,请查阅前面的名称空间一章以获取最简单的配置。 有时您需要自定义内容。
该实现使用的专门版本
SessionAuthenticationStrategy
,称为
ConcurrentSessionControlAuthenticationStrategy
。
|
以前,并发身份验证检查是由进行的
|
要使用并发会话支持,您需要将以下内容添加到
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" />
将侦听器添加到
web.xml
会导致
每次a
开始或终止
时将
an
ApplicationEvent
发布到Spring
。
这很关键,因为它允许在
会话结束时通知。
如果没有它,即使用户退出另一个会话或超时,一旦超出会话允许量,用户将永远无法再次登录。
ApplicationContext
HttpSession
SessionRegistryImpl
查询SessionRegistry中当前经过身份验证的用户及其会话
通过名称空间或使用普通bean设置并发控制具有有益的副作用,即为您提供对
SessionRegistry
可以直接在应用程序中使用的
的引用
,因此即使您不想限制会话数用户可能已经拥有了,但仍然值得建立基础架构。
您可以将该
maximumSession
属性
设置
为-1,以允许无限制的会话。
如果使用的是名称空间,则可以
SessionRegistry
使用
session-registry-alias
属性
设置内部创建的别名
,并提供一个引用,您可以将其注入到自己的bean中。
该
getAllPrincipals()
方法为您提供了当前已认证用户的列表。
您可以通过调用
getAllSessions(Object principal, boolean includeExpiredSessions)
方法
列出用户的会话,该
方法将返回
SessionInformation
对象
列表
。
您还可以通过调用终止用户的会话
expireNow()
上一个
SessionInformation
实例。
当用户返回到应用程序时,将阻止他们继续操作。
例如,您可能会发现这些方法在管理应用程序中很有用。
看看Javadoc了解更多信息。
9.13. 记住我身份验证
9.13.1. 总览
“记住我”或“持久登录”身份验证是指网站能够记住会话之间的主体身份。 通常,这是通过向浏览器发送一个cookie来实现的,该cookie在以后的会话中被检测到并导致自动登录。 Spring Security提供了进行这些操作所需的钩子,并具有两个具体的“记住我”实现。 一种使用散列来保留基于cookie的令牌的安全性,另一种使用数据库或其他持久性存储机制来存储生成的令牌。
请注意,两个实现都需要一个
UserDetailsService
。
如果您正在使用不使用的身份验证提供程序
UserDetailsService
(例如LDAP提供程序),那么除非您
UserDetailsService
在应用程序上下文中
也有
bean,
否则它将无法工作
。
9.13.2. 简单的基于哈希的令牌方法
这种方法使用哈希来实现有用的“记住我”策略。 本质上,在成功进行交互式身份验证后,会将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
因此,“记住我”令牌仅在指定的期限内有效,并且前提是用户名,密码和密钥不变。 值得注意的是,这存在潜在的安全问题,因为捕获的“记住我”令牌将可从任何用户代理使用,直到令牌到期为止。 这与摘要身份验证相同。 如果委托人知道已捕获令牌,则他们可以轻松更改密码并立即使所有出现问题的“记住我”令牌失效。 如果需要更重要的安全性,则应使用下一节所述的方法。 另外,根本不应该使用“记住我”服务。
如果您熟悉
名称空间配置
一章中讨论的主题,则
只需添加以下
<remember-me>
元素
即可启用“记住我”身份验证
:
<http>
...
<remember-me key="myAppKey"/>
</http>
在
UserDetailsService
通常将被自动地选择。
如果您的应用程序上下文中有多个,则需要指定应与哪个
user-service-ref
属性
一起使用
,其中的值是
UserDetailsService
bean
的名称
。
9.13.3. 持久令牌方法
这种方法是基于 http://jaspan.com/improved_persistent_login_cookie_best_practice 文章 进行的一些细微修改 [ 5 ] 。 要将这种方法与名称空间配置一起使用,您将提供一个数据源 Reference:
<http>
...
<remember-me data-source-ref="someDataSource"/>
</http>
数据库应包含一个
persistent_logins
使用以下SQL(或等效SQL)创建
的
表:
create table persistent_logins (username varchar(64) not null,
series varchar(64) primary key,
token varchar(64) not null,
last_used timestamp not null)
9.13.4. 记住我的接口和实现
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()
方法。
每当
autoLogin()
方法
不包含
RememberMeAuthenticationFilter
时
,
都会通过
调用
该
方法
。
因此,此接口为底层的“记住我”实现提供了与身份验证相关的事件的充分通知,并在候选Web请求可能包含cookie并希望被记住时委托给该实现。
这种设计允许使用任何数量的“记住我”实施策略。
上面我们已经看到,Spring Security提供了两种实现。
我们将依次研究这些。
SecurityContextHolder
Authentication
TokenBasedRememberMeServices
此实现支持“
基于简单哈希的令牌方法”中
描述的更
简单方法
。
TokenBasedRememberMeServices
生成一个
RememberMeAuthenticationToken
,由处理
RememberMeAuthenticationProvider
。
A
key
在此身份验证提供程序和之间共享
TokenBasedRememberMeServices
。
另外,还
TokenBasedRememberMeServices
需要一个UserDetailsService,从中可以检索用户名和密码以进行签名比较,并生成
RememberMeAuthenticationToken
包含正确
GrantedAuthority
的。
应用程序应提供某种注销命令,如果用户请求,该命令会使cookie无效。
TokenBasedRememberMeServices
还实现了Spring Security的
LogoutHandler
界面,因此可用于
LogoutFilter
自动清除cookie。
在应用程序上下文中启用“记住我”服务所需的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
到您的
属性中
FilterChainProxy
(通常紧随在之后
UsernamePasswordAuthenticationFilter
)。
PersistentTokenBasedRememberMeServices
此类的使用方式与相同
TokenBasedRememberMeServices
,但还需要配置一个
PersistentTokenRepository
来存储令牌。
有两种标准实现。
-
InMemoryTokenRepositoryImpl仅用于测试。 -
JdbcTokenRepositoryImpl将令牌存储在数据库中。
上面的“ 持久令牌方法”中 描述了数据库模式 。
9.14. OpenID支持
<http>
<intercept-url pattern="/**" access="ROLE_USER" />
<openid-login />
</http>
然后,您应该向OpenID提供者注册自己(例如myopenid.com),并将用户信息添加到内存中
<user-service>
:
<user name="https://jimi.hendrix.myopenid.com/" authorities="ROLE_USER" />
您应该能够使用该
myopenid.com
网站
登录进行
身份验证。
UserDetailsService
通过
user-service-ref
在
openid-login
元素
上
设置
属性,
还可以选择一个特定的
bean来使用OpenID
。
有关
更多信息,
请参见上一节中的
身份验证提供程序
。
请注意,我们已从上述用户配置中省略了password属性,因为这组用户数据仅用于加载用户的权限。
系统会在内部生成一个随机密码,以防止您意外地将此用户数据用作配置中其他位置的身份验证源。
9.14.1. 属性交换
支持OpenID 属性交换 。 例如,以下配置将尝试从OpenID提供程序中检索电子邮件和全名,以供应用程序使用:
<openid-login>
<attribute-exchange>
<openid-attribute name="email" type="https://axschema.org/contact/email" required="true"/>
<openid-attribute name="name" type="https://axschema.org/namePerson"/>
</attribute-exchange>
</openid-login>
每个OpenID属性的“类型”是一个URI,由特定模式确定,在这种情况下为
https://axschema.org/
。
如果必须检索属性以成功进行身份验证,则
required
可以设置
该
属性。
支持的确切架构和属性将取决于您的OpenID提供程序。
属性值作为身份验证过程的一部分返回,之后可以使用以下代码进行访问:
OpenIDAuthenticationToken token =
(OpenIDAuthenticationToken)SecurityContextHolder.getContext().getAuthentication();
List<OpenIDAttribute> attributes = token.getAttributes();
该
OpenIDAttribute
包含的属性类型和所检索的值(或多个值在多值的属性的情况下)。
SecurityContextHolder
当我们在
技术概述
一章中
查看核心Spring Security组件时,
将了解有关如何使用
该类的
更多信息
。
如果您希望使用多个身份提供者,则还支持多个属性交换配置。
您可以
attribute-exchange
使用
identifier-matcher
每个属性
提供多个
元素
。
它包含一个正则表达式,它将与用户提供的OpenID标识符匹配。
有关示例配置,请参见代码库中的OpenID示例应用程序,为Google,Yahoo和MyOpenID提供程序提供了不同的属性列表。
9.15. 匿名认证
9.15.1. 总览
通常,采取“默认拒绝”的做法被认为是一种良好的安全做法,在该行为中,您明确指定允许的内容,并禁止其他所有内容。
定义未经身份验证的用户可以访问的内容的情况与此类似,尤其是对于Web应用程序。
许多站点要求用户必须通过身份验证才能使用少数几个URL(例如,主页和登录页面)。
在这种情况下,最简单的是为这些特定的URL定义访问配置属性,而不是为每个受保护的资源定义访问配置属性。
换句话说,有时候很高兴
ROLE_SOMETHING
默认情况下是必需的,并且仅允许此规则的某些例外,例如应用程序的登录,注销和主页。
您也可以从过滤器链中完全忽略这些页面,从而绕过访问控制检查,但是由于其他原因,这可能是不可取的,特别是如果这些页面对于经过身份验证的用户而言行为不同。
这就是我们所说的匿名身份验证。
请注意,“匿名身份验证”的用户和未经身份验证的用户之间没有真正的概念差异。
Spring Security的匿名身份验证只是为您提供了一种更方便的方式来配置访问控制属性。
getCallerPrincipal
即使确实存在匿名身份验证对象,
对Servlet API调用的调用(
例如)仍将返回null
SecurityContextHolder
。
在其他情况下,匿名身份验证很有用,例如,当审核拦截器查询时,
SecurityContextHolder
以标识哪个主体负责给定操作。
如果类知道
SecurityContextHolder
始终包含一个
Authentication
对象,并且永不
包含一个
对象,
则可以更强大地编写类
null
。
9.15.2. 组态
使用HTTP配置Spring Security 3.0时会自动提供匿名身份验证支持,并且可以使用
<anonymous>
元素
进行自定义(或禁用)匿名身份验证
。
除非您使用传统的Bean配置,否则无需配置此处描述的Bean。
三个类共同提供了匿名身份验证功能。
AnonymousAuthenticationToken
是的实现
Authentication
,并存储
GrantedAuthority
适用于匿名主体的。
有一个对应的
AnonymousAuthenticationProvider
,它被链接到,
ProviderManager
因此
AnonymousAuthenticationToken
可以接受。
最后,有一个
AnonymousAuthenticationFilter
,在正常的身份验证机制之后链接在一起
AnonymousAuthenticationToken
,
SecurityContextHolder
如果那里不存在
,
则会
自动将添加
到
Authentication
。
筛选器和身份验证提供程序的定义如下所示:
<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
是在过滤器和认证提供器之间共享,使得由前创建的标记由后者所接受
[ 6 ]
。
该
userAttribute
在的形式来表达
usernameInTheAuthenticationToken,grantedAuthority[,grantedAuthority]
。
这与的
userMap
属性
等号后使用的语法相同
InMemoryDaoImpl
。
如前所述,匿名身份验证的好处是所有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>
9.15.3. AuthenticationTrustResolver
完善匿名认证讨论的是
AuthenticationTrustResolver
接口及其相应的
AuthenticationTrustResolverImpl
实现。
该接口提供了一种
isAnonymous(Authentication)
方法,该方法允许感兴趣的类考虑这种特殊类型的身份验证状态。
在
ExceptionTranslationFilter
使用该接口在处理
AccessDeniedException
秒。
如果
AccessDeniedException
抛出,并且身份验证为匿名类型,则过滤器将启动,而不是引发403(禁止)响应,
AuthenticationEntryPoint
以便委托人可以正确地进行身份验证。
这是必要的区别,否则主体将始终被视为“已认证”,并且永远不会获得通过表单,基本,摘要或某些其他常规认证机制登录的机会。
您通常会
ROLE_ANONYMOUS
在上面的拦截器配置中
看到该
属性被替换为
IS_AUTHENTICATED_ANONYMOUSLY
,这在定义访问控制时实际上是相同的。
这是一个使用示例
AuthenticatedVoter
,我们将在
授权一章中
看到
。
它使用
AuthenticationTrustResolver
来处理此特定的配置属性,并将访问权限授予匿名用户。
该
AuthenticatedVoter
方法功能更强大,因为它使您可以区分匿名用户,记住我的用户和经过完全认证的用户。
如果您不需要此功能,则可以坚持使用
ROLE_ANONYMOUS
,它将由Spring Security的standard处理
RoleVoter
。
9.16. 验证前方案
在某些情况下,您想使用Spring Security进行授权,但是在访问该应用程序之前,某些外部系统已经对该用户进行了可靠的身份验证。 我们将这些情况称为“预身份验证”方案。 示例包括X.509,Siteminder和运行应用程序的Java EE容器进行的身份验证。 使用预身份验证时,Spring Security必须
-
标识发出请求的用户。
-
获取用户的权限。
详细信息将取决于外部身份验证机制。
如果是X.509,则可以通过其证书信息来标识用户;如果是Siteminder,则可以通过HTTP请求标头来标识用户。
如果依靠容器身份验证,将通过
getUserPrincipal()
在传入的HTTP请求上
调用
方法
来标识用户
。
在某些情况下,外部机制可能会为用户提供角色/权限信息,但在其他情况下,必须从单独的来源(例如)获得权限
UserDetailsService
。
9.16.1. 预身份验证框架类
因为大多数预认证机制遵循相同的模式,所以Spring Security具有一组类,这些类提供了用于实现预认证的认证提供程序的内部框架。
这消除了重复,并允许以结构化的方式添加新的实现,而不必从头开始编写所有内容。
如果您想使用
X.509身份验证之
类的内容,则无需了解这些类
,因为它已经具有一个名称空间配置选项,该选项更易于使用和入门。
如果您需要使用显式的bean配置或计划编写自己的实现,那么对提供的实现如何工作的理解将很有用。
您会在
org.springframework.security.web.authentication.preauth
。
我们仅在此处提供概述,因此您应该在适当的地方查阅Javadoc和源代码。
AbstractPreAuthenticatedProcessingFilter
此类将检查安全性上下文的当前内容,如果为空,它将尝试从HTTP请求中提取用户信息并将其提交给
AuthenticationManager
。
子类重写以下方法来获取此信息:
protected abstract Object getPreAuthenticatedPrincipal(HttpServletRequest request);
protected abstract Object getPreAuthenticatedCredentials(HttpServletRequest request);
调用完这些之后,过滤器将创建一个
PreAuthenticatedAuthenticationToken
包含返回数据的,并将其提交进行身份验证。
这里的“身份验证”实际上只是意味着进一步处理以加载用户的权限,但是遵循标准的Spring Security身份验证体系结构。
像其他Spring Security身份验证过滤器一样,预身份验证过滤器具有一个
authenticationDetailsSource
属性,
该
属性默认情况下将创建一个
WebAuthenticationDetails
对象来存储其他信息,例如会话标识符和
对象
details
属性中的
始发IP地址
Authentication
。
如果可以从预身份验证机制获取用户角色信息,则数据也将存储在此属性中,其中详细信息实现了
GrantedAuthoritiesContainer
接口。
这使身份验证提供程序可以读取从外部分配给用户的权限。
接下来,我们将看一个具体示例。
J2eeBasedPreAuthenticatedWebAuthenticationDetailsSource
如果为筛选器配置了
authenticationDetailsSource
此类的实例,则通过
isUserInRole(String role)
为每个“映射角色”的预定集合
调用
方法来
获得权限信息
。
该类从configure中获取这些
MappableAttributesRetriever
。
可能的实现包括在应用程序上下文中对列表进行硬编码,以及从
文件中
的
<security-role>
信息中
读取角色信息
web.xml
。
预认证示例应用程序使用后一种方法。
还有一个附加阶段,其中
GrantedAuthority
使用configureed
将角色(或属性)映射到Spring Security
对象
Attributes2GrantedAuthoritiesMapper
。
默认值只是将通常的
ROLE_
前缀
添加
到名称中,但是它使您可以完全控制行为。
PreAuthenticatedAuthenticationProvider
经过预身份验证的提供程序除了
UserDetails
为用户
加载
对象外,
仅需做其他事情
。
它通过委派给来实现此目的
AuthenticationUserDetailsService
。
后者与标准类似,
UserDetailsService
但采用一个
Authentication
对象而不只是用户名:
public interface AuthenticationUserDetailsService {
UserDetails loadUserDetails(Authentication token) throws UsernameNotFoundException;
}
该接口可能还有其他用途,但具有预身份验证功能,它允许访问包装在
Authentication
对象中
的授权机构
,如上一节所述。
该
PreAuthenticatedGrantedAuthoritiesUserDetailsService
班做到这一点。
或者,它可以
UserDetailsService
通过
UserDetailsByNameServiceWrapper
实现
委派给标准
。
Http403ForbiddenEntryPoint
AuthenticationEntryPoint
在
技术概述
一章中
对此
进行了讨论
。
通常,它负责启动未经身份验证的用户的身份验证过程(当他们尝试访问受保护的资源时),但是在经过预先身份验证的情况下,这并不适用。
ExceptionTranslationFilter
如果您没有将预身份验证与其他身份验证机制结合使用,则只能使用此类的实例
配置
。
如果用户被
AbstractPreAuthenticatedProcessingFilter
空身份验证
拒绝而将调用该方法
。
403
如果被调用,
它将始终返回
-forbidden响应代码。
9.16.2. 具体实施
X.509身份验证在其 单独的章节中介绍 。 在这里,我们将看一些为其他预先认证的场景提供支持的类。
请求标头身份验证(Siteminder)
外部认证系统可以通过在HTTP请求上设置特定的标头来向应用程序提供信息。
一个著名的例子是Siteminder,它在名为的标头中传递用户名
SM_USER
。
该类支持该机制,该类
RequestHeaderAuthenticationFilter
仅从标头中提取用户名。
默认情况下,使用名称
SM_USER
作为标题名称。
有关更多详细信息,请参见Javadoc。
|
请注意,当使用这样的系统时,框架完全不执行身份验证检查,并且 正确配置外部系统并保护对应用程序的所有访问 非常 重要。 如果攻击者能够在不检测到原始请求的情况下伪造标头,则他们可能会选择所需的任何用户名。 |
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>
我们在这里假设
安全名称空间
用于配置。
还假定您已
UserDetailsService
在配置中
添加了一个
(称为“ userDetailsService”)以加载用户的角色。
Java EE容器认证
该类
J2eePreAuthenticatedProcessingFilter
将从的
userPrincipal
属性中
提取用户名
HttpServletRequest
。
如上文
J2eeBasedPreAuthenticatedWebAuthenticationDetailsSource中
所述,通常将此过滤器的使用与Java EE角色的使用结合起来
。
代码库中有一个使用此方法的示例应用程序,因此如果您有兴趣,可以从github上获取代码,并查看应用程序上下文文件。
代码在
samples/xml/preauth
目录中。
9.17. Java身份验证和授权服务(JAAS)提供程序
9.17.2. AbstractJaasAuthenticationProvider
这
AbstractJaasAuthenticationProvider
是所提供的JAAS
AuthenticationProvider
实现
的基础
。
子类必须实现创建的方法
LoginContext
。
在
AbstractJaasAuthenticationProvider
具有许多下面讨论的是可注入它依赖。
JAAS CallbackHandler
大多数JAAS
LoginModule
都需要某种回调。
这些回调通常用于从用户获取用户名和密码。
在Spring Security部署中,Spring Security负责此用户交互(通过身份验证机制)。
因此,当认证请求被委托给JAAS时,Spring Security的认证机制将已经完全填充了一个
Authentication
对象,其中包含JAAS所需的所有信息
LoginModule
。
因此,Spring Security的JAAS包提供了两个默认的回调处理程序,
JaasNameCallbackHandler
和
JaasPasswordCallbackHandler
。
这些回调处理程序均实现
JaasAuthenticationCallbackHandler
。
在大多数情况下,无需了解内部机制即可简单地使用这些回调处理程序。
对于那些需要完全控制回调行为,内部
AbstractJaasAuthenticationProvider
封装这些
JaasAuthenticationCallbackHandler
s的一个
InternalCallbackHandler
。
该
InternalCallbackHandler
是真正实现JAAS正常类
CallbackHandler
接口。
每当使用JAAS时
LoginModule
,都会向其传递一个已配置
InternalCallbackHandler
的
应用程序上下文列表
。
如果
LoginModule
请求针对的回调
InternalCallbackHandler
,则该回调又传递给
JaasAuthenticationCallbackHandler
正在包装的。
JAAS授权人
JAAS与校长合作。
JAAS中甚至将“角色”表示为主体。
另一方面,Spring Security可以处理
Authentication
对象。
每个
Authentication
对象包含单个主体和多个
GrantedAuthority
。
为了促进这些不同概念之间的映射,Spring Security的JAAS软件包包括一个
AuthorityGranter
接口。
An
AuthorityGranter
负责检查JAAS委托人并返回一组
String
,代表分配给委托人的权限。
对于每个返回的授权字符串,
AbstractJaasAuthenticationProvider
会创建一个
JaasGrantedAuthority
(实现Spring Security的
GrantedAuthority
接口),其中包含授权字符串和
AuthorityGranter
传递
的JAAS主体
。
首先
AbstractJaasAuthenticationProvider
通过使用JAAS成功验证用户的凭据
LoginModule
,然后访问,
获得JAAS主体
LoginContext
。
进行调用
LoginContext.getSubject().getPrincipals()
,并将每个结果主体传递给
AuthorityGranter
针对该
AbstractJaasAuthenticationProvider.setAuthorityGranters(List)
属性
定义的
每个主体
。
AuthorityGranter
鉴于每个JAAS主体都具有特定于实现的含义,因此
Spring Security不包含任何production
。
但是,
TestAuthorityGranter
在单元测试中
有一个
演示了一个简单的
AuthorityGranter
实现。
9.17.3. DefaultJaasAuthenticationProvider
所述
DefaultJaasAuthenticationProvider
允许JAAS
Configuration
对象被注入到其作为依赖。
然后
LoginContext
使用注入的JAAS
创建一个
Configuration
。
这意味着,
DefaultJaasAuthenticationProvider
没有绑定任何特定的实现
Configuration
为
JaasAuthenticationProvider
是。
InMemory配置
为了可以很容易地注入
Configuration
到
DefaultJaasAuthenticationProvider
,在内存中执行名为默认
InMemoryConfiguration
设置。
实施构造函数接受一个
Map
,其中每个键代表一个登录配置名和值表示
Array
的
AppConfigurationEntry
第
InMemoryConfiguration
还支持默认
Array
的
AppConfigurationEntry
对象,如果在提供的中找不到映射,将使用这些对象
的默认值
Map
。
有关详细信息,请 Reference的类级别javadoc
InMemoryConfiguration
。
DefaultJaasAuthenticationProvider示例配置
尽管Spring的配置
InMemoryConfiguration
比标准JAAS配置文件更冗长,但结合使用它
DefaultJaasAuthenticationProvider
比
JaasAuthenticationProvider
不依赖默认
Configuration
实现
更为灵活
。
下面提供
了
DefaultJaasAuthenticationProvider
using
的示例配置
InMemoryConfiguration
。
请注意,的自定义实现也
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>
9.17.4. 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>
9.17.5. 作为主题跑步
如果已配置,
JaasApiIntegrationFilter
则会尝试在
Subject
上
以方式运行
JaasAuthenticationToken
。
这意味着
Subject
可以使用以下命令访问:
Subject subject = Subject.getSubject(AccessController.getContext());
可以使用 jaas-api-provision 属性 轻松配置此集成 。 与依赖于填充JAAS主题的旧版或外部API集成时,此功能很有用。
9.18. CAS认证
9.18.1. 总览
JA-SIG产生了企业范围内的单点登录系统,称为CAS。 与其他计划不同,JA-SIG的中央身份验证服务是开源的,被广泛使用,易于理解,独立于平台并支持代理功能。 Spring Security完全支持CAS,并提供了从Spring Security的单应用程序部署到由企业范围的CAS服务器保护的多应用程序部署的简便迁移路径。
您可以在 https://www.apereo.org上 了解有关CAS的更多信息 。 您还需要访问此站点以下载CAS Server文件。
9.18.2. CAS如何工作
尽管CAS网站包含详细介绍CAS体系结构的文档,但我们还是在Spring Security的上下文中再次介绍了总体概述。 Spring Security 3.x支持CAS3。在撰写本文时,CAS服务器的版本为3.4.
您需要在企业中的某个位置设置CAS服务器。 CAS服务器只是一个标准的WAR文件,因此设置服务器没有任何困难。 在WAR文件中,您将自定义显示给用户的登录页面和其他单一登录页面。
部署CAS 3.4服务器时,您还需要
AuthenticationHandler
在
deployerConfigContext.xml
CAS随附的文件中
指定一个
。
AuthenticationHandler
具有一个简单的方法,
该
方法返回关于给定凭据集是否有效的布尔值。
您的
AuthenticationHandler
实现将需要链接到某种类型的后端身份验证存储库,例如LDAP服务器或数据库。
CAS本身提供了许多
AuthenticationHandler
现成的功能来协助完成此任务。
在下载和部署服务器war文件时,该文件将设置为成功验证输入与用户名匹配的密码的用户的身份,这对于测试非常有用。
除了CAS服务器本身之外,其他关键参与者当然是整个企业中部署的安全Web应用程序。 这些Web应用程序称为“服务”。 有三种类型的服务。 那些对服务票证进行身份验证的人,那些可以获取代理票证的人以及那些对代理票证进行身份验证的人。 验证代理票证的方式有所不同,因为必须验证代理列表,并且通常可以重复使用代理票证。
Spring Security和CAS交互序列
Web浏览器,CAS服务器和受Spring Security保护的服务之间的基本交互如下:
-
Web用户正在浏览服务的公共页面。 不涉及CAS或Spring Security。
-
用户最终请求的页面是安全的,或者它使用的其中一个bean是安全的。 Spring Security
ExceptionTranslationFilter会检测到AccessDeniedException或AuthenticationException。 -
由于用户的
Authentication对象(或缺少对象)引起了AuthenticationException,该ExceptionTranslationFilter会调用configureAuthenticationEntryPoint。 如果使用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将使用 上面讨论 的
PasswordHandler(或AuthenticationHandler如果使用CAS 3.0)来决定用户名和密码是否有效。 -
成功登录后,CAS会将用户的浏览器重定向回原始服务。 它还将包含一个
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,而依次配置CasAuthenticationProvider。 将CasAuthenticationProvider只响应UsernamePasswordAuthenticationToken包含CAS特异性主要(如SCasAuthenticationFilter.CAS_STATEFUL_IDENTIFIER)和CasAuthenticationTokenS(稍后讨论)。 -
CasAuthenticationProvider将使用TicketValidator实现 来验证服务票证 。 这通常是Cas20ServiceTicketValidatorCAS客户端库中包含的类之一。 如果应用程序需要验证代理票证,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将pgtIou在XML响应中 包括一个 字符串。 这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。 它将返回到CasAuthenticationProvideraTicketResponse,其中包括用户名(必填),代理列表(如果涉及)和代理授予票证IOU(如果请求了代理回调)。 -
接下来
CasAuthenticationProvider将调用已配置的CasProxyDecider。 该CasProxyDecider指示在代理列表是否TicketResponse是可以接受的服务。 多次实现Spring Security提供:RejectProxyTickets,AcceptAnyCasProxy和NamedCasProxyDecider。 这些名称在很大程度上是不言自明的,除非NamedCasProxyDecider允许提供List受信任的代理。 -
CasAuthenticationProvider接下来将请求AuthenticationUserDetailsService加载GrantedAuthority适用于的用户 的 对象Assertion。 -
如果没有问题,则
CasAuthenticationProvider构造一个CasAuthenticationToken包括TicketResponse和中 包含的详细信息GrantedAuthority的。 -
然后
CasAuthenticationFilter, 控制权返回 ,将创建的内容CasAuthenticationToken放置在安全上下文中。 -
用户的浏览器将重定向到引起该错误的原始页面
AuthenticationException(或 自定义目标, 具体取决于配置)。
您还在这里真是太好了! 现在让我们看一下它的配置方式
9.18.3. CAS客户端的配置
由于Spring Security,使CAS的Web应用程序端变得容易。 假定您已经知道使用Spring Security的基础知识,因此下面不再赘述。 我们假设正在使用基于名称空间的配置,并根据需要添加CAS Bean。 每个部分都基于上一部分。 完整的 CAS示例应用程序 可以在Spring Security Samples中找到。
服务票证认证
本节描述如何设置Spring Security来认证Service Ticket。
通常,这是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
必须是一个被监控的URL
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
豆(如上所述),它提供的URL企业CAS登录服务器。
这是将重定向用户浏览器的位置。
的
CasAuthenticationFilter
属性与
UsernamePasswordAuthenticationFilter
(用于基于表单的登录名)
非常相似
。
您可以使用这些属性来自定义行为,例如验证成功和失败的行为。
接下来,您需要添加
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
并不会实际使用密码进行身份验证,但会使用授权机构。
如果您 Reference “ CAS的工作原理” 部分, 那么所有这些bean都是不言自明的 。
这样就完成了CAS的最基本配置。 如果您没有犯任何错误,则您的Web应用程序应该很高兴在CAS单点登录框架内工作。 Spring Security的其他部分无需关心CAS处理的身份验证这一事实。 在以下各节中,我们将讨论一些(可选)更高级的配置。
单次登出
CAS协议支持单一注销,可以轻松添加到您的Spring Security配置中。 以下是处理单点注销的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 Server注销URL。
然后,CAS服务器将向已登录的所有服务发送“单一注销”请求。
该
singleLogoutFilter
处理通过查找单注销请求
HttpSession
静态
Map
,然后无效的。
为什么同时
需要
logout
元素和元素
可能会造成混淆
singleLogoutFilter
。
最好先从本地注销,这是因为最好将其
SingleSignOutFilter
存储
HttpSession
在静态
Map
中以便对其调用无效。
使用上述配置,注销流程为:
-
用户请求
/logout将用户从本地应用程序注销,然后将用户发送到注销成功页面。 -
注销成功页面
/cas-logout.jsp应当指示用户单击指向的链接/logout/cas以注销所有应用程序。 -
当用户单击链接时,该用户将被重定向到CAS单一注销URL( https:// localhost:9443 / cas / logout )。
-
然后,在CAS服务器端,CAS单一注销URL向所有CAS服务提交单一注销请求。 在CAS Service方面,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时,您可能会遇到一些编码问题。
因此,建议添加,
CharacterEncodingFilter
以确保使用时字符编码正确
SingleSignOutFilter
。
同样,请参阅JASIG的文档以了解详细信息。
的
SingleSignOutHttpSessionListener
确保,当
HttpSession
期满时,用于单注销的映射被去除。
通过CAS向无状态服务进行身份验证
本节介绍如何使用CAS对服务进行身份验证。 换句话说,本节讨论如何设置使用通过CAS认证的服务的客户端。 下一节将介绍如何设置无状态服务以使用CAS进行身份验证。
配置CAS以获取代理授予票证
为了向无状态服务进行身份验证,应用程序需要获取代理授予票证(PGT)。 本节描述了如何配置Spring Security以获得基于cassst [Service Ticket Authentication]配置的PGT。
第一步是
ProxyGrantingTicketStorage
在Spring Security配置中包含。
这用于存储由所获得的PGT,
CasAuthenticationFilter
以便它们可用于获取代理凭单。
配置示例如下所示
<!--
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
。
这一点很重要的
proxyReceptorUrl
比赛
proxyCallbackUrl
的
Cas20ProxyTicketValidator
。
配置示例如下所示。
<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示例应用程序
包含了一个工作示例
ProxyTicketSampleServlet
。
示例代码可以在下面找到:
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
有状态客户端和无状态客户端之间
的
区别。
一个有状态的客户机被认为是任何提交给
filterProcessUrl
的
CasAuthenticationFilter
。
无状态客户端是指向
CasAuthenticationFilter
网址以外的URL
提出身份验证请求的客户端
filterProcessUrl
。
由于远程协议无法在的上下文中展示自己
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>
下一步是指定
serviceProperties
和
authenticationDetailsSource
的
CasAuthenticationFilter
。
该
serviceProperties
属性指示
CasAuthenticationFilter
,尝试验证所有工件,而不是仅对上的工件进行身份验证
filterProcessUrl
。
在
ServiceAuthenticationDetailsSource
一个创建
ServiceAuthenticationDetails
,以确保当前的URL,基于
HttpServletRequest
被验证检票时使用的服务URL。
可以通过注入
AuthenticationDetailsSource
返回定制
的定制来定制生成服务URL的方法
ServiceAuthenticationDetails
。
<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>
9.19。 X.509验证
9.19.1. 总览
X.509证书身份验证的最常见用法是在使用SSL时(尤其是在浏览器中使用HTTPS时)验证服务器的身份。 浏览器将自动检查服务器维护的受信任证书颁发机构列表之一是否已颁发(即进行数字签名)服务器提供的证书。
您还可以将SSL与“相互身份验证”结合使用; 然后,服务器将向客户端请求有效证书,作为SSL握手的一部分。 服务器将通过检查其证书是否由可接受的授权机构签名来对客户端进行身份验证。 如果提供了有效证书,则可以通过应用程序中的Servlet API获得该证书。 Spring Security X.509模块使用过滤器提取证书。 它将证书映射到应用程序用户,并加载该用户的授权权限集以与标准Spring Security基础结构一起使用。
在尝试将其与Spring Security结合使用之前,您应该熟悉使用证书并为Servlet容器设置客户端身份验证。 大多数工作是在创建和安装合适的证书和密钥。 例如,如果您使用的是Tomcat,请阅读 https://tomcat.apache.org/tomcat-6.0-doc/ssl-howto.html上 的说明 。 在使用Spring Security进行尝试之前,一定要先做好这项工作,这一点很重要
9.19.2. 向您的Web应用程序添加X.509身份验证
启用X.509客户端身份验证非常简单。
只需将
<x509/>
元素
添加
到您的http安全名称空间配置中即可。
<http>
...
<x509 subject-principal-regex="CN=(.*?)," user-service-ref="userService"/>;
</http>
元素具有两个可选属性:
-
subject-principal-regex。 用于从证书的使用者名称中提取用户名的正则表达式。 默认值如上所示。 这是用户名,该用户名将传递给,UserDetailsService以加载用户的权限。 -
user-service-ref。 这是UserDetailsService与X.509一起使用 的Bean的ID 。 如果您的应用程序上下文中仅定义了一个,则不需要。
本
subject-principal-regex
应该包含一个组。
例如,默认表达式“ CN =(。*?)”与通用名称字段匹配。
因此,如果证书中的使用者名称为``CN = Jimi Hendrix,OU = ...'',则用户名称为``Jimi Hendrix''。
匹配不区分大小写。
因此,“ emailAddress =(。*?),”将匹配“ EMAILADDRESS =
jimi@hendrix.org
,CN =…”,并赋予用户名“
jimi@hendrix.org
”。
如果客户端出示证书并且成功提取了有效的用户名,则
Authentication
安全上下文中
应该有一个有效的
对象。
如果找不到证书,或者找不到相应的用户,则安全上下文将保持为空。
这意味着您可以轻松使用X。
9.19.3. 在Tomcat中设置SSL
samples/certificate
Spring Security项目
的
目录中
有一些预先生成的证书
。
如果您不想生成自己的SSL,则可以使用它们来启用SSL以进行测试。
该文件
server.jks
包含服务器证书,私钥和颁发证书颁发机构的证书。
示例应用程序中还为用户提供了一些客户端证书文件。
您可以将它们安装在浏览器中以启用SSL客户端身份验证。
要运行具有SSL支持
server.jks
的tomcat
conf
,请将
文件
拖放
到tomcat
目录中,并将以下连接器添加到
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"
/>
clientAuth
也可以设置为
want
即使客户端不提供证书,仍然希望SSL连接成功。
除非您使用非X.509身份验证机制(例如表单身份验证),否则不提供证书的客户端将无法访问Spring Security保护的任何对象。
9.20。 运行身份验证替换
9.20.1. 总览
在
AbstractSecurityInterceptor
能够暂时替换
Authentication
对象的
SecurityContext
,并
SecurityContextHolder
在安全对象回调阶段。
仅当
and
Authentication
对象成功处理
了原始
对象
时,才会发生这种情况
。
该
会指示替换
对象,如果有的话,应该在使用
。
AuthenticationManager
AccessDecisionManager
RunAsManager
Authentication
SecurityInterceptorCallback
通过
Authentication
在安全对象回调阶段
临时替换
对象,安全调用将能够调用需要不同身份验证和授权凭证的其他对象。
它还将能够对特定
GrantedAuthority
对象
执行任何内部安全检查
。
由于Spring Security提供了许多帮助程序类,它们基于的内容自动配置远程协议
SecurityContextHolder
,因此这些运行方式替换在调用远程Web服务时特别有用。
9.20.2. 组态
RunAsManager
Spring Security提供了
一个
接口:
Authentication buildRunAs(Authentication authentication, Object object,
List<ConfigAttribute> config);
boolean supports(ConfigAttribute attribute);
boolean supports(Class clazz);
第一个方法返回
在方法调用期间
Authentication
应替换现有
Authentication
对象的对象。
如果该方法返回
null
,则表明不应进行任何替换。
使用第二种方法
AbstractSecurityInterceptor
作为配置属性启动验证的一部分。
supports(Class)
安全拦截器实现调用
该
方法,以确保配置的
RunAsManager
支持安全拦截器将呈现的安全对象的类型。
RunAsManager
Spring Security提供了
a的一个具体实现
。
如果
RunAsManagerImpl
类以
开头,
RunAsUserToken
则
该类返回一个替换
。
如果
找到
任何这样
的替换,替换
将包含与原始
对象
相同的主体,凭据和授予的权限
,并
为每个
对象添加新的对象
。
每个新名称
都将以
开头,后跟
。
例如,a
将导致
包含
授权
的替换
。
ConfigAttribute
RUN_AS_
ConfigAttribute
RunAsUserToken
Authentication
SimpleGrantedAuthority
RUN_AS_ ConfigAttribute
SimpleGrantedAuthority
ROLE_
RUN_AS ConfigAttribute
RUN_AS_SERVER
RunAsUserToken
ROLE_RUN_AS_SERVER
替换
RunAsUserToken
就像任何其他
Authentication
对象一样。
它需要由进行身份验证
AuthenticationManager
,可能需要通过委派给合适的
AuthenticationProvider
。
在
RunAsImplAuthenticationProvider
执行这样的验证。
它只是接受任何
RunAsUserToken
呈现
为有效的内容
。
为确保恶意代码不会创建
RunAsUserToken
并提供给,以确保被接受
RunAsImplAuthenticationProvider
,密钥的哈希存储在所有生成的令牌中。
该
RunAsManagerImpl
和
RunAsImplAuthenticationProvider
在使用相同的密钥豆上下文中创建的:
<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
出于安全原因,创建后
该
属性是不变的
9.21. 表格登入
9.21.1. 表单登录Java配置
您可能想知道提示您登录时登录表单的来源,因为我们没有提及任何HTML文件或JSP。 由于Spring Security的默认配置没有显式设置登录页面的URL,因此Spring Security将基于已启用的功能并使用处理提交的登录的URL的标准值(用户将使用的默认目标URL)自动生成一个URL。登录后发送至。
虽然自动生成的登录页面便于快速启动和运行,但大多数应用程序都希望提供自己的登录页面。
当我们想要更改默认配置时,可以
WebSecurityConfigurerAdapter
通过扩展它来自
定义
我们前面提到的
配置
:
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
// ...
}
然后覆盖
configure
如下所示
的
方法:
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests(authorizeRequests ->
authorizeRequests
.anyRequest().authenticated()
)
.formLogin(formLogin ->
formLogin
.loginPage("/login") (1)
.permitAll() (2)
);
}
| 1个 | 更新的配置指定登录页面的位置。 |
| 2 |
我们必须授予所有用户(即未经身份验证的用户)访问我们的登录页面的权限。
该
formLogin().permitAll()
方法允许向所有用户授予与基于表单的登录相关联的所有URL的访问权限。
|
下面是使用JSP为当前配置实现的示例登录页面:
| 下面的登录页面代表我们当前的配置。 如果某些默认设置不能满足我们的需求,我们可以轻松地更新我们的配置。 |
<c:url value="/login" var="loginUrl"/>
<form action="${loginUrl}" method="post"> (1)
<c:if test="${param.error != null}"> (2)
<p>
Invalid username and password.
</p>
</c:if>
<c:if test="${param.logout != null}"> (3)
<p>
You have been logged out.
</p>
</c:if>
<p>
<label for="username">Username</label>
<input type="text" id="username" name="username"/> (4)
</p>
<p>
<label for="password">Password</label>
<input type="password" id="password" name="password"/> (5)
</p>
<input type="hidden" (6)
name="${_csrf.parameterName}"
value="${_csrf.token}"/>
<button type="submit" class="btn">Log in</button>
</form>
| 1个 |
/login
URL
的POST
将尝试验证用户
|
| 2 |
如果查询参数
error
存在,则尝试认证失败
|
| 3 |
如果查询参数
logout
存在,则说明用户已成功注销
|
| 4 | 用户名必须作为名为username 的HTTP参数存在 |
| 5 | 密码必须作为名为password 的HTTP参数存在 |
| 6 | 我们必须 包括CSRF令牌 要了解更多信息,请阅读 Reference中的“ 跨站点请求伪造(CSRF)” 部分 |
9.21.2. 表单登录XML配置
表单和基本登录选项
您可能想知道提示您登录时登录表单的来源,因为我们没有提及任何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
元素来表示对登录页面的任何请求都应对匿名用户可用
[ 7 ],
并且还对
AuthenticatedVoter
类
可用,以
获取有关如何
IS_AUTHENTICATED_ANONYMOUSLY
处理
该值的更多详细信息
。
否则,请求将被模式/ **匹配,并且将无法访问登录页面本身!
这是一个常见的配置错误,将导致应用程序中的无限循环。
如果您的登录页面受到保护,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
元素为不同的请求模式定义单独的安全过滤器链配置。
如果
元素中
pattern
省略了
该
属性
http
,则它匹配所有请求。
创建不安全的模式是此语法的一个简单示例,其中该模式被映射到一个空的过滤器链
[ 8 ]
。
我们将在“
安全过滤器链
”一章中更详细地介绍这种新语法
。
重要的是要意识到,这些不安全的请求将完全忽略任何与Spring Security Web相关的配置或诸如的其他属性
requires-channel
,因此您将无法在请求期间访问当前用户的信息或调用安全方法。
access='IS_AUTHENTICATED_ANONYMOUSLY'
如果您仍然希望应用安全过滤器链,
请使用它
作为替代。
如果要使用基本身份验证而不是表单登录,则将配置更改为
<http use-expressions="false">
<intercept-url pattern="/**" access="ROLE_USER" />
<http-basic />
</http>
然后,基本身份验证将具有优先权,并将在用户尝试访问受保护资源时用于提示登录。 如果您希望使用表单登录,则在此配置中仍然可用,例如通过嵌入在另一个网页中的登录表单。
9.22. 基本和摘要身份验证
基本身份验证和摘要身份验证是在Web应用程序中流行的替代身份验证机制。 基本身份验证通常与无状态客户端一起使用,无状态客户端会在每个请求中传递其凭据。 将其与基于表单的身份验证结合使用是很常见的,在该表单中,通过基于浏览器的用户界面和Web服务来使用应用程序。 但是,基本身份验证以纯文本形式传输密码,因此,仅应在诸如HTTPS之类的加密传输层上实际使用该密码。
9.22.1. BasicAuthenticationFilter
BasicAuthenticationFilter
负责处理HTTP标头中显示的基本身份验证凭据。
这可用于验证Spring远程协议(例如Hessian和Burlap)以及普通浏览器用户代理(例如Firefox和Internet Explorer)发出的调用。
HTTP基本身份验证的标准由RFC 1945第11节定义,并
BasicAuthenticationFilter
符合该RFC。
基本身份验证是一种有吸引力的身份验证方法,因为它已广泛部署在用户代理中,并且实现极为简单(它只是在HTTP标头中指定的username:password的Base64编码)。
9.22.2. 组态
要实现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 Basic身份验证。
如果身份验证成功,则将生成的
Authentication
对象
SecurityContextHolder
照常
放入
。
如果身份验证事件成功,或者由于HTTP标头不包含受支持的身份验证请求而未尝试进行身份验证,则过滤器链将照常继续。
过滤器链唯一会中断的时间是身份验证失败并
AuthenticationEntryPoint
调用。
9.23. DigestAuthenticationFilter
DigestAuthenticationFilter
能够处理HTTP标头中提供的摘要身份验证凭据。
摘要式身份验证试图解决基本身份验证的许多弱点,特别是通过确保凭据不会以明文形式通过Web发送来解决。
许多用户代理都支持摘要式身份验证,包括Mozilla Firefox和Internet Explorer。
HTTP摘要认证的标准由RFC 2617定义,该标准更新了RFC 2069所规定的摘要认证的早期版本。大多数用户代理均实现RFC2617。SpringSecurity
DigestAuthenticationFilter
与“认证”保护质量兼容(
qop
)由RFC 2617规定,它也提供与RFC 2069的向后兼容性。如果您需要使用未加密的HTTP(即,没有TLS / HTTPS)并且希望最大程度地提高身份验证过程的安全性,则摘要身份验证是一种更具吸引力的选择。
实际上,如RFC 2518第17.1节所述,摘要式身份验证是WebDAV协议的强制性要求。
|
您不应该在现代应用程序中使用Digest,因为它不安全。 最明显的问题是您必须以纯文本,加密或MD5格式存储密码。 所有这些存储格式都被认为是不安全的。 相反,您应该使用一种单向自适应密码哈希(即bCrypt,PBKDF2,SCrypt等)。 |
摘要式身份验证的核心是“一次性”。 这是服务器生成的值。 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
该
DigestAuthenticationEntryPoint
有一个属性指定
key
用于生成随机数的令牌,具有沿着
nonceValiditySeconds
属性用于确定的到期时间(默认300,它等于5分钟)。
在随机数有效的情况下,摘要是通过串联各种字符串来计算的,这些字符串包括用户名,密码,随机数,所请求的URI,客户端生成的随机数(仅由用户代理生成每个请求的随机值),领域名称等,然后执行MD5哈希。
服务器和用户代理都执行此摘要计算,如果它们不同意包含的值(例如密码),则会产生不同的哈希码。
在Spring Security的实现中,如果服务器生成的随机数刚刚过期(但摘要有效),
DigestAuthenticationEntryPoint
它将发送一个
"stale=true"
标头。
这告诉用户代理无需打扰用户(因为密码和用户名等是正确的),而只需使用新的随机数重试即可。
nonceValiditySeconds
参数的
适当值
DigestAuthenticationEntryPoint
取决于您的应用程序。
极其安全的应用程序应注意,可以使用拦截的身份验证头来模拟主体,直到
expirationTime
达到现时
所
包含的内容
为止
。
这是选择适当设置时的关键原则,但是对于非常安全的应用程序,在最初的情况下不会在TLS / HTTPS上运行是不寻常的。
由于摘要式身份验证的实施更为复杂,因此经常会出现用户代理问题。 例如,Internet Explorer无法在同一会话中的后续请求上提供“不透明”令牌。 因此,Spring Security过滤器将所有状态信息封装到“ nonce”令牌中。 在我们的测试中,Spring Security的实现可与Mozilla Firefox和Internet Explorer可靠地配合,正确处理随机数超时等。
9.23.1. 组态
现在,我们已经回顾了该理论,让我们看看如何使用它。
要实现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
[ 9 ]
中使用编码的密码,则摘要身份验证将不起作用
。
DAO协作者
UserCache
通常与一起直接与共享
DaoAuthenticationProvider
。
该
authenticationEntryPoint
属性必须是
DigestAuthenticationEntryPoint
,这样
DigestAuthenticationFilter
能获得正确
realmName
和
key
用于消化的计算。
就像
BasicAuthenticationFilter
,如果身份验证成功,则将
Authentication
请求令牌放入
SecurityContextHolder
。
如果身份验证事件成功,或者由于HTTP标头不包含摘要身份验证请求而未尝试身份验证,则过滤器链将照常继续。
AuthenticationEntryPoint
如上
一段所述,只有当身份验证失败并
调用
时,过滤器链才会被中断
。
摘要式身份验证的RFC提供了一系列附加功能,以进一步提高安全性。 例如,可以在每个请求上更改随机数。 尽管如此,Spring Security实现仍被设计为使实现的复杂性最小化(以及无疑会出现的用户代理不兼容性),并避免需要存储服务器端状态。 如果您希望更详细地探索这些功能,则可以邀请您阅读RFC 2617. 据我们所知,Spring Security的实现确实符合该RFC的最低标准。
9.24. 处理注销
9.24.1. 注销Java配置
使用时
WebSecurityConfigurerAdapter
,将自动应用注销功能。
默认设置是访问URL
/logout
将通过以下方式注销用户:
-
使HTTP会话无效
-
清理配置的所有RememberMe身份验证
-
清除
SecurityContextHolder -
重定向到
/login?logout
但是,与配置登录功能相似,您还可以使用各种选项来进一步自定义注销要求:
protected void configure(HttpSecurity http) throws Exception {
http
.logout(logout -> (1)
logout
.logoutUrl("/my/logout") (2)
.logoutSuccessUrl("/my/index") (3)
.logoutSuccessHandler(logoutSuccessHandler) (4)
.invalidateHttpSession(true) (5)
.addLogoutHandler(logoutHandler) (6)
.deleteCookies(cookieNamesToClear) (7)
)
...
}
| 1个 |
提供注销支持。
使用时会自动应用
WebSecurityConfigurerAdapter
。
|
| 2 |
触发注销的URL发生(默认为
/logout
)。
如果启用了CSRF保护(默认),则请求也必须是POST。
有关更多信息,请查阅
JavaDoc
。
|
| 3 |
注销发生后重定向到的URL。
默认值为
/login?logout
。
有关更多信息,请查阅
JavaDoc
。
|
| 4 |
让我们指定一个custom
LogoutSuccessHandler
。
如果指定,将
logoutSuccessUrl()
被忽略。
有关更多信息,请查阅
JavaDoc
。
|
| 5 |
指定
HttpSession
在注销时
是否使无效
。
默认情况下
是
这样
。
SecurityContextLogoutHandler
在幕后进行
配置
。
有关更多信息,请查阅
JavaDoc
。
|
| 6 |
添加一个
LogoutHandler
。
默认情况下
SecurityContextLogoutHandler
被添加为最后一个
LogoutHandler
。
|
| 7 |
允许指定成功注销后将删除的cookie名称。
这是
CookieClearingLogoutHandler
显式
添加快捷方式
。
|
| ===当然,也可以使用XML命名空间符号配置注销。 请参阅 Spring Security XML命名空间部分中 注销元素 的文档以 获取更多详细信息。 === |
通常,为了自定义注销功能,可以添加
LogoutHandler
和/或
LogoutSuccessHandler
实现。
对于许多常见方案,使用流畅的API时会在后台应用这些处理程序。
9.24.2. 注销XML配置
该
logout
元素通过导航到特定URL来增加对注销的支持。
默认注销URL为
/logout
,但您可以使用
logout-url
属性
将其设置为其他名称
。
有关其他可用属性的更多信息,请参见名称空间附录。
9.24.3. 注销处理程序
通常,
LogoutHandler
实现指示能够参与注销处理的类。
预计将调用它们以执行必要的清理。
因此,它们不应引发异常。
提供了各种实现:
有关详细信息, 请参见“ 记住我的界面和实现 ”。
除了
LogoutHandler
直接
提供
实现之外,fluent API还提供了快捷方式,这些快捷方式在幕后提供了相应的
LogoutHandler
实现。
例如,
deleteCookies()
允许指定注销成功后要删除的一个或多个Cookie的名称。
与添加相比,这是捷径
CookieClearingLogoutHandler
。
9.24.4. 注销成功处理程序
LogoutSuccessHandler
在成功注销后
,
将调用
LogoutFilter
,以处理例如重定向或转发到适当的目的地。
请注意,该接口与几乎相同,
LogoutHandler
但可能会引发异常。
提供以下实现:
-
HttpStatusReturningLogoutSuccessHandler
如上所述,您无需
SimpleUrlLogoutSuccessHandler
直接
指定
。
相反,fluent API通过设置来提供快捷方式
logoutSuccessUrl()
。
这将设置
SimpleUrlLogoutSuccessHandler
底盖。
提供的URL将在注销后重定向到。
默认值为
/login?logout
。
本
HttpStatusReturningLogoutSuccessHandler
可以在REST API类型场景有趣。
无需在成功注销后重定向到URL,而是
LogoutSuccessHandler
允许您提供要返回的纯HTTP状态代码。
如果未配置,默认情况下将返回状态码200。
9.25. 设置自定义AuthenticationEntryPoint
如果您不通过名称空间使用表单登录,OpenID或基本身份验证,则可能希望使用传统的bean语法定义身份验证过滤器和入口点,并将它们链接到名称空间中,如我们所见。
AuthenticationEntryPoint
可以使用
元素
entry-point-ref
上的属性
来设置
对应
的
属性
<http>
。
CAS示例应用程序是将自定义bean与命名空间一起使用的一个很好的示例,包括此语法。 如果您不熟悉身份验证入口点,则会在“ 技术概述” 一章 中对它们进行讨论 。
10.授权
Spring Security中的高级授权功能代表了其受欢迎程度的最令人信服的原因之一。 无论选择哪种身份验证方式(使用Spring Security提供的机制和提供程序,还是与容器或其他非Spring Security身份验证机构集成),您都会发现可以在应用程序中以一致且简单的方式使用授权服务。方式。
在这一部分中,我们将探索
AbstractSecurityInterceptor
在第I部分中介绍
的不同
实现。然后,我们继续探索如何通过使用域访问控制列表来微调授权。
10.1. 授权架构
10.1.1. 当局
正如我们在
技术概述中
看到的
,所有
Authentication
实现都存储
GrantedAuthority
对象
列表
。
这些代表已授予委托人的权限。
的
GrantedAuthority
对象被插入到
Authentication
由所述对象
AuthenticationManager
和由稍后读
AccessDecisionManager
小号作出授权决策时。
GrantedAuthority
是只有一种方法的接口:
String getAuthority();
此方法允许
AccessDecisionManager
s获得的精确
String
表示
GrantedAuthority
。
通过将表示形式返回为a
String
,
GrantedAuthority
大多数
AccessDecisionManager
s
都可以轻松地“读取” a
。
如果a
GrantedAuthority
不能精确地表示为a
String
,则将
GrantedAuthority
其视为“复杂”的,并且
getAuthority()
必须返回
null
。
“复杂”的一个示例是一种
GrantedAuthority
存储适用于不同客户帐号的操作和权限阈值列表的实现。
将这种复杂表示
GrantedAuthority
为a
String
会非常困难,因此该
getAuthority()
方法应返回
null
。
这将向任何人
AccessDecisionManager
表明
GrantedAuthority
,为了理解其内容
,将需要专门支持该
实现。
Spring Security包含一个具体的
GrantedAuthority
实现
SimpleGrantedAuthority
。
这允许将任何用户指定的内容
String
转换为
GrantedAuthority
。
AuthenticationProvider
安全体系结构随附的
所有都
SimpleGrantedAuthority
用于填充
Authentication
对象。
10.1.2. 调用前处理
正如我们在“
技术概述”
一章中
已经看到的那样
,Spring Security提供了拦截器,该拦截器控制对安全对象(例如方法调用或Web请求)的访问。
由做出关于是否允许进行调用的预调用决定
AccessDecisionManager
。
AccessDecisionManager
由
AccessDecisionManager
调用,
AbstractSecurityInterceptor
并负责做出最终的访问控制决策。
该
AccessDecisionManager
接口包含三种方法:
void decide(Authentication authentication, Object secureObject,
Collection<ConfigAttribute> attrs) throws AccessDeniedException;
boolean supports(ConfigAttribute attribute);
boolean supports(Class clazz);
该
AccessDecisionManager
的
decide
方法通过了所有它为了使授权决策所需要的相关信息。
特别是,通过传递安全
Object
保护,可以检查实际安全对象调用中包含的那些参数。
例如,假设安全对象是
MethodInvocation
。
查询
MethodInvocation
任何
Customer
参数然后在中实施某种安全性逻辑
AccessDecisionManager
以确保允许委托人对该客户进行操作
将很容易
。
AccessDeniedException
如果访问被拒绝,
则预期实现会抛出
。
在启动时,
supports(ConfigAttribute)
由方法调用
此
方法
AbstractSecurityInterceptor
,以确定是否
AccessDecisionManager
可以处理传递的
ConfigAttribute
。
supports(Class)
安全拦截器实现调用
该
方法,以确保配置的
AccessDecisionManager
支持安全拦截器将呈现的安全对象的类型。
基于投票的AccessDecisionManager实现
尽管用户可以实现自己的功能
AccessDecisionManager
来控制授权的各个方面,但Spring Security包括几种
AccessDecisionManager
基于投票的实现。
投票决策管理器
说明了相关的类。
使用这种方法,将
AccessDecisionVoter
根据授权决策轮询
一系列
实现。
该
AccessDecisionManager
然后决定是否要抛出一个
AccessDeniedException
基于它的投票评估。
该
AccessDecisionVoter
接口具有三种方法:
int vote(Authentication authentication, Object object, Collection<ConfigAttribute> attrs);
boolean supports(ConfigAttribute attribute);
boolean supports(Class clazz);
具体的实现返回an
int
,可能的值反映在
AccessDecisionVoter
静态字段中
ACCESS_ABSTAIN
,
ACCESS_DENIED
并且
ACCESS_GRANTED
。
ACCESS_ABSTAIN
如果对授权决定没有意见,
则将返回有表决权的实现
。
如果确实有意见,则必须返回
ACCESS_DENIED
或
ACCESS_GRANTED
。
AccessDecisionManager
Spring Security提供了
三个具体
的选票。
该
ConsensusBased
实现将基于未弃权票的共识来授予或拒绝访问权限。
提供属性以控制在票数相等或所有票都弃权的情况下的行为。
该
AffirmativeBased
如果一个或多个实施将授予访问
ACCESS_GRANTED
共收到选票(即一个拒绝票将被忽略,只要有至少有一个赞成票)。
像
ConsensusBased
实现
一样
,如果所有投票者都弃权,则有一个控制行为的参数。
该
UnanimousBased
供应商预期一致
ACCESS_GRANTED
表决,以授予访问权限,忽略弃权票。
如果有,它将拒绝访问
ACCESS_DENIED
投票。
像其他实现一样,如果所有投票者都弃权,则有一个控制行为的参数。
可以实现一种
AccessDecisionManager
以不同方式计算票数
的习惯
。
例如,来自某个特定人士的投票
AccessDecisionVoter
可能会获得更多的权重,而来自特定选民的拒绝投票可能会产生否决权。
角色投票器
AccessDecisionVoter
Spring Security提供
的最常用的方法
是simple
RoleVoter
,它将配置属性视为简单的角色名称,如果用户被分配了该角色,则投票将授予访问权限。
如果有任何
ConfigAttribute
前缀开头,
它将投票
ROLE_
。
如果存在一个
GrantedAuthority
返回的
String
表示形式(通过
getAuthority()
方法)完全等于一个或多个
ConfigAttributes
以prefix开头
的
表示形式
,它将投票授予访问权限
ROLE_
。
如果没有以
ConfigAttribute
开头的
完全匹配
ROLE_
,
RoleVoter
则将投票拒绝访问。
如果否
ConfigAttribute
以开头
ROLE_
,则选民弃权。
认证选票
我们暗中看到的另一个投票者是
AuthenticatedVoter
,可以用来区分匿名用户,完全认证用户和记住我认证用户。
许多站点允许使用“记住我”身份验证进行某些受限访问,但是要求用户通过登录以进行完全访问来确认其身份。
当我们使用该属性
IS_AUTHENTICATED_ANONYMOUSLY
授予匿名访问权限时,该属性正在由处理
AuthenticatedVoter
。
有关更多信息,请参见Javadoc。
定制选民
显然,您还可以实现一个自定义,
AccessDecisionVoter
并且可以在其中添加几乎任何所需的访问控制逻辑。
它可能特定于您的应用程序(与业务逻辑相关),也可能实现某些安全管理逻辑。
例如,您将
在Spring网站上
找到一篇
博客文章
,其中描述了如何使用投票器实时拒绝帐户被暂停的用户的实时访问。
10.1.3. 调用处理后
尽管
在继续进行安全对象调用之前
AccessDecisionManager
由
AbstractSecurityInterceptor
调用了,但是某些应用程序需要一种方法来修改由安全对象调用实际返回的对象。
尽管您可以轻松实现自己的AOP问题来实现这一目标,但Spring Security提供了一个方便的挂钩,该挂钩具有几种与其ACL功能集成的具体实现。
调用后实现
说明了Spring Security的
AfterInvocationManager
实现及其具体实现。
与Spring Security的许多其他部分一样,
AfterInvocationManager
具有一个具体的实现
AfterInvocationProviderManager
,它轮询
AfterInvocationProvider
s
的列表
。
每个
AfterInvocationProvider
都允许修改返回对象或抛出一个
AccessDeniedException
。
实际上,由于前一个提供程序的结果将传递到列表中的下一个,因此多个提供程序可以修改对象。
请注意,如果你使用
AfterInvocationManager
,你仍然需要配置属性,让
MethodSecurityInterceptor
的
AccessDecisionManager
允许的操作。
如果您使用的是典型的Spring Security包含的
AccessDecisionManager
实现,则没有为特定的安全方法调用定义配置属性,则将导致每个人
AccessDecisionVoter
都放弃投票。
反之,如果
AccessDecisionManager
属性“ allowIfAllAbstainDecisions”为
false
,
AccessDeniedException
则会抛出一个。
您可以通过(i)将“ allowIfAllAbstainDecisions”设置为
true
(尽管通常不建议这样做)或(ii)仅仅确保至少有一个配置属性
AccessDecisionVoter
将被投票授予访问权限来
避免此潜在问题
。
后一种(推荐)方法通常通过以下方式实现:
ROLE_USER
或
ROLE_AUTHENTICATED
配置属性。
10.1.4. 层次角色
通常要求应用程序中的特定角色应自动“包括”其他角色。 例如,在具有“管理员”和“用户”角色概念的应用程序中,您可能希望管理员能够执行普通用户可以执行的所有操作。 为此,您可以确保还为所有管理员用户分配了“用户”角色。 或者,您可以修改每个需要“用户”角色也要包括“管理员”角色的访问约束。 如果您的应用程序中有很多不同的角色,这可能会变得非常复杂。
使用角色层次结构,可以配置哪些角色(或权限)应包括其他角色。
Spring Security的扩展版本
的RoleVoter
,
RoleHierarchyVoter
被配置了
RoleHierarchy
,从它获得用户被分配所有的“可达当局”。
典型的配置可能如下所示:
<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
当
AccessDecisionManager
使用上述配置
评估安全性约束时
,通过身份验证的用户的
行为就好像他们具有所有四个角色一样
RoleHierarchyVoter
。
该
>
符号可以被认为是“包含”的意思。
角色层次结构为简化应用程序的访问控制配置数据和/或减少需要分配给用户的权限数量提供了一种方便的方法。 对于更复杂的要求,您可能希望在应用程序需要的特定访问权限与分配给用户的角色之间定义逻辑映射,并在加载用户信息时在两者之间进行转换。
10.2. 安全对象实施
10.2.1. AOP联盟(方法调用)安全拦截器
在Spring Security 2.0之前,安全性
MethodInvocation
需要大量的样板配置。
现在,推荐的方法安全性方法是使用
名称空间配置
。
这样,方法安全性基础结构bean将自动为您配置,因此您实际上不需要了解实现类。
我们将仅简要介绍此处涉及的类。
方法安全性使用
MethodSecurityInterceptor
来确保
MethodInvocation
的安全。
根据配置方法,拦截器可能特定于单个bean,也可能在多个bean之间共享。
拦截器使用
MethodSecurityMetadataSource
实例来获取适用于特定方法调用的配置属性。
MapBasedMethodSecurityMetadataSource
用于存储以方法名称作为关键字的配置属性(可以通配),当在属性中使用
<intercept-methods>
或
<protect-point>
元素
定义属性时,将在内部使用这些属性
。
其他实现将用于处理基于注释的配置。
显式方法SecurityInterceptor配置
当然,您可以
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>
10.2.2. AspectJ(JoinPoint)安全拦截器
AspectJ安全拦截器与上一节中讨论的AOP Alliance安全拦截器非常相似。 实际上,我们将仅讨论本节中的区别。
AspectJ拦截器的名称为
AspectJSecurityInterceptor
。
与依赖于Spring应用程序上下文通过代理编织在安全拦截器中的AOP Alliance安全拦截器不同,它
AspectJSecurityInterceptor
是通过AspectJ编译器进行编织的。
在同一应用程序中同时使用两种类型的安全拦截器,并
AspectJSecurityInterceptor
用于域对象实例安全,而AOP联盟
MethodSecurityInterceptor
用于服务层安全
,这并不少见
。
首先让我们考虑如何
AspectJSecurityInterceptor
在Spring应用程序上下文中配置:
<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
,
SecurityMetadataSource
与使用
java.lang.reflect.Method
s而不是AOP库特定的类
共享
。
当然,您的访问决策可以访问相关的AOP库特定的调用(即
MethodInvocation
或
JoinPoint
),因此在制定访问决策(例如方法参数)时可以考虑一系列附加条件。
接下来,您需要定义一个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,
并且将应用安全拦截器。
10.3. 基于表达式的访问控制
Spring Security 3.0引入了使用Spring EL表达式作为授权机制的能力,此外还可以简单地使用配置属性和访问决定投票器。 基于表达式的访问控制基于相同的体系结构,但允许将复杂的布尔逻辑封装在单个表达式中。
10.3.1. 总览
Spring Security使用Spring EL来支持表达式,如果您想更深入地了解该主题,则应该看看它的工作方式。 使用“根对象”评估表达式作为评估上下文的一部分。 Spring Security使用特定的类将Web和方法的安全性用作根对象,以提供内置的表达式并访问诸如当前主体的值。
常见的内置表达式
表达式根对象的基类是
SecurityExpressionRoot
。
这提供了Web和方法安全性中都可用的一些常用表达式。
| 表达 | 描述 |
|---|---|
|
|
返回
|
|
|
返回
|
|
|
|
|
|
返回
|
|
|
允许直接访问代表当前用户的主体对象 |
|
|
允许直接访问
|
|
|
始终评估为
|
|
|
始终评估为
|
|
|
返回
|
|
|
返回
|
|
|
|
|
|
返回
|
|
|
返回
|
|
|
返回
|
10.3.2. Web安全表达式
要使用表达式保护单个网址,您首先需要
use-expressions
将
<http>
元素中
的
属性
设置
为
true
。
然后,Spring Security将期望
元素
的
access
属性
<intercept-url>
包含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访问表达式时
,
该类的实例用作表达式根对象。
该对象还直接
HttpServletRequest
在名称下
公开该
对象,
request
因此您可以直接在表达式中调用请求。
如果使用表达式,
WebExpressionVoter
则将
a
添加到
AccessDecisionManager
由名称空间使用。
因此,如果您不使用名称空间而想使用表达式,则必须将其中之一添加到配置中。
在Web安全表达式中引用Bean
如果您希望扩展可用的表达式,则可以轻松地引用您公开的任何Spring Bean。
例如,假设您有一个名称为Bean的Bean,
webSecurity
其中包含以下方法签名:
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中的路径变量是很好的。
例如,考虑一个RESTful应用程序,该应用程序通过格式从URL路径中的ID通过ID查找用户
/user/{userId}
。
您可以通过将路径变量放在模式中来轻松地引用它。
例如,如果您有一个Bean,其名称
webSecurity
包含以下方法签名:
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(authorizeRequests ->
authorizeRequests
.antMatchers("/user/{userId}/**").access("@webSecurity.checkUserId(authentication,#userId)")
...
);
在这两种配置中,匹配的URL会将路径变量传递(并将其转换)为checkUserId方法。
例如,如果URL为
/user/123/resource
,则传入的ID为
123
。
10.3.3. 方法安全性表达式
方法安全性比简单的允许或拒绝规则要复杂一些。 为了提供对表达式使用的全面支持,Spring Security 3.0引入了一些新的注释。
@Pre和@Post批注
有四个注释支持表达式属性,以允许调用前和调用后的授权检查,还支持过滤提交的集合参数或返回值。
它们是
@PreAuthorize
,
@PreFilter
,
@PostAuthorize
和
@PostFilter
。
通过
global-method-security
命名空间元素
启用它们的使用
:
<global-method-security pre-post-annotations="enabled"/>
使用@PreAuthorize和@PostAuthorize的访问控制
最明显有用的注释是
@PreAuthorize
决定是否可以实际调用方法的
注释
。
例如(来自“联系人”示例应用程序)
@PreAuthorize("hasRole('USER')")
public void create(Contact contact);
这意味着只有角色为“ ROLE_USER”的用户才能访问。 显然,使用传统配置和所需角色的简单配置属性可以轻松实现同一目标。 但是关于:
@PreAuthorize("hasPermission(#contact, 'admin')")
public void deletePermission(Contact contact, Sid recipient, Permission permission);
在这里,我们实际上是使用方法参数作为表达式的一部分,以确定当前用户是否具有给定联系人的“管理员”权限。
内置
hasPermission()
表达式通过应用程序上下文链接到Spring Security ACL模块,
如下所示
。
您可以按名称作为表达式变量访问任何方法参数。
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方法。
@PreAuthorize("#contact.name == authentication.name")
public void doSomething(Contact contact);
在这里,我们正在访问另一个内置表达式
authentication
,该
表达式
Authentication
存储在安全上下文中。
您还可以使用表达式直接访问其“主要”属性
principal
。
该值通常是一个
UserDetails
实例,因此您可以使用类似
principal.username
或
的表达式
principal.enabled
。
使用@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
此批注
的
属性
通过名称选择一个
。
请注意,过滤显然不能替代调整数据检索查询。 如果要过滤大型集合并删除许多条目,则效率可能很低。
内置表达式
有一些特定于方法安全性的内置表达式,我们已经在上面使用过。
该
filterTarget
和
returnValue
值是很简单,但使用的
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
不提供
第一个参数(
对象)。
第一种方法用于已经控制访问的域对象已经加载的情况。
如果当前用户对该对象具有给定的权限,则expression将返回true。
第二种版本用于未加载对象但已知其标识符的情况。
还需要域对象的抽象“类型”说明符,以允许加载正确的ACL权限。
传统上,这是对象的Java类,但是不必与对象的权限加载方式一致。
要使用
hasPermission()
表达式,您必须
PermissionEvaluator
在应用程序上下文中
显式配置a
。
看起来像这样:
<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
实现的bean
在哪里
PermissionEvaluator
?
通常,这将是来自称为的ACL模块的实现
AclPermissionEvaluator
。
有关更多详细信息,请参见“联系人”示例应用程序配置。
方法安全性元注释
您可以使用元注释来保证方法的安全性,以使代码更具可读性。 如果发现在整个代码库中重复相同的复杂表达式,这将特别方便。 例如,考虑以下内容:
@PreAuthorize("#contact.name == authentication.name")
无需在所有地方重复此操作,我们可以创建可以使用的元注释。
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("#contact.name == authentication.name")
public @interface ContactPermission {}
元注释可以用于任何Spring Security方法安全注释。 为了保持符合规范,JSR-250批注不支持元批注。
10.4. 授权请求
我们的示例仅要求对用户进行身份验证,并且对应用程序中的每个URL都进行了身份验证。
我们可以通过向我们的
http.authorizeRequests()
方法
添加多个子级来为URL指定自定义要求
。
例如:
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests(authorizeRequests -> (1)
authorizeRequests
.antMatchers("/resources/**", "/signup", "/about").permitAll() (2)
.antMatchers("/admin/**").hasRole("ADMIN") (3)
.antMatchers("/db/**").access("hasRole('ADMIN') and hasRole('DBA')") (4)
.anyRequest().authenticated() (5)
)
.formLogin(withDefaults());
}
| 1个 |
该方法有多个子级,
http.authorizeRequests()
每个子级都按照声明它们的顺序考虑。
|
| 2 | 我们指定了任何用户都可以访问的多个URL模式。 具体来说,如果URL以“ / resources /”开头,等于“ / signup”或等于“ / about”,则任何用户都可以访问请求。 |
| 3 |
以“ / admin /”开头的任何URL都将限于角色为“ ROLE_ADMIN”的用户。
您会注意到,由于我们正在调用该
hasRole
方法,因此无需指定“ ROLE_”前缀。
|
| 4 |
任何以“ / db /”开头的URL都要求用户同时具有“ ROLE_ADMIN”和“ ROLE_DBA”。
您会注意到,由于我们使用的是
hasRole
表达式,因此无需指定“ ROLE_”前缀。
|
| 5 | 任何尚未匹配的URL仅要求对用户进行身份验证 |
10.5. 方法安全性
从2.0版开始,Spring Security大大改进了对为服务层方法增加安全性的支持。
它提供对JSR-250注释安全性以及框架原始
@Secured
注释的支持。
从3.0开始,您还可以使用新
的基于表达式的注释
。
您可以使用
intercept-methods
元素来装饰Bean声明,
从而对单个Bean应用安全
性,或者可以使用AspectJ样式切入点在整个服务层中保护多个Bean。
10.5.1. EnableGlobalMethodSecurity
我们可以
@EnableGlobalMethodSecurity
在任何
@Configuration
实例
上
使用
注释
来启用基于注释的安全性
。
例如,以下将启用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);
}
10.5.2. GlobalMethodSecurityConfiguration
有时您可能需要执行比使用
@EnableGlobalMethodSecurity
注释允许
可能还要复杂的操作
。
对于这些实例,您可以扩展
GlobalMethodSecurityConfiguration
确保
@EnableGlobalMethodSecurity
子类中存在注释。
例如,如果您想提供一个custom
MethodSecurityExpressionHandler
,则可以使用以下配置:
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
@Override
protected MethodSecurityExpressionHandler createExpressionHandler() {
// ... create and return custom MethodSecurityExpressionHandler ...
return expressionHandler;
}
}
有关可以覆盖的方法的其他信息,请参阅
GlobalMethodSecurityConfiguration
Javadoc。
10.5.3. <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。
===
|
| ===您可以在同一应用程序中启用不止一种类型的注释,但是任何接口或类都只能使用一种类型的注释,否则将无法很好地定义行为。 如果找到两个适用于特定方法的注释,则将仅应用其中一个。 === |
10.5.4. 使用保护切入点添加安全切入点
的使用
protect-pointcut
特别强大,因为它允许您仅通过简单的声明就可以将安全性应用于许多bean。
考虑以下示例:
<global-method-security>
<protect-pointcut expression="execution(* com.mycompany.*Service.*(..))"
access="ROLE_USER"/>
</global-method-security>
这将保护在应用程序上下文中声明的,其类在
com.mycompany
包中且其类名以“ Service”结尾的
bean上的所有方法
。
只有具有该
ROLE_USER
角色的
用户
才能调用这些方法。
与URL匹配一样,最具体的匹配项必须在切入点列表中排在第一位,因为将使用第一个匹配表达式。
安全注释优先于切入点。
10.6. 域对象安全性(ACL)
10.6.1. 总览
复杂的应用程序经常会发现需要定义访问权限,而不仅仅是在Web请求或方法调用级别。
取而代之的是,安全决策需要同时包含谁(
Authentication
),哪里(
MethodInvocation
)和什么(
SomeDomainObject
)。
换句话说,授权决策还需要考虑方法调用的实际域对象实例主题。
假设您正在设计宠物诊所的应用程序。 基于Spring的应用程序将有两个主要的用户组:宠物诊所的工作人员以及宠物诊所的客户。 工作人员将有权访问所有数据,而您的客户只能看到自己的客户记录。 为了使其更具趣味性,您的客户可以允许其他用户查看其客户记录,例如其“幼教幼犬”导师或本地“小马俱乐部”的总裁。 以Spring Security为基础,您可以使用几种方法:
-
编写您的业务方法以增强安全性。 您可以在
Customer域对象实例中 查询集合, 以确定哪些用户可以访问。 通过使用SecurityContextHolder.getContext().getAuthentication(),您将可以访问该Authentication对象。 -
编写一个,
AccessDecisionVoter以从GrantedAuthority[]存储在Authentication对象 中的 强制实施安全性 。 这意味着您AuthenticationManager将需要Authentication用custom 填充, 以GrantedAuthority[]表示Customer委托人有权访问的 每个 域对象实例。 -
编写一个
AccessDecisionVoter来增强安全性并Customer直接 打开目标 域对象。 这意味着您的投票者需要访问DAO,以允许其检索Customer对象。 然后它将访问Customer对象的已批准用户的集合并做出适当的决定。
这些方法中的每一种都是完全合法的。
但是,第一个将您的授权检查与您的业务代码结合在一起。
这样做的主要问题包括单元测试的难度增加,以及在
Customer
其他地方
重用
授权逻辑
会更加困难
。
GrantedAuthority[]
从
Authentication
对象中
获得
s
也可以,但是不会缩放为大量
Customer
s。
如果用户能够访问5,000
Customer
s(在这种情况下不太可能,但是可以想象如果它是大型Pony Club的受欢迎的兽医!),那么消耗的内存量和构造
Authentication
对象
所需的时间
将是不希望的。
最后的方法,打开
Customer
直接来自外部代码,可能是三者中最好的。
它实现了关注点的分离,并且不会滥用内存或CPU周期,但是仍然效率不高,因为
AccessDecisionVoter
最终的业务方法本身都会执行对负责检索
Customer
对象
的DAO的调用
。
每个方法调用两次访问显然是不可取的。
此外,列出每种方法时,您需要从头开始编写自己的访问控制列表(ACL)持久性和业务逻辑。
幸运的是,还有另一种选择,我们将在下面讨论。
10.6.2. 关键概念
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”代表“安全身份”)。 唯一的列是ID,SID的文本表示形式以及用于指示文本表示形式是引用主体名称还是a的标志
GrantedAuthority。 因此,每个唯一的主体或都有一行GrantedAuthority。 当在接收许可的上下文中使用SID时,通常将其称为“收件人”。 -
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表。 -
AccessControlEntry:Acl持有多个AccessControlEntry,在框架中通常缩写为ACE。 每个ACE是指一个特定的元组Permission,Sid和Acl。 ACE也可以是授予或不授予的,并且包含审核设置。 ACE存储在ACL_ENTRY表中。 -
Permission:权限表示特定的不可变位掩码,并提供用于位掩码和输出信息的便捷功能。 上面提供的基本权限(位0到4)包含在BasePermission该类中。 -
Sid:ACL模块需要引用principal和GrantedAuthority[]s。Sid接口 提供了间接级别 ,它是“安全身份”的缩写。 常见的类包括PrincipalSid(代表Authentication对象 内部的主体 )和GrantedAuthoritySid。 安全标识信息存储在ACL_SID表中。 -
ObjectIdentity:每个域对象在ACL模块内部由表示ObjectIdentity。 默认实现称为ObjectIdentityImpl。 -
AclService:检索Acl给定 的 适用对象ObjectIdentity。 在包含的实现(JdbcAclService)中,检索操作委托给LookupStrategy。 它LookupStrategy提供了一种高度优化的策略,用于使用批量检索来检索ACL信息,(BasicLookupStrategy并支持利用实例化视图,分层查询和类似的以性能为中心的非ANSI SQL功能的自定义实现。 -
MutableAclService:允许提出修改Acl以保持持久性。 如果您不愿意,则不必使用此界面。
请注意,我们现成的AclService和相关数据库类均使用ANSI SQL。 因此,这应该适用于所有主要数据库。 在撰写本文时,已使用Hypersonic SQL,PostgreSQL,Microsoft SQL Server和Oracle成功测试了该系统。
Spring Security附带了两个示例,它们演示了ACL模块。 第一个是联系人样本,另一个是文档管理系统(DMS)样本。 我们建议您看一下这些作为示例。
10.6.3. 入门
要开始使用Spring Security的ACL功能,您需要将ACL信息存储在某处。
这需要实例化
DataSource
使用Spring。
在
DataSource
随后被注入
JdbcMutableAclService
和
BasicLookupStrategy
实例。
后者提供高性能的ACL检索功能,而前者提供了mutator功能。
有关示例配置,请参阅Spring Security附带的示例之一。
您还需要使用上一节中列出的四个特定于ACL的表填充数据库(有关适当的SQL语句,请参阅ACL示例)。
创建所需的架构并实例化之后
JdbcMutableAclService
,接下来需要确保您的域模型支持与Spring Security ACL软件包的互操作性。
希望
ObjectIdentityImpl
它将证明是足够的,因为它提供了许多可以使用它的方式。
大多数人将拥有包含
public Serializable getId()
方法的
域对象
。
如果返回类型为long或与long兼容(例如int),您将发现无需进一步考虑
ObjectIdentity
问题。
ACL模块的许多部分都依赖长标识符。
如果您不使用long(或int,byte等),则很有可能需要重新实现许多类。
我们不打算在Spring Security的ACL模块中支持非长标识符,因为长已经与所有数据库序列(最常见的标识符数据类型)兼容,并且长度足以容纳所有常见的使用场景。
以下代码片段显示了如何创建
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”的主体可以“管理”该对象。 除了insertAce方法外,该代码段是相对不言自明的。 insertAce方法的第一个参数是确定新条目将在Acl中的哪个位置插入。 在上面的示例中,我们只是将新ACE放在现有ACE的末尾。 最后一个参数是布尔值,指示ACE是授予还是拒绝。 在大多数情况下,它会被授予(true),但是如果它被拒绝(false),则实际上会阻止该权限。
Spring Security没有提供任何特殊的集成来自动创建,更新或删除ACL,这是DAO或存储库操作的一部分。 相反,您将需要为单个域对象编写如上所示的代码。 值得考虑在服务层上使用AOP来自动将ACL信息与服务层操作集成在一起。 过去,我们发现这是一种非常有效的方法。
一旦使用了上述技术在数据库中存储了一些ACL信息,下一步就是实际使用ACL信息作为授权决策逻辑的一部分。
您在这里有很多选择。
你可以写你自己
AccessDecisionVoter
或
AfterInvocationProvider
前或方法调用后分别触发。
这样的类将用于
AclService
检索相关的ACL,然后调用
Acl.isGranted(Permission[] permission, Sid[] sids, boolean administrativeMode)
以确定是否授予许可。
或者,您可以使用我们的
AclEntryVoter
,
AclEntryAfterInvocationProvider
还是
AclEntryAfterInvocationCollectionFilteringProvider
类。
所有这些类都提供了一种基于声明的方法,用于在运行时评估ACL信息,使您无需编写任何代码。
请 Reference示例应用程序以了解如何使用这些类。
11. OAuth2
11.1. OAuth 2.0登录
OAuth 2.0登录功能为应用程序提供了使用户能够通过使用其在OAuth 2.0提供程序(例如GitHub)或OpenID Connect 1.0提供程序(例如Google)上的现有帐户登录该应用程序的功能。 OAuth 2.0登录实现了以下用例:“使用Google登录”或“使用GitHub登录”。
| OAuth 2.0登录是通过使用 OAuth 2.0授权框架 和 OpenID Connect Core 1.0中 指定 的 授权代码授予 来实现的 。 |
11.1.1. Spring Boot 2.x示例
Spring Boot 2.x为OAuth 2.0登录带来了完整的自动配置功能。
本部分显示了如何 使用Google 作为身份验证提供程序 来配置 OAuth 2.0登录示例, 并涵盖以下主题:
最初设定
要使用Google的OAuth 2.0身份验证系统进行登录,您必须在Google API控制台中设置一个项目以获得OAuth 2.0凭据。
| Google的OAuth 2.0 身份验证 实现 符合 OpenID Connect 1.0 规范,并且已通过 OpenID认证 。 |
请按照 “设置OAuth 2.0”部分开始 的 OpenID Connect 页面 上的说明进行操作 。
完成“获取OAuth 2.0凭据”说明后,您应该拥有一个新的OAuth客户端,其凭据由客户端ID和客户端密钥组成。
设置重定向URI
重定向URI是最终用户的用户代理在通过Google身份验证并授予 对“同意”页面上的 OAuth客户端 (在上一步中创建)的 访问权限后,重定向到应用程序中的路径 。
在“设置重定向URI”子部分中,确保将“
授权重定向URI”
字段设置为
http://localhost:8080/login/oauth2/code/google
。
默认重定向URI模板为
{baseUrl}/login/oauth2/code/{registrationId}
。
该
registrationId
是用于唯一标识符
ClientRegistration
。
|
配置application.yml
现在,您有了Google的新OAuth客户端,您需要配置应用程序以将OAuth客户端用于 身份验证流程 。 为此:
-
转到
application.yml并设置以下配置:spring: security: oauth2: client: registration: (1) google: (2) client-id: google-client-id client-secret: google-client-secret例子24. OAuth客户端属性1个 spring.security.oauth2.client.registration是OAuth客户端属性的基本属性前缀。2 基本属性前缀后面是 ClientRegistration 的ID ,例如google。 -
将
client-id和client-secret属性中 的值替换为 您之前创建的OAuth 2.0凭据。
启动应用程序
启动Spring Boot 2.x示例并转到
http://localhost:8080
。
然后,您将重定向到默认的
自动生成的
登录页面,该页面显示Google的链接。
单击Google链接,然后您将重定向到Google进行身份验证。
在使用您的Google帐户凭据进行身份验证之后,显示给您的下一页是“同意”屏幕。 “同意”屏幕要求您允许或拒绝访问您之前创建的OAuth客户端。 点击 允许 以授权OAuth客户端访问您的电子邮件地址和基本个人资料信息。
此时,OAuth客户端将从 UserInfo端点 检索您的电子邮件地址和基本个人资料信息, 并建立经过身份验证的会话。
11.1.2. Spring Boot 2.x属性映射
下表概述了Spring Boot 2.x OAuth客户端属性到 ClientRegistration 属性 的映射 。
| Spring Boot 2.x | 客户注册 |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
11.1.3. CommonOAuth2Provider
CommonOAuth2Provider
为许多知名提供商(Google,GitHub,Facebook和Okta)预定义了一组默认客户端属性。
例如,
authorization-uri
,
token-uri
,和
user-info-uri
不经常对供应商变更。
因此,提供默认值以减少所需的配置是有意义的。
如前所述,在
配置Google客户端时
,仅
需要
client-id
和
client-secret
属性。
以下清单显示了一个示例:
spring:
security:
oauth2:
client:
registration:
google:
client-id: google-client-id
client-secret: google-client-secret
客户端属性的自动默认设置在这里可以无缝工作,因为
registrationId
(
google
)与中的
GOOGLE enum
(不区分大小写)
匹配
CommonOAuth2Provider
。
|
对于可能需要指定其他值
registrationId
(例如)的情况
google-login
,您仍然可以通过配置属性来利用客户端属性的自动默认设置
provider
。
以下清单显示了一个示例:
spring:
security:
oauth2:
client:
registration:
google-login: (1)
provider: google (2)
client-id: google-client-id
client-secret: google-client-secret
| 1个 |
将
registrationId
设置为
google-login
。
|
| 2 |
该
provider
属性设置为
google
,这将利用中设置的客户端属性的自动默认设置
CommonOAuth2Provider.GOOGLE.getBuilder()
。
|
11.1.4. 配置自定义提供程序属性
有些OAuth 2.0提供程序支持多租户,这会导致每个租户(或子域)使用不同的协议端点。
例如,向Okta注册的OAuth客户端被分配给特定的子域,并拥有自己的协议端点。
对于这些情况,Spring Boot 2.x提供以下用于配置自定义提供程序属性的基本属性:
。
spring.security.oauth2.client.provider.[providerId]
以下清单显示了一个示例:
spring:
security:
oauth2:
client:
registration:
okta:
client-id: okta-client-id
client-secret: okta-client-secret
provider:
okta: (1)
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
)允许自定义配置协议端点位置。
|
11.1.5. 覆盖Spring Boot 2.x自动配置
用于OAuth客户端支持的Spring Boot 2.x自动配置类为
OAuth2ClientAutoConfiguration
。
它执行以下任务:
-
从配置的OAuth客户端属性中 注册
ClientRegistrationRepository@Bean由组成的ClientRegistration。 -
提供
WebSecurityConfigurerAdapter@Configuration并通过启用OAuth 2.0登录httpSecurity.oauth2Login()。
如果您需要根据自己的特定要求覆盖自动配置,则可以通过以下方式进行:
注册一个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
和启用的OAuth 2.0登录通过
httpSecurity.oauth2Login()
:
@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests(authorizeRequests ->
authorizeRequests
.anyRequest().authenticated()
)
.oauth2Login(withDefaults());
}
}
完全覆盖自动配置
以下示例显示了如何通过注册a
ClientRegistrationRepository @Bean
并提供a
来完全覆盖自动配置
WebSecurityConfigurerAdapter
。
@Configuration
public class OAuth2LoginConfig {
@EnableWebSecurity
public static class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests(authorizeRequests ->
authorizeRequests
.anyRequest().authenticated()
)
.oauth2Login(withDefaults());
}
}
@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();
}
}
11.1.6. 没有Spring Boot 2.x的Java配置
如果您无法使用Spring Boot 2.x,并且想要在其中配置预定义的提供程序之一
CommonOAuth2Provider
(例如Google),请应用以下配置:
@Configuration
public class OAuth2LoginConfig {
@EnableWebSecurity
public static class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests(authorizeRequests ->
authorizeRequests
.anyRequest().authenticated()
)
.oauth2Login(withDefaults());
}
}
@Bean
public ClientRegistrationRepository clientRegistrationRepository() {
return new InMemoryClientRegistrationRepository(this.googleClientRegistration());
}
@Bean
public OAuth2AuthorizedClientService authorizedClientService(
ClientRegistrationRepository clientRegistrationRepository) {
return new InMemoryOAuth2AuthorizedClientService(clientRegistrationRepository);
}
@Bean
public OAuth2AuthorizedClientRepository authorizedClientRepository(
OAuth2AuthorizedClientService authorizedClientService) {
return new AuthenticatedPrincipalOAuth2AuthorizedClientRepository(authorizedClientService);
}
private ClientRegistration googleClientRegistration() {
return CommonOAuth2Provider.GOOGLE.getBuilder("google")
.clientId("google-client-id")
.clientSecret("google-client-secret")
.build();
}
}
11.1.7. 进阶设定
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(oauth2Login ->
oauth2Login
.authorizationEndpoint(authorizationEndpoint ->
authorizationEndpoint
...
)
.redirectionEndpoint(redirectionEndpoint ->
redirectionEndpoint
...
)
.tokenEndpoint(tokenEndpoint ->
tokenEndpoint
...
)
.userInfoEndpoint(userInfoEndpoint ->
userInfoEndpoint
...
)
);
}
}
oauth2Login()
DSL
的主要目标
是使名称与规范中定义的名称保持一致。
OAuth 2.0授权框架对 协议端点的 定义 如下:
授权过程利用两个授权服务器端点(HTTP资源):
-
授权端点:客户端用于通过用户代理重定向从资源所有者获取授权。
-
令牌端点:由客户端用于交换访问令牌的授权授权,通常与客户端身份验证进行交换。
以及一个客户端端点:
-
重定向端点:由授权服务器用于通过资源所有者用户代理将包含授权证书的响应返回给客户端。
OpenID Connect Core 1.0规范定义了 UserInfo端点 ,如下所示:
UserInfo端点是OAuth 2.0受保护的资源,它返回有关经过身份验证的最终用户的声明。 为了获得所请求的有关最终用户的声明,客户端使用通过OpenID Connect Authentication获得的访问令牌向UserInfo端点发出请求。 这些声明通常由JSON对象表示,该对象包含声明的名称/值对的集合。
以下代码显示了可用于
oauth2Login()
DSL
的完整配置选项
:
@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.oauth2Login(oauth2Login ->
oauth2Login
.clientRegistrationRepository(this.clientRegistrationRepository())
.authorizedClientRepository(this.authorizedClientRepository())
.authorizedClientService(this.authorizedClientService())
.loginPage("/login")
.authorizationEndpoint(authorizationEndpoint ->
authorizationEndpoint
.baseUri(this.authorizationRequestBaseUri())
.authorizationRequestRepository(this.authorizationRequestRepository())
.authorizationRequestResolver(this.authorizationRequestResolver())
)
.redirectionEndpoint(redirectionEndpoint ->
redirectionEndpoint
.baseUri(this.authorizationResponseBaseUri())
)
.tokenEndpoint(tokenEndpoint ->
tokenEndpoint
.accessTokenResponseClient(this.accessTokenResponseClient())
)
.userInfoEndpoint(userInfoEndpoint ->
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登录)。
为了
DefaultLoginPageGeneratingFilter
显示已配置的OAuth客户端的链接,注册
ClientRegistrationRepository
者还需要实现
Iterable<ClientRegistration>
。
请参阅
InMemoryClientRegistrationRepository
以供 Reference。
|
每个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(oauth2Login ->
oauth2Login
.loginPage("/login/oauth2")
...
.authorizationEndpoint(authorizationEndpoint ->
authorizationEndpoint
.baseUri("/login/oauth2/authorization")
...
)
);
}
}
您需要提供
@Controller
一个
@RequestMapping("/login/oauth2")
能够呈现的自定义登录页面。
|
|
如前所述,配置
下面的行显示了一个示例:
|
重定向端点
重定向端点由授权服务器用于通过资源所有者用户代理将授权响应(包含授权凭证)返回给客户端。
| 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(oauth2Login ->
oauth2Login
.redirectionEndpoint(redirectionEndpoint ->
redirectionEndpoint
.baseUri("/login/oauth2/callback/*")
...
)
);
}
}
|
您还需要确保
以下清单显示了一个示例:
|
UserInfo端点
UserInfo端点包括许多配置选项,如以下小节所述:
映射用户权限
在用户成功通过OAuth 2.0提供程序进行身份验证之后,
OAuth2User.getAuthorities()
(或
OidcUser.getAuthorities()
)可能会映射到一组新的
GrantedAuthority
实例,这些实例将
OAuth2AuthenticationToken
在完成身份验证时
提供给它们
。
OAuth2AuthenticationToken.getAuthorities()
用于授权请求,例如in
hasRole('USER')
或
hasRole('ADMIN')
。
|
映射用户权限时,有两个选项可供选择:
提供一个实现
GrantedAuthoritiesMapper
并对其进行配置,如以下示例所示:
@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.oauth2Login(oauth2Login ->
oauth2Login
.userInfoEndpoint(userInfoEndpoint ->
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(withDefaults());
}
@Bean
public GrantedAuthoritiesMapper userAuthoritiesMapper() {
...
}
}
与使用相比,此策略是高级的
GrantedAuthoritiesMapper
,但是,它也更灵活,因为它使您可以访问
OAuth2UserRequest
and
OAuth2User
(使用OAuth 2.0 UserService时)或
OidcUserRequest
and
OidcUser
(使用OpenID Connect 1.0 UserService时)。
在
OAuth2UserRequest
(和
OidcUserRequest
)为您提供访问到相关的
OAuth2AccessToken
,这是在哪里的情况下非常有用的
委托
需要从受保护的资源获取权威信息,才能为用户映射海关当局。
以下示例显示如何使用OpenID Connect 1.0 UserService实施和配置基于委派的策略:
@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.oauth2Login(oauth2Login ->
oauth2Login
.userInfoEndpoint(userInfoEndpoint ->
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
CustomUserTypesOAuth2UserService
是的实现,
OAuth2UserService
为自定义
OAuth2User
类型
提供支持
。
如果默认实现(
DefaultOAuth2User
)不符合您的需求,则可以定义自己的实现
OAuth2User
。
以下代码演示了如何
OAuth2User
为GitHub
注册自定义
类型:
@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.oauth2Login(oauth2Login ->
oauth2Login
.userInfoEndpoint(userInfoEndpoint ->
userInfoEndpoint
.customUserType(GitHubOAuth2User.class, "github")
...
)
);
}
}
以下代码显示了
OAuth2User
GitHub
自定义
类型
的示例
:
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;
}
}
id
,
name
,
login
,和
email
是属性在GitHub的的UserInfo响应返回。
有关从UserInfo端点返回的详细信息,请参阅API文档
“获取经过身份验证的用户”
。
|
OAuth 2.0 UserService
DefaultOAuth2UserService
是
OAuth2UserService
支持标准OAuth 2.0提供程序的实现。
OAuth2UserService
从UserInfo端点获取最终用户(资源所有者)的用户属性(使用授权流程中授予客户端的访问令牌),并
AuthenticatedPrincipal
以的形式
返回
OAuth2User
。
|
DefaultOAuth2UserService
使用
RestOperations
在用户信息端点时,要求用户属性。
如果您需要自定义UserInfo请求的预处理,则可以提供
DefaultOAuth2UserService.setRequestEntityConverter()
一个custom
Converter<OAuth2UserRequest, RequestEntity<?>>
。
默认实现
OAuth2UserRequestEntityConverter
会构建
RequestEntity
UserInfo Request
的
表示形式,
默认情况下
会
OAuth2AccessToken
在
Authorization
标头中设置。
另一方面,如果您需要自定义UserInfo Response的后处理,则需要提供
DefaultOAuth2UserService.setRestOperations()
一个自定义的configure
RestOperations
。
默认
RestOperations
配置如下:
RestTemplate restTemplate = new RestTemplate();
restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
OAuth2ErrorResponseErrorHandler
是
ResponseErrorHandler
可以处理OAuth 2.0错误(400错误请求)的。
它使用
OAuth2ErrorHttpMessageConverter
将OAuth 2.0错误参数转换为
OAuth2Error
。
无论您是自定义
DefaultOAuth2UserService
还是提供自己的实现
OAuth2UserService
,都需要按以下示例所示进行配置:
@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.oauth2Login(oauth2Login ->
oauth2Login
.userInfoEndpoint(userInfoEndpoint ->
userInfoEndpoint
.userService(this.oauth2UserService())
...
)
);
}
private OAuth2UserService<OAuth2UserRequest, OAuth2User> oauth2UserService() {
...
}
}
OpenID Connect 1.0用户服务
OidcUserService
是
OAuth2UserService
支持OpenID Connect 1.0提供程序的实现。
在UserInfo端点上请求用户属性时
,
OidcUserService
杠杆会利用
DefaultOAuth2UserService
。
如果您需要自定义UserInfo请求的预处理和/或UserInfo Response的后处理,则需要提供
OidcUserService.setOauth2UserService()
一个custom configure
DefaultOAuth2UserService
。
无论您
OidcUserService
是
OAuth2UserService
为OpenID Connect 1.0提供程序
自定义
还是提供自己的实现
,都需要按以下示例所示进行配置:
@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.oauth2Login(oauth2Login ->
oauth2Login
.userInfoEndpoint(userInfoEndpoint ->
userInfoEndpoint
.oidcUserService(this.oidcUserService())
...
)
);
}
private OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService() {
...
}
}
ID令牌签名验证
OpenID Connect 1.0身份验证引入了 ID令牌 ,它是一个安全令牌,其中包含有关由客户端使用的授权服务器对最终用户的身份验证的声明。
ID令牌表示为 JSON Web令牌 (JWT),并且必须使用 JSON Web签名 (JWS)进行 签名 。
在
OidcIdTokenDecoderFactory
一个提供
JwtDecoder
用于
OidcIdToken
签名验证。
默认算法是
RS256
但在客户端注册期间分配时可能会有所不同。
对于这些情况,可以将解析程序配置为返回分配给特定客户端的预期JWS算法。
JWS算法解析器是a
Function
,接受a
ClientRegistration
并返回
JwsAlgorithm
客户端
的期望值
,例如。
SignatureAlgorithm.RS256
要么
MacAlgorithm.HS256
下面的代码演示了如何配置
OidcIdTokenDecoderFactory @Bean
默认为
MacAlgorithm.HS256
所有
ClientRegistration
:
@Bean
public JwtDecoderFactory<ClientRegistration> idTokenDecoderFactory() {
OidcIdTokenDecoderFactory idTokenDecoderFactory = new OidcIdTokenDecoderFactory();
idTokenDecoderFactory.setJwsAlgorithmResolver(clientRegistration -> MacAlgorithm.HS256);
return idTokenDecoderFactory;
}
对于基于MAC的算法,如
HS256
,
HS384
或者
HS512
,在
client-secret
对应于
client-id
被用作用于签名验证的对称密钥。
|
如果
ClientRegistration
为OpenID Connect 1.0身份验证配置了
多个
,则JWS算法解析器可以评估提供的内容
ClientRegistration
以确定返回哪种算法。
|
OpenID Connect 1.0注销
OpenID Connect会话管理1.0允许使用客户端在提供商处注销最终用户。 可用的策略之一是 RP发起的注销 。
如果OpenID提供程序同时支持会话管理和
发现
,则客户端可以
end_session_endpoint URL
从OpenID提供程序的
发现元数据中获取
。
这可以通过配置来实现
ClientRegistration
与
issuer-uri
,如在下面的例子:
spring:
security:
oauth2:
client:
registration:
okta:
client-id: okta-client-id
client-secret: okta-client-secret
...
provider:
okta:
issuer-uri: https://dev-1234.oktapreview.com
…和
OidcClientInitiatedLogoutSuccessHandler
实现RP启动的注销的,可以配置如下:
@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private ClientRegistrationRepository clientRegistrationRepository;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests(authorizeRequests ->
authorizeRequests
.anyRequest().authenticated()
)
.oauth2Login(withDefaults())
.logout(logout ->
logout
.logoutSuccessHandler(oidcLogoutSuccessHandler())
);
}
private LogoutSuccessHandler oidcLogoutSuccessHandler() {
OidcClientInitiatedLogoutSuccessHandler oidcLogoutSuccessHandler =
new OidcClientInitiatedLogoutSuccessHandler(this.clientRegistrationRepository);
// Sets the `URI` that the End-User's User Agent will be redirected to
// after the logout has been performed at the Provider
oidcLogoutSuccessHandler.setPostLogoutRedirectUri(URI.create("https://localhost:8080"));
return oidcLogoutSuccessHandler;
}
}
11.2. OAuth 2.0客户端
OAuth 2.0客户端功能提供了对 OAuth 2.0授权框架中 定义的客户端角色的支持 。
在较高级别,可用的核心功能包括:
-
WebClientServlet环境的集成 (用于请求受保护的资源)
所述
HttpSecurity.oauth2Client()
DSL提供了许多配置选项用于定制由OAuth 2.0用户端所使用的核心组件。
此外,
HttpSecurity.oauth2Client().authorizationCodeGrant()
启用自定义授权码授予。
以下代码显示了
HttpSecurity.oauth2Client()
DSL
提供的完整配置选项
:
@EnableWebSecurity
public class OAuth2ClientSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.oauth2Client(oauth2Client ->
oauth2Client
.clientRegistrationRepository(this.clientRegistrationRepository())
.authorizedClientRepository(this.authorizedClientRepository())
.authorizedClientService(this.authorizedClientService())
.authorizationCodeGrant(authorizationCodeGrant ->
authorizationCodeGrant
.authorizationRequestRepository(this.authorizationRequestRepository())
.authorizationRequestResolver(this.authorizationRequestResolver())
.accessTokenResponseClient(this.accessTokenResponseClient())
)
);
}
}
该
OAuth2AuthorizedClientManager
负责与一个或一个以上管理授权一个OAuth 2.0客户端的(或重新授权),在协作
OAuth2AuthorizedClientProvider
(一个或多个)。
下面的代码示出了如何注册一个示例
OAuth2AuthorizedClientManager @Bean
,并将其与一个相关联
OAuth2AuthorizedClientProvider
,它提供支持用于复合
authorization_code
,
refresh_token
,
client_credentials
和
password
授权许可类型:
@Bean
public OAuth2AuthorizedClientManager authorizedClientManager(
ClientRegistrationRepository clientRegistrationRepository,
OAuth2AuthorizedClientRepository authorizedClientRepository) {
OAuth2AuthorizedClientProvider authorizedClientProvider =
OAuth2AuthorizedClientProviderBuilder.builder()
.authorizationCode()
.refreshToken()
.clientCredentials()
.password()
.build();
DefaultOAuth2AuthorizedClientManager authorizedClientManager =
new DefaultOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientRepository);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
return authorizedClientManager;
}
以下各节将详细介绍OAuth 2.0客户端使用的核心组件以及可用的配置选项:
11.2.1. 核心接口/类
客户注册
ClientRegistration
是向OAuth 2.0或OpenID Connect 1.0提供程序注册的客户端的表示。
客户端注册包含信息,例如客户端ID,客户端密钥,授权授予类型,重定向URI,作用域,授权URI,令牌URI和其他详细信息。
ClientRegistration
其属性定义如下:
public final class ClientRegistration {
private String registrationId; (1)
private String clientId; (2)
private String clientSecret; (3)
private ClientAuthenticationMethod clientAuthenticationMethod; (4)
private AuthorizationGrantType authorizationGrantType; (5)
private String redirectUriTemplate; (6)
private Set<String> scopes; (7)
private ProviderDetails providerDetails;
private String clientName; (8)
public class ProviderDetails {
private String authorizationUri; (9)
private String tokenUri; (10)
private UserInfoEndpoint userInfoEndpoint;
private String jwkSetUri; (11)
private Map<String, Object> configurationMetadata; (12)
public class UserInfoEndpoint {
private String uri; (13)
private AuthenticationMethod authenticationMethod; (14)
private String userNameAttributeName; (15)
}
}
}
| 1个 | registrationId
:唯一标识的ID
ClientRegistration
。
|
| 2 | clientId
:客户端标识符。
|
| 3 | clientSecret
:客户机密。
|
| 4 | clientAuthenticationMethod
:用于通过提供者对客户端进行身份验证的方法。
支持的值为
basic
,
post
和
none
(公共客户端)
。
|
| 5 | authorizationGrantType
:OAuth 2.0授权框架定义了四种
授权授予
类型。
支持的值是
authorization_code
,
client_credentials
,
password
和
implicit
。
|
| 6 | redirectUriTemplate
:
授权服务器
在最终用户对客户端进行身份验证和授权访问后将
客户端的注册重定向URI
重定向到最终用户的用户代理。
|
| 7 | scopes
:客户在授权请求流程中请求的范围,例如openid,电子邮件或个人资料。
|
| 8 | clientName
:用于客户端的描述性名称。
该名称可能在某些情况下使用,例如在自动生成的登录页面中显示客户端名称时。
|
| 9 | authorizationUri
:授权服务器的授权端点URI。
|
| 10 | tokenUri
:授权服务器的令牌端点URI。
|
| 11 | jwkSetUri
:用于
从授权服务器
检索
JSON Web密钥(JWK)
集
的URI
,其中包含用于验证
ID令牌
的
JSON Web签名(JWS)
以及用户信息响应(可选
)的加密密钥
。
|
| 12 | configurationMetadata
:
OpenID提供程序配置信息
。
仅当
spring.security.oauth2.client.provider.[providerId].issuerUri
配置
了Spring Boot 2.x属性时,此信息才可用
。
|
| 13 | (userInfoEndpoint)uri
:用于访问经过身份验证的最终用户的声明/属性的UserInfo端点URI。
|
| 14 | (userInfoEndpoint)authenticationMethod
:将访问令牌发送到UserInfo端点时使用的身份验证方法。
支持的值为
header
,
form
和
query
。
|
| 15 | userNameAttributeName
:在UserInfo响应中返回的属性名称,该属性引用了最终用户的名称或标识符。
|
ClientRegistrations
提供了
ClientRegistration
以这种方式
配置a的便捷方法
,如以下示例所示:
ClientRegistration clientRegistration =
ClientRegistrations.fromIssuerLocation("https://idp.example.com/issuer").build();
上面的代码将
https://idp.example.com/issuer/.well-known/openid-configuration
依次
查询
,然后
https://idp.example.com/.well-known/openid-configuration/issuer
,最后
查询,
https://idp.example.com/.well-known/oauth-authorization-server/issuer
首先停止,返回200响应。
作为替代方案,您可以
ClientRegistrations.fromOidcIssuerLocation()
用来仅查询OpenID Connect提供程序的配置终结点。
客户注册资料库
将
ClientRegistrationRepository
用作的OAuth 2.0的存储库/ ID连接1.0
ClientRegistration
(S)。
| 客户端注册信息最终由关联的授权服务器存储和拥有。 此存储库提供了检索与授权服务器一起存储的主要客户端注册信息的子集的功能。 |
Spring Boot 2.x自动配置将的每个属性绑定
到的一个实例,
然后将组成每个
实例
。
spring.security.oauth2.client.registration.[registrationId]
ClientRegistration
ClientRegistration
ClientRegistrationRepository
的默认实现
ClientRegistrationRepository
是
InMemoryClientRegistrationRepository
。
|
自动配置还会在中将
ClientRegistrationRepository
as
注册
为
@Bean
,
ApplicationContext
以便在应用程序需要时可用于依赖项注入。
以下清单显示了一个示例:
@Controller
public class OAuth2ClientController {
@Autowired
private ClientRegistrationRepository clientRegistrationRepository;
@GetMapping("/")
public String index() {
ClientRegistration oktaRegistration =
this.clientRegistrationRepository.findByRegistrationId("okta");
...
return "index";
}
}
OAuth2AuthorizedClient
OAuth2AuthorizedClient
是授权客户的代表。
当最终用户(资源所有者)已向客户端授予访问其受保护资源的权限时,则认为该客户端已被授权。
OAuth2AuthorizedClient
用于将
OAuth2AccessToken
(和可选
OAuth2RefreshToken
)与
ClientRegistration
(客户端)和资源所有者
相关联
,资源所有者是
Principal
授予授权
的
最终用户。
OAuth2AuthorizedClientRepository / OAuth2AuthorizedClientService
OAuth2AuthorizedClientRepository
负责
OAuth2AuthorizedClient
Web请求之间的
持久化
。
鉴于,的主要作用
OAuth2AuthorizedClientService
是
OAuth2AuthorizedClient
在应用程序级别
进行管理
。
从开发人员的角度来看,
OAuth2AuthorizedClientRepository
或
OAuth2AuthorizedClientService
提供了查找
OAuth2AccessToken
与客户端关联
的功能,
以便可以将其用于发起受保护的资源请求。
以下清单显示了一个示例:
@Controller
public class OAuth2ClientController {
@Autowired
private OAuth2AuthorizedClientService authorizedClientService;
@GetMapping("/")
public String index(Authentication authentication) {
OAuth2AuthorizedClient authorizedClient =
this.authorizedClientService.loadAuthorizedClient("okta", authentication.getName());
OAuth2AccessToken accessToken = authorizedClient.getAccessToken();
...
return "index";
}
}
春天开机2.X自动配置寄存器的
OAuth2AuthorizedClientRepository
和/或
OAuth2AuthorizedClientService @Bean
在
ApplicationContext
。
但是,应用程序可以选择覆盖并注册自定义
OAuth2AuthorizedClientRepository
或
OAuth2AuthorizedClientService @Bean
。
|
OAuth2AuthorizedClientManager / OAuth2AuthorizedClientProvider
该
OAuth2AuthorizedClientManager
负责全面管理
OAuth2AuthorizedClient
(一个或多个)。
主要职责包括:
-
使用来授权(或重新授权)OAuth 2.0客户端
OAuth2AuthorizedClientProvider。 -
委托an的持久性
OAuth2AuthorizedClient,通常使用OAuth2AuthorizedClientServiceorOAuth2AuthorizedClientRepository。
一个
OAuth2AuthorizedClientProvider
实施用于授权(或重新授权)OAuth 2.0客户端的策略。
实施通常将实施授权授予类型,例如。
authorization_code
,
client_credentials
等等。
的默认实现
OAuth2AuthorizedClientManager
是
DefaultOAuth2AuthorizedClientManager
,它与关联
OAuth2AuthorizedClientProvider
,可以使用基于委托的组合来支持多种授权授予类型。
该
OAuth2AuthorizedClientProviderBuilder
可用于配置和构建基于代表团复合。
下面的代码示出了如何配置和构建的一个实例
OAuth2AuthorizedClientProvider
,其提供支持用于复合
authorization_code
,
refresh_token
,
client_credentials
和
password
授权许可类型:
@Bean
public OAuth2AuthorizedClientManager authorizedClientManager(
ClientRegistrationRepository clientRegistrationRepository,
OAuth2AuthorizedClientRepository authorizedClientRepository) {
OAuth2AuthorizedClientProvider authorizedClientProvider =
OAuth2AuthorizedClientProviderBuilder.builder()
.authorizationCode()
.refreshToken()
.clientCredentials()
.password()
.build();
DefaultOAuth2AuthorizedClientManager authorizedClientManager =
new DefaultOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientRepository);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
return authorizedClientManager;
}
的
DefaultOAuth2AuthorizedClientManager
也与相关联的
contextAttributesMapper
类型的
Function<OAuth2AuthorizeRequest, Map<String, Object>>
,它负责从映射属性(一个或多个)
OAuth2AuthorizeRequest
到一个
Map
属性将被关联到
OAuth2AuthorizationContext
。
当您需要提供
OAuth2AuthorizedClientProvider
具有必需(受支持)属性的属性
时,此功能将非常有用
。
在
PasswordOAuth2AuthorizedClientProvider
需要资源的所有者
username
,并
password
为处于可用
OAuth2AuthorizationContext.getAttributes()
。
以下代码显示的示例
contextAttributesMapper
:
@Bean
public OAuth2AuthorizedClientManager authorizedClientManager(
ClientRegistrationRepository clientRegistrationRepository,
OAuth2AuthorizedClientRepository authorizedClientRepository) {
OAuth2AuthorizedClientProvider authorizedClientProvider =
OAuth2AuthorizedClientProviderBuilder.builder()
.password()
.refreshToken()
.build();
DefaultOAuth2AuthorizedClientManager authorizedClientManager =
new DefaultOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientRepository);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
// Assuming the `username` and `password` are supplied as `HttpServletRequest` parameters,
// map the `HttpServletRequest` parameters to `OAuth2AuthorizationContext.getAttributes()`
authorizedClientManager.setContextAttributesMapper(contextAttributesMapper());
return authorizedClientManager;
}
private Function<OAuth2AuthorizeRequest, Map<String, Object>> contextAttributesMapper() {
return authorizeRequest -> {
Map<String, Object> contextAttributes = Collections.emptyMap();
HttpServletRequest servletRequest = authorizeRequest.getAttribute(HttpServletRequest.class.getName());
String username = servletRequest.getParameter(OAuth2ParameterNames.USERNAME);
String password = servletRequest.getParameter(OAuth2ParameterNames.PASSWORD);
if (StringUtils.hasText(username) && StringUtils.hasText(password)) {
contextAttributes = new HashMap<>();
// `PasswordOAuth2AuthorizedClientProvider` requires both attributes
contextAttributes.put(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, username);
contextAttributes.put(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, password);
}
return contextAttributes;
};
}
11.2.2. 授权拨款支持
授权码
| 有关 授权代码 授予的 更多详细信息,请 ReferenceOAuth 2.0授权框架 。 |
获得授权
| 请参阅 授权请求/响应 协议流程以获取授权码。 |
发起授权请求
在
OAuth2AuthorizationRequestRedirectFilter
使用一个
OAuth2AuthorizationRequestResolver
解决一个
OAuth2AuthorizationRequest
,并通过重定向最终用户的用户代理到认证服务器的授权端点发起授权码流补助。
的主要作用
OAuth2AuthorizationRequestResolver
是解决
OAuth2AuthorizationRequest
所提供的Web请求中的。
预设实作会
DefaultOAuth2AuthorizationRequestResolver
在(预设)路径上进行比
/oauth2/authorization/{registrationId}
对
registrationId
,以
撷取,
然后使用建构
OAuth2AuthorizationRequest
关联的
ClientRegistration
。
为OAuth 2.0客户端注册提供以下Spring Boot 2.x属性:
spring:
security:
oauth2:
client:
registration:
okta:
client-id: okta-client-id
client-secret: okta-client-secret
authorization-grant-type: authorization_code
redirect-uri: "{baseUrl}/authorized/okta"
scope: read, write
provider:
okta:
authorization-uri: https://dev-1234.oktapreview.com/oauth2/v1/authorize
token-uri: https://dev-1234.oktapreview.com/oauth2/v1/token
具有基本路径
/oauth2/authorization/okta
的请求将通过发起授权请求重定向,
OAuth2AuthorizationRequestRedirectFilter
并最终启动授权码授予流程。
该
AuthorizationCodeOAuth2AuthorizedClientProvider
是一个实现
OAuth2AuthorizedClientProvider
了授权码准许,后者也是由发起授权请求重定向
OAuth2AuthorizationRequestRedirectFilter
。
|
如果OAuth 2.0客户端是 Public Client ,则按以下方式配置OAuth 2.0客户端注册:
spring:
security:
oauth2:
client:
registration:
okta:
client-id: okta-client-id
client-authentication-method: none
authorization-grant-type: authorization_code
redirect-uri: "{baseUrl}/authorized/okta"
...
使用 代码交换证明密钥 (PKCE) 支持公共客户端 。 如果客户端在不受信任的环境中运行(例如,本机应用程序或基于Web浏览器的应用程序),因此无法维护其凭据的机密性,则在满足以下条件时将自动使用PKCE:
-
client-secret被省略(或为空) -
client-authentication-method设置为“无”(ClientAuthenticationMethod.NONE)
该
DefaultOAuth2AuthorizationRequestResolver
还支持
URI
为模板变量
redirect-uri
使用
UriComponentsBuilder
。
以下配置使用所有受支持的
URI
模板变量:
spring:
security:
oauth2:
client:
registration:
okta:
...
redirect-uri: "{baseScheme}://{baseHost}{basePort}{basePath}/authorized/{registrationId}"
...
{baseUrl}
决心
{baseScheme}://{baseHost}{basePort}{basePath}
|
当OAuth 2.0客户端在
代理服务器
后面运行时,
配置
redirect-uri
with
URI
模板变量特别有用
。
这样可确保
在扩展时使用标头
。
X-Forwarded-*
redirect-uri
定制授权请求
OAuth2AuthorizationRequestResolver
可以实现
的主要用例之一是
能够
使用
OAuth 2.0授权框架中定义的标准参数之外的其他参数自定义授权请求。
例如,OpenID Connect为
授权代码流
定义了其他OAuth 2.0请求参数,该参数
从
OAuth 2.0授权框架中
定义的标准参数扩展而来
。
这些扩展参数之一是
prompt
参数。
| 可选的。 用空格分隔的,区分大小写的ASCII字符串值列表,用于指定授权服务器是否提示最终用户进行重新认证和同意。 定义的值是:无,登录,同意,select_account |
以下示例说明了如何
通过包含request参数
来实现
OAuth2AuthorizationRequestResolver
为其定制授权请求的
。
oauth2Login()
prompt=consent
@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private ClientRegistrationRepository clientRegistrationRepository;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests(authorizeRequests ->
authorizeRequests
.anyRequest().authenticated()
)
.oauth2Login(oauth2Login ->
oauth2Login
.authorizationEndpoint(authorizationEndpoint ->
authorizationEndpoint
.authorizationRequestResolver(
new CustomAuthorizationRequestResolver(
this.clientRegistrationRepository)) (1)
)
);
}
}
public class CustomAuthorizationRequestResolver implements OAuth2AuthorizationRequestResolver {
private final OAuth2AuthorizationRequestResolver defaultAuthorizationRequestResolver;
public CustomAuthorizationRequestResolver(
ClientRegistrationRepository clientRegistrationRepository) {
this.defaultAuthorizationRequestResolver =
new DefaultOAuth2AuthorizationRequestResolver(
clientRegistrationRepository, "/oauth2/authorization");
}
@Override
public OAuth2AuthorizationRequest resolve(HttpServletRequest request) {
OAuth2AuthorizationRequest authorizationRequest =
this.defaultAuthorizationRequestResolver.resolve(request); (2)
return authorizationRequest != null ? (3)
customAuthorizationRequest(authorizationRequest) :
null;
}
@Override
public OAuth2AuthorizationRequest resolve(
HttpServletRequest request, String clientRegistrationId) {
OAuth2AuthorizationRequest authorizationRequest =
this.defaultAuthorizationRequestResolver.resolve(
request, clientRegistrationId); (2)
return authorizationRequest != null ? (3)
customAuthorizationRequest(authorizationRequest) :
null;
}
private OAuth2AuthorizationRequest customAuthorizationRequest(
OAuth2AuthorizationRequest authorizationRequest) {
Map<String, Object> additionalParameters =
new LinkedHashMap<>(authorizationRequest.getAdditionalParameters());
additionalParameters.put("prompt", "consent"); (4)
return OAuth2AuthorizationRequest.from(authorizationRequest) (5)
.additionalParameters(additionalParameters) (6)
.build();
}
}
| 1个 |
配置自定义
OAuth2AuthorizationRequestResolver |
| 2 |
尝试解决
OAuth2AuthorizationRequest
使用
DefaultOAuth2AuthorizationRequestResolver |
| 3 |
如果
OAuth2AuthorizationRequest
已解决,则返回自定义版本,否则返回
null |
| 4 |
将自定义参数添加到现有参数
OAuth2AuthorizationRequest.additionalParameters |
| 5 |
创建默认值的副本,该副本
OAuth2AuthorizationRequest
将返回
OAuth2AuthorizationRequest.Builder
进行进一步修改
|
| 6 |
覆盖默认
additionalParameters |
OAuth2AuthorizationRequest.Builder.build()
构造
OAuth2AuthorizationRequest.authorizationRequestUri
,代表使用
application/x-www-form-urlencoded
格式
的完整授权请求URI,包括所有查询参数
。
|
对于简单的用例,附加请求参数对于特定提供者始终是相同的,可以直接将其添加到中
authorization-uri
。
例如,如果request参数的
prompt
值始终
consent
是provider
的值,则
可以
okta
简单地进行如下配置:
spring:
security:
oauth2:
client:
provider:
okta:
authorization-uri: https://dev-1234.oktapreview.com/oauth2/v1/authorize?prompt=consent
前面的示例显示了在标准参数之上添加自定义参数的常见用例。
或者,如果您的要求更高级,则可以通过简单地覆盖
OAuth2AuthorizationRequest.authorizationRequestUri
属性
来完全控制构建授权请求URI
。
下面的示例显示了该
customAuthorizationRequest()
方法与先前示例的不同形式,而是覆盖了该
OAuth2AuthorizationRequest.authorizationRequestUri
属性。
private OAuth2AuthorizationRequest customAuthorizationRequest(
OAuth2AuthorizationRequest authorizationRequest) {
String customAuthorizationRequestUri = UriComponentsBuilder
.fromUriString(authorizationRequest.getAuthorizationRequestUri())
.queryParam("prompt", "consent")
.build(true)
.toUriString();
return OAuth2AuthorizationRequest.from(authorizationRequest)
.authorizationRequestUri(customAuthorizationRequestUri)
.build();
}
存储授权请求
该
AuthorizationRequestRepository
负责的持久性
OAuth2AuthorizationRequest
从授权请求发起到授权响应接收的时间(回调)的时间。
将
OAuth2AuthorizationRequest
被用来关联和验证授权响应。
|
的默认实现
AuthorizationRequestRepository
是
HttpSessionOAuth2AuthorizationRequestRepository
,将实现存储在
OAuth2AuthorizationRequest
中
HttpSession
。
如果您具有的自定义实现
AuthorizationRequestRepository
,则可以按以下示例所示进行配置:
@EnableWebSecurity
public class OAuth2ClientSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.oauth2Client(oauth2Client ->
oauth2Client
.authorizationCodeGrant(authorizationCodeGrant ->
authorizationCodeGrant
.authorizationRequestRepository(this.authorizationRequestRepository())
...
)
);
}
}
请求访问令牌
| 请参阅 访问令牌请求/响应 协议流程以获取授权码。 |
OAuth2AccessTokenResponseClient
授权代码授予
的默认实现
是
DefaultAuthorizationCodeTokenResponseClient
,它使用
RestOperations
来在授权服务器的令牌端点处为访问令牌交换授权代码。
的
DefaultAuthorizationCodeTokenResponseClient
,因为它允许您自定义的令牌响应的令牌请求和/或装卸后的前处理非常灵活。
自定义访问令牌请求
如果您需要自定义令牌请求的预处理,则可以提供
DefaultAuthorizationCodeTokenResponseClient.setRequestEntityConverter()
一个custom
Converter<OAuth2AuthorizationCodeGrantRequest, RequestEntity<?>>
。
默认实现
OAuth2AuthorizationCodeGrantRequestEntityConverter
构建
RequestEntity
标准
OAuth 2.0访问令牌请求的表示
。
但是,提供一个custom
Converter
,将允许您扩展标准Token Request并添加自定义参数。
定制
Converter
必须返回
RequestEntity
预期的OAuth 2.0提供程序可以理解的OAuth 2.0访问令牌请求
的有效
表示形式。
|
自定义访问令牌响应
另一方面,如果您需要自定义令牌响应的后处理,则需要提供
DefaultAuthorizationCodeTokenResponseClient.setRestOperations()
一个自定义的configure
RestOperations
。
默认
RestOperations
配置如下:
RestTemplate restTemplate = new RestTemplate(Arrays.asList(
new FormHttpMessageConverter(),
new OAuth2AccessTokenResponseHttpMessageConverter()));
restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
FormHttpMessageConverter
发送MAuth 2.0访问令牌请求时需要使用
Spring MVC
。
|
OAuth2AccessTokenResponseHttpMessageConverter
是
HttpMessageConverter
OAuth 2.0访问令牌响应的。
您可以提供
用于将OAuth 2.0访问令牌响应参数转换
为
OAuth2AccessTokenResponseHttpMessageConverter.setTokenResponseConverter()
的自定义
。
Converter<Map<String, String>, OAuth2AccessTokenResponse>
OAuth2AccessTokenResponse
OAuth2ErrorResponseErrorHandler
是
ResponseErrorHandler
可以处理OAuth 2.0错误的,例如。
400错误的请求。
它使用
OAuth2ErrorHttpMessageConverter
将OAuth 2.0错误参数转换为
OAuth2Error
。
无论您是自定义
DefaultAuthorizationCodeTokenResponseClient
还是提供自己的实现
OAuth2AccessTokenResponseClient
,都需要按以下示例所示进行配置:
@EnableWebSecurity
public class OAuth2ClientSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.oauth2Client(oauth2Client ->
oauth2Client
.authorizationCodeGrant(authorizationCodeGrant ->
authorizationCodeGrant
.accessTokenResponseClient(this.accessTokenResponseClient())
...
)
);
}
}
刷新令牌
| 有关 刷新令牌的 更多详细信息,请 ReferenceOAuth 2.0授权框架 。 |
刷新访问令牌
| 请参阅 访问令牌请求/响应 协议流程以获取刷新令牌授权。 |
OAuth2AccessTokenResponseClient
刷新令牌授予
的默认实现
是
DefaultRefreshTokenTokenResponseClient
,
RestOperations
在授权服务器的令牌端点刷新访问令牌时使用。
的
DefaultRefreshTokenTokenResponseClient
,因为它允许您自定义的令牌响应的令牌请求和/或装卸后的前处理非常灵活。
自定义访问令牌请求
如果您需要自定义令牌请求的预处理,则可以提供
DefaultRefreshTokenTokenResponseClient.setRequestEntityConverter()
一个custom
Converter<OAuth2RefreshTokenGrantRequest, RequestEntity<?>>
。
默认实现
OAuth2RefreshTokenGrantRequestEntityConverter
构建
RequestEntity
标准
OAuth 2.0访问令牌请求的表示
。
但是,提供一个custom
Converter
,将允许您扩展标准Token Request并添加自定义参数。
定制
Converter
必须返回
RequestEntity
预期的OAuth 2.0提供程序可以理解的OAuth 2.0访问令牌请求
的有效
表示形式。
|
自定义访问令牌响应
另一方面,如果您需要自定义令牌响应的后处理,则需要提供
DefaultRefreshTokenTokenResponseClient.setRestOperations()
一个自定义的configure
RestOperations
。
默认
RestOperations
配置如下:
RestTemplate restTemplate = new RestTemplate(Arrays.asList(
new FormHttpMessageConverter(),
new OAuth2AccessTokenResponseHttpMessageConverter()));
restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
FormHttpMessageConverter
发送MAuth 2.0访问令牌请求时需要使用
Spring MVC
。
|
OAuth2AccessTokenResponseHttpMessageConverter
是
HttpMessageConverter
OAuth 2.0访问令牌响应的。
您可以提供
用于将OAuth 2.0访问令牌响应参数转换
为
OAuth2AccessTokenResponseHttpMessageConverter.setTokenResponseConverter()
的自定义
。
Converter<Map<String, String>, OAuth2AccessTokenResponse>
OAuth2AccessTokenResponse
OAuth2ErrorResponseErrorHandler
是
ResponseErrorHandler
可以处理OAuth 2.0错误的,例如。
400错误的请求。
它使用
OAuth2ErrorHttpMessageConverter
将OAuth 2.0错误参数转换为
OAuth2Error
。
无论您是自定义
DefaultRefreshTokenTokenResponseClient
还是提供自己的实现
OAuth2AccessTokenResponseClient
,都需要按以下示例所示进行配置:
// Customize
OAuth2AccessTokenResponseClient<OAuth2RefreshTokenGrantRequest> refreshTokenTokenResponseClient = ...
OAuth2AuthorizedClientProvider authorizedClientProvider =
OAuth2AuthorizedClientProviderBuilder.builder()
.authorizationCode()
.refreshToken(configurer -> configurer.accessTokenResponseClient(refreshTokenTokenResponseClient))
.build();
...
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
OAuth2AuthorizedClientProviderBuilder.builder().refreshToken()
配置
RefreshTokenOAuth2AuthorizedClientProvider
,这是
OAuth2AuthorizedClientProvider
“刷新令牌”授予的实现。
|
该
OAuth2RefreshToken
可任选的访问令牌响应返还
authorization_code
和
password
补助的类型。
如果
OAuth2AuthorizedClient.getRefreshToken()
可用且
OAuth2AuthorizedClient.getAccessToken()
过期,则它将自动刷新
RefreshTokenOAuth2AuthorizedClientProvider
。
客户凭证
| 有关“ 客户端证书” 授予的 更多详细信息,请 ReferenceOAuth 2.0授权框架 。 |
请求访问令牌
| 请参阅“ 访问令牌请求/响应” 协议流以获取“客户端证书”授予。 |
OAuth2AccessTokenResponseClient
客户端证书授予
的默认实现
是
DefaultClientCredentialsTokenResponseClient
,
RestOperations
在授权服务器的令牌端点请求访问令牌时使用。
的
DefaultClientCredentialsTokenResponseClient
,因为它允许您自定义的令牌响应的令牌请求和/或装卸后的前处理非常灵活。
自定义访问令牌请求
如果您需要自定义令牌请求的预处理,则可以提供
DefaultClientCredentialsTokenResponseClient.setRequestEntityConverter()
一个custom
Converter<OAuth2ClientCredentialsGrantRequest, RequestEntity<?>>
。
默认实现
OAuth2ClientCredentialsGrantRequestEntityConverter
构建
RequestEntity
标准
OAuth 2.0访问令牌请求的表示
。
但是,提供一个custom
Converter
,将允许您扩展标准Token Request并添加自定义参数。
定制
Converter
必须返回
RequestEntity
预期的OAuth 2.0提供程序可以理解的OAuth 2.0访问令牌请求
的有效
表示形式。
|
自定义访问令牌响应
另一方面,如果您需要自定义令牌响应的后处理,则需要提供
DefaultClientCredentialsTokenResponseClient.setRestOperations()
一个自定义的configure
RestOperations
。
默认
RestOperations
配置如下:
RestTemplate restTemplate = new RestTemplate(Arrays.asList(
new FormHttpMessageConverter(),
new OAuth2AccessTokenResponseHttpMessageConverter()));
restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
FormHttpMessageConverter
发送MAuth 2.0访问令牌请求时需要使用
Spring MVC
。
|
OAuth2AccessTokenResponseHttpMessageConverter
是
HttpMessageConverter
OAuth 2.0访问令牌响应的。
您可以提供
用于将OAuth 2.0访问令牌响应参数转换
为
OAuth2AccessTokenResponseHttpMessageConverter.setTokenResponseConverter()
的自定义
。
Converter<Map<String, String>, OAuth2AccessTokenResponse>
OAuth2AccessTokenResponse
OAuth2ErrorResponseErrorHandler
是
ResponseErrorHandler
可以处理OAuth 2.0错误的,例如。
400错误的请求。
它使用
OAuth2ErrorHttpMessageConverter
将OAuth 2.0错误参数转换为
OAuth2Error
。
无论您是自定义
DefaultClientCredentialsTokenResponseClient
还是提供自己的实现
OAuth2AccessTokenResponseClient
,都需要按以下示例所示进行配置:
// Customize
OAuth2AccessTokenResponseClient<OAuth2ClientCredentialsGrantRequest> clientCredentialsTokenResponseClient = ...
OAuth2AuthorizedClientProvider authorizedClientProvider =
OAuth2AuthorizedClientProviderBuilder.builder()
.clientCredentials(configurer -> configurer.accessTokenResponseClient(clientCredentialsTokenResponseClient))
.build();
...
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
OAuth2AuthorizedClientProviderBuilder.builder().clientCredentials()
配置
ClientCredentialsOAuth2AuthorizedClientProvider
,这是
OAuth2AuthorizedClientProvider
“客户端凭据”授予的的实现。
|
使用访问令牌
为OAuth 2.0客户端注册提供以下Spring Boot 2.x属性:
spring:
security:
oauth2:
client:
registration:
okta:
client-id: okta-client-id
client-secret: okta-client-secret
authorization-grant-type: client_credentials
scope: read, write
provider:
okta:
token-uri: https://dev-1234.oktapreview.com/oauth2/v1/token
......和
OAuth2AuthorizedClientManager @Bean
:
@Bean
public OAuth2AuthorizedClientManager authorizedClientManager(
ClientRegistrationRepository clientRegistrationRepository,
OAuth2AuthorizedClientRepository authorizedClientRepository) {
OAuth2AuthorizedClientProvider authorizedClientProvider =
OAuth2AuthorizedClientProviderBuilder.builder()
.clientCredentials()
.build();
DefaultOAuth2AuthorizedClientManager authorizedClientManager =
new DefaultOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientRepository);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
return authorizedClientManager;
}
您可以获得
OAuth2AccessToken
以下内容:
@Controller
public class OAuth2ClientController {
@Autowired
private OAuth2AuthorizedClientManager authorizedClientManager;
@GetMapping("/")
public String index(Authentication authentication,
HttpServletRequest servletRequest,
HttpServletResponse servletResponse) {
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId("okta")
.principal(authentication)
.attributes(attrs -> {
attrs.put(HttpServletRequest.class.getName(), servletRequest);
attrs.put(HttpServletResponse.class.getName(), servletResponse);
})
.build();
OAuth2AuthorizedClient authorizedClient = this.authorizedClientManager.authorize(authorizeRequest);
OAuth2AccessToken accessToken = authorizedClient.getAccessToken();
...
return "index";
}
}
HttpServletRequest
和
HttpServletResponse
均为OPTIONAL属性。
如果未提供,则默认
ServletRequestAttributes
使用
RequestContextHolder.getRequestAttributes()
。
|
资源所有者密码凭证
| 有关 资源所有者密码凭据 授予的 更多详细信息,请 ReferenceOAuth 2.0授权框架 。 |
请求访问令牌
| 请参阅“ 访问令牌请求/响应” 协议流,以获取“资源所有者密码凭据”授予。 |
“
OAuth2AccessTokenResponseClient
资源所有者密码凭据”授予
的默认实现
是
DefaultPasswordTokenResponseClient
,
RestOperations
在授权服务器的令牌端点请求访问令牌时使用。
的
DefaultPasswordTokenResponseClient
,因为它允许您自定义的令牌响应的令牌请求和/或装卸后的前处理非常灵活。
自定义访问令牌请求
如果您需要自定义令牌请求的预处理,则可以提供
DefaultPasswordTokenResponseClient.setRequestEntityConverter()
一个custom
Converter<OAuth2PasswordGrantRequest, RequestEntity<?>>
。
默认实现
OAuth2PasswordGrantRequestEntityConverter
构建
RequestEntity
标准
OAuth 2.0访问令牌请求的表示
。
但是,提供一个custom
Converter
,将允许您扩展标准Token Request并添加自定义参数。
定制
Converter
必须返回
RequestEntity
预期的OAuth 2.0提供程序可以理解的OAuth 2.0访问令牌请求
的有效
表示形式。
|
自定义访问令牌响应
另一方面,如果您需要自定义令牌响应的后处理,则需要提供
DefaultPasswordTokenResponseClient.setRestOperations()
一个自定义的configure
RestOperations
。
默认
RestOperations
配置如下:
RestTemplate restTemplate = new RestTemplate(Arrays.asList(
new FormHttpMessageConverter(),
new OAuth2AccessTokenResponseHttpMessageConverter()));
restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
FormHttpMessageConverter
发送MAuth 2.0访问令牌请求时需要使用
Spring MVC
。
|
OAuth2AccessTokenResponseHttpMessageConverter
是
HttpMessageConverter
OAuth 2.0访问令牌响应的。
您可以提供
用于将OAuth 2.0访问令牌响应参数转换
为
OAuth2AccessTokenResponseHttpMessageConverter.setTokenResponseConverter()
的自定义
。
Converter<Map<String, String>, OAuth2AccessTokenResponse>
OAuth2AccessTokenResponse
OAuth2ErrorResponseErrorHandler
是
ResponseErrorHandler
可以处理OAuth 2.0错误的,例如。
400错误的请求。
它使用
OAuth2ErrorHttpMessageConverter
将OAuth 2.0错误参数转换为
OAuth2Error
。
无论您是自定义
DefaultPasswordTokenResponseClient
还是提供自己的实现
OAuth2AccessTokenResponseClient
,都需要按以下示例所示进行配置:
// Customize
OAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> passwordTokenResponseClient = ...
OAuth2AuthorizedClientProvider authorizedClientProvider =
OAuth2AuthorizedClientProviderBuilder.builder()
.password(configurer -> configurer.accessTokenResponseClient(passwordTokenResponseClient))
.refreshToken()
.build();
...
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
OAuth2AuthorizedClientProviderBuilder.builder().password()
配置
PasswordOAuth2AuthorizedClientProvider
,这是
OAuth2AuthorizedClientProvider
“资源所有者密码凭据”授予的的实现。
|
使用访问令牌
为OAuth 2.0客户端注册提供以下Spring Boot 2.x属性:
spring:
security:
oauth2:
client:
registration:
okta:
client-id: okta-client-id
client-secret: okta-client-secret
authorization-grant-type: password
scope: read, write
provider:
okta:
token-uri: https://dev-1234.oktapreview.com/oauth2/v1/token
......和
OAuth2AuthorizedClientManager @Bean
:
@Bean
public OAuth2AuthorizedClientManager authorizedClientManager(
ClientRegistrationRepository clientRegistrationRepository,
OAuth2AuthorizedClientRepository authorizedClientRepository) {
OAuth2AuthorizedClientProvider authorizedClientProvider =
OAuth2AuthorizedClientProviderBuilder.builder()
.password()
.refreshToken()
.build();
DefaultOAuth2AuthorizedClientManager authorizedClientManager =
new DefaultOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientRepository);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
// Assuming the `username` and `password` are supplied as `HttpServletRequest` parameters,
// map the `HttpServletRequest` parameters to `OAuth2AuthorizationContext.getAttributes()`
authorizedClientManager.setContextAttributesMapper(contextAttributesMapper());
return authorizedClientManager;
}
private Function<OAuth2AuthorizeRequest, Map<String, Object>> contextAttributesMapper() {
return authorizeRequest -> {
Map<String, Object> contextAttributes = Collections.emptyMap();
HttpServletRequest servletRequest = authorizeRequest.getAttribute(HttpServletRequest.class.getName());
String username = servletRequest.getParameter(OAuth2ParameterNames.USERNAME);
String password = servletRequest.getParameter(OAuth2ParameterNames.PASSWORD);
if (StringUtils.hasText(username) && StringUtils.hasText(password)) {
contextAttributes = new HashMap<>();
// `PasswordOAuth2AuthorizedClientProvider` requires both attributes
contextAttributes.put(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, username);
contextAttributes.put(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, password);
}
return contextAttributes;
};
}
您可以获得
OAuth2AccessToken
以下内容:
@Controller
public class OAuth2ClientController {
@Autowired
private OAuth2AuthorizedClientManager authorizedClientManager;
@GetMapping("/")
public String index(Authentication authentication,
HttpServletRequest servletRequest,
HttpServletResponse servletResponse) {
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId("okta")
.principal(authentication)
.attributes(attrs -> {
attrs.put(HttpServletRequest.class.getName(), servletRequest);
attrs.put(HttpServletResponse.class.getName(), servletResponse);
})
.build();
OAuth2AuthorizedClient authorizedClient = this.authorizedClientManager.authorize(authorizeRequest);
OAuth2AccessToken accessToken = authorizedClient.getAccessToken();
...
return "index";
}
}
HttpServletRequest
和
HttpServletResponse
均为OPTIONAL属性。
如果未提供,则默认
ServletRequestAttributes
使用
RequestContextHolder.getRequestAttributes()
。
|
11.2.3. 附加的功能
解决授权客户
所述
@RegisteredOAuth2AuthorizedClient
注释提供解决方法参数,以类型的参数值的能力
OAuth2AuthorizedClient
。
与
OAuth2AuthorizedClient
使用
OAuth2AuthorizedClientManager
或
相比,这是一种方便的选择
OAuth2AuthorizedClientService
。
@Controller
public class OAuth2ClientController {
@GetMapping("/")
public String index(@RegisteredOAuth2AuthorizedClient("okta") OAuth2AuthorizedClient authorizedClient) {
OAuth2AccessToken accessToken = authorizedClient.getAccessToken();
...
return "index";
}
}
该
@RegisteredOAuth2AuthorizedClient
注释被处理
OAuth2AuthorizedClientArgumentResolver
,其中直接使用的
OAuth2AuthorizedClientManager
,因此继承了它的功能。
11.2.4. Servlet环境的WebClient集成
要OAuth 2.0客户端支持,集成
WebClient
使用
ExchangeFilterFunction
。
该
ServletOAuth2AuthorizedClientExchangeFilterFunction
规定通过使用请求保护的资源的简单机制
OAuth2AuthorizedClient
,并包括相关
OAuth2AccessToken
的承载令牌。
它直接使用
OAuth2AuthorizedClientManager
,因此继承了以下功能:
-
一个
OAuth2AccessToken如果客户尚未获得授权将被请求。-
authorization_code-触发授权请求重定向以启动流程 -
client_credentials-直接从令牌端点获取访问令牌 -
password-直接从令牌端点获取访问令牌
-
-
如果
OAuth2AccessToken过期,则如果OAuth2AuthorizedClientProvider可以执行授权 ,将刷新(或更新)
以下代码显示了如何配置
WebClient
OAuth 2.0客户端支持
的示例
:
@Bean
WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) {
ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client =
new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
return WebClient.builder()
.apply(oauth2Client.oauth2Configuration())
.build();
}
提供授权客户
所述
ServletOAuth2AuthorizedClientExchangeFilterFunction
确定客户端通过解析使用(对于请求)
OAuth2AuthorizedClient
从
ClientRequest.attributes()
(请求属性)。
以下代码显示了如何将设置
OAuth2AuthorizedClient
为请求属性:
@GetMapping("/")
public String index(@RegisteredOAuth2AuthorizedClient("okta") OAuth2AuthorizedClient authorizedClient) {
String resourceUri = ...
String body = webClient
.get()
.uri(resourceUri)
.attributes(oauth2AuthorizedClient(authorizedClient)) (1)
.retrieve()
.bodyToMono(String.class)
.block();
...
return "index";
}
| 1个 | oauth2AuthorizedClient()
是中的一种
static
方法
ServletOAuth2AuthorizedClientExchangeFilterFunction
。
|
以下代码显示了如何将设置
ClientRegistration.getRegistrationId()
为请求属性:
@GetMapping("/")
public String index() {
String resourceUri = ...
String body = webClient
.get()
.uri(resourceUri)
.attributes(clientRegistrationId("okta")) (1)
.retrieve()
.bodyToMono(String.class)
.block();
...
return "index";
}
| 1个 | clientRegistrationId()
是中的一种
static
方法
ServletOAuth2AuthorizedClientExchangeFilterFunction
。
|
默认授权客户
如果都不
提供
OAuth2AuthorizedClient
或
ClientRegistration.getRegistrationId()
作为请求属性提供,则
ServletOAuth2AuthorizedClientExchangeFilterFunction
可以
根据其配置
确定
要使用
的
默认
客户端。
如果
setDefaultOAuth2AuthorizedClient(true)
已配置,并且用户已使用进行
HttpSecurity.oauth2Login()
了
身份验证
,则
OAuth2AccessToken
使用与当前
的
关联
OAuth2AuthenticationToken
。
以下代码显示了特定的配置:
@Bean
WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) {
ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client =
new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
oauth2Client.setDefaultOAuth2AuthorizedClient(true);
return WebClient.builder()
.apply(oauth2Client.oauth2Configuration())
.build();
}
| 建议谨慎使用此功能,因为所有HTTP请求都会收到访问令牌。 |
或者,如果
setDefaultClientRegistrationId("okta")
配置了有效的
ClientRegistration
,
则使用
与
OAuth2AccessToken
关联的
OAuth2AuthorizedClient
。
以下代码显示了特定的配置:
@Bean
WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) {
ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client =
new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
oauth2Client.setDefaultClientRegistrationId("okta");
return WebClient.builder()
.apply(oauth2Client.oauth2Configuration())
.build();
}
| 建议谨慎使用此功能,因为所有HTTP请求都会收到访问令牌。 |
11.3. OAuth 2.0资源服务器
Spring Security支持使用两种OAuth 2.0 承载令牌 形式保护端点 :
-
不透明令牌
在应用程序将其权限管理委派给 授权服务器 (例如Okta或Ping Identity)的 情况下,这很方便 。 资源服务器可以咨询该授权服务器以授权请求。
|
Spring Security存储库 中提供 了 JWT 和 不透明令牌的 工作样本 。 |
11.3.1. 依存关系
大多数资源服务器支持都收集到中
spring-security-oauth2-resource-server
。
但是,中支持对JWT进行解码和验证
spring-security-oauth2-jose
,这意味着这两者都是必需的,这样才能拥有支持JWT编码的承载令牌的工作资源服务器。
11.3.2. JWT的最小配置
使用 Spring Boot时 ,将应用程序配置为资源服务器包括两个基本步骤。 首先,包括所需的依赖关系,其次,指示授权服务器的位置。
指定授权服务器
在Spring Boot应用程序中,要指定要使用的授权服务器,只需执行以下操作:
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://idp.example.com/issuer
授权服务器将发出的JWT令牌声明中
https://idp.example.com/issuer
包含的值在
哪里
iss
。
资源服务器将使用此属性进行进一步的自我配置,发现授权服务器的公钥,然后验证传入的JWT。
要使用该
issuer-uri
属性,它也必须是真实的一个
https://idp.example.com/issuer/.well-known/openid-configuration
,
https://idp.example.com/.well-known/openid-configuration/issuer
或者
https://idp.example.com/.well-known/oauth-authorization-server/issuer
是授权服务器支持的端点。
此终结点称为
提供者配置
终结点或
授权服务器元数据
终结点。
|
就是这样!
启动期望
使用此属性和这些依赖关系时,资源服务器将自动配置自身以验证JWT编码的承载令牌。
它通过确定性的启动过程来实现:
-
点击提供者配置或授权服务器元数据终结点,处理该
jwks_url属性 的响应 -
配置验证策略以查询
jwks_url有效的公共密钥 -
配置验证策略以验证每个JWT的
iss主张https://idp.example.com。
此过程的结果是,授权服务器必须启动并接收请求,才能成功启动资源服务器。
| 如果在资源服务器查询授权服务器时授权服务器已关闭(给出适当的超时),则启动将失败。 |
运行时期望
应用程序启动后,资源服务器将尝试处理任何包含
Authorization: Bearer
标头的
请求
:
GET / HTTP/1.1
Authorization: Bearer some-token-value # Resource Server will process this
只要指示了此方案,资源服务器就会尝试根据Bearer Token规范处理请求。
给定格式正确的JWT,资源服务器将:
-
根据
jwks_url启动期间 从 端点 获取 并与JWT头匹配的 公钥来验证其签名 -
验证JWT
exp和nbf时间戳以及JWTiss声明,并且 -
将每个范围映射到具有前缀的授权
SCOPE_。
| 当授权服务器提供新的密钥时,Spring Security将自动旋转用于验证JWT令牌的密钥。 |
Authentication#getPrincipal
默认情况下,
生成的结果
是Spring Security
Jwt
对象,并且
如果存在的话
,
Authentication#getName
将映射到JWT的
sub
属性。
从这里,考虑跳到:
11.3.3. 直接指定授权服务器JWK设置Uri
如果授权服务器不支持任何配置端点,或者如果Resource Server必须能够独立于授权服务器启动,则
jwk-set-uri
也可以提供:
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://idp.example.com
jwk-set-uri: https://idp.example.com/.well-known/jwks.json
| JWK Set uri不是标准化的,但是通常可以在授权服务器的文档中找到 |
因此,资源服务器在启动时不会对授权服务器执行ping操作。
我们仍然指定,
issuer-uri
以便Resource Server仍然验证
iss
传入JWT的声明。
| 此属性也可以直接在 DSL 上提供 。 |
11.3.4. 覆盖或替换引导自动配置
@Bean
Spring Boot代表资源服务器生成
两个
。
第一个是
WebSecurityConfigurerAdapter
将应用程序配置为资源服务器的。
当包含时
spring-security-oauth2-jose
,它
WebSecurityConfigurerAdapter
看起来像:
protected void configure(HttpSecurity http) {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)
}
如果应用程序没有公开
WebSecurityConfigurerAdapter
bean,那么Spring Boot将公开上述默认
的
bean。
替换它就像在应用程序中公开Bean一样简单:
@EnableWebSecurity
public class MyCustomSecurityConfiguration extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) {
http
.authorizeRequests()
.mvcMatchers("/messages/**").hasAuthority("SCOPE_message:read")
.anyRequest().authenticated()
.and()
.oauth2ResourceServer()
.jwt()
.jwtAuthenticationConverter(myConverter());
}
}
以上要求以
message:read
开头的所有URL
的范围
/messages/
。
oauth2ResourceServer
DSL
上的方法
还将覆盖或替换自动配置。
例如,第二个
@Bean
Spring Boot创建的是一个
JwtDecoder
,它将
String
令牌
解码
为以下经过验证的实例
Jwt
:
@Bean
public JwtDecoder jwtDecoder() {
return JwtDecoders.fromIssuerLocation(issuerUri);
}
调用
JwtDecoders#fromIssuerLocation
是调用提供程序配置或授权服务器元数据终结点以派生JWK设置Uri的功能。
|
如果应用程序没有公开
JwtDecoder
bean,那么Spring Boot将公开上述默认
的
bean。
可以使用覆盖它的配置,也可以使用
jwkSetUri()
替换
它的配置
decoder()
。
使用
jwkSetUri()
授权服务器的JWK Set Uri可以配置 为配置属性 ,也可以在DSL中提供:
@EnableWebSecurity
public class DirectlyConfiguredJwkSetUri extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.oauth2ResourceServer()
.jwt()
.jwkSetUri("https://idp.example.com/.well-known/jwks.json");
}
}
使用
jwkSetUri()
优先于任何配置属性。
使用
decoder()
比
jwkSetUri()
现在
更强大的功能
decoder()
,它将完全替代以下的任何Boot自动配置
JwtDecoder
:
@EnableWebSecurity
public class DirectlyConfiguredJwtDecoder extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.oauth2ResourceServer()
.jwt()
.decoder(myCustomDecoder());
}
}
11.3.5. 配置可信算法
默认情况下,
NimbusJwtDecoder
因此资源服务器将仅使用来信任和验证令牌
RS256
。
您可以通过 Spring Boot , NimbusJwtDecoder构建器 或从 JWK Set响应中 对此进行自定义 。
通过Spring Boot
设置算法的最简单方法是作为属性:
spring:
security:
oauth2:
resourceserver:
jwt:
jws-algorithm: RS512
jwk-set-uri: https://idp.example.org/.well-known/jwks.json
使用生成器
但是,为了获得更大的功能,我们可以使用附带以下内容的构建器
NimbusJwtDecoder
:
@Bean
JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.fromJwkSetUri(this.jwkSetUri)
.jwsAlgorithm(RS512).build();
}
jwsAlgorithm
多次
调用
将配置
NimbusJwtDecoder
为信任多个算法,如下所示:
@Bean
JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.fromJwkSetUri(this.jwkSetUri)
.jwsAlgorithm(RS512).jwsAlgorithm(EC512).build();
}
或者,您可以致电
jwsAlgorithms
:
@Bean
JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.fromJwkSetUri(this.jwkSetUri)
.jwsAlgorithms(algorithms -> {
algorithms.add(RS512);
algorithms.add(EC512);
}).build();
}
来自JWK Set的回复
由于Spring Security的JWT支持基于Nimbus,因此您也可以使用其所有出色的功能。
例如,Nimbus的
JWSKeySelector
实现将基于JWK Set URI响应选择算法集。
您可以使用它生成
NimbusJwtDecoder
类似这样的内容:
@Bean
public JwtDecoder jwtDecoder() {
// makes a request to the JWK Set endpoint
JWSKeySelector<SecurityContext> jwsKeySelector =
JWSAlgorithmFamilyJWSKeySelector.fromJWKSetURL(this.jwkSetUrl);
DefaultJWTProcessor<SecurityContext> jwtProcessor =
new DefaultJWTProcessor<>();
jwtProcessor.setJWSKeySelector(jwsKeySelector);
return new NimbusJwtDecoder(jwtProcessor);
}
11.3.6. 信任单个非对称密钥
比使用JWK Set端点备份资源服务器更简单的方法是对RSA公钥进行硬编码。 可以通过 Spring Boot 或 使用Builder 提供公共密钥 。
通过Spring Boot
通过Spring Boot指定密钥非常简单。 密钥的位置可以这样指定:
spring:
security:
oauth2:
resourceserver:
jwt:
public-key-location: classpath:my-key.pub
或者,为了进行更复杂的查找,您可以对进行后处理
RsaKeyConversionServicePostProcessor
:
@Bean
BeanFactoryPostProcessor conversionServiceCustomizer() {
return beanFactory ->
beanFactory.getBean(RsaKeyConversionServicePostProcessor.class)
.setResourceLoader(new CustomResourceLoader());
}
指定密钥的位置:
key.location: hfds://my-key.pub
然后自动装配值:
@Value("${key.location}")
RSAPublicKey key;
11.3.7. 信任单个对称密钥
使用单个对称密钥也很简单。
您可以简单地加载
SecretKey
并使用适当的
NimbusJwtDecoder
构建器,如下所示:
@Bean
public JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withSecretKey(this.key).build();
}
11.3.8. 配置授权
从OAuth 2.0授权服务器发出的JWT通常具有
scope
或
scp
属性,指示已被授予的范围(或权限),例如:
{ …, "scope" : "messages contacts"}
在这种情况下,资源服务器将尝试将这些作用域强制为已授予权限的列表,并为每个作用域添加字符串“ SCOPE_”作为前缀。
这意味着为了保护具有从JWT派生的作用域的端点或方法,相应的表达式应包含以下前缀:
@EnableWebSecurity
public class DirectlyConfiguredJwkSetUri extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) {
http
.authorizeRequests(authorizeRequests -> authorizeRequests
.mvcMatchers("/contacts/**").hasAuthority("SCOPE_contacts")
.mvcMatchers("/messages/**").hasAuthority("SCOPE_messages")
.anyRequest().authenticated()
)
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
}
}
或类似地具有方法安全性:
@PreAuthorize("hasAuthority('SCOPE_messages')")
public List<Message> getMessages(...) {}
手动提取权限
但是,在许多情况下,此默认设置不足。
例如,某些授权服务器不使用该
scope
属性,而是拥有自己的自定义属性。
或者,在其他时候,资源服务器可能需要将属性或属性组成调整为内部化的权限。
为此,DSL公开
jwtAuthenticationConverter()
:
@EnableWebSecurity
public class DirectlyConfiguredJwkSetUri extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.oauth2ResourceServer()
.jwt()
.jwtAuthenticationConverter(grantedAuthoritiesExtractor());
}
}
Converter<Jwt, AbstractAuthenticationToken> grantedAuthoritiesExtractor() {
JwtAuthenticationConverter jwtAuthenticationConverter =
new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter
(new GrantedAuthoritiesExtractor());
return jwtAuthenticationConveter;
}
负责将a
Jwt
转换为
Authentication
。
作为其配置的一部分,我们可以提供一个附属转换器从去
Jwt
到
Collection
授予的权限的。
最终的转换器可能
GrantedAuthoritiesExtractor
如下所示:
static class GrantedAuthoritiesExtractor
implements Converter<Jwt, Collection<GrantedAuthority>> {
public Collection<GrantedAuthority> convert(Jwt jwt) {
Collection<String> authorities = (Collection<String>)
jwt.getClaims().get("mycustomclaim");
return authorities.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
}
}
为了获得更大的灵活性,DSL支持使用以下实现的任何类完全替代转换器
Converter<Jwt, AbstractAuthenticationToken>
:
static class CustomAuthenticationConverter implements Converter<Jwt, AbstractAuthenticationToken> {
public AbstractAuthenticationToken convert(Jwt jwt) {
return new CustomAuthenticationToken(jwt);
}
}
11.3.9。 配置验证
用
最少的春天启动配置
,指示授权服务器的发布者URI,资源服务器将默认为验证
iss
要求,以及在
exp
和
nbf
时间戳索赔。
在需要自定义验证的情况下,资源服务器附带两个标准验证器,并且也接受自定义
OAuth2TokenValidator
实例。
自定义时间戳验证
JWT通常具有有效期窗口,该窗口的开始在
nbf
权利要求中
指示,
而结束在
exp
权利要求中
指示
。
但是,每台服务器都会经历时钟漂移,这可能导致令牌在一个服务器上显得过期,而在另一台服务器上过期。 随着分布式系统中协作服务器数量的增加,这可能会导致某些实现上的胃口。
资源服务器用于
JwtTimestampValidator
验证令牌的有效性窗口,并且可以将其配置为
clockSkew
来缓解上述问题:
@Bean
JwtDecoder jwtDecoder() {
NimbusJwtDecoder jwtDecoder = (NimbusJwtDecoder)
JwtDecoders.fromIssuerLocation(issuerUri);
OAuth2TokenValidator<Jwt> withClockSkew = new DelegatingOAuth2TokenValidator<>(
new JwtTimestampValidator(Duration.ofSeconds(60)),
new IssuerValidator(issuerUri));
jwtDecoder.setJwtValidator(withClockSkew);
return jwtDecoder;
}
| 默认情况下,资源服务器将时钟偏差配置为30秒。 |
配置自定义验证器
aud
使用
OAuth2TokenValidator
API
为
索赔
添加支票
很简单
:
public class AudienceValidator implements OAuth2TokenValidator<Jwt> {
OAuth2Error error = new OAuth2Error("invalid_token", "The required audience is missing", null);
public OAuth2TokenValidatorResult validate(Jwt jwt) {
if (jwt.getAudience().contains("messaging")) {
return OAuth2TokenValidatorResult.success();
} else {
return OAuth2TokenValidatorResult.failure(error);
}
}
}
然后,要添加到资源服务器中,只需指定
JwtDecoder
实例即可:
@Bean
JwtDecoder jwtDecoder() {
NimbusJwtDecoder jwtDecoder = (NimbusJwtDecoder)
JwtDecoders.fromIssuerLocation(issuerUri);
OAuth2TokenValidator<Jwt> audienceValidator = new AudienceValidator();
OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer(issuerUri);
OAuth2TokenValidator<Jwt> withAudience = new DelegatingOAuth2TokenValidator<>(withIssuer, audienceValidator);
jwtDecoder.setJwtValidator(withAudience);
return jwtDecoder;
}
11.3.10。 配置声明集映射
Spring Security使用 Nimbus 库来解析JWT并验证其签名。 因此,Spring Security受制于Nimbus对每个字段值以及如何将每个字段值强制转换为Java类型的解释。
例如,由于Nimbus保持与Java 7兼容,因此它不
Instant
用于表示时间戳字段。
并且完全有可能使用其他库或进行JWT处理,这可能会自行做出需要调整的决策。
或者,很简单,出于特定于域的原因,资源服务器可能希望从JWT中添加或删除声明。
为此,Resource Server支持将JWT声明集映射为
MappedJwtClaimSetConverter
。
自定义单个索赔的转换
默认情况下,
MappedJwtClaimSetConverter
将尝试将声明强制转换为以下类型:
|
要求 |
Java类型 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
可以使用
MappedJwtClaimSetConverter.withDefaults
以下命令
配置单个声明的转换策略
:
@Bean
JwtDecoder jwtDecoder() {
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build();
MappedJwtClaimSetConverter converter = MappedJwtClaimSetConverter
.withDefaults(Collections.singletonMap("sub", this::lookupUserIdBySub));
jwtDecoder.setClaimSetConverter(converter);
return jwtDecoder;
}
这将保留所有默认值,但会覆盖的默认声明转换器
sub
。
添加索赔
MappedJwtClaimSetConverter
也可以用于添加自定义声明,例如以适应现有系统:
MappedJwtClaimSetConverter.withDefaults(Collections.singletonMap("custom", custom -> "value"));
删除索赔
使用相同的API删除声明也很简单:
MappedJwtClaimSetConverter.withDefaults(Collections.singletonMap("legacyclaim", legacy -> null));
重命名索赔
在更复杂的情况下,例如一次查询多个声明或重命名一个声明,Resource Server接受实现
Converter<Map<String, Object>, Map<String,Object>>
以下
任何类
:
public class UsernameSubClaimAdapter implements Converter<Map<String, Object>, Map<String, Object>> {
private final MappedJwtClaimSetConverter delegate =
MappedJwtClaimSetConverter.withDefaults(Collections.emptyMap());
public Map<String, Object> convert(Map<String, Object> claims) {
Map<String, Object> convertedClaims = this.delegate.convert(claims);
String username = (String) convertedClaims.get("user_name");
convertedClaims.put("sub", username);
return convertedClaims;
}
}
然后,可以像平常一样提供实例:
@Bean
JwtDecoder jwtDecoder() {
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build();
jwtDecoder.setClaimSetConverter(new UsernameSubClaimAdapter());
return jwtDecoder;
}
11.3.11. 配置超时
默认情况下,资源服务器使用30秒钟的连接和套接字超时来与授权服务器进行协调。
在某些情况下,这可能太短了。 此外,它不考虑退避和发现等更复杂的模式。
要调整资源服务器连接到授权服务器的方式,请
NimbusJwtDecoder
接受以下实例
RestOperations
:
@Bean
public JwtDecoder jwtDecoder(RestTemplateBuilder builder) {
RestOperations rest = builder
.setConnectionTimeout(60000)
.setReadTimeout(60000)
.build();
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).restOperations(rest).build();
return jwtDecoder;
}
11.3.12. 内省的最小配置
通常,不透明令牌可以通过 授权服务器托管 的 OAuth 2.0自省端点 进行验证 。 当需要撤销时,这可能很方便。
使用 Spring Boot时 ,将应用程序配置为使用自省的资源服务器包括两个基本步骤。 首先,包括所需的依赖性,其次,指示自省端点详细信息。
指定授权服务器
要指定自省端点的位置,只需执行以下操作:
security:
oauth2:
resourceserver:
opaque-token:
introspection-uri: https://idp.example.com/introspect
client-id: client
client-secret: secret
https://idp.example.com/introspect
授权服务器托管的自省端点
在哪里
,
client-id
并且
client-secret
是到达该端点所需的凭据。
资源服务器将使用这些属性进一步进行自我配置,并随后验证传入的JWT。
| 使用自省时,授权服务器的字眼就是法律。 如果授权服务器响应令牌是有效的,那么令牌是有效的。 |
就是这样!
运行时期望
应用程序启动后,资源服务器将尝试处理任何包含
Authorization: Bearer
标头的
请求
:
GET / HTTP/1.1
Authorization: Bearer some-token-value # Resource Server will process this
只要指示了此方案,资源服务器就会尝试根据Bearer Token规范处理请求。
给定一个不透明的令牌,资源服务器将
-
使用提供的凭据和令牌查询提供的自省端点
-
检查响应的
{ 'active' : true }属性 -
将每个范围映射到具有前缀的授权
SCOPE_
Authentication#getPrincipal
默认情况下,
生成的结果
是Spring Security
OAuth2AuthenticatedPrincipal
对象,并且
如果存在
Authentication#getName
令牌
sub
,则
映射到令牌的
属性。
从这里,您可能要跳转到:
11.3.13. 查找身份验证后的属性
令牌通过身份验证后,将在
BearerTokenAuthentication
中设置
的实例
SecurityContext
。
这意味着
在您的配置中
@Controller
使用
@EnableWebMvc
该
方法时
,该
方法
可用
:
@GetMapping("/foo")
public String foo(BearerTokenAuthentication authentication) {
return authentication.getTokenAttributes().get("sub") + " is the subject";
}
由于
BearerTokenAuthentication
拥有
OAuth2AuthenticatedPrincipal
,这也意味着它也可用于控制器方法:
@GetMapping("/foo")
public String foo(@AuthenticationPrincipal OAuth2AuthenticatedPrincipal principal) {
return principal.getAttribute("sub") + " is the subject";
}
11.3.14. 覆盖或替换引导自动配置
@Bean
Spring Boot代表资源服务器生成
两个
。
第一个是
WebSecurityConfigurerAdapter
将应用程序配置为资源服务器的。
使用不透明令牌时,如下
WebSecurityConfigurerAdapter
所示:
protected void configure(HttpSecurity http) {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::opaqueToken)
}
如果应用程序没有公开
WebSecurityConfigurerAdapter
bean,那么Spring Boot将公开上述默认
的
bean。
替换它就像在应用程序中公开Bean一样简单:
@EnableWebSecurity
public class MyCustomSecurityConfiguration extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) {
http
.authorizeRequests()
.mvcMatchers("/messages/**").hasAuthority("SCOPE_message:read")
.anyRequest().authenticated()
.and()
.oauth2ResourceServer()
.opaqueToken()
.introspector(myIntrospector());
}
}
以上要求以
message:read
开头的所有URL
的范围
/messages/
。
oauth2ResourceServer
DSL
上的方法
还将覆盖或替换自动配置。
例如,第二个
@Bean
Spring Boot创建的是一个
OpaqueTokenIntrospector
,它将
String
令牌
解码
为以下经过验证的实例
OAuth2AuthenticatedPrincipal
:
@Bean
public OpaqueTokenIntrospector introspector() {
return new NimbusOpaqueTokenIntrospector(introspectionUri, clientId, clientSecret);
}
如果应用程序没有公开
OpaqueTokenIntrospector
bean,那么Spring Boot将公开上述默认
的
bean。
可以使用
introspectionUri()
和
覆盖其配置,也可以使用
introspectionClientCredentials()
替换
它的配置
introspector()
。
使用
introspectionUri()
授权服务器的Introspection Uri可以配置 为配置属性 ,也可以在DSL中提供:
@EnableWebSecurity
public class DirectlyConfiguredIntrospectionUri extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.oauth2ResourceServer()
.opaqueToken()
.introspectionUri("https://idp.example.com/introspect")
.introspectionClientCredentials("client", "secret");
}
}
使用
introspectionUri()
优先于任何配置属性。
使用
introspector()
比
introspectionUri()
现在
更强大的功能
introspector()
,它将完全替代以下的任何Boot自动配置
OpaqueTokenIntrospector
:
@EnableWebSecurity
public class DirectlyConfiguredIntrospector extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.oauth2ResourceServer()
.opaqueToken()
.introspector(myCustomIntrospector());
}
}
11.3.15. 配置授权
OAuth 2.0自省端点通常会返回一个
scope
属性,指示该属性已被授予的范围(或权限),例如:
{ …, "scope" : "messages contacts"}
在这种情况下,资源服务器将尝试将这些作用域强制为已授予权限的列表,并为每个作用域添加字符串“ SCOPE_”作为前缀。
这意味着要保护具有不透明令牌派生范围的端点或方法,相应的表达式应包含以下前缀:
@EnableWebSecurity
public class MappedAuthorities extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) {
http
.authorizeRequests(authorizeRequests -> authorizeRequests
.mvcMatchers("/contacts/**").hasAuthority("SCOPE_contacts")
.mvcMatchers("/messages/**").hasAuthority("SCOPE_messages")
.anyRequest().authenticated()
)
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::opaqueToken);
}
}
或类似地具有方法安全性:
@PreAuthorize("hasAuthority('SCOPE_messages')")
public List<Message> getMessages(...) {}
手动提取权限
默认情况下,不透明令牌支持将从自省响应中提取范围声明,并将其解析为单个
GrantedAuthority
实例。
例如,如果自省响应为:
{
"active" : true,
"scope" : "message:read message:write"
}
然后,资源服务器将生成
Authentication
具有两个权限的,一个用于
message:read
,另一个用于
message:write
。
当然,这可以使用
OpaqueTokenIntrospector
查看属性集并以自己的方式进行转换
的自定义项进行自定义
:
public class CustomAuthoritiesOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
private OpaqueTokenIntrospector delegate =
new NimbusOpaqueTokenIntrospector("https://idp.example.org/introspect", "client", "secret");
public OAuth2AuthenticatedPrincipal introspect(String token) {
OAuth2AuthenticatedPrincipal principal = this.delegate.introspect(token);
return new DefaultOAuth2AuthenticatedPrincipal(
principal.getName(), principal.getAttributes(), extractAuthorities(principal));
}
private Collection<GrantedAuthority> extractAuthorities(OAuth2AuthenticatedPrincipal principal) {
List<String> scopes = principal.getAttribute(OAuth2IntrospectionClaimNames.SCOPE);
return scopes.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
}
}
此后,可以通过将其公开为来简单地配置此自定义内省器
@Bean
:
@Bean
public OpaqueTokenIntrospector introspector() {
return new CustomAuthoritiesOpaqueTokenIntrospector();
}
11.3.16. 配置超时
默认情况下,资源服务器使用30秒钟的连接和套接字超时来与授权服务器进行协调。
在某些情况下,这可能太短了。 此外,它不考虑退避和发现等更复杂的模式。
要调整资源服务器连接到授权服务器的方式,请
NimbusOpaqueTokenIntrospector
接受以下实例
RestOperations
:
@Bean
public OpaqueTokenIntrospector introspector(RestTemplateBuilder builder) {
RestOperations rest = builder
.basicAuthentication(clientId, clientSecret)
.setConnectionTimeout(60000)
.setReadTimeout(60000)
.build();
return new NimbusOpaqueTokenIntrospector(introspectionUri, rest);
}
11.3.17. 对JWT使用自省
一个常见的问题是内省是否与JWT兼容。 Spring Security的Opaque令牌支持被设计为不关心令牌的格式-它将很高兴将任何令牌传递给提供的自省端点。
因此,假设您有一个要求,如果JWT被吊销,则要求您在每个请求中与授权服务器进行核对。
即使您为令牌使用JWT格式,您的验证方法也是自省的,这意味着您想要执行以下操作:
spring:
security:
oauth2:
resourceserver:
opaque-token:
introspection-uri: https://idp.example.org/introspection
client-id: client
client-secret: secret
在这种情况下,结果
Authentication
将是
BearerTokenAuthentication
。
对应项中的任何属性
OAuth2AuthenticatedPrincipal
将是自省端点返回的
任何属性
。
但是,可以说,奇怪的是,自省端点仅返回令牌是否处于活动状态。 怎么办?
在这种情况下,您可以创建一个
OpaqueTokenIntrospector
仍会命中端点
的自定义
,然后更新返回的主体以将JWT声明作为属性:
public class JwtOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
private OpaqueTokenIntrospector delegate =
new NimbusOpaqueTokenIntrospector("https://idp.example.org/introspect", "client", "secret");
private JwtDecoder jwtDecoder = new NimbusJwtDecoder(new ParseOnlyJWTProcessor());
public OAuth2AuthenticatedPrincipal introspect(String token) {
OAuth2AuthenticatedPrincipal principal = this.delegate.introspect(token);
try {
Jwt jwt = this.jwtDecoder.decode(token);
return new DefaultOAuth2AuthenticatedPrincipal(jwt.getClaims(), NO_AUTHORITIES);
} catch (JwtException e) {
throw new OAuth2IntrospectionException(e);
}
}
private static class ParseOnlyJWTProcessor extends DefaultJWTProcessor<SecurityContext> {
JWTClaimsSet process(SignedJWT jwt, SecurityContext context)
throws JOSEException {
return jwt.getJWTClaimSet();
}
}
}
此后,可以通过将其公开为来简单地配置此自定义内省器
@Bean
:
@Bean
public OpaqueTokenIntrospector introspector() {
return new JwtOpaqueTokenIntropsector();
}
11.3.18.
呼叫
/userinfo
端点
一般而言,资源服务器并不关心基础用户,而是关心已授予的权限。
就是说,有时将授权声明绑定到用户可能很有价值。
如果一个应用程序也正在使用
spring-security-oauth2-client
,并且已经设置了适当的设置
ClientRegistrationRepository
,那么使用定制就很简单了
OpaqueTokenIntrospector
。
下面的实现实现了三件事:
-
委托自省端点确认令牌的有效性
-
查找与
/userinfo端点 关联的适当的客户端注册 -
调用并返回
/userinfo端点 的响应
public class UserInfoOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
private final OpaqueTokenIntrospector delegate =
new NimbusOpaqueTokenIntrospector("https://idp.example.org/introspect", "client", "secret");
private final OAuth2UserService oauth2UserService = new DefaultOAuth2UserService();
private final ClientRegistrationRepository repository;
// ... constructor
@Override
public OAuth2AuthenticatedPrincipal introspect(String token) {
OAuth2AuthenticatedPrincipal authorized = this.delegate.introspect(token);
Instant issuedAt = authorized.getAttribute(ISSUED_AT);
Instant expiresAt = authorized.getAttribute(EXPIRES_AT);
ClientRegistration clientRegistration = this.repository.findByRegistrationId("registration-id");
OAuth2AccessToken token = new OAuth2AccessToken(BEARER, token, issuedAt, expiresAt);
OAuth2UserRequest oauth2UserRequest = new OAuth2UserRequest(clientRegistration, token);
return this.oauth2UserService.loadUser(oauth2UserRequest);
}
}
如果您不使用
spring-security-oauth2-client
,它仍然非常简单。
您只需要
/userinfo
使用您自己的实例
调用即可
WebClient
:
public class UserInfoOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
private final OpaqueTokenIntrospector delegate =
new NimbusOpaqueTokenIntrospector("https://idp.example.org/introspect", "client", "secret");
private final WebClient rest = WebClient.create();
@Override
public OAuth2AuthenticatedPrincipal introspect(String token) {
OAuth2AuthenticatedPrincipal authorized = this.delegate.introspect(token);
return makeUserInfoRequest(authorized);
}
}
无论哪种方式,创建了之后
OpaqueTokenIntrospector
,您都应该将其发布为,
@Bean
以覆盖默认值:
@Bean
OpaqueTokenIntrospector introspector() {
return new UserInfoOpaqueTokenIntrospector(...);
}
到目前为止,我们只看了最基本的身份验证配置。 让我们看一下配置身份验证的一些高级选项。
11.3.19。 承载令牌解析
默认情况下,资源服务器在
Authorization
标头中
查找承载令牌
。
但是,可以通过两种方式进行自定义。
11.3.20。 承载令牌传播
现在您已经拥有了一个承载令牌,将它传递给下游服务可能会很方便。
使用
ServletBearerExchangeFilterFunction
,这
非常简单
,您可以在以下示例中看到它:
@Bean
public WebClient rest() {
return WebClient.builder()
.filter(new ServletBearerExchangeFilterFunction())
.build();
}
当上述
WebClient
方法用于执行请求时,Spring Security将查找当前信息
Authentication
并提取所有
AbstractOAuth2Token
凭证。
然后,它将在
Authorization
标头中
传播该令牌
。
例如:
this.rest.get()
.uri("https://other-service.example.com/endpoint")
.retrieve()
.bodyToMono(String.class)
.block()
将调用
https://other-service.example.com/endpoint
,
Authorization
为您
添加承载令牌
标头。
在需要覆盖此行为的地方,只需提供一个标题就很简单了,就像这样:
this.rest.get()
.uri("https://other-service.example.com/endpoint")
.headers(headers -> headers.setBearerAuth(overridingToken))
.retrieve()
.bodyToMono(String.class)
.block()
在这种情况下,过滤器将回退并将请求转发到Web过滤器链的其余部分。
| 与 OAuth 2.0客户端过滤器功能不同 ,该过滤器功能不会在令牌过期时尝试更新令牌。 要获得此级别的支持,请使用OAuth 2.0客户端过滤器。 |
RestTemplate
支持
目前尚无专门的支持
RestTemplate
,但是您可以使用自己的拦截器非常简单地实现传播:
@Bean
RestTemplate rest() {
RestTemplate rest = new RestTemplate();
rest.getInterceptors().add((request, body, execution) -> {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null) {
return execution.execute(request, body);
}
if (!(authentication.getCredentials() instanceof AbstractOAuth2Token)) {
return execution.execute(request, body);
}
AbstractOAuth2Token token = (AbstractOAuth2Token) authentication.getCredentials();
request.getHeaders().setBearerAuth(token.getTokenValue());
return execution.execute(request, body);
});
return rest;
}
12. SAML2
12.1. SAML 2.0登录
SAML 2.0登录
saml2Login()
功能为应用程序提供了使用户能够通过使用他们在SAML 2.0身份提供程序(Okta,ADFS等)上的现有帐户登录到该应用程序的功能。
| 通过使用 SAML 2配置文件中 指定 的 Web浏览器SSO配置文件 来实现SAML 2.0登录 。 目前,我们的实现仅限于简单的身份验证方案。 |
12.1.1. Spring Security中的SAML 2支持
自2009年以来 ,作为 独立项目的 SAML 2 Service Provider(即依赖方)的支持就一直存在 。1.0.x分支仍在使用中,包括 Cloud Foundry用户帐户和身份验证服务器中 , 该分支 还创建了基于在SP实施上。
在2018年,我们尝试将 服务提供商和身份提供商 的更新实现创建 为独立库。 经过认真,冗长的讨论,我们,Spring Security团队决定停止这项工作。 尽管这项工作为该独立的1.0.x库创建了替代品,但我们并不认为应该在另一个库之上构建一个库。
相反,我们选择提供对SAML 2身份验证的框架支持,作为 核心Spring Security的 一部分 。
12.1.2. Saml 2登录-高级概念
saml2Login()
旨在支持
SAML 2功能集的一部分
,重点是作为身份验证方,即服务提供者SP,依赖方,从身份提供者(也称为声明方)接收XML声明。
SAML 2登录或身份验证是SP接收并验证来自IDP的XML消息(称为断言)的概念。
当前有两种支持的身份验证流程
-
IDP启动的流程-示例:您直接登录Okta,然后选择要进行身份验证的Web应用程序。 Okta,IDP,将一个断言发送到Web应用程序SP。
-
SP发起的流程-示例:您访问一个Web应用程序,一个SP,该应用程序将身份验证请求发送到IDP,以请求断言。 在IDP上成功身份验证后,IDP会向SP发送一个断言。
12.1.3. Saml 2登录-当前功能集
-
服务提供商(SP /依赖方)由
entityId = {baseUrl}/saml2/service-provider-metadata/{registrationId} -
通过Http-POST或Http-Redirect接收嵌入在SAML响应中的断言
{baseUrl}/login/saml2/sso/{registrationId} -
要求对断言进行签名,除非响应已签名
-
支持加密的断言
-
支持加密的NameId元素
-
允许使用
Converter<Assertion, Collection<? extends GrantedAuthority>> -
允许使用
GrantedAuthoritiesMapper -
公钥
java.security.cert.X509Certificate格式。 -
通过一个SP启动的身份验证
AuthNRequest
12.1.4. Saml 2登录-Java配置简介
要添加
saml2Login()
到Spring Security过滤器链中,最小的Java配置需要一个配置存储库
RelyingPartyRegistrationRepository
,其中包含SAML配置和
HttpSecurity.saml2Login()
方法
的调用
:
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public RelyingPartyRegistrationRepository relyingPartyRegistrationRepository() {
//SAML configuration
//Mapping this application to one or more Identity Providers
return new InMemoryRelyingPartyRegistrationRepository(...);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.saml2Login()
;
}
}
bean声明是一种方便但可选的方法。 您可以使用方法调用直接连接存储库
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.saml2Login()
.relyingPartyRegistrationRepository(...)
;
}
}
依赖方注册
该
RelyingPartyRegistration
对象表示此应用程序SP和声明方IDP之间的映射。
URI模式
URI模式经常用于根据传入的请求自动生成URI。
中的URI模式
saml2Login
可以包含以下变量
-
baseUrl -
registrationId -
baseScheme -
baseHost -
basePort
例如:
{baseUrl}/login/saml2/sso/{registrationId}
依赖党
-
registrationId-(必需)此配置映射的唯一标识符。 该标识符可以在URI路径中使用,因此应注意不需要URI编码。 -
localEntityIdTemplate-(可选)一个URI模式,它根据传入的请求为此应用程序创建一个实体ID。 默认值为{baseUrl}/saml2/service-provider-metadata/{registrationId},对于小样本应用程序,它看起来像
http://localhost:8080/saml2/service-provider-metadata/my-test-configuration
不需要此配置选项是一种模式,它可以是固定的URI值。
-
remoteIdpEntityId-(必需)身份提供者的实体ID。 始终为固定的URI值或字符串,不允许使用任何模式。 -
assertionConsumerServiceUrlTemplate-(可选)一个URI模式,它表示AuthNRequest在SP发起流程期间要与SP 一起 从 ID发送 到IDP 的断言使用者服务URI 。 尽管这可能是一种模式,但实际的URI必须解析为SP上的ACS端点。 默认值为,{baseUrl}/login/saml2/sso/{registrationId}并且直接映射到Saml2WebSsoAuthenticationFilter端点 -
idpWebSsoUrl-(必需)用于SP发送AuthNRequest消息 的IDP单一登录端点的固定URI值 。 -
credentials-凭据,私钥和x509证书的列表,用于消息签名,验证,加密和解密。 该列表可以包含冗余凭据,以便轻松旋转凭据。 例如-
[0]-X509Certificate {VERIFICATION,ENCRYPTION}-IDP的第一个用于验证和加密的公钥。
-
[1]-X509Certificate / {VERIFICATION,ENCRYPTION}-IDP的第二个验证密钥,用于验证。 始终使用
ENCRYPTION列表中 的第一个 密钥 进行加密 。 -
[2]-PrivateKey / X509Certificate {SIGNING,DECRYPTION}-SP的第一个签名和解密凭据。
-
[3]-PrivateKey / X509Certificate {SIGNING,DECRYPTION}-SP的第二个解密凭据。 始终使用
SIGNING列表中 的第一个 键 进行签名 。
-
收到传入消息时,始终需要签名,系统将首先尝试使用索引为[0]的证书来验证签名,并且如果第一个证书失败,则仅移至第二个证书。
以类似的方式,将SP配置的私钥用于解密并以相同的顺序尝试。
type=SIGNING
当签名到IDP的消息时,将使用
第一个SP凭据(
)。
服务提供商元数据
Spring Security SAML 2实现尚未提供用于下载XML格式的SP元数据的端点。 交换的最小件
-
实体ID- 默认为
{baseUrl}/saml2/service-provider-metadata/{registrationId}其他也使用相同值的已知配置名称-
观众限制
-
-
单一登录URL- 默认为
{baseUrl}/login/saml2/sso/{registrationId}其他也使用相同值的已知配置名称-
收件人URL
-
目标网址
-
断言消费者服务URL
-
-
X509Certificate-您作为{SIGNING,DECRYPTION}凭据的一部分配置的证书必须与身份提供者共享
身份验证请求-SP启动的流程
要从Web应用程序启动身份验证,只需重定向到
{baseUrl}/saml2/authenticate/{registrationId}
端点将
AuthNRequest
通过
createAuthenticationRequest
在可配置工厂上
调用该
方法
来生成一个
。
只需
Saml2AuthenticationRequestFactory
在您的配置中将
公开
为bean。
public interface Saml2AuthenticationRequestFactory {
String createAuthenticationRequest(Saml2AuthenticationRequest request);
}
12.1.5. Spring Boot 2.x示例
我们目前正在与Spring Boot团队合作 进行Spring Security SAML登录 的 自动配置 。 同时,我们提供了一个支持Yaml配置的Spring Boot示例。
若要运行该示例,请按照以下三个步骤
-
启动Spring Boot应用程序
-
./gradlew :spring-security-samples-boot-saml2login:bootRun
-
-
开启浏览器
-
这将带您到身份提供者,使用以下方式登录:
-
用户:
user -
密码:
password
-
多身份提供者示例
使用多个提供程序非常简单,但是如果您不注意,有一些默认设置可能会使您不满意。
在
RelyingPartyRegistration
对象
的SAML配置中
,我们默认将SP实体ID设置为
{baseUrl}/saml2/service-provider-metadata/{registrationId}
这意味着在我们的两个提供程序配置中,我们的系统看起来像
registration-1 (Identity Provider 1) - Our local SP Entity ID is:
http://localhost:8080/saml2/service-provider-metadata/registration-1
registration-2 (Identity Provider 2) - Our local SP Entity ID is:
http://localhost:8080/saml2/service-provider-metadata/registration-2
在下面的示例所示的外部环境中,我们实际上创建了在同一应用程序中承载的两个虚拟服务提供商标识。
spring:
security:
saml2:
login:
relying-parties:
- entity-id: &idp-entity-id https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/metadata.php
registration-id: simplesamlphp
web-sso-url: &idp-sso-url https://simplesaml-for-spring-saml.cfapps.io/saml2/idp/SSOService.php
signing-credentials: &service-provider-credentials
- private-key: |
-----BEGIN PRIVATE KEY-----
MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBANG7v8QjQGU3MwQE
...................SHORTENED FOR READ ABILITY...................
INrtuLp4YHbgk1mi
-----END PRIVATE KEY-----
certificate: |
-----BEGIN CERTIFICATE-----
MIICgTCCAeoCCQCuVzyqFgMSyDANBgkqhkiG9w0BAQsFADCBhDELMAkGA1UEBhMC
...................SHORTENED FOR READ ABILITY...................
RZ/nbTJ7VTeZOSyRoVn5XHhpuJ0B
-----END CERTIFICATE-----
verification-credentials: &idp-certificates
- |
-----BEGIN CERTIFICATE-----
MIIEEzCCAvugAwIBAgIJAIc1qzLrv+5nMA0GCSqGSIb3DQEBCwUAMIGfMQswCQYD
...................SHORTENED FOR READ ABILITY...................
lx13Y1YlQ4/tlpgTgfIJxKV6nyPiLoK0nywbMd+vpAirDt2Oc+hk
-----END CERTIFICATE-----
- entity-id: *idp-entity-id
registration-id: simplesamlphp2
web-sso-url: *idp-sso-url
signing-credentials: *service-provider-credentials
verification-credentials: *idp-certificates
如果不希望这样做,则可以使用以下命令手动覆盖本地SP实体ID:
localEntityIdTemplate = {baseUrl}/saml2/service-provider-metadata
如果我们将本地SP实体ID更改为该值,则仍然非常重要的是,我们应根据注册ID为每个注册的身份提供者提供正确的单一URL URL(断言消费者服务URL)。
{baseUrl}/login/saml2/sso/{registrationId}
13.防止利用漏洞
13.1. 跨站请求伪造(CSRF)
本节讨论Spring Security的 跨站点请求伪造(CSRF) 支持。
13.1.1. 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可以使整个过程自动化。 这意味着您甚至不需要单击该按钮。 那么,我们如何保护自己免受此类攻击?
13.1.2. 同步器令牌模式
问题在于,来自银行网站的HTTP请求与来自邪恶网站的请求完全相同。 这意味着没有办法拒绝来自邪恶网站的请求并允许来自银行网站的请求。 为了防御CSRF攻击,我们需要确保恶意站点无法提供请求中的某些内容。
一种解决方案是使用“ 同步器令牌模式” 。 该解决方案是确保除我们的会话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参数的正确值(必须在邪恶网站上明确提供),并且当服务器将实际令牌与预期令牌进行比较时,传输将失败。
13.1.3. 何时使用CSRF保护
什么时候应该使用CSRF保护? 我们的建议是对普通用户可能由浏览器处理的任何请求使用CSRF保护。 如果仅创建非浏览器客户端使用的服务,则可能需要禁用CSRF保护。
CSRF保护和JSON
一个常见的问题是“我需要保护由javascript发出的JSON请求吗?” 简短的答案是,这取决于。 但是,您必须非常小心,因为有些CSRF漏洞会影响JSON请求。 例如,恶意用户可以 使用以下格式使用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,则该应用程序将被暴露。 根据设置的不同,仍然可以通过更新URL后缀以“ .json”结尾来利用验证内容类型的Spring MVC应用程序,如下所示:
<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>
13.1.4. 使用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)获取敏感信息的一般指导, 请参阅 URI中的RFC 2616第15.1.3节“编码 敏感信息”。
配置CSRF保护
下一步是在应用程序中包含Spring Security的CSRF保护。
一些框架通过使用户会话无效来处理无效的CSRF令牌,但这会导致
其自身的问题
。
相反,默认情况下,Spring Security的CSRF保护将产生拒绝的HTTP 403访问。
可以通过配置
AccessDeniedHandler
进行自定义,
以进行
InvalidCsrfTokenException
不同的
处理
。
从Spring Security 4.0开始,默认情况下使用XML配置启用CSRF保护。 如果要禁用CSRF保护,则可以在下面看到相应的XML配置。
<http>
<!-- ... -->
<csrf disabled="true"/>
</http>
默认情况下,使用Java配置会启用CSRF保护。 如果要禁用CSRF,则可以在下面看到相应的Java配置。 有关如何配置CSRF保护的更多自定义信息,请 Referencecsrf()的Javadoc。
@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf(csrf ->
csrf.disable()
);
}
}
包括CSRF令牌
表格提交
最后一步是确保在所有PATCH,POST,PUT和DELETE方法中都包含CSRF令牌。
一种解决方法是使用
_csrf
request属性获取current
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标签库中 的csrfInput标签 。
|
如果您正在使用Spring MVC
|
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 标签,而不是手动创建meta标签 。
然后,您可以将令牌包含在所有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 模块提供了在REST风格方式的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的 所有 请求都将包含令牌。 范围限定请求接收令牌的能力有助于防止CSRF令牌泄漏给第三方。 请 Reference rest.js Reference文档 以获取关于rest.js的更多信息。
CookieCsrfTokenRepository
在某些情况下,用户可能希望将
CsrfToken
Cookie
保留
在cookie中。
默认情况下,
CookieCsrfTokenRepository
它将写入名为的cookie
XSRF-TOKEN
,并从名为
X-XSRF-TOKEN
或HTTP参数
的标头中读取它
_csrf
。
这些默认值来自
AngularJS
您可以
CookieCsrfTokenRepository
使用以下内容以XML进行
配置
:
<http>
<!-- ... -->
<csrf token-repository-ref="tokenRepository"/>
</http>
<b:bean id="tokenRepository"
class="org.springframework.security.web.csrf.CookieCsrfTokenRepository"
p:cookieHttpOnly="false"/>
|
该示例显式设置
|
您可以
CookieCsrfTokenRepository
使用以下命令在Java配置中进行配置:
@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf(csrf ->
csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
);
}
}
|
该示例显式设置
|
13.1.5. CSRF警告
实施CSRF时有一些警告。
超时时间
一个问题是预期的CSRF令牌存储在HttpSession中,因此,一旦HttpSession过期,您配置的
AccessDeniedHandler
将收到InvalidCsrfTokenException。
如果使用默认值
AccessDeniedHandler
,则浏览器将获得HTTP 403并显示错误的错误消息。
|
有人可能会问,为什么
|
缓解活动用户超时的一种简单方法是使用一些JavaScript,让该用户知道其会话即将到期。 用户可以单击按钮继续并刷新会话。
或者,指定自定义
AccessDeniedHandler
允许您以
InvalidCsrfTokenException
任何喜欢的方式
进行处理
。
有关如何自定义示例,
AccessDeniedHandler
请 Reference提供的
xml
和
Java配置
链接
。
最后,可以将应用程序配置为使用 不会过期的 CookieCsrfTokenRepository 。 如前所述,这并不像使用会话那样安全,但是在许多情况下可能已经足够了。
在登录
为了防止
伪造登录请求
,登录表单也应受到保护以免受CSRF攻击。
由于
CsrfToken
s存储在HttpSession中,因此这意味着在
CsrfToken
访问令牌属性
后将立即创建HttpSession
。
尽管这在RESTful /无状态架构中听起来很糟糕,但现实是状态对于实现实际的安全性是必需的。
没有状态,如果令牌被泄露,我们将无能为力。
实际上,CSRF令牌的大小很小,对我们的体系结构的影响应该可以忽略不计。
保护登录表单的常用技术是使用JavaScript函数在提交表单之前获取有效的CSRF令牌。
这样,无需考虑会话超时(在上一节中讨论过),因为会话是在表单提交之前创建的(假设
未配置
CookieCsrfTokenRepository
),因此用户可以停留在登录页面上并在需要时提交用户名/密码。
为了实现这一点,你可以利用的
CsrfTokenArgumentResolver
Spring Security中提供,并且喜欢它的描述上暴露的端点
位置
。
注销
添加CSRF会将LogoutFilter更新为仅使用HTTP POST。 这样可以确保注销需要CSRF令牌,并且恶意用户不能强制注销用户。
一种方法是使用表单注销。 如果您确实想要一个链接,则可以使用JavaScript使该链接执行POST(即,以隐藏形式)。 对于禁用了JavaScript的浏览器,您可以选择使该链接将用户带到将执行POST的注销确认页面。
如果您确实想在注销时使用HTTP GET,则可以这样做,但是请记住,通常不建议这样做。 例如,以下Java配置将使用URL执行注销/通过任何HTTP方法请求注销:
@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.logout(logout ->
logout
.logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
);
}
}
分段(文件上传)
将CSRF保护与multipart / form-data一起使用有两种选择。 每个选项都有其权衡。
|
在将Spring Security的CSRF保护与分段文件上传集成之前,请确保您可以首先在没有CSRF保护的情况下进行上传。 有关在Spring上使用多部分表单的更多信息,请参见 Spring Reference 的 17.10 Spring的多部分(文件上传)支持 部分和 MultipartFilter javadoc 。 |
在Spring Security之前放置MultipartFilter
第一种选择是确保
MultipartFilter
在Spring Security过滤器之前指定。
如果
MultipartFilter
在Spring Security过滤器之前
指定,
则意味着没有授权进行调用,
MultipartFilter
这意味着任何人都可以在您的服务器上放置临时文件。
但是,只有授权用户才能提交由您的应用程序处理的文件。
通常,这是推荐的方法,因为临时文件上传对大多数服务器的影响应该忽略不计。
为了确保
MultipartFilter
在具有Java配置的Spring Security过滤器之前指定用户,用户可以覆盖beforeSpringSecurityFilterChain,如下所示:
public class SecurityApplicationInitializer extends AbstractSecurityWebApplicationInitializer {
@Override
protected void beforeSpringSecurityFilterChain(ServletContext servletContext) {
insertFilters(servletContext, new MultipartFilter());
}
}
为了确保
MultipartFilter
在具有XML配置的Spring Security过滤器之前指定,用户可以确保的<filter-mapping>元素
MultipartFilter
放在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中编码敏感信息”中 找到其他信息 。
13.1.6. 覆盖默认值
Spring Security的目标是提供默认值,以保护您的用户免遭攻击。 这并不意味着您被迫接受其所有默认值。
例如,您可以提供一个自定义CsrfTokenRepository来覆盖其
CsrfToken
存储方式。
您还可以指定一个自定义RequestMatcher来确定哪些请求受CSRF保护(即,您可能不在乎是否利用了注销)。
简而言之,如果Spring Security的CSRF保护行为不完全符合您的期望,则可以自定义行为。
有关使用XML进行这些自定义的详细信息,
请参见
<csrf>
文档,有关
CsrfConfigurer
使用Java配置时如何进行这些自定义的详细信息,
请参见
javadoc。
13.2. 安全HTTP响应标头
本节讨论了Spring Security对在响应中添加各种安全标头的支持。
13.2.1. 默认安全标题
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
具体来说,您希望所有默认标头都具有以下自定义设置:
-
X-Frame-Options 允许来自相同域的任何请求
-
HTTP严格传输安全性(HSTS) 不会添加到响应中
您可以使用以下Java配置轻松完成此操作:
@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// ...
.headers(headers ->
headers
.frameOptions(frameOptions ->
frameOptions.sameOrigin()
)
.httpStrictTransportSecurity(hsts ->
hsts.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(headers ->
headers
// do not use any default headers unless explicitly listed
.defaultsDisabled()
.cacheControl(withDefaults())
);
}
}
以下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(headers ->
headers.disable()
);
}
}
如有必要,您可以使用以下XML配置禁用所有HTTP安全响应标头:
<http>
<!-- ... -->
<headers disabled="true" />
</http>
13.2.2. 缓存控制
过去,Spring Security要求您为Web应用程序提供自己的缓存控件。 当时看来这是合理的,但是浏览器缓存已经演变为包括用于安全连接的缓存。 这意味着用户可以查看经过身份验证的页面,然后注销,然后恶意用户可以使用浏览器历史记录来查看缓存的页面。 为了帮助缓解这种情况,Spring Security添加了缓存控制支持,该支持将在响应中插入以下标头。
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
只需添加 没有子元素 的 <headers >元素,即可自动添加Cache Control和许多其他保护。 但是,如果只希望缓存控制,则可以使用Spring Security的XML名称空间通过 <cache-control >元素和 headers @ defaults-disabled 属性 来启用此功能 。
<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(headers ->
headers
.defaultsDisabled()
.cacheControl(withDefaults())
);
}
}
如果您确实想缓存特定的响应,则您的应用程序可以有选择地调用 HttpServletResponse.setHeader(String,String) 来覆盖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);
}
// ...
}
13.2.3. 内容类型选项
从历史上看,包括Internet Explorer在内的浏览器都会尝试使用 内容嗅探 来猜测请求的内容类型 。 这允许浏览器通过猜测未指定内容类型的资源上的内容类型来改善用户体验。 例如,如果浏览器遇到一个未指定内容类型的JavaScript文件,它将能够猜测该内容类型然后执行。
|
允许上载内容时,还有许多其他事情(即,仅在不同的域中显示文档,确保设置了Content-Type标头,清理文档等)。 但是,这些措施不在Spring Security提供的范围之内。 指出禁用内容嗅探时也很重要,您必须指定内容类型才能使内容正常工作。 |
内容嗅探的问题在于,这允许恶意用户使用多义标记(即,可以作为多种内容类型有效的文件)执行XSS攻击。 例如,某些网站可能允许用户向网站提交有效的附言文档并进行查看。 恶意用户可能会创建一个 也是有效JavaScript文件 的 Postscript文档, 并对其执行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(headers ->
headers
.defaultsDisabled()
.contentTypeOptions(withDefaults())
);
}
}
13.2.4. 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 。 这大大降低了发生中间人攻击的可能性。
|
根据 RFC6797 ,HSTS标头仅注入HTTPS响应中。 为了使浏览器能够确认标头,浏览器必须首先信任对用于建立连接的SSL证书(不仅仅是SSL证书)进行签名的CA。 |
将站点标记为HSTS主机的一种方法是将主机预加载到浏览器中。 另一方法是将“ Strict-Transport-Security”标头添加到响应中。 例如,以下内容将指示浏览器将域视为一年的HSTS主机(一年大约31536000秒):
Strict-Transport-Security: max-age=31536000 ; includeSubDomains ; preload
可选的includeSubDomains指令指示Spring Security子域(即secure.mybank.example.com)也应被视为HSTS域。
可选的preload指令指示Spring Security该域应在浏览器中预加载为HSTS域。 有关HSTS预加载的更多详细信息,请参见 https://hstspreload.org 。
与其他头文件一样,Spring Security默认添加HSTS。 您可以使用 <hsts >元素来自 定义HSTS标头, 如下所示:
<http>
<!-- ... -->
<headers>
<hsts
include-subdomains="true"
max-age-seconds="31536000" preload="true" />
</headers>
</http>
同样,您只能使用Java配置启用HSTS标头:
@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// ...
.headers(headers ->
headers
.httpStrictTransportSecurity(hsts ->
hsts
.includeSubDomains(true)
.preload(true)
.maxAgeInSeconds(31536000)
)
);
}
}
13.2.5. HTTP公钥固定(HPKP)
HTTP公共密钥固定(HPKP)是一项安全功能,它告诉Web客户端将特定的加密公共密钥与某个Web服务器相关联,以防止使用伪造证书的中间人(MITM)攻击。
为了确保TLS会话中使用的服务器公钥的真实性,此公钥被包装到X.509证书中,该证书通常由证书颁发机构(CA)签名。 浏览器之类的Web客户端信任许多这样的CA,它们都可以为任意域名创建证书。 如果攻击者能够入侵单个CA,则他们可以对各种TLS连接执行MITM攻击。 HPKP可以通过告诉客户端哪个公钥属于某个Web服务器来规避HTTPS协议的这种威胁。 HPKP是首次使用信任(TOFU)技术。 Web服务器第一次通过特殊的HTTP标头告知客户端哪个公钥属于它时,客户端会在给定的时间段内存储此信息。 当客户端再次访问服务器时, 它需要包含公钥的证书,该公钥的指纹已经通过HPKP知道。 如果服务器提供了未知的公共密钥,则客户端应向用户显示警告。
|
由于用户代理需要根据SSL证书链验证引脚,因此HPKP标头仅注入HTTPS响应中。 |
通过HTTPS访问网站时,为您的网站启用此功能就像返回Public-Key-Pins HTTP标头一样简单。 例如,以下内容将指示用户代理仅将2个引脚的引脚验证失败报告给指定的URI(通过 report-uri 指令):
Public-Key-Pins-Report-Only: max-age=5184000 ; pin-sha256="d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM=" ; pin-sha256="E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g=" ; report-uri="https://example.net/pkp-report" ; includeSubDomains
一 针验证失败报告, 是一个标准的JSON的结构,可以通过Web应用程序的自己的API或由公开托管HPKP报告服务,例如,被捕获 REPORT-URI 。
可选的includeSubDomains指令指示浏览器也使用给定的引脚来验证子域。
与其他标头相反,Spring Security默认情况下不添加HPKP。 您可以使用 <hpkp >元素来自 定义HPKP标头, 如下所示:
<http>
<!-- ... -->
<headers>
<hpkp
include-subdomains="true"
report-uri="https://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(headers ->
headers
.httpPublicKeyPinning(hpkp ->
hpkp
.includeSubDomains(true)
.reportUri("https://example.net/pkp-report")
.addSha256Pins("d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM=", "E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g=")
)
);
}
}
13.2.6. X框架选项
允许将您的网站添加到框架可能是一个安全问题。 例如,使用聪明的CSS样式用户可能会被诱骗点击他们不想要的内容( 视频演示 )。 例如,登录到其银行的用户可以单击将按钮授予其他用户访问权限。 这种攻击称为 Clickjacking 。
|
处理点击劫持的另一种现代方法是使用 内容安全策略(CSP) 。 |
有许多方法可以缓解点击劫持攻击。 例如,为了保护旧版浏览器免受点击劫持攻击,您可以使用 破帧代码 。 虽然不完美,但是对于传统浏览器而言,破帧代码是最好的选择。
解决点击劫持的更现代方法是使用 X-Frame-Options 标头:
X-Frame-Options: DENY
X-Frame-Options响应标头指示浏览器阻止响应中带有此标头的任何网站呈现在框架中。 默认情况下,Spring Security禁用iframe中的呈现。
您可以使用 frame-options 元素来自 定义X-Frame-Options 。 例如,以下内容将指示Spring Security使用“ X-Frame-Options:SAMEORIGIN”,它允许在同一域内的iframe:
<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(headers ->
headers
.frameOptions(frameOptions ->
frameOptions
.sameOrigin()
)
);
}
}
13.2.7. X-XSS保护
一些浏览器内置了对过滤掉 反射的XSS攻击的支持 。 这绝非万无一失,但确实有助于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(headers ->
headers
.xssProtection(xssProtection ->
xssProtection
.block(false)
)
);
}
}
13.2.8. 内容安全政策(CSP)
内容安全策略(CSP) 是Web应用程序可以利用的一种机制,可以缓解诸如跨站点脚本(XSS)之类的内容注入漏洞。 CSP是一种声明性策略,为Web应用程序作者提供了一种工具,可以声明该Web应用程序希望从中加载资源的来源,并最终将这些信息通知客户端(用户代理)。
|
内容安全策略并非旨在解决所有内容注入漏洞。 取而代之的是,可以利用CSP帮助减少内容注入攻击所造成的危害。 作为第一道防线,Web应用程序作者应验证其输入并对其输出进行编码。 |
Web应用程序可以通过在响应中包括以下HTTP标头之一来使用CSP:
-
内容安全政策
-
内容安全政策仅报告
这些标头中的每一个都用作将 安全策略 传递给客户端的机制。 安全策略包含一组 安全策略指令 (例如 script-src 和 object-src ),每个指令 负责声明特定资源表示形式的限制。
例如,Web应用程序可以通过在响应中包含以下标头来声明它希望从特定的受信任源中加载脚本:
Content-Security-Policy: script-src https://trustedscripts.example.com
用户代理会阻止 尝试从另一个源(而不是 script-src 指令中 声明的内容)加载脚本 。 此外,如果 在安全策略中声明 了 report-uri 指令,则用户代理会将违反行为报告给声明的URL。
例如,如果Web应用程序违反了已声明的安全策略,则以下响应标头将指示用户代理将违规报告发送到策略的 report-uri 指令中 指定的URL 。
Content-Security-Policy: script-src https://trustedscripts.example.com; report-uri /csp-report-endpoint/
违规报告 是标准JSON结构,可以由Web应用程序自己的API或公共托管的CSP违规报告服务(例如 REPORT-URI) 捕获 。
在 内容安全,策略报告,只有 头提供了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 默认情况下 不会添加 内容安全策略。 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 “仅报告” 标头,请按以下方式配置元素:
<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(headers ->
headers
.contentSecurityPolicy(csp ->
csp
.policyDirectives("script-src 'self' https://trustedscripts.example.com; object-src https://trustedplugins.example.com; report-uri /csp-report-endpoint/")
)
);
}
}
要启用CSP “仅报告” 标头,请提供以下Java配置:
@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// ...
.headers(headers ->
headers
.contentSecurityPolicy(csp ->
csp
.policyDirectives("script-src 'self' https://trustedscripts.example.com; object-src https://trustedplugins.example.com; report-uri /csp-report-endpoint/")
.reportOnly()
)
);
}
}
13.2.9。 推荐人政策
引荐来源网址 政策 是一种机制,Web应用程序可以利用该机制来管理引荐来源网址字段,该字段包含用户所在的最后一页。
Spring Security的方法是使用 Referrer Policy 标头,该标头提供了不同的 策略 :
Referrer-Policy: same-origin
Referrer-Policy响应标头指示浏览器让目的地知道用户先前所在的源。
配置推荐人策略
Spring Security 默认 不添加 Referrer Policy标头。
您可以使用带有 <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(headers ->
headers
.referrerPolicy(referrerPolicy ->
referrerPolicy
.policy(ReferrerPolicy.SAME_ORIGIN)
)
);
}
}
13.2.10。 功能政策
功能策略 是一种机制,允许Web开发人员在浏览器中选择性地启用,禁用和修改某些API和Web功能的行为。
Feature-Policy: geolocation 'self'
借助功能策略,开发人员可以为浏览器选择一套“策略”,以实施整个站点中使用的特定功能。 这些策略限制了站点可以访问或修改某些功能的浏览器默认行为的API。
配置功能策略
Spring Security 默认 不添加 功能策略头。
您可以使用带有 <feature-policy >元素的 XML配置来启用Feature-Policy标头 ,如下所示:
<http>
<!-- ... -->
<headers>
<feature-policy policy-directives="geolocation 'self'" />
</headers>
</http>
同样,您可以使用Java配置启用功能策略标头,如下所示:
@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// ...
.headers(headers ->
headers
.featurePolicy("geolocation 'self'")
);
}
}
13.2.11. 清除网站数据
清除站点数据 是一种机制,通过该机制,当HTTP响应包含以下标头时,可以删除所有浏览器端数据(Cookie,本地存储等):
Clear-Site-Data: "cache", "cookies", "storage", "executionContexts"
这是注销时执行的不错的清理操作。
配置清除站点数据
Spring Security 默认情况下 不添加 “清除站点数据”标头。
您可以将应用程序配置为在注销时发送此标头,如下所示:
@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// ...
.logout()
.addLogoutHandler(new HeaderWriterLogoutHandler(new ClearSiteDataHeaderWriter(CACHE, COOKIES)));
}
}
不建议您通过
headers()
指令
配置此标头
编写器。
原因是
JSESSIONID
将删除
任何会话状态(例如
cookie),从而有效地注销用户。
|
13.2.12. 自定义标题
Spring Security具有使您可以方便地将更常见的安全标头添加到您的应用程序的机制。 但是,它也提供了挂钩来启用添加自定义标头。
静态标题
有时您可能希望将不支持的自定义安全标头注入应用程序中。 例如,给定以下自定义安全标头:
X-Custom-Security-Header: header-value
使用XML名称空间时,可以使用 <header >元素 将这些标头添加到响应中 ,如下所示:
<http>
<!-- ... -->
<headers>
<header name="X-Custom-Security-Header" value="header-value"/>
</headers>
</http>
同样,可以使用Java配置将标头添加到响应中,如下所示:
@EnableWebSecurity
public class WebSecurityConfig extends
WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// ...
.headers(headers ->
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 https://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(headers ->
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(headers ->
headers
.frameOptions(frameOptions ->
frameOptions.disable()
)
.addHeaderWriter(headerWriter)
);
}
}
13.3. HTTPS
13.3.1. 添加HTTP / HTTPS通道安全性
如果您的应用程序同时支持HTTP和HTTPS,并且您要求只能通过HTTPS访问特定的URL,那么可以使用以下
requires-channel
属性
直接支持
它
<intercept-url>
:
<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 [ 10 ] 。 可用选项为“ 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)开始,并在整个过程中使用安全连接,以避免任何中间人攻击的可能性。
14.整合
14.1. Servlet API集成
本节描述了如何将Spring Security与Servlet API集成在一起。 所述 servletapi-XML 示例应用程序演示每一种方法的使用。
14.1.1. Servlet 2.5+集成
HttpServletRequest.getRemoteUser()
所述
HttpServletRequest.getRemoteUser()
将返回结果的
SecurityContextHolder.getContext().getAuthentication().getName()
其通常是当前用户名。
如果要在应用程序中显示当前用户名,这将很有用。
此外,检查此属性是否为null可以用来指示用户已通过身份验证还是匿名。
知道用户是否通过身份验证对于确定是否应显示某些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(String)
该
HttpServletRequest.isUserInRole(字符串)
将确定是否
SecurityContextHolder.getContext().getAuthentication().getAuthorities()
包含
GrantedAuthority
与传入的作用
isUserInRole(String)
。
通常,用户不应将“ ROLE_”前缀传递给此方法,因为它是自动添加的。
例如,如果要确定当前用户是否具有权限“ ROLE_ADMIN”,则可以使用以下命令:
boolean isAdmin = httpServletRequest.isUserInRole("ADMIN");
这对于确定是否应显示某些UI组件可能很有用。 例如,仅当当前用户是管理员时,才可以显示管理员链接。
14.1.2. Servlet 3+集成
下一节描述了Spring Security与之集成的Servlet 3方法。
HttpServletRequest.authenticate(HttpServletRequest,HttpServletResponse)
所述 HttpServletRequest.authenticate(HttpServletRequest的,HttpServletResponse的) 方法可用于确保用户被认证。 如果未通过身份验证,则将使用配置的AuthenticationEntryPoint来请求用户进行身份验证(即,重定向到登录页面)。
HttpServletRequest.login(String,String)
所述
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无效,将清除所有“记住我”身份验证,依此类推。但是,配置的LogoutHandler实现取决于您的Spring Security配置。 重要的是要注意,在调用HttpServletRequest.logout()之后,您仍然负责写出响应。 通常,这将涉及重定向到欢迎页面。
AsyncContext.start(Runnable)
确保您的凭据将被传播到新线程 的 AsynchContext.start(Runnable) 方法。 使用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 https://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();
问题是Spring Security不知道该线程,因此不会将SecurityContext传播给它。 这意味着当我们提交HttpServletResponse时,没有SecuriytContext。 当Spring Security在提交HttpServletResponse时自动保存SecurityContext时,它将丢失我们的登录用户。
从3.2版本开始,Spring Security足够聪明,不再会在调用HttpServletRequest.startAsync()时自动保存SecurityContext来提交HttpServletResponse。
14.1.3. Servlet 3.1+集成
下一节描述了与Spring Security集成的Servlet 3.1方法。
HttpServletRequest#changeSessionId()
所述 HttpServletRequest.changeSessionId() 是用于防止的默认方法 会话固定 在Servlet的3.1和更高的攻击。
14.2. Spring数据集成
Spring Security提供了Spring Data集成,允许在查询中引用当前用户。 将用户包括在查询中以支持分页结果不仅有用,而且有必要,因为事后过滤结果将无法扩展。
14.2.1. Spring数据和Spring安全配置
要使用此支持,请添加
org.springframework.security:spring-security-data
依赖项并提供type的bean
SecurityEvaluationContextExtension
。
在Java配置中,这看起来像:
@Bean
public SecurityEvaluationContextExtension securityEvaluationContextExtension() {
return new SecurityEvaluationContextExtension();
}
在XML配置中,这看起来像:
<bean class="org.springframework.security.data.repository.query.SecurityEvaluationContextExtension"/>
14.2.2. @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属性的Object。
通过公开
SecurityEvaluationContextExtension
Bean,所有
通用安全表达式
都可以在查询中使用。
14.3. 并发支持
在大多数环境中,安全性是按
Thread
基础
存储的
。
这意味着当在new上完成工作时
Thread
,
SecurityContext
会丢失。
Spring Security提供了一些基础架构来帮助用户轻松实现这一点。
Spring Security提供了用于在多线程环境中使用Spring Security的底层抽象。
实际上,这就是Spring Security与
AsyncContext.start(Runnable)
和
Spring MVC Async Integration集成的基础
。
14.3.1. DelegatingSecurityContextRunnable
Spring Security的并发支持中最基本的构建块之一是
DelegatingSecurityContextRunnable
。
它包装了一个委托
Runnable
,以便
SecurityContextHolder
使用
SecurityContext
为委托
指定的名称
来初始化
。
然后,它将调用委托Runnable,以确保
SecurityContextHolder
事后
清除
。
本
DelegatingSecurityContextRunnable
看起来是这样的:
public void run() {
try {
SecurityContextHolder.setContext(securityContext);
delegate.run();
} finally {
SecurityContextHolder.clearContext();
}
}
尽管非常简单,但可以无缝地将SecurityContext从一个线程传输到另一个线程。
这很重要,因为在大多数情况下,SecurityContextHolder会基于每个线程进行操作。
例如,您可能使用了Spring Security的
<global-method-security>
支持来保护您的一项服务。
现在,您可以轻松地将
SecurityContext
当前
流转
Thread
移到
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 -
SecurityContext从中 获取 我们要使用的SecurityContextHolder并初始化DelegatingSecurityContextRunnable -
使用
DelegatingSecurityContextRunnable创建线程 -
启动我们创建的线程
由于通常
DelegatingSecurityContextRunnable
使用
SecurityContext
from
来创建一个
,
因此
SecurityContextHolder
有一个快捷方式构造函数。
以下代码与上面的代码相同:
Runnable originalRunnable = new Runnable() {
public void run() {
// invoke secured service
}
};
DelegatingSecurityContextRunnable wrappedRunnable =
new DelegatingSecurityContextRunnable(originalRunnable);
new Thread(wrappedRunnable).start();
我们拥有的代码易于使用,但仍然需要了解我们正在使用Spring Security。
在下一节中,我们将研究如何利用
DelegatingSecurityContextExecutor
自身来隐藏我们正在使用Spring Security的事实。
14.3.2. 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如果需要, 我们都可以从 那里获得)都没有关系 。 -
创建一个负责执行提交的
Runnables 的委托执行器 -
最后,我们创建一个
DelegatingSecurityContextExecutor,负责使用来包装传递给execute方法的所有RunnableDelegatingSecurityContextRunnable。 然后,它将包装的Runnable传递给委托执行器。 在这种情况下,SecurityContext提交给我们的每个Runnable都将使用 相同 的内容DelegatingSecurityContextExecutor。 如果我们正在运行需要由特权较高的用户运行的后台任务,那就很好。 -
在这一点上,您可能会问自己“这如何屏蔽我的代码,使其不了解Spring Security?” 代替 在自己的代码 中创建
SecurityContext和DelegatingSecurityContextExecutor,我们可以注入已初始化的实例DelegatingSecurityContextExecutor。
@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
清除。
在此示例中,使用同一用户执行每个线程。
如果要
SecurityContextHolder
在调用时
使用用户
executor.execute(Runnable)
(即当前登录的用户)进行处理该
originalRunnable
怎么办?
这可以通过
SecurityContext
从
DelegatingSecurityContextExecutor
构造函数中
删除
参数
来完成
。
例如:
SimpleAsyncTaskExecutor delegateExecutor = new SimpleAsyncTaskExecutor();
DelegatingSecurityContextExecutor executor =
new DelegatingSecurityContextExecutor(delegateExecutor);
现在,随时
executor.execute(Runnable)
可以执行,
SecurityContext
它首先由获取
SecurityContextHolder
,然后
SecurityContext
用于创建我们的
DelegatingSecurityContextRunnable
。
这意味着我们将
Runnable
使用与调用
executor.execute(Runnable)
代码
相同的用户来执行
代码。
14.3.3. Spring Security并发类
有关与Java并发API和Spring Task抽象的其他集成,请 ReferenceJavadoc。 一旦您理解了先前的代码,它们就非常不言自明。
-
DelegatingSecurityContextCallable
-
DelegatingSecurityContextExecutor
-
DelegatingSecurityContextExecutorService
-
DelegatingSecurityContextRunnable
-
DelegatingSecurityContextScheduledExecutorService
-
DelegatingSecurityContextSchedulingTaskExecutor
-
DelegatingSecurityContextAsyncTaskExecutor
-
DelegatingSecurityContextTaskExecutor
-
DelegatingSecurityContextTaskScheduler
14.4. 杰克逊支持
Spring Security已添加了Jackson支持以持久化与Spring Security相关的类。 在使用分布式会话(例如,会话复制,Spring Session等)时,这可以提高序列化Spring Security相关类的性能。
要使用它,请将其注册
SecurityJackson2Modules.getModules(ClassLoader)
为
Jackson模块
。
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);
14.5. 本土化
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
的内容。
您可以自己在过滤器中执行此操作(该过滤器必须位于的Spring Security过滤器之前
web.xml
),也可以使用Spring的
RequestContextFilter
。
请参阅Spring Framework文档以获取有关在Spring中使用本地化的更多详细信息。
“联系人”样本应用程序被设置为使用本地化消息。
14.6. Spring MVC集成
Spring Security提供了许多与Spring MVC的可选集成。 本节将详细介绍集成。
14.6.1. @EnableWebMvcSecurity
从Spring Security 4.0开始,
@EnableWebMvcSecurity
已弃用。
替换
@EnableWebSecurity
将确定基于类路径添加Spring MVC功能。
|
要启用与Spring MVC的Spring Security集成,
@EnableWebSecurity
请在配置中
添加
注释。
Spring Security使用Spring MVC的
WebMvcConfigurer
提供配置
。
这意味着,如果您使用的是更高级的选项,例如
WebMvcConfigurationSupport
直接
集成
,那么您将需要手动提供Spring Security配置。
|
14.6.2. MvcRequestMatcher
Spring Security提供了Spring MVC如何与URL匹配的深度集成
MvcRequestMatcher
。
这有助于确保您的安全规则与用于处理请求的逻辑相匹配。
为了使用它,
MvcRequestMatcher
您必须将Spring Security Configuration与
ApplicationContext
您
的相同
DispatcherServlet
。
这是必需的,因为Spring Security
MvcRequestMatcher
希望
您的Spring MVC配置会注册
一个
HandlerMappingIntrospector
名称为
的
Bean,该Bean
mvcHandlerMappingIntrospector
用于执行匹配。
对于
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>
在下面
WebSecurityConfiguration
放置在
DispatcherServlet
s中
ApplicationContext
。
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[] { "/" };
}
}
|
始终建议通过匹配
|
考虑一个映射如下的控制器:
@RequestMapping("/admin")
public String admin() {
如果我们想将对这种控制器方法的访问限制为管理员用户,则开发人员可以通过将匹配
HttpServletRequest
以下内容
来提供授权规则
:
protected configure(HttpSecurity http) throws Exception {
http
.authorizeRequests(authorizeRequests ->
authorizeRequests
.antMatchers("/admin").hasRole("ADMIN")
);
}
或XML
<http>
<intercept-url pattern="/admin" access="hasRole('ADMIN')"/>
</http>
无论采用哪种配置,URL
/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(authorizeRequests ->
authorizeRequests
.mvcMatchers("/admin").hasRole("ADMIN")
);
}
或XML
<http request-matcher="mvc">
<intercept-url pattern="/admin" access="hasRole('ADMIN')"/>
</http>
14.6.3. @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
返回一个
Object
实现并由
UserDetails
您自己
实现
的自定义
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
可能会返回
Object
实现
UserDetails
并提供名为
getCustomUser
access
的方法的
CustomUser
。
例如,它可能看起来像:
public class CustomUserUserDetails extends User {
// ...
public CustomUser getCustomUser() {
return customUser;
}
}
然后,我们可以访问
CustomUser
使用
规划环境地政司表示
,使用
Authentication.getPrincipal()
作为根对象:
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表达式中引用Bean。 例如,如果我们使用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 ...
}
14.6.4. Spring MVC异步集成
Spring Web MVC 3.2+对
异步请求处理
提供了出色的支持
。
由于没有额外的配置,Spring Security中会自动设置的
SecurityContext
到
Thread
执行一个
Callable
由你的控制器返回。
例如,以下方法将自动
Callable
以
创建
SecurityContext
时可用的
方法
执行
Callable
:
@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与集成
|
没有与
DeferredResult
控制器返回的
自动集成
。
这是因为
DeferredResult
由用户处理,因此无法自动与其集成。
但是,您仍然可以使用
并发支持
来提供与Spring Security的透明集成。
14.6.5. Spring MVC和CSRF集成
自动令牌包含
Spring Security将 在使用 Spring MVC表单标签的 表单中 自动 包含CSRF令牌 。 例如,以下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
其他域的机密
非常重要
。
这意味着,如果您使用
跨源共享(CORS)
,
则不
应将
暴露
CsrfToken
给任何外部域。
14.7. WebSocket安全
Spring Security 4增加了对确保 Spring的WebSocket支持的支持 。 本节描述了如何使用Spring Security的WebSocket支持。
| 您可以在 https://github.com/spring-projects/spring-session/tree/master/samples/boot/websocket中 找到WebSocket安全性的完整工作示例 。 |
14.7.1. WebSocket配置
Spring Security 4.0通过Spring Messaging抽象引入了对WebSocket的授权支持。
要使用Java配置来配置授权,只需扩展
AbstractSecurityWebSocketMessageBrokerConfigurer
并配置即可
MessageSecurityMetadataSourceRegistry
。
例如:
@Configuration
public class WebSocketSecurityConfig
extends AbstractSecurityWebSocketMessageBrokerConfigurer { (1) (2)
protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
messages
.simpDestMatchers("/user/**").authenticated() (3)
}
}
这将确保:
| 1个 | 任何入站CONNECT消息均需要有效的CSRF令牌来实施“ 同源起点策略” |
| 2 | 对于任何入站请求,在simpUser标头属性中使用用户填充SecurityContextHolder。 |
| 3 | 我们的消息需要适当的授权。 具体来说,任何以“ / user /”开头的入站消息都需要ROLE_USER。 有关授权的其他详细信息,请参见 WebSocket授权。 |
Spring Security还提供 XML命名空间 支持以保护WebSocket。 可比较的基于XML的配置如下所示:
<websocket-message-broker> (1) (2)
(3)
<intercept-message pattern="/user/**" access="hasRole('USER')" />
</websocket-message-broker>
这将确保:
| 1个 | 任何入站CONNECT消息均需要有效的CSRF令牌来实施“ 同源起点策略” |
| 2 | 对于任何入站请求,在simpUser标头属性中使用用户填充SecurityContextHolder。 |
| 3 | 我们的消息需要适当的授权。 具体来说,任何以“ / user /”开头的入站消息都需要ROLE_USER。 有关授权的其他详细信息,请参见 WebSocket授权。 |
14.7.2. WebSocket认证
WebSockets重用建立WebSocket连接时在HTTP请求中找到的相同身份验证信息。
这意味着
Principal
on
HttpServletRequest
将会移交给WebSockets。
如果您使用的是Spring Security,则
Principal
上的
HttpServletRequest
会被自动覆盖。
更具体地说,要确保用户已通过WebSocket应用程序的身份验证,所需要做的就是确保您设置Spring Security以对基于HTTP的Web应用程序进行身份验证。
14.7.3. WebSocket授权
Spring Security 4.0通过Spring Messaging抽象引入了对WebSocket的授权支持。
要使用Java配置来配置授权,只需扩展
AbstractSecurityWebSocketMessageBrokerConfigurer
并配置即可
MessageSecurityMetadataSourceRegistry
。
例如:
@Configuration
public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {
@Override
protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
messages
.nullDestMatcher().authenticated() (1)
.simpSubscribeDestMatchers("/user/queue/errors").permitAll() (2)
.simpDestMatchers("/app/**").hasRole("USER") (3)
.simpSubscribeDestMatchers("/user/**", "/topic/friends/*").hasRole("USER") (4)
.simpTypeMatchers(MESSAGE, SUBSCRIBE).denyAll() (5)
.anyMessage().denyAll(); (6)
}
}
这将确保:
| 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还提供 XML命名空间 支持以保护WebSocket。 可比较的基于XML的配置如下所示:
<websocket-message-broker>
(1)
<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" /> (2)
<intercept-message pattern="/app/**" access="hasRole('USER')" /> (3)
(4)
<intercept-message pattern="/user/**" access="hasRole('USER')" />
<intercept-message pattern="/topic/friends/*" access="hasRole('USER')" />
(5)
<intercept-message type="MESSAGE" access="denyAll" />
<intercept-message type="SUBSCRIBE" access="denyAll" />
<intercept-message pattern="/**" access="denyAll" /> (6)
</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”通知
-
客户可以通过订阅接收到“ / topic / system / notifications”的通知。
尽管我们希望客户能够订阅“ / topic / system / notifications”,但我们不想让他们将MESSAGE发送到该目的地。 如果我们允许向“ / topic / system / notifications”发送消息,则客户端可以直接向该端点发送消息并模拟系统。
通常,应用程序通常拒绝发送到以 代理前缀 (即“ / topic /”或“ / queue /”) 开头的目标的任何MESSAGE 。
目的地上的WebSocket授权
了解目的地如何转变也很重要。
考虑聊天应用程序。
-
用户可以通过将消息发送到“ / app / chat”的目的地来向特定用户发送消息。
-
应用程序会看到该消息,并确保将“ from”属性指定为当前用户(我们不能信任客户端)。
-
然后,应用程序使用将消息发送给收件人
SimpMessageSendingOperations.convertAndSendToUser("toUser", "/queue/messages", message)。 -
消息变成目标“ / queue / user / messages- <sessionid>”
使用上面的应用程序,我们希望允许我们的客户端收听“ / user / queue”,它被转换为“ / queue / user / messages- <sessionid>”。 但是,我们不希望客户端能够收听“ / queue / *”,因为那样会使客户端看到每个用户的消息。
通常,应用程序通常拒绝发送到以 代理前缀 (即“ / topic /”或“ / queue /”) 开头的消息的任何SUBSCRIBE 。 当然,我们可能会提供例外情况来说明类似
出站邮件
Spring包含一个标题
为“消息流”
的部分,该部分
描述了消息如何在系统中流动。
重要的是要注意,Spring Security只保护.NET
clientInboundChannel
。
Spring Security不会尝试保护
clientOutboundChannel
。
最重要的原因是性能。 对于每条传入的消息,通常会有更多的出去消息。 我们鼓励保护对端点的订阅,而不是保护出站消息。
14.7.4. 实施同源政策
需要强调的是,浏览器不会 对WebSocket连接 强制执行“ 相同来源策略 ”。 这是一个非常重要的考虑因素。
为什么起源相同?
请考虑以下情形。 用户访问bank.com并验证其帐户。 同一用户在其浏览器中打开另一个选项卡,并访问evil.com。 相同来源政策可确保evil.com无法读取数据或将数据写入bank.com。
对于WebSocket,不适用“相同来源策略”。 实际上,除非bank.com明确禁止,否则evil.com可以代表用户读取和写入数据。 这意味着用户可以通过webSocket进行任何操作(即转账),evil.com可以代表该用户进行操作。
由于SockJS尝试模拟WebSocket,因此它也绕过了相同起源策略。 这意味着开发人员在使用SockJS时需要明确保护其应用程序不受外部域的影响。
将CSRF添加到Stomp标头
默认情况下,Spring Security需要 任何CONNECT消息类型 的 CSRF令牌 。 这样可以确保只有有权访问CSRF令牌的站点才能连接。 由于只有 相同来源 可以访问CSRF令牌,因此不允许外部域进行连接。
通常,我们需要在HTTP标头或HTTP参数中包含CSRF令牌。 但是,SockJS不允许使用这些选项。 相反,我们必须在Stomp标头中包含令牌
应用程序可以
通过访问名为_csrf的请求属性
来
获取CSRF令牌
。
例如,以下将允许
CsrfToken
在JSP中
访问
:
var headerName = "${_csrf.headerName}";
var token = "${_csrf.token}";
如果使用的是静态HTML,则可以
CsrfToken
在REST端点上
公开
。
例如,以下代码将
CsrfToken
在/ csrf网址上公开
@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) {
...
}
14.7.5. 使用SockJS
SockJS 提供后备传输以支持较旧的浏览器。 使用后备选项时,我们需要放松一些安全性约束,以允许SockJS与Spring Security一起使用。
SockJS和框架选项
SockJS可以使用 利用iframe 的 传输 。 默认情况下,Spring Security会 拒绝 对网站进行构架以防止Clickjacking攻击。 为了允许基于SockJS框架的传输正常工作,我们需要配置Spring Security以允许相同的来源对内容进行框架化。
您可以使用 frame-options 元素来自 定义X-Frame-Options 。 例如,以下内容将指示Spring Security使用“ X-Frame-Options:SAMEORIGIN”,它允许在同一域内的iframe:
<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(headers ->
headers
.frameOptions(frameOptions ->
frameOptions
.sameOrigin()
)
);
}
}
SockJS和令人放松的CSRF
SockJS在CONNECT消息上使用POST进行任何基于HTTP的传输。 通常,我们需要在HTTP标头或HTTP参数中包含CSRF令牌。 但是,SockJS不允许使用这些选项。 相反,我们必须将令牌包含在Stomp标头中,如 将CSRF添加到Stomp 标头中所述 。
这也意味着我们需要通过Web层放宽对CSRF的保护。 具体来说,我们要为连接URL禁用CSRF保护。 我们不想禁用每个URL的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(csrf ->
csrf
// ignore our stomp endpoints since they are protected using Stomp headers
.ignoringAntMatchers("/chat/**")
)
.headers(headers ->
headers
// allow same origin to frame our site to support iframe SockJS
.frameOptions(frameOptions ->
frameOptions
.sameOrigin()
)
)
.authorizeRequests(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>
14.8. CORS
Spring Framework
为CORS
提供了
一流的支持
。
必须在Spring Security之前处理CORS,因为飞行前请求将不包含任何cookie(即
JSESSIONID
)。
如果请求不包含任何cookie,并且首先使用Spring Security,则该请求将确定用户未通过身份验证(因为请求中没有cookie),并拒绝该用户。
确保首先处理CORS的最简单方法是使用
CorsFilter
。
用户可以
CorsFilter
通过
CorsConfigurationSource
使用以下命令
将集成到
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(withDefaults())
...
}
@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(withDefaults())
...
}
}
或XML
<http>
<!-- Default to Spring MVC's CORS configuration -->
<cors />
...
</http>
14.9。 JSP标签库
Spring Security有自己的taglib,它为访问安全信息和在JSP中应用安全约束提供了基本的支持。
14.9.1. 声明标签库
要使用任何标记,必须在JSP中声明安全标记库:
<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %>
14.9.2. 授权标签
该标签用于确定是否应评估其内容。
在Spring Security 3.0中,它可以两种方式使用
[ 11 ]
。
第一种方法使用
在标签属性中
指定
的
Web安全表达式
access
。
表达式评估将委派给
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请求,并调用安全拦截器以查看该请求是成功还是失败。
这使您可以委托给使用
命名空间配置中的
intercept-url
声明
定义的访问控制设置,
<http>
并且省去了在JSP中重复信息(例如所需角色)的麻烦。
此方法也可以与
method
提供HTTP方法
的
属性
结合使用
,以进行更具体的匹配。
通过将
var
属性
设置
为变量名称,
可以将评估标记(无论是授予还是拒绝访问)的布尔结果存储在页面上下文范围变量中
,而无需在页面的其他位置重复和重新评估条件。
禁用测试的标签授权
在页面上为未授权用户隐藏链接不会阻止他们访问URL。
例如,他们可以直接将其键入浏览器中。
在测试过程中,您可能希望显示隐藏区域,以检查链接在后端是否真正固定。
如果将system属性设置
spring.security.disableUISecurity
为
true
,则
authorize
标记仍将运行,但不会隐藏其内容。
默认情况下,它还将用
<span class="securityHiddenUI">…</span>
标签
包围内容
。
这使您可以显示具有特定CSS样式(例如不同的背景颜色)的“隐藏”内容。
例如,尝试在启用此属性的情况下运行“教程”示例应用程序。
您还可以设置属性,
spring.security.securedUIPrefix
以及
spring.security.securedUISuffix
是否要从默认
span
标签
更改周围的文本
(或使用空字符串将其完全删除)。
14.9.3. 认证标签
该标签允许访问
Authentication
存储在安全性上下文中
的当前
对象。
它直接在JSP中呈现对象的属性。
因此,例如,如果的
principal
属性
Authentication
是Spring Security
UserDetails
对象
的实例
,则using
<sec:authentication property="principal.username" />
将呈现当前用户的名称。
当然,对于这种事情,不必使用JSP标记,并且某些人希望在视图中保持尽可能少的逻辑。
您可以
Authentication
在MVC控制器中
访问该
对象(通过调用
SecurityContextHolder.getContext().getAuthentication()
),并将数据直接添加到模型中以通过视图进行渲染。
14.9.4. accesscontrollist标记
该标签仅在与Spring Security的ACL模块一起使用时才有效。 它检查以逗号分隔的指定域对象的所需权限列表。 如果当前用户拥有所有这些权限,则将评估标签正文。 如果他们不这样做,它将被跳过。 一个例子可能是
| 通常,应将此标签视为已弃用。 而是使用 The Authorize Tag 。 |
<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
实例,因此它们可以是工厂支持的任何格式-它们不必是整数,它们可以是诸如
READ
或的
字符串
WRITE
。
如果未
PermissionFactory
找到,
DefaultPermissionFactory
将使用
的实例
。
在
AclService
从应用程序上下文将被用于加载
Acl
例如用于所提供的对象。
该
Acl
会与所需的权限调用,以检查是否所有的人都理所当然的。
此标签也支持与标签
var
相同
的
属性
authorize
。
14.9.5. csrfInput标签
如果启用了CSRF保护,则此标签会插入一个隐藏的表单字段,其中包含CSRF保护令牌的正确名称和值。 如果未启用CSRF保护,则此标签不输出任何内容。
通常,Spring Security会为
<form:form>
您使用的
任何
标签
自动插入CSRF表单字段
,但是如果由于某些原因您不能使用
<form:form>
,
csrfInput
则可以方便地替换。
您应该将此标记放置在HTML
<form></form>
块中,通常将其放置在其他输入字段中。
不要将此标签放在弹簧
<form:form></form:form>
块中。
Spring Security自动处理Spring表单。
<form method="post" action="/do/something">
<sec:csrfInput />
Name:<br />
<input type="text" name="name" />
...
</form>
14.9.6. csrfMetaTags标签
如果启用了CSRF保护,则此标记将插入包含CSRF保护令牌形式字段,标头名称和CSRF保护令牌值的元标记。 这些元标记对于在应用程序中的JavaScript中采用CSRF保护很有用。
您应将其放置
csrfMetaTags
在HTML
<head></head>
块中,通常将其放置在其他
HTML
标签中。
使用此标记后,您可以使用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", "https://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", "https://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: "https://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: "https://www.example.org/do/something",
type: "POST",
headers: headers,
...
});
<script>
</head>
<body>
...
</body>
</html>
如果未启用CSRF保护,则不
csrfMetaTags
输出任何内容。
15. Java配置
Spring 3.1在Spring Framework中添加了 对 Java配置的 常规支持 。 从Spring Security 3.2开始,已经有Spring Security Java Configuration支持,它使用户可以轻松配置Spring Security,而无需使用任何XML。
如果您熟悉 安全命名空间配置, 则应该在它与安全Java配置支持之间找到很多相似之处。
| Spring Security提供了 许多示例应用程序 ,它们演示了Spring Security Java Configuration的使用。 |
15.1. Hello Web Security Java配置
第一步是创建我们的Spring Security Java配置。
该配置将创建一个称为的Servlet过滤器,该过滤器
springSecurityFilterChain
负责应用程序中的所有安全性(保护应用程序URL,验证提交的用户名和密码,重定向到登录表单等)。
您可以在下面找到Spring Security Java配置的最基本示例:
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 {
@Bean
public UserDetailsService userDetailsService() {
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withDefaultPasswordEncoder().username("user").password("password").roles("USER").build());
return manager;
}
}
此配置的确没有太多,但是它做了很多。 您可以找到以下功能的摘要:
-
要求对应用程序中的每个URL进行身份验证
-
为您生成一个登录表单
-
允许具有“ 用户名” 用户 和“ 密码” 密码 的用户使用基于表单的身份验证进行身份验证
-
允许用户注销
-
CSRF攻击 预防
-
会话固定 保护
-
安全标题集成
-
HTTP严格传输安全性 用于安全请求
-
缓存控制(以后可以由您的应用程序覆盖,以允许缓存您的静态资源)
-
X-Frame-Options集成有助于防止 Clickjacking
-
-
与以下Servlet API方法集成
15.1.1. AbstractSecurityWebApplicationInitializer
下一步是向
springSecurityFilterChain
战争
登记
。
可以在Java配置中,
在Servlet 3.0+环境中
使用
Spring的WebApplicationInitializer支持
。
毫不奇怪,Spring Security提供了一个基类
AbstractSecurityWebApplicationInitializer
,它将确保
springSecurityFilterChain
为您注册获取。
我们使用的方式会
AbstractSecurityWebApplicationInitializer
有所不同,具体取决于我们是否已经在使用Spring,或者Spring Security是应用程序中唯一的Spring组件。
-
不存在Spring的AbstractSecurityWebApplicationInitializer- 如果您尚未使用Spring,请使用这些说明
-
带有Spring MVC的AbstractSecurityWebApplicationInitializer- 如果您已经在使用Spring,请使用这些说明
15.1.2. 没有现有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 。
15.1.3. 带有Spring MVC的AbstractSecurityWebApplicationInitializer
如果我们在应用程序的其他地方使用Spring,则可能已经有一个
WebApplicationInitializer
正在加载Spring Configuration的代码。
如果我们使用以前的配置,将会得到一个错误。
相反,我们应该在现有上注册Spring Security
ApplicationContext
。
例如,如果我们使用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 ...
}
15.2. HttpSecurity
到目前为止,我们的
WebSecurityConfig
仅包含有关如何验证用户身份的信息。
Spring Security如何知道我们要要求所有用户进行身份验证?
Spring Security如何知道我们要支持基于表单的身份验证?
实际上,在后台调用了一个配置类
WebSecurityConfigurerAdapter
。
它具有
configure
使用以下默认实现
调用的方法
:
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests(authorizeRequests ->
authorizeRequests
.anyRequest().authenticated()
)
.formLogin(withDefaults())
.httpBasic(withDefaults());
}
上面的默认配置:
-
确保对我们应用程序的任何请求都需要对用户进行身份验证
-
允许用户使用基于表单的登录进行身份验证
-
允许用户使用HTTP Basic身份验证进行身份验证
您会注意到该配置与XML命名空间配置非常相似:
<http>
<intercept-url pattern="/**" access="authenticated"/>
<form-login />
<http-basic />
</http>
15.3. 多个HttpSecurity
我们可以配置多个HttpSecurity实例,就像我们可以拥有多个
<http>
块一样。
关键是要
WebSecurityConfigurerAdapter
多次
扩展
。
例如,以下是对以开头的URL具有不同配置的示例
/api/
。
@EnableWebSecurity
public class MultiHttpSecurityConfig {
@Bean (1)
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) (2)
public static class ApiWebSecurityConfigurationAdapter extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) throws Exception {
http
.antMatcher("/api/**") (3)
.authorizeRequests(authorizeRequests ->
authorizeRequests
.anyRequest().hasRole("ADMIN")
)
.httpBasic(withDefaults());
}
}
@Configuration (4)
public static class FormLoginWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests(authorizeRequests ->
authorizeRequests
.anyRequest().authenticated()
)
.formLogin(withDefaults());
}
}
}
| 1个 | 正常配置身份验证 |
| 2 |
创建一个
WebSecurityConfigurerAdapter
包含
的实例
,
@Order
以指定
WebSecurityConfigurerAdapter
应首先考虑的对象。
|
| 3 |
在
http.antMatcher
美国,这
HttpSecurity
将只适用于与开头的网址
/api/ |
| 4 |
创建的另一个实例
WebSecurityConfigurerAdapter
。
如果URL不以
/api/
该配置
开头,
则将使用此配置。
以后考虑此配置,
ApiWebSecurityConfigurationAdapter
因为它的
@Order
值是after
1
(没有
@Order
默认值是last)。
|
15.4. 定制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方法中的代码被调用
如果需要,可以
使用默认
WebSecurityConfiguerAdapter
添加
。
例如,您将在
具有以下内容
的类路径上创建资源
:
MyCustomDsl
SpringFactories
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()
...;
}
}
15.5. 后处理配置的对象
Spring Security的Java配置并未公开其配置的每个对象的每个属性。 这简化了大多数用户的配置。 毕竟,如果每个属性都公开,则用户可以使用标准Bean配置。
尽管有充分的理由不直接公开每个属性,但用户可能仍需要更多高级配置选项。
为了解决这个问题,Spring Security引入了an的概念,该概念
ObjectPostProcessor
可用于修改或替换Java配置创建的许多Object实例。
例如,如果要配置
filterSecurityPublishAuthorizationSuccess
属性,
FilterSecurityInterceptor
可以使用以下命令:
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests(authorizeRequests ->
authorizeRequests
.anyRequest().authenticated()
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
public <O extends FilterSecurityInterceptor> O postProcess(
O fsi) {
fsi.setPublishAuthorizationSuccess(true);
return fsi;
}
})
);
}
16.安全命名空间配置
16.1. 介绍
从Spring Framework 2.0版开始,可以使用命名空间配置。 它允许您使用其他XML模式中的元素来补充传统的Spring bean应用程序上下文语法。 您可以在Spring Reference文档中 找到更多信息。 。 名称空间元素可以简单地用于允许以更简洁的方式配置单个bean,或者更强大地定义一种替代配置语法,该语法与问题域更紧密地匹配并向用户隐藏底层的复杂性。 一个简单的元素可能掩盖了将多个bean和处理步骤添加到应用程序上下文的事实。 例如,将以下元素从安全名称空间添加到应用程序上下文中将启动嵌入式LDAP服务器,以测试应用程序中的使用情况:
<security:ldap-server />
这比连接等效的Apache Directory Server Bean要简单得多。
ldap-server
元素
上的属性支持最常见的替代配置要求,
并且用户不必担心他们需要创建哪些bean以及bean属性名称是什么。
[ 12 ]
。
在编辑应用程序上下文文件时使用良好的XML编辑器应提供有关可用属性和元素的信息。
我们建议您尝试一下
Spring Tool Suite,
因为它具有使用标准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
https://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/security
https://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
https://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/security
https://www.springframework.org/schema/security/spring-security.xsd">
...
</beans:beans>
从本章开始,我们将假定使用此语法。
16.1.1. 命名空间的设计
命名空间旨在捕获框架的最常见用法,并提供简化和简洁的语法以在应用程序中启用它们。 该设计基于框架内的大规模依赖关系,并且可以分为以下几个方面:
-
Web / HTTP安全性 -最复杂的部分。 设置用于应用框架身份验证机制的过滤器和相关服务Bean,以保护URL,呈现登录页面和错误页面等。
-
业务对象(方法)安全性 -用于保护服务层的选项。
-
AuthenticationManager- 处理来自框架其他部分的身份验证请求。
-
AccessDecisionManager- 提供有关Web和方法安全性的访问决策。 默认的将被注册,但是您也可以选择使用自定义的,使用常规的Spring bean语法声明。
-
AuthenticationProvider s-身份验证管理器用来验证用户身份 的机制。 命名空间提供了对几个标准选项的支持,还提供了添加使用传统语法声明的自定义bean的方法。
-
UserDetailsService- 与身份验证提供者密切相关,但其他bean通常也需要。
我们将在以下各节中介绍如何配置它们。
16.2. 安全命名空间配置入门
在本节中,我们将研究如何构建名称空间配置以使用框架的一些主要功能。 假设您最初希望尽快启动并运行,并通过一些测试登录来向现有Web应用程序添加身份验证支持和访问控制。 然后,我们将研究如何转换为针对数据库或其他安全性存储库的身份验证。 在后面的部分中,我们将介绍更高级的名称空间配置选项。
16.2.1. 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 Framework类,它委托一个过滤器实现,该实现在您的应用程序上下文中定义为Spring Bean。
在这种情况下,该Bean名为“ springSecurityFilterChain”,它是由名称空间创建的内部基础结构Bean,用于处理Web安全。
请注意,您不应自己使用此bean名称。
将其添加到中后
web.xml
,就可以开始编辑应用程序上下文文件了。
Web安全服务是使用
<http>
元素
配置的
。
16.2.2. 最小的<http>配置
启用Web安全所需要做的就是
<http>
<intercept-url pattern="/**" access="hasRole('USER')" />
<form-login />
<logout />
</http>
这表示我们希望保护应用程序中所有URL的安全,需要角色
ROLE_USER
来访问它们,我们希望使用带有用户名和密码的表单登录到该应用程序,并希望注册一个注销URL,这将使我们能够注销该应用程序。
<http>
元素是所有与Web相关的名称空间功能的父级。
<intercept-url>
元素
pattern
使用ant路径样式语法
[ 13 ]
定义了
与传入请求的URL匹配
的
元素
。
您还可以使用正则表达式匹配作为替代(有关更多详细信息,请参见名称空间附录)。
的
access
属性定义与给定模式匹配的请求的访问要求。
在默认配置下,这通常是一个用逗号分隔的角色列表,必须允许用户发出一个角色来进行请求。
前缀“ ROLE_”是一个标记,指示应与用户权限进行简单比较。
换句话说,应该使用基于角色的常规检查。
Spring Security中的访问控制不限于使用简单角色(因此使用前缀来区分不同类型的安全属性)。
稍后我们将看到解释如何变化
[ 14 ]
。
在Spring Security 3.0中,该属性还可以填充“ 1”。
| === |
您可以使用多个
<intercept-url>
元素为不同的URL集定义不同的访问要求,但是将按照列出的顺序评估它们,并且将使用第一个匹配项。
因此,您必须将最具体的匹配项放在顶部。
还可以添加一个
method
属性匹配限制在一个特定的HTTP方法(
GET
,
POST
,
PUT
等等)。
===
要添加一些用户,可以直接在名称空间中定义一组测试数据:
<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}
来指示
DelegatingPasswordEncoder
,它支持任何被配置
PasswordEncoder
为匹配,即密码使用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>
上面的配置定义了两个用户,他们的密码和他们在应用程序中的角色(将用于访问控制)。
也可以使用
properties
on
属性从标准属性文件中加载用户信息
user-service
。
有关
文件格式的更多详细信息,
请参见
内存中身份验证
部分
。
使用该
<authentication-provider>
元素意味着身份验证管理器将使用用户信息来处理身份验证请求。
您可以具有多个
<authentication-provider>
元素来定义不同的身份验证源,并且将依次进行咨询。
此时,您应该可以启动应用程序,并且需要登录才能继续。 试试看,或尝试使用该项目随附的“教程”示例应用程序。
设置默认的登录后目标
如果尝试访问受保护的资源未提示登录表单,则该
default-target-url
选项起作用。
这是用户成功登录后将转到的URL,默认为“ /”。
您还可以配置
属性,以
使用户
始终
将页面设置
always-use-default-target
为“ true”
(无论登录是“按需”还是明确选择登录,无论
该用户是
最终
用户
)
。
如果您的应用程序始终要求用户从“主页”页面开始,这将很有用,例如:
<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
。
您可以在“
核心过滤器”
一章以及名称空间附录中
找到有关此内容的更多
信息,以及有关在身份验证失败时如何自定义流程的信息。
16.3. 进阶网页功能
16.3.1. 添加自己的过滤器
如果您以前使用过Spring Security,那么您会知道该框架维护着一系列过滤器以应用其服务。
您可能想在特定位置将自己的过滤器添加到堆栈中,或使用当前没有名称空间配置选项(例如CAS)的Spring Security过滤器。
或者,您可能希望使用标准名称空间过滤器的自定义版本,例如
UsernamePasswordAuthenticationFilter
由
<form-login>
元素
创建的
,以利用显式使用Bean可用的一些额外配置选项。
由于过滤器链未直接公开,您如何使用名称空间配置来做到这一点?
使用名称空间时,始终严格执行过滤器的顺序。 创建应用程序上下文时,过滤器bean按照名称空间处理代码进行排序,标准的Spring Security过滤器每个在名称空间中都有一个别名和一个众所周知的位置。
===在以前的版本中,在应用程序上下文的后处理期间,排序是在创建过滤器实例之后进行的。
在版本3.0+中,现在在实例化类之前在bean元数据级别完成排序。
这对如何将自己的过滤器添加到堆栈有影响,因为在解析
<http>
元素
期间必须知道整个过滤器列表
,因此在3.0中语法略有更改。
===
|
创建过滤器的过滤器,别名和名称空间元素/属性在“ 标准过滤器别名和顺序”中显示 。 筛选器按它们在筛选器链中出现的顺序列出。
| 别名 | 过滤等级 | 命名空间元素或属性 |
|---|---|---|
|
CHANNEL_FILTER |
|
|
|
SECURITY_CONTEXT_FILTER |
|
|
|
CONCURRENT_SESSION_FILTER |
|
|
|
HEADERS_FILTER |
|
|
|
CSRF_FILTER |
|
|
|
LOGOUT_FILTER |
|
|
|
X509_FILTER |
|
|
|
PRE_AUTH_FILTER |
|
不适用 |
|
CAS_FILTER |
|
不适用 |
|
FORM_LOGIN_FILTER |
|
|
|
BASIC_AUTH_FILTER |
|
|
|
SERVLET_API_SUPPORT_FILTER |
|
|
|
JAAS_API_SUPPORT_FILTER |
|
|
|
REMEMBER_ME_FILTER |
|
|
|
ANONYMOUS_FILTER |
|
|
|
SESSION_MANAGEMENT_FILTER |
|
|
|
EXCEPTION_TRANSLATION_FILTER |
|
|
|
FILTER_SECURITY_INTERCEPTOR |
|
|
|
SWITCH_USER_FILTER |
|
不适用 |
您可以使用
custom-filter
元素和以下名称之一指定
自己的过滤器到堆栈中,
以指定过滤器应出现的位置:
<http>
<custom-filter position="FORM_LOGIN_FILTER" ref="myFilter" />
</http>
<beans:bean id="myFilter" class="com.mycompany.MySpecialAuthenticationFilter"/>
如果要在堆栈中的另一个过滤器之前或之后插入过滤器,
也可以使用
after
或
before
属性。
名称“ FIRST”和“ LAST”可与该
position
属性一起
使用,
以指示您希望过滤器分别出现在整个堆栈之前或之后。
|
避免过滤器位置冲突
===
|
如果您要插入一个自定义过滤器,该过滤器可能与名称空间创建的标准过滤器之一处于同一位置,那么不要误输入名称空间版本,这一点很重要。 删除所有创建要替换其功能的过滤器的元素。
请注意,你不能替换那些使用中创建的过滤器
<http>
元件本身-
SecurityContextPersistenceFilter
,
ExceptionTranslationFilter
或
FilterSecurityInterceptor
。
默认情况下会添加其他一些过滤器,但是您可以禁用它们。
AnonymousAuthenticationFilter
默认情况下会添加
an
,除非您
禁用了
会话固定保护
,否则
a
SessionManagementFilter
也将添加到过滤器链中。
===
如果要替换需要身份验证入口点的名称空间过滤器(即,身份验证过程是由未经身份验证的用户尝试访问安全资源而触发的),则也需要添加自定义入口点Bean。
16.4. 方法安全性
从2.0版开始,Spring Security大大改进了对为服务层方法增加安全性的支持。
它提供对JSR-250注释安全性以及框架原始
@Secured
注释的支持。
从3.0开始,您还可以使用新
的基于表达式的注释
。
您可以使用
intercept-methods
元素来装饰Bean声明,
从而对单个Bean应用安全
性,或者可以使用AspectJ样式切入点在整个服务层中保护多个Bean。
16.5. 默认的AccessDecisionManager
本部分假定您具有Spring Security中用于访问控制的基础架构的一些知识。 如果不这样做,则可以跳过它,稍后再返回,因为此部分仅与需要进行一些自定义以便使用更多功能(而不是简单的基于角色的安全性)的人员有关。
当您使用命名空间配置时,
AccessDecisionManager
系统会根据您在
intercept-url
和
protect-pointcut
声明(如果使用的是注解)中
指定的访问属性,自动为您注册
一个默认实例,
并将其用于为方法调用和Web URL访问制定访问决策。
使用注释安全方法)。
默认策略是使用
AffirmativeBased AccessDecisionManager
带有
RoleVoter
和的
AuthenticatedVoter
。
您可以在
授权
一章中找到更多有关这些的信息
。
16.5.1. 自定义AccessDecisionManager
如果您需要使用更复杂的访问控制策略,则可以轻松设置方法和Web安全性的替代方案。
为了实现方法安全,您可以通过
在应用程序上下文
中将
access-decision-manager-ref
属性
设置
为适当的
bean的
on
global-method-security
来实现
:
id
AccessDecisionManager
<global-method-security access-decision-manager-ref="myAccessDecisionManagerBean">
...
</global-method-security>
Web安全的语法是相同的,但是在
http
元素上:
<http access-decision-manager-ref="myAccessDecisionManagerBean">
...
</http>
17.测试
本节描述了Spring Security提供的测试支持。
|
要使用Spring Security测试支持,您必须包括
|
17.1. 测试方法安全
本节演示了如何使用Spring Security的Test支持来测试基于方法的安全性。
我们首先引入一个
MessageService
,要求用户经过身份验证才能访问它。
public class HelloMessageService implements MessageService {
@PreAuthorize("authenticated")
public String getMessage() {
Authentication authentication = SecurityContextHolder.getContext()
.getAuthentication();
return "Hello " + authentication;
}
}
结果
getMessage
是一个字符串,向当前的Spring Security说“ Hello”
Authentication
。
输出示例如下所示。
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
17.1.1. 安全测试设置
在使用Spring Security Test支持之前,我们必须执行一些设置。 可以在下面看到一个示例:
@RunWith(SpringJUnit4ClassRunner.class) (1)
@ContextConfiguration (2)
public class WithMockUserTests {
这是如何设置Spring Security Test的基本示例。 重点是:
| 1个 | @RunWith
指示spring-test模块应该创建一个
ApplicationContext
。
这与使用现有的Spring Test支持没有什么不同。
有关更多信息,请 Reference
Spring Reference。
|
| 2 | @ContextConfiguration
指示spring-test配置用于创建
ApplicationContext
。
由于未指定任何配置,因此将尝试使用默认配置位置。
这与使用现有的Spring Test支持没有什么不同。
有关更多信息,请 Reference
Spring Reference。
|
Spring Security使用来加入Spring Test支持,
WithSecurityContextTestExecutionListener
这将确保我们的测试以正确的用户运行。
它是通过
SecurityContextHolder
在运行我们的测试之前先
填充
来实现的。
如果您正在使用反应性方法安全性,则还需要
ReactorContextTestExecutionListener
填充
ReactiveSecurityContextHolder
。
测试完成后,它将清除
SecurityContextHolder
。
如果只需要与Spring Security相关的支持,则可以替换
@ContextConfiguration
为
@SecurityTestExecutionListeners
。
|
请记住,我们在我们的
@PreAuthorize
注释中
添加了
注释
HelloMessageService
,因此它需要经过身份验证的用户才能调用它。
如果我们运行以下测试,我们期望以下测试将通过:
@Test(expected = AuthenticationCredentialsNotFoundException.class)
public void getMessageUnauthenticated() {
messageService.getMessage();
}
17.1.2. @WithMockUser
问题是“我们如何最轻松地以特定用户身份运行测试?”
答案是使用
@WithMockUser
。
以下测试将以用户名“ user”,密码“ password”和角色“ ROLE_USER”的用户身份运行。
@Test
@WithMockUser
public void getMessageWithMockUser() {
String message = messageService.getMessage();
...
}
具体来说,以下是正确的:
-
用户名“ user”的用户不必存在,因为我们在模拟用户
-
将
Authentication所填充的SecurityContext类型为UsernamePasswordAuthenticationToken -
的主体
Authentication是Spring Security的User对象 -
在
User将“用户”,密码为“密码”的用户名和一个GrantedAuthority名为“ROLE_USER”一词。
我们的示例很好,因为我们能够利用很多默认值。 如果我们想使用其他用户名运行测试该怎么办? 以下测试将使用用户名“ 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_作为前缀,则可以利用Authority属性。 例如,将使用用户名“ 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 {
默认情况下,
SecurityContext
是在
TestExecutionListener.beforeTestMethod
活动
期间设置的
。
这等效于在JUnit之前发生的情况
@Before
。
您可以将其更改为在
TestExecutionListener.beforeTestExecution
事件
期间发生,该
事件在JUnit之后
@Before
但在调用test方法之前。
@WithMockUser(setupBefore = TestExecutionEvent.TEST_EXECUTION)
17.1.3. @WithAnonymousUser
使用
@WithAnonymousUser
允许以匿名用户身份运行。
当您希望与特定用户一起运行大多数测试,但希望以匿名用户身份运行一些测试时,这特别方便。
例如,以下将使用
@WithMockUser
和匿名用户(匿名用户)
在withMockUser1和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
}
}
默认情况下,
SecurityContext
是在
TestExecutionListener.beforeTestMethod
活动
期间设置的
。
这等效于在JUnit之前发生的情况
@Before
。
您可以将其更改为在
TestExecutionListener.beforeTestExecution
事件
期间发生,该
事件在JUnit之后
@Before
但在调用test方法之前。
@WithAnonymousUser(setupBefore = TestExecutionEvent.TEST_EXECUTION)
17.1.4. @WithUserDetails
虽然这
@WithMockUser
是一种非常方便的入门方法,但可能并非在所有情况下都有效。
例如,对于应用程序来说,通常期望
Authentication
主体是特定类型。
这样做是为了使应用程序可以将委托人称为自定义类型,并减少Spring Security上的耦合。
自定义主体通常是由一个自定义返回的,该自定义
UserDetailsService
返回一个同时实现
UserDetails
自定义类型和自定义类型
的对象
。
在这种情况下,使用custom创建测试用户很有用
UserDetailsService
。
正是
@WithUserDetails
这样。
假设我们有一个
UserDetailsService
公开的bean,下面的测试将使用
Authentication
类型
UsernamePasswordAuthenticationToken
为,并从
UserDetailsService
用户名为“ user”
的主体返回的
测试调用
。
@Test
@WithUserDetails
public void getMessageWithUserDetails() {
String message = messageService.getMessage();
...
}
我们还可以自定义用于从中查找用户的用户名
UserDetailsService
。
例如,将使用从
UserDetailsService
用户名“ customUsername”
返回的委托人执行此测试
。
@Test
@WithUserDetails("customUsername")
public void getMessageWithUserDetailsCustomUsername() {
String message = messageService.getMessage();
...
}
我们还可以提供一个明确的bean名称来查找
UserDetailsService
。
例如,此测试将使用
UserDetailsService
具有Bean名称“ myUserDetailsService”的
来查找用户
名“ customUsername”。
@Test
@WithUserDetails(value="customUsername", userDetailsServiceBeanName="myUserDetailsService")
public void getMessageWithUserDetailsServiceBeanName() {
String message = messageService.getMessage();
...
}
就像
@WithMockUser
我们也可以将注释放在类级别一样,以便每个测试使用相同的用户。
但是,与不同
@WithMockUser
,
@WithUserDetails
需要用户存在。
默认情况下,
SecurityContext
是在
TestExecutionListener.beforeTestMethod
活动
期间设置的
。
这等效于在JUnit之前发生的情况
@Before
。
您可以将其更改为在
TestExecutionListener.beforeTestExecution
事件
期间发生,该
事件在JUnit之后
@Before
但在调用test方法之前。
@WithUserDetails(setupBefore = TestExecutionEvent.TEST_EXECUTION)
17.1.5. @WithSecurityContext
我们已经看到,
@WithMockUser
如果不使用自定义
Authentication
主体
,这
是一个绝佳的选择
。
接下来,我们发现这
@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
,将创建一个新的
SecurityContext
给我们的
@WithMockCustomUser
注解。
您可以在
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;
}
}
默认情况下,
SecurityContext
是在
TestExecutionListener.beforeTestMethod
活动
期间设置的
。
这等效于在JUnit之前发生的情况
@Before
。
您可以将其更改为在
TestExecutionListener.beforeTestExecution
事件
期间发生,该
事件在JUnit之后
@Before
但在调用test方法之前。
@WithSecurityContext(setupBefore = TestExecutionEvent.TEST_EXECUTION)
17.1.6. 测试元注释
如果您经常在测试中重用同一用户,则不理想的是必须重复指定属性。
例如,如果有很多相关的管理用户的用户名“管理员”和角色的测试
ROLE_USER
和
ROLE_ADMIN
你会写:
@WithMockUser(username="admin",roles={"USER","ADMIN"})
我们可以使用元注释,而不是在所有地方重复此操作。
例如,我们可以创建一个名为的元注释
WithMockAdmin
:
@Retention(RetentionPolicy.RUNTIME)
@WithMockUser(value="rob",roles="ADMIN")
public @interface WithMockAdmin { }
现在,我们可以
@WithMockAdmin
以更详细的方式
使用
@WithMockUser
。
元注释可与上述任何测试注释一起使用。
例如,这意味着我们也可以为其创建一个元注释
@WithUserDetails("admin")
。
17.2. Spring MVC测试集成
Spring Security提供与 Spring MVC Test的 全面集成
17.2.1. 设置MockMvc和Spring Security
为了将Spring Security与Spring MVC Test一起使用,必须将Spring Security添加
FilterChainProxy
为
Filter
。
还需要添加Spring Security
TestSecurityContextHolderPostProcessor
来支持
在带有注释的Spring MVC Test中以用户身份运行
。
可以使用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()) (1)
.build();
}
...
| 1个 | SecurityMockMvcConfigurers.springSecurity()
将执行我们将Spring Security与Spring MVC Test集成所需的所有初始设置
|
17.2.2. 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 Test中使用RequestPostProcessor以用户身份运行
有许多选项可将用户与当前用户相关联
HttpServletRequest
。
例如,以下将以用户名(用户),密码“ password”和角色“ ROLE_USER”的用户身份(不需要存在)运行:
|
该支持通过将用户与关联而起作用
|
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
(不需要存在)与
UsernamePasswordAuthenticationToken
具有指定的主体的
一起运行
UserDetails
:
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
创建用户
的替代方法
,您可以使用
测试方法安全性中
描述的注释
。
例如,以下将对具有用户名“ 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("/"))
...
}
17.2.3. 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"))
测试承载身份验证
为了在资源服务器上发出授权请求,您需要一个承载令牌。 如果为JWT配置了资源服务器,则这意味着需要对承载令牌进行签名,然后根据JWT规范进行编码。 所有这些都可能令人望而生畏,特别是当这不是您测试的重点时。
幸运的是,您可以通过多种简单的方法来克服此困难,并使您的测试专注于授权而不是表示承载令牌。 我们现在来看其中两个:
jwt() RequestPostProcessor
第一种方法是通过
RequestPostProcessor
。
其中最简单的如下所示:
mvc
.perform(get("/endpoint").with(jwt()));
要做的是创建一个模拟程序
Jwt
,将其正确地通过任何身份验证API传递,以便您的授权机制可以对其进行验证。
默认情况下,
JWT
它创建的具有以下特征:
{
"headers" : { "alg" : "none" },
"claims" : {
"sub" : "user",
"scope" : "read"
}
}
并将得到的
Jwt
,是测试它,会通过下列方式:
assertThat(jwt.getTokenValue()).isEqualTo("token");
assertThat(jwt.getHeaders().get("alg")).isEqualTo("none");
assertThat(jwt.getSubject()).isEqualTo("sub");
GrantedAuthority authority = jwt.getAuthorities().iterator().next();
assertThat(authority.getAuthority()).isEqualTo("read");
当然可以配置这些值。
可以使用其相应方法配置任何标题或声明:
mvc
.perform(get("/endpoint")
.with(jwt(jwt -> jwt.header("kid", "one").claim("iss", "https://idp.example.org"))));
mvc
.perform(get("/endpoint")
.with(jwt(jwt -> jwt.claims(claims -> claims.remove("scope")))));
的
scope
和
scp
权利要求被处理的相同的方式在这里,因为它们是在一个正常的承载令牌请求。
但是,可以通过提供
GrantedAuthority
测试所需
的
实例
列表来覆盖
它:
mvc
.perform(get("/endpoint")
.with(jwt().authorities(new SimpleGrantedAuthority("SCOPE_messages"))));
或者,如果您有
Jwt
要
Collection<GrantedAuthority>
转换
的自定义
,也可以使用它来导出权限:
mvc
.perform(get("/endpoint")
.with(jwt().authorities(new MyConverter())));
您还可以指定一个complete
Jwt
,对于它
Jwt.Builder
来说非常方便:
Jwt jwt = Jwt.withTokenValue("token")
.header("alg", "none")
.claim("sub", "user")
.claim("scope", "read");
mvc
.perform(get("/endpoint")
.with(jwt(jwt)));
authentication() RequestPostProcessor
第二种方法是使用
authentication() RequestPostProcessor
。
本质上,您可以实例化自己的实例
JwtAuthenticationToken
并在测试中提供它,如下所示:
Jwt jwt = Jwt.withTokenValue("token")
.header("alg", "none")
.claim("sub", "user")
.build();
Collection<GrantedAuthority> authorities = AuthorityUtils.createAuthorityList("SCOPE_read");
JwtAuthenticationToken token = new JwtAuthenticationToken(jwt, authorities);
mvc
.perform(get("/endpoint")
.with(authentication(token)));
请注意,作为替代方法,您还可以
JwtDecoder
使用
@MockBean
注释
来模拟
bean本身
。
17.2.4. 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)));
18. Spring Security加密模块
18.1. 介绍
Spring Security Crypto模块提供对对称加密,密钥生成和密码编码的支持。 该代码作为核心模块的一部分分发,但与任何其他Spring Security(或Spring)代码无关。
18.2. 加密器
Encryptors类提供了用于构造对称加密器的工厂方法。 使用此类,您可以创建ByteEncryptor来以原始byte []形式加密数据。 您还可以构造TextEncryptor来加密文本字符串。 加密器是线程安全的。
18.2.1. 字节加密器
使用Encryptors.standard工厂方法来构造“标准” BytesEncryptor:
Encryptors.standard("password", "salt");
“标准”加密方法是使用PKCS#5的PBKDF2(基于密码的密钥派生功能#2)的256位AES。 此方法需要Java6。用于生成SecretKey的密码应保存在安全的地方,并且不能共享。 如果您的加密数据遭到破坏,该盐可用于防止针对密钥的字典攻击。 还应用了16字节的随机初始化向量,因此每个加密的消息都是唯一的。
提供的盐应采用十六进制编码的字符串形式,并且是随机的,并且长度至少为8个字节。 可以使用KeyGenerator生成这种盐:
String salt = KeyGenerators.string().generateKey(); // generates a random 8-byte salt that is then hex-encoded
18.2.2. TextEncryptor
使用Encryptors.text工厂方法构造一个标准的TextEncryptor:
Encryptors.text("password", "salt");
TextEncryptor使用标准的BytesEncryptor来加密文本数据。 加密结果以十六进制编码的字符串形式返回,以便于存储在文件系统或数据库中。
使用Encryptors.queryableText工厂方法构造一个“可查询的” TextEncryptor:
Encryptors.queryableText("password", "salt");
可查询的TextEncryptor与标准TextEncryptor之间的区别与初始化向量(iv)处理有关。 可查询TextEncryptor#encrypt操作中使用的iv是共享的或常量,并且不会随机生成。 这意味着多次加密相同的文本将始终产生相同的加密结果。 这不太安全,但是对于需要查询的加密数据来说是必需的。 可查询的加密文本的一个示例是OAuth apiKey。
18.3. 密钥生成器
KeyGenerators类为构造不同类型的密钥生成器提供了许多便利的工厂方法。 使用此类,您可以创建一个BytesKeyGenerator来生成byte []键。 您还可以构造一个StringKeyGenerator来生成字符串键。 KeyGenerators是线程安全的。
18.3.1. 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);
18.4. 密码编码
spring-security-crypto模块的密码软件包提供了对密码编码的支持。
PasswordEncoder
是中央服务接口,具有以下签名:
public interface PasswordEncoder {
String encode(String rawPassword);
boolean matches(String rawPassword, String encodedPassword);
}
如果rawPassword一旦编码,等于已编码的Password,则matchs方法返回true。 此方法旨在支持基于密码的身份验证方案。
该
BCryptPasswordEncoder
实现使用广泛支持的“ bcrypt”算法对密码进行哈希处理。
Bcrypt使用一个随机的16字节盐值,并且是一种故意慢速的算法,目的是阻止密码破解者。
可以使用“ strength”参数调整它所做的工作量,该参数的取值范围为4到31。值越高,计算散列所需的工作就越多。
默认值为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));
19.附录
19.1. 安全数据库架构
框架使用了各种数据库架构,本附录为它们提供了单个 Reference点。 您只需要提供所需功能范围的表即可。
为HSQLDB数据库提供了DDL语句。 您可以将这些用作定义正在使用的数据库的架构的准则。
19.1.1. 用户架构
UserDetailsService
(
JdbcDaoImpl
)
的标准JDBC实现
要求表为用户加载密码,帐户状态(启用或禁用)和权限列表(角色)。
您将需要调整此架构以匹配您正在使用的数据库方言。
create table users(
username varchar_ignorecase(50) not null primary key,
password varchar_ignorecase(50) not null,
enabled boolean not null
);
create table authorities (
username varchar_ignorecase(50) not null,
authority varchar_ignorecase(50) not null,
constraint fk_authorities_users foreign key(username) references users(username)
);
create unique index ix_auth_username on authorities (username,authority);
对于Oracle数据库
CREATE TABLE USERS (
USERNAME NVARCHAR2(128) PRIMARY KEY,
PASSWORD NVARCHAR2(128) NOT NULL,
ENABLED CHAR(1) CHECK (ENABLED IN ('Y','N') ) NOT NULL
);
CREATE TABLE AUTHORITIES (
USERNAME NVARCHAR2(128) NOT NULL,
AUTHORITY NVARCHAR2(128) NOT NULL
);
ALTER TABLE AUTHORITIES ADD CONSTRAINT AUTHORITIES_UNIQUE UNIQUE (USERNAME, AUTHORITY);
ALTER TABLE AUTHORITIES ADD CONSTRAINT AUTHORITIES_FK1 FOREIGN KEY (USERNAME) REFERENCES USERS (USERNAME) ENABLE;
团体授权
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
实现
时才需要这些表
。
如果您自己编写或选择
AuthenticationProvider
不带实现
UserDetailsService
,则只要满足接口协定,就可以完全自由地存储数据。
19.1.2. 永久登录(记住我)架构
该表用于存储更安全的
持久令牌“
记住我”实现所使用的数据。
如果
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
);
19.1.3. ACL模式
Spring Security ACL 实现 使用了四个表 。
-
acl_sid存储ACL系统识别的安全标识。 这些可以是唯一的委托人,也可以适用于多个委托人。 -
acl_class定义适用于ACL的域对象类型。 该class列存储对象的Java类名称。 -
acl_object_identity存储特定域对象的对象标识定义。 -
acl_entry存储适用于特定对象标识和安全标识的ACL权限。
假设数据库将为每个身份自动生成主键。
在
JdbcMutableAclService
必须能够检索这些当它在创造了一个新的行
acl_sid
或
acl_class
表。
它具有两个属性,这些属性定义检索这些值
classIdentityQuery
和
所需的SQL
sidIdentityQuery
。
这些都默认为
call identity()
ACL工件JAR包含用于在HyperSQL(HSQLDB),PostgreSQL,MySQL / MariaDB,Microsoft SQL Server和Oracle数据库中创建ACL模式的文件。 在以下各节中还将演示这些架构。
超级SQL
默认模式与框架中的单元测试中使用的嵌入式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)
);
您必须分别将
classIdentityQuery
和的
sidIdentityQuery
属性设置
JdbcMutableAclService
为以下值:
-
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服务器
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)
);
甲骨文数据库
CREATE TABLE ACL_SID (
ID NUMBER(18) PRIMARY KEY,
PRINCIPAL NUMBER(1) NOT NULL CHECK (PRINCIPAL IN (0, 1 )),
SID NVARCHAR2(128) NOT NULL,
CONSTRAINT ACL_SID_UNIQUE UNIQUE (SID, PRINCIPAL)
);
CREATE SEQUENCE ACL_SID_SQ START WITH 1 INCREMENT BY 1 NOMAXVALUE;
CREATE OR REPLACE TRIGGER ACL_SID_SQ_TR BEFORE INSERT ON ACL_SID FOR EACH ROW
BEGIN
SELECT ACL_SID_SQ.NEXTVAL INTO :NEW.ID FROM DUAL;
END;
CREATE TABLE ACL_CLASS (
ID NUMBER(18) PRIMARY KEY,
CLASS NVARCHAR2(128) NOT NULL,
CONSTRAINT ACL_CLASS_UNIQUE UNIQUE (CLASS)
);
CREATE SEQUENCE ACL_CLASS_SQ START WITH 1 INCREMENT BY 1 NOMAXVALUE;
CREATE OR REPLACE TRIGGER ACL_CLASS_ID_TR BEFORE INSERT ON ACL_CLASS FOR EACH ROW
BEGIN
SELECT ACL_CLASS_SQ.NEXTVAL INTO :NEW.ID FROM DUAL;
END;
CREATE TABLE ACL_OBJECT_IDENTITY(
ID NUMBER(18) PRIMARY KEY,
OBJECT_ID_CLASS NUMBER(18) NOT NULL,
OBJECT_ID_IDENTITY NVARCHAR2(64) NOT NULL,
PARENT_OBJECT NUMBER(18),
OWNER_SID NUMBER(18),
ENTRIES_INHERITING NUMBER(1) NOT NULL CHECK (ENTRIES_INHERITING IN (0, 1)),
CONSTRAINT ACL_OBJECT_IDENTITY_UNIQUE UNIQUE (OBJECT_ID_CLASS, OBJECT_ID_IDENTITY),
CONSTRAINT ACL_OBJECT_IDENTITY_PARENT_FK FOREIGN KEY (PARENT_OBJECT) REFERENCES ACL_OBJECT_IDENTITY(ID),
CONSTRAINT ACL_OBJECT_IDENTITY_CLASS_FK FOREIGN KEY (OBJECT_ID_CLASS) REFERENCES ACL_CLASS(ID),
CONSTRAINT ACL_OBJECT_IDENTITY_OWNER_FK FOREIGN KEY (OWNER_SID) REFERENCES ACL_SID(ID)
);
CREATE SEQUENCE ACL_OBJECT_IDENTITY_SQ START WITH 1 INCREMENT BY 1 NOMAXVALUE;
CREATE OR REPLACE TRIGGER ACL_OBJECT_IDENTITY_ID_TR BEFORE INSERT ON ACL_OBJECT_IDENTITY FOR EACH ROW
BEGIN
SELECT ACL_OBJECT_IDENTITY_SQ.NEXTVAL INTO :NEW.ID FROM DUAL;
END;
CREATE TABLE ACL_ENTRY (
ID NUMBER(18) NOT NULL PRIMARY KEY,
ACL_OBJECT_IDENTITY NUMBER(18) NOT NULL,
ACE_ORDER INTEGER NOT NULL,
SID NUMBER(18) 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 ACL_ENTRY_UNIQUE UNIQUE (ACL_OBJECT_IDENTITY, ACE_ORDER),
CONSTRAINT ACL_ENTRY_OBJECT_FK FOREIGN KEY (ACL_OBJECT_IDENTITY) REFERENCES ACL_OBJECT_IDENTITY (ID),
CONSTRAINT ACL_ENTRY_ACL_FK FOREIGN KEY (SID) REFERENCES ACL_SID(ID)
);
CREATE SEQUENCE ACL_ENTRY_SQ 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_SQ.NEXTVAL INTO :NEW.ID FROM DUAL;
END;
19.2. 安全命名空间
本附录提供了对安全性名称空间中可用元素的引用以及它们创建的基础Bean的信息(假定您了解各个类以及它们如何协同工作-您可以在项目Javadoc和本文档的其他地方找到更多信息。 )。 如果您以前没有使用过命名空间,请阅读 有关命名空间配置 的 介绍性章节 ,因为这是对此处信息的补充。 建议在编辑基于模式的配置时使用高质量的XML编辑器,因为这将提供有关哪些元素和属性可用的上下文信息,以及说明其用途的注释。 名称空间用 RELAX NG 编写 紧凑格式,后来转换为XSD模式。 如果您熟悉此格式,则可能希望 直接 检查 模式文件 。
19.2.1. Web应用安全
<http>
如果您
<http>
在应用程序中
使用
元素,
FilterChainProxy
则会创建
一个
名为“ springSecurityFilterChain”
的
bean
,并
使用该
元素中
的配置在中建立过滤器链
FilterChainProxy
。
从Spring Security 3.1开始,
http
可以使用
其他
元素来添加额外的过滤器链
[ 15 ]
。
某些核心过滤器始终在过滤器链中创建,而其他核心过滤器将根据存在的属性和子元素添加到堆栈中。
标准过滤器的位置是固定的(请参见
名称空间介绍中
的过滤器顺序表
),当用户必须在配置文件中显式配置过滤器链时,消除了以前版本框架的常见错误源。
FilterChainProxy
豆角,扁豆。
当然,如果需要完全控制配置,您仍然可以这样做。
所有需要引用的过滤器
AuthenticationManager
都会自动注入由名称空间配置创建的内部实例(有关的
更多信息,
请参见
介绍性章节
AuthenticationManager
)。
每个
<http>
名称空间块始终创建an
SecurityContextPersistenceFilter
,an
ExceptionTranslationFilter
和a
FilterSecurityInterceptor
。
这些是固定的,不能用替代方法替代。
<http>属性
<http>
元素
上的属性
控制核心过滤器上的某些属性。
-
access-decision-manager-ref 可选属性,用于指定
AccessDecisionManager应用于授权HTTP请求 的 实现 的ID 。 默认情况下,AffirmativeBased实现用于RoleVoter和AuthenticatedVoter。
-
authentication-manager-ref 对此http元素创建 的
AuthenticationManager所使用 的引用FilterChain。
-
自动配置 自动注册登录表单,BASIC身份验证,注销服务。 如果设置为“ true”,则会添加所有这些功能(尽管您仍然可以通过提供相应的元素来自定义每个功能的配置)。 如果未指定,则默认为“ false”。 不建议使用此属性。 请使用显式配置元素来避免混淆。
-
create-session 控制Spring Security类创建HTTP会话的迫切性。 选项包括:
-
always-如果不存在会话,Spring Security将主动创建一个会话。 -
ifRequired-仅在需要一个会话时,Spring Security才会创建一个会话(默认值)。 -
never-Spring Security永远不会创建会话,但是如果应用程序确实会使用一个会话。 -
stateless-Spring Security将不会创建会话,并且会忽略该会话以获取SpringAuthentication。
-
-
disable-url-rewriting 禁止将会话ID附加到应用程序中的URL。 如果此属性设置为,则客户端必须使用Cookie
true。 默认值为true。
-
entry-point-ref 通常,
AuthenticationEntryPoint将根据已配置的身份验证机制来设置使用的入口 。 此属性允许通过定义AuthenticationEntryPoint将启动认证过程 的自定义 bean 来覆盖此行为 。
-
jaas-api-provision( 如果可用),
Subject以从中获取 请求的方式运行请求,该请求JaasAuthenticationToken是通过将JaasApiIntegrationFilterbean 添加 到堆栈中来实现的。 默认为false。
-
名称 bean标识符,用于在上下文中的其他地方引用bean。
-
每次请求 对应于的
observeOncePerRequest属性FilterSecurityInterceptor。 默认为true。
-
模式 为 http 元素 定义模式可 控制将通过其定义的过滤器列表进行过滤的请求。 解释取决于配置的 request-matcher 。 如果未定义任何模式,则将匹配所有请求,因此应首先声明最具体的模式。
-
领域 设置用于基本身份验证的领域名称(如果启用)。 对应于的
realmName属性BasicAuthenticationEntryPoint。
-
request-matcher 定义
RequestMatcher用于FilterChainProxy和中创建的Beanintercept-url来匹配传入请求的策略。 选项目前mvc,ant,regex和ciRegex,对Spring MVC的,蚂蚁,正则表达式和不区分大小写的分别为正则表达式。 使用其 pattern , method 和 servlet-path 属性 为每个 拦截URL 元素 创建一个单独的实例 。 蚂蚁路径使用进行匹配 ,正则表达式使用进行匹配 ,而Spring MVC路径使用进行匹配AntPathRequestMatcherRegexRequestMatcherMvcRequestMatcher用来。 有关这些类的确切执行方式的更多详细信息,请参见Javadoc。 蚂蚁路径是默认策略。
-
request-matcher-ref 对实现的Bean的引用,该引用
RequestMatcher将确定是否FilterChain应使用它。 这是 pattern 的更强大的替代方法 。
-
安全性 通过将此属性设置为,可以将请求模式映射到一个空的过滤器链
none。 不会应用任何安全性,并且Spring Security的任何功能都不可用。
-
security-context-repository-ref 允许将自定义
SecurityContextRepository注入SecurityContextPersistenceFilter。
-
servlet-api-provision 提供
HttpServletRequest诸如isUserInRole()和 的 安全方法 版本, 这些方法getPrincipal()是通过将SecurityContextHolderAwareRequestFilterbean 添加 到堆栈中来实现的。 默认为true。
-
use-expressions 启用
access属性 中的EL表达式 ,如关于 基于表达式的访问控制 一章所述 。 默认值是true。
<拒绝访问处理程序>
通过此元素,您可以
使用
error-page
属性
将
errorPage
属性
设置
为所
AccessDeniedHandler
使用
的默认值
,或者使用
ref
属性
提供您自己的实现
。
在
ExceptionTranslationFilter
的部分中对此进行了更详细的讨论
。
ExceptionTranslationFilter
<cors>
该元素允许配置
CorsFilter
。
如果未
指定
CorsFilter
或
CorsConfigurationSource
,并且Spring MVC在类路径上,则将a
HandlerMappingIntrospector
用作
CorsConfigurationSource
。
<cors>属性
<cors>
元素
上的属性
控制headers元素。
-
ref 可选属性,指定a的bean名称
CorsFilter。
-
cors-configuration-source-ref 可选属性,用于指定
CorsConfigurationSource要注入CorsFilterXML名称空间创建 的a的bean名称 。
<标题>
-
Cache-Control,Pragma和和Expires-可以使用 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-Pinning或Public-Key-Pinning-Report-Only-可以使用 hpkp 元素 进行设置 。 这允许HTTPS网站使用错误签发或欺诈性证书来抵制攻击者的冒名顶替。 -
Content-Security-Policy或Content-Security-Policy-Report-Only-可以使用 content-security-policy 元素 进行设置 。 内容安全策略(CSP) 是Web应用程序可以利用的一种机制,可以缓解诸如跨站点脚本(XSS)之类的内容注入漏洞。 -
Referrer-Policy-可以使用 Referrer -policy 元素 进行设置 , Refererer-Policy 是一种机制,Web应用程序可以利用该机制来管理 Referrer 字段,该字段包含用户所在的最后一页。 -
Feature-Policy-可以使用 feature-policy 元素 进行设置 , Feature-Policy 是一种机制,可让Web开发人员有选择地启用,禁用和修改浏览器中某些API和Web功能的行为。
<headers>属性
<headers>
元素
上的属性
控制headers元素。
-
defaults-disabled 可选属性,用于指定禁用默认Spring Security的HTTP响应标头。 默认值为false(包括默认标题)。
-
disable 可选属性,用于指定禁用Spring Security的HTTP响应标头。 缺省值为false(已启用标头)。
<hsts>
启用后,会将 Strict-Transport-Security 标头添加到任何安全请求的响应中。 这允许服务器指示浏览器自动将HTTPS用于将来的请求。
<hsts>属性
-
已禁用 指定是否应禁用“严格传输安全性”。 默认为false。
-
include-sub-domains 指定是否应包含子域。 默认为true。
-
max-age-seconds 指定应将主机视为已知HSTS主机的最长时间。 默认一年。
-
request-matcher-ref 用于确定是否应设置标头的RequestMatcher实例。 默认值为HttpServletRequest.isSecure()为true。
-
preload 指定是否应包含预加载。 默认为false。
<hpkp>
<hpkp>属性
-
已禁用 指定是否应禁用HTTP公钥固定(HPKP)。 默认为true。
-
include-sub-domains 指定是否应包含子域。 默认为false。
-
max-age-seconds 设置Public-Key-Pins标头的max-age指令的值。 默认为60天。
-
report-only 指定浏览器是否仅应报告引脚验证失败。 默认为true。
-
report-uri 指定浏览器应向其报告引脚验证失败的URI。
<内容安全策略>
启用后,将 内容安全策略(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。
<frame-options>
启用后,将 X-Frame-Options标头添加 到响应中,这使较新的浏览器可以进行一些安全检查并防止 点击劫持 攻击。
<frame-options>属性
-
禁用 如果禁用,则不包含X-Frame-Options标头。 默认为false。
-
政策
-
DENY该页面不能显示在框架中,无论站点试图这样做。 当指定frame-options-policy时,这是默认设置。 -
SAMEORIGIN该页面只能以与页面本身相同的原点显示在框架中 -
ALLOW-FROM origin该页面只能显示在指定原点的框架中。
换句话说,如果指定DENY,则从其他站点加载时,不仅尝试在框架中加载页面失败,而且从同一站点加载时,尝试也会失败。 另一方面,如果指定SAMEORIGIN,则只要框架中包含该站点的页面与提供该页面的站点相同,您仍可以在框架中使用该页面。
-
-
策略 选择
AllowFromStrategy使用ALLOW-FROM策略时要使用的策略。-
regexp使用regelur表达式来验证传入请求以及是否允许传入请求。 可以通过 value 属性 设置正则表达式 。 可以使用 from-parameter 指定用于检索要验证的值的request 参数 。 -
whitelist包含允许的域的逗号分隔列表。 可以通过 value 属性 设置逗号分隔的列表 。 可以使用 from-parameter 指定用于检索要验证的值的request 参数 。
-
ref 除了使用一种预定义策略之外,还可以使用custom
AllowFromStrategy。 可以通过此ref属性指定对该bean的引用。
-
value 当使用ALLOW-FROM 策略 时要使用的值 。
-
from-parameter 指定将regexp或白名单用于ALLOW-FROM策略时要使用的请求参数的名称。
<xss-保护>
将 X-XSS-Protection标头添加 到响应中,以帮助防止 反射的/ Type-1跨站点脚本(XSS) 攻击。 这绝不是对XSS攻击的全面保护!
<xss-protection>属性
-
xss-protection-disabled 不要包含用于 反射/ Type-1跨站点脚本(XSS) 保护 的标头 。
-
xss-protection-enabled 显式启用或禁用 反射/ Type-1跨站点脚本(XSS) 保护。
-
xss-protection-block为 true且xss-protection-enabled为true时,将mode = block添加到标头。 这向浏览器指示根本不应加载该页面。 如果为false并且xss-protection-enabled为true,则在检测到反射攻击时仍将呈现页面,但是将修改响应以防止受到攻击。 请注意,有时存在绕过此模式的方法,这些方法常常使阻塞页面更为可取。
<匿名>
将一个添加
AnonymousAuthenticationFilter
到堆栈中,然后
添加
一个
AnonymousAuthenticationProvider
。
如果使用该
IS_AUTHENTICATED_ANONYMOUSLY
属性,
则为必需
。
<csrf>
该元素将向 应用程序 添加 跨站点请求伪造(CSRF) 保护。 它还将默认的RequestCache更新为仅在成功身份验证后重播“ GET”请求。 可以在 Reference的 跨站点请求伪造(CSRF) 部分中 找到其他信息 。
<自定义过滤器>
该元素用于向过滤器链添加过滤器。
它不会创建任何其他bean,而是用于选择
javax.servlet.Filter
在应用程序上下文中已经定义
的类型的bean,
并将其添加到Spring Security维护的过滤器链中的特定位置。
完整的细节可以在
命名空间一章中
找到
。
<表达式处理程序>
定义
SecurityExpressionHandler
启用基于表达式的访问控制时将使用
的
实例。
如果未提供,则将使用默认实现(不支持ACL)。
<表单登录>
用于向
UsernamePasswordAuthenticationFilter
过滤器堆栈和
LoginUrlAuthenticationEntryPoint
向应用程序上下文
添加,
以
按需提供身份验证。
这将始终优先于其他由名称空间创建的入口点。
如果未提供任何属性,则将在URL“ / login”
[ 16 ]上
自动生成一个登录页面
。
可以使用
<form-login>Attributes
自定义行为
。
<form-login>属性
-
always-use-default-target 如果设置为
true,则 无论用户如何到达登录页面 ,用户都将始终从 default-target-url 给定的值开始 。 映射到的alwaysUseDefaultTargetUrl属性UsernamePasswordAuthenticationFilter。 默认值为false。
-
authentication-details-source-ref
AuthenticationDetailsSource对认证过滤器将使用的的 引用
-
authentication-failure-handler-ref 可以用作 authentication-failure-url 的替代方法 ,使您可以在身份验证失败后完全控制导航流程。 该值应该是
AuthenticationFailureHandler应用程序上下文 中的 bean 的名称 。
-
authentication-failure-url 映射到的
authenticationFailureUrl属性UsernamePasswordAuthenticationFilter。 定义登录失败时浏览器将重定向到的URL。 默认值为/login?error,将由自动登录页面生成器自动处理,并使用错误消息重新呈现登录页面。
-
authentication-success-handler-ref, 它可以用作 default-target-url 和 always-use-default-target 的替代方法 ,使您可以在成功认证后完全控制导航流程。 该值应该是
AuthenticationSuccessHandler应用程序上下文 中的 bean 的名称 。 默认情况下,使用的实现SavedRequestAwareAuthenticationSuccessHandler并将其注入 default-target-url 。
-
default-target-url 映射到的
defaultTargetUrl属性UsernamePasswordAuthenticationFilter。 如果未设置,则默认值为“ /”(应用程序根目录)。 如果将用户带到最初请求的URL,则在尝试访问受保护的资源时不要求用户登录后,将在登录后将其带到该URL。
-
login-page 应该用于呈现登录页面的URL。 映射到的
loginFormUrl属性LoginUrlAuthenticationEntryPoint。 默认为“ /登录”。
-
login-processing-url 映射到的
filterProcessesUrl属性UsernamePasswordAuthenticationFilter。 默认值为“ / login”。
-
password-parameter 包含密码的请求参数 的名称。 默认为“密码”。
-
username-parameter 包含用户名的请求参数的名称。 默认为“用户名”。
-
authentication-success-forward-url将 a映射
ForwardAuthenticationSuccessHandler到authenticationSuccessHandler属性UsernamePasswordAuthenticationFilter。
-
authentication-failure-forward-url将 a映射
ForwardAuthenticationFailureHandler到authenticationFailureHandler属性UsernamePasswordAuthenticationFilter。
<http-basic>
将
BasicAuthenticationFilter
和
添加
BasicAuthenticationEntryPoint
到配置中。
如果未启用基于表单的登录,则后者将仅用作配置入口点。
<intercept-url>
此元素用于定义应用程序感兴趣的URL模式集,并配置应如何处理它们。
它被用来构造
FilterInvocationSecurityMetadataSource
使用的
FilterSecurityInterceptor
。
例如,它还负责配置
ChannelProcessingFilter
是否需要通过HTTPS访问特定的URL。
当将指定的模式与传入的请求进行匹配时,将按照声明元素的顺序进行匹配。
因此,最具体的模式应该放在首位,最一般的模式应该放在最后。
<intercept-url>属性
-
access 列出访问属性,这些属性将存储在
FilterInvocationSecurityMetadataSource已定义的URL模式/方法组合中。 这应该是安全配置属性(例如角色名称)的逗号分隔列表。
-
method HTTP方法,它将与模式和servlet路径(可选)结合使用,以匹配传入的请求。 如果省略,则任何方法都将匹配。 如果使用和不使用方法指定了相同的模式,则特定于方法的匹配将优先。
-
pattern 定义URL路径的模式。 内容将取决于
request-matcher包含http元素 的 属性,因此默认为ant path语法。
-
request-matcher-ref 对的引用,该引用
RequestMatcher将用于确定是否使用了该<intercept-url>引用。
-
require-channel 可以为“ http”或“ https”,具体取决于应分别通过HTTP还是HTTPS访问特定的URL模式。 或者,在没有首选项时可以使用值“ any”。 如果任何
<intercept-url>元素 上都存在此属性 ,则将aChannelProcessingFilter添加到过滤器堆栈中,并将其其他依赖项添加到应用程序上下文中。
如果
<port-mappings>
添加
了
配置,
SecureChannelProcessor
和
InsecureChannelProcessor
bean
将使用它
来确定用于重定向到HTTP / HTTPS的端口。
| 此属性对于 filter-security-metadata-source 无效 |
-
servlet-path servlet路径,它将与模式和HTTP方法结合使用以匹配传入的请求。 仅当 请求匹配器 为“ mvc” 时,此属性才适用 。 此外,仅在以下两个用例中需要该值:1)在中有2个或多个
HttpServlet注册的ServletContext具有以开头'/'且不同的 映射 。 2)模式以已注册HttpServlet路径 的相同值开头 ,但默认路径(root)除外HttpServlet'/'。
| 此属性对于 filter-security-metadata-source 无效 |
<登出>
将a添加
LogoutFilter
到过滤器堆栈。
这配置了一个
SecurityContextLogoutHandler
。
<logout>属性
-
invalidate-session 映射到
invalidateHttpSession的SecurityContextLogoutHandler。 默认为“ true”,因此会话将在注销时失效。
-
logout-success-url注销 后将被带到用户的目标URL。 默认为<form-login-login-page> /?logout(即/ login?logout)
设置此属性将注入
SessionManagementFilter带有SimpleRedirectInvalidSessionStrategy配置的属性值。 提交无效的会话ID后,将调用该策略,并重定向到配置的URL。
-
logout-url 导致注销的URL(即由过滤器处理的URL)。 默认为“ /注销”。
-
success-handler-ref 可能用于提供
LogoutSuccessHandler注销后将调用 其实例 以控制导航 的实例 。
<openid登录>
类似
<form-login>
并具有相同的属性。
的默认值为
login-processing-url
“ / login / openid”。
一个
OpenIDAuthenticationFilter
和
OpenIDAuthenticationProvider
登记手续。
后者需要引用
UserDetailsService
。
同样,可以
id
使用
user-service-ref
属性
使用
来指定
,或者将其自动定位在应用程序上下文中。
<openid-login>属性
-
always-use-default-target 登录后是否应始终将用户重定向到default-target-url。
-
authentication-details-source-ref 对AuthenticationDetailsSource的引用,将由身份验证过滤器使用
-
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将 a映射
ForwardAuthenticationSuccessHandler到authenticationSuccessHandler属性UsernamePasswordAuthenticationFilter。
-
authentication-failure-forward-url将 a映射
ForwardAuthenticationFailureHandler到authenticationFailureHandler属性UsernamePasswordAuthenticationFilter。
-
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 包含密码的请求参数 的名称。 默认为“密码”。
-
user-service-ref 对用户服务(或UserDetailsService bean)ID的引用
-
username-parameter 包含用户名的请求参数的名称。 默认为“用户名”。
<属性交换>
该
attribute-exchange
元素定义应该由身份提供商请求属性的列表。
可以
在名称空间配置一章
的“
OpenID支持”
部分中
找到一个示例
。
可以使用多个,在这种情况下,每个
identifier-match
属性
都必须具有一个
属性,
该
属性包含与提供的OpenID标识符匹配的正则表达式。
这允许从不同的提供程序(Google,Yahoo等)获取不同的属性列表。
<openid-attribute>
发出OpenID AX 提取请求 时使用的属性
<openid-attribute>属性
-
count 指定要获取的属性数。 例如,返回3封电子邮件。 预设值为1.
-
name 指定要获取的属性的名称。 例如,电子邮件。
-
required 指定该属性是否为OP所必需,但如果OP不返回该属性,则不会出错。 默认为false。
-
type 指定属性类型。 例如, https://axschema.org/contact/email 。 有关有效的属性类型,请参见OP的文档。
<端口映射>
默认情况下,
PortMapperImpl
会将
的实例
添加到配置中,以用于重定向到安全和不安全的URL。
可以选择使用此元素来覆盖该类定义的默认映射。
每个子
<port-mapping>
元素定义一对HTTP:HTTPS端口。
默认映射为80:443和8080:8443.
命名空间简介
中提供了覆盖这些内容的示例
。
<记住我>
将添加
RememberMeAuthenticationFilter
到堆栈。
依次使用
TokenBasedRememberMeServices
,a
PersistentTokenBasedRememberMeServices
或用户指定的bean
进行配置,
RememberMeServices
具体取决于属性设置。
<remember-me>属性
-
authentication-success-handler-ref 如果需要自定义导航,则 设置
authenticationSuccessHandler属性RememberMeAuthenticationFilter。 该值应该是AuthenticationSuccessHandler应用程序上下文 中的 bean 的名称 。
-
data-source-ref 对
DataSourcebean 的引用 。 如果设置,PersistentTokenBasedRememberMeServices将使用JdbcTokenRepositoryImpl实例 并进行配置 。
-
“记住我 ”参数用于切换“记住我”身份验证的请求参数 的名称。 默认为“记住我”。 映射到的“参数”属性
AbstractRememberMeServices。
-
key 映射到的“ key”属性
AbstractRememberMeServices。 应该将其设置为唯一值,以确保“记住我的Cookie”仅在一个应用程序中有效 [ 17 ] 。 如果未设置,将生成一个安全的随机值。 由于生成安全的随机值可能需要一段时间,因此,在使用“记住我”功能时,显式设置该值有助于缩短启动时间。
-
services-alias 导出内部定义
RememberMeServices为Bean别名,从而允许应用程序上下文中的其他Bean使用它。
-
services-ref 允许完全控制
RememberMeServices过滤器将使用 的 实现。 该值应该是id实现此接口的应用程序上下文中的bean的。 还应该实现LogoutHandler是否正在使用注销过滤器。
-
token-repository-ref 配置a,
PersistentTokenBasedRememberMeServices但允许使用自定义PersistentTokenRepositorybean。
-
token-validity-seconds 映射到的
tokenValiditySeconds属性AbstractRememberMeServices。 指定记住我的cookie有效的时间段(以秒为单位)。 默认情况下,有效期为14天。
-
user-service-ref “记住我”服务实现需要访问
UserDetailsService,因此必须在应用程序上下文中定义一个。 如果只有一个,它将由名称空间配置自动选择和使用。 如果有多个实例,则可以id使用此属性显式 指定bean 。
<request-cache>元素
设置
RequestCache
实例,
该
实例将被用来
ExceptionTranslationFilter
在调用之前存储请求信息
AuthenticationEntryPoint
。
<会话管理>
与会话管理相关的功能是通过
SessionManagementFilter
在过滤器堆栈中
添加a
来实现的。
<session-management>属性
-
invalid-session-url 设置此属性将注入
SessionManagementFilter带有SimpleRedirectInvalidSessionStrategy配置的属性值的。 提交无效的会话ID后,将调用该策略,并重定向到配置的URL。
-
invalid-session-url 允许注入由SessionManagementFilter使用的InvalidSessionStrategy实例。 使用此
invalid-session-url属性 或 属性,但不能同时使用。
-
session-authentication-error-url 定义错误页面的URL,当SessionAuthenticationStrategy引发异常时应显示该页面。 如果未设置,则将未经授权的(401)错误代码返回给客户端。 请注意,如果在基于表单的登录过程中发生错误,则该属性将不适用,其中,身份验证失败的URL将优先。
-
session-authentication-strategy-ref 允许注入由SessionManagementFilter使用的SessionAuthenticationStrategy实例
-
session-fixation-protection 指示用户认证时如何应用会话固定保护。 如果设置为“ none”,则不会应用任何保护。 “ newSession”将创建一个新的空会话,仅迁移与Spring Security相关的属性。 “ migrateSession”将创建一个新会话并将所有会话属性复制到新会话。 在Servlet 3.1(Java EE 7)和更高版本的容器中,指定“ changeSessionId”将保留现有会话并使用容器提供的会话固定保护(HttpServletRequest#changeSessionId())。 在Servlet 3.1和更高版本的容器中,默认值为“ changeSessionId”;在较旧的容器中,默认值为“ migrateSession”。 如果在较旧的容器中使用“ changeSessionId”,则抛出异常。
如果启用了会话固定保护,
SessionManagementFilter则会为注入适当配置的DefaultSessionAuthenticationStrategy。 有关更多详细信息,请参见此类的Javadoc。
<并发控制>
增加了对并发会话控制的支持,从而可以限制用户可以拥有的活动会话的数量。
ConcurrentSessionFilter
将创建
A
,并将
A
与
ConcurrentSessionControlAuthenticationStrategy
一起使用
SessionManagementFilter
。
如果
form-login
已声明元素,则策略对象也将注入到创建的身份验证过滤器中。
将创建一个实例
SessionRegistry
(
SessionRegistryImpl
除非用户希望使用自定义bean,否则该实例)将被策略使用。
<concurrency-control>属性
-
如果超出最大错误数 如果设置为“ true”,
SessionAuthenticationException则当用户尝试超过允许的最大会话数时,将引发a。 默认行为是使原始会话到期。
-
expired-url 如果用户尝试使用并发会话控制器已“过期”的会话,则该用户将被重定向到该URL,因为该用户超过了允许的会话数,并且已在其他位置再次登录。 除非设置,否则应
exception-if-maximum-exceeded设置。 如果未提供任何值,则到期消息将直接写回到响应中。
-
expired-url 允许注入ConcurrentSessionFilter使用的ExpiredSessionStrategy实例
-
max-sessions 映射到的
maximumSessions属性ConcurrentSessionControlAuthenticationStrategy。 指定-1为支持无限会话的值。
-
session-registry-alias 引用内部会话注册表以在您自己的bean或管理界面中使用也很有用。 您可以使用
session-registry-alias属性 公开内部bean ,为其提供一个名称,您可以在配置中的其他位置使用它。
-
session-registry-ref 用户可以
SessionRegistry使用该session-registry-ref属性 提供自己的 实现 。 其他并发会话控制bean将被连接起来以使用它。
<x509>
添加了对X.509身份验证的支持。
一个
X509AuthenticationFilter
将被添加到堆栈和
Http403ForbiddenEntryPoint
bean将被创建。
仅当不使用其他身份验证机制时才使用后者(它的唯一功能是返回HTTP 403错误代码)。
PreAuthenticatedAuthenticationProvider
还将创建
一个,
将用户权限的加载委托给
UserDetailsService
。
<filter-chain-map>
用于通过FilterChainMap显式配置FilterChainProxy实例
<filter-chain-map>属性
-
request-matcher 定义用于匹配传入请求的策略。 当前,选项为“ ant”(用于ant路径模式),“ regex”用于正则表达式,“ ciRegex”用于不区分大小写的正则表达式。
<过滤链>
用于内部以定义特定的URL模式以及适用于与该模式匹配的URL的过滤器列表。 当在列表中组合多个过滤器链元素以配置FilterChainProxy时,最特定的模式必须放在列表的顶部,最普通的模式应放在底部。
<filter-chain>属性
-
过滤器 以逗号分隔的对实现的Spring bean的引用列表
Filter。 值“ none”表示不Filter应该为此使用FilterChain。
-
模式 一种模式,该模式结合 请求匹配器 创建RequestMatcher
-
request-matcher-ref 对的引用,该引用
RequestMatcher将用于确定是否 应调用Filter该filters属性中的 任何内容 。
<filter-security-metadata-source>
用于显式配置FilterSecurityMetadataSource bean与FilterSecurityInterceptor一起使用。 通常仅在显式配置FilterChainProxy而不是使用<http>元素时才需要。 使用的拦截URL元素应仅包含模式,方法和访问属性。 其他任何情况都将导致配置错误。
<filter-security-metadata-source>属性
-
id bean标识符,用于引用上下文中其他位置的bean。
-
request-matcher 定义用于匹配传入请求的策略。 当前,选项为“ ant”(用于ant路径模式),“ regex”用于正则表达式,“ ciRegex”用于不区分大小写的正则表达式。
-
use-expressions 允许在<intercept-url>元素的'access'属性中使用表达式,而不是在传统的配置属性列表中使用。 默认为“ true”。 如果启用,则每个属性应包含一个布尔表达式。 如果表达式的计算结果为“ true”,则将授予访问权限。
19.2.2. WebSocket安全
Spring Security 4.0+提供了对消息授权的支持。 一个有用的具体示例是在基于WebSocket的应用程序中提供授权。
<websocket-message-broker>
websocket-message-broker元素具有两种不同的模式。 如果 未指定 websocket-message-broker @ id ,则它将执行以下操作:
-
确保任何SimpAnnotationMethodMessageHandler都将AuthenticationPrincipalArgumentResolver注册为自定义参数解析器。 这允许使用
@AuthenticationPrincipal来解析当前Authentication -
确保已为clientInboundChannel自动注册SecurityContextChannelInterceptor。 这将使用Message中找到的用户填充SecurityContextHolder
-
确保ChannelSecurityInterceptor已向clientInboundChannel注册。 这允许为消息指定授权规则。
-
确保CsrfChannelInterceptor已在clientInboundChannel中注册。 这样可以确保仅启用来自原始域的请求。
-
确保已向WebSocketHttpRequestHandler,TransportHandlingSockJsService或DefaultSockJsService注册CsrfTokenHandshakeInterceptor。 这样可以确保将来自HttpServletRequest的预期CsrfToken复制到WebSocket会话属性中。
如果需要其他控制,则可以指定ID,并将ChannelSecurityInterceptor分配给指定的ID。 然后,可以手动完成与Spring的消息传递基础结构的所有连接。 这比较麻烦,但是可以更好地控制配置。
<websocket-message-broker>属性
-
id bean标识符,用于引用上下文中其他位置的ChannelSecurityInterceptor bean。 如果指定,Spring Security需要在Spring Messaging中进行显式配置。 如果未指定,Spring Security将按照 <websocket-message-broker>中的说明 自动与消息传递基础结构集成。
-
same-origin-disabled 禁用Stomp标头中必须存在CSRF令牌的要求(默认为false)。 如果需要允许其他来源建立SockJS连接,则更改默认值很有用。
<拦截消息>
定义消息的授权规则。
<intercept-message>属性
-
pattern 一个基于ant的模式,它在Message目标上匹配。 例如,“ / ”将任何消息与目标匹配;“ / admin / ”与目标以“ / admin / **”开头的任何邮件匹配。
-
type 要匹配的消息的类型。 有效值在SimpMessageType中定义(即CONNECT,CONNECT_ACK,HEARTBEAT,MESSAGE,SUBSCRIBE,UNSUBSCRIBE,DISCONNECT,DISCONNECT_ACK,OTHER)。
-
访问 用于保护消息的表达式。 例如,“ denyAll”将拒绝访问所有匹配的消息; “ permitAll”将授予对所有匹配消息的访问权限; “ hasRole('ADMIN')要求当前用户具有匹配消息的角色'ROLE_ADMIN'。
19.2.3. 认证服务
在Spring Security 3.0之前,会在
AuthenticationManager
内部自动注册。
现在,您必须使用
<authentication-manager>
元素
显式注册一个
。
这将创建一个Spring Security
ProviderManager
类
的实例,
该类需要配置一个或多个
AuthenticationProvider
实例
的列表
。
这些可以使用命名空间提供的语法元素创建,也可以是标准bean定义,并使用该
authentication-provider
元素
标记为添加到列表中
。
<身份验证管理器>
每个使用名称空间的Spring Security应用程序都必须在某处包含此元素。
它负责注册
AuthenticationManager
向应用程序提供身份验证服务的。
所有创建
AuthenticationProvider
实例的
元素都
应该是该元素的子元素。
<authentication-manager>属性
-
alias 此属性使您可以定义内部实例的别名,以用于您自己的配置。 在 名称空间介绍中 描述了它的用法 。
-
擦除凭据 如果设置为true,则在验证用户身份之后,AuthenticationManager将尝试清除返回的Authentication对象中的所有凭据数据。 从字面上看,它映射到的
eraseCredentialsAfterAuthentication属性ProviderManager。 核心服务 一章 对此进行了讨论 。
-
id 此属性使您可以定义内部实例的ID,以在您自己的配置中使用。 它与alias元素相同,但是使用id属性的元素提供了更一致的体验。
<身份验证提供者>
除非与
ref
属性
一起使用,否则
此元素是配置
DaoAuthenticationProvider的
简写
。
DaoAuthenticationProvider
从中加载用户信息
UserDetailsService
,并将用户名/密码组合与登录时提供的值进行比较。
的
UserDetailsService
实例可以通过使用可用的名字空间元素(定义或者
jdbc-user-service
或通过使用
user-service-ref
属性指向在应用程序上下文别处定义的豆)。
您可以在
名称空间介绍中
找到这些变体的示例
。
<authentication-provider>属性
-
ref 定义对实现的Spring bean的引用
AuthenticationProvider。
如果您已经编写了自己的
AuthenticationProvider
实现(或者出于某种原因想要将Spring Security自己的实现之一配置为传统Bean,则可以使用以下语法将其添加到以下内部列表中
ProviderManager
:
<security:authentication-manager>
<security:authentication-provider ref="myAuthenticationProvider" />
</security:authentication-manager>
<bean id="myAuthenticationProvider" class="com.something.MyAuthenticationProvider"/>
-
user-service-ref 对实现UserDetailsService的bean的引用,该UserDetailsService可以使用标准bean元素或自定义user-service元素创建。
<jdbc用户服务>
导致创建基于JDBC的UserDetailsService。
<jdbc-user-service>属性
默认是
select username, authority from authorities where username = ?
-
cache-ref 定义对与UserDetailsService一起使用的缓存的引用。
-
data-source-ref 提供所需表的DataSource的bean ID。
-
id bean标识符,用于引用上下文中其他位置的bean。
-
role-prefix(角色前缀) 一个非空字符串前缀,将添加到从持久性存储中加载的角色字符串中(默认值为“ ROLE_”)。 在默认为非空的情况下,将值“ none”用于无前缀。
-
用户按用户名查询 SQL语句,用于查询给定用户名的用户名,密码和启用状态。 默认是
select username, password, enabled from users where username = ?
<密码编码器>
如
名称空间介绍中
所述,可以将身份验证提供程序配置为使用密码编码器
。
这将导致为bean注入适当的
PasswordEncoder
实例。
<用户服务>
从属性文件或“用户”子元素列表创建内存UserDetailsService。 用户名在内部会转换为小写形式,以允许不区分大小写的查找,因此如果需要区分大小写,则不应使用此名称。
<用户服务>属性
-
id bean标识符,用于引用上下文中其他位置的bean。
-
properties 属性文件的位置,其中每一行的格式为
username=password,grantedAuthority[,grantedAuthority][,enabled|disabled]
19.2.4. 方法安全性
<全局方法安全性>
该元素是添加对Spring Security bean上的安全方法的支持的主要方法。 可以使用AspectJ语法通过使用注释(在接口或类级别定义)或将一组切入点定义为子元素来保护方法。
<global-method-security>属性
-
access-decision-manager-ref 方法安全性使用与
AccessDecisionManagerWeb安全性 相同的 配置,但是可以使用此属性来覆盖此配置。 默认情况下,AffirmativeBased实现用于RoleVoter和AuthenticatedVoter。
-
authentication-manager-ref 对
AuthenticationManager应用于方法安全性的的 引用 。
-
jsr250-annotations 指定是否使用JSR-250样式属性(例如“ RolesAllowed”)。 这将需要classpath上的javax.annotation.security类。 设置为true,还增加了一个
Jsr250Voter到AccessDecisionManager,所以你需要确保你做这个,如果你使用的是自定义的实现,然后想要使用这些注解。
-
metadata-source-ref
MethodSecurityMetadataSource可以提供 一个外部 实例,该实例的优先级高于其他源(例如默认注释)。
-
模式 可以将此属性设置为“ aspectj”,以指定应使用AspectJ代替默认的Spring AOP。 担保方法必须与被编织
AnnotationSecurityAspect从spring-security-aspects模块。
重要的是要注意,AspectJ遵循Java的规则,即不继承接口上的注释。 这意味着在接口上定义安全性注释的方法将不安全。 相反,在使用AspectJ时,必须在类上放置Security批注。
-
order 允许为方法安全性拦截器设置建议“ order”。
-
pre-post-annotations 指定是否应为此应用程序上下文启用Spring Security的调用前后注释(@ PreFilter,@ PreAuthorize,@ PostFilter,@ PostAuthorize)。 默认为“禁用”。
-
proxy-target-class 如果为true,将使用基于类的代理而不是基于接口的代理。
-
run-as-manager-ref 对可选
RunAsManager实现的 引用,该 实现将由已配置MethodSecurityInterceptor
-
secure -annotations 指定是否应为此应用程序上下文启用Spring Security的@Secured注释。 默认为“禁用”。
<after-invocation-provider>
此元素可用于装饰
名称空间
AfterInvocationProvider
维护的安全拦截器使用的
<global-method-security>
。
您可以在
global-method-security
元素中
定义零个或多个
,每个都具有一个
ref
指向
AfterInvocationProvider
应用程序上下文中
的
bean实例
的
属性
。
<注释后处理>
允许完全替换基于默认表达式的机制来处理Spring Security的调用前后注释(@ PreFilter,@ PreAuthorize,@ PostFilter,@ PostAuthorize)。 仅在启用这些注释的情况下适用。
<invocation-attribute-factory>
定义PrePostInvocationAttributeFactory实例,该实例用于从带注释的方法中生成调用前后的元数据。
<调用后建议>
PostInvocationAdviceProvider
使用ref作为
PostInvocationAuthorizationAdvice
<pre-post-annotation-handling>元素的
定制
。
<预调用建议>
PreInvocationAuthorizationAdviceVoter
使用ref作为
PreInvocationAuthorizationAdviceVoter
<pre-post-annotation-handling>元素的
定制
。
使用以下方法保护方法
<protect-pointcut>
@Secured
您可以使用
<protect-pointcut>
元素
在服务层中的整个方法和接口集上定义跨领域安全约束,
而不必使用
注释
在单个方法或类的基础上定义安全属性
。
您可以在
名称空间介绍中
找到示例
。
<method-security-metadata-source>
创建一个MethodSecurityMetadataSource实例
<method-security-metadata-source>属性
-
id bean标识符,用于引用上下文中其他位置的bean。
-
use-expressions 允许在<intercept-url>元素的'access'属性中使用表达式,而不是在传统的配置属性列表中使用。 默认为'false'。 如果启用,则每个属性应包含一个布尔表达式。 如果表达式的计算结果为“ true”,则将授予访问权限。
19.2.5. LDAP命名空间选项
LDAP在 其自己的章节 中进行了一些详细 介绍 。 我们将在此扩展它,并提供一些有关名称空间选项如何映射到Spring Bean的解释。 LDAP实现广泛使用Spring LDAP,因此熟悉该项目的API可能会有用。
使用以下命令定义LDAP服务器
<ldap-server>
元素此元素设置一个
ContextSource
供其他LDAP Bean使用
的Spring LDAP
,定义LDAP服务器的位置以及用于与其连接的其他信息(例如用户名和密码,如果不允许匿名访问)。
它还可以用于创建嵌入式服务器以进行测试。
LDAP一章介绍
了这两个选项的语法细节
。
实际的
ContextSource
实现是
DefaultSpringSecurityContextSource
扩展Spring LDAP的
LdapContextSource
类的。
在
manager-dn
与
manager-password
后者的属性,地图
userDn
和
password
分别特性。
如果在应用程序上下文中仅定义了一个服务器,则其他LDAP名称空间定义的Bean将自动使用它。
否则,您可以为元素赋予“ id”属性,并使用该
server-ref
属性
从其他名称空间Bean引用该元素
。
这实际上是bean
id
中的
ContextSource
例子,如果你想要在其他传统Spring bean中使用它。
<ldap-server>属性
-
模式 明确指定应使用哪个嵌入式ldap服务器。 值为
apacheds和unboundid。 默认情况下,将取决于该库在类路径中是否可用。
-
id bean标识符,用于引用上下文中其他位置的bean。
-
ldif 明确指定要加载到嵌入式LDAP服务器中的ldif文件资源。 ldiff应该是一个Spring资源模式(即classpath:init.ldiff)。 默认值为classpath *:*。ldiff
-
manager-dn “ manager”用户身份的用户名(DN),将用于向(非嵌入式)LDAP服务器进行身份验证。 如果省略,将使用匿名访问。
-
manager-password 管理员DN的密码。 如果指定了manager-dn,则这是必需的。
-
port 指定IP端口号。 例如,用于配置嵌入式LDAP服务器。 默认值为33389。
-
root 嵌入式LDAP服务器的可选根后缀。 默认值为“ dc = springframework,dc = org”
-
url 在不使用嵌入式LDAP服务器时指定ldap服务器URL。
<ldap-authentication-provider>
此元素是创建
LdapAuthenticationProvider
实例的
简写
。
默认情况下,将使用
BindAuthenticator
实例和
进行配置
DefaultAuthoritiesPopulator
。
与所有名称空间身份验证提供程序一样,它必须作为
authentication-provider
元素
的子
元素
包含在内
。
<ldap-authentication-provider>属性
-
group-role-attribute LDAP属性名称,其中包含将在Spring Security中使用的角色名称。 映射到
DefaultLdapAuthoritiesPopulator的groupRoleAttribute属性。 默认为“ cn”。
-
group-search-base 组成员资格搜索的搜索基础。 映射到
DefaultLdapAuthoritiesPopulator的groupSearchBase构造函数参数。 默认为“”(从根目录搜索)。
-
group-search-filter 组搜索过滤器。 映射到
DefaultLdapAuthoritiesPopulator的groupSearchFilter属性。 默认为(uniqueMember = {0})。 替换的参数是用户的DN。
-
role-prefix(角色前缀) 一个非空字符串前缀,将添加到从持久性加载的角色字符串中。 映射到
DefaultLdapAuthoritiesPopulator的rolePrefix属性。 默认为“ ROLE_”。 在默认为非空的情况下,将值“ none”用于无前缀。
-
server-ref 要使用的可选服务器。 如果省略,并且注册了默认的LDAP服务器(使用没有ID的<ldap-server>),则将使用该服务器。
-
user-context-mapper-ref 通过指定UserDetailsContextMapper Bean允许显式自定义已加载的用户对象,该bean将与用户目录条目中的上下文信息一起调用
-
user-details-class 允许指定用户条目的objectClass。 如果设置,框架将尝试将已定义类的标准属性加载到返回的UserDetails对象中
-
user-dn-pattern 如果用户位于目录中的固定位置(即,您可以直接从用户名计算DN,而无需进行目录搜索),则可以使用此属性直接映射到DN。 它直接映射到的
userDnPatterns属性AbstractLdapAuthenticator。 该值是用于构建用户DN的特定模式,例如“ uid = {0},ou = people”。 键“ {0}”必须存在,并将被用户名替换。
-
用户 搜索库用户搜索的搜索库。 默认为“”。 仅与“用户搜索过滤器”一起使用。
如果需要执行搜索以在目录中找到用户,则可以设置这些属性来控制搜索。 在
BindAuthenticator将与配置FilterBasedLdapUserSearch和属性值直接对应bean构造的前两个参数。 如果未设置这些属性,并且没有user-dn-pattern提供 no 作为替代,则将 使用user-search-filter="(uid={0})"和 的默认搜索值user-search-base=""。
-
user-search-filter 用于搜索用户的LDAP过滤器(可选)。 例如“(uid = {0})”。 替换的参数是用户的登录名。
如果需要执行搜索以在目录中找到用户,则可以设置这些属性来控制搜索。 在
BindAuthenticator将与配置FilterBasedLdapUserSearch和属性值直接对应bean构造的前两个参数。 如果未设置这些属性,并且没有user-dn-pattern提供 no 作为替代,则将 使用user-search-filter="(uid={0})"和 的默认搜索值user-search-base=""。
<密码比较>
用作子元素,
<ldap-provider>
并将身份验证策略从切换
BindAuthenticator
为
PasswordComparisonAuthenticator
。
<password-compare>属性
-
hash 定义用于用户密码的哈希算法。 我们强烈建议您不要使用MD4,因为它是一种非常弱的哈希算法。
-
password-attribute 目录中包含用户密码的属性。 默认为“ userPassword”。
<ldap用户服务>
该元素配置LDAP
UserDetailsService
。
使用的类是
LdapUserDetailsService
a
FilterBasedLdapUserSearch
和a
的组合
DefaultLdapAuthoritiesPopulator
。
它支持的属性与中的用法相同
<ldap-provider>
。
<ldap-user-service>属性
-
cache-ref 定义对与UserDetailsService一起使用的缓存的引用。
-
group-role-attribute LDAP属性名称,其中包含将在Spring Security中使用的角色名称。 默认为“ cn”。
-
group-search-base 组成员资格搜索的搜索基础。 默认为“”(从根目录搜索)。
-
group-search-filter 组搜索过滤器。 默认为(uniqueMember = {0})。 替换的参数是用户的DN。
-
id bean标识符,用于引用上下文中其他位置的bean。
-
role-prefix(角色前缀) 一个非空字符串前缀,将添加到从持久性存储(例如“ ROLE_”)加载的角色字符串中。 在默认为非空的情况下,将值“ none”用于无前缀。
-
server-ref 要使用的可选服务器。 如果省略,并且注册了默认的LDAP服务器(使用没有ID的<ldap-server>),则将使用该服务器。
-
user-context-mapper-ref 通过指定UserDetailsContextMapper Bean允许显式自定义已加载的用户对象,该bean将与用户目录条目中的上下文信息一起调用
-
user-details-class 允许指定用户条目的objectClass。 如果设置,框架将尝试将已定义类的标准属性加载到返回的UserDetails对象中
-
用户 搜索库用户搜索的搜索库。 默认为“”。 仅与“用户搜索过滤器”一起使用。
-
user-search-filter 用于搜索用户的LDAP过滤器(可选)。 例如“(uid = {0})”。 替换的参数是用户的登录名。
19.3. Spring安全性依赖
本附录提供了Spring Security中的模块以及它们在运行中的应用程序中运行所需的其他依赖项的 Reference。 我们不包括仅在构建或测试Spring Security本身时使用的依赖项。 我们也没有包括外部依赖项所要求的传递性依赖项。
项目网站上列出了所需的Spring版本,因此下面的Spring依赖项省略了特定版本。 请注意,Spring应用程序中的其他非安全功能可能仍需要下面列出为“可选”的某些依赖项。 此外,如果大多数应用程序中都使用了列为“可选”的依赖项,则在项目的Maven POM文件中可能实际上并未将其标记为此类依赖项。 除非您使用指定的功能,否则它们仅在不需要它们的意义上是“可选的”。
在模块依赖于另一个Spring Security模块的情况下,也假定该模块所依赖的模块的非可选依赖关系是必需的,因此未单独列出。
19.3.1. Spring Security核心
使用Spring Security的任何项目中都必须包含核心模块。
| 相依性 | 版 | 描述 |
|---|---|---|
|
ehcache |
1.6.2 |
如果使用基于Ehcache的用户缓存实现,则为必需(可选)。 |
|
春a |
方法安全性基于Spring AOP |
|
|
春豆 |
Spring配置必需 |
|
|
春季表达 |
基于表达式的方法安全性必需(可选) |
|
|
春天的JDBC |
如果使用数据库存储用户数据,则为必需(可选)。 |
|
|
春天TX |
如果使用数据库存储用户数据,则为必需(可选)。 |
|
|
方面 |
1.6.10 |
如果使用AspectJ支持,则为必需(可选)。 |
|
jsr250-api |
1.0 |
如果您正在使用JSR-250方法安全性注释(可选),则为必需。 |
19.3.2. Spring Security远程
使用Servlet API的Web应用程序通常需要此模块。
| 相依性 | 版 | 描述 |
|---|---|---|
|
Spring Security核心 |
||
|
弹簧网 |
使用HTTP远程支持的客户端需要。 |
19.3.3. Spring Security网
使用Servlet API的Web应用程序通常需要此模块。
| 相依性 | 版 | 描述 |
|---|---|---|
|
Spring Security核心 |
||
|
弹簧网 |
Spring Web支持类被广泛使用。 |
|
|
春天的JDBC |
对于基于JDBC的永久性“记住我”令牌存储库是必需的(可选)。 |
|
|
春天TX |
“记住我”持久令牌存储库实现必需(可选)。 |
19.3.4. 春天安全ldap
仅在使用LDAP身份验证时才需要此模块。
| 相依性 | 版 | 描述 |
|---|---|---|
|
Spring Security核心 |
||
|
弹簧ldap-core |
1.3.0 |
LDAP支持基于Spring LDAP。 |
|
春天TX |
数据异常类是必需的。 |
|
|
apache-ds [ 18 ] |
1.5.5 |
如果您使用嵌入式LDAP服务器(可选),则为必需。 |
|
共享ldap |
0.9.15 |
如果您使用嵌入式LDAP服务器(可选),则为必需。 |
|
ldapsdk |
4.1 |
Mozilla LdapSDK。 例如,如果您在OpenLDAP中使用密码策略功能,则用于解码LDAP密码策略控件。 |
19.3.5. 弹簧安全配置
如果您使用的是Spring Security名称空间配置,那么此模块是必需的。
| 相依性 | 版 | 描述 |
|---|---|---|
|
Spring Security核心 |
||
|
Spring Security网 |
如果使用任何与Web相关的名称空间配置,则为必需(可选)。 |
|
|
春天安全ldap |
如果您正在使用LDAP名称空间选项(可选),则为必需。 |
|
|
春天安全openid |
如果使用的是OpenID身份验证,则为必需(可选)。 |
|
|
AspectJweaver |
1.6.10 |
如果使用protect-pointcut名称空间语法(必需),则为必需。 |
19.3.6. Spring Security性
ACL模块。
| 相依性 | 版 | 描述 |
|---|---|---|
|
Spring Security核心 |
||
|
ehcache |
1.6.2 |
如果使用基于Ehcache的ACL缓存实现,则为必需(如果使用自己的实现,则为可选)。 |
|
春天的JDBC |
如果使用的是默认的基于JDBC的AclService,则为必需(如果实现自己的,则为可选)。 |
|
|
春天TX |
如果使用的是默认的基于JDBC的AclService,则为必需(如果实现自己的,则为可选)。 |
19.3.7. 春天安全cas
CAS模块提供与JA-SIG CAS的集成。
| 相依性 | 版 | 描述 |
|---|---|---|
|
Spring Security核心 |
||
|
Spring Security网 |
||
|
cas-client-core |
3.1.12 |
JA-SIG CAS客户端。 这是Spring Security集成的基础。 |
|
ehcache |
1.6.2 |
如果您使用的是基于Ehcache的票证缓存(可选),则为必需。 |
19.4. 代理服务器配置
使用代理服务器时,确保已正确配置应用程序很重要。 例如,许多应用程序将具有响应对请求的负载平衡器 https://example.com/ 通过在将请求转发给应用服务器 的https://192.168.1:8080 没有适当的配置中,应用服务器将不知道负载均衡器存在,并将请求视为 客户端请求 https://192.168.1:8080 。
要解决此问题,您可以使用 RFC 7239 来指定正在使用负载平衡器。 为了使应用程序意识到这一点,您需要配置应用程序服务器以了解X-Forwarded标头。 例如,Tomcat使用 RemoteIpValve, 而Jetty使用 ForwardedRequestCustomizer 。 另外,Spring 4.3及更高版本的用户可以利用 ForwardedHeaderFilter 。
Spring Boot用户可以使用该
server.use-forward-headers
属性来配置应用程序。
有关
更多详细信息,
请参见
Spring Boot文档
。
19.5. Spring Security常见问题解答
19.5.1. 一般的问题
Spring Security会满足我所有的应用程序安全性要求吗?
Spring Security为您的身份验证和授权要求提供了一个非常灵活的框架,但是在构建安全应用程序时还需要考虑许多其他因素。 Web应用程序容易受到各种您应该熟悉的攻击的攻击,最好在开始开发之前就进行攻击,因此您可以从一开始就在设计和编写代码时就牢记在心。 请访问http://www.owasp.org/[OWASP网站],以获取有关Web应用程序开发人员面临的主要问题的信息,以及可以对他们使用的对策。
为什么不仅仅使用web.xml安全性?
假设您正在开发基于Spring的企业应用程序。 通常需要解决四个安全问题:身份验证,Web请求安全性,服务层安全性(即,实现业务逻辑的方法)和域对象实例的安全性(即不同的域对象具有不同的权限)。 牢记以下典型要求:
-
认证 :Servlet规范提供了一种认证方法。 但是,您将需要配置容器以执行身份验证,这通常需要编辑特定于容器的“领域”设置。 这将导致不可移植的配置,如果您需要编写实际的Java类来实现容器的身份验证接口,则它甚至变得更加不可移植。 使用Spring Security,您可以实现完全的可移植性-一直到WAR级别。 此外,Spring Security还提供了经过生产验证的身份验证提供程序和机制,供您选择,这意味着您可以在部署时切换身份验证方法。 对于编写需要在未知目标环境中工作的产品的软件供应商而言,这特别有价值。
-
Web请求安全性: Servlet规范提供了一种保护请求URI的方法。 但是,这些URI只能以Servlet规范自己的受限URI路径格式表示。 Spring Security提供了一种更为全面的方法。 例如,您可以使用Ant路径或正则表达式,可以考虑URI的一部分,而不仅仅是请求的页面(例如,可以考虑HTTP GET参数),并且可以实现自己的配置数据的运行时源。 这意味着您的Web请求安全性可以在Webapp的实际执行过程中动态更改。
-
服务层和域对象安全性: Servlet规范中缺少对服务层安全性或域对象实例安全性的支持,这表示对多层应用程序的严重限制。 通常,开发人员要么忽略这些要求,要么在其MVC控制器代码中实现安全逻辑(或者更糟的是在视图内部)。 这种方法有严重的缺点:
-
关注点分离: 授权是一个横切关注点,应照此实施。 MVC控制器或实现授权代码的视图使测试控制器和授权逻辑更加困难,调试更加困难,并且通常会导致代码重复。
-
对富客户端和Web服务的支持: 如果最终必须支持其他客户端类型,则嵌入在Web层中的任何授权代码都是不可重用的。 应该考虑到Spring远程出口商仅出口服务层bean(而不是MVC控制器)。 这样,授权逻辑需要位于服务层中以支持多种客户端类型。
-
分层问题: MVC控制器或视图只是错误的体系结构层,无法实现有关服务层方法或域对象实例的授权决策。 尽管可以将主体传递到服务层以使其能够做出授权决策,但这样做会在每个服务层方法上引入一个附加参数。 一种更优雅的方法是使用ThreadLocal来容纳Principal,尽管这可能会增加开发时间,以至于仅使用专用的安全框架就变得更加经济(基于成本效益)。
-
授权代码质量: Web框架经常被提及为“它们使做正确的事变得更容易,而做错事则更难”。 安全框架是相同的,因为它们以抽象的方式设计用于多种用途。 从头开始编写自己的授权代码不会提供框架会提供的“设计检查”,而且内部授权代码通常将缺乏广泛部署,同行评审和新版本带来的改进。
-
对于简单的应用程序,servlet规范安全性可能就足够了。 尽管在Web容器可移植性,配置要求,有限的Web请求安全性以及不存在的服务层和域对象实例安全性的上下文中进行考虑,但很清楚的是,为什么开发人员经常寻求替代解决方案。
需要哪些Java和Spring Framework版本?
Spring Security 3.0和3.1至少需要JDK 1.5,还至少需要Spring 3.0.3. 理想情况下,您应该使用最新版本,以避免出现问题。
Spring Security 2.0.x要求JDK的最低版本为1.4,并且是针对Spring 2.0.x构建的。 它也应该与使用Spring 2.5.x的应用程序兼容。
我是Spring Security的新手,我需要构建一个支持通过HTTPS进行CAS单一登录的应用程序,同时允许对某些URL在本地进行基本身份验证,并针对多个后端用户信息源(LDAP和JDBC)进行身份验证。 我已经复制了一些找到的配置文件,但是它不起作用。
有什么事吗
或替代其他复杂方案...
实际上,您需要先了解要使用的技术,然后才能成功使用它们构建应用程序。 安全性很复杂。 使用登录表单设置简单的配置,并使用Spring Security的命名空间设置一些硬编码的用户,这相当简单。 转向使用支持的JDBC数据库也很容易。 但是,如果您尝试直接跳入这种复杂的部署方案,则几乎肯定会感到沮丧。 设置CAS之类的系统,配置LDAP服务器以及正确安装SSL证书所需的学习曲线有了很大的提高。 因此,您需要一次一步。
从Spring Security的角度来看,您应该做的第一件事是遵循网站上的“入门”指南。 这将带您完成一系列步骤,以启动并运行并了解框架的运行方式。 如果使用的是您不熟悉的其他技术,则应进行一些研究,并尝试确保在将它们组合到复杂系统中之前可以单独使用它们。
19.5.2. 常见问题
-
认证方式
-
会话管理
-
杂
当我尝试登录时,出现一条错误消息,提示“凭据错误”。 怎么了?
这意味着认证失败。 它并没有说明原因,因为最好的做法是避免提供可能有助于攻击者猜测帐户名或密码的详细信息。
这也意味着,如果您在论坛中提出此问题,除非您提供其他信息,否则您将无法获得答案。
与任何问题一样,您应该检查调试日志的输出,注意所有异常堆栈跟踪和相关消息。
在调试器中单步执行代码以查看身份验证失败的原因以及原因。
编写一个测试案例,在应用程序外部练习您的身份验证配置。
失败通常是由于数据库中存储的密码数据与用户输入的密码数据不同。
如果您使用哈希密码,请确保数据库中存储
的值与
应用程序中配置
的值
完全相同
PasswordEncoder
。
当我尝试登录时,我的应用程序进入“无限循环”,这是怎么回事?
无限循环和重定向到登录页面的常见用户问题是由于不小心将登录页面配置为“安全”资源引起的。 通过从安全过滤器链中排除登录页面或将其标记为需要ROLE_ANONYMOUS,确保您的配置允许匿名访问登录页面。
如果您的AccessDecisionManager包含AuthenticatedVoter,则可以使用属性“ IS_AUTHENTICATED_ANONYMOUSLY”。 如果您使用标准名称空间配置设置,则该选项自动可用。
从Spring Security 2.0.1开始,当您使用基于名称空间的配置时,将在加载应用程序上下文时进行检查,并且如果登录页面似乎受到保护,则会记录一条警告消息。
我收到一条消息“访问被拒绝(用户是匿名用户);”的异常。 怎么了?
这是调试级别的消息,它在匿名用户首次尝试访问受保护的资源时发生。
DEBUG [ExceptionTranslationFilter] - Access is denied (user is anonymous); redirecting to authentication entry point
org.springframework.security.AccessDeniedException: Access is denied
at org.springframework.security.vote.AffirmativeBased.decide(AffirmativeBased.java:68)
at org.springframework.security.intercept.AbstractSecurityInterceptor.beforeInvocation(AbstractSecurityInterceptor.java:262)
这是正常现象,不必担心。
为什么即使我退出了应用程序,仍然可以看到受保护的页面?
造成这种情况的最常见原因是您的浏览器已经缓存了该页面,并且您看到的是从浏览器缓存中检索到的副本。
通过检查浏览器是否确实在发送请求来验证这一点(检查服务器访问日志,调试日志或使用合适的浏览器调试插件,例如Firefox的“ Tamper Data”)。
这与Spring Security无关,您应该配置应用程序或服务器以设置适当的
Cache-Control
响应头。
请注意,永远不会缓存SSL请求。
我收到一条消息“在SecurityContext中找不到身份验证对象”的异常。 怎么了?
这是另一条调试级别消息,该消息在匿名用户首次尝试访问受保护的资源时出现,但是
AnonymousAuthenticationFilter
在您的过滤器链配置中没有时出现。
DEBUG [ExceptionTranslationFilter] - Authentication exception occurred; redirecting to authentication entry point
org.springframework.security.AuthenticationCredentialsNotFoundException:
An Authentication object was not found in the SecurityContext
at org.springframework.security.intercept.AbstractSecurityInterceptor.credentialsNotFound(AbstractSecurityInterceptor.java:342)
at org.springframework.security.intercept.AbstractSecurityInterceptor.beforeInvocation(AbstractSecurityInterceptor.java:254)
这是正常现象,不必担心。
我无法使用LDAP身份验证。
我的配置有什么问题?
请注意,LDAP目录的权限通常不允许您读取用户密码。 因此,通常无法使用 什么是UserDetailsService,我需要一个吗? Spring Security将存储的密码与用户提交的密码进行比较。 最常见的方法是使用LDAP“绑定”,这是 LDAP协议 支持的操作之一 。 通过这种方法,Spring Security通过尝试以用户身份验证目录来验证密码。
LDAP认证最常见的问题是缺乏对目录服务器树结构和配置的了解。 不同公司的情况会有所不同,因此您必须自己找出来。 在将Spring Security LDAP配置添加到应用程序之前,最好使用标准Java LDAP代码(不涉及Spring Security)编写一个简单的测试,并确保您可以使其首先工作。 例如,要验证用户身份,可以使用以下代码:
@Test
public void ldapAuthenticationIsSuccessful() throws Exception {
Hashtable<String,String> env = new Hashtable<String,String>();
env.put(Context.SECURITY_AUTHENTICATION, "simple");
env.put(Context.SECURITY_PRINCIPAL, "cn=joe,ou=users,dc=mycompany,dc=com");
env.put(Context.PROVIDER_URL, "ldap://mycompany.com:389/dc=mycompany,dc=com");
env.put(Context.SECURITY_CREDENTIALS, "joespassword");
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
InitialLdapContext ctx = new InitialLdapContext(env, null);
}
会话管理
会话管理问题是论坛问题的常见来源。 如果要开发Java Web应用程序,则应了解如何在Servlet容器和用户浏览器之间维护会话。 您还应该了解安全和非安全Cookie的区别,以及使用HTTP / HTTPS以及在两者之间进行切换的含义。 Spring Security与维护会话或提供会话标识符无关。 这完全由servlet容器处理。
我正在使用Spring Security的并发会话控制来防止用户一次登录多次。
登录后打开另一个浏览器窗口时,它不会阻止我再次登录。 为什么我可以多次登录?
浏览器通常每个浏览器实例维护一个会话。 您不能一次有两个单独的会话。 因此,如果您再次在另一个窗口或选项卡中登录,那么您将在同一会话中重新进行身份验证。 服务器对选项卡,窗口或浏览器实例一无所知。 它所看到的只是HTTP请求,并根据它们所包含的JSESSIONID cookie的值将它们与特定会话相关联。 当用户在会话期间进行身份验证时,Spring Security的并发会话控件会检查 他们拥有 的 其他已身份验证会话 的数量 。 如果它们已经通过同一会话进行了身份验证,则重新身份验证将无效。
通过Spring Security进行身份验证时,为什么会话ID会更改?
使用默认配置,Spring Security在用户认证时更改会话ID。 如果您使用的是Servlet 3.1或更高版本的容器,则只需更改会话ID。 如果您使用的是较旧的容器,Spring Security将使现有会话无效,创建一个新会话,并将会话数据传输到新会话。 以这种方式改变会话标识符可以防止“会话固定”攻击。 您可以在网上和 Reference手册中找到有关此内容的更多信息。
我正在使用Tomcat(或其他一些servlet容器),并且已为登录页面启用HTTPS,然后再切换回HTTP。
它不起作用-经过身份验证后,我只是回到登录页面。
发生这种情况是因为在HTTPS下创建的会话(会话cookie标记为“安全”)无法随后在HTTP下使用。 浏览器不会将cookie发送回服务器,并且任何会话状态都将丢失(包括安全上下文信息)。 首先使用HTTP启动会话应该可以正常工作,因为会话cookie不会被标记为安全。 但是,Spring Security的 会话固定保护 之所以会对此造成干扰,因为它通常会使用安全标志将新的会话ID Cookie发送回用户的浏览器。 要解决此问题,您可以禁用会话固定保护,但是在较新的Servlet容器中,您也可以配置会话cookie,使其从不使用安全标志。 请注意,在HTTP和HTTPS之间切换通常不是一个好主意,因为任何完全使用HTTP的应用程序都容易受到中间人攻击。 为了真正确保安全,用户应开始使用HTTPS访问您的站点并继续使用它,直到注销为止。 即使从通过HTTP访问的页面上单击HTTPS链接也可能存在风险。 如果您需要更多说服力,请查看 sslstrip之 类的工具 。
我没有在HTTP和HTTPS之间切换,但是我的会话仍然丢失
可以通过交换会话Cookie或
jsessionid
向URL
添加
参数来
维护会话
(如果您使用JSTL输出URL或调用
HttpServletResponse.encodeUrl
URL(例如在重定向之前)
,则会自动发生
。如果客户端禁用了Cookie,并且您没有重写URL以包括
jsessionid
,那么会话将丢失。请注意,出于安全原因,首选使用cookie,因为它不会在URL中公开会话信息。
我正在尝试使用并发会话控制支持,但是即使我确定我已经注销并且没有超出允许的会话,它也不允许我重新登录。
确保已将侦听器添加到web.xml文件。 必须确保在会话被销毁时通知Spring Security会话注册表。 没有它,会话信息将不会从注册表中删除。
<listener>
<listener-class>org.springframework.security.web.session.HttpSessionEventPublisher</listener-class>
</listener>
通过将create-session属性设置为never,Spring Security在某个地方创建了会话,即使我没有配置会话也是如此。
这通常意味着用户的应用程序正在某个地方创建会话,但是他们不知道该会话。
最常见的罪魁祸首是JSP。
许多人不知道默认情况下,JSP创建会话。
为了防止JSP创建会话,请将指令添加
<%@ page session="false" %>
到页面顶部。
如果在确定创建会话的位置时遇到麻烦,可以添加一些调试代码来跟踪位置。
一种方法是
javax.servlet.http.HttpSessionListener
在应用程序中
添加一个
,
Thread.dumpStack()
以在
sessionCreated
方法中
进行调用
。
执行POST时收到403 Forbidden
如果为HTTP POST返回了HTTP 403 Forbidden,但对于HTTP GET适用,则该问题很可能与 CSRF 有关 。 提供CSRF令牌或禁用CSRF保护(不建议)。
我正在使用RequestDispatcher将请求转发到另一个URL,但是没有应用我的安全约束。
过滤器默认情况下不应用于转发或包含。 如果您确实希望将安全过滤器应用于转发和/或包含,则必须使用<dispatcher>元素(<filter-mapping>的子元素)在web.xml中显式配置这些过滤器。
我已经在应用程序上下文中添加了Spring Security的<global-method-security>元素,但是如果我在Spring MVC控制器bean(Struts操作等)中添加了安全注释,那么它们似乎没有效果。
在Spring Web应用程序中,保存用于调度程序Servlet的Spring MVC bean的应用程序上下文通常与主应用程序上下文分开。
它往往是在一个名为文件中定义
myapp-servlet.xml
,其中“MYAPP”是分配给春天的名称
DispatcherServlet
中
web.xml
。
一个应用程序可以有多个
DispatcherServlet
,每个都有自己的隔离应用程序上下文。
这些“子”上下文中的Bean对应用程序的其余部分不可见。
“父”应用程序上下文由
ContextLoaderListener
您在中定义的
加载,
web.xml
并且对所有子上下文可见。
通常在此父上下文中定义安全配置,包括
<global-method-security>
元件)。
结果,将无法实施应用于这些Web Bean中的方法的任何安全性约束,因为无法从
DispatcherServlet
上下文中
看到这些Bean
。
您需要将
<global-method-security>
声明移至Web上下文,或将要保护的Bean移至主应用程序上下文。
通常,我们建议在服务层而不是单个Web控制器上应用方法安全性。
19.5.3. Spring Security架构问题
我怎么知道X属于哪个包类?
定位类的最佳方法是在IDE中安装Spring Security源代码。
该发行版包括项目分成的每个模块的源jar。
将它们添加到项目源路径中,然后您可以直接导航到Spring Security类(
Ctrl-Shift-T
在Eclipse中)。
这也使调试更加容易,并允许您通过直接查看异常发生的地方来查看异常情况,从而对异常进行故障排除。
命名空间元素如何映射到常规bean配置?
在 Reference指南的名称空间附录中,概述了由名称空间创建的bean。
在
blog.springsource.com
上还有一篇详细的博客文章,名为“ Spring Security命名空间的背后”
。
如果想知道全部细节,那么代码在
spring-security-config
Spring Security 3.0发行版
的
模块中。
您可能应该先阅读标准Spring Framework Reference文档中有关名称空间解析的章节。
“ ROLE_”是什么意思,为什么我在角色名称上需要它?
Spring Security具有基于投票者的架构,这意味着访问决策由一系列
AccessDecisionVoter
s决定。
投票者根据为安全资源指定的“配置属性”(例如方法调用)进行操作。
使用这种方法,并非所有属性都可能与所有选民相关,并且选民需要知道何时应该忽略属性(弃权)以及何时应该投票基于属性值授予或拒绝访问权限。
最常见的选民是
RoleVoter
默认情况下,只要找到带有“ ROLE_”前缀的属性,它都会投票。
它将属性(例如“ ROLE_USER”)与当前用户已分配的权限名称进行简单比较。
如果找到匹配项(它们具有称为“ ROLE_USER”的权限),则投票批准授予访问权限,否则投票拒绝访问。
可以通过设置的
rolePrefix
属性
来更改前缀
RoleVoter
。
如果只需要在应用程序中使用角色,而无需其他自定义投票者,则可以将前缀设置为空白字符串,在这种情况下,
RoleVoter
会将所有属性视为角色。
我如何知道要添加到我的应用程序中的哪些依赖项才能与Spring Security一起使用?
这将取决于您使用的功能以及所开发的应用程序类型。
使用Spring Security 3.0,将项目jar分为明显不同的功能区域,因此可以很容易地从应用程序需求中确定所需的Spring Security jar。
所有应用程序都需要
spring-security-core
jar。
如果要开发Web应用程序,则需要
spring-security-web
jar。
如果使用安全名称空间配置,则需要
spring-security-config
jar,要获得LDAP支持,则需要
spring-security-ldap
jar,依此类推。
对于第三方罐子,情况并不总是那么明显。 一个好的起点是从预先构建的示例应用程序WEB-INF / lib目录之一复制那些目录。 对于基本应用程序,您可以从教程示例开始。 如果要对嵌入式测试服务器使用LDAP,请以LDAP示例为起点。 Reference手册还包括http://static.springsource.org/spring-security/site/docs/3.1.x/reference/springsecurity-single.html#appendix-dependencies [附录]列出了每个Spring的第一级依赖关系安全模块,其中包含有关它们是否可选以及所需功能的一些信息。
如果您正在使用maven构建项目,则将适当的Spring Security模块作为依赖项添加到pom.xml中,将自动提取框架所需的核心jar。 如果需要,任何在Spring Security POM文件中标记为“可选”的文件都必须添加到您自己的pom.xml文件中。
运行嵌入式ApacheDS LDAP服务器需要什么依赖关系?
如果使用的是Maven,则需要将以下内容添加到pom依赖项中:
<dependency>
<groupId>org.apache.directory.server</groupId>
<artifactId>apacheds-core</artifactId>
<version>1.5.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.apache.directory.server</groupId>
<artifactId>apacheds-server-jndi</artifactId>
<version>1.5.5</version>
<scope>runtime</scope>
</dependency>
其他需要的罐子应暂时移入。
什么是UserDetailsService,我需要一个吗?
UserDetailsService
是DAO界面,用于加载特定于用户帐户的数据。
除了加载该数据以供框架中的其他组件使用外,它没有其他功能。
它不负责验证用户身份。
使用用户名/密码组合对用户进行身份验证通常由进行
DaoAuthenticationProvider
,向中注入
UserDetailsService
可以允许用户加载用户的密码(和其他数据),以便将其与提交的值进行比较。
请注意,如果您使用的是LDAP,则
此方法可能无效
。
如果要自定义身份验证过程,则应
AuthenticationProvider
自己
实施
。
请参阅此
博客文章
,以获取将Spring Security身份验证与Google App Engine集成的示例。
19.5.4. 常见的“操作方法”请求
我需要登录的信息不仅仅是用户名。
如何添加对额外登录字段(例如公司名称)的支持?
这个问题在Spring Security论坛中反复出现,因此您可以通过搜索档案(或通过google)在那里找到更多信息。
提交的登录信息由的实例处理
UsernamePasswordAuthenticationFilter
。
您将需要自定义此类以处理额外的数据字段。
一种选择是使用您自己的自定义身份验证令牌类(而不是标准的
UsernamePasswordAuthenticationToken
),另一种选择是简单地将多余的字段与用户名连接起来(例如,使用“:”作为分隔符),并将其传递给
UsernamePasswordAuthenticationToken
。
您还需要自定义实际的身份验证过程。
例如,如果您使用的是自定义身份验证令牌类,则必须编写一个
AuthenticationProvider
来处理它(或扩展standard
DaoAuthenticationProvider
)。
如果已串联字段,则可以实现自己的字段,将
UserDetailsService
其拆分并加载适当的用户数据以进行身份验证。
如果只有所请求的URL的片段值不同(例如,/ foo#bar和/ foo#blah),如何应用不同的拦截URL约束?
您无法执行此操作,因为该片段不会从浏览器传输到服务器。 从服务器的角度来看,上面的URL是相同的。 这是GWT用户的常见问题。
如何在UserDetailsService中访问用户的IP地址(或其他Web请求数据)?
显然,您不能(不求助于线程局部变量),因为提供给接口的唯一信息是用户名。
除了实现之外
UserDetailsService
,您应该
AuthenticationProvider
直接
实现
并从提供的
Authentication
令牌中
提取信息
。
在标准的Web设置中,
对象
getDetails()
上的方法
Authentication
将返回的实例
WebAuthenticationDetails
。
如果需要其他信息,可以将自定义项
AuthenticationDetailsSource
注入正在使用的身份验证过滤器中。
如果您正在使用名称空间(例如与
<form-login>
元素一起使用),则应删除该元素,并用
<custom-filter>
指向显式配置
的
声明
代替它
UsernamePasswordAuthenticationFilter
。
如何从UserDetailsService访问HttpSession?
您不能,因为尚不
UserDetailsService
了解Servlet API。
如果要存储自定义用户数据,则应该自定义
UserDetails
返回
的
对象。
然后可以通过thread-local在任何时候访问它
SecurityContextHolder
。
的调用
SecurityContextHolder.getContext().getAuthentication().getPrincipal()
将返回此自定义对象。
如果您确实需要访问该会话,则必须通过自定义Web层来完成。
如何在UserDetailsService中访问用户密码?
您不能(也不应该)。 您可能会误解其目的。 请参阅上面的“ 什么是UserDetailsService? ”。
如何动态定义应用程序中的安全URL?
人们经常问如何在数据库中而不是在应用程序上下文中存储安全URL和安全元数据属性之间的映射。
您应该问自己的第一件事是您是否真的需要这样做。 如果应用程序需要安全保护,则还要求根据定义的策略对安全性进行彻底测试。 在将其推广到生产环境之前,可能需要进行审核和验收测试。 一个安全意识强的组织应该意识到,通过更改配置数据库中的一两行,可以在运行时修改安全设置,可以立即消除其辛苦的测试过程的好处。 如果考虑到这一点(可能在应用程序中使用多层安全性),那么Spring Security允许您完全自定义安全性元数据的来源。 您可以选择使其完全动态。
方法安全性和Web安全性均受其子类保护,这些子类
AbstractSecurityInterceptor
均配置有
子类,
SecurityMetadataSource
从
子类中
获取特定方法或过滤器调用的元数据。
为了Web安全,拦截器类为
FilterSecurityInterceptor
,它使用marker接口
FilterInvocationSecurityMetadataSource
。
它操作的“受保护对象”类型是
FilterInvocation
。
使用的默认实现(在名称空间中
<http>
和在显式配置拦截器时
ConfigAttribute
)均在内存映射中存储
URL模式列表及其“配置属性”(的实例
)
的对应列表
。
要从备用源加载数据,必须使用显式声明的安全过滤器链(通常是Spring Security的链
FilterChainProxy
)以自定义
FilterSecurityInterceptor
Bean。
您不能使用名称空间。
然后,您将实现
FilterInvocationSecurityMetadataSource
根据需要为特定的
FilterInvocation [ 19 ]
加载数据
。
一个非常基本的轮廓如下所示:
public class MyFilterSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
public List<ConfigAttribute> getAttributes(Object object) {
FilterInvocation fi = (FilterInvocation) object;
String url = fi.getRequestUrl();
String httpMethod = fi.getRequest().getMethod();
List<ConfigAttribute> attributes = new ArrayList<ConfigAttribute>();
// Lookup your database (or other source) using this information and populate the
// list of attributes
return attributes;
}
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
public boolean supports(Class<?> clazz) {
return FilterInvocation.class.isAssignableFrom(clazz);
}
}
有关更多信息,请参见的代码
DefaultFilterInvocationSecurityMetadataSource
。
如何针对LDAP进行身份验证,但如何从数据库中加载用户角色?
该
LdapAuthenticationProvider
豆(负责处理Spring Security的正常LDAP认证)配置了两个独立的策略接口,一个执行身份验证和一个用来加载用户权限,所谓的
LdapAuthenticator
和
LdapAuthoritiesPopulator
分别。
该
DefaultLdapAuthoritiesPopulator
载荷从LDAP目录中的用户当局和具有各种配置参数,允许您指定它们如何被检索。
要改为使用JDBC,您可以使用适合您的模式的任何SQL自己实现接口:
public class MyAuthoritiesPopulator implements LdapAuthoritiesPopulator {
@Autowired
JdbcTemplate template;
List<GrantedAuthority> getGrantedAuthorities(DirContextOperations userData, String username) {
List<GrantedAuthority> = template.query("select role from roles where username = ?",
new String[] {username},
new RowMapper<GrantedAuthority>() {
/**
* We're assuming here that you're using the standard convention of using the role
* prefix "ROLE_" to mark attributes which are supported by Spring Security's RoleVoter.
*/
public GrantedAuthority mapRow(ResultSet rs, int rowNum) throws SQLException {
return new SimpleGrantedAuthority("ROLE_" + rs.getString(1);
}
}
}
}
然后,您可以将这种类型的bean添加到您的应用程序上下文中,并将其注入
LdapAuthenticationProvider
。
在 Reference手册的LDAP章节中有关使用显式Spring Bean配置LDAP的部分中对此进行了介绍。
请注意,在这种情况下,不能使用名称空间进行配置。
您还应该向Javadoc查询相关的类和接口。
我想修改由名称空间创建的bean的属性,但是架构中没有任何东西可以支持它。
除了放弃使用命名空间外,我还能做什么?
命名空间功能是有意限制的,因此无法涵盖使用普通bean可以做的所有事情。
如果您想做一些简单的事情,例如修改Bean或注入不同的依赖项,则可以通过
BeanPostProcessor
在配置中
添加A
来实现。
可以在
Spring Reference Manual中
找到更多信息
。
为此,您需要对创建哪些bean有一点了解,因此,您还应该阅读上述问题中有关
名称空间如何映射到Spring bean
的博客文章
。
通常,您需要将所需的功能添加到的
postProcessBeforeInitialization
方法中
BeanPostProcessor
。
比方说,你想定制
AuthenticationDetailsSource
使用的
UsernamePasswordAuthenticationFilter
,(通过创建的
form-login
元素)。
您想要
CUSTOM_HEADER
从请求中
提取调用的特定标头
,并在验证用户身份时使用它。
处理器类如下所示:
public class BeanPostProcessor implements BeanPostProcessor {
public Object postProcessAfterInitialization(Object bean, String name) {
if (bean instanceof UsernamePasswordAuthenticationFilter) {
System.out.println("********* Post-processing " + name);
((UsernamePasswordAuthenticationFilter)bean).setAuthenticationDetailsSource(
new AuthenticationDetailsSource() {
public Object buildDetails(Object context) {
return ((HttpServletRequest)context).getHeader("CUSTOM_HEADER");
}
});
}
return bean;
}
public Object postProcessBeforeInitialization(Object bean, String name) {
return bean;
}
}
然后,您将在应用程序上下文中注册此bean。 Spring将在应用程序上下文中定义的bean上自动调用它。
反应性应用
20. WebFlux安全
Spring Security的WebFlux支持依赖于,
WebFilter
并且对Spring WebFlux和Spring WebFlux.Fn相同。
您可以找到一些示例程序来演示以下代码:
-
您好WebFlux hellowebflux
-
您好WebFlux.Fn hellowebfluxfn
-
Hello WebFlux方法 hellowebflux方法
20.1. 最小的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保护等。
20.2. 显式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(exchanges ->
exchanges
.anyExchange().authenticated()
)
.httpBasic(withDefaults())
.formLogin(withDefaults());
return http.build();
}
}
该配置显式设置与最小配置相同的所有内容。 在这里,您可以轻松地更改默认值。
安全HTTP响应标头 本节讨论Spring Security对在WebFlux的响应中添加各种安全标头的支持。
21.默认安全标题
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
具体来说,您希望所有默认标头都具有以下自定义设置:
-
X-Frame-Options 允许来自相同域的任何请求
-
HTTP严格传输安全性(HSTS) 不会添加到响应中
您可以使用以下Java配置轻松完成此操作:
@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http
// ...
.headers(headers ->
headers
.hsts(hsts ->
hsts
.disable()
)
.frameOptions(frameOptions ->
frameOptions
.mode(Mode.SAMEORIGIN)
)
);
return http.build();
}
如果您不想添加默认值,并且希望对应使用的内容进行明确控制,则可以禁用默认值。 下面提供了基于Java和XML的配置示例:
如有必要,可以使用以下Java配置禁用所有HTTP安全响应标头:
@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http
// ...
.headers(headers ->
headers
.disable()
);
return http.build();
}
21.1. 缓存控制
过去,Spring Security要求您为Web应用程序提供自己的缓存控件。 当时看来这是合理的,但是浏览器缓存已经演变为包括用于安全连接的缓存。 这意味着用户可以查看经过身份验证的页面,然后注销,然后恶意用户可以使用浏览器历史记录来查看缓存的页面。 为了缓解这种情况,Spring Security添加了缓存控制支持,默认情况下会将以下标头插入响应中。
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
如果您确实想缓存特定的响应,则您的应用程序可以有选择地设置缓存控制标头以覆盖Spring Security设置的标头。 这对于确保正确缓存CSS,JavaScript和图像之类的内容很有用。
您还可以使用以下Java配置禁用缓存控制:
@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http
// ...
.headers(headers ->
headers
.cache(cache -> cache.disable())
);
return http.build();
}
21.2. 内容类型选项
从历史上看,包括Internet Explorer在内的浏览器都会尝试使用 内容嗅探 来猜测请求的内容类型 。 这允许浏览器通过猜测未指定内容类型的资源上的内容类型来改善用户体验。 例如,如果浏览器遇到一个未指定内容类型的JavaScript文件,它将能够猜测该内容类型然后执行。
| ==在允许上载内容时,还有许多其他事情要做(即,仅在不同的域中显示文档,确保设置了Content-Type标头,清理文档等)。 但是,这些措施不在Spring Security提供的范围之内。 指出禁用内容嗅探时也很重要,您必须指定内容类型才能使内容正常工作。 == |
内容嗅探的问题在于,这允许恶意用户使用多义标记(即,可以作为多种内容类型有效的文件)执行XSS攻击。 例如,某些网站可能允许用户向网站提交有效的附言文档并进行查看。 恶意用户可能会创建一个 也是有效JavaScript文件 的 Postscript文档, 并对其执行XSS攻击。
可以通过在响应中添加以下标头来禁用内容嗅探:
X-Content-Type-Options: nosniff
与缓存控制元素一样,默认情况下会添加nosniff指令。 但是,如果需要禁用标题,则可以使用以下内容:
@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http
// ...
.headers(headers ->
headers
.contentTypeOptions(contentTypeOptions -> contentTypeOptions.disable())
);
return http.build();
}
21.3. 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 。 这大大降低了发生中间人攻击的可能性。
| ==根据 RFC6797 ,HSTS标头仅注入HTTPS响应中。 为了使浏览器能够确认标头,浏览器必须首先信任对用于建立连接的SSL证书(不仅仅是SSL证书)进行签名的CA。 == |
将站点标记为HSTS主机的一种方法是将主机预加载到浏览器中。 另一方法是将“ Strict-Transport-Security”标头添加到响应中。 例如,以下内容将指示浏览器将域视为一年的HSTS主机(一年大约31536000秒):
Strict-Transport-Security: max-age=31536000 ; includeSubDomains ; preload
可选的includeSubDomains指令指示Spring Security子域(即secure.mybank.example.com)也应被视为HSTS域。
可选的preload指令指示Spring Security该域应在浏览器中预加载为HSTS域。 有关HSTS预加载的更多详细信息,请参见 https://hstspreload.org 。
与其他头文件一样,Spring Security默认添加HSTS。 您可以使用Java配置自定义HSTS标头:
@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http
// ...
.headers(headers ->
headers
.hsts(hsts ->
hsts
.includeSubdomains(true)
.preload(true)
.maxAge(Duration.ofDays(365))
)
);
return http.build();
}
21.4. X框架选项
允许将您的网站添加到框架可能是一个安全问题。 例如,使用聪明的CSS样式用户可能会被诱骗点击他们不想要的内容( 视频演示 )。 例如,登录到其银行的用户可以单击将按钮授予其他用户访问权限。 这种攻击称为 Clickjacking 。
| ==处理点击劫持的另一种现代方法是使用 内容安全策略(CSP) 。 == |
有许多方法可以缓解点击劫持攻击。 例如,为了保护旧版浏览器免受点击劫持攻击,您可以使用 破帧代码 。 虽然不完美,但是对于传统浏览器而言,破帧代码是最好的选择。
解决点击劫持的更现代方法是使用 X-Frame-Options 标头:
X-Frame-Options: DENY
X-Frame-Options响应标头指示浏览器阻止响应中带有此标头的任何网站呈现在框架中。 默认情况下,Spring Security禁用iframe中的呈现。
您可以使用以下命令使用Java配置来自定义X-Frame-Options:
@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http
// ...
.headers(headers ->
headers
.frameOptions(frameOptions ->
frameOptions
.mode(SAMEORIGIN)
)
);
return http.build();
}
21.5. X-XSS保护
一些浏览器内置了对过滤掉 反射的XSS攻击的支持 。 这绝非万无一失,但确实有助于XSS保护。
通常默认情况下会启用过滤,因此添加标头通常只会确保标头已启用,并指示浏览器在检测到XSS攻击时应采取的措施。 例如,过滤器可能会尝试以最小侵入性的方式更改内容以仍然呈现所有内容。 有时,这种替换 本身 可能会成为 XSS漏洞 。 相反,最好是阻止内容,而不要尝试对其进行修复。 为此,我们可以添加以下标头:
X-XSS-Protection: 1; mode=block
默认情况下包含此标头。 但是,我们可以使用以下内容使用Java配置进行自定义:
@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http
// ...
.headers(headers ->
headers
.xssProtection(xssProtection -> xssProtection.disable())
);
return http.build();
}
21.6. 内容安全政策(CSP)
内容安全策略(CSP) 是Web应用程序可以利用的一种机制,可以缓解诸如跨站点脚本(XSS)之类的内容注入漏洞。 CSP是一种声明性策略,为Web应用程序作者提供了一种工具,可以声明该Web应用程序希望从中加载资源的来源,并最终将这些信息通知客户端(用户代理)。
| ==内容安全策略并非旨在解决所有内容注入漏洞。 取而代之的是,可以利用CSP帮助减少内容注入攻击所造成的危害。 作为第一道防线,Web应用程序作者应验证其输入并对其输出进行编码。 == |
Web应用程序可以通过在响应中包括以下HTTP标头之一来使用CSP:
-
内容安全政策
-
内容安全政策仅报告
这些标头中的每一个都用作将 安全策略 传递给客户端的机制。 安全策略包含一组 安全策略指令 (例如 script-src 和 object-src ),每个指令 负责声明特定资源表示形式的限制。
例如,Web应用程序可以通过在响应中包含以下标头来声明它希望从特定的受信任源中加载脚本:
Content-Security-Policy: script-src https://trustedscripts.example.com
用户代理会阻止 尝试从另一个源(而不是 script-src 指令中 声明的内容)加载脚本 。 此外,如果 在安全策略中声明 了 report-uri 指令,则用户代理会将违反行为报告给声明的URL。
例如,如果Web应用程序违反了已声明的安全策略,则以下响应标头将指示用户代理将违规报告发送到策略的 report-uri 指令中 指定的URL 。
Content-Security-Policy: script-src https://trustedscripts.example.com; report-uri /csp-report-endpoint/
违规报告 是标准JSON结构,可以由Web应用程序自己的API或公共托管的CSP违规报告服务(例如 REPORT-URI) 捕获 。
在 内容安全,策略报告,只有 头提供了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 ,但是仍然允许违规资源加载。
21.6.1. 配置内容安全策略
重要的是要注意,Spring Security 默认情况下 不会添加 内容安全策略。 Web应用程序作者必须声明安全策略以强制执行和/或监视受保护的资源。
例如,给定以下安全策略:
script-src 'self' https://trustedscripts.example.com; object-src https://trustedplugins.example.com; report-uri /csp-report-endpoint/
您可以使用Java配置启用CSP标头,如下所示:
@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http
// ...
.headers(headers ->
headers
.contentSecurityPolicy(contentSecurityPolicy ->
contentSecurityPolicy
.policyDirectives("script-src 'self' https://trustedscripts.example.com; object-src https://trustedplugins.example.com; report-uri /csp-report-endpoint/")
)
);
return http.build();
}
要启用CSP “仅报告” 标头,请提供以下Java配置:
@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http
// ...
.headers(headers ->
headers
.contentSecurityPolicy(contentSecurityPolicy ->
contentSecurityPolicy
.policyDirectives("script-src 'self' https://trustedscripts.example.com; object-src https://trustedplugins.example.com; report-uri /csp-report-endpoint/")
.reportOnly()
)
);
return http.build();
}
21.7. 推荐人政策
引荐来源网址 政策 是一种机制,Web应用程序可以利用该机制来管理引荐来源网址字段,该字段包含用户所在的最后一页。
Spring Security的方法是使用 Referrer Policy 标头,该标头提供了不同的 策略 :
Referrer-Policy: same-origin
Referrer-Policy响应标头指示浏览器让目的地知道用户先前所在的源。
21.7.1. 配置推荐人策略
Spring Security 默认 不添加 Referrer Policy标头。
您可以使用Java配置启用Referrer-Policy标头,如下所示:
@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http
// ...
.headers(headers ->
headers
.referrerPolicy(referrerPolicy ->
referrerPolicy
.policy(ReferrerPolicy.SAME_ORIGIN)
)
);
return http.build();
}
21.8. 功能政策
功能策略 是一种机制,允许Web开发人员在浏览器中选择性地启用,禁用和修改某些API和Web功能的行为。
Feature-Policy: geolocation 'self'
借助功能策略,开发人员可以为浏览器选择一套“策略”,以实施整个站点中使用的特定功能。 这些策略限制了站点可以访问或修改某些功能的浏览器默认行为的API。
21.9。 清除网站数据
清除站点数据 是一种机制,通过该机制,当HTTP响应包含以下标头时,可以删除所有浏览器端数据(Cookie,本地存储等):
Clear-Site-Data: "cache", "cookies", "storage", "executionContexts"
这是注销时执行的不错的清理操作。
21.9.1. 配置清除站点数据
Spring Security 默认情况下 不添加 “清除站点数据”标头。
您可以将应用程序配置为在注销时发送此标头,如下所示:
@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
ServerLogoutHandler securityContext = new SecurityContextServerLogoutHandler();
ServerLogoutHandler clearSiteData = new HeaderWriterServerLogoutHandler(new ClearSiteDataServerHttpHeadersWriter());
DelegatingServerLogoutHandler logoutHandler = new DelegatingServerLogoutHandler(securityContext, clearSiteData);
http
// ...
.logout()
.logoutHandler(logoutHandler);
return http.build();
}
不建议您通过
headers()
指令
配置此标头
编写器。
原因是
JSESSIONID
将删除
任何会话状态(例如
cookie),从而有效地注销用户。
|
22.重定向到HTTPS
需要HTTPS才能提供安全的应用程序。 可以使用以下Java配置将Spring Security配置为执行重定向到https的操作:
@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http
// ...
.redirectToHttps(withDefaults());
return http.build();
}
可以轻松地将配置包装在if语句周围,仅在生产环境中将其打开。
或者,可以通过查找仅在生产中发生的有关请求的属性来启用它。
例如,如果生产环境添加了名为
X-Forwarded-Proto
以下Java配置
的标头,则
可以使用:
@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http
// ...
.redirectToHttps(redirectToHttps ->
redirectToHttps
.httpsRedirectWhen(e -> e.getRequest().getHeaders().containsKey("X-Forwarded-Proto"))
);
return http.build();
}
23. OAuth2 WebFlux
Spring Security为响应式应用程序提供了OAuth2和WebFlux集成。
23.1. OAuth 2.0登录
OAuth 2.0登录功能为应用程序提供了使用户能够通过使用其在OAuth 2.0提供程序(例如GitHub)或OpenID Connect 1.0提供程序(例如Google)上的现有帐户登录该应用程序的功能。 OAuth 2.0登录实现了以下用例:“使用Google登录”或“使用GitHub登录”。
| OAuth 2.0登录是通过使用 OAuth 2.0授权框架 和 OpenID Connect Core 1.0中 指定 的 授权代码授予 来实现的 。 |
23.1.1. Spring Boot 2.0示例
Spring Boot 2.0为OAuth 2.0登录带来了完整的自动配置功能。
本部分说明如何 使用Google 作为身份验证提供程序 来配置 OAuth 2.0登录WebFlux示例, 并涵盖以下主题:
最初设定
要使用Google的OAuth 2.0身份验证系统进行登录,您必须在Google API控制台中设置一个项目以获得OAuth 2.0凭据。
| Google的OAuth 2.0 身份验证 实现 符合 OpenID Connect 1.0 规范,并且已通过 OpenID认证 。 |
请按照 “设置OAuth 2.0”部分开始 的 OpenID Connect 页面 上的说明进行操作 。
完成“获取OAuth 2.0凭据”说明后,您应该拥有一个新的OAuth客户端,其凭据由客户端ID和客户端密钥组成。
设置重定向URI
重定向URI是最终用户的用户代理在通过Google身份验证并授予 对“同意”页面上的 OAuth客户端 (在上一步中创建)的 访问权限后,重定向到应用程序中的路径 。
在“设置重定向URI”子部分中,确保将“
授权重定向URI”
字段设置为
http://localhost:8080/login/oauth2/code/google
。
默认重定向URI模板为
{baseUrl}/login/oauth2/code/{registrationId}
。
该
registrationId
是用于唯一标识符
ClientRegistration
。
对于我们的示例,
registrationId
是
google
。
|
配置
application.yml
现在,您有了Google的新OAuth客户端,您需要配置应用程序以将OAuth客户端用于 身份验证流程 。 为此:
-
转到
application.yml并设置以下配置:spring: security: oauth2: client: registration: (1) google: (2) client-id: google-client-id client-secret: google-client-secret例子25. OAuth客户端属性1个 spring.security.oauth2.client.registration是OAuth客户端属性的基本属性前缀。2 基本属性前缀后面是 ClientRegistration 的ID ,例如google。 -
将
client-id和client-secret属性中 的值替换为 您之前创建的OAuth 2.0凭据。
启动应用程序
启动Spring Boot 2.0示例并转到
http://localhost:8080
。
然后,您将重定向到默认的
自动生成的
登录页面,该页面显示Google的链接。
单击Google链接,然后您将重定向到Google进行身份验证。
在使用您的Google帐户凭据进行身份验证之后,显示给您的下一页是“同意”屏幕。 “同意”屏幕要求您允许或拒绝访问您之前创建的OAuth客户端。 点击 允许 以授权OAuth客户端访问您的电子邮件地址和基本个人资料信息。
此时,OAuth客户端将从 UserInfo端点 检索您的电子邮件地址和基本个人资料信息, 并建立经过身份验证的会话。
23.1.2. 使用OpenID提供程序配置
对于知名的提供程序,Spring Security为OAuth授权提供程序的配置提供了必要的默认值。
如果您正在使用一个支持自己的授权提供工作
OpenID提供配置
或
授权服务器的元数据
中,
OpenID提供配置响应
的
issuer-uri
可用于配置应用程序。
spring:
security:
oauth2:
client:
provider:
keycloak:
issuer-uri: https://idp.example.com/auth/realms/demo
registration:
keycloak:
client-id: spring-security
client-secret: 6cea952f-10d0-4d00-ac79-cc865820dc2c
该
issuer-uri
指示的Spring Security串联查询端点
https://idp.example.com/auth/realms/demo/.well-known/openid-configuration
,
https://idp.example.com/.well-known/openid-configuration/auth/realms/demo
或
https://idp.example.com/.well-known/oauth-authorization-server/auth/realms/demo
发现配置。
| Spring Security将一次查询一个端点,并在第一个端点发出200响应时停止。 |
该
client-id
和
client-secret
被链接到提供者,因为
keycloak
用于提供程序和注册两者。
23.1.3. 显式OAuth2登录配置
最小的OAuth2登录配置如下所示:
@Bean
ReactiveClientRegistrationRepository clientRegistrations() {
ClientRegistration clientRegistration = ClientRegistrations
.fromIssuerLocation("https://idp.example.com/auth/realms/demo")
.clientId("spring-security")
.clientSecret("6cea952f-10d0-4d00-ac79-cc865820dc2c")
.build();
return new InMemoryReactiveClientRegistrationRepository(clientRegistration);
}
@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http
// ...
.oauth2Login(withDefaults());
return http.build();
}
其他配置选项如下所示:
@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http
// ...
.oauth2Login(oauth2Login ->
oauth2Login
.authenticationConverter(converter)
.authenticationManager(manager)
.authorizedClientRepository(authorizedClients)
.clientRegistrationRepository(clientRegistrations)
);
return http.build();
}
23.2. OAuth2客户端
Spring Security的OAuth支持允许无需身份验证即可获取访问令牌。 Spring Boot的基本配置如下所示:
spring:
security:
oauth2:
client:
registration:
github:
client-id: replace-with-client-id
client-secret: replace-with-client-secret
scope: read:user,public_repo
您将需要用在
GitHub注册的值
替换
client-id
和
client-secret
。
下一步是指示Spring Security您希望充当OAuth2客户端,以便获取访问令牌。
@Bean
SecurityWebFilterChain configure(ServerHttpSecurity http) throws Exception {
http
// ...
.oauth2Client(withDefaults());
return http.build();
}
现在,您可以利用Spring Security的 WebClient 或 @ RegisteredOAuth2AuthorizedClient 支持来获取和使用访问令牌。
23.3. OAuth 2.0资源服务器
Spring Security支持使用两种OAuth 2.0 承载令牌 形式保护端点 :
-
不透明令牌
在应用程序将其权限管理委派给 授权服务器 (例如Okta或Ping Identity)的 情况下,这很方便 。 资源服务器可以咨询该授权服务器以授权请求。
|
Spring Security存储库中 提供 了 JWT的 完整工作示例 。 |
23.3.1. 依存关系
大多数资源服务器支持都收集到中
spring-security-oauth2-resource-server
。
但是,中支持对JWT进行解码和验证
spring-security-oauth2-jose
,这意味着这两者都是必需的,这样才能拥有支持JWT编码的承载令牌的工作资源服务器。
23.3.2. JWT的最小配置
使用 Spring Boot时 ,将应用程序配置为资源服务器包括两个基本步骤。 首先,包括所需的依赖关系,其次,指示授权服务器的位置。
指定授权服务器
在Spring Boot应用程序中,要指定要使用的授权服务器,只需执行以下操作:
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://idp.example.com/issuer
授权服务器将发出的JWT令牌声明中
https://idp.example.com/issuer
包含的值在
哪里
iss
。
资源服务器将使用此属性进行进一步的自我配置,发现授权服务器的公钥,然后验证传入的JWT。
要使用该
issuer-uri
属性,它也必须是真实的一个
https://idp.example.com/issuer/.well-known/openid-configuration
,
https://idp.example.com/.well-known/openid-configuration/issuer
或者
https://idp.example.com/.well-known/oauth-authorization-server/issuer
是授权服务器支持的端点。
此终结点称为
提供者配置
终结点或
授权服务器元数据
终结点。
|
就是这样!
启动期望
使用此属性和这些依赖关系时,资源服务器将自动配置自身以验证JWT编码的承载令牌。
它通过确定性的启动过程来实现:
-
点击提供者配置或授权服务器元数据终结点,处理该
jwks_url属性 的响应 -
配置验证策略以查询
jwks_url有效的公共密钥 -
配置验证策略以验证每个JWT的
iss主张https://idp.example.com。
此过程的结果是,授权服务器必须启动并接收请求,才能成功启动资源服务器。
| 如果在资源服务器查询授权服务器时授权服务器已关闭(给出适当的超时),则启动将失败。 |
运行时期望
应用程序启动后,资源服务器将尝试处理任何包含
Authorization: Bearer
标头的
请求
:
GET / HTTP/1.1
Authorization: Bearer some-token-value # Resource Server will process this
只要指示了此方案,资源服务器就会尝试根据Bearer Token规范处理请求。
给定格式正确的JWT,资源服务器将:
-
根据
jwks_url启动期间 从 端点 获取 并与JWT头匹配的 公钥来验证其签名 -
验证JWT
exp和nbf时间戳以及JWTiss声明,并且 -
将每个范围映射到具有前缀的授权
SCOPE_。
| 当授权服务器提供新的密钥时,Spring Security将自动旋转用于验证JWT令牌的密钥。 |
Authentication#getPrincipal
默认情况下,
生成的结果
是Spring Security
Jwt
对象,并且
如果存在的话
,
Authentication#getName
将映射到JWT的
sub
属性。
从这里,考虑跳到:
直接指定授权服务器JWK设置Uri
如果授权服务器不支持任何配置端点,或者如果Resource Server必须能够独立于授权服务器启动,则
jwk-set-uri
也可以提供:
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://idp.example.com
jwk-set-uri: https://idp.example.com/.well-known/jwks.json
| JWK Set uri不是标准化的,但是通常可以在授权服务器的文档中找到 |
因此,资源服务器在启动时不会对授权服务器执行ping操作。
我们仍然指定,
issuer-uri
以便Resource Server仍然验证
iss
传入JWT的声明。
| 此属性也可以直接在 DSL 上提供 。 |
覆盖或替换引导自动配置
@Bean
Spring Boot代表资源服务器生成
两个
。
第一个是
SecurityWebFilterChain
将应用程序配置为资源服务器的。
当包含时
spring-security-oauth2-jose
,它
WebSecurityConfigurerAdapter
看起来像:
@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http
.authorizeExchange(exchanges ->
exchanges
.anyExchange().authenticated()
)
.oauth2ResourceServer(OAuth2ResourceServerSpec::jwt)
return http.build();
}
如果应用程序没有公开
SecurityWebFilterChain
bean,那么Spring Boot将公开上述默认
的
bean。
替换它就像在应用程序中公开Bean一样简单:
@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http
.authorizeExchange(exchanges ->
exchanges
.pathMatchers("/message/**").hasAuthority("SCOPE_message:read")
.anyExchange().authenticated()
)
.oauth2ResourceServer(oauth2ResourceServer ->
oauth2ResourceServer
.jwt(withDefaults())
);
return http.build();
}
以上要求以
message:read
开头的所有URL
的范围
/messages/
。
oauth2ResourceServer
DSL
上的方法
还将覆盖或替换自动配置。
例如,第二个
@Bean
Spring Boot创建的是一个
ReactiveJwtDecoder
,它将
String
令牌
解码
为以下经过验证的实例
Jwt
:
@Bean
public ReactiveJwtDecoder jwtDecoder() {
return ReactiveJwtDecoders.fromIssuerLocation(issuerUri);
}
调用
ReactiveJwtDecoders#fromIssuerLocation
是调用提供程序配置或授权服务器元数据终结点以派生JWK设置Uri的功能。
如果应用程序没有公开
ReactiveJwtDecoder
bean,那么Spring Boot将公开上述默认
的
bean。
|
可以使用覆盖它的配置,也可以使用
jwkSetUri()
替换
它的配置
decoder()
。
使用
jwkSetUri()
授权服务器的JWK Set Uri可以配置 为配置属性 ,也可以在DSL中提供:
@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http
.authorizeExchange(exchanges ->
exchanges
.anyExchange().authenticated()
)
.oauth2ResourceServer(oauth2ResourceServer ->
oauth2ResourceServer
.jwt(jwt ->
jwt
.jwkSetUri("https://idp.example.com/.well-known/jwks.json")
)
);
return http.build();
}
使用
jwkSetUri()
优先于任何配置属性。
使用
decoder()
比
jwkSetUri()
现在
更强大的功能
decoder()
,它将完全替代以下的任何Boot自动配置
JwtDecoder
:
@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http
.authorizeExchange()
.anyExchange().authenticated()
.and()
.oauth2ResourceServer()
.jwt()
.decoder(myCustomDecoder());
return http.build();
}
当需要进行更深入的配置(例如 验证)时 , 这非常方便 。
23.3.3. 配置可信算法
默认情况下,
NimbusReactiveJwtDecoder
因此资源服务器将仅使用来信任和验证令牌
RS256
。
您可以通过 Spring Boot 或 NimbusJwtDecoder构建器 对此进行自定义 。
通过Spring Boot
设置算法的最简单方法是作为属性:
spring:
security:
oauth2:
resourceserver:
jwt:
jws-algorithm: RS512
jwk-set-uri: https://idp.example.org/.well-known/jwks.json
使用生成器
但是,为了获得更大的功能,我们可以使用附带以下内容的构建器
NimbusReactiveJwtDecoder
:
@Bean
ReactiveJwtDecoder jwtDecoder() {
return NimbusReactiveJwtDecoder.fromJwkSetUri(this.jwkSetUri)
.jwsAlgorithm(RS512).build();
}
jwsAlgorithm
多次
调用
将配置
NimbusReactiveJwtDecoder
为信任多个算法,如下所示:
@Bean
ReactiveJwtDecoder jwtDecoder() {
return NimbusReactiveJwtDecoder.fromJwkSetUri(this.jwkSetUri)
.jwsAlgorithm(RS512).jwsAlgorithm(EC512).build();
}
或者,您可以致电
jwsAlgorithms
:
@Bean
ReactiveJwtDecoder jwtDecoder() {
return NimbusReactiveJwtDecoder.fromJwkSetUri(this.jwkSetUri)
.jwsAlgorithms(algorithms -> {
algorithms.add(RS512);
algorithms.add(EC512);
}).build();
}
信任单个非对称密钥
比使用JWK Set端点备份资源服务器更简单的方法是对RSA公钥进行硬编码。 可以通过 Spring Boot 或 使用Builder 提供公共密钥 。
通过Spring Boot
通过Spring Boot指定密钥非常简单。 密钥的位置可以这样指定:
spring:
security:
oauth2:
resourceserver:
jwt:
public-key-location: classpath:my-key.pub
或者,为了进行更复杂的查找,您可以对进行后处理
RsaKeyConversionServicePostProcessor
:
@Bean
BeanFactoryPostProcessor conversionServiceCustomizer() {
return beanFactory ->
beanFactory.getBean(RsaKeyConversionServicePostProcessor.class)
.setResourceLoader(new CustomResourceLoader());
}
指定密钥的位置:
key.location: hfds://my-key.pub
然后自动装配值:
@Value("${key.location}")
RSAPublicKey key;
信任单个对称密钥
使用单个对称密钥也很简单。
您可以简单地加载
SecretKey
并使用适当的
NimbusReactiveJwtDecoder
构建器,如下所示:
@Bean
public ReactiveJwtDecoder jwtDecoder() {
return NimbusReactiveJwtDecoder.withSecretKey(this.key).build();
}
配置授权
从OAuth 2.0授权服务器发出的JWT通常具有
scope
或
scp
属性,指示已被授予的范围(或权限),例如:
{ …, "scope" : "messages contacts"}
在这种情况下,资源服务器将尝试将这些作用域强制为已授予权限的列表,并为每个作用域添加字符串“ SCOPE_”作为前缀。
这意味着为了保护具有从JWT派生的作用域的端点或方法,相应的表达式应包含以下前缀:
@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http
.authorizeExchange(exchanges ->exchanges
.mvcMatchers("/contacts/**").hasAuthority("SCOPE_contacts")
.mvcMatchers("/messages/**").hasAuthority("SCOPE_messages")
.anyExchange().authenticated()
)
.oauth2ResourceServer(OAuth2ResourceServerSpec::jwt);
return http.build();
}
或类似地具有方法安全性:
@PreAuthorize("hasAuthority('SCOPE_messages')")
public Flux<Message> getMessages(...) {}
手动提取权限
但是,在许多情况下,此默认设置不足。
例如,某些授权服务器不使用该
scope
属性,而是拥有自己的自定义属性。
或者,在其他时候,资源服务器可能需要将属性或属性组成调整为内部化的权限。
为此,DSL公开
jwtAuthenticationConverter()
:
@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http
.authorizeExchange()
.anyExchange().authenticated()
.and()
.oauth2ResourceServer()
.jwt()
.jwtAuthenticationConverter(grantedAuthoritiesExtractor());
return http.build();
}
Converter<Jwt, Mono<AbstractAuthenticationToken>> grantedAuthoritiesExtractor() {
JwtAuthenticationConverter jwtAuthenticationConverter =
new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter
(new GrantedAuthoritiesExtractor());
return new ReactiveJwtAuthenticationConverterAdapter(jwtAuthenticationConverter);
}
负责将a
Jwt
转换为
Authentication
。
作为其配置的一部分,我们可以提供一个附属转换器从去
Jwt
到
Collection
授予的权限的。
最终的转换器可能
GrantedAuthoritiesExtractor
如下所示:
static class GrantedAuthoritiesExtractor
implements Converter<Jwt, Collection<GrantedAuthority>> {
public Collection<GrantedAuthority> convert(Jwt jwt) {
Collection<String> authorities = (Collection<String>)
jwt.getClaims().get("mycustomclaim");
return authorities.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
}
}
为了获得更大的灵活性,DSL支持使用以下实现的任何类完全替代转换器
Converter<Jwt, Mono<AbstractAuthenticationToken>>
:
static class CustomAuthenticationConverter implements Converter<Jwt, Mono<AbstractAuthenticationToken>> {
public AbstractAuthenticationToken convert(Jwt jwt) {
return Mono.just(jwt).map(this::doConversion);
}
}
配置验证
用
最少的春天启动配置
,指示授权服务器的发布者URI,资源服务器将默认为验证
iss
要求,以及在
exp
和
nbf
时间戳索赔。
在需要自定义验证的情况下,资源服务器附带两个标准验证器,并且也接受自定义
OAuth2TokenValidator
实例。
自定义时间戳验证
JWT通常具有有效期窗口,该窗口的开始在
nbf
权利要求中
指示,
而结束在
exp
权利要求中
指示
。
但是,每台服务器都会经历时钟漂移,这可能导致令牌在一个服务器上显得过期,而在另一台服务器上过期。 随着分布式系统中协作服务器数量的增加,这可能会导致某些实现上的胃口。
资源服务器用于
JwtTimestampValidator
验证令牌的有效性窗口,并且可以将其配置为
clockSkew
来缓解上述问题:
@Bean
ReactiveJwtDecoder jwtDecoder() {
NimbusReactiveJwtDecoder jwtDecoder = (NimbusReactiveJwtDecoder)
ReactiveJwtDecoders.fromIssuerLocation(issuerUri);
OAuth2TokenValidator<Jwt> withClockSkew = new DelegatingOAuth2TokenValidator<>(
new JwtTimestampValidator(Duration.ofSeconds(60)),
new IssuerValidator(issuerUri));
jwtDecoder.setJwtValidator(withClockSkew);
return jwtDecoder;
}
| 默认情况下,资源服务器将时钟偏差配置为30秒。 |
配置自定义验证器
aud
使用
OAuth2TokenValidator
API
为
索赔
添加支票
很简单
:
public class AudienceValidator implements OAuth2TokenValidator<Jwt> {
OAuth2Error error = new OAuth2Error("invalid_token", "The required audience is missing", null);
public OAuth2TokenValidatorResult validate(Jwt jwt) {
if (jwt.getAudience().contains("messaging")) {
return OAuth2TokenValidatorResult.success();
} else {
return OAuth2TokenValidatorResult.failure(error);
}
}
}
然后,要添加到资源服务器中,只需指定
ReactiveJwtDecoder
实例即可:
@Bean
ReactiveJwtDecoder jwtDecoder() {
NimbusReactiveJwtDecoder jwtDecoder = (NimbusReactiveJwtDecoder)
ReactiveJwtDecoders.fromIssuerLocation(issuerUri);
OAuth2TokenValidator<Jwt> audienceValidator = new AudienceValidator();
OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer(issuerUri);
OAuth2TokenValidator<Jwt> withAudience = new DelegatingOAuth2TokenValidator<>(withIssuer, audienceValidator);
jwtDecoder.setJwtValidator(withAudience);
return jwtDecoder;
}
内省的最小配置
通常,不透明令牌可以通过 授权服务器托管 的 OAuth 2.0自省端点 进行验证 。 当需要撤销时,这可能很方便。
使用 Spring Boot时 ,将应用程序配置为使用自省的资源服务器包括两个基本步骤。 首先,包括所需的依赖性,其次,指示自省端点详细信息。
指定授权服务器
要指定自省端点的位置,只需执行以下操作:
security:
oauth2:
resourceserver:
opaque-token:
introspection-uri: https://idp.example.com/introspect
client-id: client
client-secret: secret
https://idp.example.com/introspect
授权服务器托管的自省端点
在哪里
,
client-id
并且
client-secret
是到达该端点所需的凭据。
资源服务器将使用这些属性进一步进行自我配置,并随后验证传入的JWT。
| 使用自省时,授权服务器的字眼就是法律。 如果授权服务器响应令牌是有效的,那么令牌是有效的。 |
就是这样!
运行时期望
应用程序启动后,资源服务器将尝试处理任何包含
Authorization: Bearer
标头的
请求
:
GET / HTTP/1.1
Authorization: Bearer some-token-value # Resource Server will process this
只要指示了此方案,资源服务器就会尝试根据Bearer Token规范处理请求。
给定一个不透明的令牌,资源服务器将
-
使用提供的凭据和令牌查询提供的自省端点
-
检查响应的
{ 'active' : true }属性 -
将每个范围映射到具有前缀的授权
SCOPE_
Authentication#getPrincipal
默认情况下,
生成的结果
是Spring Security
OAuth2AuthenticatedPrincipal
对象,并且
如果存在
Authentication#getName
令牌
sub
,则
映射到令牌的
属性。
从这里,您可能要跳转到:
查找身份验证后的属性
令牌通过身份验证后,将在
BearerTokenAuthentication
中设置
的实例
SecurityContext
。
这意味着
在您的配置中
@Controller
使用
@EnableWebFlux
该
方法时
,该
方法
可用
:
@GetMapping("/foo")
public Mono<String> foo(BearerTokenAuthentication authentication) {
return Mono.just(authentication.getTokenAttributes().get("sub") + " is the subject");
}
由于
BearerTokenAuthentication
拥有
OAuth2AuthenticatedPrincipal
,这也意味着它也可用于控制器方法:
@GetMapping("/foo")
public Mono<String> foo(@AuthenticationPrincipal OAuth2AuthenticatedPrincipal principal) {
return Mono.just(principal.getAttribute("sub") + " is the subject");
}
覆盖或替换引导自动配置
@Bean
Spring Boot代表资源服务器生成
两个
。
第一个是
SecurityWebFilterChain
将应用程序配置为资源服务器的。
使用不透明令牌时,如下
SecurityWebFilterChain
所示:
@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http
.authorizeExchange()
.anyExchange().authenticated()
.and()
.oauth2ResourceServer(ServerHttpSecurity.OAuth2ResourceServerSpec::opaqueToken)
return http.build();
}
如果应用程序没有公开
SecurityWebFilterChain
bean,那么Spring Boot将公开上述默认
的
bean。
替换它就像在应用程序中公开Bean一样简单:
@EnableWebFluxSecurity
public class MyCustomSecurityConfiguration {
@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http
.authorizeExchange()
.pathMatchers("/messages/**").hasAuthority("SCOPE_message:read")
.anyExchange().authenticated()
.and()
.oauth2ResourceServer()
.opaqueToken()
.introspector(myIntrospector());
return http.build();
}
}
以上要求以
message:read
开头的所有URL
的范围
/messages/
。
oauth2ResourceServer
DSL
上的方法
还将覆盖或替换自动配置。
例如,第二个
@Bean
Spring Boot创建的是一个
ReactiveOpaqueTokenIntrospector
,它将
String
令牌
解码
为以下经过验证的实例
OAuth2AuthenticatedPrincipal
:
@Bean
public ReactiveOpaqueTokenIntrospector introspector() {
return new NimbusReactiveOpaqueTokenIntrospector(introspectionUri, clientId, clientSecret);
}
如果应用程序没有公开
ReactiveOpaqueTokenIntrospector
bean,那么Spring Boot将公开上述默认
的
bean。
可以使用
introspectionUri()
和
覆盖其配置,也可以使用
introspectionClientCredentials()
替换
它的配置
introspector()
。
使用
introspectionUri()
授权服务器的Introspection Uri可以配置 为配置属性 ,也可以在DSL中提供:
@EnableWebFluxSecurity
public class DirectlyConfiguredIntrospectionUri {
@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http
.authorizeExchange()
.anyExchange().authenticated()
.and()
.oauth2ResourceServer()
.opaqueToken()
.introspectionUri("https://idp.example.com/introspect")
.introspectionClientCredentials("client", "secret");
return http.build();
}
}
使用
introspectionUri()
优先于任何配置属性。
使用
introspector()
比
introspectionUri()
现在
更强大的功能
introspector()
,它将完全替代以下的任何Boot自动配置
ReactiveOpaqueTokenIntrospector
:
@EnableWebFluxSecurity
public class DirectlyConfiguredIntrospector {
@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http
.authorizeExchange()
.anyExchange().authenticated()
.and()
.oauth2ResourceServer()
.opaqueToken()
.introspector(myCustomIntrospector());
return http.build();
}
}
配置授权
OAuth 2.0自省端点通常会返回一个
scope
属性,指示该属性已被授予的范围(或权限),例如:
{ …, "scope" : "messages contacts"}
在这种情况下,资源服务器将尝试将这些作用域强制为已授予权限的列表,并为每个作用域添加字符串“ SCOPE_”作为前缀。
这意味着要保护具有不透明令牌派生范围的端点或方法,相应的表达式应包含以下前缀:
@EnableWebFluxSecurity
public class MappedAuthorities {
@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http
.authorizeExchange(exchange -> exchange
.pathMatchers("/contacts/**").hasAuthority("SCOPE_contacts")
.pathMatchers("/messages/**").hasAuthority("SCOPE_messages")
.anyExchange().authenticated()
)
.oauth2ResourceServer(ServerHttpSecurity.OAuth2ResourceServerSpec::opaqueToken);
return http.build();
}
}
或类似地具有方法安全性:
@PreAuthorize("hasAuthority('SCOPE_messages')")
public Flux<Message> getMessages(...) {}
手动提取权限
默认情况下,不透明令牌支持将从自省响应中提取范围声明,并将其解析为单个
GrantedAuthority
实例。
例如,如果自省响应为:
{
"active" : true,
"scope" : "message:read message:write"
}
然后,资源服务器将生成
Authentication
具有两个权限的,一个用于
message:read
,另一个用于
message:write
。
当然,这可以使用
ReactiveOpaqueTokenIntrospector
查看属性集并以自己的方式进行转换
的自定义项进行自定义
:
public class CustomAuthoritiesOpaqueTokenIntrospector implements ReactiveOpaqueTokenIntrospector {
private ReactiveOpaqueTokenIntrospector delegate =
new NimbusReactiveOpaqueTokenIntrospector("https://idp.example.org/introspect", "client", "secret");
public Mono<OAuth2AuthenticatedPrincipal> introspect(String token) {
return this.delegate.introspect(token)
.map(principal -> new DefaultOAuth2AuthenticatedPrincipal(
principal.getName(), principal.getAttributes(), extractAuthorities(principal)));
}
private Collection<GrantedAuthority> extractAuthorities(OAuth2AuthenticatedPrincipal principal) {
List<String> scopes = principal.getAttribute(OAuth2IntrospectionClaimNames.SCOPE);
return scopes.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
}
}
此后,可以通过将其公开为来简单地配置此自定义内省器
@Bean
:
@Bean
public ReactiveOpaqueTokenIntrospector introspector() {
return new CustomAuthoritiesOpaqueTokenIntrospector();
}
对JWT使用自省
一个常见的问题是内省是否与JWT兼容。 Spring Security的Opaque令牌支持被设计为不关心令牌的格式-它将很高兴将任何令牌传递给提供的自省端点。
因此,假设您有一个要求,如果JWT被吊销,则要求您在每个请求中与授权服务器进行核对。
即使您为令牌使用JWT格式,您的验证方法也是自省的,这意味着您想要执行以下操作:
spring:
security:
oauth2:
resourceserver:
opaque-token:
introspection-uri: https://idp.example.org/introspection
client-id: client
client-secret: secret
在这种情况下,结果
Authentication
将是
BearerTokenAuthentication
。
对应项中的任何属性
OAuth2AuthenticatedPrincipal
将是自省端点返回的
任何属性
。
但是,可以说,奇怪的是,自省端点仅返回令牌是否处于活动状态。 怎么办?
在这种情况下,您可以创建一个
ReactiveOpaqueTokenIntrospector
仍会命中端点
的自定义
,然后更新返回的主体以将JWT声明作为属性:
public class JwtOpaqueTokenIntrospector implements ReactiveOpaqueTokenIntrospector {
private ReactiveOpaqueTokenIntrospector delegate =
new NimbusReactiveOpaqueTokenIntrospector("https://idp.example.org/introspect", "client", "secret");
private ReactiveJwtDecoder jwtDecoder = new NimbusReactiveJwtDecoder(new ParseOnlyJWTProcessor());
public Mono<OAuth2AuthenticatedPrincipal> introspect(String token) {
return this.delegate.introspect(token)
.flatMap(principal -> this.jwtDecoder.decode(token))
.map(jwt -> new DefaultOAuth2AuthenticatedPrincipal(jwt.getClaims(), NO_AUTHORITIES));
}
private static class ParseOnlyJWTProcessor implements Converter<JWT, Mono<JWTClaimsSet>> {
public Mono<JWTClaimsSet> convert(JWT jwt) {
try {
return Mono.just(jwt.getJWTClaimsSet());
} catch (Exception e) {
return Mono.error(e);
}
}
}
}
此后,可以通过将其公开为来简单地配置此自定义内省器
@Bean
:
@Bean
public ReactiveOpaqueTokenIntrospector introspector() {
return new JwtOpaqueTokenIntropsector();
}
呼叫
/userinfo
端点
一般而言,资源服务器并不关心基础用户,而是关心已授予的权限。
就是说,有时将授权声明绑定到用户可能很有价值。
如果一个应用程序也正在使用
spring-security-oauth2-client
,并且已经设置了适当的设置
ClientRegistrationRepository
,那么使用定制就很简单了
OpaqueTokenIntrospector
。
下面的实现实现了三件事:
-
委托自省端点确认令牌的有效性
-
查找与
/userinfo端点 关联的适当的客户端注册 -
调用并返回
/userinfo端点 的响应
public class UserInfoOpaqueTokenIntrospector implements ReactiveOpaqueTokenIntrospector {
private final ReactiveOpaqueTokenIntrospector delegate =
new NimbusReactiveOpaqueTokenIntrospector("https://idp.example.org/introspect", "client", "secret");
private final ReactiveOAuth2UserService<OAuth2UserRequest, OAuth2User> oauth2UserService =
new DefaultReactiveOAuth2UserService();
private final ReactiveClientRegistrationRepository repository;
// ... constructor
@Override
public Mono<OAuth2AuthenticatedPrincipal> introspect(String token) {
return Mono.zip(this.delegate.introspect(token), this.repository.findByRegistrationId("registration-id"))
.map(t -> {
OAuth2AuthenticatedPrincipal authorized = t.getT1();
ClientRegistration clientRegistration = t.getT2();
Instant issuedAt = authorized.getAttribute(ISSUED_AT);
Instant expiresAt = authorized.getAttribute(OAuth2IntrospectionClaimNames.EXPIRES_AT);
OAuth2AccessToken accessToken = new OAuth2AccessToken(BEARER, token, issuedAt, expiresAt);
return new OAuth2UserRequest(clientRegistration, accessToken);
})
.flatMap(this.oauth2UserService::loadUser);
}
}
如果您不使用
spring-security-oauth2-client
,它仍然非常简单。
您只需要
/userinfo
使用您自己的实例
调用即可
WebClient
:
public class UserInfoOpaqueTokenIntrospector implements ReactiveOpaqueTokenIntrospector {
private final ReactiveOpaqueTokenIntrospector delegate =
new NimbusReactiveOpaqueTokenIntrospector("https://idp.example.org/introspect", "client", "secret");
private final WebClient rest = WebClient.create();
@Override
public Mono<OAuth2AuthenticatedPrincipal> introspect(String token) {
return this.delegate.introspect(token)
.map(this::makeUserInfoRequest);
}
}
无论哪种方式,创建了之后
ReactiveOpaqueTokenIntrospector
,您都应该将其发布为,
@Bean
以覆盖默认值:
@Bean
ReactiveOpaqueTokenIntrospector introspector() {
return new UserInfoOpaqueTokenIntrospector(...);
}
23.3.4. 承载令牌传播
现在您已经拥有了一个承载令牌,将它传递给下游服务可能会很方便。
使用
ServerBearerExchangeFilterFunction
,这
非常简单
,您可以在以下示例中看到它:
@Bean
public WebClient rest() {
return WebClient.builder()
.filter(new ServerBearerExchangeFilterFunction())
.build();
}
当上述
WebClient
方法用于执行请求时,Spring Security将查找当前信息
Authentication
并提取所有
AbstractOAuth2Token
凭证。
然后,它将在
Authorization
标头中
传播该令牌
。
例如:
this.rest.get()
.uri("https://other-service.example.com/endpoint")
.retrieve()
.bodyToMono(String.class)
将调用
https://other-service.example.com/endpoint
,
Authorization
为您
添加承载令牌
标头。
在需要覆盖此行为的地方,只需提供一个标题就很简单了,就像这样:
this.rest.get()
.uri("https://other-service.example.com/endpoint")
.headers(headers -> headers.setBearerAuth(overridingToken))
.retrieve()
.bodyToMono(String.class)
在这种情况下,过滤器将回退并将请求转发到Web过滤器链的其余部分。
| 与 OAuth 2.0客户端过滤器功能不同 ,该过滤器功能不会在令牌过期时尝试更新令牌。 要获得此级别的支持,请使用OAuth 2.0客户端过滤器。 |
24. @ RegisteredOAuth2AuthorizedClient
Spring Security允许使用解析访问令牌
@RegisteredOAuth2AuthorizedClient
。
|
在 OAuth 2.0 WebClient WebFlux示例中 可以找到一个有效的示例 。 |
将Spring Security配置为
OAuth2登录名
或作为
OAuth2客户端后
,
OAuth2AuthorizedClient
可以使用以下方法解决:
@GetMapping("/explicit")
Mono<String> explicit(@RegisteredOAuth2AuthorizedClient("client-id") OAuth2AuthorizedClient authorizedClient) {
// ...
}
它集成到Spring Security中以提供以下功能:
-
Spring Security将自动刷新过期的令牌(如果存在刷新令牌)
-
如果请求访问令牌但不存在,则Spring Security将自动请求访问令牌。
-
为此,
authorization_code需要执行重定向,然后重播原始请求 -
因为
client_credentials令牌只是被请求并保存
-
如果用户使用验证了身份
oauth2Login()
,则
client-id
是可选的。
例如,以下将起作用:
@GetMapping("/implicit")
Mono<String> implicit(@RegisteredOAuth2AuthorizedClient OAuth2AuthorizedClient authorizedClient) {
// ...
}
如果用户始终使用OAuth2登录进行身份验证,并且需要来自同一授权服务器的访问令牌,这将非常方便。
25.响应式X.509身份验证
如同 Servlet X.509身份验证 ,反应性x509身份验证过滤器允许从客户端提供的证书中提取身份验证令牌。
以下是反应式x509安全配置的示例:
@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
http
.x509(withDefaults())
.authorizeExchange(exchanges ->
exchanges
.anyExchange().permitAll()
);
return http.build();
}
在上述配置中,如果
principalExtractor
未
authenticationManager
提供或未提供,将使用默认值。
默认的主体提取器
SubjectDnX509PrincipalExtractor
从客户端提供的证书中提取CN(通用名称)字段。
默认的身份验证管理器
ReactivePreAuthenticatedAuthenticationManager
将执行用户帐户验证,检查名称由by提取的用户帐户是否
principalExtractor
存在,并且该
帐户
未锁定,禁用或过期。
下一个示例演示如何覆盖这些默认值。
@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
SubjectDnX509PrincipalExtractor principalExtractor =
new SubjectDnX509PrincipalExtractor();
principalExtractor.setSubjectDnRegex("OU=(.*?)(?:,|$)");
ReactiveAuthenticationManager authenticationManager = authentication -> {
authentication.setAuthenticated("Trusted Org Unit".equals(authentication.getName()));
return Mono.just(authentication);
};
http
.x509(x509 ->
x509
.principalExtractor(principalExtractor)
.authenticationManager(authenticationManager)
)
.authorizeExchange(exchanges ->
exchanges
.anyExchange().authenticated()
);
return http.build();
}
在此示例中,从客户端证书的OU字段而不是CN中提取用户名,并使用
ReactiveUserDetailsService
根本不
进行
。
相反,如果提供的证书颁发给名为“受信任的组织单位”的OU,则将对请求进行身份验证。
有关配置Netty和/
WebClient
或
curl
命令行工具以使用双向TLS并启用X.509身份验证
的示例
,请参阅
https://github.com/spring-projects/spring-security/tree/master/samples/boot/ webflux-x509
。
26. WebClient
Spring Framework内置了对设置Bearer令牌的支持。
webClient.get()
.headers(h -> h.setBearerAuth(token))
...
Spring Security在此支持的基础上提供了更多好处:
-
Spring Security将自动刷新过期的令牌(如果存在刷新令牌)
-
如果请求访问令牌但不存在,则Spring Security将自动请求访问令牌。
-
对于authorization_code,这涉及执行重定向,然后重播原始请求
-
对于client_credentials,只需请求并保存令牌
-
-
支持透明地包含当前OAuth令牌或显式选择应使用的令牌的功能。
26.1. WebClient OAuth2设置
第一步是确保
WebClient
正确
设置
。
WebClient
在完全反应的环境中进行
设置的示例
如下:
@Bean
WebClient webClient(ReactiveClientRegistrationRepository clientRegistrations,
ServerOAuth2AuthorizedClientRepository authorizedClients) {
ServerOAuth2AuthorizedClientExchangeFilterFunction oauth =
new ServerOAuth2AuthorizedClientExchangeFilterFunction(clientRegistrations, authorizedClients);
// (optional) explicitly opt into using the oauth2Login to provide an access token implicitly
// oauth.setDefaultOAuth2AuthorizedClient(true);
// (optional) set a default ClientRegistration.registrationId
// oauth.setDefaultClientRegistrationId("client-registration-id");
return WebClient.builder()
.filter(oauth)
.build();
}
26.2. 隐式OAuth2AuthorizedClient
如果我们
在设置中
设置
defaultOAuth2AuthorizedClient
为
true
,并且用户使用oauth2Login(即OIDC)进行了身份验证,那么当前身份验证将用于自动提供访问令牌。
或者,如果我们将其设置
defaultClientRegistrationId
为有效的
ClientRegistration
ID,则该注册将用于提供访问令牌。
这很方便,但是在并非所有端点都应获取访问令牌的环境中,这很危险(您可能为端点提供了错误的访问令牌)。
Mono<String> body = this.webClient
.get()
.uri(this.uri)
.retrieve()
.bodyToMono(String.class);
26.3. 显式OAuth2AuthorizedClient
该
OAuth2AuthorizedClient
可以通过设置它的请求属性进行明确规定。
在下面的示例中,我们解决了
OAuth2AuthorizedClient
使用Spring WebFlux或Spring MVC参数解析器支持的问题。
但是,
OAuth2AuthorizedClient
解决方法
并不重要
。
@GetMapping("/explicit")
Mono<String> explicit(@RegisteredOAuth2AuthorizedClient("client-id") OAuth2AuthorizedClient authorizedClient) {
return this.webClient
.get()
.uri(this.uri)
.attributes(oauth2AuthorizedClient(authorizedClient))
.retrieve()
.bodyToMono(String.class);
}
27. EnableReactiveMethodSecurity
Spring Security使用
Reactor的Context
支持方法的安全性
,该
上下文
使用设置
ReactiveSecurityContextHolder
。
例如,这演示了如何检索当前登录用户的消息。
|
为此,方法的返回类型必须为
|
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
。
但是,目前我们仅支持
Boolean
或的
返回类型
boolean
表达式
。
这意味着该表达式不能阻塞。
与 WebFlux Security 集成时 ,Spring Security会根据已认证的用户自动建立Reactor上下文。
@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(exchanges ->
exchanges
.anyExchange().permitAll()
)
.httpBasic(withDefaults())
.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);
}
}
您可以在 hellowebflux方法中 找到完整的示例
28.反应式测试支持
28.1. 测试反应性方法的安全性
例如,我们可以 使用与“ 测试方法安全性 ”中相同的设置和注释, 从 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();
}
}
28.2. 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();
}
// ...
}
28.2.1. 认证方式
在将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!");
}
28.2.2. CSRF支持
Spring Security还提供了对CSRF测试的支持
WebTestClient
。
例如:
this.rest
// provide a valid CSRF token
.mutateWith(csrf())
.post()
.uri("/login")
...
28.2.3. 测试承载身份验证
为了在资源服务器上发出授权请求,您需要一个承载令牌。 如果为JWT配置了资源服务器,则这意味着需要对承载令牌进行签名,然后根据JWT规范进行编码。 所有这些都可能令人望而生畏,特别是当这不是您测试的重点时。
幸运的是,您可以通过多种简单的方法来克服此困难,并使您的测试专注于授权而不是表示承载令牌。 我们现在来看其中两个:
mockJwt() WebTestClientConfigurer
第一种方法是通过
WebTestClientConfigurer
。
其中最简单的如下所示:
client
.mutateWith(mockJwt()).get().uri("/endpoint").exchange();
要做的是创建一个模拟程序
Jwt
,将其正确地通过任何身份验证API传递,以便您的授权机制可以对其进行验证。
默认情况下,
JWT
它创建的具有以下特征:
{
"headers" : { "alg" : "none" },
"claims" : {
"sub" : "user",
"scope" : "read"
}
}
并将得到的
Jwt
,是测试它,会通过下列方式:
assertThat(jwt.getTokenValue()).isEqualTo("token");
assertThat(jwt.getHeaders().get("alg")).isEqualTo("none");
assertThat(jwt.getSubject()).isEqualTo("sub");
GrantedAuthority authority = jwt.getAuthorities().iterator().next();
assertThat(authority.getAuthority()).isEqualTo("read");
当然可以配置这些值。
可以使用其相应方法配置任何标题或声明:
client
.mutateWith(jwt(jwt -> jwt.header("kid", "one")
.claim("iss", "https://idp.example.org")))
.get().uri("/endpoint").exchange();
client
.mutateWith(jwt(jwt -> jwt.claims(claims -> claims.remove("scope"))))
.get().uri("/endpoint").exchange();
的
scope
和
scp
权利要求被处理的相同的方式在这里,因为它们是在一个正常的承载令牌请求。
但是,可以通过提供
GrantedAuthority
测试所需
的
实例
列表来覆盖
它:
client
.mutateWith(jwt().authorities(new SimpleGrantedAuthority("SCOPE_messages")))
.get().uri("/endpoint").exchange();
或者,如果您有
Jwt
要
Collection<GrantedAuthority>
转换
的自定义
,也可以使用它来导出权限:
client
.mutateWith(jwt().authorities(new MyConverter()))
.get().uri("/endpoint").exchange();
您还可以指定一个complete
Jwt
,对于它
Jwt.Builder
来说非常方便:
Jwt jwt = Jwt.withTokenValue("token")
.header("alg", "none")
.claim("sub", "user")
.claim("scope", "read");
client
.mutateWith(jwt(jwt))
.get().uri("/endpoint").exchange();
authentication() WebTestClientConfigurer
第二种方法是使用
authentication() Mutator
。
本质上,您可以实例化自己的实例
JwtAuthenticationToken
并在测试中提供它,如下所示:
Jwt jwt = Jwt.withTokenValue("token")
.header("alg", "none")
.claim("sub", "user")
.build();
Collection<GrantedAuthority> authorities = AuthorityUtils.createAuthorityList("SCOPE_read");
JwtAuthenticationToken token = new JwtAuthenticationToken(jwt, authorities);
client
.mutateWith(authentication(token))
.get().uri("/endpoint").exchange();
请注意,作为替代方法,您还可以
ReactiveJwtDecoder
使用
@MockBean
注释
来模拟
bean本身
。
29. RSocket安全
Spring Security的RSocket支持依赖
SocketAcceptorInterceptor
。
在中找到了安全性的主要入口点
PayloadSocketAcceptorInterceptor
,该方法使RSocket API适应了允许拦截
PayloadExchange
with
PayloadInterceptor
实现的情况。
您可以找到一些示例程序来演示以下代码:
-
你好RSocket hellorsocket
29.1. 最小的RSocket安全配置
您可以在下面找到最小的RSocket安全配置:
@Configuration
@EnableRSocketSecurity
public class HelloRSocketSecurityConfig {
@Bean
public MapReactiveUserDetailsService userDetailsService() {
UserDetails user = User.withDefaultPasswordEncoder()
.username("user")
.password("user")
.roles("USER")
.build();
return new MapReactiveUserDetailsService(user);
}
}
29.2. 添加SecuritySocketAcceptorInterceptor
对于Spring Security工作,我们需要应用
SecuritySocketAcceptorInterceptor
到
ServerRSocketFactory
。
这就是我们将
PayloadSocketAcceptorInterceptor
我们创建的RSocket基础结构
连接起来的原因
。
在Spring Boot应用程序中,可以使用以下代码完成此操作。
@Bean
ServerRSocketFactoryCustomizer springSecurityRSocketSecurity(
SecuritySocketAcceptorInterceptor interceptor) {
return builder -> builder.addSocketAcceptorPlugin(interceptor);
}
29.3. RSocket认证
执行RSocket身份验证,将
AuthenticationPayloadInterceptor
其用作调用
ReactiveAuthenticationManager
实例
的控制器
。
29.3.1. 设置时的身份验证与请求时间
通常,认证可以在建立时间和/或请求时间进行。
在某些情况下,设置时进行身份验证是有意义的。 常见的情况是单个用户(即移动连接)利用RSocket连接时。 在这种情况下,只有一个用户可以利用该连接,因此可以在连接时进行一次身份验证。
在共享RSocket连接的情况下,有必要在每个请求上发送凭据。 例如,作为下游服务连接到RSocket服务器的Web应用程序将建立所有用户利用的单个连接。 在这种情况下,如果RSocket服务器需要根据Web应用程序的用户凭据执行授权,则每个请求都是有意义的。
在某些情况下,设置和按请求进行身份验证是有意义的。
考虑一个如上所述的Web应用程序。
如果我们需要限制与Web应用程序本身的连接,则可以
SETUP
在连接时
提供具有
权限
的凭据
。
这样,每个用户将具有不同的权限,但没有
SETUP
权限。
这意味着单个用户可以发出请求,但不能建立其他连接。
29.3.2. 基本认证
Spring Security对 RSocket的基本身份验证元数据扩展 提供了早期支持 。
RSocket接收器可以对凭据进行解码,使用凭据可以通过
DSL
BasicAuthenticationPayloadExchangeConverter
的
basicAuthentication
一部分
自动设置
凭据
。
可以在下面找到显式配置。
@Bean
PayloadSocketAcceptorInterceptor rsocketInterceptor(RSocketSecurity rsocket) {
rsocket
.authorizePayload(authorize ->
authorize
.anyRequest().authenticated()
.anyExchange().permitAll()
)
.basicAuthentication(Customizer.withDefaults());
return rsocket.build();
}
RSocket发送者可以发送凭证,使用凭证
BasicAuthenticationEncoder
可以添加到Spring的
凭证中
RSocketStrategies
。
RSocketStrategies.Builder strategies = ...;
strategies.encoder(new BasicAuthenticationEncoder());
然后可以使用它在设置中向接收方发送用户名和密码:
UsernamePasswordMetadata credentials = new UsernamePasswordMetadata("user", "password");
Mono<RSocketRequester> requester = RSocketRequester.builder()
.setupMetadata(credentials, UsernamePasswordMetadata.BASIC_AUTHENTICATION_MIME_TYPE)
.rsocketStrategies(strategies.build())
.connectTcp(host, port);
替代地或附加地,可以在请求中发送用户名和密码。
Mono<RSocketRequester> requester;
UsernamePasswordMetadata credentials = new UsernamePasswordMetadata("user", "password");
public Mono<AirportLocation> findRadar(String code) {
return this.requester.flatMap(req ->
req.route("find.radar.{code}", code)
.metadata(credentials, UsernamePasswordMetadata.BASIC_AUTHENTICATION_MIME_TYPE)
.retrieveMono(AirportLocation.class)
);
}
29.3.3. 智威汤逊
Spring Security对 RSocket的承载令牌认证元数据扩展 提供了早期支持 。 支持以对JWT进行身份验证(确定JWT有效)的形式出现,然后使用JWT做出授权决策。
RSocket接收器可以对凭据进行解码,使用凭据可以通过
DSL
BearerPayloadExchangeConverter
的
jwt
一部分
自动设置
凭据
。
可以在下面找到示例配置:
@Bean
PayloadSocketAcceptorInterceptor rsocketInterceptor(RSocketSecurity rsocket) {
rsocket
.authorizePayload(authorize ->
authorize
.anyRequest().authenticated()
.anyExchange().permitAll()
)
.jwt(Customizer.withDefaults());
return rsocket.build();
}
上面的配置依赖于存在的
ReactiveJwtDecoder @Bean
存在。
在发行人处创建一个示例的示例如下:
@Bean
ReactiveJwtDecoder jwtDecoder() {
return ReactiveJwtDecoders
.fromIssuerLocation("https://example.com/auth/realms/demo");
}
RSocket发送方不需要执行任何特殊操作即可发送令牌,因为该值只是一个简单的String。 例如,可以在设置时发送令牌:
String token = ...;
Mono<RSocketRequester> requester = RSocketRequester.builder()
.setupMetadata(token, BearerTokenMetadata.BEARER_AUTHENTICATION_MIME_TYPE)
.connectTcp(host, port);
替代地或附加地,可以在请求中发送令牌。
Mono<RSocketRequester> requester;
String token = ...;
public Mono<AirportLocation> findRadar(String code) {
return this.requester.flatMap(req ->
req.route("find.radar.{code}", code)
.metadata(token, BearerTokenMetadata.BEARER_AUTHENTICATION_MIME_TYPE)
.retrieveMono(AirportLocation.class)
);
}
29.4. RSocket授权
执行RSocket授权,
AuthorizationPayloadInterceptor
它充当调用
ReactiveAuthorizationManager
实例
的控制器
。
DSL可用于基于来建立授权规则
PayloadExchange
。
可以在下面找到示例配置:
o
.authorizePayload(授权->
身份验证
.setup()。hasRole(“ SETUP”) (1)
.route(“ fetch.profile.me”)。authenticated() (2)
.matcher(payloadExchange-> isMatch(payloadExchange)) (3)
.hasRole(“自定义”)
.route(“ fetch.profile。{username}”) (4)
.access((身份验证,上下文)-> checkFriends(身份验证,上下文))
.anyRequest()。authenticated() (5)
.anyExchange()。permitAll() (6)
)
| 1个 |
建立连接需要权限
ROLE_SETUP |
| 2 |
如果路由是
fetch.profile.me
授权,则仅要求对用户进行身份验证
|
| 3 |
在此规则中,我们设置了一个自定义匹配器,其中的授权要求用户具有权限
ROLE_CUSTOM |
| 4 |
此规则利用自定义授权。
该匹配表示与名称的变量
username
是在提供
context
。
该
checkFriends
方法
公开了自定义授权规则
。
|
| 5 | 此规则可确保没有规则的请求将要求对用户进行身份验证。 请求是包含元数据的地方。 它不会包括其他有效载荷。 |
| 6 | 该规则可确保任何人都没有规则的任何交换。 在此示例中,这意味着没有元数据的有效负载没有授权规则。 |
重要的是要了解授权规则是按顺序执行的。 仅匹配的第一个授权规则将被调用。