diff --git a/docs/spring-for-graphql/spring-graphql.md b/docs/spring-for-graphql/spring-graphql.md new file mode 100644 index 0000000000000000000000000000000000000000..f0409622cd40e207b701d89313e7660bd4230b67 --- /dev/null +++ b/docs/spring-for-graphql/spring-graphql.md @@ -0,0 +1,1282 @@ +# Spring 用于 GraphQL 文档 + +## [](#overview)1. 概述 + +Spring for GraphQL 为 Spring 构建在[GraphQL Java](https://www.graphql-java.com/)上的应用程序提供支持。这是两个团队的联合合作。我们的共同理念是少些固执己见,更多地关注全面和广泛的支持。 + +Spring 对于 GraphQL 来说,它是 GraphQL Java 团队的[GraphQL Java Spring](https://github.com/graphql-java/graphql-java-spring)项目的继承者。它旨在成为所有 Spring、GraphQL 应用程序的基础。 + +目前,该项目正处于迈向 1.0 版本的里程碑阶段,并正在寻找反馈。请使用我们的[问题追踪器](https://github.com/spring-projects/spring-graphql/issues)报告问题,讨论设计问题,或请求一个功能。 + +要开始,请检查[start.spring.io](https://start.spring.io)上的 Spring GraphQL 启动器和[Samples](#samples)部分。 + +## [](#requirements)2. 所需经费 + +Spring 对于 GraphQL,需要以下作为基线: + +* JDK8 + +* Spring 框架 5.3 + +* GraphQL Java17 + +* Spring 用于 QueryDSL 或通过示例查询的数据 2021.1.0 或更高版本 + +## [](#web-transports)3. 网络传输 + +Spring for GraphQL 支持 HTTP 和 Over WebSocket 上的 GraphQL 请求。 + +### [](#web-http)3.1. HTTP + +`GraphQlHttpHandler`通过 HTTP 请求处理 GraphQL,并将其委托给[网络拦截](#web-interception)链以执行请求。有两种变体,一种适用于 Spring MVC,另一种适用于 Spring WebFlux。两者都异步处理请求,并具有等效的功能,但在编写 HTTP 响应时分别依赖于阻塞和非阻塞 I/O。 + +请求必须使用 HTTP POST,而 GraphQL 请求详细信息作为 JSON 包含在请求主体中,如提议的[HTTP 上的 GraphQL](https://github.com/graphql/graphql-over-http/blob/main/spec/GraphQLOverHTTP.md)规范中所定义的那样。一旦成功解码了 JSON 主体,HTTP 响应状态总是 200(OK),来自 GraphQL 请求执行的任何错误都会出现在 GraphQL 响应的“错误”部分。 + +通过声明`RouterFunction` Bean 并使用 Spring MVC 或 WebFlux 中的`RouterFunctions`来创建路由,`GraphQlHttpHandler`可以作为 HTTP 端点公开。引导启动器执行此操作,请参阅[Web 端点](https://docs.spring.io/spring-boot/docs/2.7.0-SNAPSHOT/reference/html/web.html#web.graphql.web-endpoints)小节以获取详细信息,或检查其包含的`GraphQlWebMvcAutoConfiguration`或`GraphQlWebFluxAutoConfiguration`以获取实际配置。 + +Spring for GraphQL 存储库包含一个 Spring MVC应用程序。 + +### [](#web-websocket)3.2. WebSocket + +`GraphQlWebSocketHandler`基于[graphql-ws](https://github.com/enisdenjo/graphql-ws)库中定义的[protocol](https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md)处理 WebSocket 请求上的 GraphQL。在 WebSocket 以上使用 GraphQL 的主要原因是订阅允许发送 GraphQL 响应流,但它也可以用于具有单个响应的常规查询。处理程序将每个请求委托给[网络拦截](#web-interception)链,以进一步执行请求。 + +| |GraphQL over WebSocket 协议

有两个这样的协议,一个在[订阅-transport-ws](https://github.com/apollographql/subscriptions-transport-ws)库中,另一个在[graphql-ws](https://github.com/enisdenjo/graphql-ws)库中。前者不是活动的,而
是由后者继承的。阅读此[blog post](https://the-guild.dev/blog/graphql-over-websockets)查看历史。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +`GraphQlWebSocketHandler`有两种变体,一种用于 Spring MVC,另一种用于 Spring WebFlux。两者都异步处理请求,并具有等效的功能。WebFlux 处理程序还使用非阻塞 I/O 和反压来传输消息流,这很好地工作,因为在 GraphQL Java 中,订阅响应是一个反应流`Publisher`。 + +`graphql-ws`项目列出了许多用于客户机的[recipes](https://github.com/enisdenjo/graphql-ws#recipes)。 + +通过声明`SimpleUrlHandlerMapping` Bean 并使用它将处理程序映射到 URL 路径,可以将`GraphQlWebSocketHandler`公开为 WebSocket 端点。引导启动器有启用此功能的选项,有关详细信息,请参见[Web 端点](https://docs.spring.io/spring-boot/docs/2.7.0-SNAPSHOT/reference/html/web.html#web.graphql.web-endpoints)部分,或检查其包含的`GraphQlWebMvcAutoConfiguration`或`GraphQlWebFluxAutoConfiguration`以获取实际配置。 + +Spring for GraphQL 存储库包含一个 WebFlux[WebSocket sample](https://github.com/spring-projects/spring-graphql/tree/main/samples/webflux-websocket)应用程序。 + +### [](#web-interception)3.3. 网络拦截 + +[HTTP](#web-http)和[WebSocket](#web-websocket)传输处理程序委托给公共 Web 拦截链以执行请求。该链由`WebInterceptor`组件序列组成,然后是调用 GraphQL Java 引擎的`GraphQlService`组件序列。 + +`WebInterceptor`是在 Spring MVC 和 WebFlux 应用程序中使用的通用契约。使用它来拦截请求、检查 HTTP 请求头或注册`graphql.ExecutionInput`的转换: + +``` +class MyInterceptor implements WebInterceptor { + + @Override + public Mono intercept(WebInput webInput, WebInterceptorChain chain) { + webInput.configureExecutionInput((executionInput, builder) -> { + Map map = ... ; + return builder.extensions(map).build(); + }); + return chain.next(webInput); + } +} + +``` + +也可以使用`WebInterceptor`来拦截响应,添加 HTTP 响应头,或转换`graphql.ExecutionResult`: + +``` +class MyInterceptor implements WebInterceptor { + + @Override + public Mono intercept(WebInput webInput, WebInterceptorChain chain) { + return chain.next(webInput) + .map(webOutput -> { + Object data = webOutput.getData(); + Object updatedData = ... ; + return webOutput.transform(builder -> builder.data(updatedData)); + }); + } +} + +``` + +`WebGraphQlHandler`提供了一个构建器来初始化 Web 拦截链。在构建链后,可以使用生成的`WebGraphQlHandler`初始化 HTTP 或 WebSocket 传输处理程序。引导启动器配置所有这些,请参阅[Web 端点](https://docs.spring.io/spring-boot/docs/2.7.0-SNAPSHOT/reference/html/web.html#web.graphql.web-endpoints)小节以获取详细信息,或检查其包含的`GraphQlWebMvcAutoConfiguration`或`GraphQlWebFluxAutoConfiguration`以获取实际配置。 + +## [](#execution)4. 请求执行 + +`GraphQlService`是调用 GraphQL Java 执行请求的主要 Spring 抽象。底层传输,例如[网络传输](#web-transports),委托给`GraphQlService`来处理请求。 + +主要的实现`ExecutionGraphQlService`是围绕`graphql.GraphQL`调用的一个很薄的外观。它配置了`GraphQlSource`以访问`graphql.GraphQL`实例。 + +### [](#execution-graphqlsource)4.1. `GraphQLSource` + +`GraphQlSource`是用于访问用于执行请求的`graphql.GraphQL`实例的核心 Spring 抽象。它提供了一个 Builder API 来初始化 GraphQL Java 并构建`GraphQlSource`。 + +默认的`GraphQlSource`Builder 可通过`GraphQlSource.builder()`访问,支持[active`DataFetcher`](#execution-active-datafetcher)、[上下文传播](#execution-context)和[异常解决](#execution-exceptions)。 + +Spring boot[starter](https://docs.spring.io/spring-boot/docs/2.7.0-SNAPSHOT/reference/html/web.html#web.graphql)通过默认的`GraphQlSource.Builder`初始化`GraphQlSource`实例,并启用以下功能: + +* 从可配置位置加载[模式文件](#execution-graphqlsource-schema-resources)。 + +* 公开适用于`GraphQlSource.Builder`的[properties](https://docs.spring.io/spring-boot/docs/2.7.0-SNAPSHOT/reference/html/application-properties.html#appendix.application-properties.web)。 + +* 检测[`RuntimeWiringConfigurer`]bean。 + +* 检测[仪器仪表](https://www.graphql-java.com/documentation/instrumentation)bean 的[GraphQL 度量](https://docs.spring.io/spring-boot/docs/2.7.0-SNAPSHOT/reference/html/actuator.html#actuator.metrics.supported.spring-graphql)。 + +* 检测`DataFetcherExceptionResolver`bean 的[异常解决](#execution-exceptions)。 + +* 检测`GraphQlSourceBuilderCustomizer`bean 的任何其他自定义。 + +#### [](#execution-graphqlsource-schema-resources)4.1.1. 模式资源 + +`GraphQlSource.Builder`可以配置一个或多个`Resource`实例来进行解析和合并。这意味着模式文件可以从几乎任何位置加载。 + +默认情况下, Spring 引导启动器[查找架构文件](https://docs.spring.io/spring-boot/docs/2.7.0-SNAPSHOT/reference/html/web.html#web.graphql.schema)来自一个众所周知的 Classpath 位置,但是你可以通过`FileSystemResource`将其更改为文件系统上的一个位置,通过`ByteArrayResource`将字节内容更改为通过`ByteArrayResource`,或者实现一个自定义的`Resource`从远程位置或存储空间加载模式文件。 + +#### [](#execution-graphqlsource-schema-creation)4.1.2. 模式创建 + +默认情况下,`GraphQlSource.Builder`使用 GraphQLJava来创建。这适用于大多数应用程序,但如果有必要,你可以通过构建器连接到模式创建: + +``` +// Typically, accessed through Spring Boot's GraphQlSourceBuilderCustomizer +GraphQlSource.Builder builder = ... + +builder.schemaResources(..) + .configureRuntimeWiring(..) + .schemaFactory((typeDefinitionRegistry, runtimeWiring) -> { + // create GraphQLSchema + }) + +``` + +这样做的主要原因是通过联合库创建模式。 + +#### [](#execution-graphqlsource-runtimewiring-configurer)4.1.3. `RuntimeWiringConfigurer` + +你可以使用`RuntimeWiringConfigurer`注册: + +* 自定义标量类型。 + +* 指令处理代码。 + +* `TypeResolver`,如果需要覆盖类型的[default`TypeResolver`](#execution-grapqlsource-default-type-resolver)。 + +* 对于字段`DataFetcher`,尽管大多数应用程序只会简单地配置`AnnotatedControllerConfigurer`,其中检测带注释的`DataFetcher`处理程序方法。 Spring 引导启动器默认添加`AnnotatedControllerConfigurer`。 + +Spring 引导启动器检测类型`RuntimeWiringConfigurer`的 bean,并将它们注册在`GraphQlSource.Builder`中。这意味着,在大多数情况下,在配置中都会有如下内容: + +``` +@Configuration +public class GraphQlConfig { + + @Bean + public RuntimeWiringConfigurer runtimeWiringConfigurer(BookRepository repository) { + + GraphQLScalarType scalarType = ... ; + SchemaDirectiveWiring directiveWiring = ... ; + DataFetcher dataFetcher = QuerydslDataFetcher.builder(repository).single(); + + return wiringBuilder -> wiringBuilder + .scalar(scalarType) + .directiveWiring(directiveWiring) + .type("Query", builder -> builder.dataFetcher("book", dataFetcher)); + } +} + +``` + +如果需要添加`WiringFactory`,例如,为了使注册考虑到模式定义,实现替代的`configure`方法,该方法同时接受`RuntimeWiring.Builder`和输出`List`。这允许你添加任意数量的工厂,然后按顺序调用这些工厂。 + +#### [](#execution-graphqlsource-default-type-resolver)4.1.4. 默认`TypeResolver` + +`GraphQlSource.Builder`将`ClassNameTypeResolver`注册为默认的`TypeResolver`,用于尚未通过[`RuntimeWiringConfigurer`]进行注册的 GraphQL 接口和联合。在 GraphQL Java 中,`TypeResolver`的目的是确定从`DataFetcher`返回的值的 GraphQL 对象类型,用于 GraphQL 接口或 Union 字段。 + +`ClassNameTypeResolver`尝试将该值的简单类名与 GraphQL 对象类型匹配,如果不成功,它还会导航其超级类型,包括基类和接口,以寻找匹配。`ClassNameTypeResolver`提供了一个选项,可以将名称提取函数与`Class`一起配置为 GraphQL 对象类型名称映射,这应该有助于覆盖更多的角情况。 + +#### [](#execution-graphqlsource-operation-caching)4.1.5. 操作缓存 + +GraphQL Java 在执行操作之前必须*解析*和*验证*。这可能会对业绩产生重大影响。为了避免重新解析和验证的需要,应用程序可以配置一个`PreparsedDocumentProvider`来缓存和重用文档实例。[GraphQL Java DOCS](https://www.graphql-java.com/documentation/execution/#query-caching)通过`PreparsedDocumentProvider`提供有关查询缓存的更多详细信息。 + +在 Spring GraphQL 中,你可以通过`GraphQlSource.Builder#configureGraphQl`注册一个`PreparsedDocumentProvider`:。 + +``` +// Typically, accessed through Spring Boot's GraphQlSourceBuilderCustomizer +GraphQlSource.Builder builder = ... + +// Create provider +PreparsedDocumentProvider provider = ... + +builder.schemaResources(..) + .configureRuntimeWiring(..) + .configureGraphQl(graphQLBuilder -> graphQLBuilder.preparsedDocumentProvider(provider)) + +``` + +#### [](#execution-graphqlsource-directives)4.1.6. 指令 + +GraphQL 语言支持“描述 GraphQL 文档中的替代运行时执行和类型验证行为”的指令。指令类似于 Java 中的注释,但在 GraphQL 文档中对类型、字段、片段和操作进行了声明。 + +GraphQL Java 提供`SchemaDirectiveWiring`契约来帮助应用程序检测和处理指令。有关更多详细信息,请参见 GraphQL Java 文档中的[模式指令](https://www.graphql-java.com/documentation/sdl-directives/)。 + +在 Spring GraphQL 中,可以通过[`RuntimeWiringConfigurer`](#execution-graphqlsource-runtimewilling-configurer)注册`SchemaDirectiveWiring`。 Spring 引导启动器会检测到这样的 bean,因此你可能会有如下内容: + +``` +@Configuration +public class GraphQlConfig { + + @Bean + public RuntimeWiringConfigurer runtimeWiringConfigurer() { + return builder -> builder.directiveWiring(new MySchemaDirectiveWiring()); + } + +} + +``` + +| |对于指令支持的示例,请查看[GraphQL Java 的扩展验证](https://github.com/graphql-java/graphql-java-extended-validation)库。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### [](#execution-reactive-datafetcher)4.2. Reactive `DataFetcher` + +默认的`GraphQlSource`Builder 使对`DataFetcher`的支持返回`Mono`或`Flux`,这将这些支持调整为`CompletableFuture`,其中`Flux`值被聚合并转换为一个列表,除非该请求是 GraphQL 订阅请求,在这种情况下,返回值仍然是用于流式 GraphQL 响应的反应流`Publisher`。 + +反应性`DataFetcher`可以依赖于对从传输层传播的反应器上下文的访问,例如来自 WebFlux 请求的处理,请参见[WebFlux 上下文](#execution-context-webflux)。 + +### [](#execution-context)4.3. 执行上下文 + +Spring 对于 GraphQL 提供了支持,以通过 GraphQL 引擎从[网络传输](#web-transports)透明地传播上下文,并支持`DataFetcher`和它调用的其他组件。这既包括来自 Spring MVC 请求处理线程的`ThreadLocal`上下文,也包括来自 WebFlux 处理管道的反应器`Context`上下文。 + +#### [](#execution-context-webmvc)4.3.1. WebMVC + +GraphQL Java 调用的`DataFetcher`和其他组件可能并不总是在与 Spring MVC 处理程序相同的线程上执行,例如,如果异步[`WebInterceptor`]或`DataFetcher`切换到不同的线程。 + +Spring 对于 GraphQL 支持将`ThreadLocal`值从 Servlet 容器线程传播到`DataFetcher`线程和由 GraphQL 引擎调用的其他组件上执行。要做到这一点,应用程序需要创建`ThreadLocalAccessor`以提取感兴趣的`ThreadLocal`值: + +``` +public class RequestAttributesAccessor implements ThreadLocalAccessor { + + private static final String KEY = RequestAttributesAccessor.class.getName(); + + @Override + public void extractValues(Map container) { + container.put(KEY, RequestContextHolder.getRequestAttributes()); + } + + @Override + public void restoreValues(Map values) { + if (values.containsKey(KEY)) { + RequestContextHolder.setRequestAttributes((RequestAttributes) values.get(KEY)); + } + } + + @Override + public void resetValues(Map values) { + RequestContextHolder.resetRequestAttributes(); + } + +} + +``` + +可以在[Webgraphandler](#web-interception)构建器中注册`ThreadLocalAccessor`。引导启动器检测这种类型的 bean 并自动为 Spring MVC 应用程序注册它们,请参见[Web 端点](https://docs.spring.io/spring-boot/docs/2.7.0-SNAPSHOT/reference/html/web.html#web.graphql.web-endpoints)小节。 + +#### [](#execution-context-webflux)4.3.2. WebFlux + +[acceptive`DataFetcher`](#execution-acceptive-datafetcher)可以依赖于对源自 WebFlux 请求处理链的反应器上下文的访问。这包括由[网络拦截器](#web-interception)组件添加的反应堆上下文。 + +### [](#execution-exceptions)4.4. 异常解决 + +GraphQL Java 应用程序可以注册`DataFetcherExceptionHandler`,以决定如何在 GraphQL 响应的“错误”部分中表示来自数据层的异常。 + +Spring 对于 GraphQL,有一个内置的`DataFetcherExceptionHandler`,它被配置为由[`GraphQLSource`](#execution-grapqlsource)生成器使用。它使应用程序能够注册一个或多个 Spring `DataFetcherExceptionResolver`按顺序调用的组件,直到将`Exception`解析为`graphql.GraphQLError`对象的列表。 + +`DataFetcherExceptionResolver`是一种异步契约。对于大多数实现方式,扩展`DataFetcherExceptionResolverAdapter`并覆盖其同步解决异常的`resolveToSingleError`或`resolveToMultipleErrors`方法之一就足够了。 + +a`GraphQLError`可以被分配一个`graphql.ErrorClassification`。 Spring 对于 GraphQL 定义了一个`ErrorType`枚举,具有常见的、错误的分类类别: + +* `BAD_REQUEST` + +* `UNAUTHORIZED` + +* `FORBIDDEN` + +* `NOT_FOUND` + +* `INTERNAL_ERROR` + +应用程序可以使用它来对错误进行分类。如果错误仍未解决,则默认情况下将其标记为`INTERNAL_ERROR`。 + +### [](#execution-batching)4.5. 批量装载 + +给定一个`Book`和它的`Author`,我们可以为一本书创建一个`DataFetcher`,为它的作者创建另一个。这允许在有或没有作者的情况下选择书籍,但这意味着书籍和作者 AREN 不能一起加载,这在查询多本书时效率特别低,因为每本书的作者都是单独加载的。这就是所谓的 N+1 选择问题。 + +#### [](#execution-batching-dataloader)4.5.1. `DataLoader` + +GraphQL Java 提供了一种`DataLoader`机制,用于批量加载相关实体。你可以在[GraphQL Java DOCS](https://www.graphql-java.com/documentation/batching/)中找到完整的详细信息。以下是其工作原理的摘要: + +1. 在`DataLoaderRegistry`中注册`DataLoader`的可以加载实体的项,给定唯一的键。 + +2. `DataFetcher`的可以访问`DataLoader`的,并使用它们通过 ID 加载实体。 + +3. a`DataLoader`通过返回 future 来延迟加载,因此可以在批处理中完成。 + +4. `DataLoader`的每请求保持一个加载实体的缓存,这可以进一步提高效率。 + +#### [](#execution-batching-batch-loader-registry)4.5.2. `BatchLoaderRegistry` + +GraphQL Java 中完整的批处理加载机制需要实现几个`BatchLoader`接口中的一个,然后用`DataLoader`中的名称将这些接口包装并注册为`DataLoader`s。 + +Spring GraphQL 中的 API 略有不同。对于注册,只有一个 central`BatchLoaderRegistry`公开工厂方法和构建器,以创建和注册任意数量的批处理加载函数: + +``` +@Configuration +public class MyConfig { + + public MyConfig(BatchLoaderRegistry registry) { + + registry.forTypePair(Long.class, Author.class).registerMappedBatchLoader((authorIds, env) -> { + // return Mono + }); + + // more registrations ... + } + +} + +``` + +Spring 引导启动器声明一个`BatchLoaderRegistry` Bean,你可以将其注入到你的配置中,如上面所示,或注入到任何组件中,例如控制器中的顺序寄存器批处理加载功能。然后,将`BatchLoaderRegistry`注入`ExecutionGraphQlService`,从而确保每个请求的注册量`DataLoader`。 + +默认情况下,`DataLoader`名称是基于目标实体的类名。这允许`@SchemaMapping`方法使用泛型类型声明[Dataloader 参数](#controllers-schema-mapping-data-loader),而不需要指定名称。但是,如果有必要,可以通过`BatchLoaderRegistry`Builder 自定义名称,以及其他`DataLoader`选项。 + +对于许多情况,在加载相关实体时,可以使用[@batchmapping](#controllers-batch-mapping)控制器方法,这对于需要使用`BatchLoaderRegistry`和`DataLoader`直接替换的快捷方式也提供了重要的好处。S`BatchLoaderRegistry`也提供了其他好处。它支持从批装载函数和从`@BatchMapping`方法访问相同的`GraphQLContext`,并确保[上下文传播](#execution-context)到它们。这就是为什么应用程序需要使用它。可以直接执行你自己的`DataLoader`注册,但这种注册将放弃上述好处。 + +#### [](#execution-batching-testing)4.5.3. 测试批装载 + +首先让`BatchLoaderRegistry`在`DataLoaderRegistry`上执行注册: + +``` +BatchLoaderRegistry batchLoaderRegistry = new DefaultBatchLoaderRegistry(); +// perform registrations... + +DataLoaderRegistry dataLoaderRegistry = DataLoaderRegistry.newRegistry().build(); +batchLoaderRegistry.registerDataLoaders(dataLoaderRegistry, graphQLContext); + +``` + +现在你可以访问和测试个别`DataLoader`的如下所示: + +``` +DataLoader loader = dataLoaderRegistry.getDataLoader(Book.class.getName()); +loader.load(1L); +loader.loadMany(Arrays.asList(2L, 3L)); +List books = loader.dispatchAndJoin(); // actual loading + +assertThat(books).hasSize(3); +assertThat(books.get(0).getName()).isEqualTo("..."); +// ... + +``` + +## [](#data)5. 数据整合 + +Spring 对于 GraphQL,你可以利用现有的 Spring 技术,遵循常见的编程模型来通过 GraphQL 公开底层数据源。 + +本节讨论了用于 Spring 数据的集成层,该集成层提供了一种简单的方法,将 QueryDSL 或查询 by example 存储库调整为`DataFetcher`,包括用于标记为`@GraphQlRepository`的存储库的自动检测和 GraphQL 查询注册的选项。 + +### [](#data-querydsl)5.1. QueryDSL + +Spring 对于 GraphQL 支持使用[Querydsl](http://www.querydsl.com/)来通过 Spring 数据获取数据[QueryDSL 扩展](https://docs.spring.io/spring-data/commons/docs/current/reference/html/#core.extensions)。QueryDSL 提供了一种灵活的 TypeSafe 方法,通过使用注释处理器生成元模型来表示查询谓词。 + +例如,将存储库声明为`QuerydslPredicateExecutor`: + +``` +public interface AccountRepository extends Repository, + QuerydslPredicateExecutor { +} + +``` + +然后使用它创建`DataFetcher`: + +``` +// For single result queries +DataFetcher dataFetcher = + QuerydslDataFetcher.builder(repository).single(); + +// For multi-result queries +DataFetcher> dataFetcher = + QuerydslDataFetcher.builder(repository).many(); + +``` + +你现在可以通过[`RuntimeWiringConfigurer`](#execution-graphqlsource-runtimewilling-configurer)注册上面的`DataFetcher`。 + +`DataFetcher`从 GraphQL 请求参数构建一个 QueryDSL`Predicate`,并使用它来获取数据。 Spring 对于 JPA、MongoDB 和 LDAP,数据支持`QuerydslPredicateExecutor`。 + +如果存储库是`ReactiveQuerydslPredicateExecutor`,则构建器返回`DataFetcher>`或`DataFetcher>`。 Spring 数据支持 MongoDB 的这种变体。 + +#### [](#data-querydsl-build)5.1.1. 构建设置 + +要在构建中配置 QueryDSL,请遵循[正式参考文件](https://querydsl.com/static/querydsl/latest/reference/html/ch02.html): + +例如: + +Gradle + +``` +dependencies { + //... + + annotationProcessor "com.querydsl:querydsl-apt:$querydslVersion:jpa", + 'org.hibernate.javax.persistence:hibernate-jpa-2.1-api:1.0.2.Final', + 'javax.annotation:javax.annotation-api:1.3.2' +} + +compileJava { + options.annotationProcessorPath = configurations.annotationProcessor +} +``` + +Maven + +``` + + + + com.querydsl + querydsl-apt + ${querydsl.version} + jpa + provided + + + org.hibernate.javax.persistence + hibernate-jpa-2.1-api + 1.0.2.Final + + + javax.annotation + javax.annotation-api + 1.3.2 + + + + + + com.mysema.maven + apt-maven-plugin + ${apt-maven-plugin.version} + + + + process + + + target/generated-sources/java + com.querydsl.apt.jpa.JPAAnnotationProcessor + + + + + +``` + +[WebMVC-HTTP](https://github.com/spring-projects/spring-graphql/tree/main/samples/webmvc-http)样本使用 queryDSL 表示`artifactRepositories`。 + +#### [](#data-querydsl-customizations)5.1.2. 定制 + +`QuerydslDataFetcher`支持自定义如何将 GraphQL 参数绑定到属性上以创建 QueryDSL`Predicate`。默认情况下,对于每个可用的属性,参数被绑定为“is equaled to”。要对此进行定制,可以使用`QuerydslDataFetcher`Builder 方法来提供`QuerydslBinderCustomizer`。 + +存储库本身可能是`QuerydslBinderCustomizer`的实例。这是在[自动注册](#data-querydsl-registration)期间自动检测和透明地应用的。然而,当手动构建`QuerydslDataFetcher`时,你将需要使用 Builder 方法来应用它。 + +`QuerydslDataFetcher`支持接口和 DTO 投影来转换查询结果,然后返回这些结果进行进一步的 GraphQL 处理。 + +| |要了解投影是什么,请参阅[Spring Data docs](https://docs.spring.io/spring-data/commons/docs/current/reference/html/#projections)。
要了解如何在 GraphQL 中使用投影,请参见[选择集与投影](#data-projections)。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +要在 QueryDSL 存储库中使用 Spring 数据投影,请创建一个投影接口或一个目标 DTO 类,并通过`projectAs`方法对其进行配置,以获得产生目标类型的`DataFetcher`: + +``` +class Account { + + String name, identifier, description; + + Person owner; +} + +interface AccountProjection { + + String getName(); + + String getIdentifier(); +} + +// For single result queries +DataFetcher dataFetcher = + QuerydslDataFetcher.builder(repository).projectAs(AccountProjection.class).single(); + +// For multi-result queries +DataFetcher> dataFetcher = + QuerydslDataFetcher.builder(repository).projectAs(AccountProjection.class).many(); + +``` + +#### [](#data-querydsl-registration)5.1.3. 自动注册 + +如果存储库使用`@GraphQlRepository`进行注释,那么对于尚未注册`DataFetcher`且其返回类型与存储库域类型匹配的查询,将自动对其进行注册。这包括单值查询和多值查询。 + +默认情况下,查询返回的 GraphQL 类型的名称必须与存储库域类型的简单名称匹配。如果需要,可以使用`@GraphQlRepository`的`typeName`属性来指定目标 GraphQL 类型名。 + +自动注册检测给定存储库是否实现`QuerydslBinderCustomizer`,并通过`QuerydslDataFetcher`Builder 方法透明地应用该方法。 + +自动注册是通过可从`QuerydslDataFetcher`获得的内置`RuntimeWiringConfigurer`执行的。[引导启动器](https://docs.spring.io/spring-boot/docs/2.7.0-SNAPSHOT/reference/html/web.html#web.graphql.data-query)自动检测`@GraphQlRepository`bean,并使用它们初始化`RuntimeWiringConfigurer`with。 + +自动注册不支持[定制](#data-querybyexample-customizations)。如果需要,你需要使用`QueryByExampleDataFetcher`通过[`RuntimeWiringConfigurer`](#execution-graphqlsource-runtimewilling-configurer)手动构建和注册`DataFetcher`。 + +### [](#data-querybyexample)5.2. 示例查询 + +Spring 数据支持使用[示例查询](https://docs.spring.io/spring-data/commons/docs/current/reference/html/#query-by-example)来获取数据。Query by Example 是一种简单的查询技术,不需要你通过特定于存储的查询语言编写查询。 + +从声明`QueryByExampleExecutor`的存储库开始: + +``` +public interface AccountRepository extends Repository, + QueryByExampleExecutor { +} + +``` + +使用`QueryByExampleDataFetcher`将存储库转换为`DataFecher`: + +``` +// For single result queries +DataFetcher dataFetcher = + QueryByExampleDataFetcher.builder(repository).single(); + +// For multi-result queries +DataFetcher> dataFetcher = + QueryByExampleDataFetcher.builder(repository).many(); + +``` + +你现在可以通过[`RuntimeWiringConfigurer`]注册上面的`DataFetcher`(#execution-graphqlsource-runtimewilling-configurer)。 + +`DataFetcher`使用 GraphQL 参数映射来创建存储库的域类型,并将其作为示例对象来获取数据。 Spring 对于 JPA、MongoDB、NEO4J 和 Redis,数据支持`QueryByExampleDataFetcher`。 + +如果存储库是`ReactiveQueryByExampleExecutor`,则构建器返回`DataFetcher>`或`DataFetcher>`。 Spring 数据支持 MongoDB、NEO4J、Redis 和 R2DBC 的这种变体。 + +#### [](#data-querybyexample-build)5.2.1. 构建设置 + +Spring 对于支持它的数据存储,示例查询已经包括在 Spring 数据模块中,因此不需要额外的设置来启用它。 + +#### [](#data-querybyexample-customizations)5.2.2. 定制 + +`QueryByExampleDataFetcher`支持接口和 DTO 投影来转换查询结果,然后返回这些结果进行进一步的 GraphQL 处理。 + +| |要了解投影是什么,请参阅[Spring Data documentation](https://docs.spring.io/spring-data/commons/docs/current/reference/html/#projections)。
要了解投影在 GraphQL 中的作用,请参见[选择集与投影](#data-projections)。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +Spring 要通过示例存储库查询使用数据投影,可以创建投影接口或目标 DTO 类,并通过`projectAs`方法对其进行配置,以获得产生目标类型的`DataFetcher`: + +``` +class Account { + + String name, identifier, description; + + Person owner; +} + +interface AccountProjection { + + String getName(); + + String getIdentifier(); +} + +// For single result queries +DataFetcher dataFetcher = + QueryByExampleDataFetcher.builder(repository).projectAs(AccountProjection.class).single(); + +// For multi-result queries +DataFetcher> dataFetcher = + QueryByExampleDataFetcher.builder(repository).projectAs(AccountProjection.class).many(); + +``` + +#### [](#data-querybyexample-registration)5.2.3. 自动注册 + +如果存储库使用`@GraphQlRepository`进行注释,那么对于尚未注册`DataFetcher`且其返回类型与存储库域类型匹配的查询,将自动对其进行注册。这包括单值查询和多值查询。 + +默认情况下,查询返回的 GraphQL 类型的名称必须与存储库域类型的简单名称匹配。如果需要,可以使用`@GraphQlRepository`的`typeName`属性来指定目标 GraphQL 类型名。 + +自动注册是通过可从`QueryByExampleDataFetcher`获得的内置`RuntimeWiringConfigurer`执行的。[引导启动器](https://docs.spring.io/spring-boot/docs/2.7.0-SNAPSHOT/reference/html/web.html#web.graphql.data-query)自动检测`@GraphQlRepository`bean,并使用它们初始化`RuntimeWiringConfigurer`with。 + +自动注册不支持[定制](#data-querybyexample-customizations)。如果需要,你需要使用`QueryByExampleDataFetcher`通过[`RuntimeWiringConfigurer`]手动构建和注册`DataFetcher`(#execution-grapqlsource-runtimewilling-configurer)。 + +### [](#data-projections)5.3. 选择集与投影 + +出现的一个常见问题是,GraphQL 选择集与[Spring Data projections](https://docs.spring.io/spring-data/commons/docs/current/reference/html/#projections)相比如何,每个选择集起什么作用? + +简短的回答是, Spring for GraphQL 不是将 GraphQL 查询直接转换为 SQL 或 JSON 查询的数据网关。相反,它允许你利用现有的 Spring 技术,并且不假定 GraphQL 模式和底层数据模型之间存在一对一的映射。这就是为什么客户机驱动的选择和数据模型的服务器端转换可以发挥互补作用的原因。 + +为了更好地理解,考虑 Spring 数据促进了域驱动(DDD)设计,作为管理数据层中复杂性的推荐方法。在 DDD 中,坚持总量约束是很重要的。根据定义,聚合只有在全部加载的情况下才是有效的,因为部分加载的聚合可能会对聚合功能施加限制。 + +在 Spring 数据中,你可以选择是希望将聚合按原样公开,还是在将其作为 GraphQL 结果返回之前将转换应用于数据模型。有时,做前者就足够了,默认情况下,[Querydsl](#data-querydsl)和[示例查询](#data-querybyexample)集成将 GraphQL 选择集转换为属性路径提示,底层 Spring 数据模块使用这些属性路径提示来限制选择。 + +在其他情况下,为了适应 GraphQL 模式,减少甚至转换底层数据模型是有用的。 Spring 数据通过接口和 DTO 投影来支持这一点。 + +接口投影定义了一组固定的属性,根据数据存储查询结果,在其中属性可以`null`,也可以不是`null`。有两种类型的接口预测,它们都决定从底层数据源加载哪些属性: + +* 如果不能部分实现聚合对象,但仍然希望公开属性的子集,则[闭合界面投影](https://docs.spring.io/spring-data/commons/docs/current/reference/html/#projections.interfaces.closed)是有帮助的。 + +* [开放接口投影](https://docs.spring.io/spring-data/commons/docs/current/reference/html/#projections.interfaces.open)利用 Spring 的`@Value`注释和[SpEL](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#expressions)表达式来应用轻量级数据转换,例如连接、计算或将静态函数应用于属性。 + +DTO 投影提供了更高级别的定制,因为你可以将转换代码放置在构造函数或 getter 方法中。 + +DTO 投影是通过查询实现的,查询中的各个属性是由投影本身确定的。DTO 投影通常用于 Full-Args 构造函数(例如 Java 记录),因此只有当所有必需的字段(或列)都是数据库查询结果的一部分时,才能构造它们。 + +## [](#controllers)6. 带注释的控制器 + +Spring 对于 GraphQL 提供了一种基于注释的编程模型,其中`@Controller`组件使用注释来声明具有灵活方法签名的处理程序方法,以获取特定 GraphQL 字段的数据。例如: + +``` +@Controller +public class GreetingController { + + @QueryMapping (1) + public String hello() { (2) + return "Hello, world!"; + } + +} + +``` + +|**1**|将此方法绑定到查询,即查询类型下的字段。| +|-----|---------------------------------------------------------------------------| +|**2**|如果未在注释上声明,则从方法名确定查询。| + +Spring 对于 GraphQL 使用`RuntimeWiring.Builder`将上述处理程序方法注册为用于名为“Hello”的查询的`graphql.schema.DataFetcher`。 + +### [](#controllers-declaration)6.1. 声明 + +你可以将`@Controller`bean 定义为标准 Spring Bean 定义。该`@Controller`原型允许自动检测,与 Spring 对齐,用于在 Classpath 上检测`@Controller`和`@Component`类并为它们自动注册 Bean 定义。它还充当带注释的类的原型,指示其作为 GraphQL 应用程序中的数据获取组件的角色。 + +`AnnotatedControllerConfigurer`通过`RuntimeWiring.Builder`检测`@Controller`bean 并将其注释的处理程序方法注册为`DataFetcher`s。它是`RuntimeWiringConfigurer`的一个实现,它可以被添加到`GraphQlSource.Builder`中。 Spring 引导启动器会自动将`AnnotatedControllerConfigurer`声明为 Bean,并将所有`RuntimeWiringConfigurer`bean 添加到`GraphQlSource.Builder`中,从而支持带注释的`DataFetcher`s,请参见引导启动器文档中的[GraphQL RuntimeWiring](https://docs.spring.io/spring-boot/docs/2.7.0-SNAPSHOT/reference/html/web.html#web.graphql.runtimewiring)部分。 + +### [](#controllers-schema-mapping)6.2. `@SchemaMapping` + +`@SchemaMapping`注释将处理程序方法映射到 GraphQL 模式中的字段,并声明该字段为该字段的`DataFetcher`。注释可以指定父类型名和字段名: + +``` +@Controller +public class BookController { + + @SchemaMapping(typeName="Book", field="author") + public Author getAuthor(Book book) { + // ... + } +} + +``` + +`@SchemaMapping`注释也可以省略这些属性,在这种情况下,字段名称默认为方法名称,而类型名称默认为注入到方法中的源/父对象的简单类名。例如,以下默认输入“book”和字段“author”: + +``` +@Controller +public class BookController { + + @SchemaMapping + public Author author(Book book) { + // ... + } +} + +``` + +可以在类级别声明`@SchemaMapping`注释,以指定类中所有处理程序方法的默认类型名。 + +``` +@Controller +@SchemaMapping(typeName="Book") +public class BookController { + + // @SchemaMapping methods for fields of the "Book" type + +} + +``` + +`@QueryMapping`、`@MutationMapping`和`@SubscriptionMapping`是元注释,它们本身用`@SchemaMapping`进行注释,并且将类型名称预设为`Query`、`Mutation`或`Subscription`。实际上,这些是分别针对查询、突变和订阅类型下的字段的快捷方式注释。例如: + +``` +@Controller +public class BookController { + + @QueryMapping + public Book bookById(@Argument Long id) { + // ... + } + + @MutationMapping + public Book addBook(@Argument BookInput bookInput) { + // ... + } + + @SubscriptionMapping + public Flux newPublications() { + // ... + } +} + +``` + +`@SchemaMapping`处理程序方法具有灵活的签名,可以从一系列方法参数和返回值中进行选择。 + +#### [](#controllers-schema-mapping-signature)6.2.1. 方法签名 + +模式映射处理程序方法可以具有以下任何一个方法参数: + +| Method Argument |说明| +|-------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------| +| `@Argument` |要访问将命名字段参数转换为更高级别的类型化对象。
请参见[`@Argument`](#controllers-schema-mapping-convention)。| +| `@Arguments` |有关对转换为更高级别的类型化对象的所有字段参数的访问。
参见[`@Arguments`](#controllers-schema-mapping-arguments)。| +| `@ProjectedPayload` Interface |通过项目接口访问字段参数。
参见[`@ProjectPayload`接口](#controllers-schema-mapping-projectedpayload-pargument)。| +| Source |关于字段的源(即父/容器)实例的访问。
参见[Source](#controllers-schema-mapping-source)。| +| `DataLoader` |要访问`DataLoader`中的`DataLoader`。
请参见[`DataLoader`](#controllers-schema-mapping-data-loader)。| +| `@ContextValue` |对于从 localContext 访问一个值,如果它是`GraphQLContext`、
的实例,或者来自`GraphQLContext`的`DataFetchingEnvironment`的实例。| +| `GraphQLContext` |用于从`DataFetchingEnvironment`访问上下文。| +| `java.security.Principal` |从 Spring 安全上下文中获得的,如果可用的话。| +| `@AuthenticationPrincipal` |用于从 Spring 安全上下文访问`Authentication#getPrincipal()`。| +|`DataFetchingFieldSelectionSet`|用于通过`DataFetchingEnvironment`访问查询的选择集。| +| `Locale`, `Optional` |从`DataFetchingEnvironment`访问`Locale`。| +| `DataFetchingEnvironment` |直接访问底层`DataFetchingEnvironment`。| + +模式映射处理程序方法可以返回任意值,包括反应器`Mono`和`Flux`中描述的[ractive`DataFetcher`](#execution-ractive-datafetcher)。 + +#### [](#controllers-schema-mapping-argument)6.2.2. `@Argument` + +在 GraphQL Java 中,`DataFetchingEnvironment`提供对特定字段参数值的映射的访问。这些值可以是简单的标量值(例如 String,long)、用于更复杂输入的`Map`的值,或者是`List`的值。 + +使用`@Argument`注释将命名字段参数注入到处理程序方法中。方法参数可以是任何类型的更高级别的类型化对象。它是根据已命名字段参数的值创建和初始化的,或者将它们匹配到单个数据构造函数参数,或者使用默认构造函数,然后通过`org.springframework.validation.DataBinder`将键匹配到对象属性上: + +``` +@Controller +public class BookController { + + @QueryMapping + public Book bookById(@Argument Long id) { + // ... + } + + @MutationMapping + public Book addBook(@Argument BookInput bookInput) { + // ... + } +} + +``` + +默认情况下,如果方法参数名是可用的(需要`-parameters`带有 Java8+ 或来自编译器的调试信息的编译器标志),它将用于查找参数。如果需要,可以通过注释自定义名称,例如`@Argument("bookInput")`。 + +| |`@Argument`注释没有“required”标志,也没有
指定默认值的选项。这两个都可以在 GraphQL 模式级别指定,
由 GraphQL 引擎强制执行。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +你可以在`Map`参数上使用`@Argument`,以获得所有参数的值。不能设置`@Argument`上的 name 属性。 + +#### [](#controllers-schema-mapping-arguments)6.2.3. `@Arguments` + +如果你想将完整的参数映射到单个目标对象上,请使用`@Arguments`注释,与`@Argument`相反,后者绑定一个特定的命名参数。 + +例如,`@Argument BookInput bookInput`使用参数“bookinput”的值初始化`BookInput`,而`@Arguments`使用完整的参数映射,在这种情况下,顶层参数绑定到`BookInput`属性。 + +#### [](#controllers-schema-mapping-validation)6.2.4. `@Argument(s)`验证 + +如果在应用程序上下文中存在一个[Bean Validation](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#validation-beanvalidation-overview)`Validator`(或者通常,一个`LocalValidatorFactoryBean`) Bean,则`AnnotatedControllerConfigurer`将自动检测它并配置用于验证的支持。然后在方法调用之前验证用`@Valid`和`@Validated`注释的控制器参数。 + +Bean 验证允许你声明对类型的约束,如下例所示: + +``` +public class BookInput { + + @NotNull + private String title; + + @NotNull + @Size(max=13) + private String isbn; +} + +``` + +然后,我们可以用`@Valid`标记我们的验证参数: + +``` +@Controller +public class BookController { + + @MutationMapping + public Book addBook(@Argument @Valid BookInput bookInput) { + // ... + } +} + +``` + +如果在验证过程中发生错误,将抛出一个`ConstraintViolationException`,并可以在以后[使用自定义`DataFetcherExceptionResolver`解决](#execution-exceptions)。 + +| |与 Spring MVC 不同,处理程序方法签名不支持注入`BindingResult`以对验证错误做出反应:这些将作为例外情况在全局范围内处理。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### [](#controllers-schema-mapping-projectedpayload-argument)6.2.5. `@ProjectPayload`接口 + +作为使用带有[`@Argument`](#controllers-schema-mapping-argument)的完整对象的一种替代方法,你还可以使用投影接口通过定义良好的最小接口访问 GraphQL 请求参数。当 Spring 数据在类路径上时,参数投影由[Spring Data’s Interface projections](https://docs.spring.io/spring-data/commons/docs/current/reference/html/#projections.interfaces)提供。 + +要利用这一点,请创建一个带有`@ProjectedPayload`注释的接口,并将其声明为控制器方法参数。如果参数被注释为`@Argument`,则它将应用于`DataFetchingEnvironment.getArguments()`映射中的单个参数。当声明时不带`@Argument`,投影工作于完整参数映射中的顶层参数。 + +例如: + +``` +@Controller +public class BookController { + + @QueryMapping + public Book bookById(BookIdProjection bookId) { + // ... + } + + @MutationMapping + public Book addBook(@Argument BookInputProjection bookInput) { + // ... + } +} + +@ProjectedPayload +interface BookIdProjection { + + Long getId(); +} + +@ProjectedPayload +interface BookInputProjection { + + String getName(); + + @Value("#{target.author + ' ' + target.name}") + String getAuthorAndName(); +} + +``` + +#### [](#controllers-schema-mapping-source)6.2.6. 来源 + +在 GraphQL Java 中,`DataFetchingEnvironment`提供对字段的源(即父/容器)实例的访问。要访问它,只需声明一个预期目标类型的方法参数。 + +``` +@Controller +public class BookController { + + @SchemaMapping + public Author author(Book book) { + // ... + } +} + +``` + +源方法参数还有助于确定映射的类型名。如果 Java 类的简单名称与 GraphQL 类型匹配,则不需要在`@SchemaMapping`注释中显式指定类型名称。 + +| |[`@BatchMapping`](#controllers-batch-mapping)处理程序方法可以为一个查询批装载所有作者,
给定源/父书对象的列表。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### [](#controllers-schema-mapping-data-loader)6.2.7. `DataLoader` + +当你注册一个实体的批处理加载函数时,如[批量装载](#execution-batching)中所解释的那样,你可以通过声明一个类型为`DataLoader`的方法参数来访问该实体的`DataLoader`,并使用它来加载该实体: + +``` +@Controller +public class BookController { + + public BookController(BatchLoaderRegistry registry) { + registry.forTypePair(Long.class, Author.class).registerMappedBatchLoader((authorIds, env) -> { + // return Map + }); + } + + @SchemaMapping + public CompletableFuture author(Book book, DataLoader loader) { + return loader.load(book.getAuthorId()); + } + +} + +``` + +默认情况下,`BatchLoaderRegistry`使用值类型的完整类名(例如,`Author`的类名)作为注册的键,因此只需声明带有泛型类型的`DataLoader`方法参数,就可以提供足够的信息来在`DataLoaderRegistry`中定位它。作为后备,`DataLoader`方法参数解析器也将尝试方法参数名称作为键,但通常不需要这样做。 + +请注意,对于许多加载相关实体的情况,其中`@SchemaMapping`只是将其委托给`DataLoader`,你可以使用[@batchmapping](#controllers-batch-mapping)方法来减少样板文件,如下一节所述。 + +### [](#controllers-batch-mapping)6.3. `@BatchMapping` + +[批量装载](#execution-batching)通过使用`org.dataloader.DataLoader`来延迟单个实体实例的加载,从而解决了 N+1SELECT 问题,从而可以将它们一起加载。例如: + +``` +@Controller +public class BookController { + + public BookController(BatchLoaderRegistry registry) { + registry.forTypePair(Long.class, Author.class).registerMappedBatchLoader((authorIds, env) -> { + // return Map + }); + } + + @SchemaMapping + public CompletableFuture author(Book book, DataLoader loader) { + return loader.load(book.getAuthorId()); + } + +} + +``` + +对于加载关联实体的简单情况(如上图所示),`@SchemaMapping`方法所做的不过是将其委托给`DataLoader`。这是用`@BatchMapping`方法可以避免的样板。例如: + +``` +@Controller +public class BookController { + + @BatchMapping + public Mono> author(List books) { + // ... + } +} + +``` + +上面变成了`BatchLoaderRegistry`中的批处理加载函数,其中键是`Book`实例和加载的值它们的作者。此外,`DataFetcher`也透明地绑定到类型`author`的`author`字段,对于作者来说,它只是将其委托给`DataLoader`,给定其源/父`Book`实例。 + +| |要作为唯一键使用,`Book`必须实现`hashcode`和`equals`。| +|---|--------------------------------------------------------------------------| + +默认情况下,字段名称默认为方法名称,而类型名称默认为输入`List`元素类型的简单类名。两者都可以通过注释属性进行定制。类型名也可以从类级别`@SchemaMapping`继承。 + +#### [](#controllers-batch-mapping-signature)6.3.1. 方法签名 + +批处理映射方法支持以下参数: + +| Method Argument |说明| +|-------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------| +| `List` |源/父对象。| +|`java.security.Principal`|从 Spring 安全上下文获得(如果可用的话)。| +| `@ContextValue` |要访问来自`GraphQLContext`的`BatchLoaderEnvironment`的值,请使用
,该上下文与来自`DataFetchingEnvironment`的上下文相同。| +| `GraphQLContext` |要访问来自`BatchLoaderEnvironment`的上下文,请访问
,该上下文与来自`DataFetchingEnvironment`的上下文相同。| +|`BatchLoaderEnvironment` |在 GraphQL Java 中可用的环境为`org.dataloader.BatchLoaderWithContext`。| + +批处理映射方法可以返回: + +| Return Type |说明| +|---------------------|--------------------------------------------------------------------------------------------------------------------------| +| `Mono>` |以父对象为键,以批处理加载对象为值的映射。| +| `Flux` |批装载对象的序列,其顺序必须与传递到方法中的源/父
对象的顺序相同。| +|`Map`, `List`|命令式变体,例如,不需要进行远程调用。| + +## [](#security)7. 安全 + +可以使用 HTTP URL 安全性来保护[Web](#web-transports)GraphQL 端点的路径,以确保只有经过身份验证的用户才能访问它。然而,这并不能区分在单个 URL 上的共享端点上的不同 GraphQL 请求。 + +要应用更细粒度的安全性,可以在获取 GraphQL 响应的特定部分所涉及的服务方法中添加 Spring 安全性注释,例如`@PreAuthorize`或`@Secured`。由于[上下文传播](#execution-context)的目的是使安全性和其他上下文在数据获取级别可用,所以这种方法应该有效。 + +Spring for GraphQL 存储库包含[Spring MVC](https://github.com/spring-projects/spring-graphql/tree/main/samples/webmvc-http-security)和[WebFlux](https://github.com/spring-projects/spring-graphql/tree/main/samples/webflux-security)的示例。 + +## [](#testing)8. 测试 + +用 Spring 的`WebTestClient`测试 GraphQL 请求是可能的,只需要发送和接收 JSON,但是许多 GraphQL 特定的细节使得这种方法比必要的更麻烦。 + +要获得完整的测试支持,你需要在构建中添加`spring-graphql-test`依赖项: + +Gradle + +``` +dependencies { + // ... + testImplementation 'org.springframework.graphql:spring-graphql-test:1.0.0-SNAPSHOT' +} +``` + +Maven + +``` + + + + org.springframework.graphql + spring-graphql-test + 1.0.0-SNAPSHOT + test + + +``` + +### [](#testing-graphqltester)8.1. `GraphQlTester` + +`GraphQlTester`定义了一个工作流,用于测试 GraphQL 请求,该工作流具有以下优点: + +* 在响应中的“errors”键下验证没有意外错误。 + +* 在响应中的“数据”键下进行解码。 + +* 使用 JSONPath 来解码响应的不同部分。 + +* 测试订阅。 + +要创建`GraphQlTester`,只需要一个`GraphQlService`,并且不需要传输: + +``` +GraphQlSource graphQlSource = GraphQlSource.builder() + .schemaResources(...) + .runtimeWiringConfigurer(...) + .build(); + +GraphQlService graphQlService = new ExecutionGraphQlService(graphQlSource); + +GraphQlTester graphQlTester = GraphQlTester.builder(graphQlService).build(); + +``` + +### [](#testing-webgraphqltester)8.2. `WebGraphQlTester` + +`WebGraphQlTester`扩展`GraphQlTester`以添加特定于[网络传输](#web-transports)的工作流和配置,并且它始终验证 GraphQL HTTP 响应是 200(OK)。 + +要创建`WebGraphQlTester`,你需要以下输入之一: + +* `WebTestClient`——作为 HTTP 客户机执行请求,或者针对没有服务器的[HTTP](#web-http)处理程序,或者针对活动服务器。 + +* `WebGraphQlHandler`——通过[网络拦截](#web-interception)和[WebSocket](#web-websocket)处理程序都使用的[网络拦截](#web-interception)链执行请求,这实际上是在没有 Web 框架的情况下进行的测试。使用这个的一个原因是[订阅](#testing-subscriptions)。 + +对于没有服务器的 Spring WebFlux,你可以指向你的 Spring 配置: + +``` +ApplicationContext context = ... ; + +WebTestClient client = + WebTestClient.bindToApplicationContext(context) + .configureClient() + .baseUrl("/graphql") + .build(); + +WebGraphQlTester tester = WebGraphQlTester.builder(client).build(); + +``` + +对于没有服务器的 Spring MVC,使用`MockMvcWebTestClient`的方法是相同的: + +``` +WebApplicationContext context = ... ; + +WebTestClient client = + MockMvcWebTestClient.bindToApplicationContext(context) + .configureClient() + .baseUrl("/graphql") + .build(); + +WebGraphQlTester tester = WebGraphQlTester.builder(client).build(); + +``` + +对运行中的实时服务器进行测试: + +``` +WebTestClient client = + WebTestClient.bindToServer() + .baseUrl("http://localhost:8080/graphql") + .build(); + +WebGraphQlTester tester = WebGraphQlTester.builder(client).build(); + +``` + +`WebGraphQlTester`支持设置 HTTP 请求标头和访问 HTTP 响应标头。这对于检查或设置与安全性相关的标题可能很有用。 + +``` +this.graphQlTester.queryName("{ myQuery }") + .httpHeaders(headers -> headers.setBasicAuth("rob", "...")) + .execute() + .httpHeadersSatisfy(headers -> { + // check response headers + }) + .path("myQuery.field1").entity(String.class).isEqualTo("value1") + .path("myQuery.field2").entity(String.class).isEqualTo("value2"); + +``` + +你还可以在构建器级别设置默认的请求标题: + +``` +WebGraphQlTester tester = WebGraphQlTester.builder(client) + .defaultHttpHeaders(headers -> headers.setBasicAuth("rob", "...")) + .build(); + +``` + +### [](#testing-queries)8.3. 查询 + +下面是一个使用[JsonPath](https://github.com/json-path/JsonPath)提取 GraphQL 响应中所有发布版本的示例查询测试。 + +``` +String query = "{" + + " project(slug:\"spring-framework\") {" + + " releases {" + + " version" + + " }"+ + " }" + + "}"; + +graphQlTester.query(query) + .execute() + .path("project.releases[*].version") + .entityList(String.class) + .hasSizeGreaterThan(1); + +``` + +JSONPath 是相对于响应的“数据”部分的。 + +还可以在 Classpath 上的`"graphql/"`下创建扩展名为`.graphql`或`.gql`的查询文件,并通过文件名引用它们。例如,给定一个名为`projectReleases.graphql`in`src/main/resources/graphql`的文件,其内容如下: + +``` +query projectReleases($slug: ID!) { + project(slug: $slug) { + releases { + version + } + } +} +``` + +你可以编写相同的测试,如下所示: + +``` +graphQlTester.queryName("projectReleases") (1) + .variable("slug", "spring-framework") (2) + .execute() + .path("project.releases[*].version") + .entityList(String.class) + .hasSizeGreaterThan(1); + +``` + +|**1**|请参阅名为“ProjectReleases”的文件中的查询。| +|-----|-------------------------------------------------------| +|**2**|设置`slug`变量。| + +| |IntelliJ 的“JS GraphQL”插件支持带有代码补全功能的 GraphQL 查询文件。| +|---|---------------------------------------------------------------------------------------| + +### [](#testing-errors)8.4. 错误 + +当响应中的“错误”键下有错误时,验证将不会成功。 + +如果有必要忽略错误,请使用错误过滤器`Predicate`: + +``` +graphQlTester.query(query) + .execute() + .errors() + .filter(error -> ...) + .verify() + .path("project.releases[*].version") + .entityList(String.class) + .hasSizeGreaterThan(1); + +``` + +错误筛选器可以全局注册并应用于所有测试: + +``` +WebGraphQlTester graphQlTester = WebGraphQlTester.builder(client) + .errorFilter(error -> ...) + .build(); + +``` + +或者预期会出现错误,与`filter`相反,如果响应中不存在断言错误,则抛出该断言错误: + +``` +graphQlTester.query(query) + .execute() + .errors() + .expect(error -> ...) + .verify() + .path("project.releases[*].version") + .entityList(String.class) + .hasSizeGreaterThan(1); + +``` + +或直接检查所有错误,并将其标记为已过滤的: + +``` +graphQlTester.query(query) + .execute() + .errors() + .satisfy(errors -> { + // ... + }); + +``` + +如果请求没有任何响应数据(例如,突变),请使用`executeAndVerify`而不是`execute`来验证响应中没有错误: + +``` +graphQlTester.query(query).executeAndVerify(); + +``` + +### [](#testing-subscriptions)8.5. 订阅 + +`executeSubscription`方法定义了一个特定于订阅的工作流,该工作流将返回一个响应流,而不是单个响应。 + +要测试订阅,你可以使用`GraphQlTester`创建`GraphQlService`,它直接调用`graphql.GraphQL`,并返回一个响应流: + +``` +GraphQlService service = ... ; + +GraphQlTester graphQlTester = GraphQlTester.builder(service).build(); + +Flux result = graphQlTester.query("subscription { greetings }") + .executeSubscription() + .toFlux("greetings", String.class); // decode each response + +``` + +来自 Project Reactor 的对于验证流是有用的: + +``` +Flux result = graphQlTester.query("subscription { greetings }") + .executeSubscription() + .toFlux("greetings", String.class); + +StepVerifier.create(result) + .expectNext("Hi") + .expectNext("Bonjour") + .expectNext("Hola") + .verifyComplete(); + +``` + +要测试[网络拦截](#web-interception)链,你可以使用`WebGraphQlHandler`创建`WebGraphQlTester`: + +``` +GraphQlService service = ... ; + +WebGraphQlHandler handler = WebGraphQlHandler.builder(service) + .interceptor((input, next) -> next.handle(input)) + .build(); + +WebGraphQlTester graphQlTester = WebGraphQlTester.builder(handler).build(); + +``` + +目前, Spring for GraphQL 不支持使用 WebSocket 客户端进行测试,并且它不能用于在 WebSocket 请求上对 GraphQL 进行集成测试。 + +## [](#samples)9. 样本 + +Spring 对于 GraphQL 存储库,针对各种场景包含[示例应用程序](https://github.com/spring-projects/spring-graphql/tree/main/samples)。 + +你可以通过克隆此存储库并从你的 IDE 中运行主应用程序类,或者通过在命令行中键入以下内容来运行这些应用程序: + +``` +$ ./gradlew :samples:{sample-directory-name}:bootRun +``` \ No newline at end of file diff --git a/docs/spring-security/community.md b/docs/spring-security/community.md new file mode 100644 index 0000000000000000000000000000000000000000..c7d448915d38ed6dc7d197e5aa896fbdfbcef0c3 --- /dev/null +++ b/docs/spring-security/community.md @@ -0,0 +1,31 @@ +# Spring 安全共同体 + +欢迎来到 Spring 安全社区!本节讨论如何充分利用我们庞大的社区。 + +## 获得帮助 + +如果你在安全方面需要帮助,我们会提供帮助。以下是获得帮助的一些最佳方式: + +* 通读这份文件。 + +* 试试我们的许多[示例应用程序](samples.html#samples)中的一个。 + +* 用`spring-security`标记在[https://stackoverflow.com](https://stackoverflow.com/questions/tagged/spring-security)上提问。 + +* 在[https://github.com/spring-projects/spring-security/issues](https://github.com/spring-projects/spring-security/issues)上报告错误和增强请求 + +## 参与 + +我们欢迎你参与 Spring 安全项目。有很多方法可以帮助你,包括回答有关堆栈溢出的问题,编写新代码,改进现有代码,协助编写文档,开发示例或教程,报告错误,或者只是提出建议。有关更多信息,请参见我们的[贡献](https://github.com/spring-projects/spring-security/blob/main/CONTRIBUTING.adoc)文档。 + +## 源代码 + +你可以在 GitHub 上找到 Spring Security 的源代码,网址为[https://github.com/spring-projects/spring-security/](https://github.com/spring-projects/spring-security/) + +## Apache 2 许可证 + +Spring 安全性是在[Apache 2.0 license](https://www.apache.org/licenses/LICENSE-2.0.html)下发布的开源软件。 + +## 社交媒体 + +你可以在 Twitter 上关注[@SpringSecurity](https://twitter.com/SpringSecurity)和[Spring Security team](https://twitter.com/SpringSecurity/lists/team),了解最新消息。你还可以跟踪[@SpringCentral](https://twitter.com/SpringCentral),以了解整个 Spring 投资组合的最新情况。 \ No newline at end of file diff --git a/docs/spring-security/features-authentication-password-storage.md b/docs/spring-security/features-authentication-password-storage.md new file mode 100644 index 0000000000000000000000000000000000000000..faecef658040c8984b494aa9069faa6604590b12 --- /dev/null +++ b/docs/spring-security/features-authentication-password-storage.md @@ -0,0 +1,445 @@ +# 密码存储 + +Spring Security 的`PasswordEncoder`接口用于执行密码的单向转换,以允许安全地存储密码。给定`PasswordEncoder`是单向转换,当密码转换需要双向(即存储用于对数据库进行身份验证的凭据)时,并不打算这样做。通常`PasswordEncoder`用于存储需要与用户在身份验证时提供的密码进行比较的密码。 + +## 密码存储历史 + +多年来,存储密码的标准机制一直在发展。在开始的时候,密码是用纯文本格式存储的。这些密码被认为是安全的,因为数据存储中的密码被保存在访问它所需的凭据中。然而,恶意用户能够通过 SQL 注入等攻击,找到获取大量用户名和密码的“数据转储”的方法。随着越来越多的用户证书成为公共安全专家意识到,我们需要做更多的工作来保护用户的密码。 + +然后鼓励开发人员在通过 SHA-256 等单向散列运行密码后存储密码。当用户试图进行身份验证时,会将散列密码与他们键入的密码的散列进行比较。这意味着系统只需要存储密码的单向散列。如果发生了漏洞,那么只会公开密码的单向散列。由于散列是一种方式,并且在计算上很难猜测给定散列的密码,因此不值得花费精力来计算系统中的每个密码。为了击败这个新系统,恶意用户决定创建名为[彩虹桌](https://en.wikipedia.org/wiki/Rainbow_table)的查找表。他们不是每次都猜测每个密码,而是计算一次密码并将其存储在查找表中。 + +为了降低 Rainbow 表的有效性,鼓励开发人员使用盐渍密码。不再只使用密码作为散列函数的输入,而是为每个用户的密码生成随机字节(称为 salt)。SALT 和用户的密码将通过散列函数运行,该函数将生成一个唯一的散列。这种盐将与用户的密码一起以明文形式存储。然后,当用户试图进行身份验证时,散列密码将与存储的盐的散列和他们输入的密码进行比较。独特的盐意味着彩虹表格不再有效,因为每种盐和密码组合的散列值都不同。 + +在现代,我们意识到加密散列(如 SHA-256)不再安全。原因在于,有了现代硬件,我们每秒钟可以进行数十亿次散列计算。这意味着我们可以轻松地单独破解每个密码。 + +现在鼓励开发人员利用自适应单向功能来存储密码。具有自适应单向功能的密码的验证是有意的资源密集型的(如 CPU、内存等)。一个自适应的单向功能允许配置一个“工作因素”,可以随着硬件变得更好而增长。建议将“工作因子”调整为在系统上验证密码所需的时间约为 1 秒。这样做的代价是让攻击者很难破解密码,但也不会因为代价太高而给自己的系统带来过大的负担。 Spring 安全性已经尝试为“工作因素”提供了一个很好的起点,但是鼓励用户为自己的系统定制“工作因素”,因为系统之间的性能会有很大的差异。应该使用的自适应单向函数的示例包括[bcrypt](#authentication-password-storage-bcrypt)、[PBKDF2](#authentication-password-storage-pbkdf2)、[scrypt](#authentication-password-storage-scrypt)和[argon2](#authentication-password-storage-argon2)。 + +由于自适应单向函数是故意的资源密集型的,因此为每个请求验证用户名和密码将大大降低应用程序的性能。 Spring 安全性(或任何其他库)不能做任何事情来加速密码的验证,因为安全性是通过使验证资源密集型来获得的。鼓励用户将长期凭据(即用户名和密码)交换为短期凭据(即会话、OAuth 令牌等)。短期凭据可以被快速验证,而不会损失任何安全性。 + +## PasswordEncoder + +在 Spring Security5.0 之前,默认的`PasswordEncoder`是`NoOpPasswordEncoder`,它需要纯文本密码。基于[密码历史记录](#authentication-password-storage-history)部分,你可能认为默认的`PasswordEncoder`现在类似于`bcryptpasswordencoder`。然而,这忽略了三个现实世界中的问题: + +* 有许多应用程序使用旧的密码编码,无法轻松地进行迁移。 + +* 密码存储的最佳实践将再次改变。 + +* 作为一种框架 Spring,安全不能频繁地进行破坏更改 + +Spring 安全性引入了`DelegatingPasswordEncoder`,它通过以下方式解决了所有问题: + +* 确保使用当前的密码存储建议对密码进行编码 + +* 允许验证现代和遗留格式的密码。 + +* 允许在将来升级编码 + +你可以使用`PasswordEncoderFactories`轻松地构造`DelegatingPasswordEncoder`的实例。 + +例 1。创建默认的代理 PasswordEncoder + +爪哇 + +``` +PasswordEncoder passwordEncoder = + PasswordEncoderFactories.createDelegatingPasswordEncoder(); +``` + +Kotlin + +``` +val passwordEncoder: PasswordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder() +``` + +或者,你可以创建自己的自定义实例。例如: + +例 2。创建自定义代理 PasswordEncoder + +爪哇 + +``` +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); +``` + +Kotlin + +``` +val idForEncode = "bcrypt" +val encoders: MutableMap = mutableMapOf() +encoders[idForEncode] = BCryptPasswordEncoder() +encoders["noop"] = NoOpPasswordEncoder.getInstance() +encoders["pbkdf2"] = Pbkdf2PasswordEncoder() +encoders["scrypt"] = SCryptPasswordEncoder() +encoders["sha256"] = StandardPasswordEncoder() + +val passwordEncoder: PasswordEncoder = DelegatingPasswordEncoder(idForEncode, encoders) +``` + +### 密码存储格式 + +密码的一般格式是: + +例 3。passwordencoder 存储格式 + +``` +{id}encodedPassword +``` + +使得`id`是用于查找应该使用`PasswordEncoder`的标识符,而`encodedPassword`是所选`PasswordEncoder`的原始编码密码。`id`必须位于密码的开头,以`{`开头,以`}`结尾。如果不能找到`id`,则`id`将为空。例如,以下可能是使用不同的`id`编码的密码列表。所有的原始密码都是“密码”。 + +例 4。代理 PasswordEncoder 编码的密码示例 + +``` +{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`。| +|**3**|第三个密码的`PasswordEncoder`ID 为`pbkdf2`,编码密码为`5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc`。| +|**4**|第四个密码的`PasswordEncoder`ID 为`scrypt`,编码密码为`$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc=`,当与之匹配时,它将委托给`ScryptPassWordEncoder`。| +|**5**|最终的密码将具有`PasswordEncoder`的 ID`sha256`和`97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0`的编码密码。
匹配时,它将委托给`StandardPasswordEncoder`。| + +| |一些用户可能担心存储格式是为潜在的黑客提供的。
这不是一个问题,因为密码的存储不依赖于算法是一个秘密。
此外,大多数格式都很容易被攻击者在没有前缀的情况下发现。
例如,bcrypt 密码通常以`$2a$`开头。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### 密码编码 + +传递到构造函数的`idForEncode`确定将使用哪个`PasswordEncoder`来编码密码。在我们上面构造的`DelegatingPasswordEncoder`中,这意味着编码的结果`password`将被委托给`BCryptPasswordEncoder`,并以`{bcrypt}`作为前缀。最终的结果会是: + +例 5。delegatingPassWordEncoder 编码示例 + +``` +{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG +``` + +### 密码匹配 + +匹配是基于`{id}`和`id`到构造函数中提供的`PasswordEncoder`的映射完成的。我们在[密码存储格式](#authentication-password-storage-dpe-format)中的示例提供了如何实现这一点的工作示例。默认情况下,使用密码调用`matches(CharSequence, String)`并调用未映射(包括空 ID)的`id`的结果将导致`IllegalArgumentException`。可以使用`DelegatingPasswordEncoder.setDefaultPasswordEncoderForMatches(PasswordEncoder)`定制此行为。 + +通过使用`id`,我们可以在任何密码编码上进行匹配,但是可以使用最现代的密码编码来编码密码。这一点很重要,因为与加密不同,密码散列的设计使得没有简单的方法来恢复明文。由于无法恢复明文,因此很难迁移密码。虽然迁移`NoOpPasswordEncoder`对用户来说很简单,但我们选择在默认情况下包含它,以使入门体验更简单。 + +### 入门经验 + +如果你正在组装一个演示或示例,那么花时间对用户的密码进行散列会有点麻烦。有方便的机制使这一点更容易,但这仍然不打算用于生产。 + +示例 6.与 DefaultPassWordEncoder 示例 + +爪哇 + +``` +User user = User.withDefaultPasswordEncoder() + .username("user") + .password("password") + .roles("user") + .build(); +System.out.println(user.getPassword()); +// {bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG +``` + +Kotlin + +``` +val user = User.withDefaultPasswordEncoder() + .username("user") + .password("password") + .roles("user") + .build() +println(user.password) +// {bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG +``` + +如果要创建多个用户,还可以重用构建器。 + +示例 7.WithDefaultPassWordEncoder 重用构建器 + +爪哇 + +``` +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(); +``` + +Kotlin + +``` +val users = User.withDefaultPasswordEncoder() +val user = users + .username("user") + .password("password") + .roles("USER") + .build() +val admin = users + .username("admin") + .password("password") + .roles("USER", "ADMIN") + .build() +``` + +这会对存储的密码进行散列,但这些密码仍会在内存和编译后的源代码中公开。因此,对于生产环境来说,它仍然不被认为是安全的。对于生产,你应该[在外部散列密码](#authentication-password-storage-boot-cli)。 + +### 用 Spring boot cli 编码 + +正确编码密码的最简单方法是使用[Spring Boot CLI](https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-cli.html)。 + +例如,下面将对`password`的密码进行编码,以便与[PasswordEncoder](#authentication-password-storage-dpe)一起使用: + +例 8。 Spring 启动 CLI EncodePassword 示例 + +``` +spring encodepassword password +{bcrypt}$2a$10$X5wFBtLrL/kHcmrOGGTrGufsBX8CJ0WpQpF3pgeuxBB/H73BK1DW6 +``` + +### 故障排除 + +当存储的密码之一没有[密码存储格式](#authentication-password-storage-dpe-format)中所述的 ID 时,会发生以下错误。 + +``` +java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null" + at org.springframework.security.crypto.password.DelegatingPasswordEncoder$UnmappedIdPasswordEncoder.matches(DelegatingPasswordEncoder.java:233) + at org.springframework.security.crypto.password.DelegatingPasswordEncoder.matches(DelegatingPasswordEncoder.java:196) +``` + +解决该错误的最简单方法是切换到显式提供你的密码所使用的`PasswordEncoder`。解决此问题的最简单方法是弄清楚当前如何存储密码,并显式地提供正确的`PasswordEncoder`。 + +如果你正在从 Spring Security4.2.x 迁移,则可以通过[公开`NoOpPasswordEncoder` Bean](# 身份验证-密码-存储-配置)来恢复到以前的行为。 + +或者,你可以在所有密码前加上正确的 ID,然后继续使用`DelegatingPasswordEncoder`。例如,如果你正在使用 bcrypt,那么你将从以下内容迁移密码: + +``` +$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG +``` + +to + +``` +{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG +``` + +对于映射的完整列表,请参考[PasswordEncoderFactories](https://docs.spring.io/spring-security/site/docs/5.0.x/api/org/springframework/security/crypto/factory/PasswordEncoderFactories.html)上的 爪哇doc。 + +## BCryptPasswordEncoder + +`BCryptPasswordEncoder`实现使用广泛支持的[bcrypt](https://en.wikipedia.org/wiki/Bcrypt)算法来散列密码。为了使其更好地抵抗密码破解,bcrypt 故意放慢速度。像其他自适应单向功能一样,它应该调整为在系统上验证密码所需的时间约为 1 秒。`BCryptPasswordEncoder`的默认实现使用了在[bcryptpasswordencoder](https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/crypto/bcrypt/BCryptPasswordEncoder.html)的 爪哇doc 中提到的强度 10。我们鼓励你在自己的系统上调整和测试强度参数,这样验证密码大约需要 1 秒钟。 + +例 9。bcryptpasswordencoder + +爪哇 + +``` +// Create an encoder with strength 16 +BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(16); +String result = encoder.encode("myPassword"); +assertTrue(encoder.matches("myPassword", result)); +``` + +Kotlin + +``` +// Create an encoder with strength 16 +val encoder = BCryptPasswordEncoder(16) +val result: String = encoder.encode("myPassword") +assertTrue(encoder.matches("myPassword", result)) +``` + +## Argon2Passwordencoder + +`Argon2PasswordEncoder`实现使用[Argon2](https://en.wikipedia.org/wiki/Argon2)算法来散列密码。Argon2 是[密码散列竞赛](https://en.wikipedia.org/wiki/Password_Hashing_Competition)的获胜者。为了击败定制硬件上的密码破解,Argon2 是一种故意缓慢的算法,需要大量内存。像其他自适应单向功能一样,它应该调整为在系统上验证密码所需的时间约为 1 秒。`Argon2PasswordEncoder`的当前实现需要 bouncycastle。 + +例 10。Argon2Passwordencoder + +爪哇 + +``` +// Create an encoder with all the defaults +Argon2PasswordEncoder encoder = new Argon2PasswordEncoder(); +String result = encoder.encode("myPassword"); +assertTrue(encoder.matches("myPassword", result)); +``` + +Kotlin + +``` +// Create an encoder with all the defaults +val encoder = Argon2PasswordEncoder() +val result: String = encoder.encode("myPassword") +assertTrue(encoder.matches("myPassword", result)) +``` + +## PBKDF2PASSWORDENCODER + +`Pbkdf2PasswordEncoder`实现使用[PBKDF2](https://en.wikipedia.org/wiki/PBKDF2)算法来散列密码。为了击败密码破解,PBKDF2 是一种故意缓慢的算法。像其他自适应单向功能一样,它应该调整为在系统上验证密码所需的时间约为 1 秒。当需要 FIPS 认证时,该算法是一个很好的选择。 + +例 11。PBKDF2PASSWORDENCODER + +爪哇 + +``` +// Create an encoder with all the defaults +Pbkdf2PasswordEncoder encoder = new Pbkdf2PasswordEncoder(); +String result = encoder.encode("myPassword"); +assertTrue(encoder.matches("myPassword", result)); +``` + +Kotlin + +``` +// Create an encoder with all the defaults +val encoder = Pbkdf2PasswordEncoder() +val result: String = encoder.encode("myPassword") +assertTrue(encoder.matches("myPassword", result)) +``` + +## SCryptPasswordEncoder + +`SCryptPasswordEncoder`实现使用[scrypt](https://en.wikipedia.org/wiki/Scrypt)算法来散列密码。为了击败定制硬件上的密码破解,Scrypt 是一种故意缓慢的算法,需要大量的内存。像其他自适应单向功能一样,它应该调整为在系统上验证密码所需的时间约为 1 秒。 + +例 12。ScryptPassWordEncoder + +爪哇 + +``` +// Create an encoder with all the defaults +SCryptPasswordEncoder encoder = new SCryptPasswordEncoder(); +String result = encoder.encode("myPassword"); +assertTrue(encoder.matches("myPassword", result)); +``` + +Kotlin + +``` +// Create an encoder with all the defaults +val encoder = SCryptPasswordEncoder() +val result: String = encoder.encode("myPassword") +assertTrue(encoder.matches("myPassword", result)) +``` + +## 其他 PasswordEncoders + +还有相当数量的其他`PasswordEncoder`实现完全是为了向后兼容而存在的。它们都被弃用,以表明它们不再被认为是安全的。但是,由于很难迁移现有的遗留系统,因此没有删除它们的计划。 + +## 密码存储配置 + +Spring 安全性默认使用[PasswordEncoder](#authentication-password-storage-dpe)。然而,这可以通过将`PasswordEncoder`公开为 Spring Bean 来定制。 + +如果从 Spring Security4.2.x 迁移,则可以通过公开`NoOpPasswordEncoder` Bean 来恢复到以前的行为。 + +| |恢复到`NoOpPasswordEncoder`不被认为是安全的。
你应该转而使用`DelegatingPasswordEncoder`来支持安全的密码编码。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +例 13。NoopPassWordEncoder + +爪哇 + +``` +@Bean +public static PasswordEncoder passwordEncoder() { + return NoOpPasswordEncoder.getInstance(); +} +``` + +XML + +``` + +``` + +Kotlin + +``` +@Bean +fun passwordEncoder(): PasswordEncoder { + return NoOpPasswordEncoder.getInstance(); +} +``` + +| |XML 配置要求`NoOpPasswordEncoder` Bean 名称为`passwordEncoder`。| +|---|---------------------------------------------------------------------------------------| + +## 更改密码配置 + +大多数允许用户指定密码的应用程序也需要更新该密码的功能。 + +[一个众所周知的更改密码的 URL](https://w3c.github.io/webappsec-change-password-url/)表示一种机制,通过这种机制,密码管理器可以发现给定应用程序的密码更新端点。 + +你可以配置 Spring 安全性以提供此发现端点。例如,如果应用程序中的更改密码端点是`/change-password`,那么你可以这样配置 Spring 安全性: + +例 14。默认更改密码端点 + +Java + +``` +http + .passwordManagement(Customizer.withDefaults()) +``` + +XML + +``` + +``` + +Kotlin + +``` +http { + passwordManagement { } +} +``` + +然后,当密码管理器导航到`/.well-known/change-password`时, Spring 安全性将重定向你的端点,`/change-password`。 + +或者,如果你的端点不是`/change-password`,也可以这样指定: + +例 15。更改密码端点 + +Java + +``` +http + .passwordManagement((management) -> management + .changePasswordPage("/update-password") + ) +``` + +XML + +``` + +``` + +Kotlin + +``` +http { + passwordManagement { + changePasswordPage = "/update-password" + } +} +``` + +通过上述配置,当密码管理器导航到`/.well-known/change-password`时, Spring Security 将重定向到`/update-password`。 \ No newline at end of file diff --git a/docs/spring-security/features-authentication.md b/docs/spring-security/features-authentication.md new file mode 100644 index 0000000000000000000000000000000000000000..b19d4e6eb805b8bedb141349660479f3870a697b --- /dev/null +++ b/docs/spring-security/features-authentication.md @@ -0,0 +1,5 @@ +# 认证 + +Spring 安全性为[认证](https://en.wikipedia.org/wiki/Authentication)提供了全面的支持。身份验证是指我们如何验证试图访问特定资源的人的身份。验证用户身份的一种常见方法是要求用户输入用户名和密码。一旦进行了身份验证,我们就知道了身份并可以执行授权。 + +Spring 安全性为用户身份验证提供了内置支持。本节专门介绍在 Servlet 和 WebFlux 环境中都适用的通用身份验证支持。有关每个堆栈支持什么的详细信息,请参阅[Servlet](../../servlet/authentication/index.html#servlet-authentication)和 WebFlux 的身份验证部分。 \ No newline at end of file diff --git a/docs/spring-security/features-exploits-csrf.md b/docs/spring-security/features-exploits-csrf.md new file mode 100644 index 0000000000000000000000000000000000000000..a385b8999147d62ff99d47ab3c4e3fc3cdbb6273 --- /dev/null +++ b/docs/spring-security/features-exploits-csrf.md @@ -0,0 +1,296 @@ +# 跨站点请求伪造 + +Spring 为防范[跨站点请求伪造](https://en.wikipedia.org/wiki/Cross-site_request_forgery)攻击提供了全面的支持。在以下几节中,我们将探讨: + +* [什么是 CSRF 攻击?](#csrf-explained) + +* [防范 CSRF 攻击](#csrf-protection) + +* [CSRF 考虑因素](#csrf-considerations) + +| |这部分的文档讨论了 CSRF 保护的一般主题。
关于基于[servlet](../../servlet/exploits/csrf.html#servlet-csrf)和[WebFlux](../../reactive/exploits/csrf.html#webflux-csrf)的应用程序的 CSRF 保护的具体信息,请参阅相关章节。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +## 什么是 CSRF 攻击? + +理解 CSRF 攻击的最好方法是看一个具体的例子。 + +假设你的银行的网站提供了一种表单,允许将当前登录的用户的资金转移到另一个银行帐户。例如,传输表单可能看起来像: + +例 1。转移形式 + +``` +
+ + + + +
+``` + +相应的 HTTP 请求可能看起来像: + +例 2。传输 HTTP 请求 + +``` +POST /transfer HTTP/1.1 +Host: bank.example.com +Cookie: JSESSIONID=randomid +Content-Type: application/x-www-form-urlencoded + +amount=100.00&routingNumber=1234&account=9876 +``` + +现在,假装你对银行的网站进行了认证,然后在不登出的情况下,访问一个邪恶的网站。邪恶网站包含一个 HTML 页面,其形式如下: + +例 3。邪恶转移形式 + +``` +
+ + + + +
+``` + +你想赢钱,所以你点击提交按钮。在此过程中,你无意中向恶意用户转移了 100 美元。发生这种情况的原因是,虽然邪恶的网站无法看到你的 cookie,但与你的银行相关的 cookie 仍会随请求一起发送。 + +最糟糕的是,整个过程都可以使用 JavaScript 实现自动化。这意味着你甚至不需要点击这个按钮。此外,当访问一个受[XSS attack](https://www.owasp.org/index.php/Cross-site_Scripting_(XSS)影响的诚实网站时,也很容易发生这种情况。那么,我们如何保护我们的用户免受此类攻击呢? + +## 防范 CSRF 攻击 + +可能发生 CSRF 攻击的原因是,来自受害者网站的 HTTP 请求和来自攻击者网站的请求完全相同。这意味着,无法拒绝来自邪恶网站的请求,也无法允许来自该行网站的请求。为了防止 CSRF 攻击,我们需要确保请求中有邪恶站点无法提供的内容,因此我们可以区分这两个请求。 + +Spring 提供了两种机制来防止 CSRF 攻击: + +* the[同步器令牌模式](#csrf-protection-stp) + +* 在会话cookie 上指定[Samesite 属性](#csrf-protection-ssa) + +| |这两种保护都要求[安全的方法必须是幂等的。](#csrf-protection-idempotent)| +|---|--------------------------------------------------------------------------------------------| + +### 安全的方法必须是幂等的。 + +为了使针对 CSRF 的[要么保护](#csrf-protection)工作,应用程序必须确保[“安全的”HTTP 方法是幂等的](https://tools.ietf.org/html/rfc7231#section-4.2.1)。这意味着使用 http 方法`GET`、`HEAD`、`OPTIONS`和`TRACE`的请求不应改变应用程序的状态。 + +### 同步器令牌模式 + +防止 CSRF 攻击的主要和最全面的方法是使用[同步器令牌模式](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#synchronizer-token-pattern)。这种解决方案是为了确保每个 HTTP 请求,除了我们的会话cookie 之外,还必须在 HTTP 请求中存在一个安全的随机生成值,称为 CSRF 令牌。 + +当提交 HTTP 请求时,服务器必须查找预期的 CSRF 令牌,并将其与 HTTP 请求中的实际 CSRF 令牌进行比较。如果值不匹配,则应拒绝 HTTP 请求。 + +这一工作的关键是,实际的 CSRF 令牌应该位于 HTTP 请求的一部分,该部分不会被浏览器自动包含。例如,在 HTTP 参数或 HTTP 报头中要求实际的 CSRF 令牌,可以防止 CSRF 攻击。在 Cookie 中要求实际的 CSRF 令牌是不起作用的,因为浏览器会自动将 Cookie 包含在 HTTP 请求中。 + +我们可以放松预期,只需要每个更新应用程序状态的 HTTP 请求的实际 CSRF 令牌。要使其工作,我们的应用程序必须确保[安全的 HTTP 方法是幂等的](#csrf-protection-idempotent)。这提高了可用性,因为我们希望允许使用外部站点的链接来链接到我们的网站。此外,我们不想在 HTTP GET 中包含随机令牌,因为这可能导致令牌泄漏。 + +让我们来看看使用 Synchronizer 令牌模式时[我们的例子](#csrf-explained)将如何更改。假设实际的 CSRF 令牌需要位于一个名为`_csrf`的 HTTP 参数中。我们的申请的转移表看起来是这样的: + +例 4。同步器令牌形式 + +``` +
+ + + + + +
+``` + +表单现在包含一个隐藏的输入,其值为 CSRF 令牌。外部站点无法读取 CSRF 令牌,因为相同的源策略确保邪恶站点无法读取响应。 + +相应的转移资金的 HTTP 请求将如下所示: + +例 5。同步器令牌请求 + +``` +POST /transfer HTTP/1.1 +Host: bank.example.com +Cookie: JSESSIONID=randomid +Content-Type: application/x-www-form-urlencoded + +amount=100.00&routingNumber=1234&account=9876&_csrf=4bfd1575-3ad1-4d21-96c7-4ef2d9f86721 +``` + +你将注意到,HTTP 请求现在包含带有安全随机值的`_csrf`参数。当服务器将实际的 CSRF 令牌与预期的 CSRF 令牌进行比较时,Evil 网站将无法为`_csrf`参数(必须在 Evil 网站上显式提供)提供正确的值,并且传输将失败。 + +### Samesite 属性 + +防止[CSRF 攻击](#csrf)的一种新兴方法是在 cookie 上指定[Samesite 属性](https://tools.ietf.org/html/draft-west-first-party-cookies)。服务器在设置 cookie 时可以指定`SameSite`属性,以指示当来自外部站点时不应发送 cookie。 + +| |Spring 安全性并不直接控制会话cookie 的创建,因此它不提供对 Samesite 属性的支持。[Spring Session](https://spring.io/projects/spring-session)在基于 Servlet 的应用程序中提供了对`SameSite`属性的支持。
Spring 框架的[CookiewebessionDresolver](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/server/session/CookieWebSessionIdResolver.html)在基于 WebFlux 的应用程序中提供了对`SameSite`属性的开箱即用支持。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +例如,具有`SameSite`属性的 HTTP 响应头可能看起来如下所示: + +例 6。Samesite HTTP 响应 + +``` +Set-Cookie: JSESSIONID=randomid; Domain=bank.example.com; Secure; HttpOnly; SameSite=Lax +``` + +`SameSite`属性的有效值是: + +* `Strict`-当指定时,来自[same-site](https://tools.ietf.org/html/draft-west-first-party-cookies-07#section-2.1)的任何请求都将包括 cookie。否则,Cookie 将不会包含在 HTTP 请求中。 + +* `Lax`-当指定的 cookie 来自[same-site](https://tools.ietf.org/html/draft-west-first-party-cookies-07#section-2.1)或当请求来自顶级导航和[方法是幂等的](#csrf-protection-idempotent)时将被发送。否则,Cookie 将不会包含在 HTTP 请求中。 + +让我们来看看如何使用[我们的例子](#csrf-explained)属性来保护`SameSite`。银行应用程序可以通过在会话cookie 上指定`SameSite`属性来防止 CSRF。 + +通过在我们的会话cookie 上设置`SameSite`属性,浏览器将继续发送来自银行网站的请求的`JSESSIONID`cookie。但是,浏览器将不再发送带有来自 Evil 网站的传输请求的`JSESSIONID`cookie。由于会话在来自邪恶网站的传输请求中不再存在,因此应用程序受到 CSRF 攻击的保护。 + +在使用`SameSite`属性来防止 CSRF 攻击时,应该注意一些重要的[注意事项](https://tools.ietf.org/html/draft-west-first-party-cookies-07#section-5)属性。 + +将`SameSite`属性设置为`Strict`提供了更强的防御能力,但可能会使用户感到困惑。考虑一个用户保持登录到托管在[https://social.example.com](https://social.example.com)的社交媒体网站。用户在[https://email.example.org](https://email.example.org)处收到一封电子邮件,其中包括一个到该社交媒体网站的链接。如果用户点击了该链接,他们理所当然地希望得到该社交媒体网站的认证。但是,如果`SameSite`属性是`Strict`,则不会发送 cookie,因此不会对用户进行身份验证。 + +| |通过实现[gh-7537](https://github.com/spring-projects/spring-security/issues/7537),可以提高`SameSite`对 CSRF 攻击的防护能力和可用性。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +另一个明显的考虑因素是,为了让`SameSite`属性保护用户,浏览器必须支持`SameSite`属性。大多数现代浏览器都[支持 Samesite 属性](https://developer.mozilla.org/en-US/docs/Web/HTTP/headers/Set-Cookie#Browser_compatibility)。然而,仍在使用的旧浏览器可能不会。 + +出于这个原因,通常建议使用`SameSite`属性作为深度防御,而不是针对 CSRF 攻击的唯一保护。 + +## 何时使用 CSRF 保护 + +你什么时候应该使用 CSRF 保护?我们的建议是,对于普通用户可能通过浏览器处理的任何请求,使用 CSRF 保护。如果你只是创建一个由非浏览器客户端使用的服务,那么你可能希望禁用 CSRF 保护。 + +### CSRF 保护和 JSON + +一个常见的问题是“我需要保护 JavaScript 发出的 JSON 请求吗?”简短的回答是,这要看情况而定。但是,你必须非常小心,因为有 CSRF 漏洞可能会影响 JSON 请求。例如,恶意用户可以创建[使用以下表单使用 JSON 的 CSRF](http://blog.opensecurityresearch.com/2012/02/json-csrf-with-parameter-padding.html): + +例 7。使用 JSON 表单的 CSRF + +``` +
+ + +
+``` + +这将产生以下 JSON 结构 + +例 8。CSRF 与 JSON 请求 + +``` +{ "amount": 100, +"routingNumber": "evilsRoutingNumber", +"account": "evilsAccountNumber", +"ignore_me": "=test" +} +``` + +如果应用程序没有验证 Content-type,那么它就会受到此攻击。根据设置,验证 Content-type 的 Spring MVC 应用程序仍然可以通过将 URL 后缀更新为`.json`来利用该漏洞,如下所示: + +例 9。使用 JSON Spring MVC 表单的 CSRF + +``` +
+ + +
+``` + +### CSRF 和无状态浏览器应用程序 + +如果我的应用程序是无状态的,该怎么办?这并不一定意味着你受到了保护。实际上,如果用户不需要在 Web 浏览器中为给定的请求执行任何操作,那么他们很可能仍然容易受到 CSRF 攻击。 + +例如,考虑一个应用程序,它使用一个自定义 Cookie 来进行身份验证,而不是使用 JSessionID。当发生 CSRF 攻击时,将以与我们上一个示例中发送 JSessionID Cookie 相同的方式发送自定义 Cookie 和请求。此应用程序易受 CSRF 攻击。 + +使用基本身份验证的应用程序也容易受到 CSRF 攻击。该应用程序很容易受到攻击,因为浏览器将在任何请求中自动包括用户名和密码,其方式与我们上一个示例中发送的 JSessionID cookie 的方式相同。 + +## CSRF 考虑因素 + +在实施针对 CSRF 攻击的保护时,有几个特殊的考虑因素需要考虑。 + +### 登录 + +为了防止[请求中的伪造日志](https://en.wikipedia.org/wiki/Cross-site_request_forgery#Forging_login_requests),应该保护 HTTP 请求中的日志不受 CSRF 攻击。防止在请求中伪造日志是必要的,这样恶意用户就无法读取受害者的敏感信息。攻击的执行方式如下: + +* 恶意用户使用恶意用户的凭据执行 CSRF 登录。受害者现在被认证为恶意用户。 + +* 然后,恶意用户诱使受害者访问受损网站并输入敏感信息。 + +* 该信息与恶意用户的帐户相关联,因此恶意用户可以使用自己的凭据登录并查看 Vicitim 的敏感信息。 + +确保登录 HTTP 请求不受 CSRF 攻击的一个可能的复杂情况是,用户可能会经历会话超时,这会导致请求被拒绝。会话超时对于那些预计不需要会话才能登录的用户来说是令人惊讶的。有关更多信息,请参见[CSRF 和会话暂停](#csrf-considerations-timeouts)。 + +### 注销 + +为了防止伪造注销请求,应该保护注销 HTTP 请求不受 CSRF 攻击。防止伪造注销请求是必要的,这样恶意用户就无法读取受害者的敏感信息。有关攻击的详细信息,请参阅[这篇博文](https://labs.detectify.com/2017/03/15/loginlogout-csrf-time-to-reconsider/)。 + +确保退出 HTTP 请求不受 CSRF 攻击的一个可能的复杂情况是,用户可能会经历会话超时,这会导致请求被拒绝。会话超时对于那些不期望为了注销而需要会话超时的用户来说是令人惊讶的。有关更多信息,请参见[CSRF and Session Timeouts](#csrf-considerations-timeouts)。 + +### CSRF and Session Timeouts + +通常,期望的 CSRF 令牌存储在会话中。这意味着,一旦会话过期,服务器将找不到预期的 CSRF 令牌并拒绝 HTTP 请求。解决超时问题有多种选择,每种选择都需要权衡利弊。 + +* 减少超时的最佳方法是使用 JavaScript 在表单提交时请求 CSRF 令牌。然后用 CSRF 令牌更新表单并提交表单。 + +* 另一种选择是使用一些 JavaScript,让用户知道他们的会话即将到期。用户可以单击按钮继续并刷新会话。 + +* 最后,预期的 CSRF 令牌可以存储在 Cookie 中。这允许预期的 CSRF 令牌比会话有效。 + + 有人可能会问,为什么预期的 CSRF 令牌在默认情况下没有存储在 Cookie 中。这是因为存在已知的利用漏洞攻击,其中的头(例如,指定 cookie)可以由另一个域设置。这也是 Ruby on Rails[当标题 x-request-with 出现时,不再跳过 CSRF 检查](https://weblog.rubyonrails.org/2011/2/8/csrf-protection-bypass-in-ruby-on-rails/)的原因。有关如何执行此漏洞利用的详细信息,请参见[这个 webappsec.org 线程](https://web.archive.org/web/20210221120355/https://lists.webappsec.org/pipermail/websecurity_lists.webappsec.org/2011-February/007533.html)。另一个缺点是,通过删除状态(即超时),如果令牌受到损害,你将失去强制使其无效的能力。 + +### + +保护多部分请求(文件上传)不受 CSRF 攻击会导致[鸡和蛋](https://en.wikipedia.org/wiki/Chicken_or_the_egg)问题。为了防止 CSRF 攻击的发生,必须读取 HTTP 请求的主体以获得实际的 CSRF 令牌。然而,读取主体意味着文件将被上传,这意味着外部站点可以上传文件。 + +有两个选择使用 CSRF 保护与多部分/形式数据。每一种选择都有其利弊得失。 + +* [将 CSRF 标记放入体内](#csrf-considerations-multipart-body) + +* [在 URL 中放置 CSRF 令牌](#csrf-considerations-multipart-url) + +| |在将 Spring Security 的 CSRF 保护与多部分文件上载集成在一起之前,请确保首先可以在没有 CSRF 保护的情况下进行上传。
关于使用 Spring 的多部分表单的更多信息可以在 Spring 引用的[1.1.11.多部分旋转变压器](https://docs.spring.io/spring/docs/5.2.x/spring-framework-reference/web.html#mvc-multipart)部分和[MultipartFilter Javadoc](https://docs.spring.io/spring/docs/5.2.x/javadoc-api/org/springframework/web/multipart/support/MultipartFilter.html)中找到。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 将 CSRF 标记放入体内 + +第一个选项是在请求主体中包含实际的 CSRF 令牌。通过将 CSRF 令牌放置在主体中,主体将在执行授权之前被读取。这意味着任何人都可以在你的服务器上放置临时文件。但是,只有经过授权的用户才能提交由你的应用程序处理的文件。通常,这是推荐的方法,因为临时文件上传对大多数服务器的影响可以忽略不计。 + +#### 在 URL 中包含 CSRF 令牌 + +如果允许未经授权的用户上传临时文件是不可接受的,那么另一种选择是在表单的 Action 属性中包含预期的 CSRF 令牌作为查询参数。这种方法的缺点是查询参数可能会泄露。更普遍地说,将敏感数据放置在主体或标头内以确保其不会泄漏被认为是最佳实践。其他信息可以在[RFC2616 第 15.1.3 节在 URI 中对敏感信息进行编码](https://www.w3.org/Protocols/rfc2616/rfc2616-sec15.html#sec15.1.3)中找到。 + +#### HiddenHttpMethodFilter + +在某些应用程序中,表单参数可用于覆盖 HTTP 方法。例如,下面的表单可以用来将 HTTP 方法视为`delete`,而不是`post`。 + +例 10。CSRF 隐藏 HTTP 方法表单 + +``` +
+ + +
+``` + +在筛选器中重写 HTTP 方法。这个过滤器必须放在安全部门的支持之前。请注意,覆盖只发生在`post`上,因此这实际上不太可能导致任何实际问题。然而,最好的做法仍然是确保将其置于 Spring Security 的过滤器之前。 \ No newline at end of file diff --git a/docs/spring-security/features-exploits-headers.md b/docs/spring-security/features-exploits-headers.md new file mode 100644 index 0000000000000000000000000000000000000000..92aa00d3191be34cdd0165c7235da8e34cd23ad9 --- /dev/null +++ b/docs/spring-security/features-exploits-headers.md @@ -0,0 +1,270 @@ +# 安全 HTTP 响应标头 + +| |文档的这一部分讨论了安全 HTTP 响应头的一般主题。
关于基于应用程序的安全 HTTP 响应头[servlet](../../servlet/exploits/headers.html#servlet-headers)和[WebFlux](../../reactive/exploits/headers.html#webflux-headers)的特定信息,请参阅相关部分。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +有许多[HTTP 响应标头](https://owasp.org/www-project-secure-headers/#div-headers)可以用来增加 Web 应用程序的安全性。本节专门讨论 Spring Security 提供明确支持的各种 HTTP 响应头。如果需要, Spring 还可以将安全性配置为提供[自定义标头](#headers-custom)。 + +## 默认安全标头 + +| |参考相关章节,了解如何为基于[servlet](../../servlet/exploits/headers.html#servlet-headers-default)和[webflux](../../reactive/exploits/headers.html#webflux-headers-default)的应用程序定制默认值。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +Spring 安全性提供了一组与安全性相关的默认 HTTP 响应头,以提供安全的默认值。 + +Spring 安全性的默认设置是包含以下标题: + +例 1。默认安全 HTTP 响应标头 + +``` +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 请求中添加| +|---|---------------------------------------------------------| + +如果默认值不能满足你的需求,那么你可以轻松地从这些默认值中删除、修改或添加标题。有关每个标题的其他详细信息,请参阅相应的部分: + +* [缓存控制](#headers-cache-control) + +* [内容类型选项](#headers-content-type-options) + +* [HTTP 严格的传输安全](#headers-hsts) + +* [X-帧-选项](#headers-frame-options) + +* [X-XSS-保护](#headers-xss-protection) + +## 缓存控制 + +| |参考相关章节,了解如何为基于[servlet](../../servlet/exploits/headers.html#servlet-headers-cache-control)和[webflux](../../reactive/exploits/headers.html#webflux-headers-cache-control)的应用程序定制默认值。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +Spring 安全性的默认方式是禁用缓存,以保护用户的内容。 + +如果用户通过身份验证查看敏感信息,然后注销,我们不希望恶意用户能够单击“后退”按钮查看敏感信息。默认情况下发送的缓存控制头是: + +例 2。默认缓存控制 HTTP 响应标头 + +``` +Cache-Control: no-cache, no-store, max-age=0, must-revalidate +Pragma: no-cache +Expires: 0 +``` + +为了在默认情况下保持安全, Spring Security 在默认情况下添加了这些标题。但是,如果你的应用程序提供了自己的缓存控制头 Spring,安全性就会退缩。这允许应用程序确保 CSS 和 JavaScript 等静态资源可以被缓存。 + +## 内容类型选项 + +| |参考相关章节,了解如何为基于[servlet](../../servlet/exploits/headers.html#servlet-headers-content-type-options)和[webflux](../../reactive/exploits/headers.html#webflux-headers-content-type-options)的应用程序定制默认值。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +历史上,包括 Internet Explorer 在内的浏览器都会尝试使用[内容嗅探](https://en.wikipedia.org/wiki/Content_sniffing)来猜测请求的内容类型。这允许浏览器通过猜测未指定内容类型的资源上的内容类型来改善用户体验。例如,如果浏览器遇到一个没有指定内容类型的 JavaScript 文件,它将能够猜测内容类型,然后运行它。 + +| |在允许上传内容时,还应该做很多其他事情(例如,只在一个不同的域中显示文档,确保设置了 Content-Type 头,对文档进行了消毒等)。
但是,这些措施超出了 Spring 安全性所提供的范围。
还需要指出的是,在禁用内容嗅探时,必须指定内容类型,以便使事情正常工作。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +内容嗅探的问题在于,这使得恶意用户能够使用多义词(即作为多个内容类型有效的文件)来执行 XSS 攻击。例如,一些网站可能允许用户向网站提交有效的 PostScript 文档并查看它。恶意用户可能会创建[PostScript 文档,也是一个有效的 JavaScript 文件](http://webblaze.cs.berkeley.edu/papers/barth-caballero-song.pdf)并对其执行 XSS 攻击。 + +Spring 安全性通过在 HTTP 响应中添加以下头来在默认情况下禁用内容嗅探: + +示例 3.Nosniff HTTP 响应头 + +``` +X-Content-Type-Options: nosniff +``` + +## + +| |参考相关章节,了解如何为基于[servlet](../../servlet/exploits/headers.html#servlet-headers-hsts)和[webflux](../../reactive/exploits/headers.html#webflux-headers-hsts)的应用程序定制默认值。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +当你在你的银行的网站上输入时,你是输入 mybank.example.com 还是输入[https://mybank.example.com](https://mybank.example.com)?如果你省略了 HTTPS 协议,那么你可能会受到[中间派攻击](https://en.wikipedia.org/wiki/Man-in-the-middle_attack)的攻击。即使网站执行重定向到[https://mybank.example.com](https://mybank.example.com),恶意用户也可能拦截初始的 HTTP 请求并操纵响应(例如,重定向到[https://mibank.example.com](https://mibank.example.com)并窃取其凭据)。 + +许多用户省略了 HTTPS 协议,这就是创建[HTTP 严格传输安全](https://tools.ietf.org/html/rfc6797)的原因。一旦 mybank.example.com 被添加为[HSTS host](https://tools.ietf.org/html/rfc6797#section-5.1),浏览器就可以提前知道,对 mybank.example.com 的任何请求都应该被解释为[https://mybank.example.com](https://mybank.example.com)。这大大降低了中锋出现的可能性。 + +| |根据[RFC6797](https://tools.ietf.org/html/rfc6797#section-7.2),HSTS 报头只被注入到 HTTPS 响应中。
为了使浏览器承认报头,浏览器必须首先信任签署了用于建立连接的 SSL 证书(而不仅仅是 SSL 证书)的 CA。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +将站点标记为 HSTS 主机的一种方法是将主机预装到浏览器中。另一种方法是将`Strict-Transport-Security`头添加到响应中。例如, Spring Security 的默认行为是添加以下标题,该标题指示浏览器在一年内将该域视为 HSTS 主机(一年中大约有 31536000 秒): + +例 4。严格的传输安全 HTTP 响应头 + +``` +Strict-Transport-Security: max-age=31536000 ; includeSubDomains ; preload +``` + +可选的`includeSubDomains`指令指示浏览器将子域(例如 secure.mybank.example.com)也视为 HSTS 域。 + +可选的`preload`指令指示浏览器将域作为 HSTS 域预装到浏览器中。有关 HSTS 预加载的更多详细信息,请参见[https://hstspreload.org](https://hstspreload.org)。 + +## + +| |为了保持被动 Spring 安全仍然提供,但是由于上面列出的原因,HPKP 不再由安全团队推荐。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +[HTTP 公钥固定](https://developer.mozilla.org/en-US/docs/Web/HTTP/Public_Key_Pinning)向 Web 客户端指定在特定 Web 服务器上使用哪种公钥,以防止使用伪造证书的 MITM 攻击。如果正确使用,HPKP 可以添加额外的保护层,以防止受损的证书。然而,由于 HPKP 的复杂性,许多专家不再推荐使用它和[Chrome 甚至取消了支持](https://www.chromestatus.com/feature/5903385005916160)。 + +有关不再推荐 HPKP 的其他详细信息,请参见[HTTP 公钥钉死了吗?](https://blog.qualys.com/ssllabs/2016/09/06/is-http-public-key-pinning-dead)和[我要放弃 HPKP 了。](https://scotthelme.co.uk/im-giving-up-on-hpkp/)。 + +## X-帧-选项 + +| |参考相关章节,了解如何为基于[servlet](../../servlet/exploits/headers.html#servlet-headers-frame-options)和[webflux](../../reactive/exploits/headers.html#webflux-headers-frame-options)的应用程序定制默认值。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +允许你的网站被添加到一个框架可能是一个安全问题。例如,使用聪明的 CSS 样式的用户可能会被诱使点击一些他们不打算点击的东西。例如,已登录其银行的用户可能会单击授予其他用户访问权限的按钮。这种攻击被称为[点击劫持](https://en.wikipedia.org/wiki/Clickjacking)。 + +| |另一种处理点击劫持的现代方法是使用[内容安全策略](#headers-csp)。| +|---|-------------------------------------------------------------------------------------------------------------| + +有很多方法可以减轻点击劫持攻击。例如,要保护遗留浏览器免受点击劫持攻击,你可以使用[帧断代码](https://www.owasp.org/index.php/Clickjacking_Defense_Cheat_Sheet#Best-for-now_Legacy_Browser_Frame_Breaking_Script)。虽然不是完美的,但断帧代码是你可以为传统浏览器做的最好的事情。 + +一个更现代的解决点击劫持的方法是使用[X-帧-选项](https://developer.mozilla.org/en-US/docs/HTTP/X-Frame-Options)头。 Spring 默认情况下,安全性禁用使用以下标题在 IFRAME 中呈现页面: + +``` +X-Frame-Options: DENY +``` + +## X-XSS-保护 + +| |参考相关章节,了解如何为基于[servlet](../../servlet/exploits/headers.html#servlet-headers-xss-protection)和[webflux](../../reactive/exploits/headers.html#webflux-headers-xss-protection)的应用程序定制默认值。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +一些浏览器内置了对过滤掉[反射 XSS 攻击](https://www.owasp.org/index.php/Testing_for_Reflected_Cross_site_scripting_(OWASP-DV-001)的支持。这绝不是万无一失的,但确实有助于 XSS 保护。 + +默认情况下,过滤通常是启用的,因此添加标题通常只会确保它已启用,并指示浏览器在检测到 XSS 攻击时该做什么。例如,过滤器可能会尝试以最不具侵入性的方式更改内容,以便仍然呈现所有内容。有时,这种类型的替换可以变成[XSS 本身的漏洞](https://hackademix.net/2009/11/21/ies-xss-filter-creates-xss-vulnerabilities/)。相反,最好是屏蔽内容,而不是试图修复它。 Spring 默认情况下,安全性使用以下头来阻止内容: + +``` +X-XSS-Protection: 1; mode=block +``` + +## + +| |参考相关章节,了解如何配置基于[servlet](../../servlet/exploits/headers.html#servlet-headers-csp)和[webflux](../../reactive/exploits/headers.html#webflux-headers-csp)的应用程序。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +[内容安全策略](https://www.w3.org/TR/CSP2/)是一种机制,Web 应用程序可以利用该机制来减轻内容注入漏洞,例如跨站点脚本。CSP 是一种声明性策略,它为 Web 应用程序的作者提供了一种工具,用于声明并最终通知客户机(用户代理)Web 应用程序期望从哪些源加载资源。 + +| |内容安全策略并不是要解决所有的内容注入漏洞。
相反,可以利用 CSP 来帮助减少内容注入攻击造成的危害。
作为第一道防线,Web 应用程序作者应该验证其输入并对其输出进行编码。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +Web 应用程序可以通过在响应中包含以下 HTTP 头中的一个来使用 CSP: + +* `Content-Security-Policy` + +* `Content-Security-Policy-Report-Only` + +这些头中的每一个都被用作向客户机交付安全策略的机制。安全策略包含一组安全策略指令,每个指令负责声明特定资源表示的限制。 + +例如,Web 应用程序可以声明它希望从特定的、受信任的源加载脚本,方法是在响应中包含以下头: + +例 5。内容安全策略示例 + +``` +Content-Security-Policy: script-src https://trustedscripts.example.com +``` + +试图从除`script-src`指令中声明的内容以外的其他来源加载脚本的行为将被 User-Agent 阻止。此外,如果[report-uri](https://www.w3.org/TR/CSP2/#directive-report-uri)指令是在安全策略中声明的,那么违反行为将由用户代理报告给声明的 URL。 + +例如,如果 Web 应用程序违反了声明的安全策略,那么下面的响应头将指示用户代理将违规报告发送到策略的`report-uri`指令中指定的 URL。 + +例 6。带有 report-uri 的内容安全策略 + +``` +Content-Security-Policy: script-src https://trustedscripts.example.com; report-uri /csp-report-endpoint/ +``` + +[违规报告](https://www.w3.org/TR/CSP2/#violation-reports)是标准的 JSON 结构,可以由 Web 应用程序自己的 API 或公共托管的 CSP 违规报告服务捕获,例如,[https://report-uri.com/](https://report-uri.com/)。 + +`Content-Security-Policy-Report-Only`头为 Web 应用程序的作者和管理员提供了监视安全策略而不是强制执行它们的功能。此标头通常用于为站点进行试验和/或开发安全策略。当策略被认为有效时,可以使用`Content-Security-Policy`头字段来强制执行该策略。 + +给定以下响应头,策略声明脚本可以从两个可能的源中的一个加载。 + +例 7。仅提供内容安全策略报告 + +``` +Content-Security-Policy-Report-Only: script-src 'self' https://trustedscripts.example.com; report-uri /csp-report-endpoint/ +``` + +如果站点违反了此策略,通过尝试从*Evil.com*加载脚本,用户代理将向*报告-URI*指令指定的声明 URL 发送违规报告,但仍然允许违规资源加载。 + +将内容安全策略应用于 Web 应用程序通常是一项非常重要的工作。以下资源可以为你的站点提供进一步的帮助,帮助你制定有效的安全策略。 + +[内容安全策略介绍](https://www.html5rocks.com/en/tutorials/security/content-security-policy/) + +[CSP 指南-Mozilla 开发者网络](https://developer.mozilla.org/en-US/docs/Web/Security/CSP) + +[W3C 候选推荐](https://www.w3.org/TR/CSP2/) + +## 推荐人政策 + +| |参考相关章节,了解如何配置基于[servlet](../../servlet/exploits/headers.html#servlet-headers-referrer)和[webflux](../../reactive/exploits/headers.html#webflux-headers-referrer)的应用程序。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +[推荐人政策](https://www.w3.org/TR/referrer-policy)是一种机制,Web 应用程序可以利用它来管理 Referrer 字段,该字段包含用户所在的最后一页。 + +Spring Security 的方法是使用[推荐人政策](https://www.w3.org/TR/referrer-policy/)头,它提供不同的[policies](https://www.w3.org/TR/referrer-policy/#referrer-policies): + +例 8。推荐人政策示例 + +``` +Referrer-Policy: same-origin +``` + +Referrer-Policy 响应头指示浏览器让目标知道用户先前所在的源。 + +## 特征策略 + +| |参考相关章节,了解如何配置基于[servlet](../../servlet/exploits/headers.html#servlet-headers-feature)和[webflux](../../reactive/exploits/headers.html#webflux-headers-feature)的应用程序。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +[特征策略](https://wicg.github.io/feature-policy/)是一种机制,允许 Web 开发人员有选择地启用、禁用和修改浏览器中某些 API 和 Web 功能的行为。 + +例 9。功能策略示例 + +``` +Feature-Policy: geolocation 'self' +``` + +通过功能策略,开发人员可以 OPT 到一组“策略”中,以便浏览器对整个站点中使用的特定功能进行强制执行。这些策略限制了站点可以访问哪些 API,或修改浏览器的某些功能的默认行为。 + +## 权限策略 + +| |参考相关章节,了解如何配置基于[servlet](../../servlet/exploits/headers.html#servlet-headers-permissions)和[webflux](../../reactive/exploits/headers.html#webflux-headers-permissions)的应用程序。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +[权限策略](https://w3c.github.io/webappsec-permissions-policy/)是一种机制,允许 Web 开发人员有选择地启用、禁用和修改浏览器中某些 API 和 Web 功能的行为。 + +例 10。权限策略示例 + +``` +Permissions-Policy: geolocation=(self) +``` + +通过权限策略,开发人员可以 OPT 到一组“策略”中,以便浏览器对整个站点中使用的特定功能进行强制执行。这些策略限制了站点可以访问哪些 API,或修改浏览器的某些功能的默认行为。 + +## 清除站点数据 + +| |参考相关章节,了解如何配置基于[servlet](../../servlet/exploits/headers.html#servlet-headers-clear-site-data)和[webflux](../../reactive/exploits/headers.html#webflux-headers-clear-site-data)的应用程序。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +[清除站点数据](https://www.w3.org/TR/clear-site-data/)是一种机制,通过这种机制,当 HTTP 响应包含此标头时,可以删除任何浏览器端数据(cookie、本地存储等): + +``` +Clear-Site-Data: "cache", "cookies", "storage", "executionContexts" +``` + +这是一个很好的清理行动,执行注销。 + +## 自定义标头 + +| |请参阅相关部分,以了解如何配置基于[servlet](../../servlet/exploits/headers.html#servlet-headers-custom)的应用程序。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------| + +Spring 安全性有一些机制,可以方便地将更常见的安全性标题添加到应用程序中。然而,它也提供了钩子来支持添加自定义头。 \ No newline at end of file diff --git a/docs/spring-security/features-exploits-http.md b/docs/spring-security/features-exploits-http.md new file mode 100644 index 0000000000000000000000000000000000000000..2ea2f48d2730a7da816b82b252e2097cc381ba6a --- /dev/null +++ b/docs/spring-security/features-exploits-http.md @@ -0,0 +1,21 @@ +# HTTP + +所有基于 HTTP 的通信,包括[静态资源](https://www.troyhunt.com/heres-why-your-static-website-needs-https/),都应该受到[using TLS](https://cheatsheetseries.owasp.org/cheatsheets/Transport_Layer_Protection_Cheat_Sheet.html)的保护。 + +作为一个框架, Spring 安全性不处理 HTTP 连接,因此不直接提供对 HTTPS 的支持。然而,它确实提供了许多有助于 HTTPS 使用的功能。 + +## 重定向到 HTTPS + +当客户端使用 HTTP 时, Spring 安全性可以被配置为将[Servlet](../../servlet/exploits/http.html#servlet-http-redirect)和[WebFlux](../../reactive/exploits/http.html#webflux-http-redirect)环境重定向到 HTTPS。 + +## 严格的运输安全 + +Spring 安全性为[严格的运输安全](headers.html#headers-hsts)提供支持,并在默认情况下启用它。 + +## 代理服务器配置 + +在使用代理服务器时,重要的是要确保你已经正确地配置了应用程序。例如,许多应用程序将有一个负载均衡器,该负载均衡器通过在[https://192.168.1:8080](https://192.168.1:8080)处将请求转发到应用程序服务器来响应[https://example.com/](https://example.com/)的请求。如果没有适当的配置,应用程序服务器将不知道负载均衡器的存在,并将请求视为[https://192.168.1:8080](https://192.168.1:8080)是由客户机请求的。 + +要解决这个问题,你可以使用[RFC 7239](https://tools.ietf.org/html/rfc7239)来指定正在使用负载均衡器。要使应用程序意识到这一点,你需要配置你的应用程序服务器来了解 X 转发头。例如, Tomcat 使用[Remoteipvalve](https://tomcat.apache.org/tomcat-8.0-doc/api/org/apache/catalina/valves/RemoteIpValve.html), Jetty 使用[ForwardeDrequestCustomizer](https://www.eclipse.org/jetty/javadoc/jetty-9/org/eclipse/jetty/server/ForwardedRequestCustomizer.html)。或者, Spring 用户可以利用[ForwardedHeaderFilter](https://github.com/spring-projects/spring-framework/blob/v4.3.3.RELEASE/spring-web/src/main/java/org/springframework/web/filter/ForwardedHeaderFilter.java)。 + +Spring 引导用户可以使用`server.use-forward-headers`属性来配置应用程序。有关更多详细信息,请参见[Spring Boot documentation](https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#howto-use-tomcat-behind-a-proxy-server)。 \ No newline at end of file diff --git a/docs/spring-security/features-exploits.md b/docs/spring-security/features-exploits.md new file mode 100644 index 0000000000000000000000000000000000000000..7bae47e923cba043c3de28b06e1c7c77bf504b04 --- /dev/null +++ b/docs/spring-security/features-exploits.md @@ -0,0 +1,9 @@ +# 保护免受剥削 + +Spring 安全性提供了针对常见攻击的保护。只要有可能,默认情况下都会启用该保护。下面,你将看到安全防范的各种攻击的高级描述。 + +## 章节摘要 + +* [CSRF](csrf.html) +* [HTTP 头](headers.html) +* [HTTP 请求](http.html) \ No newline at end of file diff --git a/docs/spring-security/features-integrations-concurrency.md b/docs/spring-security/features-integrations-concurrency.md new file mode 100644 index 0000000000000000000000000000000000000000..e1b62bd10e503cbe3f6ee3eb94bc537355ee250d --- /dev/null +++ b/docs/spring-security/features-integrations-concurrency.md @@ -0,0 +1,231 @@ +# 并发支持 + +在大多数环境中,安全性是以 per`Thread`为基础存储的。这意味着,当在新的`Thread`上完成工作时,`SecurityContext`将丢失。 Spring 安全性提供了一些基础设施,以帮助用户更容易地实现这一点。 Spring 安全性为在多线程环境中使用 Spring 安全性提供了低层次的抽象。事实上,这就是 Spring 安全性构建到与[AsyncContext.start(可运行)](../../servlet/integrations/servlet-api.html#servletapi-start-runnable)和[Spring MVC Async Integration](../../servlet/integrations/mvc.html#mvc-async)集成的基础。 + +## 在可撤销的情况下将证券转让 + +Spring Security 的并发支持中最基本的构建块之一是`DelegatingSecurityContextRunnable`。它包装了一个委托`Runnable`,以便用指定的`SecurityContext`为委托初始化`SecurityContextHolder`。然后,它调用委托 Runnable 确保在之后清除`SecurityContextHolder`。`DelegatingSecurityContextRunnable`看起来是这样的: + +爪哇 + +``` +public void run() { +try { + SecurityContextHolder.setContext(securityContext); + delegate.run(); +} finally { + SecurityContextHolder.clearContext(); +} +} +``` + +Kotlin + +``` +fun run() { + try { + SecurityContextHolder.setContext(securityContext) + delegate.run() + } finally { + SecurityContextHolder.clearContext() + } +} +``` + +虽然非常简单,但它可以无缝地将 SecurityContext 从一个线程转移到另一个线程。这一点很重要,因为在大多数情况下,SecurityContextholder 是以每个线程为基础的。例如,你可能使用了 Spring Security 的[\](../../servlet/appendix/namespace/method-security.html#nsa-global-method-security)支持来保护你的某个服务。现在,你可以轻松地将当前`Thread`的`SecurityContext`传输到调用安全服务的`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(); +``` + +Kotlin + +``` +val originalRunnable = Runnable { + // invoke secured service +} +val context: SecurityContext = SecurityContextHolder.getContext() +val wrappedRunnable = DelegatingSecurityContextRunnable(originalRunnable, context) + +Thread(wrappedRunnable).start() +``` + +上面的代码执行以下步骤: + +* 创建将调用我们的安全服务的`Runnable`。请注意,它并不了解 Spring 安全性 + +* 从`SecurityContextHolder`获取我们希望使用的`SecurityContext`,并初始化`DelegatingSecurityContextRunnable` + +* 使用`DelegatingSecurityContextRunnable`创建线程 + +* 启动我们创建的线程 + +由于从`SecurityContextHolder`中使用`SecurityContext`创建`DelegatingSecurityContextRunnable`是很常见的,因此它有一个快捷构造函数。以下代码与上述代码相同: + +爪哇 + +``` +Runnable originalRunnable = new Runnable() { +public void run() { + // invoke secured service +} +}; + +DelegatingSecurityContextRunnable wrappedRunnable = + new DelegatingSecurityContextRunnable(originalRunnable); + +new Thread(wrappedRunnable).start(); +``` + +Kotlin + +``` +val originalRunnable = Runnable { + // invoke secured service +} + +val wrappedRunnable = DelegatingSecurityContextRunnable(originalRunnable) + +Thread(wrappedRunnable).start() +``` + +我们拥有的代码使用起来很简单,但仍然需要了解我们正在使用 Spring 安全性。在下一节中,我们将研究如何利用`委派安全环境专家`来隐藏我们正在使用 Spring 安全性的事实。 + +## DelegatingSecurityContextExecutor + +在上一节中,我们发现使用`DelegatingSecurityContextRunnable`很容易,但并不理想,因为我们必须意识到 Spring 安全性才能使用它。让我们来看看`DelegatingSecurityContextExecutor`如何保护我们的代码不受我们正在使用 Spring 安全性的任何知识的影响。 + +`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); +``` + +Kotlin + +``` +val context: SecurityContext = SecurityContextHolder.createEmptyContext() +val authentication: Authentication = + UsernamePasswordAuthenticationToken("user", "doesnotmatter", AuthorityUtils.createAuthorityList("ROLE_USER")) +context.authentication = authentication + +val delegateExecutor = SimpleAsyncTaskExecutor() +val executor = DelegatingSecurityContextExecutor(delegateExecutor, context) + +val originalRunnable = Runnable { + // invoke secured service +} + +executor.execute(originalRunnable) +``` + +代码执行以下步骤: + +* 创建用于我们的`DelegatingSecurityContextExecutor`的`SecurityContext`。注意,在这个示例中,我们只需手工创建`SecurityContext`。然而,无论我们在哪里或如何获得`SecurityContext`都不重要(也就是说,如果我们愿意,我们可以从`SecurityContextHolder`获得它)。 + +* 创建一个 DelegateExecutor,它负责执行提交的`Runnable`s + +* 最后,我们创建一个`DelegatingSecurityContextExecutor`,它负责用`DelegatingSecurityContextRunnable`包装传递到 Execute 方法中的任何 runnable。然后,它将包装好的 Runnable 传递给 DelegateExecutor。在此实例中,对于提交到我们的`DelegatingSecurityContextExecutor`的每个 runnable,将使用相同的`SecurityContext`。如果我们运行的是需要由具有提升权限的用户运行的后台任务,那么这很好。 + +* 此时,你可能会问自己:“这是如何屏蔽我的代码中的任何安全知识的?”我们不需要在自己的代码中创建`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); +} +``` + +Kotlin + +``` +@Autowired +lateinit var executor: Executor // becomes an instance of our DelegatingSecurityContextExecutor + +fun submitRunnable() { + val originalRunnable = Runnable { + // invoke secured service + } + executor.execute(originalRunnable) +} +``` + +现在我们的代码不知道`SecurityContext`正在传播到`Thread`,然后运行`originalRunnable`,然后清除`SecurityContextHolder`。在本例中,使用相同的用户运行每个线程。如果我们希望在调用`executor.execute(Runnable)`时使用来自`SecurityContextHolder`的用户(即当前登录的用户)来处理`originalRunnable`,该怎么办?这可以通过从我们的`DelegatingSecurityContextExecutor`构造函数中删除`SecurityContext`参数来完成。例如: + +爪哇 + +``` +SimpleAsyncTaskExecutor delegateExecutor = new SimpleAsyncTaskExecutor(); +DelegatingSecurityContextExecutor executor = + new DelegatingSecurityContextExecutor(delegateExecutor); +``` + +Kotlin + +``` +val delegateExecutor = SimpleAsyncTaskExecutor() +val executor = DelegatingSecurityContextExecutor(delegateExecutor) +``` + +现在,每当执行`executor.execute(Runnable)`时,`SecurityContext`首先由`SecurityContextHolder`获得,然后`SecurityContext`用于创建我们的`DelegatingSecurityContextRunnable`。这意味着我们运行`Runnable`的用户与调用`executor.execute(Runnable)`代码的用户相同。 + +## Spring 安全并发类 + +有关 Java Concurrent API 和 Spring 任务抽象的附加集成,请参考 Javadoc。一旦你理解了前面的代码,它们就非常不言自明了。 + +* `DelegatingSecurityContextCallable` + +* `DelegatingSecurityContextExecutor` + +* `DelegatingSecurityContextExecutorService` + +* `DelegatingSecurityContextRunnable` + +* `DelegatingSecurityContextScheduledExecutorService` + +* `DelegatingSecurityContextSchedulingTaskExecutor` + +* `DelegatingSecurityContextAsyncTaskExecutor` + +* `DelegatingSecurityContextTaskExecutor` + +* `DelegatingSecurityContextTaskScheduler` \ No newline at end of file diff --git a/docs/spring-security/features-integrations-cryptography.md b/docs/spring-security/features-integrations-cryptography.md new file mode 100644 index 0000000000000000000000000000000000000000..a6b7e7d5d20f4508860a9fd42b535a9856f1bf0d --- /dev/null +++ b/docs/spring-security/features-integrations-cryptography.md @@ -0,0 +1,218 @@ +# Spring 安全加密模块 + +## 导言 + +Spring 安全加密模块提供对对称加密、密钥生成和密码编码的支持。该代码作为核心模块的一部分进行分发,但不依赖于任何其他 Spring 安全性(或 Spring)代码。 + +## 加密者 + +Encryptors 类提供了用于构造对称加密器的工厂方法。使用这个类,你可以创建 ByteenCryptors 来加密原始字节[]形式的数据。你还可以构造文本加密器来加密文本字符串。加密器是线程安全的。 + +### BytesenCryptor + +使用`Encryptors.stronger`工厂方法来构造一个 ByteSenCryptor: + +例 1。BytesenCryptor + +爪哇 + +``` +Encryptors.stronger("password", "salt"); +``` + +Kotlin + +``` +Encryptors.stronger("password", "salt") +``` + +“更强”的加密方法使用 256 位 AES 加密和伽罗瓦计数器模式创建一个加密器。它使用 PKCS#5 的 PBKDF2(基于密码的密钥派生函数 #2)来派生密钥。这个方法需要 爪哇6。用于生成秘密密钥的密码应保存在安全的地方,不得共享。SALT 用于防止在你的加密数据遭到破坏时对密钥发起字典攻击。还应用了 16 字节的随机初始化向量,因此每个加密消息都是唯一的。 + +所提供的 SALT 应该是十六进制编码的字符串形式,是随机的,并且至少有 8 个字节的长度。可以使用键盘生成器生成这样的 salt: + +例 2。生成密钥 + +爪哇 + +``` +String salt = KeyGenerators.string().generateKey(); // generates a random 8-byte salt that is then hex-encoded +``` + +Kotlin + +``` +val salt = KeyGenerators.string().generateKey() // generates a random 8-byte salt that is then hex-encoded +``` + +用户也可以使用`standard`加密方法,这是在密码块链接模式下的 256 位 AES。此模式不是[已认证](https://en.wikipedia.org/wiki/Authenticated_encryption),并且不提供有关数据真实性的任何保证。对于更安全的选择,用户应该更喜欢`Encryptors.stronger`。 + +### TextEncryptor + +使用 Encryptors.text Factory 方法构建一个标准的 TextEncryptor: + +例 3。TextEncryptor + +爪哇 + +``` +Encryptors.text("password", "salt"); +``` + +Kotlin + +``` +Encryptors.text("password", "salt") +``` + +TextEncryptor 使用标准的 BytesEncryptor 加密文本数据。加密的结果以十六进制编码字符串的形式返回,以便于存储在文件系统或数据库中。 + +使用 Encryptors.queryableText Factory 方法构造一个“可查询的”文本加密程序: + +例 4。可查询文本加密器 + +爪哇 + +``` +Encryptors.queryableText("password", "salt"); +``` + +Kotlin + +``` +Encryptors.queryableText("password", "salt") +``` + +可查询文本加密器和标准文本加密器之间的区别与初始化向量处理有关。在可查询的 TextEncryptor#Encrypt 操作中使用的 IV 是共享的或常量的,并且不是随机生成的。这意味着多次加密的相同文本将始终产生相同的加密结果。这不太安全,但对于需要查询的加密数据来说是必要的。可查询加密文本的一个例子是 OAuth Apikey。 + +## 关键生成器 + +keygenerators 类为构造不同类型的密钥生成器提供了许多方便的工厂方法。使用这个类,你可以创建一个 byteskeygenerator 来生成 byte[]键。你还可以构造一个 串键生成器 来生成 String 键。键盘生成器是线程安全的。 + +### Byteskeygenerator + +使用 keygenerators.secureRandom Factory 方法生成由 secureRandom 实例支持的 byteskeygenerator: + +例 5。Byteskeygenerator + +爪哇 + +``` +BytesKeyGenerator generator = KeyGenerators.secureRandom(); +byte[] key = generator.generateKey(); +``` + +Kotlin + +``` +val generator = KeyGenerators.secureRandom() +val key = generator.generateKey() +``` + +默认的密钥长度是 8 个字节。还有一个 keygenerators.secureRandom 变体,它提供对密钥长度的控制: + +例 6。keygenerators.secureRandom + +爪哇 + +``` +KeyGenerators.secureRandom(16); +``` + +Kotlin + +``` +KeyGenerators.secureRandom(16) +``` + +使用 keygenerators.shared Factory 方法构造一个 byteskeygenerator,它总是在每次调用时返回相同的键: + +例 7。keygenerators.shared + +爪哇 + +``` +KeyGenerators.shared(16); +``` + +Kotlin + +``` +KeyGenerators.shared(16) +``` + +### StringKeyGenerator + +使用 keygenerators.string Factory 方法构造一个 8 字节的安全随机密钥生成器,将每个密钥作为字符串进行十六进制编码: + +例 8。串键生成器 + +爪哇 + +``` +KeyGenerators.string(); +``` + +Kotlin + +``` +KeyGenerators.string() +``` + +## 密码编码 + +Spring-security-crypto 模块的密码包提供了对密码编码的支持。`PasswordEncoder`是中心服务接口,具有以下签名: + +``` +public interface PasswordEncoder { + +String encode(String rawPassword); + +boolean matches(String rawPassword, String encodedPassword); +} +``` + +如果编码后的 RAWPassword 等于 EncodedPassword,则 Matches 方法返回 true。该方法旨在支持基于密码的身份验证方案。 + +`BCryptPasswordEncoder`实现使用广泛支持的“bcrypt”算法来散列密码。Bcrypt 使用一个随机的 16 字节的盐值,并且是一个故意缓慢的算法,以阻止密码破解器。它所做的工作量可以使用“强度”参数进行调整,该参数的取值范围为 4 到 31。值越高,计算散列所需做的工作就越多。默认值是 10。你可以在部署的系统中更改该值,而不会影响现有的密码,因为该值也存储在编码的散列中。 + +例 9。bcryptpasswordencoder + +爪哇 + +``` +// Create an encoder with strength 16 +BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(16); +String result = encoder.encode("myPassword"); +assertTrue(encoder.matches("myPassword", result)); +``` + +Kotlin + +``` +// Create an encoder with strength 16 +val encoder = BCryptPasswordEncoder(16) +val result: String = encoder.encode("myPassword") +assertTrue(encoder.matches("myPassword", result)) +``` + +`Pbkdf2PasswordEncoder`实现使用 PBKDF2 算法来散列密码。为了击败密码破解,PBKDF2 是一个故意缓慢的算法,应该调整为大约 0.5 秒来验证你的系统上的密码。 + +例 10。PBKDF2PASSWORDENCODER + +Java + +``` +// Create an encoder with all the defaults +Pbkdf2PasswordEncoder encoder = new Pbkdf2PasswordEncoder(); +String result = encoder.encode("myPassword"); +assertTrue(encoder.matches("myPassword", result)); +``` + +Kotlin + +``` +// Create an encoder with all the defaults +val encoder = Pbkdf2PasswordEncoder() +val result: String = encoder.encode("myPassword") +assertTrue(encoder.matches("myPassword", result)) +``` \ No newline at end of file diff --git a/docs/spring-security/features-integrations-data.md b/docs/spring-security/features-integrations-data.md new file mode 100644 index 0000000000000000000000000000000000000000..68504ee72a399417e408f1315ac4d19b6bf695de --- /dev/null +++ b/docs/spring-security/features-integrations-data.md @@ -0,0 +1,57 @@ +# Spring 数据集成 + +Spring 安全性提供了 Spring 数据集成,允许在查询中引用当前用户。将用户包括在查询中以支持分页结果不仅是有用的,而且是必要的,因为在此之后对结果进行过滤将不会扩展。 + +## Spring 数据和 Spring 安全配置 + +要使用此支持,请添加`org.springframework.security:spring-security-data`依赖项,并提供类型`SecurityEvaluationContextExtension`的 Bean。在 爪哇 配置中,这看起来像是: + +爪哇 + +``` +@Bean +public SecurityEvaluationContextExtension securityEvaluationContextExtension() { + return new SecurityEvaluationContextExtension(); +} +``` + +Kotlin + +``` +@Bean +fun securityEvaluationContextExtension(): SecurityEvaluationContextExtension { + return SecurityEvaluationContextExtension() +} +``` + +在 XML 配置中,这看起来像是: + +``` + +``` + +## @query 中的安全表达式 + +现在 Spring 可以在查询中使用安全性。例如: + +Java + +``` +@Repository +public interface MessageRepository extends PagingAndSortingRepository { + @Query("select m from Message m where m.to.id = ?#{ principal?.id }") + Page findInbox(Pageable pageable); +} +``` + +Kotlin + +``` +@Repository +interface MessageRepository : PagingAndSortingRepository { + @Query("select m from Message m where m.to.id = ?#{ principal?.id }") + fun findInbox(pageable: Pageable?): Page? +} +``` + +这将检查`Authentication.getPrincipal().getId()`是否等于`Message`的接收者。请注意,本例假定你已将主体自定义为具有 ID 属性的对象。通过公开`SecurityEvaluationContextExtension` Bean,查询中的所有[常见的安全表达式](../../servlet/authorization/expression-based.html#common-expressions)都是可用的。 \ No newline at end of file diff --git a/docs/spring-security/features-integrations-jackson.md b/docs/spring-security/features-integrations-jackson.md new file mode 100644 index 0000000000000000000000000000000000000000..30debd251680abfa8e2a0afa9f5b9f8cb83cca5c --- /dev/null +++ b/docs/spring-security/features-integrations-jackson.md @@ -0,0 +1,36 @@ +# Jackson 支助 + +Spring 安全性为持久化 Spring 与安全性相关的类提供了 Jackson 支持。这可以在使用分布式会话(即会话复制、 Spring 会话等)时提高序列化 Spring 安全相关类的性能。 + +要使用它,将`SecurityJackson2Modules.getModules(ClassLoader)`注册为`ObjectMapper`([Jackson-数据库](https://github.com/FasterXML/jackson-databind)): + +爪哇 + +``` +ObjectMapper mapper = new ObjectMapper(); +ClassLoader loader = getClass().getClassLoader(); +List modules = SecurityJackson2Modules.getModules(loader); +mapper.registerModules(modules); + +// ... use ObjectMapper as normally ... +SecurityContext context = new SecurityContextImpl(); +// ... +String json = mapper.writeValueAsString(context); +``` + +Kotlin + +``` +val mapper = ObjectMapper() +val loader = javaClass.classLoader +val modules: MutableList = SecurityJackson2Modules.getModules(loader) +mapper.registerModules(modules) + +// ... use ObjectMapper as normally ... +val context: SecurityContext = SecurityContextImpl() +// ... +val json: String = mapper.writeValueAsString(context) +``` + +| |下面的 Spring 安全模块提供了 Jackson 支持:

* Spring-security-core(`CoreJackson2Module`)

* Spring-security-web(`WebJackson2Module`,`WebServletJackson2Module`,`WebServerJackson2Module`)
(<12"gt=">>(<10"/>r=“r=”10“>)r=“20”/>(<<>| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| \ No newline at end of file diff --git a/docs/spring-security/features-integrations-localization.md b/docs/spring-security/features-integrations-localization.md new file mode 100644 index 0000000000000000000000000000000000000000..9fe9fc1c64f0c4570f3e7515827f9f843460c32f --- /dev/null +++ b/docs/spring-security/features-integrations-localization.md @@ -0,0 +1,22 @@ +# 本地化 + +如果你需要支持其他语言环境,那么你需要了解的所有内容都包含在本节中。 + +所有异常消息都可以本地化,包括与身份验证失败和访问被拒绝(授权失败)相关的消息。针对开发人员或系统部署人员的异常和日志消息(包括不正确的属性、违反接口契约、使用不正确的构造函数、启动时间验证、调试级别的日志记录)没有本地化,而是在 Spring Security 的代码中用英文进行了硬编码。 + +在`spring-security-core-xx.jar`中,你将发现一个`org.springframework.security`包,该包依次包含一个`messages.properties`文件,以及一些常见语言的本地化版本。这应该由你的`ApplicationContext`来引用,因为 Spring 安全类实现了 Spring 的`MessageSourceAware`接口,并且期望消息解析程序是在应用程序上下文启动时注入的依赖项。通常,你所需要做的就是在应用程序上下文中注册一个 Bean 来引用消息。下面是一个例子: + +``` + + + +``` + +`messages.properties`是根据标准资源包命名的,表示 Spring 安全消息支持的默认语言。这个默认文件是英文的。 + +如果你希望自定义`messages.properties`文件,或者支持其他语言,那么你应该复制该文件,对其进行相应的重命名,并将其注册到上述 Bean 定义中。在这个文件中没有大量的消息键,因此本地化不应该被认为是一项主要的举措。如果你确实执行了此文件的本地化,请考虑通过记录 JIRA 任务并附加适当命名的本地化版本`messages.properties`来与社区共享你的工作。 + +Spring 安全性依赖于 Spring 的本地化支持,以便实际查找适当的消息。为了实现这一点,你必须确保来自传入请求的区域设置存储在 Spring 的`org.springframework.context.i18n.LocaleContextHolder`中。 Spring MVC 的`DispatcherServlet`自动为你的应用程序执行此操作,但是由于 Spring Security 的过滤器是在此之前调用的,因此在调用过滤器之前,需要设置`LocaleContextHolder`以包含正确的`Locale`。你可以自己在过滤器中执行此操作(它必须在`web.xml`中的 Spring 安全过滤器之前),也可以使用 Spring 的`RequestContextFilter`。请参阅 Spring 框架文档,以获取关于使用 Spring 本地化的更多详细信息。 + +将“联系人”示例应用程序设置为使用本地化消息。 \ No newline at end of file diff --git a/docs/spring-security/features-integrations.md b/docs/spring-security/features-integrations.md new file mode 100644 index 0000000000000000000000000000000000000000..119ab019e5596714c53e91c711426edee7d0e65f --- /dev/null +++ b/docs/spring-security/features-integrations.md @@ -0,0 +1,11 @@ +# 整合 + +Spring 安全性提供了与众多框架和 API 的集成。在这一节中,我们将讨论不特定于 Servlet 或反应性环境的通用集成。要查看特定的集成,请参阅[Servlet](../../servlet/integrations/index.html)和[Reactive](../../servlet/integrations/index.html)集成部分。 + +## 章节摘要 + +* [密码学](cryptography.html) +* [Spring Data](data.html) +* [Java 的并发 API](concurrency.html) +* [Jackson](jackson.html) +* [本地化](localization.html) \ No newline at end of file diff --git a/docs/spring-security/features.md b/docs/spring-security/features.md new file mode 100644 index 0000000000000000000000000000000000000000..8f6fada33a93c083e415430156e77e3058c93393 --- /dev/null +++ b/docs/spring-security/features.md @@ -0,0 +1,11 @@ +# 特征 + +Spring 安全性为[认证](authentication/index.html)、[授权](authorization/index.html)和[共同的功绩](exploits/index.html#exploits)提供了全面的支持。它还提供了与其他库的集成,以简化其使用。 + +## 章节摘要 + +* [认证](authentication/index.html) +* [保护免受剥削](exploits/index.html) +* [整合](integrations/index.html) + +[Getting Spring Security](../getting-spring-security.html)[认证](authentication/index.html) \ No newline at end of file diff --git a/docs/spring-security/getting-spring-security.md b/docs/spring-security/getting-spring-security.md new file mode 100644 index 0000000000000000000000000000000000000000..70c38a4e28967f526cd7187d840c16b949700d5a --- /dev/null +++ b/docs/spring-security/getting-spring-security.md @@ -0,0 +1,275 @@ +# 获得 Spring 安全性 + +本节讨论了有关获得 Spring 安全性二进制文件所需了解的所有信息。有关如何获得源代码,请参见[源代码](community.html#community-source)。 + +## 发行版本编号 + +Spring 安全版本的格式为 major.minor.patch,例如: + +* 主要的版本可能包含断开的更改。通常,这样做是为了提供与现代安全实践相匹配的改进的安全性。 + +* 小版本包含增强功能,但被视为被动更新。 + +* 补丁级别应该是完全兼容的,向前和向后,可能的例外情况是修复错误的更改。 + +## Maven 的用法 + +与大多数开源项目一样, Spring Security 将其依赖关系部署为 Maven 工件。本节中的主题提供了有关在使用 Maven 时如何使用 Spring 安全性的详细信息。 + +### Spring 用 Maven 引导 + +Spring Boot 提供了一个`spring-boot-starter-security`启动器,该启动器将 Spring 与安全相关的依赖关系聚合在一起。使用启动器的最简单和首选的方法是通过使用 IDE 集成([Eclipse](https://joshlong.com/jl/blogPost/tech_tip_geting_started_with_spring_boot.html),[IntelliJ](https://www.jetbrains.com/help/idea/spring-boot.html#d1489567e2),[NetBeans](https://github.com/AlexFalappa/nb-springboot/wiki/Quick-Tour))或通过[https://start.spring.io](https://start.spring.io)使用[Spring Initializr](https://docs.spring.io/initializr/docs/current/reference/html/)。 + +或者,你也可以手动添加启动器,如下例所示: + +例 1。 POM.xml + +``` + + + + org.springframework.boot + spring-boot-starter-security + + +``` + +由于 Spring 启动提供了一个 Maven BOM 来管理依赖版本,因此你不需要指定版本。如果你希望重写 Spring 安全版本,那么可以通过提供 Maven 属性来实现,如下例所示: + +例 2。 POM.xml + +``` + + + 5.6.2 + +``` + +由于 Spring 安全性仅在主要版本中进行破坏更改,因此使用具有 Spring 启动的 Spring 安全性的较新版本是安全的。然而,有时你可能还需要更新 Spring Framework 的版本。你可以通过添加一个 Maven 属性来做到这一点,如下例所示: + +例 3。 POM.xml + +``` + + + 5.3.16 + +``` + +如果使用额外的功能(例如 LDAP、OpenID 和其他功能),还需要包括适当的[项目模块和依赖项](modules.html#modules)。 + +### Maven 没有靴子 + +当使用 Spring 安全性而不启动 Spring 时,首选的方法是使用 Spring 安全性的 BOM,以确保在整个项目中使用 Spring 安全性的一致版本。下面的示例展示了如何做到这一点: + +例 4。 POM.xml + +``` + + + + + org.springframework.security + spring-security-bom + {spring-security-version} + pom + import + + + +``` + +一组最小的 Spring 安全性 Maven 依赖关系通常如下所示: + +例 5。 POM.xml + +``` + + + + org.springframework.security + spring-security-web + + + org.springframework.security + spring-security-config + + +``` + +如果你使用了额外的功能(例如 LDAP、OpenID 和其他功能),则还需要包括适当的[项目模块和依赖项](modules.html#modules)。 + +Spring 安全性是根据 Spring Framework5.3.16 构建的,但通常应该与 Spring Framework5.x 的任何较新版本一起工作。 Spring 安全性的传递依赖关系解决了 Spring Framework5.3.16,这可能会导致奇怪的 Classpath 问题,许多用户可能会对此感到不满。解决此问题的最简单方法是使用`spring-framework-bom`中``部分中的`pom.xml`,如下例所示: + +例 6。 POM.xml + +``` + + + + + org.springframework + spring-framework-bom + 5.3.16 + pom + import + + + +``` + +前面的示例确保 Spring 安全性的所有传递依赖使用 Spring 5.3.16 模块。 + +| |这种方法使用 Maven 的“物料清单”概念,并且仅在 Maven 2.0.9+ 中可用。
有关如何解决依赖关系的更多详细信息,请参见[Maven’s Introduction to the Dependency Mechanism documentation](https://maven.apache.org/guides/introduction/introduction-to-dependency-mechanism.html)。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### Maven 存储库 + +所有 GA 版本(即以.release 结尾的版本)都部署到 Maven Central,因此不需要在 POM 中声明额外的 Maven 存储库。 + +如果使用快照版本,则需要确保定义了 Spring 快照存储库,如下例所示: + +例 7。 POM.xml + +``` + + + + spring-snapshot + Spring Snapshot Repository + https://repo.spring.io/snapshot + + +``` + +如果使用里程碑或发布候选版本,则需要确保定义了 Spring 里程碑存储库,如下例所示: + +例 8。 POM.xml + +``` + + + + spring-milestone + Spring Milestone Repository + https://repo.spring.io/milestone + + +``` + +## Gradle + +与大多数开源项目一样, Spring Security 将其依赖关系部署为 Maven 工件,这允许一流的 Gradle 支持。以下主题提供了在使用 Gradle 时如何使用 Spring 安全性的详细信息。 + +### Spring 用 Gradle 引导 + +Spring 引导提供了一个`spring-boot-starter-security`启动器,该启动器将 Spring 安全相关的依赖关系聚合在一起。使用启动器的最简单和首选方法是通过使用 IDE 集成([Eclipse](https://joshlong.com/jl/blogPost/tech_tip_geting_started_with_spring_boot.html),[IntelliJ](https://www.jetbrains.com/help/idea/spring-boot.html#d1489567e2),[NetBeans](https://github.com/AlexFalappa/nb-springboot/wiki/Quick-Tour))或通过[https://start.spring.io](https://start.spring.io)使用[Spring Initializr](https://docs.spring.io/initializr/docs/current/reference/html/)。 + +或者,你也可以手动添加启动器,如下例所示: + +示例 9.build. Gradle + +``` +dependencies { + compile "org.springframework.boot:spring-boot-starter-security" +} +``` + +由于 Spring 启动提供了一个 Maven BOM 来管理依赖版本,所以不需要指定版本。如果你希望重写 Spring 安全版本,那么可以通过提供 Gradle 属性来实现,如下例所示: + +示例 10.构建。 Gradle + +``` +ext['spring-security.version']='5.6.2' +``` + +由于 Spring 安全性仅在主要版本中进行破坏更改,因此使用具有 Spring 启动的 Spring 安全性的较新版本是安全的。然而,有时你可能还需要更新 Spring 框架的版本。你可以通过添加一个 Gradle 属性来做到这一点,如下例所示: + +示例 11.build. Gradle + +``` +ext['spring.version']='5.3.16' +``` + +如果你使用了额外的功能(例如 LDAP、OpenID 和其他功能),则还需要包括适当的[项目模块和依赖项](modules.html#modules)。 + +### Gradle 没有靴子 + +当使用 Spring 安全性而不启动 Spring 时,首选的方法是使用 Spring 安全性的 BOM,以确保在整个项目中使用 Spring 安全性的一致版本。你可以通过使用[依赖管理插件 Name](https://github.com/spring-gradle-plugins/dependency-management-plugin)来实现这一点,如下例所示: + +例 12.build. Gradle + +``` +plugins { + id "io.spring.dependency-management" version "1.0.6.RELEASE" +} + +dependencyManagement { + imports { + mavenBom 'org.springframework.security:spring-security-bom:5.6.2' + } +} +``` + +Spring 安全性 Maven 依赖性的最小集合通常如下所示: + +例 13.build. Gradle + +``` +dependencies { + compile "org.springframework.security:spring-security-web" + compile "org.springframework.security:spring-security-config" +} +``` + +如果你使用了额外的功能(例如 LDAP、OpenID 和其他功能),则还需要包括适当的[项目模块和依赖项](modules.html#modules)。 + +Spring 安全性是根据 Spring Framework5.3.16 构建的,但通常应该与 Spring Framework5.x 的任何较新版本一起工作。 Spring 安全性的传递依赖关系解决了 Spring Framework5.3.16,这可能会导致奇怪的 Classpath 问题,许多用户可能会对此感到不满。解决此问题的最简单方法是在`pom.xml`的``部分中使用`spring-framework-bom`。你可以通过使用[依赖管理插件 Name](https://github.com/spring-gradle-plugins/dependency-management-plugin)来实现这一点,如下例所示: + +示例 14.build. Gradle + +``` +plugins { + id "io.spring.dependency-management" version "1.0.6.RELEASE" +} + +dependencyManagement { + imports { + mavenBom 'org.springframework:spring-framework-bom:5.3.16' + } +} +``` + +前面的示例确保 Spring 安全性的所有传递依赖使用 Spring 5.3.16 模块。 + +### Gradle 存储库 + +所有 GA 版本(即以.release 结尾的版本)都部署到 Maven Central,因此对于 GA 版本,使用 MavenCentral()存储库就足够了。下面的示例展示了如何做到这一点: + +例 15.build. Gradle + +``` +repositories { + mavenCentral() +} +``` + +如果使用快照版本,则需要确保定义了 Spring 快照存储库,如下例所示: + +示例 16.build. Gradle + +``` +repositories { + maven { url 'https://repo.spring.io/snapshot' } +} +``` + +如果使用里程碑或发布候选版本,则需要确保定义了 Spring 里程碑存储库,如下例所示: + +示例 17.build. Gradle + +``` +repositories { + maven { url 'https://repo.spring.io/milestone' } +} +``` \ No newline at end of file diff --git a/docs/spring-security/modules.md b/docs/spring-security/modules.md new file mode 100644 index 0000000000000000000000000000000000000000..fff4dc8b8a3a9bb7226e6af575626d4382f21b91 --- /dev/null +++ b/docs/spring-security/modules.md @@ -0,0 +1,163 @@ +# 项目模块和依赖项 + +即使你不使用 Maven,我们也建议你查阅`pom.xml`文件,以了解第三方的依赖关系和版本。另一个好主意是检查示例应用程序中包含的库。 + +本节提供了 Spring Security 中的模块的参考,以及它们在运行中的应用程序中起作用所需的附加依赖关系。我们不包括仅在构建或测试 Spring 安全性本身时使用的依赖关系。我们也不包括外部依赖项所要求的传递依赖项。 + +项目网站上列出了 Spring Required 的版本,因此在下面的 Spring 依赖项中省略了具体的版本。请注意,在 Spring 应用程序中的其他非安全功能可能仍然需要下面列出的一些“可选”依赖项。此外,如果在大多数应用程序中使用了被列为“可选”的依赖项,那么它们在项目的 Maven POM 文件中实际上可能不会被标记为可选的依赖项。它们是“可选的”,只是因为除非你使用指定的功能,否则你不需要它们。 + +当一个模块依赖于另一个 Spring 安全模块时,它所依赖的模块的非可选依赖也被认为是必需的,并且不会单独列出。 + +## 核心—`spring-security-core.jar` + +该模块包含核心身份验证和访问控制类和接口、远程支持和基本供应 API。任何使用 Spring 安全性的应用程序都需要它。它支持独立的应用程序、远程客户机、方法(服务层)安全性和 JDBC 用户配置。它包含以下顶级包: + +* `org.springframework.security.core` + +* `org.springframework.security.access` + +* `org.springframework.security.authentication` + +* `org.springframework.security.provisioning` + +| Dependency |Version|说明| +|-----------------|-------|---------------------------------------------------------------------------| +| ehcache | 1.6.2 |如果使用基于 EHCache 的用户缓存实现,则需要(可选)。| +| spring-aop | |方法安全性基于 Spring AOP| +| spring-beans | |Spring 配置所需| +|spring-expression| |基于表达式的方法安全性所需(可选)| +| spring-jdbc | |如果使用数据库存储用户数据(可选),则需要.| +| spring-tx | |如果使用数据库存储用户数据(可选),则需要.| +| aspectjrt |1.6.10 |如果使用 AspectJ 支持(可选),则是必需的。| +| jsr250-api | 1.0 |如果你使用的是 JSR-250 方法-安全注释(可选),则需要这样做。| + +## remoting—`spring-security-remoting.jar` + +该模块提供了与 Spring 远程的集成。你不需要这样做,除非你正在编写使用远程处理的远程客户机。主包是`org.springframework.security.remoting`。 + +| Dependency |Version|说明| +|--------------------|-------|-----------------------------------------------------| +|spring-security-core| | | +| spring-web | |对于使用 HTTP 远程支持的客户端来说是必需的。| + +## web—`spring-security-web.jar` + +这个模块包含过滤器和相关的 Web 安全基础设施代码。它包含任何具有 Servlet API 依赖关系的内容。如果你需要 Spring 安全性 Web 身份验证服务和基于 URL 的访问控制,那么你就需要它。主包是`org.springframework.security.web`。 + +| Dependency |Version|说明| +|--------------------|-------|-------------------------------------------------------------------------------| +|spring-security-core| | | +| spring-web | |Spring Web 支持类被广泛使用。| +| spring-jdbc | |基于 JDBC 的持久 Rememe-Me 令牌存储库所需(可选).| +| spring-tx | |Rememe-Me 持久令牌存储库实现所需(可选).| + +## 配置—`spring-security-config.jar` + +这个模块包含安全名称空间解析代码和 Java 配置代码。如果你使用 Spring Security XML 名称空间进行配置或 Spring Security 的 Java 配置支持,那么你就需要它。主包是`org.springframework.security.config`。这些类都不打算在应用程序中直接使用。 + +| Dependency |Version|说明| +|----------------------|-------|-----------------------------------------------------------------------------| +| spring-security-core | | | +| spring-security-web | |如果你正在使用任何与 Web 相关的名称空间配置(可选),则需要这样做。| +| spring-security-ldap | |如果你正在使用 LDAP 命名空间选项(可选),则是必需的。| +|spring-security-openid| |如果你使用的是 OpenID 身份验证(可选的),则需要这样做。| +| aspectjweaver |1.6.10 |如果使用 protect-pointcut 命名空间语法(可选),则需要.| + +## LDAP—`spring-security-ldap.jar` + +该模块提供 LDAP 身份验证和配置代码。如果你需要使用 LDAP 身份验证或管理 LDAP 用户条目,则需要使用它。顶级包是`org.springframework.security.ldap`。 + +| Dependency |Version|说明| +|-----------------------------------------------------------------------------------------------------------------------------------|-------|-----------------------------------------------------------------------------------------------------------------------------------------------| +| spring-security-core | | | +| spring-ldap-core | 1.3.0 |LDAP 支持基于 Spring LDAP。| +| spring-tx | |数据异常类是必需的。| +|apache-ds [1]| 1.5.5 |如果你使用的是嵌入式 LDAP 服务器(可选),则需要这样做。| +| shared-ldap |0.9.15 |如果你使用的是嵌入式 LDAP 服务器(可选),则需要这样做。| +| ldapsdk | 4.1 |例如,如果你使用 OpenLDAP 的密码策略功能,则使用 Mozilla ldapsdk.对 LDAP 密码策略控件进行解码。| + +## OAuth2.0 核心—`spring-security-oauth2-core.jar` + +`spring-security-oauth2-core.jar`包含支持 OAuth2.0 授权框架和 OpenID Connect Core1.0 的核心类和接口。它是使用 OAuth2.0 或 OpenID Connect Core1.0 的应用程序所必需的,例如客户机、资源服务器和授权服务器。顶级包是`org.springframework.security.oauth2.core`。 + +## OAuth2.0 客户端—`spring-security-oauth2-client.jar` + +`spring-security-oauth2-client.jar`包含 Spring Security 对 OAuth2.0 授权框架和 OpenID Connect Core1.0 的客户端支持。它是使用 OAuth2.0 登录或 OAuth 客户端支持的应用程序所必需的。顶级包是`org.springframework.security.oauth2.client`。 + +## OAuth2.0Jose—`spring-security-oauth2-jose.jar` + +`spring-security-oauth2-jose.jar`包含 Spring Security 对 Jose(JavaScript 对象签名和加密)框架的支持。Jose 框架旨在提供一种在各方之间安全地转移权利要求的方法。它是由一系列规范构建而成的: + +* JSON Web Token + +* JSON Web 签名 + +* JSON 网络加密 + +* JSON Web Key + +它包含以下顶级包: + +* `org.springframework.security.oauth2.jwt` + +* `org.springframework.security.oauth2.jose` + +## OAuth2.0 资源服务器—`spring-security-oauth2-resource-server.jar` + +`spring-security-oauth2-resource-server.jar`包含 Spring Security 对 OAuth2.0 资源服务器的支持。它用于通过 OAuth2.0 承载令牌保护 API。顶级包是`org.springframework.security.oauth2.server.resource`。 + +## ACL—`spring-security-acl.jar` + +这个模块包含一个专门的域对象 ACL 实现。它用于将安全性应用于应用程序中的特定域对象实例。顶级包是`org.springframework.security.acls`。 + +| Dependency |Version|说明| +|--------------------|-------|-------------------------------------------------------------------------------------------------------------------| +|spring-security-core| | | +| ehcache | 1.6.2 |如果使用了基于 EHCache 的 ACL 缓存实现,则需要(如果你使用自己的实现,则可以选择)。| +| spring-jdbc | |如果你使用的是默认的基于 JDBC 的 ACLService(如果你实现了自己的 ACLService,则是可选的),则是必需的。| +| spring-tx | |如果你使用的是默认的基于 JDBC 的 ACLService(如果你实现了自己的 ACLService,则是可选的),则是必需的。| + +## 化学文摘社—`spring-security-cas.jar` + +该模块包含 Spring Security 的 CAS 客户端集成。如果你想对 CAS 单点登录服务器使用 Spring 安全性 Web 身份验证,那么你应该使用它。顶级包是`org.springframework.security.cas`。 + +| Dependency |Version|说明| +|--------------------|-------|--------------------------------------------------------------------------------| +|spring-security-core| | | +|spring-security-web | | | +| cas-client-core |3.1.12 |JA-SIG CAS 客户机.
这是 Spring 安全集成的基础。| +| ehcache | 1.6.2 |如果你正在使用基于 EHCache 的票证缓存(可选),则需要这样做。| + +## OpenID—`spring-security-openid.jar` + +| |OpenID1.0 和 2.0 协议已被弃用,并鼓励用户迁移到 OpenID Connect,这得到了 Spring-Security-OAuth2 的支持。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------| + +此模块包含对 OpenID Web 身份验证的支持。它用于针对外部 OpenID 服务器对用户进行身份验证。顶级包是`org.springframework.security.openid`。它需要 OpenID4Java。 + +| Dependency |Version|说明| +|--------------------|-------|------------------------------------------------------| +|spring-security-core| | | +|spring-security-web | | | +| openid4java-nodeps | 0.9.6 |Spring Security 的 OpenID 集成使用了 OpenID4Java。| +| httpclient | 4.1.1 |OpenID4Java-Nodeps 依赖于 HttpClient4。| +| guice | 2.0 |OpenID4Java-Nodeps 依赖于 Guice2。| + +## 测试—`spring-security-test.jar` + +该模块包含对具有 Spring 安全性的测试的支持。 + +## taglibs—`spring-secuity-taglibs.jar` + +提供 Spring 安全性的 JSP 标记实现。 + +| Dependency |Version|说明| +|--------------------|-------|------------------------------------------------------------------------------------------------------------| +|spring-security-core| | | +|spring-security-web | | | +|spring-security-acl | |如果使用带有 ACLS 的`accesscontrollist`标记或`hasPermission()`表达式(可选),则需要这样做。| +| spring-expression | |如果你在标记访问约束中使用 SPEL 表达式,则需要这样做。| + +--- + +[1](#_footnoteref_1)。需要的模块有`apacheds-core`,`apacheds-core-entry`,`apacheds-protocol-shared`,`apacheds-protocol-ldap`和`apacheds-server-jndi`。 \ No newline at end of file diff --git a/docs/spring-security/overview.md b/docs/spring-security/overview.md new file mode 100644 index 0000000000000000000000000000000000000000..b9f0a75875f1f848ff6698525ffae2a31bea87b5 --- /dev/null +++ b/docs/spring-security/overview.md @@ -0,0 +1,13 @@ +# Spring 安全 + +Spring 安全性是提供[认证](features/authentication/index.html)、[授权](features/authorization/index.html)和[防范常见攻击](features/exploits/index.html)的框架。由于对[imperative](servlet/index.html)和[reactive](reactive/index.html)应用程序的安全都提供了第一类支持,因此它是保护基于 Spring 的应用程序的事实上的标准。 + +有关功能的完整列表,请参见引用的[Features](features/index.html)部分。 + +## 开始 + +如果你准备好开始保护应用程序,请参阅[servlet](servlet/getting-started.html)和[reactive](reactive/getting-started.html)的入门部分。这些部分将引导你创建你的第一个安全应用程序。 + +如果你想了解 Spring 安全性是如何工作的,那么可以参考[建筑](servlet/architecture.html)部分。 + +如果你有任何问题,有一个很棒的[community](community.html)将很乐意帮助你! \ No newline at end of file diff --git a/docs/spring-security/prerequisites.md b/docs/spring-security/prerequisites.md new file mode 100644 index 0000000000000000000000000000000000000000..18221384af11a47f3d0b45ad3ef38e897d8cee6f --- /dev/null +++ b/docs/spring-security/prerequisites.md @@ -0,0 +1,9 @@ +# 先决条件 + +Spring 安全性要求 Java8 或更高的运行时环境。 + +Spring 由于安全性旨在以一种自包含的方式进行操作,因此你不需要在 Java 运行时环境中放置任何特殊的配置文件。特别是,你不需要配置特殊的 Java 身份验证和授权服务策略文件,也不需要将 Spring 安全性放入公共 Classpath 位置。 + +类似地,如果使用 EJB 容器或 Servlet 容器,则不需要在任何地方放置任何特殊的配置文件,也不需要在服务器类加载器中包含 Spring 安全性。所有必需的文件都包含在你的应用程序中。 + +这种设计提供了最大的部署时间灵活性,因为你可以将目标工件(无论是 JAR、WAR 还是 EAR)从一个系统复制到另一个系统,并且它可以立即工作。 \ No newline at end of file diff --git a/docs/spring-security/reactive-authentication-logout.md b/docs/spring-security/reactive-authentication-logout.md new file mode 100644 index 0000000000000000000000000000000000000000..617591acafd5fce69656c3bc8927ba4fe747971b --- /dev/null +++ b/docs/spring-security/reactive-authentication-logout.md @@ -0,0 +1,24 @@ +# 注销 + +Spring 安全性默认情况下提供注销端点。登录后,你可以`GET /logout`查看默认的注销确认页,或者`POST /logout`启动注销。这将: + +* 清除`ServerCsrfTokenRepository`,`ServerSecurityContextRepository`,并 + +* 重定向回登录页面 + +通常,你也希望注销时的会话无效。为了实现这一点,你可以将`WebSessionServerLogoutHandler`添加到你的注销配置中,如下所示: + +``` +@Bean +SecurityWebFilterChain http(ServerHttpSecurity http) throws Exception { + DelegatingServerLogoutHandler logoutHandler = new DelegatingServerLogoutHandler( + new WebSessionServerLogoutHandler(), new SecurityContextServerLogoutHandler() + ); + + http + .authorizeExchange((exchange) -> exchange.anyExchange().authenticated()) + .logout((logout) -> logout.logoutHandler(logoutHandler)); + + return http.build(); +} +``` \ No newline at end of file diff --git a/docs/spring-security/reactive-authentication-x509.md b/docs/spring-security/reactive-authentication-x509.md new file mode 100644 index 0000000000000000000000000000000000000000..96dc86a0fd850ac50051ee7908671d58fe1ae4c5 --- /dev/null +++ b/docs/spring-security/reactive-authentication-x509.md @@ -0,0 +1,91 @@ +# 反应式 X.509 认证 + +与[Servlet X.509 authentication](../../servlet/authentication/x509.html#servlet-x509)类似,Active X509 身份验证过滤器允许从客户端提供的证书中提取身份验证令牌。 + +下面是一个反应式 X509 安全配置的示例: + +爪哇 + +``` +@Bean +public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { + http + .x509(withDefaults()) + .authorizeExchange(exchanges -> exchanges + .anyExchange().permitAll() + ); + return http.build(); +} +``` + +Kotlin + +``` +@Bean +fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + x509 { } + authorizeExchange { + authorize(anyExchange, authenticated) + } + } +} +``` + +在上面的配置中,当`principalExtractor`和`authenticationManager`都不提供时,将使用默认值。默认的主体提取器是`SubjectDnX509PrincipalExtractor`,它从客户机提供的证书中提取 CN(通用名称)字段。默认的身份验证管理器是`ReactivePreAuthenticatedAuthenticationManager`,它执行用户帐户验证,检查具有`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(); +} +``` + +Kotlin + +``` +@Bean +fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain? { + val customPrincipalExtractor = SubjectDnX509PrincipalExtractor() + customPrincipalExtractor.setSubjectDnRegex("OU=(.*?)(?:,|$)") + val customAuthenticationManager = ReactiveAuthenticationManager { authentication: Authentication -> + authentication.isAuthenticated = "Trusted Org Unit" == authentication.name + Mono.just(authentication) + } + return http { + x509 { + principalExtractor = customPrincipalExtractor + authenticationManager = customAuthenticationManager + } + authorizeExchange { + authorize(anyExchange, authenticated) + } + } +} +``` + +在此示例中,从客户端证书的 OU 字段中提取用户名,而不是从 CN 中提取用户名,并且根本不执行使用`ReactiveUserDetailsService`的帐户查找。相反,如果将提供的证书颁发给名为“可信的组织单元”的 OU,则将对请求进行身份验证。 + +有关配置 Netty 和`WebClient`或`curl`命令行工具以使用相互 TLS 并启用 X.509 身份验证的示例,请参见[https://github.com/spring-projects/spring-security-samples/tree/main/servlet/java-configuration/authentication/x509](https://github.com/spring-projects/spring-security-samples/tree/main/servlet/java-configuration/authentication/x509)。 \ No newline at end of file diff --git a/docs/spring-security/reactive-authorization-authorize-http-requests.md b/docs/spring-security/reactive-authorization-authorize-http-requests.md new file mode 100644 index 0000000000000000000000000000000000000000..b81351707a6bf073134ac1c4def5b4b799c00aa4 --- /dev/null +++ b/docs/spring-security/reactive-authorization-authorize-http-requests.md @@ -0,0 +1,92 @@ +# 授权 ServerHttpRequest + +Spring 安全性为授权传入的 HTTP 请求提供了支持。默认情况下, Spring Security 的授权将要求对所有请求进行身份验证。显式配置如下所示: + +例 1。所有请求都需要经过身份验证的用户。 + +爪哇 + +``` +@Bean +SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { + http + .authorizeExchange(exchanges -> exchanges + .anyExchange().authenticated() + ) + .httpBasic(withDefaults()) + .formLogin(withDefaults()); + return http.build(); +} +``` + +Kotlin + +``` +@Bean +fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + authorizeExchange { + authorize(anyExchange, authenticated) + } + formLogin { } + httpBasic { } + } +} +``` + +我们可以通过按优先级顺序添加更多规则来配置 Spring 安全性,使其具有不同的规则。 + +例 2。多个授权请求规则 + +爪哇 + +``` +import static org.springframework.security.authorization.AuthorityReactiveAuthorizationManager.hasRole; +// ... +@Bean +SecurityWebFilterChain springWebFilterChain(ServerHttpSecurity http) { + // @formatter:off + http + // ... + .authorizeExchange((authorize) -> authorize (1) + .pathMatchers("/resources/**", "/signup", "/about").permitAll() (2) + .pathMatchers("/admin/**").hasRole("ADMIN") (3) + .pathMatchers("/db/**").access((authentication, context) -> (4) + hasRole("ADMIN").check(authentication, context) + .filter(decision -> !decision.isGranted()) + .switchIfEmpty(hasRole("DBA").check(authentication, context)) + ) + .anyExchange().denyAll() (5) + ); + // @formatter:on + return http.build(); +} +``` + +Kotlin + +``` +@Bean +fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + authorizeExchange { (1) + authorize(pathMatchers("/resources/**", "/signup", "/about"), permitAll) (2) + authorize("/admin/**", hasRole("ADMIN")) (3) + authorize("/db/**", { authentication, context -> (4) + hasRole("ADMIN").check(authentication, context) + .filter({ decision -> !decision.isGranted() }) + .switchIfEmpty(hasRole("DBA").check(authentication, context)) + }) + authorize(anyExchange, denyAll) (5) + } + // ... + } +} +``` + +|**1**|指定了多个授权规则。
每个规则都按照它们被声明的顺序被考虑。| +|-----|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +|**2**|我们指定了任何用户都可以访问的多个 URL 模式。
具体来说,如果 URL 以“/resources/”开头,等于“/signup”或等于“/about”,则任何用户都可以访问请求。| +|**3**|任何以“/admin/”开头的 URL 都将被限制为具有“role\_admin”权限的用户。
你将注意到,由于我们正在调用`hasRole`方法,因此我们不需要指定“role\_”前缀。| +|**4**|任何以“/db/”开头的 URL 都需要用户同时具有“role\_admin”和“role\_DBA”。
这表明了提供自定义`ReactiveAuthorizationManager`的灵活性,允许我们实现任意的授权逻辑。
为了简单起见,示例使用 lambda 并将其委托给现有的`AuthorityReactiveAuthorizationManager.hasRole`实现。
但是,在实际情况下,应用程序可能会在实现`ReactiveAuthorizationManager`的适当类中实现该逻辑。| +|**5**|任何尚未匹配的 URL 都将被拒绝访问。
如果你不想意外地忘记更新授权规则,这是一个很好的策略。| \ No newline at end of file diff --git a/docs/spring-security/reactive-authorization-method.md b/docs/spring-security/reactive-authorization-method.md new file mode 100644 index 0000000000000000000000000000000000000000..2d1373e3b7485b8f34f256ae719b61016a8761b1 --- /dev/null +++ b/docs/spring-security/reactive-authorization-method.md @@ -0,0 +1,213 @@ +# EnableReactiveMethodSecurity + +Spring 安全性支持使用[反应堆的背景](https://projectreactor.io/docs/core/release/reference/#context)的方法安全性,其设置使用`ReactiveSecurityContextHolder`。例如,这演示了如何检索当前登录的用户消息。 + +| |要使其工作,方法的返回类型必须是`org.reactivestreams.Publisher`(即`Mono`/`Flux`),或者函数必须是 Kotlin 协程函数。
这是与反应器的`Context`积分所必需的。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +爪哇 + +``` +Authentication authentication = new TestingAuthenticationToken("user", "password", "ROLE_USER"); + +Mono 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(); +``` + +Kotlin + +``` +val authentication: Authentication = TestingAuthenticationToken("user", "password", "ROLE_USER") + +val messageByUsername: Mono = 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 findMessageByUsername(String username) { + return Mono.just("Hi " + username); +} +``` + +Kotlin + +``` +fun findMessageByUsername(username: String): Mono { + 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); + } +} +``` + +Kotlin + +``` +@EnableReactiveMethodSecurity +class SecurityConfig { + @Bean + fun userDetailsService(): MapReactiveUserDetailsService { + val userBuilder: User.UserBuilder = User.withDefaultPasswordEncoder() + val rob = userBuilder.username("rob") + .password("rob") + .roles("USER") + .build() + val admin = userBuilder.username("admin") + .password("admin") + .roles("USER", "ADMIN") + .build() + return MapReactiveUserDetailsService(rob, admin) + } +} +``` + +考虑以下类: + +爪哇 + +``` +@Component +public class HelloWorldMessageService { + @PreAuthorize("hasRole('ADMIN')") + public Mono findMessage() { + return Mono.just("Hello World!"); + } +} +``` + +Kotlin + +``` +@Component +class HelloWorldMessageService { + @PreAuthorize("hasRole('ADMIN')") + fun findMessage(): Mono { + return Mono.just("Hello World!") + } +} +``` + +或者,使用 Kotlin 协程的下列类: + +Kotlin + +``` +@Component +class HelloWorldMessageService { + @PreAuthorize("hasRole('ADMIN')") + suspend fun findMessage(): String { + delay(10) + return "Hello World!" + } +} +``` + +结合上面的配置,`@PreAuthorize("hasRole('ADMIN')")`将确保`findByMessage`仅由具有`ADMIN`角色的用户调用。需要注意的是,标准方法安全性中的任何表达式都可以用于`@EnableReactiveMethodSecurity`。但是,此时我们只支持返回类型为`Boolean`或`boolean`的表达式。这意味着表达式不能阻塞。 + +当与[WebFlux 安全性](../configuration/webflux.html#jc-webflux)集成时,反应器上下文由 Spring 安全性根据经过身份验证的用户自动建立。 + +爪哇 + +``` +@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); + } +} +``` + +Kotlin + +``` +@EnableWebFluxSecurity +@EnableReactiveMethodSecurity +class SecurityConfig { + @Bean + open fun springWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + authorizeExchange { + authorize(anyExchange, permitAll) + } + httpBasic { } + } + } + + @Bean + fun userDetailsService(): MapReactiveUserDetailsService { + val userBuilder: User.UserBuilder = User.withDefaultPasswordEncoder() + val rob = userBuilder.username("rob") + .password("rob") + .roles("USER") + .build() + val admin = userBuilder.username("admin") + .password("admin") + .roles("USER", "ADMIN") + .build() + return MapReactiveUserDetailsService(rob, admin) + } +} +``` \ No newline at end of file diff --git a/docs/spring-security/reactive-configuration-webflux.md b/docs/spring-security/reactive-configuration-webflux.md new file mode 100644 index 0000000000000000000000000000000000000000..0ceb74656931b54b872c2edf3087ffbbd9185c38 --- /dev/null +++ b/docs/spring-security/reactive-configuration-webflux.md @@ -0,0 +1,217 @@ +# WebFlux 安全性 + +Spring Security 的 WebFlux 支持依赖于`WebFilter`,并且对 Spring WebFlux 和 Spring WebFlux.FN 的工作原理相同。你可以找到一些示例应用程序来演示下面的代码: + +* Hello WebFlux[HelloWebFlux ](https://github.com/spring-projects/spring-security-samples/tree/5.6.x/reactive/webflux/java/hello-security) + +* FN[HelloWebFluxFN ](https://github.com/spring-projects/spring-security-samples/tree/5.6.x/reactive/webflux-fn/hello-security) + +* Hello WebFlux 方法[HelloWebFlux 方法](https://github.com/spring-projects/spring-security-samples/tree/5.6.x/reactive/webflux/java/method) + +## 最小 WebFlux 安全配置 + +你可以在下面找到最小的 WebFlux 安全配置: + +例 1。最小 WebFlux 安全配置 + +爪哇 + +``` +@EnableWebFluxSecurity +public class HelloWebfluxSecurityConfig { + + @Bean + public MapReactiveUserDetailsService userDetailsService() { + UserDetails user = User.withDefaultPasswordEncoder() + .username("user") + .password("user") + .roles("USER") + .build(); + return new MapReactiveUserDetailsService(user); + } +} +``` + +Kotlin + +``` +@EnableWebFluxSecurity +class HelloWebfluxSecurityConfig { + + @Bean + fun userDetailsService(): ReactiveUserDetailsService { + val userDetails = User.withDefaultPasswordEncoder() + .username("user") + .password("user") + .roles("USER") + .build() + return MapReactiveUserDetailsService(userDetails) + } +} +``` + +该配置提供表单和 HTTP 基本身份验证,设置授权以要求经过身份验证的用户访问任何页面,设置默认的登录页面和默认的注销页面,设置与安全相关的 HTTP 标题,CSRF 保护,等等。 + +## 显式 WebFlux 安全配置 + +你可以在下面找到最小 WebFlux 安全配置的显式版本: + +例 2。显式 WebFlux 安全配置 + +爪哇 + +``` +@Configuration +@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(); + } +} +``` + +Kotlin + +``` +@Configuration +@EnableWebFluxSecurity +class HelloWebfluxSecurityConfig { + + @Bean + fun userDetailsService(): ReactiveUserDetailsService { + val userDetails = User.withDefaultPasswordEncoder() + .username("user") + .password("user") + .roles("USER") + .build() + return MapReactiveUserDetailsService(userDetails) + } + + @Bean + fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + authorizeExchange { + authorize(anyExchange, authenticated) + } + formLogin { } + httpBasic { } + } + } +} +``` + +这个配置显式地设置了与我们的最小配置相同的所有内容。从这里,你可以轻松地对默认值进行更改。 + +通过在`config/src/test/`目录中搜索[enableWebfluxsecurity](https://github.com/ Spring-projects/ Spring-security/search?q=path%3aconfig%2fsrc%2ftest%2f+enableWebfluxsecurity),你可以在单元测试中找到更多显式配置的示例。 + +### 多链支撑 + +你可以通过`RequestMatcher`s 将多个`SecurityWebFilterChain`实例配置为单独的配置。 + +例如,可以为以`/api`开头的 URL 隔离配置,如下所示: + +爪哇 + +``` +@Configuration +@EnableWebFluxSecurity +static class MultiSecurityHttpConfig { + + @Order(Ordered.HIGHEST_PRECEDENCE) (1) + @Bean + SecurityWebFilterChain apiHttpSecurity(ServerHttpSecurity http) { + http + .securityMatcher(new PathPatternParserServerWebExchangeMatcher("/api/**")) (2) + .authorizeExchange((exchanges) -> exchanges + .anyExchange().authenticated() + ) + .oauth2ResourceServer(OAuth2ResourceServerSpec::jwt); (3) + return http.build(); + } + + @Bean + SecurityWebFilterChain webHttpSecurity(ServerHttpSecurity http) { (4) + http + .authorizeExchange((exchanges) -> exchanges + .anyExchange().authenticated() + ) + .httpBasic(withDefaults()); (5) + return http.build(); + } + + @Bean + ReactiveUserDetailsService userDetailsService() { + return new MapReactiveUserDetailsService( + PasswordEncodedUser.user(), PasswordEncodedUser.admin()); + } + +} +``` + +Kotlin + +``` +@Configuration +@EnableWebFluxSecurity +open class MultiSecurityHttpConfig { + @Order(Ordered.HIGHEST_PRECEDENCE) (1) + @Bean + open fun apiHttpSecurity(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + securityMatcher(PathPatternParserServerWebExchangeMatcher("/api/**")) (2) + authorizeExchange { + authorize(anyExchange, authenticated) + } + oauth2ResourceServer { + jwt { } (3) + } + } + } + + @Bean + open fun webHttpSecurity(http: ServerHttpSecurity): SecurityWebFilterChain { (4) + return http { + authorizeExchange { + authorize(anyExchange, authenticated) + } + httpBasic { } (5) + } + } + + @Bean + open fun userDetailsService(): ReactiveUserDetailsService { + return MapReactiveUserDetailsService( + PasswordEncodedUser.user(), PasswordEncodedUser.admin() + ) + } +} +``` + +|**1**|将`SecurityWebFilterChain`配置为`@Order`,以指定安全应该首先考虑哪个`SecurityWebFilterChain` Spring| +|-----|------------------------------------------------------------------------------------------------------------------------------------------------| +|**2**|使用`PathPatternParserServerWebExchangeMatcher`声明此`SecurityWebFilterChain`将仅适用于以`/api/`开头的 URL 路径| +|**3**|指定将用于`/api/**`端点的身份验证机制| +|**4**|创建另一个优先级较低的`SecurityWebFilterChain`实例,以匹配所有其他 URL| +|**5**|指定将用于应用程序其余部分的身份验证机制| + +Spring 安全性将为每个请求选择一个`SecurityWebFilterChain``@Bean`。它将按照`securityMatcher`定义的顺序匹配请求。 + +在这种情况下,这意味着如果 URL 路径以`/api`开始,那么 Spring Security 将使用`apiHttpSecurity`。如果 URL 不以`/api`开头,则 Spring Security 将默认为`webHttpSecurity`,其中隐含的`securityMatcher`与任何请求匹配。 \ No newline at end of file diff --git a/docs/spring-security/reactive-exploits-csrf.md b/docs/spring-security/reactive-exploits-csrf.md new file mode 100644 index 0000000000000000000000000000000000000000..e5b24875fce68fd766430d7471ff842bd86d2829 --- /dev/null +++ b/docs/spring-security/reactive-exploits-csrf.md @@ -0,0 +1,340 @@ +# WebFlux 环境中的跨站点请求伪造 + +本节讨论 Spring Security 对 WebFlux 环境的[跨站点请求伪造](../../features/exploits/csrf.html#csrf)支持。 + +## 使用 Spring 安全 CSRF 保护 + +使用 Spring Security 的 CSRF 保护的步骤概述如下: + +* [使用适当的 HTTP 动词](#webflux-csrf-idempotent) + +* [配置 CSRF 保护](#webflux-csrf-configure) + +* [包括 CSRF 令牌](#webflux-csrf-include) + +### 使用适当的 HTTP 动词 + +防止 CSRF 攻击的第一步是确保你的网站使用正确的 HTTP 动词。这在[安全的方法必须是幂等的。](../../features/exploits/csrf.html#csrf-protection-idempotent)中有详细介绍。 + +### 配置 CSRF 保护 + +下一步是在应用程序中配置 Spring Security 的 CSRF 保护。 Spring 默认情况下,Security 的 CSRF 保护是启用的,但你可能需要定制配置。下面是一些常见的定制。 + +#### 自定义 CSRFTokenRepository + +默认情况下, Spring Security 使用`WebSessionServerCsrfTokenRepository`将预期的 CSRF 令牌存储在`WebSession`中。在某些情况下,用户可能希望配置自定义`ServerCsrfTokenRepository`。例如,可能希望将 cookie 中的`CsrfToken`持久化到[支持基于 爪哇Script 的应用程序](#webflux-csrf-include-ajax-auto)。 + +默认情况下,`CookieServerCsrfTokenRepository`将写到一个名为`XSRF-TOKEN`的 cookie,并从一个名为`X-XSRF-TOKEN`的头部或 HTTP 参数`_csrf`读取它。这些默认值来自[AngularJS](https://docs.angularjs.org/api/ng/service/$http#cross-site-request-forgery-xsrf-protection) + +你可以在 爪哇 配置中使用以下方法配置`CookieServerCsrfTokenRepository`: + +例 1。在 cookie 中存储 CSRF 令牌 + +爪哇 + +``` +@Bean +public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { + http + // ... + .csrf(csrf -> csrf.csrfTokenRepository(CookieServerCsrfTokenRepository.withHttpOnlyFalse())) + return http.build(); +} +``` + +Kotlin + +``` +@Bean +fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + // ... + csrf { + csrfTokenRepository = CookieServerCsrfTokenRepository.withHttpOnlyFalse() + } + } +} +``` + +| |示例显式设置`cookieHttpOnly=false`.
这是允许 爪哇Script(即 Angularjs)读取它所必需的。
如果不需要直接使用 爪哇Script 读取 cookie 的能力,建议省略`cookieHttpOnly=false`(通过使用`new CookieServerCsrfTokenRepository()`代替)以提高安全性。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 禁用 CSRF 保护 + +默认情况下启用了 CSRF 保护。但是,如果 CSRF 保护[对你的应用程序来说是有意义的](../../features/exploits/csrf.html#csrf-when),则禁用 CSRF 保护非常简单。 + +下面的 Java 配置将禁用 CSRF 保护。 + +例 2。禁用 CSRF 配置 + +Java + +``` +@Bean +public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { + http + // ... + .csrf(csrf -> csrf.disable())) + return http.build(); +} +``` + +Kotlin + +``` +@Bean +fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + // ... + csrf { + disable() + } + } +} +``` + +### 包括 CSRF 令牌 + +为了使[同步器令牌模式](../../features/exploits/csrf.html#csrf-protection-stp)能够抵御 CSRF 攻击,我们必须在 HTTP 请求中包含实际的 CSRF 令牌。这必须包含在请求的一部分(即表单参数、HTTP 头等)中,而该部分不是由浏览器自动包含在 HTTP 请求中的。 + +Spring Security 的[CSRFWebfilter ](https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/web/server/csrf/CsrfWebFilter.html)将[Mono\](https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/web/csrf/CsrfToken.html)公开为名为`ServerWebExchange`的属性。这意味着,任何视图技术都可以访问`Mono`以将预期的令牌公开为[form](#webflux-csrf-include-form-attr)或[meta tag](#webflux-csrf-include-ajax-meta)。 + +如果你的视图技术没有提供订阅`Mono`的简单方法,那么一个常见的模式是使用 Spring 的`@ControllerAdvice`直接公开`CsrfToken`。例如,以下代码将在 Spring Security 的[CsrfrequestDataValueProcessor ](#webflux-csrf-include-form-auto)使用的默认属性名(`_csrf`)上放置`CsrfToken`,以自动将 CSRF 令牌作为隐藏输入。 + +例 3。`CsrfToken`as`@ModelAttribute` + +Java + +``` +@ControllerAdvice +public class SecurityControllerAdvice { + @ModelAttribute + Mono csrfToken(ServerWebExchange exchange) { + Mono csrfToken = exchange.getAttribute(CsrfToken.class.getName()); + return csrfToken.doOnSuccess(token -> exchange.getAttributes() + .put(CsrfRequestDataValueProcessor.DEFAULT_CSRF_ATTR_NAME, token)); + } +} +``` + +Kotlin + +``` +@ControllerAdvice +class SecurityControllerAdvice { + @ModelAttribute + fun csrfToken(exchange: ServerWebExchange): Mono { + val csrfToken: Mono? = exchange.getAttribute(CsrfToken::class.java.name) + return csrfToken!!.doOnSuccess { token -> + exchange.attributes[CsrfRequestDataValueProcessor.DEFAULT_CSRF_ATTR_NAME] = token + } + } +} +``` + +幸运的是,ThymeLeaf 提供了[整合](#webflux-csrf-include-form-auto),它的工作不需要任何额外的工作。 + +#### 表单 URL 编码 + +为了发布 HTML 表单,CSRF 令牌必须作为隐藏输入包含在表单中。例如,呈现的 HTML 可能看起来像: + +例 4。CSRF 令牌 HTML + +``` + +``` + +接下来,我们将讨论将 CSRF 令牌以一种形式包含为隐藏输入的各种方法。 + +##### CSRF 令牌自动包含 + +Spring Security 的 CSRF 支持通过其[CsrfrequestDataValueProcessor ](https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/web/reactive/result/view/CsrfRequestDataValueProcessor.html)提供与 Spring 的[RequestDataValueProcessor ](https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/reactive/result/view/RequestDataValueProcessor.html)的集成。为了使`CsrfRequestDataValueProcessor`工作,必须订阅`Mono`,并且`CsrfToken`必须是与[作为属性公开](#webflux-csrf-include-subscribe)匹配的[默认 \_CSRF\_ATTR\_Name ](https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/web/reactive/result/view/CsrfRequestDataValueProcessor.html#DEFAULT_CSRF_ATTR_NAME)。 + +幸运的是,ThymeLeaf[提供支持](https://www.thymeleaf.org/doc/tutorials/2.1/thymeleafspring.html#integration-with-requestdatavalueprocessor)通过与`RequestDataValueProcessor`集成来为你处理所有的样板文件,以确保具有不安全的 HTTP 方法(即 POST)的窗体将自动包括实际的 CSRF 令牌。 + +##### CSRFToken 请求属性 + +如果用于在请求中包含实际 CSRF 令牌的[其他选择](#webflux-csrf-include)不起作用,则可以利用以下事实:`Mono`[is exposed](#webflux-csrf-include)作为`ServerWebExchange`属性,该属性名为`org.springframework.security.web.server.csrf.CsrfToken`。 + +下面的 ThymeLeaf 示例假设你在一个名为`_csrf`的属性上[expose](#webflux-csrf-include-subscribe)。 + +例 5。具有请求属性的表单中的 CSRF 令牌 + +``` +
+ + +
+``` + +#### Ajax 和 JSON 请求 + +如果你正在使用 JSON,那么就不可能在 HTTP 参数中提交 CSRF 令牌。相反,你可以在 HTTP 头中提交令牌。 + +在下面的部分中,我们将讨论在基于 JavaScript 的应用程序中将 CSRF 令牌作为 HTTP 请求头包含在内的各种方法。 + +##### 自动包含 + +Spring 安全性可以很容易地[configured](#webflux-csrf-configure-custom-repository)将预期的 CSRF 令牌存储在 cookie 中。通过将预期的 CSRF 存储在 Cookie 中,像[AngularJS](https://docs.angularjs.org/api/ng/service/$http#cross-site-request-forgery-xsrf-protection)这样的 JavaScript 框架将自动在 HTTP 请求头中包含实际的 CSRF 令牌。 + +##### 元标签 + +[在 cookie 中暴露 CSRF ](#webflux-csrf-include-form-auto)的另一种模式是在`meta`标记中包含 CSRF 标记。HTML 可能看起来是这样的: + +例 6。CSRF 元标记 HTML + +``` + + + + + + + +``` + +一旦元标记包含 CSRF 令牌,JavaScript 代码将读取元标记并将 CSRF 令牌作为报头。如果你正在使用 jQuery,可以通过以下方式完成此操作: + +例 7。Ajax 发送 CSRF 令牌 + +``` +$(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); + }); +}); +``` + +下面的示例假设你在名为`_csrf`的属性上`CsrfToken`。使用 Thymeleaf 进行此操作的示例如下所示: + +例 8。CSRF 元标记 JSP + +``` + + + + + + + + +``` + +## CSRF 考虑因素 + +在实施针对 CSRF 攻击的保护时,有几个特殊的考虑因素需要考虑。本节讨论了与 WebFlux 环境相关的那些注意事项。有关更一般的讨论,请参见[CSRF 考虑因素](../../features/exploits/csrf.html#csrf-considerations)。 + +### 登录 + +这是重要的[需要 CSRF 才能登录](../../features/exploits/csrf.html#csrf-considerations-login)请求,以防止伪造日志的企图。 Spring Security 的 WebFlux 支持提供了开箱即用的服务。 + +### 注销 + +重要的是[需要 CSRF 才能注销](../../features/exploits/csrf.html#csrf-considerations-logout)请求,以防止伪造注销尝试。默认情况下 Spring Security 的`LogoutWebFilter`只处理 HTTP POST 请求。这确保了注销需要 CSRF 令牌,并且恶意用户不能强制注销你的用户。 + +最简单的方法是使用表单注销。如果你真的想要一个链接,可以使用 JavaScript 让该链接执行一个 POST(例如,可能在一个隐藏的表单上)。对于禁用了 JavaScript 的浏览器,你可以选择让链接将用户带到将执行 POST 的注销确认页面。 + +如果你真的想使用 HTTP GET 与注销,你可以这样做,但请记住,这通常是不推荐的。例如,下面的 Java 配置将使用任何 HTTP 方法请求的 URL`/logout`执行注销: + +例 9。用 HTTP GET 登出 + +Java + +``` +@Bean +public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { + http + // ... + .logout(logout -> logout.requiresLogout(new PathPatternParserServerWebExchangeMatcher("/logout"))) + return http.build(); +} +``` + +Kotlin + +``` +@Bean +fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + // ... + logout { + requiresLogout = PathPatternParserServerWebExchangeMatcher("/logout") + } + } +} +``` + +### CSRF 和会话暂停 + +默认情况下, Spring Security 将 CSRF 令牌存储在`WebSession`中。这可能导致会话过期的情况,这意味着没有预期的 CSRF 令牌来验证。 + +我们已经讨论了[一般解决方案](../../features/exploits/csrf.html#csrf-considerations-login)到会话的超时。本节讨论 CSRF 超时的细节,因为它与 WebFlux 支持有关。 + +将预期的 CSRF 令牌的存储更改为 cookie 中的存储是很简单的。有关详细信息,请参阅[自定义 CSRFTokenRepository ](#webflux-csrf-configure-custom-repository)部分。 + +### + +我们有[已经讨论过了](../../features/exploits/csrf.html#csrf-considerations-multipart)如何保护多部分请求(文件上传)不受 CSRF 攻击导致[鸡和蛋](https://en.wikipedia.org/wiki/Chicken_or_the_egg)问题。本节讨论如何在 WebFlux 应用程序中实现将 CSRF 令牌放置在[body](#webflux-csrf-considerations-multipart-body)和[url](#webflux-csrf-considerations-multipart-url)中。 + +| |关于使用具有 Spring 的多部分表单的更多信息可以在 Spring 引用的[多部分数据](https://docs.spring.io/spring/docs/5.2.x/spring-framework-reference/web-reactive.html#webflux-multipart)部分中找到。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 将 CSRF 标记放入体内 + +我们有[已经讨论过了](../../features/exploits/csrf.html#csrf-considerations-multipart)在主体中放置 CSRF 标记的权衡。 + +在 WebFlux 应用程序中,可以使用以下配置对其进行配置: + +例 10。启用从多部分/表单数据获取 CSRF 令牌 + +Java + +``` +@Bean +public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { + http + // ... + .csrf(csrf -> csrf.tokenFromMultipartDataEnabled(true)) + return http.build(); +} +``` + +Kotlin + +``` +@Bean +fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + // ... + csrf { + tokenFromMultipartDataEnabled = true + } + } +} +``` + +#### 在 URL 中包含 CSRF 令牌 + +我们有[已经讨论过了](../../features/exploits/csrf.html#csrf-considerations-multipart)在 URL 中放置 CSRF 标记的权衡。由于`CsrfToken`被公开为`ServerHttpRequest`[请求属性](#webflux-csrf-include),因此我们可以使用它来创建带有 CSRF 令牌的`action`。ThymeLeaf 的一个示例如下所示: + +例 11。CSRF 令牌正在运行 + +``` +
+``` + +### HiddenHttpMethodFilter + +我们已经[已经讨论过了](../../features/exploits/csrf.html#csrf-considerations-override-method)覆盖了 HTTP 方法。 + +在 Spring WebFlux 应用程序中,使用[HiddenHttpMethodFilter ](https://docs.spring.io/spring-framework/docs/5.2.x/javadoc-api/org/springframework/web/filter/reactive/HiddenHttpMethodFilter.html)重写 HTTP 方法。 \ No newline at end of file diff --git a/docs/spring-security/reactive-exploits-headers.md b/docs/spring-security/reactive-exploits-headers.md new file mode 100644 index 0000000000000000000000000000000000000000..471664832f5147990bd70ae604ab3b7a219cd8fc --- /dev/null +++ b/docs/spring-security/reactive-exploits-headers.md @@ -0,0 +1,534 @@ +# 安全 HTTP 响应标头 + +[安全 HTTP 响应标头](../../features/exploits/headers.html#headers)可以用来增加 Web 应用程序的安全性。本节专门讨论基于 WebFlux 的对安全 HTTP 响应头的支持。 + +## 默认安全标头 + +Spring 安全性提供了[默认的安全 HTTP 响应标头集](../../features/exploits/headers.html#headers-default)以提供安全的默认值。虽然这些标题中的每一个都被认为是最佳实践,但应该注意的是,并不是所有的客户机都使用这些标题,因此鼓励进行额外的测试。 + +你可以自定义特定的标题。例如,假设你希望使用默认值,但不希望为[X-帧-选项](../../servlet/exploits/headers.html#servlet-headers-frame-options)指定`SAMEORIGIN`。 + +你可以通过以下配置轻松地实现这一点: + +例 1。自定义默认安全标头 + +爪哇 + +``` +@Bean +SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { + http + // ... + .headers(headers -> headers + .frameOptions(frameOptions -> frameOptions + .mode(Mode.SAMEORIGIN) + ) + ); + return http.build(); +} +``` + +Kotlin + +``` +@Bean +fun webFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + // ... + headers { + frameOptions { + mode = Mode.SAMEORIGIN + } + } + } +} +``` + +如果不希望添加默认值,并且希望对应该使用的内容进行显式控制,则可以禁用默认值。下面举例说明: + +例 2。禁用 HTTP 安全响应头 + +爪哇 + +``` +@Bean +SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { + http + // ... + .headers(headers -> headers.disable()); + return http.build(); +} +``` + +Kotlin + +``` +@Bean +fun webFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + // ... + headers { + disable() + } + } +} +``` + +## 缓存控制 + +Spring 默认情况下,安全性包括[缓存控制](../../features/exploits/headers.html#headers-cache-control)标头。 + +然而,如果你实际上想要缓存特定的响应,你的应用程序可以有选择地将它们添加到[ServerHtpResponse ](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/http/server/reactive/ServerHttpResponse.html)中,以覆盖由 Spring Security 设置的头。这对于确保 CSS、爪哇Script 和图像等内容被适当地缓存是有用的。 + +当使用 Spring WebFlux 时,这通常是在你的配置中完成的。关于如何做到这一点的详细信息可以在 Spring 参考文档的[静态资源](https://docs.spring.io/spring/docs/5.0.0.RELEASE/spring-framework-reference/web-reactive.html#webflux-config-static-resources)部分中找到。 + +如果有必要,还可以禁用 Spring Security 的缓存控制 HTTP 响应头。 + +例 3。已禁用缓存控制 + +爪哇 + +``` +@Bean +SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { + http + // ... + .headers(headers -> headers + .cache(cache -> cache.disable()) + ); + return http.build(); +} +``` + +Kotlin + +``` +@Bean +fun webFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + // ... + headers { + cache { + disable() + } + } + } +} +``` + +## 内容类型选项 + +Spring 默认情况下,安全性包括[内容类型](../../features/exploits/headers.html#headers-content-type-options)标头。但是,你可以通过以下方式禁用它: + +例 4。禁用内容类型选项 + +爪哇 + +``` +@Bean +SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { + http + // ... + .headers(headers -> headers + .contentTypeOptions(contentTypeOptions -> contentTypeOptions.disable()) + ); + return http.build(); +} +``` + +Kotlin + +``` +@Bean +fun webFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + // ... + headers { + contentTypeOptions { + disable() + } + } + } +} +``` + +## + +Spring 默认情况下,Security 提供[严格的运输安全](../../features/exploits/headers.html#headers-hsts)报头。但是,你可以显式地定制结果。例如,下面是一个显式提供 HSTS 的示例: + +例 5。严格的运输安全 + +爪哇 + +``` +@Bean +SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { + http + // ... + .headers(headers -> headers + .hsts(hsts -> hsts + .includeSubdomains(true) + .preload(true) + .maxAge(Duration.ofDays(365)) + ) + ); + return http.build(); +} +``` + +Kotlin + +``` +@Bean +fun webFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + // ... + headers { + hsts { + includeSubdomains = true + preload = true + maxAge = Duration.ofDays(365) + } + } + } +} +``` + +## X-帧-选项 + +默认情况下, Spring Security 使用[X-帧-选项](../../features/exploits/headers.html#headers-frame-options)禁用在 iFrame 中的呈现。 + +你可以使用以下方法自定义框架选项以使用相同的原点: + +例 6。x-frame-options:SameOrigin + +爪哇 + +``` +@Bean +SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { + http + // ... + .headers(headers -> headers + .frameOptions(frameOptions -> frameOptions + .mode(SAMEORIGIN) + ) + ); + return http.build(); +} +``` + +Kotlin + +``` +@Bean +fun webFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + // ... + headers { + frameOptions { + mode = SAMEORIGIN + } + } + } +} +``` + +## X-XSS-保护 + +默认情况下, Spring Security 指示浏览器使用 \\来阻止反射的 XSS 攻击。可以通过以下配置禁用`X-XSS-Protection`: + +例 7。X-XSS-保护定制 + +爪哇 + +``` +@Bean +SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { + http + // ... + .headers(headers -> headers + .xssProtection(xssProtection -> xssProtection.disable()) + ); + return http.build(); +} +``` + +Kotlin + +``` +@Bean +fun webFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + // ... + headers { + xssProtection { + disable() + } + } + } +} +``` + +## + +Spring 安全默认情况下不添加[内容安全策略](../../features/exploits/headers.html#headers-csp),因为合理的默认情况是不可能在没有应用程序上下文的情况下知道的。Web 应用程序作者必须声明安全策略,以强制执行和/或监视受保护资源。 + +例如,给出以下安全策略: + +例 8。内容安全策略示例 + +``` +Content-Security-Policy: script-src 'self' https://trustedscripts.example.com; object-src https://trustedplugins.example.com; report-uri /csp-report-endpoint/ +``` + +你可以启用 CSP 报头,如下所示: + +例 9。内容安全策略 + +爪哇 + +``` +@Bean +SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { + http + // ... + .headers(headers -> headers + .contentSecurityPolicy(policy -> policy + .policyDirectives("script-src 'self' https://trustedscripts.example.com; object-src https://trustedplugins.example.com; report-uri /csp-report-endpoint/") + ) + ); + return http.build(); +} +``` + +Kotlin + +``` +@Bean +fun webFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + // ... + headers { + contentSecurityPolicy { + policyDirectives = "script-src 'self' https://trustedscripts.example.com; object-src https://trustedplugins.example.com; report-uri /csp-report-endpoint/" + } + } + } +} +``` + +要启用 CSP`report-only`头,请提供以下配置: + +例 10。仅提供内容安全策略报告 + +爪哇 + +``` +@Bean +SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { + http + // ... + .headers(headers -> headers + .contentSecurityPolicy(policy -> policy + .policyDirectives("script-src 'self' https://trustedscripts.example.com; object-src https://trustedplugins.example.com; report-uri /csp-report-endpoint/") + .reportOnly() + ) + ); + return http.build(); +} +``` + +Kotlin + +``` +@Bean +fun webFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + // ... + headers { + contentSecurityPolicy { + policyDirectives = "script-src 'self' https://trustedscripts.example.com; object-src https://trustedplugins.example.com; report-uri /csp-report-endpoint/" + reportOnly = true + } + } + } +} +``` + +## 推荐人政策 + +Spring 默认情况下,安全性不添加[推荐人政策](../../features/exploits/headers.html#headers-referrer)标头。你可以使用如下所示的配置来启用 Referrer 策略标头: + +例 11。Referrer 策略配置 + +爪哇 + +``` +@Bean +SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { + http + // ... + .headers(headers -> headers + .referrerPolicy(referrer -> referrer + .policy(ReferrerPolicy.SAME_ORIGIN) + ) + ); + return http.build(); +} +``` + +Kotlin + +``` +@Bean +fun webFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + // ... + headers { + referrerPolicy { + policy = ReferrerPolicy.SAME_ORIGIN + } + } + } +} +``` + +## 特征策略 + +Spring 默认情况下,安全性不添加[特征策略](../../features/exploits/headers.html#headers-feature)头。以下`Feature-Policy`标题: + +例 12。功能策略示例 + +``` +Feature-Policy: geolocation 'self' +``` + +你可以启用功能策略标头,如下所示: + +例 13。功能策略配置 + +爪哇 + +``` +@Bean +SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { + http + // ... + .headers(headers -> headers + .featurePolicy("geolocation 'self'") + ); + return http.build(); +} +``` + +Kotlin + +``` +@Bean +fun webFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + // ... + headers { + featurePolicy("geolocation 'self'") + } + } +} +``` + +## 权限策略 + +Spring 默认情况下,安全性不添加[权限策略](../../features/exploits/headers.html#headers-permissions)标头。以下`Permissions-Policy`标题: + +例 14。权限-策略示例 + +``` +Permissions-Policy: geolocation=(self) +``` + +你可以启用权限策略标头,如下所示: + +例 15。权限-策略配置 + +爪哇 + +``` +@Bean +SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { + http + // ... + .headers(headers -> headers + .permissionsPolicy(permissions -> permissions + .policy("geolocation=(self)") + ) + ); + return http.build(); +} +``` + +Kotlin + +``` +@Bean +fun webFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + // ... + headers { + permissionsPolicy { + policy = "geolocation=(self)" + } + } + } +} +``` + +## 清除站点数据 + +Spring 默认情况下,安全性不添加[清除站点数据](../../features/exploits/headers.html#headers-clear-site-data)标头。以下 Clear-Site-Data 报头: + +例 16。清除站点数据示例 + +``` +Clear-Site-Data: "cache", "cookies" +``` + +可以通过以下配置在注销时发送: + +例 17。清除站点数据配置 + +Java + +``` +@Bean +SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { + ServerLogoutHandler securityContext = new SecurityContextServerLogoutHandler(); + ClearSiteDataServerHttpHeadersWriter writer = new ClearSiteDataServerHttpHeadersWriter(CACHE, COOKIES); + ServerLogoutHandler clearSiteData = new HeaderWriterServerLogoutHandler(writer); + DelegatingServerLogoutHandler logoutHandler = new DelegatingServerLogoutHandler(securityContext, clearSiteData); + + http + // ... + .logout() + .logoutHandler(logoutHandler); + return http.build(); +} +``` + +Kotlin + +``` +@Bean +fun webFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + val securityContext: ServerLogoutHandler = SecurityContextServerLogoutHandler() + val writer = ClearSiteDataServerHttpHeadersWriter(CACHE, COOKIES) + val clearSiteData: ServerLogoutHandler = HeaderWriterServerLogoutHandler(writer) + val customLogoutHandler = DelegatingServerLogoutHandler(securityContext, clearSiteData) + + return http { + // ... + logout { + logoutHandler = customLogoutHandler + } + } +} +``` \ No newline at end of file diff --git a/docs/spring-security/reactive-exploits-http.md b/docs/spring-security/reactive-exploits-http.md new file mode 100644 index 0000000000000000000000000000000000000000..e968dc51f5a1a4c8e314f47f3bfb6b147a84bf77 --- /dev/null +++ b/docs/spring-security/reactive-exploits-http.md @@ -0,0 +1,79 @@ +# HTTP + +所有基于 HTTP 的通信都应该受到[using TLS](../../features/exploits/http.html#http)的保护。 + +下面你可以找到有关 WebFlux 特定特性的详细信息,这些特性有助于 HTTPS 的使用。 + +## 重定向到 HTTPS + +如果客户机使用 HTTP 而不是 HTTPS 发出请求,则可以将安全性配置为重定向到 HTTPS。 + +例如,下面的 爪哇 配置将把任何 HTTP 请求重定向到 HTTPS: + +例 1。重定向到 HTTPS + +爪哇 + +``` +@Bean +SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { + http + // ... + .redirectToHttps(withDefaults()); + return http.build(); +} +``` + +Kotlin + +``` +@Bean +fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + // ... + redirectToHttps { } + } +} +``` + +配置可以很容易地绕过 if 语句,以便只在生产中打开。或者,可以通过查找有关仅在生产中发生的请求的属性来启用它。例如,如果生产环境添加了一个名为`X-Forwarded-Proto`的标头,则可以使用以下 Java 配置: + +例 2。X 转发时重定向到 HTTPS + +Java + +``` +@Bean +SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { + http + // ... + .redirectToHttps(redirect -> redirect + .httpsRedirectWhen(e -> e.getRequest().getHeaders().containsKey("X-Forwarded-Proto")) + ); + return http.build(); +} +``` + +Kotlin + +``` +@Bean +fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + // ... + redirectToHttps { + httpsRedirectWhen { + it.request.headers.containsKey("X-Forwarded-Proto") + } + } + } +} +``` + +## 严格的运输安全 + +Spring 安全性为[严格的运输安全](../../servlet/exploits/headers.html#servlet-headers-hsts)提供支持,并在默认情况下启用它。 + +## 代理服务器配置 + +Spring 安全性[与代理服务器集成](../../features/exploits/http.html#http-proxy-server)。 \ No newline at end of file diff --git a/docs/spring-security/reactive-exploits.md b/docs/spring-security/reactive-exploits.md new file mode 100644 index 0000000000000000000000000000000000000000..8c91f00cafe000cd5e8554d0431e3a6eb3cb01a4 --- /dev/null +++ b/docs/spring-security/reactive-exploits.md @@ -0,0 +1,9 @@ +# 保护免受剥削 + +Spring 安全性提供了针对众多利用漏洞的保护。本节讨论 WebFlux 对以下内容的特定支持: + +* [CSRF](csrf.html) + +* [Headers](headers.html) + +* [HTTP 请求](http.html) \ No newline at end of file diff --git a/docs/spring-security/reactive-getting-started.md b/docs/spring-security/reactive-getting-started.md new file mode 100644 index 0000000000000000000000000000000000000000..828b479f3a2d3720fb8fdde9fcfa3e70b0c61cd1 --- /dev/null +++ b/docs/spring-security/reactive-getting-started.md @@ -0,0 +1,67 @@ +# WebFlux 应用程序入门 + +本节介绍了如何在反应性应用程序中使用 Spring 安全性和 Spring 引导的最小设置。 + +| |可以找到已完成的应用程序[在我们的样品库中](https://github.com/spring-projects/spring-security-samples/tree/5.6.x/reactive/webflux/java/hello-security)。
为了你的方便,你可以通过[点击这里](https://start.spring.io/starter.zip?type=maven-project&language=java&packaging=jar&jvmVersion=1.8&groupId=example&artifactId=hello-security&name=hello-security&description=Hello%20Security&packageName=example.hello-security&dependencies=webflux,security)下载一个最小的反应式 Spring 启动 + Spring 安全应用程序。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +## 更新依赖项 + +你可以通过添加`spring-boot-starter-security`将 Spring 安全性添加到 Spring 引导项目中。 + +Maven + +``` + + org.springframework.boot + spring-boot-starter-security + +``` + +Gradle + +``` + implementation 'org.springframework.boot:spring-boot-starter-security' +``` + +## 启动 Hello Spring 安全启动 + +你现在可以通过使用 Maven 插件的`run`目标[run the Spring Boot application](https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#using-boot-running-with-the-maven-plugin)。下面的示例展示了如何这样做(以及这样做产生的输出的开始): + +例 1。运行 Spring 启动应用程序 + +Maven + +``` +$ ./mvnw spring-boot:run +... +INFO 23689 --- [ restartedMain] .s.s.UserDetailsServiceAutoConfiguration : + +Using generated security password: 8e557245-73e2-4286-969a-ff57fe326336 + +... +``` + +Gradle + +``` +$ ./gradlew bootRun +... +INFO 23689 --- [ restartedMain] .s.s.UserDetailsServiceAutoConfiguration : + +Using generated security password: 8e557245-73e2-4286-969a-ff57fe326336 + +... +``` + +## 认证 + +你可以通过[http://localhost:8080/](http://localhost:8080/)访问应用程序,该应用程序将把浏览器重定向到默认的登录页面。你可以使用随机生成的密码提供`user`的默认用户名,该密码已登录到控制台。然后将浏览器带到原始请求的页面。 + +要注销,你可以访问[http://localhost:8080/logout](http://localhost:8080/logout),然后确认你希望注销。 + +## Spring 引导自动配置 + +Spring 启动会自动添加 Spring 安全性,这需要对所有请求进行身份验证。它还使用随机生成的密码生成用户,该密码被记录到控制台,该控制台可以使用 Form 或 Basic 身份验证来进行身份验证。 + +[反应性应用](index.html)[X.509 认证](authentication/x509.html) \ No newline at end of file diff --git a/docs/spring-security/reactive-integrations-cors.md b/docs/spring-security/reactive-integrations-cors.md new file mode 100644 index 0000000000000000000000000000000000000000..608116ddd6476291dec69445fa34b01a452a71f1 --- /dev/null +++ b/docs/spring-security/reactive-integrations-cors.md @@ -0,0 +1,63 @@ +# CORS + +Spring Framework 提供[CORS 的一流支持](https://docs.spring.io/spring/docs/current/spring-framework-reference/web-reactive.html#webflux-cors-intro)。CORS 必须在 Spring 安全性之前进行处理,因为飞行前请求将不包含任何 cookie(即`JSESSIONID`)。如果请求不包含任何 cookie 并且 Spring 安全性是第一位的,则该请求将确定用户未经过身份验证(因为在该请求中没有 cookie)并拒绝它。 + +确保先处理 CORS 的最简单方法是使用`CorsWebFilter`。用户可以通过提供`CorsConfigurationSource`将`CorsWebFilter`与 Spring 安全性集成在一起。例如,以下将在 Spring 安全性中集成 CORS 支持: + +爪哇 + +``` +@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; +} +``` + +Kotlin + +``` +@Bean +fun corsConfigurationSource(): CorsConfigurationSource { + val configuration = CorsConfiguration() + configuration.allowedOrigins = listOf("https://example.com") + configuration.allowedMethods = listOf("GET", "POST") + val source = UrlBasedCorsConfigurationSource() + source.registerCorsConfiguration("/**", configuration) + return source +} +``` + +以下操作将禁用 Spring Security 中的 CORS 集成: + +爪哇 + +``` +@Bean +SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { + http + // ... + .cors(cors -> cors.disable()); + return http.build(); +} +``` + +Kotlin + +``` +@Bean +fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + // ... + cors { + disable() + } + } +} +``` + +[HTTP 请求](../exploits/http.html)[RSocket](rsocket.html) \ No newline at end of file diff --git a/docs/spring-security/reactive-integrations-rsocket.md b/docs/spring-security/reactive-integrations-rsocket.md new file mode 100644 index 0000000000000000000000000000000000000000..d49310e6e6c462235834c5673f9c55b8be965518 --- /dev/null +++ b/docs/spring-security/reactive-integrations-rsocket.md @@ -0,0 +1,365 @@ +# RSocket 安全性 + +Spring Security 的 RSocket 支持依赖于`SocketAcceptorInterceptor`。进入安全的主要入口点是`PayloadSocketAcceptorInterceptor`,它调整了 RSocket API,允许使用`PayloadInterceptor`实现来拦截`PayloadExchange`。 + +你可以找到几个示例应用程序来演示下面的代码: + +* hello rsocket[HellorSocket](https://github.com/spring-projects/spring-security-samples/tree/5.6.x/reactive/rsocket/hello-security) + +* [Spring Flights](https://github.com/rwinch/spring-flights/tree/security) + +## 最小的 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); + } +} +``` + +Kotlin + +``` +@Configuration +@EnableRSocketSecurity +open class HelloRSocketSecurityConfig { + @Bean + open fun userDetailsService(): MapReactiveUserDetailsService { + val user = User.withDefaultPasswordEncoder() + .username("user") + .password("user") + .roles("USER") + .build() + return MapReactiveUserDetailsService(user) + } +} +``` + +此配置允许[简单的身份验证](#rsocket-authentication-simple),并设置[RSocket-授权](#rsocket-authorization)以要求对任何请求进行身份验证的用户。 + +## 添加安全性:接收截取程序 + +为了使安全性工作,我们需要将`SecuritySocketAcceptorInterceptor`应用到`ServerRSocketFactory`。这就是将我们创建的`PayloadSocketAcceptorInterceptor`与 RSocket 基础架构连接起来的原因。在 Spring 引导应用程序中,这是使用`RSocketSecurityAutoConfiguration`和以下代码自动完成的。 + +``` +@Bean +RSocketServerCustomizer springSecurityRSocketSecurity(SecuritySocketAcceptorInterceptor interceptor) { + return (server) -> server.interceptors((registry) -> registry.forSocketAcceptor(interceptor)); +} +``` + +## RSocket 身份验证 + +RSocket 身份验证是通过`AuthenticationPayloadInterceptor`执行的,该控制器充当调用`ReactiveAuthenticationManager`实例的控制器。 + +### 设置时的身份验证与请求时的身份验证 + +通常,身份验证可以在设置时间和/或请求时间发生。 + +在几个场景中,设置时的身份验证是有意义的。一个常见的场景是当单个用户(即移动连接)利用 RSocket 连接时。在这种情况下,只有单个用户在利用连接,因此可以在连接时进行一次身份验证。 + +在共享 RSocket 连接的场景中,对每个请求发送凭据是有意义的。例如,作为下游服务连接到 RSocket 服务器的 Web 应用程序将生成一个所有用户都可以利用的单个连接。在这种情况下,如果 RSocket 服务器需要根据每个请求的 Web 应用程序的用户凭据来执行授权,这是有意义的。 + +在某些情况下,在设置和每个请求中进行身份验证是有意义的。考虑如前所述的 Web 应用程序。如果我们需要将连接限制到 Web 应用程序本身,那么我们可以在连接时提供一个具有`SETUP`权限的凭据。然后,每个用户将拥有不同的权限,但不是`SETUP`权限。这意味着个人用户可以提出请求,但不能建立额外的连接。 + +### 简单的身份验证 + +Spring 安全性支持[简单的身份验证元数据扩展](https://github.com/rsocket/rsocket/blob/5920ed374d008abb712cb1fd7c9d91778b2f4a68/Extensions/Security/Simple.md)。 + +| |基本的身份验证草案演变成简单的身份验证,并且只支持向后兼容性。
设置它的方法请参见`RSocketSecurity.basicAuthentication(Customizer)`。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +RSocket 接收器可以使用`AuthenticationPayloadExchangeConverter`来解码凭据,这是使用 DSL 的`simpleAuthentication`部分自动设置的。可以在下面找到一个显式配置。 + +爪哇 + +``` +@Bean +PayloadSocketAcceptorInterceptor rsocketInterceptor(RSocketSecurity rsocket) { + rsocket + .authorizePayload(authorize -> + authorize + .anyRequest().authenticated() + .anyExchange().permitAll() + ) + .simpleAuthentication(Customizer.withDefaults()); + return rsocket.build(); +} +``` + +Kotlin + +``` +@Bean +open fun rsocketInterceptor(rsocket: RSocketSecurity): PayloadSocketAcceptorInterceptor { + rsocket + .authorizePayload { authorize -> authorize + .anyRequest().authenticated() + .anyExchange().permitAll() + } + .simpleAuthentication(withDefaults()) + return rsocket.build() +} +``` + +RSocket 发送者可以使用`SimpleAuthenticationEncoder`发送凭据,该凭据可以添加到 Spring 的`RSocketStrategies`中。 + +爪哇 + +``` +RSocketStrategies.Builder strategies = ...; +strategies.encoder(new SimpleAuthenticationEncoder()); +``` + +Kotlin + +``` +var strategies: RSocketStrategies.Builder = ... +strategies.encoder(SimpleAuthenticationEncoder()) +``` + +然后可以使用它向设置中的接收器发送用户名和密码: + +爪哇 + +``` +MimeType authenticationMimeType = + MimeTypeUtils.parseMimeType(WellKnownMimeType.MESSAGE_RSOCKET_AUTHENTICATION.getString()); +UsernamePasswordMetadata credentials = new UsernamePasswordMetadata("user", "password"); +Mono requester = RSocketRequester.builder() + .setupMetadata(credentials, authenticationMimeType) + .rsocketStrategies(strategies.build()) + .connectTcp(host, port); +``` + +Kotlin + +``` +val authenticationMimeType: MimeType = + MimeTypeUtils.parseMimeType(WellKnownMimeType.MESSAGE_RSOCKET_AUTHENTICATION.string) +val credentials = UsernamePasswordMetadata("user", "password") +val requester: Mono = RSocketRequester.builder() + .setupMetadata(credentials, authenticationMimeType) + .rsocketStrategies(strategies.build()) + .connectTcp(host, port) +``` + +另外,也可以在请求中发送用户名和密码。 + +爪哇 + +``` +Mono requester; +UsernamePasswordMetadata credentials = new UsernamePasswordMetadata("user", "password"); + +public Mono findRadar(String code) { + return this.requester.flatMap(req -> + req.route("find.radar.{code}", code) + .metadata(credentials, authenticationMimeType) + .retrieveMono(AirportLocation.class) + ); +} +``` + +Kotlin + +``` +import org.springframework.messaging.rsocket.retrieveMono + +// ... + +var requester: Mono? = null +var credentials = UsernamePasswordMetadata("user", "password") + +open fun findRadar(code: String): Mono { + return requester!!.flatMap { req -> + req.route("find.radar.{code}", code) + .metadata(credentials, authenticationMimeType) + .retrieveMono() + } +} +``` + +### JWT + +Spring 安全性支持[承载令牌身份验证元数据扩展](https://github.com/rsocket/rsocket/blob/5920ed374d008abb712cb1fd7c9d91778b2f4a68/Extensions/Security/Bearer.md)。支持的形式是对 JWT 进行身份验证(确定 JWT 是有效的),然后使用 JWT 做出授权决策。 + +RSocket 接收器可以使用`BearerPayloadExchangeConverter`来解码凭据,这是使用 DSL 的`jwt`部分自动设置的。下面是一个配置示例: + +爪哇 + +``` +@Bean +PayloadSocketAcceptorInterceptor rsocketInterceptor(RSocketSecurity rsocket) { + rsocket + .authorizePayload(authorize -> + authorize + .anyRequest().authenticated() + .anyExchange().permitAll() + ) + .jwt(Customizer.withDefaults()); + return rsocket.build(); +} +``` + +Kotlin + +``` +@Bean +fun rsocketInterceptor(rsocket: RSocketSecurity): PayloadSocketAcceptorInterceptor { + rsocket + .authorizePayload { authorize -> authorize + .anyRequest().authenticated() + .anyExchange().permitAll() + } + .jwt(withDefaults()) + return rsocket.build() +} +``` + +上述配置依赖于存在`ReactiveJwtDecoder``@Bean`。可以在下面找到从发行者创建一个发行者的示例: + +爪哇 + +``` +@Bean +ReactiveJwtDecoder jwtDecoder() { + return ReactiveJwtDecoders + .fromIssuerLocation("https://example.com/auth/realms/demo"); +} +``` + +Kotlin + +``` +@Bean +fun jwtDecoder(): ReactiveJwtDecoder { + return ReactiveJwtDecoders + .fromIssuerLocation("https://example.com/auth/realms/demo") +} +``` + +RSocket 发送者不需要做任何特殊的事情来发送令牌,因为该值只是一个简单的字符串。例如,可以在设置时发送令牌: + +爪哇 + +``` +MimeType authenticationMimeType = + MimeTypeUtils.parseMimeType(WellKnownMimeType.MESSAGE_RSOCKET_AUTHENTICATION.getString()); +BearerTokenMetadata token = ...; +Mono requester = RSocketRequester.builder() + .setupMetadata(token, authenticationMimeType) + .connectTcp(host, port); +``` + +Kotlin + +``` +val authenticationMimeType: MimeType = + MimeTypeUtils.parseMimeType(WellKnownMimeType.MESSAGE_RSOCKET_AUTHENTICATION.string) +val token: BearerTokenMetadata = ... + +val requester = RSocketRequester.builder() + .setupMetadata(token, authenticationMimeType) + .connectTcp(host, port) +``` + +可选地或附加地,令牌可以在请求中发送。 + +爪哇 + +``` +MimeType authenticationMimeType = + MimeTypeUtils.parseMimeType(WellKnownMimeType.MESSAGE_RSOCKET_AUTHENTICATION.getString()); +Mono requester; +BearerTokenMetadata token = ...; + +public Mono findRadar(String code) { + return this.requester.flatMap(req -> + req.route("find.radar.{code}", code) + .metadata(token, authenticationMimeType) + .retrieveMono(AirportLocation.class) + ); +} +``` + +Kotlin + +``` +val authenticationMimeType: MimeType = + MimeTypeUtils.parseMimeType(WellKnownMimeType.MESSAGE_RSOCKET_AUTHENTICATION.string) +var requester: Mono? = null +val token: BearerTokenMetadata = ... + +open fun findRadar(code: String): Mono { + return this.requester!!.flatMap { req -> + req.route("find.radar.{code}", code) + .metadata(token, authenticationMimeType) + .retrieveMono() + } +} +``` + +## RSocket 授权 + +RSocket 授权是用`AuthorizationPayloadInterceptor`执行的,它作为一个控制器来调用`ReactiveAuthorizationManager`实例。DSL 可用于基于`PayloadExchange`设置授权规则。下面是一个配置示例: + +爪哇 + +``` +rsocket + .authorizePayload(authz -> + authz + .setup().hasRole("SETUP") (1) + .route("fetch.profile.me").authenticated() (2) + .matcher(payloadExchange -> isMatch(payloadExchange)) (3) + .hasRole("CUSTOM") + .route("fetch.profile.{username}") (4) + .access((authentication, context) -> checkFriends(authentication, context)) + .anyRequest().authenticated() (5) + .anyExchange().permitAll() (6) + ); +``` + +Kotlin + +``` +rsocket + .authorizePayload { authz -> + authz + .setup().hasRole("SETUP") (1) + .route("fetch.profile.me").authenticated() (2) + .matcher { payloadExchange -> isMatch(payloadExchange) } (3) + .hasRole("CUSTOM") + .route("fetch.profile.{username}") (4) + .access { authentication, context -> checkFriends(authentication, context) } + .anyRequest().authenticated() (5) + .anyExchange().permitAll() + } (6) +``` + +|**1**|建立连接需要授权`ROLE_SETUP`| +|-----|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +|**2**|如果路由是`fetch.profile.me`,则授权只需要对用户进行身份验证即可。| +|**3**|在这个规则中,我们设置了一个自定义的匹配器,其中授权要求用户拥有`ROLE_CUSTOM`的权限| +|**4**|该规则利用自定义授权。
Matcher 表示一个名为`username`的变量,该变量在`context`中可用。
在`checkFriends`方法中公开了一个自定义授权规则。| +|**5**|此规则确保尚未具有规则的请求将需要对用户进行身份验证。
包含元数据的位置是一个请求。
它将不包括额外的有效负载。| +|**6**|此规则确保任何人都可以使用任何尚未拥有规则的交换。
在本例中,这意味着没有元数据的有效负载没有授权规则。| + +重要的是要理解授权规则是按顺序执行的。将只调用匹配的第一个授权规则。 + +[CORS](cors.html)[Testing](../test/index.html) \ No newline at end of file diff --git a/docs/spring-security/reactive-oauth2-client-authorization-grants.md b/docs/spring-security/reactive-oauth2-client-authorization-grants.md new file mode 100644 index 0000000000000000000000000000000000000000..1f8f1bc5d9ce166265bb5232d9ccae7b73f52969 --- /dev/null +++ b/docs/spring-security/reactive-oauth2-client-authorization-grants.md @@ -0,0 +1,1016 @@ +# 授权赠款支助 + +## 授权代码 + +| |有关[授权代码](https://tools.ietf.org/html/rfc6749#section-1.3.1)授权的更多详细信息,请参阅 OAuth2.0 授权框架。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### 获得授权 + +| |请参阅[授权请求/回应](https://tools.ietf.org/html/rfc6749#section-4.1.1)协议流以获得授权代码授权。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------| + +### 发起授权请求 + +`OAuth2AuthorizationRequestRedirectWebFilter`使用`ServerOAuth2AuthorizationRequestResolver`解析`OAuth2AuthorizationRequest`,并通过将最终用户的用户代理重定向到授权服务器的授权端点来启动授权代码授予流。 + +`ServerOAuth2AuthorizationRequestResolver`的主要作用是从提供的 Web 请求中解析`OAuth2AuthorizationRequest`。默认实现`DefaultServerOAuth2AuthorizationRequestResolver`在(默认)路径`/oauth2/authorization/{registrationId}`上匹配,提取`registrationId`并使用它为关联的`OAuth2AuthorizationRequest`构建`OAuth2AuthorizationRequest`。 + +给出了 OAuth2.0 客户端注册的以下 Spring Boot2.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`的请求将启动由`OAuth2AuthorizationRequestRedirectWebFilter`重定向的授权请求,并最终启动授权代码授予流。 + +| |`AuthorizationCodeReactiveOAuth2AuthorizedClientProvider`是用于授权代码授予的`ReactiveOAuth2AuthorizedClientProvider`的实现,
还通过`OAuth2AuthorizationRequestRedirectWebFilter`发起重定向的授权请求。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +如果 OAuth2.0 客户端是[公共客户](https://tools.ietf.org/html/rfc6749#section-2.1),则按照以下方式配置 OAuth2.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" + ... +``` + +使用[代码交换的证明密钥](https://tools.ietf.org/html/rfc7636)支持公共客户端。如果客户机运行在不受信任的环境中(例如,本地应用程序或基于 Web 浏览器的应用程序),因此不能维护其凭据的机密性,则当以下条件成立时,将自动使用 PKCE: + +1. `client-secret`省略(或为空) + +2. `client-authentication-method`设置为“无”(`ClientAuthenticationMethod.NONE`) + +`DefaultServerOAuth2AuthorizationRequestResolver`还支持`URI`使用`UriComponentsBuilder`的`redirect-uri`模板变量。 + +以下配置使用了所有受支持的`URI`模板变量: + +``` +spring: + security: + oauth2: + client: + registration: + okta: + ... + redirect-uri: "{baseScheme}://{baseHost}{basePort}{basePath}/authorized/{registrationId}" + ... +``` + +| |`{baseUrl}`解析为`{baseScheme}://{baseHost}{basePort}{basePath}`| +|---|-----------------------------------------------------------------------| + +当 OAuth2.0 客户端运行在[代理服务器](../../../features/exploits/http.html#http-proxy-server)之后时,使用`redirect-uri`模板变量配置`URI`模板变量特别有用。这确保了在展开`redirect-uri`时使用`X-Forwarded-*`头。 + +### 自定义授权请求 + +`ServerOAuth2AuthorizationRequestResolver`可以实现的主要用例之一是,能够使用 OAuth2.0 授权框架中定义的标准参数之上的附加参数来定制授权请求。 + +例如,OpenID Connect 为[授权代码流](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest)定义了额外的 OAuth2.0 请求参数,它是从[OAuth2.0 授权框架](https://tools.ietf.org/html/rfc6749#section-4.1.1)中定义的标准参数扩展而来的。其中一个扩展参数是`prompt`参数。 + +| |可选的。以空格分隔、区分大小写的 ASCII 字符串值列表,该列表指定授权服务器是否提示最终用户进行重新身份验证和同意。定义的值是:none、login、consent、select\_account| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +下面的示例展示了如何使用`DefaultServerOAuth2AuthorizationRequestResolver`配置`Consumer`,该配置通过包括请求参数`oauth2Login()`来定制`oauth2Login()`的授权请求。 + +爪哇 + +``` +@EnableWebFluxSecurity +public class OAuth2LoginSecurityConfig { + + @Autowired + private ReactiveClientRegistrationRepository clientRegistrationRepository; + + @Bean + public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { + http + .authorizeExchange(authorize -> authorize + .anyExchange().authenticated() + ) + .oauth2Login(oauth2 -> oauth2 + .authorizationRequestResolver( + authorizationRequestResolver(this.clientRegistrationRepository) + ) + ); + return http.build(); + } + + private ServerOAuth2AuthorizationRequestResolver authorizationRequestResolver( + ReactiveClientRegistrationRepository clientRegistrationRepository) { + + DefaultServerOAuth2AuthorizationRequestResolver authorizationRequestResolver = + new DefaultServerOAuth2AuthorizationRequestResolver( + clientRegistrationRepository); + authorizationRequestResolver.setAuthorizationRequestCustomizer( + authorizationRequestCustomizer()); + + return authorizationRequestResolver; + } + + private Consumer authorizationRequestCustomizer() { + return customizer -> customizer + .additionalParameters(params -> params.put("prompt", "consent")); + } +} +``` + +Kotlin + +``` +@EnableWebFluxSecurity +class SecurityConfig { + + @Autowired + private lateinit var customClientRegistrationRepository: ReactiveClientRegistrationRepository + + @Bean + fun securityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + authorizeExchange { + authorize(anyExchange, authenticated) + } + oauth2Login { + authorizationRequestResolver = authorizationRequestResolver(customClientRegistrationRepository) + } + } + } + + private fun authorizationRequestResolver( + clientRegistrationRepository: ReactiveClientRegistrationRepository): ServerOAuth2AuthorizationRequestResolver { + val authorizationRequestResolver = DefaultServerOAuth2AuthorizationRequestResolver( + clientRegistrationRepository) + authorizationRequestResolver.setAuthorizationRequestCustomizer( + authorizationRequestCustomizer()) + return authorizationRequestResolver + } + + private fun authorizationRequestCustomizer(): Consumer { + return Consumer { customizer -> + customizer + .additionalParameters { params -> params["prompt"] = "consent" } + } + } +} +``` + +对于简单的用例,如果附加的请求参数对于特定的提供者总是相同的,那么可以直接在`authorization-uri`属性中添加它。 + +例如,如果请求参数`prompt`的值对于提供程序`okta`总是`consent`,那么只需配置如下: + +``` +spring: + security: + oauth2: + client: + provider: + okta: + authorization-uri: https://dev-1234.oktapreview.com/oauth2/v1/authorize?prompt=consent +``` + +前面的示例展示了在标准参数之上添加一个自定义参数的常见用例。或者,如果你的需求更高级,那么你可以通过简单地覆盖`OAuth2AuthorizationRequest.authorizationRequestUri`属性来完全控制构建授权请求 URI。 + +| |`OAuth2AuthorizationRequest.Builder.build()`构造`OAuth2AuthorizationRequest.authorizationRequestUri`,它表示授权请求 URI,包括使用`application/x-www-form-urlencoded`格式的所有查询参数。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +下面的示例显示了与前面的示例不同的`authorizationRequestCustomizer()`,并覆盖了`OAuth2AuthorizationRequest.authorizationRequestUri`属性。 + +爪哇 + +``` +private Consumer authorizationRequestCustomizer() { + return customizer -> customizer + .authorizationRequestUri(uriBuilder -> uriBuilder + .queryParam("prompt", "consent").build()); +} +``` + +Kotlin + +``` +private fun authorizationRequestCustomizer(): Consumer { + return Consumer { customizer: OAuth2AuthorizationRequest.Builder -> + customizer + .authorizationRequestUri { uriBuilder: UriBuilder -> + uriBuilder + .queryParam("prompt", "consent").build() + } + } +} +``` + +### 存储授权请求 + +从发起授权请求到接收授权响应(回调),`ServerAuthorizationRequestRepository`负责`OAuth2AuthorizationRequest`的持久性。 + +| |`OAuth2AuthorizationRequest`用于关联和验证授权响应。| +|---|----------------------------------------------------------------------------------------------| + +`ServerAuthorizationRequestRepository`的默认实现是`WebSessionOAuth2ServerAuthorizationRequestRepository`,它将`OAuth2AuthorizationRequest`存储在`WebSession`中。 + +如果你有`ServerAuthorizationRequestRepository`的自定义实现,则可以按照以下示例对其进行配置: + +例 1。ServerAuthorizationRequestRepository 配置 + +爪哇 + +``` +@EnableWebFluxSecurity +public class OAuth2ClientSecurityConfig { + + @Bean + public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { + http + .oauth2Client(oauth2 -> oauth2 + .authorizationRequestRepository(this.authorizationRequestRepository()) + ... + ); + return http.build(); + } +} +``` + +Kotlin + +``` +@EnableWebFluxSecurity +class OAuth2ClientSecurityConfig { + + @Bean + fun securityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + oauth2Client { + authorizationRequestRepository = authorizationRequestRepository() + } + } + } +} +``` + +### 请求访问令牌 + +| |请参阅[访问令牌请求/响应](https://tools.ietf.org/html/rfc6749#section-4.1.3)协议流以获得授权代码授权。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------| + +对于授权代码授予,`ReactiveOAuth2AccessTokenResponseClient`的默认实现是`WebClientReactiveAuthorizationCodeTokenResponseClient`,它使用`WebClient`在授权服务器的令牌端点将授权代码交换为访问令牌。 + +`WebClientReactiveAuthorizationCodeTokenResponseClient`非常灵活,因为它允许你定制令牌请求的预处理和/或令牌响应的后处理。 + +### 自定义访问令牌请求 + +如果需要定制令牌请求的预处理,则可以提供带有自定义`WebClientReactiveAuthorizationCodeTokenResponseClient.setParametersConverter()`的`Converter>`。默认实现构建一个`MultiValueMap`,该实现仅包含用于构造请求的标准[OAuth2.0 访问令牌请求](https://tools.ietf.org/html/rfc6749#section-4.1.3)的`grant_type`参数。授权代码授权所需的其他参数由`WebClientReactiveAuthorizationCodeTokenResponseClient`直接添加到请求主体中。但是,提供一个自定义`Converter`,将允许你扩展标准令牌请求并添加自定义参数。 + +| |如果你只喜欢添加额外的参数,那么可以使用自定义的`Converter>`为`WebClientReactiveAuthorizationCodeTokenResponseClient.addParametersConverter()`提供`Converter>`,它构造一个聚合`Converter`。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |自定义`Converter`必须返回 OAuth2.0 访问令牌请求的有效参数,该请求被预期的 OAuth2.0 提供程序理解。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------| + +### 自定义访问令牌响应 + +另一方面,如果需要自定义令牌响应的后处理,则需要为`WebClientReactiveAuthorizationCodeTokenResponseClient.setBodyExtractor()`提供自定义配置的`BodyExtractor, ReactiveHttpInputMessage>`,该配置用于将 OAuth2.0 访问令牌响应转换为`OAuth2AccessTokenResponse`。由`OAuth2BodyExtractors.oauth2AccessTokenResponse()`提供的默认实现解析响应并相应地处理错误。 + +### 自定义`WebClient` + +或者,如果你的需求更高级,那么你可以通过简单地提供`WebClientReactiveAuthorizationCodeTokenResponseClient.setWebClient()`和自定义配置的`WebClient`来完全控制请求/响应。 + +无论你是自定义`WebClientReactiveAuthorizationCodeTokenResponseClient`还是提供你自己的`ReactiveOAuth2AccessTokenResponseClient`实现,你都需要对其进行配置,如以下示例所示: + +例 2。访问令牌响应配置 + +爪哇 + +``` +@EnableWebFluxSecurity +public class OAuth2ClientSecurityConfig { + + @Bean + public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { + http + .oauth2Client(oauth2 -> oauth2 + .authenticationManager(this.authorizationCodeAuthenticationManager()) + ... + ); + return http.build(); + } + + private ReactiveAuthenticationManager authorizationCodeAuthenticationManager() { + WebClientReactiveAuthorizationCodeTokenResponseClient accessTokenResponseClient = + new WebClientReactiveAuthorizationCodeTokenResponseClient(); + ... + + return new OAuth2AuthorizationCodeReactiveAuthenticationManager(accessTokenResponseClient); + } +} +``` + +Kotlin + +``` +@EnableWebFluxSecurity +class OAuth2ClientSecurityConfig { + + @Bean + fun securityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + oauth2Client { + authenticationManager = authorizationCodeAuthenticationManager() + } + } + } + + private fun authorizationCodeAuthenticationManager(): ReactiveAuthenticationManager { + val accessTokenResponseClient = WebClientReactiveAuthorizationCodeTokenResponseClient() + ... + + return OAuth2AuthorizationCodeReactiveAuthenticationManager(accessTokenResponseClient) + } +} +``` + +## 刷新令牌 + +| |有关[刷新令牌](https://tools.ietf.org/html/rfc6749#section-1.5)的更多详细信息,请参阅 OAuth2.0 授权框架。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------| + +### 刷新访问令牌 + +| |请参阅[访问令牌请求/响应](https://tools.ietf.org/html/rfc6749#section-6)协议流以获取刷新令牌授权。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------| + +用于刷新令牌授权的`ReactiveOAuth2AccessTokenResponseClient`的默认实现是`WebClientReactiveRefreshTokenTokenResponseClient`,当在授权服务器的令牌端点刷新访问令牌时,它使用`WebClient`。 + +`WebClientReactiveRefreshTokenTokenResponseClient`非常灵活,因为它允许你定制令牌请求的预处理和/或令牌响应的后处理。 + +### 自定义访问令牌请求 + +如果需要定制令牌请求的预处理,则可以提供带有自定义`WebClientReactiveRefreshTokenTokenResponseClient.setParametersConverter()`的`Converter>`。默认实现构建一个`MultiValueMap`,其中只包含用于构造请求的标准[OAuth2.0 访问令牌请求](https://tools.ietf.org/html/rfc6749#section-6)的`grant_type`参数。刷新令牌授权所需的其他参数由`WebClientReactiveRefreshTokenTokenResponseClient`直接添加到请求主体中。但是,提供一个自定义`Converter`,将允许你扩展标准令牌请求并添加自定义参数。 + +| |如果你只喜欢添加额外的参数,那么你可以使用自定义的`WebClientReactiveRefreshTokenTokenResponseClient.addParametersConverter()`提供`Converter>`,它构造一个聚合`Converter`。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |自定义`Converter`必须返回 OAuth2.0 访问令牌请求的有效参数,该请求被预期的 OAuth2.0 提供程序理解。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------| + +### 自定义访问令牌响应 + +另一方面,如果需要自定义令牌响应的后处理,则需要为`WebClientReactiveRefreshTokenTokenResponseClient.setBodyExtractor()`提供自定义配置的`BodyExtractor, ReactiveHttpInputMessage>`,该配置用于将 OAuth2.0 访问令牌响应转换为`OAuth2AccessTokenResponse`。`OAuth2BodyExtractors.oauth2AccessTokenResponse()`提供的默认实现解析响应并相应地处理错误。 + +### 自定义`WebClient` + +或者,如果你的需求更高级,那么你可以通过简单地提供`WebClientReactiveRefreshTokenTokenResponseClient.setWebClient()`和自定义配置的`WebClient`来完全控制请求/响应。 + +无论你是自定义`WebClientReactiveRefreshTokenTokenResponseClient`还是提供你自己的`ReactiveOAuth2AccessTokenResponseClient`实现,你都需要对其进行配置,如以下示例所示: + +例 3。访问令牌响应配置 + +爪哇 + +``` +// Customize +ReactiveOAuth2AccessTokenResponseClient refreshTokenTokenResponseClient = ... + +ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = + ReactiveOAuth2AuthorizedClientProviderBuilder.builder() + .authorizationCode() + .refreshToken(configurer -> configurer.accessTokenResponseClient(refreshTokenTokenResponseClient)) + .build(); + +... + +authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); +``` + +Kotlin + +``` +// Customize +val refreshTokenTokenResponseClient: ReactiveOAuth2AccessTokenResponseClient = ... + +val authorizedClientProvider: ReactiveOAuth2AuthorizedClientProvider = ReactiveOAuth2AuthorizedClientProviderBuilder.builder() + .authorizationCode() + .refreshToken { it.accessTokenResponseClient(refreshTokenTokenResponseClient) } + .build() + +... + +authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider) +``` + +| |`ReactiveOAuth2AuthorizedClientProviderBuilder.builder().refreshToken()`配置`RefreshTokenReactiveOAuth2AuthorizedClientProvider`,
,这是用于刷新令牌授权的`ReactiveOAuth2AuthorizedClientProvider`的实现。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +`OAuth2RefreshToken`可选地在用于`authorization_code`和`password`授予类型的访问令牌响应中返回。如果`OAuth2AuthorizedClient.getRefreshToken()`可用,而`OAuth2AuthorizedClient.getAccessToken()`过期,则`RefreshTokenReactiveOAuth2AuthorizedClientProvider`将自动刷新。 + +## 客户凭据 + +| |有关[客户凭据](https://tools.ietf.org/html/rfc6749#section-1.3.4)授权的更多详细信息,请参阅 OAuth2.0 授权框架。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### 请求访问令牌 + +| |请参阅[访问令牌请求/响应](https://tools.ietf.org/html/rfc6749#section-4.4.2)协议流以获取客户端凭据授权。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------| + +对于客户端凭据授予,`ReactiveOAuth2AccessTokenResponseClient`的默认实现是`WebClientReactiveClientCredentialsTokenResponseClient`,当在授权服务器的令牌端点请求访问令牌时,它使用`WebClient`。 + +`WebClientReactiveClientCredentialsTokenResponseClient`非常灵活,因为它允许你定制令牌请求的预处理和/或令牌响应的后处理。 + +### 自定义访问令牌请求 + +如果需要定制令牌请求的预处理,则可以提供带有自定义`WebClientReactiveClientCredentialsTokenResponseClient.setParametersConverter()`的`Converter>`。默认实现构建一个`MultiValueMap`,该实现仅包含用于构造请求的标准[OAuth2.0 访问令牌请求](https://tools.ietf.org/html/rfc6749#section-4.4.2)的`grant_type`参数。由`WebClientReactiveClientCredentialsTokenResponseClient`直接将客户机凭据授权所需的其他参数添加到请求主体中。但是,提供一个自定义`Converter`,将允许你扩展标准令牌请求并添加自定义参数。 + +| |如果你只喜欢添加额外的参数,那么可以使用自定义的`Converter>`为`WebClientReactiveClientCredentialsTokenResponseClient.addParametersConverter()`提供`Converter>`,它构造一个聚合`Converter`。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |自定义`Converter`必须返回 OAuth2.0 访问令牌请求的有效参数,该请求被预期的 OAuth2.0 提供程序理解。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------| + +### 自定义访问令牌响应 + +另一方面,如果需要自定义令牌响应的后处理,则需要为`WebClientReactiveClientCredentialsTokenResponseClient.setBodyExtractor()`提供自定义配置的`BodyExtractor, ReactiveHttpInputMessage>`,该配置用于将 OAuth2.0 访问令牌响应转换为`OAuth2AccessTokenResponse`。由`OAuth2BodyExtractors.oauth2AccessTokenResponse()`提供的默认实现解析响应并相应地处理错误。 + +### 自定义`WebClient` + +或者,如果你的需求更高级,你可以通过简单地提供`WebClientReactiveClientCredentialsTokenResponseClient.setWebClient()`和自定义配置的`WebClient`来完全控制请求/响应。 + +无论你是自定义`WebClientReactiveClientCredentialsTokenResponseClient`还是提供你自己的`ReactiveOAuth2AccessTokenResponseClient`实现,你都需要对其进行配置,如以下示例所示: + +爪哇 + +``` +// Customize +ReactiveOAuth2AccessTokenResponseClient clientCredentialsTokenResponseClient = ... + +ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = + ReactiveOAuth2AuthorizedClientProviderBuilder.builder() + .clientCredentials(configurer -> configurer.accessTokenResponseClient(clientCredentialsTokenResponseClient)) + .build(); + +... + +authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); +``` + +Kotlin + +``` +// Customize +val clientCredentialsTokenResponseClient: ReactiveOAuth2AccessTokenResponseClient = ... + +val authorizedClientProvider: ReactiveOAuth2AuthorizedClientProvider = ReactiveOAuth2AuthorizedClientProviderBuilder.builder() + .clientCredentials { it.accessTokenResponseClient(clientCredentialsTokenResponseClient) } + .build() + +... + +authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider) +``` + +| |`ReactiveOAuth2AuthorizedClientProviderBuilder.builder().clientCredentials()`配置`ClientCredentialsReactiveOAuth2AuthorizedClientProvider`,
,这是用于客户端凭据授权的`ReactiveOAuth2AuthorizedClientProvider`的实现。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### 使用访问令牌 + +给出了用于 OAuth2.0 客户端注册的以下 Boot2.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 +``` + +…和`ReactiveOAuth2AuthorizedClientManager``@Bean`: + +爪哇 + +``` +@Bean +public ReactiveOAuth2AuthorizedClientManager authorizedClientManager( + ReactiveClientRegistrationRepository clientRegistrationRepository, + ServerOAuth2AuthorizedClientRepository authorizedClientRepository) { + + ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = + ReactiveOAuth2AuthorizedClientProviderBuilder.builder() + .clientCredentials() + .build(); + + DefaultReactiveOAuth2AuthorizedClientManager authorizedClientManager = + new DefaultReactiveOAuth2AuthorizedClientManager( + clientRegistrationRepository, authorizedClientRepository); + authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); + + return authorizedClientManager; +} +``` + +Kotlin + +``` +@Bean +fun authorizedClientManager( + clientRegistrationRepository: ReactiveClientRegistrationRepository, + authorizedClientRepository: ServerOAuth2AuthorizedClientRepository): ReactiveOAuth2AuthorizedClientManager { + val authorizedClientProvider: ReactiveOAuth2AuthorizedClientProvider = ReactiveOAuth2AuthorizedClientProviderBuilder.builder() + .clientCredentials() + .build() + val authorizedClientManager = DefaultReactiveOAuth2AuthorizedClientManager( + clientRegistrationRepository, authorizedClientRepository) + authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider) + return authorizedClientManager +} +``` + +你可以按以下方式获得`OAuth2AccessToken`: + +爪哇 + +``` +@Controller +public class OAuth2ClientController { + + @Autowired + private ReactiveOAuth2AuthorizedClientManager authorizedClientManager; + + @GetMapping("/") + public Mono index(Authentication authentication, ServerWebExchange exchange) { + OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId("okta") + .principal(authentication) + .attribute(ServerWebExchange.class.getName(), exchange) + .build(); + + return this.authorizedClientManager.authorize(authorizeRequest) + .map(OAuth2AuthorizedClient::getAccessToken) + ... + .thenReturn("index"); + } +} +``` + +Kotlin + +``` +class OAuth2ClientController { + + @Autowired + private lateinit var authorizedClientManager: ReactiveOAuth2AuthorizedClientManager + + @GetMapping("/") + fun index(authentication: Authentication, exchange: ServerWebExchange): Mono { + val authorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId("okta") + .principal(authentication) + .attribute(ServerWebExchange::class.java.name, exchange) + .build() + + return authorizedClientManager.authorize(authorizeRequest) + .map { it.accessToken } + ... + .thenReturn("index") + } +} +``` + +| |`ServerWebExchange`是一个可选属性。
如果不提供,它将通过[反应堆的背景](https://projectreactor.io/docs/core/release/reference/#context)键从`ServerWebExchange.class`获得。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +## 资源所有者密码凭据 + +| |有关[资源所有者密码凭据](https://tools.ietf.org/html/rfc6749#section-1.3.3)授权的更多详细信息,请参阅 OAuth2.0 授权框架。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### 请求访问令牌 + +| |请参阅[访问令牌请求/响应](https://tools.ietf.org/html/rfc6749#section-4.3.2)协议流以获取资源所有者密码凭据授权。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +对于资源所有者密码凭据授予,`ReactiveOAuth2AccessTokenResponseClient`的默认实现是`WebClientReactivePasswordTokenResponseClient`,当在授权服务器的令牌端点请求访问令牌时,它使用`WebClient`。 + +`WebClientReactivePasswordTokenResponseClient`非常灵活,因为它允许你定制令牌请求的预处理和/或令牌响应的后处理。 + +### 自定义访问令牌请求 + +如果需要对令牌请求的预处理进行自定义,则可以提供带有自定义`WebClientReactivePasswordTokenResponseClient.setParametersConverter()`的`Converter>`。默认的实现构建一个`MultiValueMap`,其中只包含用于构造请求的标准[OAuth2.0 访问令牌请求](https://tools.ietf.org/html/rfc6749#section-4.4.2)的`grant_type`参数。通过`WebClientReactivePasswordTokenResponseClient`将资源所有者密码凭据授予所需的其他参数直接添加到请求的主体中。但是,提供一个自定义`Converter`,将允许你扩展标准令牌请求并添加自定义参数。 + +| |如果你只喜欢添加额外的参数,那么你可以使用自定义的`WebClientReactivePasswordTokenResponseClient.addParametersConverter()`提供`Converter>`,它构造一个聚合`Converter`。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |自定义`Converter`必须返回 OAuth2.0 访问令牌请求的有效参数,该请求被预期的 OAuth2.0 提供程序理解。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------| + +### 自定义访问令牌响应 + +另一方面,如果需要自定义令牌响应的后处理,则需要为`WebClientReactivePasswordTokenResponseClient.setBodyExtractor()`提供自定义配置的`BodyExtractor, ReactiveHttpInputMessage>`,该配置用于将 OAuth2.0 访问令牌响应转换为`OAuth2AccessTokenResponse`。由`OAuth2BodyExtractors.oauth2AccessTokenResponse()`提供的默认实现解析响应并相应地处理错误。 + +### 自定义`WebClient` + +或者,如果你的需求更高级,你可以通过简单地提供`WebClientReactivePasswordTokenResponseClient.setWebClient()`和自定义配置的`WebClient`来完全控制请求/响应。 + +无论你是自定义`WebClientReactivePasswordTokenResponseClient`还是提供你自己的`ReactiveOAuth2AccessTokenResponseClient`实现,你都需要对其进行配置,如以下示例所示: + +爪哇 + +``` +// Customize +ReactiveOAuth2AccessTokenResponseClient passwordTokenResponseClient = ... + +ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = + ReactiveOAuth2AuthorizedClientProviderBuilder.builder() + .password(configurer -> configurer.accessTokenResponseClient(passwordTokenResponseClient)) + .refreshToken() + .build(); + +... + +authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); +``` + +Kotlin + +``` +val passwordTokenResponseClient: ReactiveOAuth2AccessTokenResponseClient = ... + +val authorizedClientProvider = ReactiveOAuth2AuthorizedClientProviderBuilder.builder() + .password { it.accessTokenResponseClient(passwordTokenResponseClient) } + .refreshToken() + .build() + +... + +authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider) +``` + +| |`ReactiveOAuth2AuthorizedClientProviderBuilder.builder().password()`配置`PasswordReactiveOAuth2AuthorizedClientProvider`,
,这是用于资源所有者密码凭据授予的`ReactiveOAuth2AuthorizedClientProvider`的实现。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### 使用访问令牌 + +给出了用于 OAuth2.0 客户端注册的以下 Boot2.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 +``` + +…和`ReactiveOAuth2AuthorizedClientManager``@Bean`: + +爪哇 + +``` +@Bean +public ReactiveOAuth2AuthorizedClientManager authorizedClientManager( + ReactiveClientRegistrationRepository clientRegistrationRepository, + ServerOAuth2AuthorizedClientRepository authorizedClientRepository) { + + ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = + ReactiveOAuth2AuthorizedClientProviderBuilder.builder() + .password() + .refreshToken() + .build(); + + DefaultReactiveOAuth2AuthorizedClientManager authorizedClientManager = + new DefaultReactiveOAuth2AuthorizedClientManager( + clientRegistrationRepository, authorizedClientRepository); + authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); + + // Assuming the `username` and `password` are supplied as `ServerHttpRequest` parameters, + // map the `ServerHttpRequest` parameters to `OAuth2AuthorizationContext.getAttributes()` + authorizedClientManager.setContextAttributesMapper(contextAttributesMapper()); + + return authorizedClientManager; +} + +private Function>> contextAttributesMapper() { + return authorizeRequest -> { + Map contextAttributes = Collections.emptyMap(); + ServerWebExchange exchange = authorizeRequest.getAttribute(ServerWebExchange.class.getName()); + ServerHttpRequest request = exchange.getRequest(); + String username = request.getQueryParams().getFirst(OAuth2ParameterNames.USERNAME); + String password = request.getQueryParams().getFirst(OAuth2ParameterNames.PASSWORD); + if (StringUtils.hasText(username) && StringUtils.hasText(password)) { + contextAttributes = new HashMap<>(); + + // `PasswordReactiveOAuth2AuthorizedClientProvider` requires both attributes + contextAttributes.put(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, username); + contextAttributes.put(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, password); + } + return Mono.just(contextAttributes); + }; +} +``` + +Kotlin + +``` +@Bean +fun authorizedClientManager( + clientRegistrationRepository: ReactiveClientRegistrationRepository, + authorizedClientRepository: ServerOAuth2AuthorizedClientRepository): ReactiveOAuth2AuthorizedClientManager { + val authorizedClientProvider: ReactiveOAuth2AuthorizedClientProvider = ReactiveOAuth2AuthorizedClientProviderBuilder.builder() + .password() + .refreshToken() + .build() + val authorizedClientManager = DefaultReactiveOAuth2AuthorizedClientManager( + clientRegistrationRepository, authorizedClientRepository) + authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider) + + // Assuming the `username` and `password` are supplied as `ServerHttpRequest` parameters, + // map the `ServerHttpRequest` parameters to `OAuth2AuthorizationContext.getAttributes()` + authorizedClientManager.setContextAttributesMapper(contextAttributesMapper()) + return authorizedClientManager +} + +private fun contextAttributesMapper(): Function>> { + return Function { authorizeRequest -> + var contextAttributes: MutableMap = mutableMapOf() + val exchange: ServerWebExchange = authorizeRequest.getAttribute(ServerWebExchange::class.java.name)!! + val request: ServerHttpRequest = exchange.request + val username: String? = request.queryParams.getFirst(OAuth2ParameterNames.USERNAME) + val password: String? = request.queryParams.getFirst(OAuth2ParameterNames.PASSWORD) + if (StringUtils.hasText(username) && StringUtils.hasText(password)) { + contextAttributes = hashMapOf() + + // `PasswordReactiveOAuth2AuthorizedClientProvider` requires both attributes + contextAttributes[OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME] = username!! + contextAttributes[OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME] = password!! + } + Mono.just(contextAttributes) + } +} +``` + +你可以按以下方式获得`OAuth2AccessToken`: + +爪哇 + +``` +@Controller +public class OAuth2ClientController { + + @Autowired + private ReactiveOAuth2AuthorizedClientManager authorizedClientManager; + + @GetMapping("/") + public Mono index(Authentication authentication, ServerWebExchange exchange) { + OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId("okta") + .principal(authentication) + .attribute(ServerWebExchange.class.getName(), exchange) + .build(); + + return this.authorizedClientManager.authorize(authorizeRequest) + .map(OAuth2AuthorizedClient::getAccessToken) + ... + .thenReturn("index"); + } +} +``` + +Kotlin + +``` +@Controller +class OAuth2ClientController { + @Autowired + private lateinit var authorizedClientManager: ReactiveOAuth2AuthorizedClientManager + + @GetMapping("/") + fun index(authentication: Authentication, exchange: ServerWebExchange): Mono { + val authorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId("okta") + .principal(authentication) + .attribute(ServerWebExchange::class.java.name, exchange) + .build() + + return authorizedClientManager.authorize(authorizeRequest) + .map { it.accessToken } + ... + .thenReturn("index") + } +} +``` + +| |`ServerWebExchange`是一个可选属性。
如果不提供,它将通过[反应堆的背景](https://projectreactor.io/docs/core/release/reference/#context)键从`ServerWebExchange.class`获得。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +## JWT 持有人 + +| |有关[JWT Bearer](https://datatracker.ietf.org/doc/html/rfc7523)授权的更多详细信息,请参考 JSON Web Token 配置文件的 OAuth2.0 客户端身份验证和授权授权。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### 请求访问令牌 + +| |请参阅[访问令牌请求/响应](https://datatracker.ietf.org/doc/html/rfc7523#section-2.1)协议流以获取 JWT 承载授权。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------| + +对于 JWT 承载授权,`ReactiveOAuth2AccessTokenResponseClient`的默认实现是`WebClientReactiveJwtBearerTokenResponseClient`,当在授权服务器的令牌端点请求访问令牌时,它使用`WebClient`。 + +`WebClientReactiveJwtBearerTokenResponseClient`非常灵活,因为它允许你定制令牌请求的预处理和/或令牌响应的后处理。 + +### 自定义访问令牌请求 + +如果需要对令牌请求的预处理进行自定义,则可以提供带有自定义`WebClientReactiveJwtBearerTokenResponseClient.setParametersConverter()`的`Converter>`。默认的实现构建一个`MultiValueMap`,其中只包含用于构造请求的标准[OAuth2.0 访问令牌请求](https://tools.ietf.org/html/rfc6749#section-4.4.2)的`grant_type`参数。JWT 承载授权所需的其他参数由`WebClientReactiveJwtBearerTokenResponseClient`直接添加到请求的主体中。但是,提供一个自定义`Converter`,将允许你扩展标准令牌请求并添加自定义参数。 + +| |如果你只喜欢添加额外的参数,那么可以使用自定义的`Converter>`为`WebClientReactiveJwtBearerTokenResponseClient.addParametersConverter()`提供`Converter>`,它构造一个聚合`Converter`。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |自定义`Converter`必须返回 OAuth2.0 访问令牌请求的有效参数,该请求被预期的 OAuth2.0 提供程序理解。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------| + +### 自定义访问令牌响应 + +另一方面,如果需要自定义令牌响应的后处理,则需要为`WebClientReactiveJwtBearerTokenResponseClient.setBodyExtractor()`提供自定义配置的`BodyExtractor, ReactiveHttpInputMessage>`,该配置用于将 OAuth2.0 访问令牌响应转换为`OAuth2AccessTokenResponse`。由`OAuth2BodyExtractors.oauth2AccessTokenResponse()`提供的默认实现解析响应并相应地处理错误。 + +### 自定义`WebClient` + +或者,如果你的需求更高级,你可以通过简单地提供`WebClientReactiveJwtBearerTokenResponseClient.setWebClient()`和自定义配置的`WebClient`来完全控制请求/响应。 + +无论你是自定义`WebClientReactiveJwtBearerTokenResponseClient`还是提供你自己的`ReactiveOAuth2AccessTokenResponseClient`实现,你都需要对其进行配置,如以下示例所示: + +爪哇 + +``` +// Customize +ReactiveOAuth2AccessTokenResponseClient jwtBearerTokenResponseClient = ... + +JwtBearerReactiveOAuth2AuthorizedClientProvider jwtBearerAuthorizedClientProvider = new JwtBearerReactiveOAuth2AuthorizedClientProvider(); +jwtBearerAuthorizedClientProvider.setAccessTokenResponseClient(jwtBearerTokenResponseClient); + +ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = + ReactiveOAuth2AuthorizedClientProviderBuilder.builder() + .provider(jwtBearerAuthorizedClientProvider) + .build(); + +... + +authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); +``` + +Kotlin + +``` +// Customize +val jwtBearerTokenResponseClient: ReactiveOAuth2AccessTokenResponseClient = ... + +val jwtBearerAuthorizedClientProvider = JwtBearerReactiveOAuth2AuthorizedClientProvider() +jwtBearerAuthorizedClientProvider.setAccessTokenResponseClient(jwtBearerTokenResponseClient) + +val authorizedClientProvider = ReactiveOAuth2AuthorizedClientProviderBuilder.builder() + .provider(jwtBearerAuthorizedClientProvider) + .build() + +... + +authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider) +``` + +### 使用访问令牌 + +给出了用于 OAuth2.0 客户端注册的以下 Spring Boot2.x 属性: + +``` +spring: + security: + oauth2: + client: + registration: + okta: + client-id: okta-client-id + client-secret: okta-client-secret + authorization-grant-type: urn:ietf:params:oauth:grant-type:jwt-bearer + scope: read + provider: + okta: + token-uri: https://dev-1234.oktapreview.com/oauth2/v1/token +``` + +…和`OAuth2AuthorizedClientManager``@Bean`: + +爪哇 + +``` +@Bean +public ReactiveOAuth2AuthorizedClientManager authorizedClientManager( + ReactiveClientRegistrationRepository clientRegistrationRepository, + ServerOAuth2AuthorizedClientRepository authorizedClientRepository) { + + JwtBearerReactiveOAuth2AuthorizedClientProvider jwtBearerAuthorizedClientProvider = + new JwtBearerReactiveOAuth2AuthorizedClientProvider(); + + ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = + ReactiveOAuth2AuthorizedClientProviderBuilder.builder() + .provider(jwtBearerAuthorizedClientProvider) + .build(); + + DefaultReactiveOAuth2AuthorizedClientManager authorizedClientManager = + new DefaultReactiveOAuth2AuthorizedClientManager( + clientRegistrationRepository, authorizedClientRepository); + authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); + + return authorizedClientManager; +} +``` + +Kotlin + +``` +@Bean +fun authorizedClientManager( + clientRegistrationRepository: ReactiveClientRegistrationRepository, + authorizedClientRepository: ServerOAuth2AuthorizedClientRepository): ReactiveOAuth2AuthorizedClientManager { + val jwtBearerAuthorizedClientProvider = JwtBearerReactiveOAuth2AuthorizedClientProvider() + val authorizedClientProvider = ReactiveOAuth2AuthorizedClientProviderBuilder.builder() + .provider(jwtBearerAuthorizedClientProvider) + .build() + val authorizedClientManager = DefaultReactiveOAuth2AuthorizedClientManager( + clientRegistrationRepository, authorizedClientRepository) + authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider) + return authorizedClientManager +} +``` + +你可以按以下方式获得`OAuth2AccessToken`: + +爪哇 + +``` +@RestController +public class OAuth2ResourceServerController { + + @Autowired + private ReactiveOAuth2AuthorizedClientManager authorizedClientManager; + + @GetMapping("/resource") + public Mono resource(JwtAuthenticationToken jwtAuthentication, ServerWebExchange exchange) { + OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId("okta") + .principal(jwtAuthentication) + .build(); + + return this.authorizedClientManager.authorize(authorizeRequest) + .map(OAuth2AuthorizedClient::getAccessToken) + ... + } +} +``` + +Kotlin + +``` +class OAuth2ResourceServerController { + + @Autowired + private lateinit var authorizedClientManager: ReactiveOAuth2AuthorizedClientManager + + @GetMapping("/resource") + fun resource(jwtAuthentication: JwtAuthenticationToken, exchange: ServerWebExchange): Mono { + val authorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId("okta") + .principal(jwtAuthentication) + .build() + return authorizedClientManager.authorize(authorizeRequest) + .map { it.accessToken } + ... + } +} +``` + +[核心接口和类](core.html)[OAuth2 客户端身份验证](client-authentication.html) \ No newline at end of file diff --git a/docs/spring-security/reactive-oauth2-client-authorized-clients.md b/docs/spring-security/reactive-oauth2-client-authorized-clients.md new file mode 100644 index 0000000000000000000000000000000000000000..e68ffa2f6577e550fe7e8915f5812cc7c4f3549d --- /dev/null +++ b/docs/spring-security/reactive-oauth2-client-authorized-clients.md @@ -0,0 +1,239 @@ +# 授权客户 + +## 解决授权客户 + +`@RegisteredOAuth2AuthorizedClient`注释提供了将方法参数解析为`OAuth2AuthorizedClient`类型的参数值的功能。与使用`ReactiveOAuth2AuthorizedClientManager`或`ReactiveOAuth2AuthorizedClientService`访问`OAuth2AuthorizedClient`相比,这是一种方便的替代方法。 + +爪哇 + +``` +@Controller +public class OAuth2ClientController { + + @GetMapping("/") + public Mono index(@RegisteredOAuth2AuthorizedClient("okta") OAuth2AuthorizedClient authorizedClient) { + return Mono.just(authorizedClient.getAccessToken()) + ... + .thenReturn("index"); + } +} +``` + +Kotlin + +``` +@Controller +class OAuth2ClientController { + @GetMapping("/") + fun index(@RegisteredOAuth2AuthorizedClient("okta") authorizedClient: OAuth2AuthorizedClient): Mono { + return Mono.just(authorizedClient.accessToken) + ... + .thenReturn("index") + } +} +``` + +`@RegisteredOAuth2AuthorizedClient`注释由`OAuth2AuthorizedClientArgumentResolver`处理,它直接使用[reactiveoauth2authorizedclientmanager ](#oauth2Client-authorized-manager-provider),因此继承了它的功能。 + +## 用于反应性环境的 WebClient 集成 + +OAuth2.0 客户端支持使用`ExchangeFilterFunction`与`WebClient`集成。 + +`ServerOAuth2AuthorizedClientExchangeFilterFunction`提供了一种简单的机制,通过使用`OAuth2AuthorizedClient`请求受保护的资源,并将相关的`OAuth2AccessToken`作为承载令牌。它直接使用[reactiveoauth2authorizedclientmanager ](#oauth2Client-authorized-manager-provider),因此继承了以下功能: + +* 如果客户端尚未获得授权,则将请求`OAuth2AccessToken`。 + + * `authorization_code`-触发授权请求重定向以初始化流 + + * `client_credentials`-访问令牌是直接从令牌端点获得的 + + * `password`-访问令牌是直接从令牌端点获得的 + +* 如果`OAuth2AccessToken`过期,如果`ReactiveOAuth2AuthorizedClientProvider`可用于执行授权,则将刷新(或更新)该权限 + +下面的代码展示了如何使用 OAuth2.0 客户端支持配置`WebClient`的示例: + +爪哇 + +``` +@Bean +WebClient webClient(ReactiveOAuth2AuthorizedClientManager authorizedClientManager) { + ServerOAuth2AuthorizedClientExchangeFilterFunction oauth2Client = + new ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager); + return WebClient.builder() + .filter(oauth2Client) + .build(); +} +``` + +Kotlin + +``` +@Bean +fun webClient(authorizedClientManager: ReactiveOAuth2AuthorizedClientManager): WebClient { + val oauth2Client = ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager) + return WebClient.builder() + .filter(oauth2Client) + .build() +} +``` + +### 提供授权客户 + +通过解析`ClientRequest.attributes()`(请求属性)中的`OAuth2AuthorizedClient`,`ServerOAuth2AuthorizedClientExchangeFilterFunction`确定要使用的客户机(用于请求)。 + +下面的代码展示了如何将`OAuth2AuthorizedClient`设置为请求属性: + +爪哇 + +``` +@GetMapping("/") +public Mono index(@RegisteredOAuth2AuthorizedClient("okta") OAuth2AuthorizedClient authorizedClient) { + String resourceUri = ... + + return webClient + .get() + .uri(resourceUri) + .attributes(oauth2AuthorizedClient(authorizedClient)) (1) + .retrieve() + .bodyToMono(String.class) + ... + .thenReturn("index"); +} +``` + +Kotlin + +``` +@GetMapping("/") +fun index(@RegisteredOAuth2AuthorizedClient("okta") authorizedClient: OAuth2AuthorizedClient): Mono { + val resourceUri: String = ... + + return webClient + .get() + .uri(resourceUri) + .attributes(oauth2AuthorizedClient(authorizedClient)) (1) + .retrieve() + .bodyToMono() + ... + .thenReturn("index") +} +``` + +|**1**|`oauth2AuthorizedClient()`是`static`中的一个`static`方法。| +|-----|--------------------------------------------------------------------------------------------------------| + +下面的代码展示了如何将`ClientRegistration.getRegistrationId()`设置为请求属性: + +爪哇 + +``` +@GetMapping("/") +public Mono index() { + String resourceUri = ... + + return webClient + .get() + .uri(resourceUri) + .attributes(clientRegistrationId("okta")) (1) + .retrieve() + .bodyToMono(String.class) + ... + .thenReturn("index"); +} +``` + +Kotlin + +``` +@GetMapping("/") +fun index(): Mono { + val resourceUri: String = ... + + return webClient + .get() + .uri(resourceUri) + .attributes(clientRegistrationId("okta")) (1) + .retrieve() + .bodyToMono() + ... + .thenReturn("index") +} +``` + +|**1**|`clientRegistrationId()`是`static`中的一个`static`方法。| +|-----|------------------------------------------------------------------------------------------------------| + +### 对授权客户违约 + +如果`OAuth2AuthorizedClient`或`ClientRegistration.getRegistrationId()`都不作为请求属性提供,则`ServerOAuth2AuthorizedClientExchangeFilterFunction`可以根据其配置来确定要使用的*默认值*客户端。 + +如果`setDefaultOAuth2AuthorizedClient(true)`被配置并且用户已经使用`ServerHttpSecurity.oauth2Login()`进行了身份验证,则使用与当前`OAuth2AccessToken`关联的`OAuth2AccessToken`。 + +以下代码显示了具体的配置: + +爪哇 + +``` +@Bean +WebClient webClient(ReactiveOAuth2AuthorizedClientManager authorizedClientManager) { + ServerOAuth2AuthorizedClientExchangeFilterFunction oauth2Client = + new ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager); + oauth2Client.setDefaultOAuth2AuthorizedClient(true); + return WebClient.builder() + .filter(oauth2Client) + .build(); +} +``` + +Kotlin + +``` +@Bean +fun webClient(authorizedClientManager: ReactiveOAuth2AuthorizedClientManager): WebClient { + val oauth2Client = ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager) + oauth2Client.setDefaultOAuth2AuthorizedClient(true) + return WebClient.builder() + .filter(oauth2Client) + .build() +} +``` + +| |由于所有 HTTP 请求都将接收访问令牌,因此建议对此功能保持谨慎。| +|---|---------------------------------------------------------------------------------------------------------| + +或者,如果`setDefaultClientRegistrationId("okta")`被配置为有效的`ClientRegistration`,则使用与`OAuth2AuthorizedClient`关联的`OAuth2AccessToken`。 + +以下代码显示了具体的配置: + +爪哇 + +``` +@Bean +WebClient webClient(ReactiveOAuth2AuthorizedClientManager authorizedClientManager) { + ServerOAuth2AuthorizedClientExchangeFilterFunction oauth2Client = + new ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager); + oauth2Client.setDefaultClientRegistrationId("okta"); + return WebClient.builder() + .filter(oauth2Client) + .build(); +} +``` + +Kotlin + +``` +@Bean +fun webClient(authorizedClientManager: ReactiveOAuth2AuthorizedClientManager): WebClient { + val oauth2Client = ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager) + oauth2Client.setDefaultClientRegistrationId("okta") + return WebClient.builder() + .filter(oauth2Client) + .build() +} +``` + +| |由于所有 HTTP 请求都将接收访问令牌,因此建议对此功能保持谨慎。| +|---|---------------------------------------------------------------------------------------------------------| + +[OAuth2 客户端身份验证](client-authentication.html)[OAuth2 资源服务器](../resource-server/index.html) \ No newline at end of file diff --git a/docs/spring-security/reactive-oauth2-client-client-authentication.md b/docs/spring-security/reactive-oauth2-client-client-authentication.md new file mode 100644 index 0000000000000000000000000000000000000000..2e50df986e7b0e689788be8b1b94fd08f7a4aed8 --- /dev/null +++ b/docs/spring-security/reactive-oauth2-client-client-authentication.md @@ -0,0 +1,139 @@ +# 客户端身份验证支持 + +## JWT 持有人 + +| |有关[JWT Bearer](https://datatracker.ietf.org/doc/html/rfc7523#section-2.2)客户端身份验证的更多详细信息,请参考 JSON Web Token 配置文件中的 OAuth2.0 客户端身份验证和授权授予。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +JWT 承载客户端身份验证的默认实现是`NimbusJwtClientAuthenticationParametersConverter`,这是一个`Converter`,它通过在`client_assertion`参数中添加签名的 JSON Web 令牌来定制令牌请求参数。 + +用于对 JWS 进行签名的`java.security.PrivateKey`或`javax.crypto.SecretKey`由与`NimbusJwtClientAuthenticationParametersConverter`关联的`com.nimbusds.jose.jwk.JWK`解析器提供。 + +### 使用`private_key_jwt`进行身份验证 + +给出了 OAuth2.0 客户端注册的以下 Spring Boot2.x 属性: + +``` +spring: + security: + oauth2: + client: + registration: + okta: + client-id: okta-client-id + client-authentication-method: private_key_jwt + authorization-grant-type: authorization_code + ... +``` + +下面的示例展示了如何配置`WebClientReactiveAuthorizationCodeTokenResponseClient`: + +爪哇 + +``` +Function jwkResolver = (clientRegistration) -> { + if (clientRegistration.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.PRIVATE_KEY_JWT)) { + // Assuming RSA key type + RSAPublicKey publicKey = ... + RSAPrivateKey privateKey = ... + return new RSAKey.Builder(publicKey) + .privateKey(privateKey) + .keyID(UUID.randomUUID().toString()) + .build(); + } + return null; +}; + +WebClientReactiveAuthorizationCodeTokenResponseClient tokenResponseClient = + new WebClientReactiveAuthorizationCodeTokenResponseClient(); +tokenResponseClient.addParametersConverter( + new NimbusJwtClientAuthenticationParametersConverter<>(jwkResolver)); +``` + +Kotlin + +``` +val jwkResolver: Function = + Function { clientRegistration -> + if (clientRegistration.clientAuthenticationMethod.equals(ClientAuthenticationMethod.PRIVATE_KEY_JWT)) { + // Assuming RSA key type + var publicKey: RSAPublicKey = ... + var privateKey: RSAPrivateKey = ... + RSAKey.Builder(publicKey) + .privateKey(privateKey) + .keyID(UUID.randomUUID().toString()) + .build() + } + null + } + +val tokenResponseClient = WebClientReactiveAuthorizationCodeTokenResponseClient() +tokenResponseClient.addParametersConverter( + NimbusJwtClientAuthenticationParametersConverter(jwkResolver) +) +``` + +### 使用`client_secret_jwt`进行身份验证 + +给出了 OAuth2.0 客户端注册的以下 Spring Boot2.x 属性: + +``` +spring: + security: + oauth2: + client: + registration: + okta: + client-id: okta-client-id + client-secret: okta-client-secret + client-authentication-method: client_secret_jwt + authorization-grant-type: client_credentials + ... +``` + +下面的示例展示了如何配置`WebClientReactiveClientCredentialsTokenResponseClient`: + +爪哇 + +``` +Function jwkResolver = (clientRegistration) -> { + if (clientRegistration.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.CLIENT_SECRET_JWT)) { + SecretKeySpec secretKey = new SecretKeySpec( + clientRegistration.getClientSecret().getBytes(StandardCharsets.UTF_8), + "HmacSHA256"); + return new OctetSequenceKey.Builder(secretKey) + .keyID(UUID.randomUUID().toString()) + .build(); + } + return null; +}; + +WebClientReactiveClientCredentialsTokenResponseClient tokenResponseClient = + new WebClientReactiveClientCredentialsTokenResponseClient(); +tokenResponseClient.addParametersConverter( + new NimbusJwtClientAuthenticationParametersConverter<>(jwkResolver)); +``` + +Kotlin + +``` +val jwkResolver = Function { clientRegistration: ClientRegistration -> + if (clientRegistration.clientAuthenticationMethod == ClientAuthenticationMethod.CLIENT_SECRET_JWT) { + val secretKey = SecretKeySpec( + clientRegistration.clientSecret.toByteArray(StandardCharsets.UTF_8), + "HmacSHA256" + ) + OctetSequenceKey.Builder(secretKey) + .keyID(UUID.randomUUID().toString()) + .build() + } + null +} + +val tokenResponseClient = WebClientReactiveClientCredentialsTokenResponseClient() +tokenResponseClient.addParametersConverter( + NimbusJwtClientAuthenticationParametersConverter(jwkResolver) +) +``` + +[OAuth2 授权授予](authorization-grants.html)[OAuth2 授权客户](authorized-clients.html) \ No newline at end of file diff --git a/docs/spring-security/reactive-oauth2-client-core.md b/docs/spring-security/reactive-oauth2-client-core.md new file mode 100644 index 0000000000000000000000000000000000000000..61acf8eced7245cd45780510ee4b98f18f14500b --- /dev/null +++ b/docs/spring-security/reactive-oauth2-client-core.md @@ -0,0 +1,395 @@ +# 核心接口/类 + +## 客户登记 + +`ClientRegistration`是在 OAuth2.0 或 OpenID Connect1.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 redirectUri; (6) + private Set 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 String issuerUri; (12) + private Map configurationMetadata; (13) + + public class UserInfoEndpoint { + private String uri; (14) + private AuthenticationMethod authenticationMethod; (15) + private String userNameAttributeName; (16) + + } + } +} +``` + +|**1** |`registrationId`:唯一标识`ClientRegistration`的 ID。| +|------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +|**2** |`clientId`:客户端标识符。| +|**3** |`clientSecret`:客户端秘密。| +|**4** |`clientAuthenticationMethod`:用于与提供程序验证客户端的方法。
支持的值是**客户端 \_Secret\_BASIC **,**客户端 \_Secret\_post **,**Private\_Key\_JWT **,**客户端 \_Secret\_JWT **和**无**[(公众客户)](https://tools.ietf.org/html/rfc6749#section-2.1)。| +|**5** |`authorizationGrantType`:OAuth2.0 授权框架定义了四个[授权授予](https://tools.ietf.org/html/rfc6749#section-1.3)类型。
支持的值是`authorization_code`,`client_credentials`,`password`,以及,扩展授权类型`urn:ietf:params:oauth:grant-type:jwt-bearer`。| +|**6** |`redirectUri`:客户端注册的重定向 URI,在最终用户对客户端进行了身份验证和授权访问之后,*授权服务器*将最终用户的用户代理
重定向到该 URI。| +|**7** |`scopes`:客户端在授权请求流期间请求的范围,例如 OpenID、电子邮件或配置文件。| +|**8** |`clientName`:用于客户机的描述性名称。
该名称可用于某些场景,例如在自动生成的登录页面中显示客户机的名称时。| +|**9** |`authorizationUri`:授权服务器的授权端点 URI。| +|**10**|`tokenUri`:授权服务器的令牌端点 URI。| +|**11**|`jwkSetUri`:用于从授权服务器检索[JSON Web Key ](https://tools.ietf.org/html/rfc7517)集的 URI,
,其中包含用于验证 ID 令牌的[JSON Web 签名](https://tools.ietf.org/html/rfc7515)的加密密钥,以及可选的 userinfo 响应。| +|**12**|`issuerUri`:返回 OpenID Connect1.0 提供程序或 OAuth2.0 授权服务器的发行者标识符 URI。| +|**13**|`configurationMetadata`:[OpenID 提供者配置信息](https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig).
只有在配置了 Spring boot2.x 属性`spring.security.oauth2.client.provider.[providerId].issuerUri`时,该信息才可用。| +|**14**|`(userInfoEndpoint)uri`:用于访问经过身份验证的最终用户的声明/属性的 userinfo 端点 URI。| +|**15**|`(userInfoEndpoint)authenticationMethod`:向 UserInfo 端点发送访问令牌时使用的身份验证方法。
支持的值是**页眉**、**形式**和**查询**。| +|**16**|`userNameAttributeName`:在引用最终用户的名称或标识符的 UserInfo 响应中返回的属性的名称。| + +可以使用发现 OpenID Connect 提供者的[配置端点](https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig)或授权服务器的[元数据端点](https://tools.ietf.org/html/rfc8414#section-3)来初始配置`ClientRegistration`。 + +`ClientRegistrations`以这种方式为配置`ClientRegistration`提供了方便的方法,如下例所示: + +爪哇 + +``` +ClientRegistration clientRegistration = + ClientRegistrations.fromIssuerLocation("https://idp.example.com/issuer").build(); +``` + +Kotlin + +``` +val clientRegistration = ClientRegistrations.fromIssuerLocation("https://idp.example.com/issuer").build() +``` + +上面的代码将在系列`[https://idp.example.com/issuer/.well-known/openid-configuration](https://idp.example.com/issuer/.well-known/openid-configuration)`中查询,然后`[https://idp.example.com/.well-known/openid-configuration/issuer](https://idp.example.com/.well-known/openid-configuration/issuer)`,最后是`[https://idp.example.com/.well-known/oauth-authorization-server/issuer](https://idp.example.com/.well-known/oauth-authorization-server/issuer)`,在第一次停止时返回 200 响应。 + +作为一种替代方法,你可以使用`ClientRegistrations.fromOidcIssuerLocation()`仅查询 OpenID Connect 提供者的配置端点。 + +## 重新激活注册存储库 + +`ReactiveClientRegistrationRepository`充当 OAuth2.0/OpenID Connect1.0`ClientRegistration`(s)的存储库。 + +| |客户端注册信息最终由关联的授权服务器存储和拥有。
此存储库提供检索主客户端注册信息的子集的能力,该子集与授权服务器一起存储。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +Spring Boot2.x Auto-Configuration 将`spring.security.oauth2.client.registration.*[registrationId]*`下的每个属性绑定到`ClientRegistration`的一个实例,然后在`ClientRegistration`中组合每个`ClientRegistration`实例。 + +| |`ReactiveClientRegistrationRepository`的默认实现是`InMemoryReactiveClientRegistrationRepository`。| +|---|-----------------------------------------------------------------------------------------------------------------------| + +自动配置还在`ApplicationContext`中将`ReactiveClientRegistrationRepository`注册为`@Bean`,以便在应用程序需要时可用于依赖注入。 + +下面的清单展示了一个示例: + +爪哇 + +``` +@Controller +public class OAuth2ClientController { + + @Autowired + private ReactiveClientRegistrationRepository clientRegistrationRepository; + + @GetMapping("/") + public Mono index() { + return this.clientRegistrationRepository.findByRegistrationId("okta") + ... + .thenReturn("index"); + } +} +``` + +Kotlin + +``` +@Controller +class OAuth2ClientController { + + @Autowired + private lateinit var clientRegistrationRepository: ReactiveClientRegistrationRepository + + @GetMapping("/") + fun index(): Mono { + return this.clientRegistrationRepository.findByRegistrationId("okta") + ... + .thenReturn("index") + } +} +``` + +## OAuth2 授权客户端 + +`OAuth2AuthorizedClient`是授权客户的表示。当最终用户(资源所有者)已向客户端授予访问其受保护资源的授权时,客户端被视为已被授权。 + +`OAuth2AuthorizedClient`的目的是将`OAuth2AccessToken`(和可选`OAuth2RefreshToken`)关联到`ClientRegistration`(客户端)和资源所有者,后者是授予授权的`Principal`最终用户。 + +## serveroauth2authorizedclientpository/reactiveoauth2authorizedclientservice + +`ServerOAuth2AuthorizedClientRepository`负责在 Web 请求之间持久化`OAuth2AuthorizedClient`。然而,`ReactiveOAuth2AuthorizedClientService`的主要作用是在应用程序级管理`OAuth2AuthorizedClient`。 + +从开发人员的角度来看,`ServerOAuth2AuthorizedClientRepository`或`ReactiveOAuth2AuthorizedClientService`提供了查找与客户端关联的`OAuth2AccessToken`的功能,以便可以使用它来发起受保护的资源请求。 + +下面的清单展示了一个示例: + +爪哇 + +``` +@Controller +public class OAuth2ClientController { + + @Autowired + private ReactiveOAuth2AuthorizedClientService authorizedClientService; + + @GetMapping("/") + public Mono index(Authentication authentication) { + return this.authorizedClientService.loadAuthorizedClient("okta", authentication.getName()) + .map(OAuth2AuthorizedClient::getAccessToken) + ... + .thenReturn("index"); + } +} +``` + +Kotlin + +``` +@Controller +class OAuth2ClientController { + + @Autowired + private lateinit var authorizedClientService: ReactiveOAuth2AuthorizedClientService + + @GetMapping("/") + fun index(authentication: Authentication): Mono { + return this.authorizedClientService.loadAuthorizedClient("okta", authentication.name) + .map { it.accessToken } + ... + .thenReturn("index") + } +} +``` + +| |Spring Boot2.x 自动配置在`ApplicationContext`中注册了一个`ServerOAuth2AuthorizedClientRepository`和/或`ReactiveOAuth2AuthorizedClientService``@Bean`。
但是,应用程序可以选择覆盖和注册一个自定义的`ServerOAuth2AuthorizedClientRepository`或`ReactiveOAuth2AuthorizedClientService`。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +`ReactiveOAuth2AuthorizedClientService`的默认实现是`InMemoryReactiveOAuth2AuthorizedClientService`,它在内存中存储`OAuth2AuthorizedClient`。 + +或者,R2DBC 实现可以被配置为在数据库中持久化。 + +| |`R2dbcReactiveOAuth2AuthorizedClientService`取决于[OAuth2.0 客户端模式](../../../servlet/appendix/database-schema.html#dbschema-oauth2-client)中描述的表定义。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +## reactiveOAuth2AuthorizedClientManager/reactiveOAuth2AuthorizedClientProvider + +`ReactiveOAuth2AuthorizedClientManager`负责`OAuth2AuthorizedClient`(s)的全面管理。 + +主要职责包括: + +* 使用`ReactiveOAuth2AuthorizedClientProvider`对 OAuth2.0 客户端进行授权(或重新授权)。 + +* 委派`OAuth2AuthorizedClient`的持久性,通常使用`ReactiveOAuth2AuthorizedClientService`或`ServerOAuth2AuthorizedClientRepository`。 + +* 当一个 OAuth2.0 客户端已被成功授权(或重新授权)时,将其委托给`ReactiveOAuth2AuthorizationSuccessHandler`。 + +* 当 OAuth2.0 客户端未能授权(或重新授权)时,将其委托给`ReactiveOAuth2AuthorizationFailureHandler`。 + +`ReactiveOAuth2AuthorizedClientProvider`实现了对 OAuth2.0 客户端进行授权(或重新授权)的策略。实现通常将实现一种授权授予类型,例如。`authorization_code`,`client_credentials`等。 + +`ReactiveOAuth2AuthorizedClientManager`的默认实现是`DefaultReactiveOAuth2AuthorizedClientManager`,它与`ReactiveOAuth2AuthorizedClientProvider`相关联,后者可能使用基于委托的组合来支持多个授权授予类型。`ReactiveOAuth2AuthorizedClientProviderBuilder`可用于配置和构建基于委托的组合。 + +下面的代码展示了如何配置和构建`ReactiveOAuth2AuthorizedClientProvider`组合的示例,该组合为`authorization_code`、`refresh_token`、`client_credentials`和`password`授权授予类型提供支持: + +爪哇 + +``` +@Bean +public ReactiveOAuth2AuthorizedClientManager authorizedClientManager( + ReactiveClientRegistrationRepository clientRegistrationRepository, + ServerOAuth2AuthorizedClientRepository authorizedClientRepository) { + + ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = + ReactiveOAuth2AuthorizedClientProviderBuilder.builder() + .authorizationCode() + .refreshToken() + .clientCredentials() + .password() + .build(); + + DefaultReactiveOAuth2AuthorizedClientManager authorizedClientManager = + new DefaultReactiveOAuth2AuthorizedClientManager( + clientRegistrationRepository, authorizedClientRepository); + authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); + + return authorizedClientManager; +} +``` + +Kotlin + +``` +@Bean +fun authorizedClientManager( + clientRegistrationRepository: ReactiveClientRegistrationRepository, + authorizedClientRepository: ServerOAuth2AuthorizedClientRepository): ReactiveOAuth2AuthorizedClientManager { + val authorizedClientProvider: ReactiveOAuth2AuthorizedClientProvider = ReactiveOAuth2AuthorizedClientProviderBuilder.builder() + .authorizationCode() + .refreshToken() + .clientCredentials() + .password() + .build() + val authorizedClientManager = DefaultReactiveOAuth2AuthorizedClientManager( + clientRegistrationRepository, authorizedClientRepository) + authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider) + return authorizedClientManager +} +``` + +当授权尝试成功时,`DefaultReactiveOAuth2AuthorizedClientManager`将委托给`ReactiveOAuth2AuthorizationSuccessHandler`,该(默认情况下)将通过`OAuth2AuthorizedClient`保存`OAuth2AuthorizedClient`。在重新授权失败的情况下,例如,刷新令牌不再有效,先前保存的`OAuth2AuthorizedClient`将通过`RemoveAuthorizedClientReactiveOAuth2AuthorizationFailureHandler`从`ServerOAuth2AuthorizedClientRepository`中删除。默认行为可以通过`setAuthorizationSuccessHandler(ReactiveOAuth2AuthorizationSuccessHandler)`和`setAuthorizationFailureHandler(ReactiveOAuth2AuthorizationFailureHandler)`进行定制。 + +`DefaultReactiveOAuth2AuthorizedClientManager`还与类型`Function>>`的`contextAttributesMapper`相关联,它负责将属性从`OAuth2AuthorizeRequest`映射到要与`OAuth2AuthorizationContext`相关联的属性的`Map`。当你需要提供带有 Required(Supported)属性的`ReactiveOAuth2AuthorizedClientProvider`时,这可能是有用的,例如,`PasswordReactiveOAuth2AuthorizedClientProvider`要求资源所有者的`username`和`password`在`OAuth2AuthorizationContext.getAttributes()`中可用。 + +下面的代码显示了`contextAttributesMapper`的示例: + +爪哇 + +``` +@Bean +public ReactiveOAuth2AuthorizedClientManager authorizedClientManager( + ReactiveClientRegistrationRepository clientRegistrationRepository, + ServerOAuth2AuthorizedClientRepository authorizedClientRepository) { + + ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = + ReactiveOAuth2AuthorizedClientProviderBuilder.builder() + .password() + .refreshToken() + .build(); + + DefaultReactiveOAuth2AuthorizedClientManager authorizedClientManager = + new DefaultReactiveOAuth2AuthorizedClientManager( + clientRegistrationRepository, authorizedClientRepository); + authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); + + // Assuming the `username` and `password` are supplied as `ServerHttpRequest` parameters, + // map the `ServerHttpRequest` parameters to `OAuth2AuthorizationContext.getAttributes()` + authorizedClientManager.setContextAttributesMapper(contextAttributesMapper()); + + return authorizedClientManager; +} + +private Function>> contextAttributesMapper() { + return authorizeRequest -> { + Map contextAttributes = Collections.emptyMap(); + ServerWebExchange exchange = authorizeRequest.getAttribute(ServerWebExchange.class.getName()); + ServerHttpRequest request = exchange.getRequest(); + String username = request.getQueryParams().getFirst(OAuth2ParameterNames.USERNAME); + String password = request.getQueryParams().getFirst(OAuth2ParameterNames.PASSWORD); + if (StringUtils.hasText(username) && StringUtils.hasText(password)) { + contextAttributes = new HashMap<>(); + + // `PasswordReactiveOAuth2AuthorizedClientProvider` requires both attributes + contextAttributes.put(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, username); + contextAttributes.put(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, password); + } + return Mono.just(contextAttributes); + }; +} +``` + +Kotlin + +``` +@Bean +fun authorizedClientManager( + clientRegistrationRepository: ReactiveClientRegistrationRepository, + authorizedClientRepository: ServerOAuth2AuthorizedClientRepository): ReactiveOAuth2AuthorizedClientManager { + val authorizedClientProvider: ReactiveOAuth2AuthorizedClientProvider = ReactiveOAuth2AuthorizedClientProviderBuilder.builder() + .password() + .refreshToken() + .build() + val authorizedClientManager = DefaultReactiveOAuth2AuthorizedClientManager( + clientRegistrationRepository, authorizedClientRepository) + authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider) + + // Assuming the `username` and `password` are supplied as `ServerHttpRequest` parameters, + // map the `ServerHttpRequest` parameters to `OAuth2AuthorizationContext.getAttributes()` + authorizedClientManager.setContextAttributesMapper(contextAttributesMapper()) + return authorizedClientManager +} + +private fun contextAttributesMapper(): Function>> { + return Function { authorizeRequest -> + var contextAttributes: MutableMap = mutableMapOf() + val exchange: ServerWebExchange = authorizeRequest.getAttribute(ServerWebExchange::class.java.name)!! + val request: ServerHttpRequest = exchange.request + val username: String? = request.queryParams.getFirst(OAuth2ParameterNames.USERNAME) + val password: String? = request.queryParams.getFirst(OAuth2ParameterNames.PASSWORD) + if (StringUtils.hasText(username) && StringUtils.hasText(password)) { + contextAttributes = hashMapOf() + + // `PasswordReactiveOAuth2AuthorizedClientProvider` requires both attributes + contextAttributes[OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME] = username!! + contextAttributes[OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME] = password!! + } + Mono.just(contextAttributes) + } +} +``` + +`DefaultReactiveOAuth2AuthorizedClientManager`被设计用于***内***`ServerWebExchange`的上下文。当操作***外面***的`ServerWebExchange`上下文时,请使用`AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager`。 + +当使用`AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager`时,*服务应用程序*是一个常见的用例。服务应用程序通常在后台运行,没有任何用户交互,并且通常在系统级帐户而不是用户帐户下运行。配置为`client_credentials`grant 类型的 OAuth2.0 客户机可以被视为服务应用程序的一种类型。 + +下面的代码展示了如何配置`AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager`的示例,该示例为`client_credentials`grant 类型提供支持: + +爪哇 + +``` +@Bean +public ReactiveOAuth2AuthorizedClientManager authorizedClientManager( + ReactiveClientRegistrationRepository clientRegistrationRepository, + ReactiveOAuth2AuthorizedClientService authorizedClientService) { + + ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = + ReactiveOAuth2AuthorizedClientProviderBuilder.builder() + .clientCredentials() + .build(); + + AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager authorizedClientManager = + new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager( + clientRegistrationRepository, authorizedClientService); + authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); + + return authorizedClientManager; +} +``` + +Kotlin + +``` +@Bean +fun authorizedClientManager( + clientRegistrationRepository: ReactiveClientRegistrationRepository, + authorizedClientService: ReactiveOAuth2AuthorizedClientService): ReactiveOAuth2AuthorizedClientManager { + val authorizedClientProvider: ReactiveOAuth2AuthorizedClientProvider = ReactiveOAuth2AuthorizedClientProviderBuilder.builder() + .clientCredentials() + .build() + val authorizedClientManager = AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager( + clientRegistrationRepository, authorizedClientService) + authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider) + return authorizedClientManager +} +``` + +[OAuth2 客户端](index.html)[OAuth2 授权授予](authorization-grants.html) \ No newline at end of file diff --git a/docs/spring-security/reactive-oauth2-client.md b/docs/spring-security/reactive-oauth2-client.md new file mode 100644 index 0000000000000000000000000000000000000000..8ce01e26fae0dac6219f39313e1bf1d7009aa91a --- /dev/null +++ b/docs/spring-security/reactive-oauth2-client.md @@ -0,0 +1,132 @@ +# OAuth2.0 客户端 + +OAuth2.0 客户机特性为[OAuth2.0 授权框架](https://tools.ietf.org/html/rfc6749#section-1.1)中定义的客户机角色提供支持。 + +在高层次上,可用的核心特性是: + +授权赠款支助 + +* [授权代码](https://tools.ietf.org/html/rfc6749#section-1.3.1) + +* [刷新令牌](https://tools.ietf.org/html/rfc6749#section-6) + +* [客户凭据](https://tools.ietf.org/html/rfc6749#section-1.3.4) + +* [资源所有者密码凭据](https://tools.ietf.org/html/rfc6749#section-1.3.3) + +* [JWT Bearer](https://datatracker.ietf.org/doc/html/rfc7523#section-2.1) + +客户端身份验证支持 + +* [JWT Bearer](https://datatracker.ietf.org/doc/html/rfc7523#section-2.2) + +HTTP 客户端支持 + +* [`WebClient`用于反应性环境的集成](#OAuth2client-webclient-webflux)(用于请求受保护的资源) + +`ServerHttpSecurity.oauth2Client()`DSL 为定制 OAuth2.0 客户端使用的核心组件提供了许多配置选项。 + +以下代码显示了`ServerHttpSecurity.oauth2Client()`DSL 提供的完整配置选项: + +例 1。OAuth2 客户端配置选项 + +爪哇 + +``` +@EnableWebFluxSecurity +public class OAuth2ClientSecurityConfig { + + @Bean + public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { + http + .oauth2Client(oauth2 -> oauth2 + .clientRegistrationRepository(this.clientRegistrationRepository()) + .authorizedClientRepository(this.authorizedClientRepository()) + .authorizationRequestRepository(this.authorizationRequestRepository()) + .authenticationConverter(this.authenticationConverter()) + .authenticationManager(this.authenticationManager()) + ); + + return http.build(); + } +} +``` + +Kotlin + +``` +@EnableWebFluxSecurity +class OAuth2ClientSecurityConfig { + + @Bean + fun securityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + oauth2Client { + clientRegistrationRepository = clientRegistrationRepository() + authorizedClientRepository = authorizedClientRepository() + authorizationRequestRepository = authorizedRequestRepository() + authenticationConverter = authenticationConverter() + authenticationManager = authenticationManager() + } + } + } +} +``` + +`ReactiveOAuth2AuthorizedClientManager`负责与一个或多个`ReactiveOAuth2AuthorizedClientProvider`协作管理 OAuth2.0 客户端的授权(或重新授权)。 + +下面的代码显示了如何注册`ReactiveOAuth2AuthorizedClientManager``@Bean`并将其与`ReactiveOAuth2AuthorizedClientProvider`复合相关联的示例,该复合提供对`authorization_code`、`refresh_token`、`client_credentials`和`password`授权授予类型的支持: + +爪哇 + +``` +@Bean +public ReactiveOAuth2AuthorizedClientManager authorizedClientManager( + ReactiveClientRegistrationRepository clientRegistrationRepository, + ServerOAuth2AuthorizedClientRepository authorizedClientRepository) { + + ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider = + ReactiveOAuth2AuthorizedClientProviderBuilder.builder() + .authorizationCode() + .refreshToken() + .clientCredentials() + .password() + .build(); + + DefaultReactiveOAuth2AuthorizedClientManager authorizedClientManager = + new DefaultReactiveOAuth2AuthorizedClientManager( + clientRegistrationRepository, authorizedClientRepository); + authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); + + return authorizedClientManager; +} +``` + +Kotlin + +``` +@Bean +fun authorizedClientManager( + clientRegistrationRepository: ReactiveClientRegistrationRepository, + authorizedClientRepository: ServerOAuth2AuthorizedClientRepository): ReactiveOAuth2AuthorizedClientManager { + val authorizedClientProvider: ReactiveOAuth2AuthorizedClientProvider = ReactiveOAuth2AuthorizedClientProviderBuilder.builder() + .authorizationCode() + .refreshToken() + .clientCredentials() + .password() + .build() + val authorizedClientManager = DefaultReactiveOAuth2AuthorizedClientManager( + clientRegistrationRepository, authorizedClientRepository) + authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider) + return authorizedClientManager +} +``` + +## 章节摘要 + +* [核心接口和类](core.html) +* [OAuth2 授权授予](authorization-grants.html) +* [OAuth2 客户端身份验证](client-authentication.html) +* [OAuth2 授权客户](authorized-clients.html) + +[高级配置](../login/advanced.html)[核心接口和类](core.html) \ No newline at end of file diff --git a/docs/spring-security/reactive-oauth2-login-advanced.md b/docs/spring-security/reactive-oauth2-login-advanced.md new file mode 100644 index 0000000000000000000000000000000000000000..ac0d0de15143189774301c150a783a8303b43655 --- /dev/null +++ b/docs/spring-security/reactive-oauth2-login-advanced.md @@ -0,0 +1,676 @@ +# 高级配置 + +OAuth2.0 授权框架将[协议端点](https://tools.ietf.org/html/rfc6749#section-3)定义如下: + +授权过程使用两个授权服务器端点(HTTP 资源): + +* 授权端点:由客户端使用,通过用户代理重定向从资源所有者处获得授权。 + +* 令牌端点:由客户端使用,用于将授权许可交换为访问令牌,通常与客户端身份验证一起使用。 + +以及一个客户端端点: + +* 重定向端点:授权服务器用于通过资源所有者 User-Agent 向客户端返回包含授权凭据的响应。 + +OpenID Connect Core1.0 规范将[用户信息端点](https://openid.net/specs/openid-connect-core-1_0.html#UserInfo)定义如下: + +UserInfo 端点是一个受 OAuth2.0 保护的资源,它返回关于经过身份验证的最终用户的声明。为了获得所请求的关于最终用户的权利要求,客户端使用通过 OpenID Connect 身份验证获得的访问令牌向 UserInfo 端点发出请求。这些权利要求通常由一个 JSON 对象表示,该对象包含权利要求的名称-值对的集合。 + +`ServerHttpSecurity.oauth2Login()`提供了许多用于定制 OAuth2.0 登录的配置选项。 + +以下代码显示了`oauth2Login()`DSL 可用的完整配置选项: + +例 1。OAuth2 登录配置选项 + +爪哇 + +``` +@EnableWebFluxSecurity +public class OAuth2LoginSecurityConfig { + + @Bean + SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { + http + .oauth2Login(oauth2 -> oauth2 + .authenticationConverter(this.authenticationConverter()) + .authenticationMatcher(this.authenticationMatcher()) + .authenticationManager(this.authenticationManager()) + .authenticationSuccessHandler(this.authenticationSuccessHandler()) + .authenticationFailureHandler(this.authenticationFailureHandler()) + .clientRegistrationRepository(this.clientRegistrationRepository()) + .authorizedClientRepository(this.authorizedClientRepository()) + .authorizedClientService(this.authorizedClientService()) + .authorizationRequestResolver(this.authorizationRequestResolver()) + .authorizationRequestRepository(this.authorizationRequestRepository()) + .securityContextRepository(this.securityContextRepository()) + ); + + return http.build(); + } +} +``` + +Kotlin + +``` +@EnableWebFluxSecurity +class OAuth2LoginSecurityConfig { + + @Bean + fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + oauth2Login { + authenticationConverter = authenticationConverter() + authenticationMatcher = authenticationMatcher() + authenticationManager = authenticationManager() + authenticationSuccessHandler = authenticationSuccessHandler() + authenticationFailureHandler = authenticationFailureHandler() + clientRegistrationRepository = clientRegistrationRepository() + authorizedClientRepository = authorizedClientRepository() + authorizedClientService = authorizedClientService() + authorizationRequestResolver = authorizationRequestResolver() + authorizationRequestRepository = authorizationRequestRepository() + securityContextRepository = securityContextRepository() + } + } + } +} +``` + +以下小节将详细介绍每种可用的配置选项: + +* [OAuth2.0 登录页面](#webflux-oauth2-login-advanced-login-page) + +* [重定向端点](#webflux-oauth2-login-advanced-redirection-endpoint) + +* [用户信息端点](#webflux-oauth2-login-advanced-userinfo-endpoint) + +* [ID 令牌签名验证](#webflux-oauth2-login-advanced-idtoken-verify) + +* [OpenID Connect1.0 注销](#webflux-oauth2-login-advanced-oidc-logout) + +## OAuth2.0 登录页面 + +默认情况下,OAuth2.0 登录页面是由`LoginPageGeneratingWebFilter`自动生成的。默认的登录页面显示了每个配置的 OAuth 客户机,其`ClientRegistration.clientName`作为链接,该链接能够发起授权请求(或 OAuth2.0 登录)。 + +| |为了让`LoginPageGeneratingWebFilter`显示配置的 OAuth 客户端的链接,已注册的`ReactiveClientRegistrationRepository`还需要实现`Iterable`。
参见`InMemoryReactiveClientRegistrationRepository`以供参考。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +每个 OAuth 客户端的链接目标默认为以下内容: + +`"/oauth2/authorization/{registrationId}"` + +下面的一行显示了一个示例: + +``` +Google +``` + +要覆盖默认的登录页面,请配置`exceptionHandling().authenticationEntryPoint()`和(可选的)`oauth2Login().authorizationRequestResolver()`。 + +下面的清单展示了一个示例: + +例 2。OAuth2 登录页面配置 + +爪哇 + +``` +@EnableWebFluxSecurity +public class OAuth2LoginSecurityConfig { + + @Bean + public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { + http + .exceptionHandling(exceptionHandling -> exceptionHandling + .authenticationEntryPoint(new RedirectServerAuthenticationEntryPoint("/login/oauth2")) + ) + .oauth2Login(oauth2 -> oauth2 + .authorizationRequestResolver(this.authorizationRequestResolver()) + ); + + return http.build(); + } + + private ServerOAuth2AuthorizationRequestResolver authorizationRequestResolver() { + ServerWebExchangeMatcher authorizationRequestMatcher = + new PathPatternParserServerWebExchangeMatcher( + "/login/oauth2/authorization/{registrationId}"); + + return new DefaultServerOAuth2AuthorizationRequestResolver( + this.clientRegistrationRepository(), authorizationRequestMatcher); + } + + ... +} +``` + +Kotlin + +``` +@EnableWebFluxSecurity +class OAuth2LoginSecurityConfig { + + @Bean + fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + exceptionHandling { + authenticationEntryPoint = RedirectServerAuthenticationEntryPoint("/login/oauth2") + } + oauth2Login { + authorizationRequestResolver = authorizationRequestResolver() + } + } + } + + private fun authorizationRequestResolver(): ServerOAuth2AuthorizationRequestResolver { + val authorizationRequestMatcher: ServerWebExchangeMatcher = PathPatternParserServerWebExchangeMatcher( + "/login/oauth2/authorization/{registrationId}" + ) + + return DefaultServerOAuth2AuthorizationRequestResolver( + clientRegistrationRepository(), authorizationRequestMatcher + ) + } + + ... +} +``` + +| |你需要提供一个`@Controller`和一个`@RequestMapping("/login/oauth2")`,它能够呈现自定义登录页面。| +|---|---------------------------------------------------------------------------------------------------------------------------------| + +| |如前所述,配置`oauth2Login().authorizationRequestResolver()`是可选的。
但是,如果你选择自定义它,请确保到每个 OAuth 客户机的链接匹配通过`ServerWebExchangeMatcher`提供的模式。

下面的一行显示了一个示例:

| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +## 重定向端点 + +授权服务器使用重定向端点通过资源所有者 User-Agent 将授权响应(其中包含授权凭据)返回给客户端。 + +| |OAuth2.0Login 利用授权代码授权。
因此,授权凭据是授权代码。| +|---|------------------------------------------------------------------------------------------------------------------------------| + +默认的授权响应重定向端点是`/login/oauth2/code/{registrationId}`。 + +如果你希望自定义授权响应重定向端点,请按照下面的示例配置它: + +例 3。重定向端点配置 + +爪哇 + +``` +@EnableWebFluxSecurity +public class OAuth2LoginSecurityConfig { + + @Bean + public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { + http + .oauth2Login(oauth2 -> oauth2 + .authenticationMatcher(new PathPatternParserServerWebExchangeMatcher("/login/oauth2/callback/{registrationId}")) + ); + + return http.build(); + } +} +``` + +Kotlin + +``` +@EnableWebFluxSecurity +class OAuth2LoginSecurityConfig { + + @Bean + fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + oauth2Login { + authenticationMatcher = PathPatternParserServerWebExchangeMatcher("/login/oauth2/callback/{registrationId}") + } + } + } +} +``` + +| |你还需要确保`ClientRegistration.redirectUri`与自定义授权响应重定向端点相匹配。

下面的列表显示了一个示例:

爪哇
```
return CommonOAuth2Provider.GOOGLE.getBuilder("google")
.clientId("google-client-id")
.clientSecret("google-client-secret")
.redirectUri("{baseUrl}/login/oauth2/callback/{registrationId}")
.build();
```
Kotlin <>r=”64“/>>| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +## 用户信息端点 + +UserInfo 端点包括许多配置选项,如下文子部分所述: + +* [映射用户权限](#webflux-oauth2-login-advanced-map-authorities) + +* [OAuth2.0 用户服务](#webflux-oauth2-login-advanced-oauth2-user-service) + +* [OpenID Connect1.0 用户服务](#webflux-oauth2-login-advanced-oidc-user-service) + +### 映射用户权限 + +在用户成功地使用 OAuth2.0 提供程序进行身份验证之后,`OAuth2User.getAuthorities()`(或`OidcUser.getAuthorities()`)可以被映射到一组新的`GrantedAuthority`实例,在完成身份验证时,该实例将被提供给`OAuth2AuthenticationToken`。 + +| |`OAuth2AuthenticationToken.getAuthorities()`用于授权请求,例如在`hasRole('USER')`或`hasRole('ADMIN')`中。| +|---|----------------------------------------------------------------------------------------------------------------------------------| + +在映射用户权限时,有两个选项可供选择: + +* [使用 grantedauthoritiesmapper ](#webflux-oauth2-login-advanced-map-authorities-grantedauthoritiesmapper) + +* [基于 reactiveOAuth2UserService 的授权策略](#webflux-oauth2-login-advanced-map-authorities-reactiveoauth2userservice) + +#### 使用 grantedauthoritiesmapper + +注册一个`GrantedAuthoritiesMapper``@Bean`以使其自动应用于配置,如以下示例所示: + +例 4。授予权限映射器配置 + +爪哇 + +``` +@EnableWebFluxSecurity +public class OAuth2LoginSecurityConfig { + + @Bean + public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { + http + ... + .oauth2Login(withDefaults()); + + return http.build(); + } + + @Bean + public GrantedAuthoritiesMapper userAuthoritiesMapper() { + return (authorities) -> { + Set 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 userAttributes = oauth2UserAuthority.getAttributes(); + + // Map the attributes found in userAttributes + // to one or more GrantedAuthority's and add it to mappedAuthorities + + } + }); + + return mappedAuthorities; + }; + } +} +``` + +Kotlin + +``` +@EnableWebFluxSecurity +class OAuth2LoginSecurityConfig { + + @Bean + fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + oauth2Login { } + } + } + + @Bean + fun userAuthoritiesMapper(): GrantedAuthoritiesMapper = GrantedAuthoritiesMapper { authorities: Collection -> + val mappedAuthorities = emptySet() + + authorities.forEach { authority -> + if (authority is OidcUserAuthority) { + val idToken = authority.idToken + val userInfo = authority.userInfo + // Map the claims found in idToken and/or userInfo + // to one or more GrantedAuthority's and add it to mappedAuthorities + } else if (authority is OAuth2UserAuthority) { + val userAttributes = authority.attributes + // Map the attributes found in userAttributes + // to one or more GrantedAuthority's and add it to mappedAuthorities + } + } + + mappedAuthorities + } +} +``` + +#### 基于 reactiveOAuth2UserService 的授权策略 + +与使用`GrantedAuthoritiesMapper`相比,这种策略是先进的,但是,它也更灵活,因为它允许你访问`OAuth2UserRequest`和`OAuth2User`(当使用 OAuth2.0 用户服务时)或`OidcUserRequest`和`OidcUser`(当使用 OpenID Connect1.0 用户服务时)。 + +`OAuth2UserRequest`(和`OidcUserRequest`)为你提供了对相关`OAuth2AccessToken`的访问,这在*委派者*需要从受保护的资源获取权限信息,然后才能为用户映射自定义权限的情况下非常有用。 + +下面的示例展示了如何使用 OpenID Connect1.0UserService 实现和配置基于委托的策略: + +例 5。ReactiveOAuth2 用户服务配置 + +爪哇 + +``` +@EnableWebFluxSecurity +public class OAuth2LoginSecurityConfig { + + @Bean + public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { + http + ... + .oauth2Login(withDefaults()); + + return http.build(); + } + + @Bean + public ReactiveOAuth2UserService oidcUserService() { + final OidcReactiveOAuth2UserService delegate = new OidcReactiveOAuth2UserService(); + + return (userRequest) -> { + // Delegate to the default implementation for loading a user + return delegate.loadUser(userRequest) + .flatMap((oidcUser) -> { + OAuth2AccessToken accessToken = userRequest.getAccessToken(); + Set 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 Mono.just(oidcUser); + }); + }; + } +} +``` + +Kotlin + +``` +@EnableWebFluxSecurity +class OAuth2LoginSecurityConfig { + + @Bean + fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + oauth2Login { } + } + } + + @Bean + fun oidcUserService(): ReactiveOAuth2UserService { + val delegate = OidcReactiveOAuth2UserService() + + return ReactiveOAuth2UserService { userRequest -> + // Delegate to the default implementation for loading a user + delegate.loadUser(userRequest) + .flatMap { oidcUser -> + val accessToken = userRequest.accessToken + val mappedAuthorities = mutableSetOf() + + // 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 + val mappedOidcUser = DefaultOidcUser(mappedAuthorities, oidcUser.idToken, oidcUser.userInfo) + + Mono.just(mappedOidcUser) + } + } + } +} +``` + +### OAuth2.0 用户服务 + +`DefaultReactiveOAuth2UserService`是支持标准 OAuth2.0 提供者的`ReactiveOAuth2UserService`的实现。 + +| |`ReactiveOAuth2UserService`从 userinfo 端点获取最终用户(资源所有者)的用户属性(通过在授权流期间使用授予客户端的访问令牌),并以`OAuth2User`的形式返回`AuthenticatedPrincipal`。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +当在 UserInfo 端点请求用户属性时,`DefaultReactiveOAuth2UserService`使用`WebClient`。 + +如果需要自定义用户信息请求的预处理和/或用户信息响应的后处理,则需要为`DefaultReactiveOAuth2UserService.setWebClient()`提供自定义配置的`WebClient`。 + +无论你是自定义`DefaultReactiveOAuth2UserService`还是提供你自己的`ReactiveOAuth2UserService`实现,你都需要对其进行配置,如以下示例所示: + +爪哇 + +``` +@EnableWebFluxSecurity +public class OAuth2LoginSecurityConfig { + + @Bean + public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { + http + ... + .oauth2Login(withDefaults()); + + return http.build(); + } + + @Bean + public ReactiveOAuth2UserService oauth2UserService() { + ... + } +} +``` + +Kotlin + +``` +@EnableWebFluxSecurity +class OAuth2LoginSecurityConfig { + + @Bean + fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + oauth2Login { } + } + } + + @Bean + fun oauth2UserService(): ReactiveOAuth2UserService { + // ... + } +} +``` + +### OpenID Connect1.0 用户服务 + +`OidcReactiveOAuth2UserService`是支持 OpenID Connect1.0Provider 的`ReactiveOAuth2UserService`的实现。 + +当在 UserInfo 端点请求用户属性时,`OidcReactiveOAuth2UserService`利用了`DefaultReactiveOAuth2UserService`。 + +如果需要自定义用户信息请求的预处理和/或用户信息响应的后处理,则需要为`OidcReactiveOAuth2UserService.setOauth2UserService()`提供自定义配置的`ReactiveOAuth2UserService`。 + +无论你是定制`OidcReactiveOAuth2UserService`还是为 OpenID Connect1.0Provider 提供你自己的`ReactiveOAuth2UserService`实现,你都需要按照下面的示例配置它: + +爪哇 + +``` +@EnableWebFluxSecurity +public class OAuth2LoginSecurityConfig { + + @Bean + public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { + http + ... + .oauth2Login(withDefaults()); + + return http.build(); + } + + @Bean + public ReactiveOAuth2UserService oidcUserService() { + ... + } +} +``` + +Kotlin + +``` +@EnableWebFluxSecurity +class OAuth2LoginSecurityConfig { + + @Bean + fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + oauth2Login { } + } + } + + @Bean + fun oidcUserService(): ReactiveOAuth2UserService { + // ... + } +} +``` + +## ID 令牌签名验证 + +OpenID Connect1.0 身份验证引入了[ID Token](https://openid.net/specs/openid-connect-core-1_0.html#IDToken),这是一种安全令牌,其中包含关于客户端使用授权服务器对最终用户进行身份验证的声明。 + +ID 令牌表示为[JSON 网络令牌](https://tools.ietf.org/html/rfc7519),并且必须使用[JSON 网页签名](https://tools.ietf.org/html/rfc7515)进行签名。 + +`ReactiveOidcIdTokenDecoderFactory`提供了用于`ReactiveJwtDecoder`签名验证的`OidcIdToken`。默认的算法是`RS256`,但在客户端注册期间分配时可能会有所不同。对于这些情况,可以将解析器配置为返回为特定客户端分配的预期 JWS 算法。 + +JWS 算法解析器是一个`Function`,它接受一个`ClientRegistration`,并为客户机返回预期的`JwsAlgorithm`,例如。`SignatureAlgorithm.RS256`或`MacAlgorithm.HS256` + +下面的代码显示了如何将`OidcIdTokenDecoderFactory``@Bean`配置为所有`MacAlgorithm.HS256`的默认`MacAlgorithm.HS256`: + +爪哇 + +``` +@Bean +public ReactiveJwtDecoderFactory idTokenDecoderFactory() { + ReactiveOidcIdTokenDecoderFactory idTokenDecoderFactory = new ReactiveOidcIdTokenDecoderFactory(); + idTokenDecoderFactory.setJwsAlgorithmResolver(clientRegistration -> MacAlgorithm.HS256); + return idTokenDecoderFactory; +} +``` + +Kotlin + +``` +@Bean +fun idTokenDecoderFactory(): ReactiveJwtDecoderFactory { + val idTokenDecoderFactory = ReactiveOidcIdTokenDecoderFactory() + idTokenDecoderFactory.setJwsAlgorithmResolver { MacAlgorithm.HS256 } + return idTokenDecoderFactory +} +``` + +| |对于基于 MAC 的算法,如`HS256`、`HS384`或`HS512`,对应于`client-secret`的`client-secret`被用作签名验证的对称密钥。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |如果一个以上的`ClientRegistration`被配置为 OpenID Connect1.0 身份验证,则 JWS 算法解析器可以对提供的`ClientRegistration`进行评估,以确定返回哪个算法。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +## OpenID Connect1.0 注销 + +OpenID Connect会话Management1.0 允许使用客户端在提供商处注销最终用户。可用的策略之一是[RP 启动的注销](https://openid.net/specs/openid-connect-session-1_0.html#RPLogout)。 + +如果 OpenID 提供程序同时支持会话管理和[Discovery](https://openid.net/specs/openid-connect-discovery-1_0.html),则客户端可以从 OpenID 提供程序的`end_session_endpoint``URL`中获得[发现元数据](https://openid.net/specs/openid-connect-session-1_0.html#OPMetadata)。这可以通过配置`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 +``` + +…和`OidcClientInitiatedServerLogoutSuccessHandler`,它实现 RP-initedlogout,可以配置如下: + +Java + +``` +@EnableWebFluxSecurity +public class OAuth2LoginSecurityConfig { + + @Autowired + private ReactiveClientRegistrationRepository clientRegistrationRepository; + + @Bean + public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { + http + .authorizeExchange(authorize -> authorize + .anyExchange().authenticated() + ) + .oauth2Login(withDefaults()) + .logout(logout -> logout + .logoutSuccessHandler(oidcLogoutSuccessHandler()) + ); + + return http.build(); + } + + private ServerLogoutSuccessHandler oidcLogoutSuccessHandler() { + OidcClientInitiatedServerLogoutSuccessHandler oidcLogoutSuccessHandler = + new OidcClientInitiatedServerLogoutSuccessHandler(this.clientRegistrationRepository); + + // Sets the location that the End-User's User Agent will be redirected to + // after the logout has been performed at the Provider + oidcLogoutSuccessHandler.setPostLogoutRedirectUri("{baseUrl}"); + + return oidcLogoutSuccessHandler; + } +} +``` + +Kotlin + +``` +@EnableWebFluxSecurity +class OAuth2LoginSecurityConfig { + + @Autowired + private lateinit var clientRegistrationRepository: ReactiveClientRegistrationRepository + + @Bean + fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + authorizeExchange { + authorize(anyExchange, authenticated) + } + oauth2Login { } + logout { + logoutSuccessHandler = oidcLogoutSuccessHandler() + } + } + } + + private fun oidcLogoutSuccessHandler(): ServerLogoutSuccessHandler { + val oidcLogoutSuccessHandler = OidcClientInitiatedServerLogoutSuccessHandler(clientRegistrationRepository) + + // Sets the location that the End-User's User Agent will be redirected to + // after the logout has been performed at the Provider + oidcLogoutSuccessHandler.setPostLogoutRedirectUri("{baseUrl}") + return oidcLogoutSuccessHandler + } +} +``` + +| |`OidcClientInitiatedServerLogoutSuccessHandler`支持`{baseUrl}`占位符。
如果使用的话,应用程序的基本 URL,如`[https://app.example.org](https://app.example.org)`,将在请求时替换它。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +[核心配置](core.html)[OAuth2 客户端](../client/index.html) \ No newline at end of file diff --git a/docs/spring-security/reactive-oauth2-login-core.md b/docs/spring-security/reactive-oauth2-login-core.md new file mode 100644 index 0000000000000000000000000000000000000000..de0ffeb9afc7bc611473e37716825b7107eb2a0b --- /dev/null +++ b/docs/spring-security/reactive-oauth2-login-core.md @@ -0,0 +1,481 @@ +# 核心配置 + +## Spring 启动 2.x 示例 + +Spring Boot2.x 为 OAuth2.0 登录带来了完整的自动配置功能。 + +本节展示如何使用*谷歌*作为*身份验证提供者*配置[**OAuth2.0 登录 WebFlux 样本 **](https://github.com/spring-projects/spring-security-samples/tree/5.6.x/reactive/webflux/java/oauth2/login),并涵盖以下主题: + +* [初始设置](#webflux-oauth2-login-sample-setup) + +* [设置重定向 URI](#webflux-oauth2-login-sample-redirect) + +* [configure`application.yml`](#WebFlux-OAuth2-login-sample-config) + +* [启动应用程序](#webflux-oauth2-login-sample-start) + +### 初始设置 + +要使用 Google 的 OAuth2.0 身份验证系统进行登录,你必须在 Google API 控制台中设置一个项目,以获得 OAuth2.0 凭据。 + +| |用于身份验证的[谷歌的 OAuth2.0 实现](https://developers.google.com/identity/protocols/OpenIDConnect)符合[OpenID Connect1.0](https://openid.net/connect/)规范,并且是[OpenID 认证](https://openid.net/certification/)。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +按照[OpenID 连接](https://developers.google.com/identity/protocols/OpenIDConnect)页面上的说明,从“设置 OAuth2.0”一节开始。 + +在完成“获取 OAuth2.0 凭据”说明之后,你应该有一个新的 OAuth 客户机,其凭据由一个客户机 ID 和一个客户机秘密组成。 + +### 设置重定向 URI + +重定向 URI 是应用程序中的路径,终端用户的用户代理在与 Google 进行身份验证并授予对同意页面上的 OAuth 客户端 *([在前一步中创建](#webflux-oauth2-login-sample-setup))* 的访问权限后,会被重定向回应用程序中的路径。 + +在“设置重定向 URI”子节中,确保将**授权重定向 URI**字段设置为`[http://localhost:8080/login/oauth2/code/google](http://localhost:8080/login/oauth2/code/google)`。 + +| |默认的重定向 URI 模板是`{baseUrl}/login/oauth2/code/{registrationId}`。
***注册 ID***是[客户登记](../client/core.html#oauth2Client-client-registration)的唯一标识符。
对于我们的示例,`registrationId`是`google`。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |如果 OAuth 客户端运行在代理服务器后面,建议检查[代理服务器配置](../../../features/exploits/http.html#http-proxy-server)以确保应用程序配置正确。
此外,请参阅支持的[`URI`模板变量](../client/Authorization-grants.html#OAuth2client-auth-code-redirect-uri)。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### 配置`application.yml` + +现在,你有了一个新的 OAuth 客户机和 Google,你需要将应用程序配置为使用*认证流程*的 OAuth 客户机。这样做: + +1. 转到`application.yml`并设置以下配置: + + ``` + spring: + security: + oauth2: + client: + registration: (1) + google: (2) + client-id: google-client-id + client-secret: google-client-secret + ``` + + 例 1。OAuth 客户属性 + + |**1**|`spring.security.oauth2.client.registration`是 OAuth 客户机属性的基本属性前缀。| + |-----|--------------------------------------------------------------------------------------------------------------------------------------------------| + |**2**|在基本属性前缀之后是[`ClientRegistration`](../client/core.html#OAuth2client-client-registration)的 ID,例如 Google。| + +2. 用前面创建的 OAuth2.0 凭据替换`client-id`和`client-secret`属性中的值。 + +### 启动应用程序 + +启动 Spring boot2.x 示例,然后转到`[http://localhost:8080](http://localhost:8080)`。然后,你将被重定向到默认的*自动生成*登录页面,该页面显示了 Google 的链接。 + +点击谷歌链接,然后你将被重定向到谷歌进行身份验证。 + +在使用谷歌帐户凭据进行身份验证后,显示给你的下一页是“同意”屏幕。同意屏幕要求你允许或拒绝访问你之前创建的 OAuth 客户端。单击**允许**授权 OAuth 客户端访问你的电子邮件地址和基本配置文件信息。 + +此时,OAuth 客户机从[用户信息端点](https://openid.net/specs/openid-connect-core-1_0.html#UserInfo)检索你的电子邮件地址和基本配置文件信息,并建立经过身份验证的会话。 + +## Spring 启动 2.x 属性映射 + +下表概述了 Spring Boot2.x OAuth 客户机属性到[客户登记](../client/core.html#oauth2Client-client-registration)属性的映射。 + +|Spring 启动 2.x| ClientRegistration | +|--------------------------------------------------------------------------------------------|--------------------------------------------------------| +|`spring.security.oauth2.client.registration.*[registrationId]*`| `registrationId` | +|`spring.security.oauth2.client.registration.*[registrationId]*.client-id`| `clientId` | +|`spring.security.oauth2.client.registration.*[registrationId]*.client-secret`| `clientSecret` | +|`spring.security.oauth2.client.registration.*[registrationId]*.client-authentication-method`| `clientAuthenticationMethod` | +|`spring.security.oauth2.client.registration.*[registrationId]*.authorization-grant-type`| `authorizationGrantType` | +|`spring.security.oauth2.client.registration.*[registrationId]*.redirect-uri`| `redirectUri` | +|`spring.security.oauth2.client.registration.*[registrationId]*.scope`| `scopes` | +|`spring.security.oauth2.client.registration.*[registrationId]*.client-name`| `clientName` | +|`spring.security.oauth2.client.provider.*[providerId]*.authorization-uri`| `providerDetails.authorizationUri` | +|`spring.security.oauth2.client.provider.*[providerId]*.token-uri`| `providerDetails.tokenUri` | +|`spring.security.oauth2.client.provider.*[providerId]*.jwk-set-uri`| `providerDetails.jwkSetUri` | +|`spring.security.oauth2.client.provider.*[providerId]*.issuer-uri`| `providerDetails.issuerUri` | +|`spring.security.oauth2.client.provider.*[providerId]*.user-info-uri`| `providerDetails.userInfoEndpoint.uri` | +|`spring.security.oauth2.client.provider.*[providerId]*.user-info-authentication-method`|`providerDetails.userInfoEndpoint.authenticationMethod` | +|`spring.security.oauth2.client.provider.*[providerId]*.user-name-attribute`|`providerDetails.userInfoEndpoint.userNameAttributeName`| + +| |通过指定`spring.security.oauth2.client.provider.*[providerId]*.issuer-uri`属性,可以使用发现 OpenID Connect 提供者的[配置端点](https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig)或授权服务器的[元数据端点](https://tools.ietf.org/html/rfc8414#section-3)来初始配置`ClientRegistration`。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +## CommonoAuth2Provider + +`CommonOAuth2Provider`为许多著名的提供商预先定义了一组默认的客户端属性:Google、GitHub、Facebook 和 OKTA。 + +例如,对于提供者,`authorization-uri`、`token-uri`和`user-info-uri`不会经常更改。因此,为了减少所需的配置,提供默认值是有意义的。 + +如前所述,当我们[配置了一个 Google 客户端](#webflux-oauth2-login-sample-config)时,只需要`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()`中设置的客户机属性的自动违约。| + +## 配置自定义提供程序属性 + +有一些 OAuth2.0 提供程序支持多租赁,这会导致每个租户(或子域)的协议端点不同。 + +例如,向 OKTA 注册的 OAuth 客户端被分配到特定的子域并具有自己的协议端点。 + +对于这些情况, Spring Boot2.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`)允许自定义配置协议端点位置。| +|-----|---------------------------------------------------------------------------------------------------------------------------------| + +## 覆盖 Spring 启动 2.x 自动配置 + +OAuth 客户端支持的 Spring Boot2.x 自动配置类是`ReactiveOAuth2ClientAutoConfiguration`。 + +它执行以下任务: + +* 从配置的 OAuth 客户机属性中寄存器`ReactiveClientRegistrationRepository``@Bean`组成的`ClientRegistration`。 + +* 注册`SecurityWebFilterChain``@Bean`并通过`serverHttpSecurity.oauth2Login()`启用 OAuth2.0 登录。 + +如果需要根据你的特定需求重写自动配置,可以通过以下方式执行: + +* [注册一个重新激活的注册存储库 @ Bean](#webflux-oauth2-login-register-reactiveclientregistrationrepository-bean) + +* [注册一个 SecurityWebFilterchain@ Bean](#webflux-oauth2-login-register-securitywebfilterchain-bean) + +* [完全覆盖自动配置](#webflux-oauth2-login-completely-override-autoconfiguration) + +### Register a ReactiveClientRegistrationRepository @Bean + +下面的示例显示了如何注册`ReactiveClientRegistrationRepository``@Bean`: + +爪哇 + +``` +@Configuration +public class OAuth2LoginConfig { + + @Bean + public ReactiveClientRegistrationRepository clientRegistrationRepository() { + return new InMemoryReactiveClientRegistrationRepository(this.googleClientRegistration()); + } + + private ClientRegistration googleClientRegistration() { + return ClientRegistration.withRegistrationId("google") + .clientId("google-client-id") + .clientSecret("google-client-secret") + .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .redirectUri("{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(); + } +} +``` + +Kotlin + +``` +@Configuration +class OAuth2LoginConfig { + + @Bean + fun clientRegistrationRepository(): ReactiveClientRegistrationRepository { + return InMemoryReactiveClientRegistrationRepository(googleClientRegistration()) + } + + private fun googleClientRegistration(): ClientRegistration { + return ClientRegistration.withRegistrationId("google") + .clientId("google-client-id") + .clientSecret("google-client-secret") + .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .redirectUri("{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() + } +} +``` + +### Register a SecurityWebFilterChain @Bean + +下面的示例显示了如何用`@EnableWebFluxSecurity`注册`SecurityWebFilterChain``@Bean`并通过`serverHttpSecurity.oauth2Login()`启用 OAuth2.0 登录: + +例 2。OAuth2 登录配置 + +爪哇 + +``` +@EnableWebFluxSecurity +public class OAuth2LoginSecurityConfig { + + @Bean + public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { + http + .authorizeExchange(authorize -> authorize + .anyExchange().authenticated() + ) + .oauth2Login(withDefaults()); + + return http.build(); + } +} +``` + +Kotlin + +``` +@EnableWebFluxSecurity +class OAuth2LoginSecurityConfig { + + @Bean + fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + authorizeExchange { + authorize(anyExchange, authenticated) + } + oauth2Login { } + } + } +} +``` + +### 完全覆盖自动配置 + +下面的示例展示了如何通过注册一个`ReactiveClientRegistrationRepository``@Bean`和一个`SecurityWebFilterChain``@Bean`来完全覆盖自动配置。 + +例 3。覆盖自动配置 + +爪哇 + +``` +@EnableWebFluxSecurity +public class OAuth2LoginConfig { + + @Bean + public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { + http + .authorizeExchange(authorize -> authorize + .anyExchange().authenticated() + ) + .oauth2Login(withDefaults()); + + return http.build(); + } + + @Bean + public ReactiveClientRegistrationRepository clientRegistrationRepository() { + return new InMemoryReactiveClientRegistrationRepository(this.googleClientRegistration()); + } + + private ClientRegistration googleClientRegistration() { + return ClientRegistration.withRegistrationId("google") + .clientId("google-client-id") + .clientSecret("google-client-secret") + .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .redirectUri("{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(); + } +} +``` + +Kotlin + +``` +@EnableWebFluxSecurity +class OAuth2LoginConfig { + + @Bean + fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + authorizeExchange { + authorize(anyExchange, authenticated) + } + oauth2Login { } + } + } + + @Bean + fun clientRegistrationRepository(): ReactiveClientRegistrationRepository { + return InMemoryReactiveClientRegistrationRepository(googleClientRegistration()) + } + + private fun googleClientRegistration(): ClientRegistration { + return ClientRegistration.withRegistrationId("google") + .clientId("google-client-id") + .clientSecret("google-client-secret") + .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .redirectUri("{baseUrl}/login/oauth2/code/{registrationId}") + .scope("openid", "profile", "email", "address", "phone") + .authorizationUri("https://accounts.google.com/o/oauth2/v2/auth") + .tokenUri("https://www.googleapis.com/oauth2/v4/token") + .userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo") + .userNameAttributeName(IdTokenClaimNames.SUB) + .jwkSetUri("https://www.googleapis.com/oauth2/v3/certs") + .clientName("Google") + .build() + } +} +``` + +## 不需要 Spring boot2.x 的 爪哇 配置 + +如果你不能使用 Spring Boot2.x,并且希望在`CommonOAuth2Provider`中配置一个预定义的提供程序(例如,Google),请应用以下配置: + +例 4。OAuth2 登录配置 + +Java + +``` +@EnableWebFluxSecurity +public class OAuth2LoginConfig { + + @Bean + public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { + http + .authorizeExchange(authorize -> authorize + .anyExchange().authenticated() + ) + .oauth2Login(withDefaults()); + + return http.build(); + } + + @Bean + public ReactiveClientRegistrationRepository clientRegistrationRepository() { + return new InMemoryReactiveClientRegistrationRepository(this.googleClientRegistration()); + } + + @Bean + public ReactiveOAuth2AuthorizedClientService authorizedClientService( + ReactiveClientRegistrationRepository clientRegistrationRepository) { + return new InMemoryReactiveOAuth2AuthorizedClientService(clientRegistrationRepository); + } + + @Bean + public ServerOAuth2AuthorizedClientRepository authorizedClientRepository( + ReactiveOAuth2AuthorizedClientService authorizedClientService) { + return new AuthenticatedPrincipalServerOAuth2AuthorizedClientRepository(authorizedClientService); + } + + private ClientRegistration googleClientRegistration() { + return CommonOAuth2Provider.GOOGLE.getBuilder("google") + .clientId("google-client-id") + .clientSecret("google-client-secret") + .build(); + } +} +``` + +Kotlin + +``` +@EnableWebFluxSecurity +class OAuth2LoginConfig { + + @Bean + fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + authorizeExchange { + authorize(anyExchange, authenticated) + } + oauth2Login { } + } + } + + @Bean + fun clientRegistrationRepository(): ReactiveClientRegistrationRepository { + return InMemoryReactiveClientRegistrationRepository(googleClientRegistration()) + } + + @Bean + fun authorizedClientService( + clientRegistrationRepository: ReactiveClientRegistrationRepository + ): ReactiveOAuth2AuthorizedClientService { + return InMemoryReactiveOAuth2AuthorizedClientService(clientRegistrationRepository) + } + + @Bean + fun authorizedClientRepository( + authorizedClientService: ReactiveOAuth2AuthorizedClientService + ): ServerOAuth2AuthorizedClientRepository { + return AuthenticatedPrincipalServerOAuth2AuthorizedClientRepository(authorizedClientService) + } + + private fun googleClientRegistration(): ClientRegistration { + return CommonOAuth2Provider.GOOGLE.getBuilder("google") + .clientId("google-client-id") + .clientSecret("google-client-secret") + .build() + } +} +``` + +[OAuth2 登录](index.html)[高级配置](advanced.html) + diff --git a/docs/spring-security/reactive-oauth2-login.md b/docs/spring-security/reactive-oauth2-login.md new file mode 100644 index 0000000000000000000000000000000000000000..7fbf97880c4fb99a7b262c1cf17468220b2b8b91 --- /dev/null +++ b/docs/spring-security/reactive-oauth2-login.md @@ -0,0 +1,14 @@ +# OAuth2.0 登录 + +OAuth2.0 登录功能提供了一个应用程序,该应用程序可以让用户使用他们在 OAuth2.0 提供商(例如 GitHub)或 OpenID Connect1.0 提供商(例如 Google)的现有帐户登录到该应用程序。OAuth2.0Login 实现了以下用例:“用 Google 登录”或“用 GitHub 登录”。 + +| |OAuth2.0 登录是通过使用**授权代码授予**实现的,如[OAuth2.0 授权框架](https://tools.ietf.org/html/rfc6749#section-4.1)和[OpenID Connect Core1.0](https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth)中所指定的。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +## 章节摘要 + +* [核心配置](core.html) +* [高级配置](advanced.html) + +[OAuth2](../index.html)[核心配置](core.html) + diff --git a/docs/spring-security/reactive-oauth2-resource-server-bearer-tokens.md b/docs/spring-security/reactive-oauth2-resource-server-bearer-tokens.md new file mode 100644 index 0000000000000000000000000000000000000000..b260e4dcc257d427780772c03871e889aec5e7b3 --- /dev/null +++ b/docs/spring-security/reactive-oauth2-resource-server-bearer-tokens.md @@ -0,0 +1,112 @@ +# OAuth2.0 资源服务器承载令牌 + +## 不记名令牌解析 + +默认情况下,Resource Server 在`Authorization`头中查找承载令牌。然而,这是可以定制的。 + +例如,你可能需要从自定义报头读取承载令牌。为了实现这一点,你可以将`ServerBearerTokenAuthenticationConverter`的一个实例连接到 DSL 中,正如你在下面的示例中所看到的那样: + +例 1。自定义承载令牌标头 + +爪哇 + +``` +ServerBearerTokenAuthenticationConverter converter = new ServerBearerTokenAuthenticationConverter(); +converter.setBearerTokenHeaderName(HttpHeaders.PROXY_AUTHORIZATION); +http + .oauth2ResourceServer(oauth2 -> oauth2 + .bearerTokenConverter(converter) + ); +``` + +Kotlin + +``` +val converter = ServerBearerTokenAuthenticationConverter() +converter.setBearerTokenHeaderName(HttpHeaders.PROXY_AUTHORIZATION) +return http { + oauth2ResourceServer { + bearerTokenConverter = converter + } +} +``` + +## 承载令牌传播 + +既然你已经拥有了一个无记名令牌,那么将其传递给下游服务可能会很方便。这对`[ServerBearerExchangeFilterFunction](https://docs.spring.io/spring-security/site/docs/5.6.2/api/org/springframework/security/oauth2/server/resource/web/reactive/function/client/ServerBearerExchangeFilterFunction.html)`来说非常简单,你可以在下面的示例中看到这一点: + +爪哇 + +``` +@Bean +public WebClient rest() { + return WebClient.builder() + .filter(new ServerBearerExchangeFilterFunction()) + .build(); +} +``` + +Kotlin + +``` +@Bean +fun rest(): WebClient { + return WebClient.builder() + .filter(ServerBearerExchangeFilterFunction()) + .build() +} +``` + +当上面的`WebClient`用于执行请求时, Spring Security 将查找当前的`Authentication`并提取任何`[AbstractOAuth2Token](https://docs.spring.io/spring-security/site/docs/5.6.2/api/org/springframework/security/oauth2/core/AbstractOAuth2Token.html)`凭据。然后,它将在`Authorization`头中传播该令牌。 + +例如: + +爪哇 + +``` +this.rest.get() + .uri("https://other-service.example.com/endpoint") + .retrieve() + .bodyToMono(String.class) +``` + +Kotlin + +``` +this.rest.get() + .uri("https://other-service.example.com/endpoint") + .retrieve() + .bodyToMono() +``` + +将调用`[https://other-service.example.com/endpoint](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) +``` + +Kotlin + +``` +rest.get() + .uri("https://other-service.example.com/endpoint") + .headers { it.setBearerAuth(overridingToken) } + .retrieve() + .bodyToMono() +``` + +在这种情况下,过滤器将向后退,只需将请求转发到 Web 筛选链的其余部分。 + +| |与[OAuth2.0 客户端过滤功能](https://docs.spring.io/spring-security/site/docs/current-SNAPSHOT/api/org/springframework/security/oauth2/client/web/reactive/function/client/ServerOAuth2AuthorizedClientExchangeFilterFunction.html)不同,如果令牌过期,此筛选函数不尝试更新令牌。
要获得此级别的支持,请使用 OAuth2.0 客户端筛选。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +[多租约](multitenancy.html)[保护免受剥削](../../exploits/index.html) + diff --git a/docs/spring-security/reactive-oauth2-resource-server-jwt.md b/docs/spring-security/reactive-oauth2-resource-server-jwt.md new file mode 100644 index 0000000000000000000000000000000000000000..be8c575f56da148abd854cfdb196053de31edce3 --- /dev/null +++ b/docs/spring-security/reactive-oauth2-resource-server-jwt.md @@ -0,0 +1,813 @@ +# OAuth2.0 资源服务器 JWT + +## JWT 的最小依赖项 + +大多数资源服务器支持被收集到`spring-security-oauth2-resource-server`中。然而,对 JWTS 的解码和验证的支持是`spring-security-oauth2-jose`,这意味着这两个都是必要的,以便拥有一个支持 JWT 编码的承载令牌的工作资源服务器。 + +## JWTS 的最小配置 + +当使用[Spring Boot](https://spring.io/projects/spring-boot)时,将应用程序配置为资源服务器包括两个基本步骤。首先,包括所需的依赖关系,其次,指示授权服务器的位置。 + +### 指定授权服务器 + +在 Spring 引导应用程序中,要指定使用哪个授权服务器,只需执行以下操作: + +``` +spring: + security: + oauth2: + resourceserver: + jwt: + issuer-uri: https://idp.example.com/issuer +``` + +其中`[https://idp.example.com/issuer](https://idp.example.com/issuer)`是授权服务器将发布的 JWT 令牌的`iss`声明中包含的值。Resource Server 将使用此属性进一步自我配置,发现授权服务器的公钥,并随后验证传入的 JWTS。 + +| |要使用`issuer-uri`属性,还必须证明`[https://idp.example.com/issuer/.well-known/openid-configuration](https://idp.example.com/issuer/.well-known/openid-configuration)`、`[https://idp.example.com/.well-known/openid-configuration/issuer](https://idp.example.com/.well-known/openid-configuration/issuer)`或`[https://idp.example.com/.well-known/oauth-authorization-server/issuer](https://idp.example.com/.well-known/oauth-authorization-server/issuer)`中的一个是授权服务器所支持的端点。
此端点被称为[提供者配置](https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig)端点或[授权服务器元数据](https://tools.ietf.org/html/rfc8414#section-3)端点。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +就这样! + +### 创业期望 + +当使用此属性和这些依赖项时,Resource Server 将自动配置自身以验证 JWT 编码的承载令牌。 + +它通过一个确定性的启动过程来实现这一点: + +1. 点击提供者配置或授权服务器元数据端点,处理`jwks_url`属性的响应 + +2. 将验证策略配置为查询`jwks_url`中的有效公钥 + +3. 将验证策略配置为针对`[https://idp.example.com](https://idp.example.com)`的每个 JWTS`iss`索赔进行验证。 + +此过程的结果是,为了使资源服务器成功启动,授权服务器必须启动并接收请求。 + +| |如果在资源服务器查询时,授权服务器处于关闭状态(给定适当的超时),那么启动将失败。| +|---|-------------------------------------------------------------------------------------------------------------------------| + +### 运行时期望 + +一旦启动应用程序,Resource Server 将尝试处理任何包含`Authorization: Bearer`报头的请求: + +``` +GET / HTTP/1.1 +Authorization: Bearer some-token-value # Resource Server will process this +``` + +只要表明了该方案,资源服务器就会尝试根据承载令牌规范来处理请求。 + +给定一个格式良好的 JWT,资源服务器将: + +1. 根据在启动过程中从`jwks_url`端点获得并与 JWTS 头匹配的公钥验证其签名 + +2. 验证 JWTS`exp`和`nbf`时间戳和 JWTS`iss`声明,并 + +3. 将每个作用域映射到一个前缀`SCOPE_`的权限。 + +| |由于授权服务器提供了可用的新密钥, Spring 安全性将自动旋转用于验证 JWT 令牌的密钥。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------| + +默认情况下,生成的`Authentication#getPrincipal`是 Spring security`Jwt`对象,并且`Authentication#getName`映射到 JWT 的`sub`属性(如果存在)。 + +从这里开始,考虑跳到: + +[如何在不将资源服务器启动与授权服务器的可用性绑定的情况下进行配置](#webflux-oauth2resourceserver-jwt-jwkseturi) + +[How to Configure without Spring Boot](#webflux-oauth2resourceserver-jwt-sansboot) + +### 直接指定授权服务器 JWK 设置的 URI + +如果授权服务器不支持任何配置端点,或者如果资源服务器必须能够独立于授权服务器启动,那么也可以提供`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 集 URI 不是标准化的,但通常可以在授权服务器的文档中找到。| +|---|-----------------------------------------------------------------------------------------------------------| + +因此,资源服务器不会在启动时对授权服务器进行 ping。我们仍然指定`issuer-uri`,以便 Resource Server 仍然在传入的 JWTS 上验证`iss`声明。 + +| |也可以在[DSL](#webflux-oauth2resourceserver-jwt-jwkseturi-dsl)上直接提供此属性。| +|---|----------------------------------------------------------------------------------------------------------| + +### 覆盖或替换 Boot Auto 配置 + +有两个`@Bean`s, Spring boot 代表资源服务器生成。 + +第一个是将应用程序配置为资源服务器的`SecurityWebFilterChain`。当包含`spring-security-oauth2-jose`时,这个`SecurityWebFilterChain`看起来是这样的: + +例 1。资源服务器 SecurityWebFilterchain + +爪哇 + +``` +@Bean +SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { + http + .authorizeExchange(exchanges -> exchanges + .anyExchange().authenticated() + ) + .oauth2ResourceServer(OAuth2ResourceServerSpec::jwt) + return http.build(); +} +``` + +Kotlin + +``` +@Bean +fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + authorizeExchange { + authorize(anyExchange, authenticated) + } + oauth2ResourceServer { + jwt { } + } + } +} +``` + +如果应用程序不公开`SecurityWebFilterChain` Bean,那么 Spring 引导将公开上面的默认引导。 + +替换它就像在应用程序中公开 Bean 一样简单: + +例 2。替换 SecurityWebFilterchain + +爪哇 + +``` +@Bean +SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { + http + .authorizeExchange(exchanges -> exchanges + .pathMatchers("/message/**").hasAuthority("SCOPE_message:read") + .anyExchange().authenticated() + ) + .oauth2ResourceServer(oauth2 -> oauth2 + .jwt(withDefaults()) + ); + return http.build(); +} +``` + +Kotlin + +``` +@Bean +fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + authorizeExchange { + authorize("/message/**", hasAuthority("SCOPE_message:read")) + authorize(anyExchange, authenticated) + } + oauth2ResourceServer { + jwt { } + } + } +} +``` + +对于任何以`/messages/`开头的 URL,上述条件要求`message:read`的范围。 + +`oauth2ResourceServer`DSL 上的方法也将覆盖或替换自动配置。 + +例如,第二个`@Bean` Spring 引导创建了一个`ReactiveJwtDecoder`,它将`String`令牌解码为`Jwt`的验证实例: + +例 3。ReactiveJWTdecoder + +爪哇 + +``` +@Bean +public ReactiveJwtDecoder jwtDecoder() { + return ReactiveJwtDecoders.fromIssuerLocation(issuerUri); +} +``` + +Kotlin + +``` +@Bean +fun jwtDecoder(): ReactiveJwtDecoder { + return ReactiveJwtDecoders.fromIssuerLocation(issuerUri) +} +``` + +| |调用`[ReactiveJwtDecoders#fromIssuerLocation](https://docs.spring.io/spring-security/site/docs/5.6.2/api/org/springframework/security/oauth2/jwt/ReactiveJwtDecoders.html#fromIssuerLocation-java.lang.String-)`是调用提供者配置或授权服务器元数据端点的目的,以便派生 JWK 集 URI。
如果应用程序不公开`ReactiveJwtDecoder` Bean,那么 Spring 引导将公开上述默认的。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +并且其配置可以使用`jwkSetUri()`重写或使用`decoder()`替换。 + +#### ` + +可以配置授权服务器的 JWK 集 URI[作为配置属性](#webflux-oauth2resourceserver-jwt-jwkseturi),也可以在 DSL 中提供它: + +爪哇 + +``` +@Bean +SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { + http + .authorizeExchange(exchanges -> exchanges + .anyExchange().authenticated() + ) + .oauth2ResourceServer(oauth2 -> oauth2 + .jwt(jwt -> jwt + .jwkSetUri("https://idp.example.com/.well-known/jwks.json") + ) + ); + return http.build(); +} +``` + +Kotlin + +``` +@Bean +fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + authorizeExchange { + authorize(anyExchange, authenticated) + } + oauth2ResourceServer { + jwt { + jwkSetUri = "https://idp.example.com/.well-known/jwks.json" + } + } + } +} +``` + +使用`jwkSetUri()`优先于任何配置属性。 + +#### ` + +比`jwkSetUri()`更强大的是`decoder()`,它将完全取代`JwtDecoder`的任何引导自动配置: + +爪哇 + +``` +@Bean +SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { + http + .authorizeExchange(exchanges -> exchanges + .anyExchange().authenticated() + ) + .oauth2ResourceServer(oauth2 -> oauth2 + .jwt(jwt -> jwt + .decoder(myCustomDecoder()) + ) + ); + return http.build(); +} +``` + +Kotlin + +``` +@Bean +fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + authorizeExchange { + authorize(anyExchange, authenticated) + } + oauth2ResourceServer { + jwt { + jwtDecoder = myCustomDecoder() + } + } + } +} +``` + +当需要更深的配置时,比如[validation](#webflux-oauth2resourceserver-jwt-validation),这是很方便的。 + +#### 曝光`ReactiveJwtDecoder``@Bean` + +或者,暴露`ReactiveJwtDecoder``@Bean`具有与`decoder()`相同的效果: + +爪哇 + +``` +@Bean +public ReactiveJwtDecoder jwtDecoder() { + return NimbusReactiveJwtDecoder.withJwkSetUri(jwkSetUri).build(); +} +``` + +Kotlin + +``` +@Bean +fun jwtDecoder(): ReactiveJwtDecoder { + return ReactiveJwtDecoders.fromIssuerLocation(issuerUri) +} +``` + +## 配置可信算法 + +默认情况下,`NimbusReactiveJwtDecoder`以及资源服务器将仅使用`RS256`信任和验证令牌。 + +你可以通过[Spring Boot](#webflux-oauth2resourceserver-jwt-boot-algorithm)或[NimbusJWTDecoder Builder](#webflux-oauth2resourceserver-jwt-decoder-builder)对此进行自定义。 + +### 通过 Spring 引导 + +设置算法的最简单方法是作为一个属性: + +``` +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.withJwkSetUri(this.jwkSetUri) + .jwsAlgorithm(RS512).build(); +} +``` + +Kotlin + +``` +@Bean +fun jwtDecoder(): ReactiveJwtDecoder { + return NimbusReactiveJwtDecoder.withJwkSetUri(this.jwkSetUri) + .jwsAlgorithm(RS512).build() +} +``` + +多次调用`jwsAlgorithm`将配置`NimbusReactiveJwtDecoder`来信任多个算法,如下所示: + +爪哇 + +``` +@Bean +ReactiveJwtDecoder jwtDecoder() { + return NimbusReactiveJwtDecoder.withJwkSetUri(this.jwkSetUri) + .jwsAlgorithm(RS512).jwsAlgorithm(ES512).build(); +} +``` + +Kotlin + +``` +@Bean +fun jwtDecoder(): ReactiveJwtDecoder { + return NimbusReactiveJwtDecoder.withJwkSetUri(this.jwkSetUri) + .jwsAlgorithm(RS512).jwsAlgorithm(ES512).build() +} +``` + +或者,你可以调用`jwsAlgorithms`: + +爪哇 + +``` +@Bean +ReactiveJwtDecoder jwtDecoder() { + return NimbusReactiveJwtDecoder.withJwkSetUri(this.jwkSetUri) + .jwsAlgorithms(algorithms -> { + algorithms.add(RS512); + algorithms.add(ES512); + }).build(); +} +``` + +Kotlin + +``` +@Bean +fun jwtDecoder(): ReactiveJwtDecoder { + return NimbusReactiveJwtDecoder.withJwkSetUri(this.jwkSetUri) + .jwsAlgorithms { + it.add(RS512) + it.add(ES512) + } + .build() +} +``` + +### 信任单一的非对称密钥 + +比支持具有 JWK 设置端点的资源服务器更简单的方法是对 RSA 公钥进行硬编码。公钥可以通过[Spring Boot](#webflux-oauth2resourceserver-jwt-decoder-public-key-boot)或[使用构建器](#webflux-oauth2resourceserver-jwt-decoder-public-key-builder)提供。 + +#### 通过 Spring 引导 + +通过 Spring 引导指定一个键非常简单。密钥的位置可以这样指定: + +``` +spring: + security: + oauth2: + resourceserver: + jwt: + public-key-location: classpath:my-key.pub +``` + +或者,为了允许更复杂的查找,你可以对`RsaKeyConversionServicePostProcessor`进行后处理: + +例 4。BeanFactoryPostprocessor + +爪哇 + +``` +@Bean +BeanFactoryPostProcessor conversionServiceCustomizer() { + return beanFactory -> + beanFactory.getBean(RsaKeyConversionServicePostProcessor.class) + .setResourceLoader(new CustomResourceLoader()); +} +``` + +Kotlin + +``` +@Bean +fun conversionServiceCustomizer(): BeanFactoryPostProcessor { + return BeanFactoryPostProcessor { beanFactory: ConfigurableListableBeanFactory -> + beanFactory.getBean() + .setResourceLoader(CustomResourceLoader()) + } +} +``` + +指定你的密钥的位置: + +``` +key.location: hfds://my-key.pub +``` + +然后自动连接该值: + +爪哇 + +``` +@Value("${key.location}") +RSAPublicKey key; +``` + +Kotlin + +``` +@Value("\${key.location}") +val key: RSAPublicKey? = null +``` + +#### 使用构建器 + +要直接连接`RSAPublicKey`,只需使用适当的`NimbusReactiveJwtDecoder`构建器,如下所示: + +爪哇 + +``` +@Bean +public ReactiveJwtDecoder jwtDecoder() { + return NimbusReactiveJwtDecoder.withPublicKey(this.key).build(); +} +``` + +Kotlin + +``` +@Bean +fun jwtDecoder(): ReactiveJwtDecoder { + return NimbusReactiveJwtDecoder.withPublicKey(key).build() +} +``` + +### 信任单一的对称密钥 + +使用单一的对称密钥也很简单。你可以简单地在`SecretKey`中加载,并使用适当的`NimbusReactiveJwtDecoder`构建器,如下所示: + +爪哇 + +``` +@Bean +public ReactiveJwtDecoder jwtDecoder() { + return NimbusReactiveJwtDecoder.withSecretKey(this.key).build(); +} +``` + +Kotlin + +``` +@Bean +fun jwtDecoder(): ReactiveJwtDecoder { + return NimbusReactiveJwtDecoder.withSecretKey(this.key).build() +} +``` + +### 配置授权 + +从 OAuth2.0 授权服务器发出的 JWT 通常具有`scope`或`scp`属性,指示已授予的范围(或权限),例如: + +`{ …​, "scope" : "messages contacts"}` + +在这种情况下,Resource Server 将尝试强制将这些作用域放入一个已授予权限的列表中,并在每个作用域前加上字符串“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(); +} +``` + +Kotlin + +``` +@Bean +fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + authorizeExchange { + authorize("/contacts/**", hasAuthority("SCOPE_contacts")) + authorize("/messages/**", hasAuthority("SCOPE_messages")) + authorize(anyExchange, authenticated) + } + oauth2ResourceServer { + jwt { } + } + } +} +``` + +或类似于方法安全性: + +爪哇 + +``` +@PreAuthorize("hasAuthority('SCOPE_messages')") +public Flux getMessages(...) {} +``` + +Kotlin + +``` +@PreAuthorize("hasAuthority('SCOPE_messages')") +fun getMessages(): Flux { } +``` + +#### 手动提取权限 + +然而,在许多情况下,这种默认设置是不够的。例如,一些授权服务器不使用`scope`属性,而是具有自己的自定义属性。或者,在其他时候,资源服务器可能需要将属性或属性的组合调整为内在化的权限。 + +为此,DSL 公开`jwtAuthenticationConverter()`: + +爪哇 + +``` +@Bean +SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { + http + .authorizeExchange(exchanges -> exchanges + .anyExchange().authenticated() + ) + .oauth2ResourceServer(oauth2 -> oauth2 + .jwt(jwt -> jwt + .jwtAuthenticationConverter(grantedAuthoritiesExtractor()) + ) + ); + return http.build(); +} + +Converter> grantedAuthoritiesExtractor() { + JwtAuthenticationConverter jwtAuthenticationConverter = + new JwtAuthenticationConverter(); + jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter + (new GrantedAuthoritiesExtractor()); + return new ReactiveJwtAuthenticationConverterAdapter(jwtAuthenticationConverter); +} +``` + +Kotlin + +``` +@Bean +fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + authorizeExchange { + authorize(anyExchange, authenticated) + } + oauth2ResourceServer { + jwt { + jwtAuthenticationConverter = grantedAuthoritiesExtractor() + } + } + } +} + +fun grantedAuthoritiesExtractor(): Converter> { + val jwtAuthenticationConverter = JwtAuthenticationConverter() + jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(GrantedAuthoritiesExtractor()) + return ReactiveJwtAuthenticationConverterAdapter(jwtAuthenticationConverter) +} +``` + +它负责将`Jwt`转换为`Authentication`。作为其配置的一部分,我们可以提供一个从`Jwt`到`Collection`的辅助转换器。 + +最后的转换器可能类似于下面的`GrantedAuthoritiesExtractor`: + +爪哇 + +``` +static class GrantedAuthoritiesExtractor + implements Converter> { + + public Collection convert(Jwt jwt) { + Collection authorities = (Collection) + jwt.getClaims().getOrDefault("mycustomclaim", Collections.emptyList()); + + return authorities.stream() + .map(Object::toString) + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toList()); + } +} +``` + +Kotlin + +``` +internal class GrantedAuthoritiesExtractor : Converter> { + override fun convert(jwt: Jwt): Collection { + val authorities: List = jwt.claims + .getOrDefault("mycustomclaim", emptyList()) as List + return authorities + .map { it.toString() } + .map { SimpleGrantedAuthority(it) } + } +} +``` + +为了具有更大的灵活性,DSL 支持用实现`Converter>`的任何类完全替换转换器: + +爪哇 + +``` +static class CustomAuthenticationConverter implements Converter> { + public AbstractAuthenticationToken convert(Jwt jwt) { + return Mono.just(jwt).map(this::doConversion); + } +} +``` + +Kotlin + +``` +internal class CustomAuthenticationConverter : Converter> { + override fun convert(jwt: Jwt): Mono { + return Mono.just(jwt).map(this::doConversion) + } +} +``` + +### 配置验证 + +使用[minimal Spring Boot configuration](#webflux-oauth2resourceserver-jwt-minimalconfiguration)(表示授权服务器的发行者 URI),资源服务器将默认验证`iss`声明以及`exp`和`nbf`时间戳声明。 + +在需要定制验证的情况下,Resource Server 附带两个标准验证器,并且还接受定制的`OAuth2TokenValidator`实例。 + +#### 自定义时间戳验证 + +JWT 通常有一个有效窗口,窗口的开始在`nbf`声明中指示,结束在`exp`声明中指示。 + +然而,每个服务器都可能经历时钟漂移,这可能导致令牌在一台服务器上出现过期,而不是在另一台服务器上出现。随着分布式系统中协作服务器数量的增加,这可能会导致一些实现令人心烦。 + +Resource Server 使用`JwtTimestampValidator`来验证令牌的有效性窗口,并且可以将其配置为`clockSkew`以缓解上述问题: + +爪哇 + +``` +@Bean +ReactiveJwtDecoder jwtDecoder() { + NimbusReactiveJwtDecoder jwtDecoder = (NimbusReactiveJwtDecoder) + ReactiveJwtDecoders.fromIssuerLocation(issuerUri); + + OAuth2TokenValidator withClockSkew = new DelegatingOAuth2TokenValidator<>( + new JwtTimestampValidator(Duration.ofSeconds(60)), + new IssuerValidator(issuerUri)); + + jwtDecoder.setJwtValidator(withClockSkew); + + return jwtDecoder; +} +``` + +Kotlin + +``` +@Bean +fun jwtDecoder(): ReactiveJwtDecoder { + val jwtDecoder = ReactiveJwtDecoders.fromIssuerLocation(issuerUri) as NimbusReactiveJwtDecoder + val withClockSkew: OAuth2TokenValidator = DelegatingOAuth2TokenValidator( + JwtTimestampValidator(Duration.ofSeconds(60)), + JwtIssuerValidator(issuerUri)) + jwtDecoder.setJwtValidator(withClockSkew) + return jwtDecoder +} +``` + +| |默认情况下,Resource Server 配置的时钟偏差为 60 秒。| +|---|------------------------------------------------------------------| + +#### 配置自定义验证器 + +使用`OAuth2TokenValidator`API 添加`aud`声明的检查很简单: + +爪哇 + +``` +public class AudienceValidator implements OAuth2TokenValidator { + 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); + } + } +} +``` + +Kotlin + +``` +class AudienceValidator : OAuth2TokenValidator { + var error: OAuth2Error = OAuth2Error("invalid_token", "The required audience is missing", null) + override fun validate(jwt: Jwt): OAuth2TokenValidatorResult { + return if (jwt.audience.contains("messaging")) { + OAuth2TokenValidatorResult.success() + } else { + OAuth2TokenValidatorResult.failure(error) + } + } +} +``` + +然后,要添加到资源服务器中,需要指定`ReactiveJwtDecoder`实例: + +爪哇 + +``` +@Bean +ReactiveJwtDecoder jwtDecoder() { + NimbusReactiveJwtDecoder jwtDecoder = (NimbusReactiveJwtDecoder) + ReactiveJwtDecoders.fromIssuerLocation(issuerUri); + + OAuth2TokenValidator audienceValidator = new AudienceValidator(); + OAuth2TokenValidator withIssuer = JwtValidators.createDefaultWithIssuer(issuerUri); + OAuth2TokenValidator withAudience = new DelegatingOAuth2TokenValidator<>(withIssuer, audienceValidator); + + jwtDecoder.setJwtValidator(withAudience); + + return jwtDecoder; +} +``` + +Kotlin + +``` +@Bean +fun jwtDecoder(): ReactiveJwtDecoder { + val jwtDecoder = ReactiveJwtDecoders.fromIssuerLocation(issuerUri) as NimbusReactiveJwtDecoder + val audienceValidator: OAuth2TokenValidator = AudienceValidator() + val withIssuer: OAuth2TokenValidator = JwtValidators.createDefaultWithIssuer(issuerUri) + val withAudience: OAuth2TokenValidator = DelegatingOAuth2TokenValidator(withIssuer, audienceValidator) + jwtDecoder.setJwtValidator(withAudience) + return jwtDecoder +} +``` + +[OAuth2 资源服务器](index.html)[不透明令牌](opaque-token.html) + diff --git a/docs/spring-security/reactive-oauth2-resource-server-multitenancy.md b/docs/spring-security/reactive-oauth2-resource-server-multitenancy.md new file mode 100644 index 0000000000000000000000000000000000000000..5627d54d4717950974730135249ef852d4f4e4fb --- /dev/null +++ b/docs/spring-security/reactive-oauth2-resource-server-multitenancy.md @@ -0,0 +1,111 @@ +# OAuth2.0 资源服务器多租户 + +## 多重租赁 + +当存在多个用于验证由某个租户标识符控制的承载令牌的策略时,资源服务器被认为是多租户的。 + +例如,你的资源服务器可能接受来自两个不同授权服务器的承载令牌。或者,你的授权服务器可能表示多个发行者。 + +在每种情况下,都需要做两件事,以及与选择如何做这些事情相关的权衡: + +1. 解决租户 + +2. 宣传租户 + +### 通过索赔来解决承租人的问题 + +区分租户的一种方法是通过签发人索赔。由于发行者声明伴随着已签名的 JWTS,因此可以使用`JwtIssuerReactiveAuthenticationManagerResolver`来完成此操作,如下所示: + +爪哇 + +``` +JwtIssuerReactiveAuthenticationManagerResolver authenticationManagerResolver = new JwtIssuerReactiveAuthenticationManagerResolver + ("https://idp.example.org/issuerOne", "https://idp.example.org/issuerTwo"); + +http + .authorizeExchange(exchanges -> exchanges + .anyExchange().authenticated() + ) + .oauth2ResourceServer(oauth2 -> oauth2 + .authenticationManagerResolver(authenticationManagerResolver) + ); +``` + +Kotlin + +``` +val customAuthenticationManagerResolver = JwtIssuerReactiveAuthenticationManagerResolver("https://idp.example.org/issuerOne", "https://idp.example.org/issuerTwo") + +return http { + authorizeExchange { + authorize(anyExchange, authenticated) + } + oauth2ResourceServer { + authenticationManagerResolver = customAuthenticationManagerResolver + } +} +``` + +这很好,因为发行者端点是懒洋洋地加载的。实际上,对应的`JwtReactiveAuthenticationManager`只有在发送了与对应的发行者的第一个请求时才被实例化。这允许应用程序启动,该应用程序独立于已启动和可用的那些授权服务器。 + +#### 动态租户 + +当然,你可能不希望每次添加新租户时都重新启动应用程序。在这种情况下,你可以使用`JwtIssuerReactiveAuthenticationManagerResolver`实例的存储库配置`ReactiveAuthenticationManager`实例,你可以在运行时对其进行编辑,如下所示: + +爪哇 + +``` +private Mono addManager( + Map authenticationManagers, String issuer) { + + return Mono.fromCallable(() -> ReactiveJwtDecoders.fromIssuerLocation(issuer)) + .subscribeOn(Schedulers.boundedElastic()) + .map(JwtReactiveAuthenticationManager::new) + .doOnNext(authenticationManager -> authenticationManagers.put(issuer, authenticationManager)); +} + +// ... + +JwtIssuerReactiveAuthenticationManagerResolver authenticationManagerResolver = + new JwtIssuerReactiveAuthenticationManagerResolver(authenticationManagers::get); + +http + .authorizeExchange(exchanges -> exchanges + .anyExchange().authenticated() + ) + .oauth2ResourceServer(oauth2 -> oauth2 + .authenticationManagerResolver(authenticationManagerResolver) + ); +``` + +Kotlin + +``` +private fun addManager( + authenticationManagers: MutableMap, issuer: String): Mono { + return Mono.fromCallable { ReactiveJwtDecoders.fromIssuerLocation(issuer) } + .subscribeOn(Schedulers.boundedElastic()) + .map { jwtDecoder: ReactiveJwtDecoder -> JwtReactiveAuthenticationManager(jwtDecoder) } + .doOnNext { authenticationManager: JwtReactiveAuthenticationManager -> authenticationManagers[issuer] = authenticationManager } +} + +// ... + +var customAuthenticationManagerResolver = JwtIssuerReactiveAuthenticationManagerResolver(authenticationManagers::get) +return http { + authorizeExchange { + authorize(anyExchange, authenticated) + } + oauth2ResourceServer { + authenticationManagerResolver = customAuthenticationManagerResolver + } +} +``` + +在这种情况下,构造`JwtIssuerReactiveAuthenticationManagerResolver`时,要使用一种策略来获得给定发行人的`ReactiveAuthenticationManager`。这种方法允许我们在运行时从存储库中添加和删除元素(在代码片段中显示为`Map`)。 + +| |简单地获取任何发行者并从中构造`ReactiveAuthenticationManager`将是不安全的。
发行者应该是代码可以从可信来源(如允许的发行者列表)验证的发行者。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +[不透明令牌](opaque-token.html)[不记名代币](bearer-tokens.html) + diff --git a/docs/spring-security/reactive-oauth2-resource-server-opaque-token.md b/docs/spring-security/reactive-oauth2-resource-server-opaque-token.md new file mode 100644 index 0000000000000000000000000000000000000000..0189d5e44462a57a2bed06e632cad55ff627eb16 --- /dev/null +++ b/docs/spring-security/reactive-oauth2-resource-server-opaque-token.md @@ -0,0 +1,722 @@ +# OAuth2.0 资源服务器不透明令牌 + +## 用于自省的最小依赖项 + +如[JWT 的最小依赖项](../../../servlet/oauth2/resource-server/jwt.html#oauth2resourceserver-jwt-minimaldependencies)中所描述的,大多数资源服务器支持都是在`spring-security-oauth2-resource-server`中收集的。但是,除非提供自定义[`ReactiveOpaqueTokenIntrospector`](#WebFlux-OAuth2Resourceserver-Opaque-Introspector- Bean),否则资源服务器将退回到 ReactiveOpaQuetokenIntrospector。这意味着`spring-security-oauth2-resource-server`和`oauth2-oidc-sdk`都是必需的,以便拥有支持不透明承载令牌的工作最小资源服务器。请参阅`spring-security-oauth2-resource-server`以确定`oauth2-oidc-sdk`的正确版本。 + +## 用于内省的最小配置 + +通常,可以通过由授权服务器托管的[OAuth2.0 内省终点](https://tools.ietf.org/html/rfc7662)来验证不透明令牌。当要求撤销时,这可能很方便。 + +当使用[Spring Boot](https://spring.io/projects/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](https://idp.example.com/introspect)`是由授权服务器托管的内省端点,`client-id`和`client-secret`是达到该端点所需的凭据。 + +Resource Server 将使用这些属性进一步自我配置,并随后验证传入的 JWTS。 + +| |在使用内省时,授权服务器的单词是 law。
如果授权服务器响应令牌是有效的,那么它是有效的。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------| + +就这样! + +### 创业期望 + +当使用此属性和这些依赖项时,Resource Server 将自动配置自身以验证不透明承载令牌。 + +这个启动过程比 JWTS 简单得多,因为不需要发现端点,也不需要添加额外的验证规则。 + +### 运行时期望 + +一旦启动应用程序,Resource Server 将尝试处理任何包含`Authorization: Bearer`报头的请求: + +``` +GET / HTTP/1.1 +Authorization: Bearer some-token-value # Resource Server will process this +``` + +只要表明了该方案,资源服务器就会尝试根据承载令牌规范来处理请求。 + +给定一个不透明的令牌,资源服务器将 + +1. 使用提供的凭据和令牌查询提供的内省端点 + +2. 检查`{ 'active' : true }`属性的响应 + +3. 将每个作用域映射到一个前缀`SCOPE_`的权限 + +默认情况下,生成的`Authentication#getPrincipal`是 Spring security`[OAuth2AuthenticatedPrincipal](https://docs.spring.io/spring-security/site/docs/5.6.2/api/org/springframework/security/oauth2/core/OAuth2AuthenticatedPrincipal.html)`对象,并且`Authentication#getName`映射到令牌的`sub`属性(如果存在)。 + +从这里,你可能想跳转到: + +* [身份验证后查找属性](#webflux-oauth2resourceserver-opaque-attributes) + +* [手动提取权限](#webflux-oauth2resourceserver-opaque-authorization-extraction) + +* [使用 JWTS 进行内省](#webflux-oauth2resourceserver-opaque-jwt-introspector) + +## 身份验证后查找属性 + +一旦对令牌进行了身份验证,就会在`SecurityContext`中设置`BearerTokenAuthentication`的实例。 + +这意味着当在配置中使用`@EnableWebFlux`时,它可以在`@Controller`方法中使用: + +爪哇 + +``` +@GetMapping("/foo") +public Mono foo(BearerTokenAuthentication authentication) { + return Mono.just(authentication.getTokenAttributes().get("sub") + " is the subject"); +} +``` + +Kotlin + +``` +@GetMapping("/foo") +fun foo(authentication: BearerTokenAuthentication): Mono { + return Mono.just(authentication.tokenAttributes["sub"].toString() + " is the subject") +} +``` + +由于`BearerTokenAuthentication`持有`OAuth2AuthenticatedPrincipal`,这也意味着控制器方法也可以使用它: + +爪哇 + +``` +@GetMapping("/foo") +public Mono foo(@AuthenticationPrincipal OAuth2AuthenticatedPrincipal principal) { + return Mono.just(principal.getAttribute("sub") + " is the subject"); +} +``` + +Kotlin + +``` +@GetMapping("/foo") +fun foo(@AuthenticationPrincipal principal: OAuth2AuthenticatedPrincipal): Mono { + return Mono.just(principal.getAttribute("sub").toString() + " is the subject") +} +``` + +### 通过 SPEL 查找属性 + +当然,这也意味着可以通过 SPEL 访问属性。 + +例如,如果使用`@EnableReactiveMethodSecurity`使你可以使用`@PreAuthorize`注释,则可以这样做: + +爪哇 + +``` +@PreAuthorize("principal?.attributes['sub'] = 'foo'") +public Mono forFoosEyesOnly() { + return Mono.just("foo"); +} +``` + +Kotlin + +``` +@PreAuthorize("principal.attributes['sub'] = 'foo'") +fun forFoosEyesOnly(): Mono { + return Mono.just("foo") +} +``` + +## 覆盖或替换 Boot Auto 配置 + +有两个`@Bean`s, Spring boot 代表资源服务器生成。 + +第一个是将应用程序配置为资源服务器的`SecurityWebFilterChain`。当使用不透明令牌时,这个`SecurityWebFilterChain`看起来像: + +爪哇 + +``` +@Bean +SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { + http + .authorizeExchange(exchanges -> exchanges + .anyExchange().authenticated() + ) + .oauth2ResourceServer(ServerHttpSecurity.OAuth2ResourceServerSpec::opaqueToken) + return http.build(); +} +``` + +Kotlin + +``` +@Bean +fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + authorizeExchange { + authorize(anyExchange, authenticated) + } + oauth2ResourceServer { + opaqueToken { } + } + } +} +``` + +如果应用程序不公开`SecurityWebFilterChain` Bean,那么 Spring 引导将公开上面的默认引导。 + +替换它就像在应用程序中公开 Bean 一样简单: + +例 1。替换 SecurityWebFilterchain + +爪哇 + +``` +@EnableWebFluxSecurity +public class MyCustomSecurityConfiguration { + @Bean + SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { + http + .authorizeExchange(exchanges -> exchanges + .pathMatchers("/messages/**").hasAuthority("SCOPE_message:read") + .anyExchange().authenticated() + ) + .oauth2ResourceServer(oauth2 -> oauth2 + .opaqueToken(opaqueToken -> opaqueToken + .introspector(myIntrospector()) + ) + ); + return http.build(); + } +} +``` + +Kotlin + +``` +@Bean +fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + authorizeExchange { + authorize("/messages/**", hasAuthority("SCOPE_message:read")) + authorize(anyExchange, authenticated) + } + oauth2ResourceServer { + opaqueToken { + introspector = myIntrospector() + } + } + } +} +``` + +对于任何以`/messages/`开头的 URL,上述条件要求`message:read`的范围。 + +`oauth2ResourceServer`DSL 上的方法也将覆盖或替换自动配置。 + +例如,第二个`@Bean` Spring 启动创建了一个`ReactiveOpaqueTokenIntrospector`,它将`String`令牌解码为`OAuth2AuthenticatedPrincipal`的验证实例: + +爪哇 + +``` +@Bean +public ReactiveOpaqueTokenIntrospector introspector() { + return new NimbusReactiveOpaqueTokenIntrospector(introspectionUri, clientId, clientSecret); +} +``` + +Kotlin + +``` +@Bean +fun introspector(): ReactiveOpaqueTokenIntrospector { + return NimbusReactiveOpaqueTokenIntrospector(introspectionUri, clientId, clientSecret) +} +``` + +如果应用程序不公开`ReactiveOpaqueTokenIntrospector` Bean,那么 Spring 引导将公开上面的默认引导。 + +并且它的配置可以使用`introspectionUri()`和`introspectionClientCredentials()`进行重写,或者使用`introspector()`进行替换。 + +### ` + +可以配置授权服务器的内省 URI[作为配置属性](#webflux-oauth2resourceserver-opaque-introspectionuri),也可以在 DSL 中提供它: + +爪哇 + +``` +@EnableWebFluxSecurity +public class DirectlyConfiguredIntrospectionUri { + @Bean + SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { + http + .authorizeExchange(exchanges -> exchanges + .anyExchange().authenticated() + ) + .oauth2ResourceServer(oauth2 -> oauth2 + .opaqueToken(opaqueToken -> opaqueToken + .introspectionUri("https://idp.example.com/introspect") + .introspectionClientCredentials("client", "secret") + ) + ); + return http.build(); + } +} +``` + +Kotlin + +``` +@Bean +fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + authorizeExchange { + authorize(anyExchange, authenticated) + } + oauth2ResourceServer { + opaqueToken { + introspectionUri = "https://idp.example.com/introspect" + introspectionClientCredentials("client", "secret") + } + } + } +} +``` + +使用`introspectionUri()`优先于任何配置属性。 + +### ` + +比`introspectionUri()`更强大的是`introspector()`,它将完全取代`ReactiveOpaqueTokenIntrospector`的任何引导自动配置: + +爪哇 + +``` +@EnableWebFluxSecurity +public class DirectlyConfiguredIntrospector { + @Bean + SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { + http + .authorizeExchange(exchanges -> exchanges + .anyExchange().authenticated() + ) + .oauth2ResourceServer(oauth2 -> oauth2 + .opaqueToken(opaqueToken -> opaqueToken + .introspector(myCustomIntrospector()) + ) + ); + return http.build(); + } +} +``` + +Kotlin + +``` +@Bean +fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + authorizeExchange { + authorize(anyExchange, authenticated) + } + oauth2ResourceServer { + opaqueToken { + introspector = myCustomIntrospector() + } + } + } +} +``` + +当需要更深的配置时,比如[权限映射](#webflux-oauth2resourceserver-opaque-authorization-extraction)或[JWT 撤销](#webflux-oauth2resourceserver-opaque-jwt-introspector),这是很方便的。 + +### 曝光`ReactiveOpaqueTokenIntrospector``@Bean` + +或者,暴露`ReactiveOpaqueTokenIntrospector``@Bean`具有与`introspector()`相同的效果: + +爪哇 + +``` +@Bean +public ReactiveOpaqueTokenIntrospector introspector() { + return new NimbusReactiveOpaqueTokenIntrospector(introspectionUri, clientId, clientSecret); +} +``` + +Kotlin + +``` +@Bean +fun introspector(): ReactiveOpaqueTokenIntrospector { + return NimbusReactiveOpaqueTokenIntrospector(introspectionUri, clientId, clientSecret) +} +``` + +## 配置授权 + +OAuth2.0 内省端点通常会返回一个`scope`属性,指示它被授予的作用域(或权限),例如: + +`{ …​, "scope" : "messages contacts"}` + +在这种情况下,Resource Server 将尝试强制将这些作用域放入一个已授予权限的列表中,并在每个作用域前加上字符串“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(); + } +} +``` + +Kotlin + +``` +@Bean +fun springSecurityFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http { + authorizeExchange { + authorize("/contacts/**", hasAuthority("SCOPE_contacts")) + authorize("/messages/**", hasAuthority("SCOPE_messages")) + authorize(anyExchange, authenticated) + } + oauth2ResourceServer { + opaqueToken { } + } + } +} +``` + +或类似于方法安全性: + +爪哇 + +``` +@PreAuthorize("hasAuthority('SCOPE_messages')") +public Flux getMessages(...) {} +``` + +Kotlin + +``` +@PreAuthorize("hasAuthority('SCOPE_messages')") +fun getMessages(): Flux { } +``` + +### 手动提取权限 + +默认情况下,不透明令牌支持将从内省响应中提取范围声明,并将其解析为单个`GrantedAuthority`实例。 + +例如,如果内省反应是: + +``` +{ + "active" : true, + "scope" : "message:read message:write" +} +``` + +然后,资源服务器将生成一个带有两个权限的`Authentication`,一个用于`message:read`,另一个用于`message:write`。 + +当然,这可以使用自定义`ReactiveOpaqueTokenIntrospector`进行定制,该自定义`ReactiveOpaqueTokenIntrospector`查看属性集并以其自己的方式进行转换: + +爪哇 + +``` +public class CustomAuthoritiesOpaqueTokenIntrospector implements ReactiveOpaqueTokenIntrospector { + private ReactiveOpaqueTokenIntrospector delegate = + new NimbusReactiveOpaqueTokenIntrospector("https://idp.example.org/introspect", "client", "secret"); + + public Mono introspect(String token) { + return this.delegate.introspect(token) + .map(principal -> new DefaultOAuth2AuthenticatedPrincipal( + principal.getName(), principal.getAttributes(), extractAuthorities(principal))); + } + + private Collection extractAuthorities(OAuth2AuthenticatedPrincipal principal) { + List scopes = principal.getAttribute(OAuth2IntrospectionClaimNames.SCOPE); + return scopes.stream() + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toList()); + } +} +``` + +Kotlin + +``` +class CustomAuthoritiesOpaqueTokenIntrospector : ReactiveOpaqueTokenIntrospector { + private val delegate: ReactiveOpaqueTokenIntrospector = NimbusReactiveOpaqueTokenIntrospector("https://idp.example.org/introspect", "client", "secret") + override fun introspect(token: String): Mono { + return delegate.introspect(token) + .map { principal: OAuth2AuthenticatedPrincipal -> + DefaultOAuth2AuthenticatedPrincipal( + principal.name, principal.attributes, extractAuthorities(principal)) + } + } + + private fun extractAuthorities(principal: OAuth2AuthenticatedPrincipal): Collection { + val scopes = principal.getAttribute>(OAuth2IntrospectionClaimNames.SCOPE) + return scopes + .map { SimpleGrantedAuthority(it) } + } +} +``` + +在此之后,可以简单地将此自定义内省检测器配置为`@Bean`: + +爪哇 + +``` +@Bean +public ReactiveOpaqueTokenIntrospector introspector() { + return new CustomAuthoritiesOpaqueTokenIntrospector(); +} +``` + +Kotlin + +``` +@Bean +fun introspector(): ReactiveOpaqueTokenIntrospector { + return CustomAuthoritiesOpaqueTokenIntrospector() +} +``` + +## 使用 JWTS 进行内省 + +一个常见的问题是,内省是否与 JWTS 兼容。 Spring Security 的不透明令牌支持被设计成不关心令牌的格式——它将很乐意将任何令牌传递给所提供的内省端点。 + +所以,假设你有一个要求,要求你在每个请求上与授权服务器进行检查,以防 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`,它仍然会到达端点,但随后会更新返回的主体,使其具有 JWTS 声明的属性: + +爪哇 + +``` +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 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> { + public Mono convert(JWT jwt) { + try { + return Mono.just(jwt.getJWTClaimsSet()); + } catch (Exception ex) { + return Mono.error(ex); + } + } + } +} +``` + +Kotlin + +``` +class JwtOpaqueTokenIntrospector : ReactiveOpaqueTokenIntrospector { + private val delegate: ReactiveOpaqueTokenIntrospector = NimbusReactiveOpaqueTokenIntrospector("https://idp.example.org/introspect", "client", "secret") + private val jwtDecoder: ReactiveJwtDecoder = NimbusReactiveJwtDecoder(ParseOnlyJWTProcessor()) + override fun introspect(token: String): Mono { + return delegate.introspect(token) + .flatMap { jwtDecoder.decode(token) } + .map { jwt: Jwt -> DefaultOAuth2AuthenticatedPrincipal(jwt.claims, NO_AUTHORITIES) } + } + + private class ParseOnlyJWTProcessor : Converter> { + override fun convert(jwt: JWT): Mono { + return try { + Mono.just(jwt.jwtClaimsSet) + } catch (e: Exception) { + Mono.error(e) + } + } + } +} +``` + +在此之后,可以简单地将此自定义内省检测器配置为`@Bean`: + +爪哇 + +``` +@Bean +public ReactiveOpaqueTokenIntrospector introspector() { + return new JwtOpaqueTokenIntropsector(); +} +``` + +Kotlin + +``` +@Bean +fun introspector(): ReactiveOpaqueTokenIntrospector { + return JwtOpaqueTokenIntrospector() +} +``` + +## 调用`/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 oauth2UserService = + new DefaultReactiveOAuth2UserService(); + + private final ReactiveClientRegistrationRepository repository; + + // ... constructor + + @Override + public Mono 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); + } +} +``` + +Kotlin + +``` +class UserInfoOpaqueTokenIntrospector : ReactiveOpaqueTokenIntrospector { + private val delegate: ReactiveOpaqueTokenIntrospector = NimbusReactiveOpaqueTokenIntrospector("https://idp.example.org/introspect", "client", "secret") + private val oauth2UserService: ReactiveOAuth2UserService = DefaultReactiveOAuth2UserService() + private val repository: ReactiveClientRegistrationRepository? = null + + // ... constructor + override fun introspect(token: String?): Mono { + return Mono.zip(delegate.introspect(token), repository!!.findByRegistrationId("registration-id")) + .map { t: Tuple2 -> + val authorized = t.t1 + val clientRegistration = t.t2 + val issuedAt: Instant? = authorized.getAttribute(ISSUED_AT) + val expiresAt: Instant? = authorized.getAttribute(OAuth2IntrospectionClaimNames.EXPIRES_AT) + val accessToken = OAuth2AccessToken(BEARER, token, issuedAt, expiresAt) + OAuth2UserRequest(clientRegistration, accessToken) + } + .flatMap { userRequest: OAuth2UserRequest -> oauth2UserService.loadUser(userRequest) } + } +} +``` + +如果你 AREN 不使用`spring-security-oauth2-client`,它仍然很简单。你只需要用你自己的`WebClient`实例调用`/userinfo`: + +爪哇 + +``` +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 introspect(String token) { + return this.delegate.introspect(token) + .map(this::makeUserInfoRequest); + } +} +``` + +Kotlin + +``` +class UserInfoOpaqueTokenIntrospector : ReactiveOpaqueTokenIntrospector { + private val delegate: ReactiveOpaqueTokenIntrospector = NimbusReactiveOpaqueTokenIntrospector("https://idp.example.org/introspect", "client", "secret") + private val rest: WebClient = WebClient.create() + + override fun introspect(token: String): Mono { + return delegate.introspect(token) + .map(this::makeUserInfoRequest) + } +} +``` + +无论哪种方式,在创建了`ReactiveOpaqueTokenIntrospector`之后,你应该将其发布为`@Bean`,以覆盖默认值: + +爪哇 + +``` +@Bean +ReactiveOpaqueTokenIntrospector introspector() { + return new UserInfoOpaqueTokenIntrospector(); +} +``` + +Kotlin + +``` +@Bean +fun introspector(): ReactiveOpaqueTokenIntrospector { + return UserInfoOpaqueTokenIntrospector() +} +``` + +[JWT](jwt.html)[多租约](multitenancy.html) + diff --git a/docs/spring-security/reactive-oauth2-resource-server.md b/docs/spring-security/reactive-oauth2-resource-server.md new file mode 100644 index 0000000000000000000000000000000000000000..f14381284d4da54e5af6ed2491a5657f89396e7b --- /dev/null +++ b/docs/spring-security/reactive-oauth2-resource-server.md @@ -0,0 +1,15 @@ +# OAuth2.0 资源服务器 + +Spring 安全性支持使用两种形式的 OAuth2.0[不记名代币](https://tools.ietf.org/html/rfc6750.html)来保护端点: + +* [JWT](https://tools.ietf.org/html/rfc7519) + +* 不透明令牌 + +在应用程序已将其权限管理委托给[授权服务器](https://tools.ietf.org/html/rfc6749)(例如,OKTA 或 ping 标识)的情况下,这很方便。资源服务器可以参考此授权服务器来授权请求。 + +| |[**JWTs**](https://github.com/spring-projects/spring-security-samples/tree/5.6.x/reactive/webflux/java/oauth2/resource-server)的完整工作示例在[Spring Security repository](https://github.com/spring-projects/spring-security-samples/tree/5.6.x)中可用。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +[OAuth2 授权客户](../client/authorized-clients.html)[JWT](jwt.html) + diff --git a/docs/spring-security/reactive-oauth2.md b/docs/spring-security/reactive-oauth2.md new file mode 100644 index 0000000000000000000000000000000000000000..eb0e36cc8ff8f7855bada5c8a92aa8234eca466f --- /dev/null +++ b/docs/spring-security/reactive-oauth2.md @@ -0,0 +1,12 @@ +# OAuth2WebFlux + +Spring 安全性为反应性应用程序提供了 OAuth2 和 WebFlux 集成。 + +* [OAuth2 登录](login/index.html)-使用 OAuth2 或 OpenID Connect1.0 提供程序进行身份验证 + +* [OAuth2 客户端](client/index.html)-向 OAuth2 资源服务器发出请求 + +* [OAuth2 资源服务器](resource-server/index.html)-使用 OAuth2 保护 REST 端点 + +[EnableReactiveMethodSecurity ](../authorization/method.html)[OAuth2 登录](login/index.html) + diff --git a/docs/spring-security/reactive-test-method.md b/docs/spring-security/reactive-test-method.md new file mode 100644 index 0000000000000000000000000000000000000000..3adbadf2571808038486a4f2689202b456ff61cd --- /dev/null +++ b/docs/spring-security/reactive-test-method.md @@ -0,0 +1,74 @@ +# 测试方法安全性 + +例如,我们可以使用与[测试方法安全性](../../servlet/test/method.html#test-method)中相同的设置和注释来测试[EnableReactiveMethodSecurity ](../authorization/method.html#jc-erms)中的示例。以下是我们可以做的最小示例: + +爪哇 + +``` +@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(); + } +} +``` + +Kotlin + +``` +@RunWith(SpringRunner::class) +@ContextConfiguration(classes = [HelloWebfluxMethodApplication::class]) +class HelloWorldMessageServiceTests { + @Autowired + lateinit var messages: HelloWorldMessageService + + @Test + fun messagesWhenNotAuthenticatedThenDenied() { + StepVerifier.create(messages.findMessage()) + .expectError(AccessDeniedException::class.java) + .verify() + } + + @Test + @WithMockUser + fun messagesWhenUserThenDenied() { + StepVerifier.create(messages.findMessage()) + .expectError(AccessDeniedException::class.java) + .verify() + } + + @Test + @WithMockUser(roles = ["ADMIN"]) + fun messagesWhenAdminThenOk() { + StepVerifier.create(messages.findMessage()) + .expectNext("Hello World!") + .verifyComplete() + } +} +``` + +[Testing](index.html)[测试 Web 安全性](web/index.html) + diff --git a/docs/spring-security/reactive-test-web-authentication.md b/docs/spring-security/reactive-test-web-authentication.md new file mode 100644 index 0000000000000000000000000000000000000000..5f514a937fe9d01bccc6ec11fa32633befebccb5 --- /dev/null +++ b/docs/spring-security/reactive-test-web-authentication.md @@ -0,0 +1,114 @@ +# 测试身份验证 + +在[将 Spring 安全支持应用到`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!"); +} +``` + +Kotlin + +``` +import org.springframework.test.web.reactive.server.expectBody + +//... + +@Test +@WithMockUser +fun messageWhenWithMockUserThenForbidden() { + this.rest.get().uri("/message") + .exchange() + .expectStatus().isEqualTo(HttpStatus.FORBIDDEN) +} + +@Test +@WithMockUser(roles = ["ADMIN"]) +fun messageWhenWithMockAdminThenOk() { + this.rest.get().uri("/message") + .exchange() + .expectStatus().isOk + .expectBody().isEqualTo("Hello World!") + +} + +// --- mutateWith mockUser --- + +@Test +fun messageWhenMutateWithMockUserThenForbidden() { + this.rest + .mutateWith(mockUser()) + .get().uri("/message") + .exchange() + .expectStatus().isEqualTo(HttpStatus.FORBIDDEN) +} + +@Test +fun messageWhenMutateWithMockAdminThenOk() { + this.rest + .mutateWith(mockUser().roles("ADMIN")) + .get().uri("/message") + .exchange() + .expectStatus().isOk + .expectBody().isEqualTo("Hello World!") +} +``` + +除了`mockUser()`之外, Spring 安全船还带有其他几个方便的突变器,用于诸如[CSRF](csrf.html)和[OAuth 2.0](oauth2.html)之类的事情。 + +[WebTestClient 设置](setup.html)[测试 CSRF ](csrf.html) + diff --git a/docs/spring-security/reactive-test-web-csrf.md b/docs/spring-security/reactive-test-web-csrf.md new file mode 100644 index 0000000000000000000000000000000000000000..bc516945ef1efc458ca3bcaaf65376ca8819c438 --- /dev/null +++ b/docs/spring-security/reactive-test-web-csrf.md @@ -0,0 +1,28 @@ +# 使用 CSRF 进行测试 + +Spring 安全性还提供了对`WebTestClient`的 CSRF 测试的支持。例如: + +爪哇 + +``` +this.rest + // provide a valid CSRF token + .mutateWith(csrf()) + .post() + .uri("/login") + ... +``` + +Kotlin + +``` +this.rest + // provide a valid CSRF token + .mutateWith(csrf()) + .post() + .uri("/login") + ... +``` + +[测试身份验证](authentication.html)[测试 OAuth2.0](oauth2.html) + diff --git a/docs/spring-security/reactive-test-web-oauth2.md b/docs/spring-security/reactive-test-web-oauth2.md new file mode 100644 index 0000000000000000000000000000000000000000..2d0e1c4beda98405d79778fdbe685e97fda02741 --- /dev/null +++ b/docs/spring-security/reactive-test-web-oauth2.md @@ -0,0 +1,1034 @@ +# 测试 OAuth2.0 + +当涉及到 OAuth2.0 时,[前面提到的原则仍然适用。](../method.html#test-erms):最终,这取决于你的测试方法在`SecurityContextHolder`中的期望。 + +例如,对于如下所示的控制器: + +爪哇 + +``` +@GetMapping("/endpoint") +public Mono foo(Principal user) { + return Mono.just(user.getName()); +} +``` + +Kotlin + +``` +@GetMapping("/endpoint") +fun foo(user: Principal): Mono { + return Mono.just(user.name) +} +``` + +它没有特定于 OAuth2 的内容,因此你可能只需[使用`@WithMockUser`](../method.html#test-erms)就可以了。 + +但是,在你的控制器绑定到 Spring Security 的 OAuth2.0 支持的某些方面的情况下,例如: + +爪哇 + +``` +@GetMapping("/endpoint") +public Mono foo(@AuthenticationPrincipal OidcUser user) { + return Mono.just(user.getIdToken().getSubject()); +} +``` + +Kotlin + +``` +@GetMapping("/endpoint") +fun foo(@AuthenticationPrincipal user: OidcUser): Mono { + return Mono.just(user.idToken.subject) +} +``` + +然后,安全的测试支持就会派上用场。 + +## 测试 OIDC 登录 + +用`WebTestClient`测试上面的方法将需要用授权服务器模拟某种授权流。当然,这将是一项艰巨的任务,这就是为什么安全船支持删除这一样板。 + +例如,我们可以使用`SecurityMockServerConfigurers#mockOidcLogin`方法告诉 Spring 安全性包含一个默认的`OidcUser`,如下所示: + +爪哇 + +``` +client + .mutateWith(mockOidcLogin()).get().uri("/endpoint").exchange(); +``` + +Kotlin + +``` +client + .mutateWith(mockOidcLogin()) + .get().uri("/endpoint") + .exchange() +``` + +这样做的目的是将关联的`MockServerRequest`配置为`OidcUser`,其中包括授予权限的简单`OidcIdToken`、`OidcUserInfo`和`Collection`。 + +具体地说,它将包括一个`OidcIdToken`,其`sub`声明设置为`user`: + +爪哇 + +``` +assertThat(user.getIdToken().getClaim("sub")).isEqualTo("user"); +``` + +Kotlin + +``` +assertThat(user.idToken.getClaim("sub")).isEqualTo("user") +``` + +没有设置索赔要求的`OidcUserInfo`: + +爪哇 + +``` +assertThat(user.getUserInfo().getClaims()).isEmpty(); +``` + +Kotlin + +``` +assertThat(user.userInfo.claims).isEmpty() +``` + +而`Collection`只有一个权限的权限,`SCOPE_read`: + +爪哇 + +``` +assertThat(user.getAuthorities()).hasSize(1); +assertThat(user.getAuthorities()).containsExactly(new SimpleGrantedAuthority("SCOPE_read")); +``` + +Kotlin + +``` +assertThat(user.authorities).hasSize(1) +assertThat(user.authorities).containsExactly(SimpleGrantedAuthority("SCOPE_read")) +``` + +Spring 安全性做了必要的工作,以确保`OidcUser`实例可用于[`@AuthenticationPrincipal`注释](.../.../ Servlet/integrations/mvc.html#mvc-authentication-principal)。 + +此外,它还将`OidcUser`链接到`OAuth2AuthorizedClient`的一个简单实例,该实例将其存入一个模拟`ServerOAuth2AuthorizedClientRepository`。如果你的测试[使用`@RegisteredOAuth2AuthorizedClient`注释](#webflux-testing-oAuth2-client),这将非常方便。 + +### 配置权限 + +在许多情况下,你的方法受到过滤器或方法安全性的保护,并且需要你的`Authentication`具有特定的授权来允许请求。 + +在这种情况下,你可以使用`authorities()`方法提供你需要的授权: + +爪哇 + +``` +client + .mutateWith(mockOidcLogin() + .authorities(new SimpleGrantedAuthority("SCOPE_message:read")) + ) + .get().uri("/endpoint").exchange(); +``` + +Kotlin + +``` +client + .mutateWith(mockOidcLogin() + .authorities(SimpleGrantedAuthority("SCOPE_message:read")) + ) + .get().uri("/endpoint").exchange() +``` + +### 配置索赔 + +虽然在所有安全领域,授予权限是很常见的,但在 OAuth2.0 的情况下,我们也有主张。 + +例如,假设你有一个`user_id`声明,它指示了系统中用户的 ID。你可以像在控制器中那样访问它: + +爪哇 + +``` +@GetMapping("/endpoint") +public Mono foo(@AuthenticationPrincipal OidcUser oidcUser) { + String userId = oidcUser.getIdToken().getClaim("user_id"); + // ... +} +``` + +Kotlin + +``` +@GetMapping("/endpoint") +fun foo(@AuthenticationPrincipal oidcUser: OidcUser): Mono { + val userId = oidcUser.idToken.getClaim("user_id") + // ... +} +``` + +在这种情况下,你需要使用`idToken()`方法来指定该声明: + +爪哇 + +``` +client + .mutateWith(mockOidcLogin() + .idToken(token -> token.claim("user_id", "1234")) + ) + .get().uri("/endpoint").exchange(); +``` + +Kotlin + +``` +client + .mutateWith(mockOidcLogin() + .idToken { token -> token.claim("user_id", "1234") } + ) + .get().uri("/endpoint").exchange() +``` + +由于`OidcUser`从`OidcIdToken`收集其索赔。 + +### 附加配置 + +还有其他方法可以用于进一步配置身份验证;它只是取决于控制器所期望的数据: + +* `userInfo(OidcUserInfo.Builder)`-用于配置`OidcUserInfo`实例 + +* `clientRegistration(ClientRegistration)`-用于配置与给定的`ClientRegistration`相关联的`OAuth2AuthorizedClient` + +* `oidcUser(OidcUser)`-用于配置完整的`OidcUser`实例 + +最后一个很方便,如果你: +1. 有你自己的`OidcUser`的实现,或者 +2. 需要更改名称属性 + +例如,假设你的授权服务器发送`user_name`声明中的主体名称,而不是`sub`声明中的主体名称。在这种情况下,你可以手动配置`OidcUser`: + +爪哇 + +``` +OidcUser oidcUser = new DefaultOidcUser( + AuthorityUtils.createAuthorityList("SCOPE_message:read"), + OidcIdToken.withTokenValue("id-token").claim("user_name", "foo_user").build(), + "user_name"); + +client + .mutateWith(mockOidcLogin().oidcUser(oidcUser)) + .get().uri("/endpoint").exchange(); +``` + +Kotlin + +``` +val oidcUser: OidcUser = DefaultOidcUser( + AuthorityUtils.createAuthorityList("SCOPE_message:read"), + OidcIdToken.withTokenValue("id-token").claim("user_name", "foo_user").build(), + "user_name" +) + +client + .mutateWith(mockOidcLogin().oidcUser(oidcUser)) + .get().uri("/endpoint").exchange() +``` + +## 测试 OAuth2.0 登录 + +与[测试 OIDC 登录](#webflux-testing-oidc-login)一样,测试 OAuth2.0Login 也会遇到类似的挑战,即模拟授予流。正因为如此, Spring Security 还具有对非 OIDC 用例的测试支持。 + +假设我们有一个控制器,可以将登录用户作为`OAuth2User`: + +爪哇 + +``` +@GetMapping("/endpoint") +public Mono foo(@AuthenticationPrincipal OAuth2User oauth2User) { + return Mono.just(oauth2User.getAttribute("sub")); +} +``` + +Kotlin + +``` +@GetMapping("/endpoint") +fun foo(@AuthenticationPrincipal oauth2User: OAuth2User): Mono { + return Mono.just(oauth2User.getAttribute("sub")) +} +``` + +在这种情况下,我们可以使用`SecurityMockServerConfigurers#mockOAuth2Login`方法告诉 Spring Security 包含一个默认的`OAuth2User`,就像这样: + +爪哇 + +``` +client + .mutateWith(mockOAuth2Login()) + .get().uri("/endpoint").exchange(); +``` + +Kotlin + +``` +client + .mutateWith(mockOAuth2Login()) + .get().uri("/endpoint").exchange() +``` + +这样做的目的是将关联的`MockServerRequest`配置为`OAuth2User`,其中包括一个简单的`Map`属性和`Collection`授予权限。 + +具体地说,它将包括一个`Map`,其键/值对为`sub`/`user`: + +爪哇 + +``` +assertThat((String) user.getAttribute("sub")).isEqualTo("user"); +``` + +Kotlin + +``` +assertThat(user.getAttribute("sub")).isEqualTo("user") +``` + +a`Collection`只有一个权限的权限,`SCOPE_read`: + +爪哇 + +``` +assertThat(user.getAuthorities()).hasSize(1); +assertThat(user.getAuthorities()).containsExactly(new SimpleGrantedAuthority("SCOPE_read")); +``` + +Kotlin + +``` +assertThat(user.authorities).hasSize(1) +assertThat(user.authorities).containsExactly(SimpleGrantedAuthority("SCOPE_read")) +``` + +Spring 安全性做了必要的工作,以确保`OAuth2User`实例可用于[`@AuthenticationPrincipal`注释](.../../ Servlet/integrations/mvc.html#mvc-authentication-principal)。 + +此外,它还将`OAuth2User`链接到它在模拟`ServerOAuth2AuthorizedClientRepository`中存放的`OAuth2AuthorizedClient`的一个简单实例。如果你的测试[使用`@RegisteredOAuth2AuthorizedClient`注释](#webflux-testing-oAuth2-client),这将非常方便。 + +### 配置权限 + +在许多情况下,你的方法受到过滤器或方法安全性的保护,并且需要你的`Authentication`具有特定的授权来允许请求。 + +在这种情况下,你可以使用`authorities()`方法提供你需要的授权: + +爪哇 + +``` +client + .mutateWith(mockOAuth2Login() + .authorities(new SimpleGrantedAuthority("SCOPE_message:read")) + ) + .get().uri("/endpoint").exchange(); +``` + +Kotlin + +``` +client + .mutateWith(mockOAuth2Login() + .authorities(SimpleGrantedAuthority("SCOPE_message:read")) + ) + .get().uri("/endpoint").exchange() +``` + +### 配置索赔 + +虽然在所有的安全中,授予权限是很常见的,但在 OAuth2.0 的情况下,我们也有主张。 + +例如,假设你有一个`user_id`属性,该属性指示系统中的用户 ID。你可以像在控制器中那样访问它: + +爪哇 + +``` +@GetMapping("/endpoint") +public Mono foo(@AuthenticationPrincipal OAuth2User oauth2User) { + String userId = oauth2User.getAttribute("user_id"); + // ... +} +``` + +Kotlin + +``` +@GetMapping("/endpoint") +fun foo(@AuthenticationPrincipal oauth2User: OAuth2User): Mono { + val userId = oauth2User.getAttribute("user_id") + // ... +} +``` + +在这种情况下,你需要使用`attributes()`方法指定该属性: + +爪哇 + +``` +client + .mutateWith(mockOAuth2Login() + .attributes(attrs -> attrs.put("user_id", "1234")) + ) + .get().uri("/endpoint").exchange(); +``` + +Kotlin + +``` +client + .mutateWith(mockOAuth2Login() + .attributes { attrs -> attrs["user_id"] = "1234" } + ) + .get().uri("/endpoint").exchange() +``` + +### 附加配置 + +还有其他方法可以用于进一步配置身份验证;它只是取决于控制器所期望的数据: + +* `clientRegistration(ClientRegistration)`-用于配置与给定的`ClientRegistration`相关联的`OAuth2AuthorizedClient` + +* `oauth2User(OAuth2User)`-用于配置完整的`OAuth2User`实例 + +最后一个很方便,如果你: +1. 有你自己的`OAuth2User`的实现,或者 +2. 需要更改名称属性 + +例如,假设你的授权服务器发送`user_name`声明中的主体名称,而不是`sub`声明中的主体名称。在这种情况下,你可以手动配置`OAuth2User`: + +爪哇 + +``` +OAuth2User oauth2User = new DefaultOAuth2User( + AuthorityUtils.createAuthorityList("SCOPE_message:read"), + Collections.singletonMap("user_name", "foo_user"), + "user_name"); + +client + .mutateWith(mockOAuth2Login().oauth2User(oauth2User)) + .get().uri("/endpoint").exchange(); +``` + +Kotlin + +``` +val oauth2User: OAuth2User = DefaultOAuth2User( + AuthorityUtils.createAuthorityList("SCOPE_message:read"), + mapOf(Pair("user_name", "foo_user")), + "user_name" +) + +client + .mutateWith(mockOAuth2Login().oauth2User(oauth2User)) + .get().uri("/endpoint").exchange() +``` + +## 测试 OAuth2.0 客户端 + +独立于你的用户如何进行身份验证,你可能有其他令牌和客户端注册,这些令牌和客户端注册正在为你正在测试的请求发挥作用。例如,你的控制器可能依赖于客户机凭据授权来获得一个与用户完全不相关的令牌: + +爪哇 + +``` +@GetMapping("/endpoint") +public Mono foo(@RegisteredOAuth2AuthorizedClient("my-app") OAuth2AuthorizedClient authorizedClient) { + return this.webClient.get() + .attributes(oauth2AuthorizedClient(authorizedClient)) + .retrieve() + .bodyToMono(String.class); +} +``` + +Kotlin + +``` +import org.springframework.web.reactive.function.client.bodyToMono + +// ... + +@GetMapping("/endpoint") +fun foo(@RegisteredOAuth2AuthorizedClient("my-app") authorizedClient: OAuth2AuthorizedClient?): Mono { + return this.webClient.get() + .attributes(oauth2AuthorizedClient(authorizedClient)) + .retrieve() + .bodyToMono() +} +``` + +用授权服务器模拟这种握手可能会很麻烦。相反,你可以使用`SecurityMockServerConfigurers#mockOAuth2Client`将`OAuth2AuthorizedClient`添加到模拟`ServerOAuth2AuthorizedClientRepository`中: + +爪哇 + +``` +client + .mutateWith(mockOAuth2Client("my-app")) + .get().uri("/endpoint").exchange(); +``` + +Kotlin + +``` +client + .mutateWith(mockOAuth2Client("my-app")) + .get().uri("/endpoint").exchange() +``` + +这将创建一个`OAuth2AuthorizedClient`,它具有一个简单的`ClientRegistration`、`OAuth2AccessToken`和资源所有者名称。 + +具体地说,它将包括一个`ClientRegistration`,其客户端 ID 为“test-client”,客户端秘密为“test-secret”: + +爪哇 + +``` +assertThat(authorizedClient.getClientRegistration().getClientId()).isEqualTo("test-client"); +assertThat(authorizedClient.getClientRegistration().getClientSecret()).isEqualTo("test-secret"); +``` + +Kotlin + +``` +assertThat(authorizedClient.clientRegistration.clientId).isEqualTo("test-client") +assertThat(authorizedClient.clientRegistration.clientSecret).isEqualTo("test-secret") +``` + +“user”的资源所有者名称: + +爪哇 + +``` +assertThat(authorizedClient.getPrincipalName()).isEqualTo("user"); +``` + +Kotlin + +``` +assertThat(authorizedClient.principalName).isEqualTo("user") +``` + +以及只有一个作用域`OAuth2AccessToken`的`read`: + +爪哇 + +``` +assertThat(authorizedClient.getAccessToken().getScopes()).hasSize(1); +assertThat(authorizedClient.getAccessToken().getScopes()).containsExactly("read"); +``` + +Kotlin + +``` +assertThat(authorizedClient.accessToken.scopes).hasSize(1) +assertThat(authorizedClient.accessToken.scopes).containsExactly("read") +``` + +然后在控制器方法中使用`@RegisteredOAuth2AuthorizedClient`可以正常地检索客户端。 + +### 配置作用域 + +在许多情况下,OAuth2.0 访问令牌都带有一组作用域。如果你的控制员检查了这些,请这样说: + +爪哇 + +``` +@GetMapping("/endpoint") +public Mono foo(@RegisteredOAuth2AuthorizedClient("my-app") OAuth2AuthorizedClient authorizedClient) { + Set scopes = authorizedClient.getAccessToken().getScopes(); + if (scopes.contains("message:read")) { + return this.webClient.get() + .attributes(oauth2AuthorizedClient(authorizedClient)) + .retrieve() + .bodyToMono(String.class); + } + // ... +} +``` + +Kotlin + +``` +import org.springframework.web.reactive.function.client.bodyToMono + +// ... + +@GetMapping("/endpoint") +fun foo(@RegisteredOAuth2AuthorizedClient("my-app") authorizedClient: OAuth2AuthorizedClient): Mono { + val scopes = authorizedClient.accessToken.scopes + if (scopes.contains("message:read")) { + return webClient.get() + .attributes(oauth2AuthorizedClient(authorizedClient)) + .retrieve() + .bodyToMono() + } + // ... +} +``` + +然后,你可以使用`accessToken()`方法配置范围: + +爪哇 + +``` +client + .mutateWith(mockOAuth2Client("my-app") + .accessToken(new OAuth2AccessToken(BEARER, "token", null, null, Collections.singleton("message:read"))) + ) + .get().uri("/endpoint").exchange(); +``` + +Kotlin + +``` +client + .mutateWith(mockOAuth2Client("my-app") + .accessToken(OAuth2AccessToken(BEARER, "token", null, null, setOf("message:read"))) +) +.get().uri("/endpoint").exchange() +``` + +### 附加配置 + +还有其他方法可以用于进一步配置身份验证;它只是取决于控制器所期望的数据: + +* `principalName(String)`-用于配置资源所有者名称 + +* `clientRegistration(Consumer)`-用于配置相关的`ClientRegistration` + +* `clientRegistration(ClientRegistration)`-用于配置完整的`ClientRegistration` + +如果你想使用真正的`ClientRegistration`,那么最后一个就很方便了。 + +例如,假设你希望使用应用程序的`ClientRegistration`定义之一,如你的`application.yml`中所指定的。 + +在这种情况下,你的测试可以自动连接`ReactiveClientRegistrationRepository`并查找你的测试所需的一个: + +爪哇 + +``` +@Autowired +ReactiveClientRegistrationRepository clientRegistrationRepository; + +// ... + +client + .mutateWith(mockOAuth2Client() + .clientRegistration(this.clientRegistrationRepository.findByRegistrationId("facebook").block()) + ) + .get().uri("/exchange").exchange(); +``` + +Kotlin + +``` +@Autowired +lateinit var clientRegistrationRepository: ReactiveClientRegistrationRepository + +// ... + +client + .mutateWith(mockOAuth2Client() + .clientRegistration(this.clientRegistrationRepository.findByRegistrationId("facebook").block()) + ) + .get().uri("/exchange").exchange() +``` + +## 测试 JWT 身份验证 + +为了在资源服务器上发出授权请求,你需要一个承载令牌。如果你的资源服务器是为 JWTS 配置的,那么这将意味着需要对承载令牌进行签名,然后根据 JWT 规范对其进行编码。所有这一切都可能令人望而生畏,尤其是当这不是测试的重点时。 + +幸运的是,有许多简单的方法可以克服这个困难,并允许你的测试专注于授权,而不是表示不记名令牌。我们现在来看看其中的两个: + +### WebTestClientConfigurer + +第一种方法是通过`WebTestClientConfigurer`。其中最简单的方法是使用`SecurityMockServerConfigurers#mockJwt`方法,如下所示: + +爪哇 + +``` +client + .mutateWith(mockJwt()).get().uri("/endpoint").exchange(); +``` + +Kotlin + +``` +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"); +``` + +Kotlin + +``` +assertThat(jwt.tokenValue).isEqualTo("token") +assertThat(jwt.headers["alg"]).isEqualTo("none") +assertThat(jwt.subject).isEqualTo("sub") +``` + +当然,可以对这些值进行配置。 + +任何标题或权利要求都可以配置相应的方法: + +爪哇 + +``` +client + .mutateWith(mockJwt().jwt(jwt -> jwt.header("kid", "one") + .claim("iss", "https://idp.example.org"))) + .get().uri("/endpoint").exchange(); +``` + +Kotlin + +``` +client + .mutateWith(mockJwt().jwt { jwt -> jwt.header("kid", "one") + .claim("iss", "https://idp.example.org") + }) + .get().uri("/endpoint").exchange() +``` + +爪哇 + +``` +client + .mutateWith(mockJwt().jwt(jwt -> jwt.claims(claims -> claims.remove("scope")))) + .get().uri("/endpoint").exchange(); +``` + +Kotlin + +``` +client + .mutateWith(mockJwt().jwt { jwt -> + jwt.claims { claims -> claims.remove("scope") } + }) + .get().uri("/endpoint").exchange() +``` + +在这里,对`scope`和`scp`声明的处理方式与在正常的无记名令牌请求中的处理方式相同。但是,只需提供测试所需的`GrantedAuthority`实例的列表,就可以覆盖此内容: + +爪哇 + +``` +client + .mutateWith(mockJwt().authorities(new SimpleGrantedAuthority("SCOPE_messages"))) + .get().uri("/endpoint").exchange(); +``` + +Kotlin + +``` +client + .mutateWith(mockJwt().authorities(SimpleGrantedAuthority("SCOPE_messages"))) + .get().uri("/endpoint").exchange() +``` + +或者,如果你有一个自定义的`Jwt`到`Collection`转换器,那么你也可以使用它来派生权威: + +爪哇 + +``` +client + .mutateWith(mockJwt().authorities(new MyConverter())) + .get().uri("/endpoint").exchange(); +``` + +Kotlin + +``` +client + .mutateWith(mockJwt().authorities(MyConverter())) + .get().uri("/endpoint").exchange() +``` + +你还可以指定一个完整的`Jwt`,其中`[Jwt.Builder](https://docs.spring.io/spring-security/site/docs/5.6.2/api/org/springframework/security/oauth2/jwt/Jwt.Builder.html)`非常方便: + +爪哇 + +``` +Jwt jwt = Jwt.withTokenValue("token") + .header("alg", "none") + .claim("sub", "user") + .claim("scope", "read") + .build(); + +client + .mutateWith(mockJwt().jwt(jwt)) + .get().uri("/endpoint").exchange(); +``` + +Kotlin + +``` +val jwt: Jwt = Jwt.withTokenValue("token") + .header("alg", "none") + .claim("sub", "user") + .claim("scope", "read") + .build() + +client + .mutateWith(mockJwt().jwt(jwt)) + .get().uri("/endpoint").exchange() +``` + +### ` `WebTestClientConfigurer` + +第二种方法是使用`authentication()``Mutator`。本质上,你可以实例化自己的`JwtAuthenticationToken`并在测试中提供它,如下所示: + +爪哇 + +``` +Jwt jwt = Jwt.withTokenValue("token") + .header("alg", "none") + .claim("sub", "user") + .build(); +Collection authorities = AuthorityUtils.createAuthorityList("SCOPE_read"); +JwtAuthenticationToken token = new JwtAuthenticationToken(jwt, authorities); + +client + .mutateWith(mockAuthentication(token)) + .get().uri("/endpoint").exchange(); +``` + +Kotlin + +``` +val jwt = Jwt.withTokenValue("token") + .header("alg", "none") + .claim("sub", "user") + .build() +val authorities: Collection = AuthorityUtils.createAuthorityList("SCOPE_read") +val token = JwtAuthenticationToken(jwt, authorities) + +client + .mutateWith(mockAuthentication(token)) + .get().uri("/endpoint").exchange() +``` + +请注意,作为这些方法的替代方法,你还可以使用`ReactiveJwtDecoder` Bean 注释来模拟`@MockBean`本身。 + +## 测试不透明令牌身份验证 + +与[JWTs](#webflux-testing-jwt)类似,不透明令牌需要授权服务器来验证其有效性,这可能会使测试更加困难。 Spring 为了帮助实现这一点,Security 提供了对不透明令牌的测试支持。 + +假设我们有一个控制器,它以`BearerTokenAuthentication`的形式检索身份验证: + +爪哇 + +``` +@GetMapping("/endpoint") +public Mono foo(BearerTokenAuthentication authentication) { + return Mono.just((String) authentication.getTokenAttributes().get("sub")); +} +``` + +Kotlin + +``` +@GetMapping("/endpoint") +fun foo(authentication: BearerTokenAuthentication): Mono { + return Mono.just(authentication.tokenAttributes["sub"] as String?) +} +``` + +在这种情况下,我们可以使用`SecurityMockServerConfigurers#mockOpaqueToken`方法告诉 Spring Security 包含一个默认的`BearerTokenAuthentication`,就像这样: + +爪哇 + +``` +client + .mutateWith(mockOpaqueToken()) + .get().uri("/endpoint").exchange(); +``` + +Kotlin + +``` +client + .mutateWith(mockOpaqueToken()) + .get().uri("/endpoint").exchange() +``` + +这样做的目的是将关联的`MockHttpServletRequest`配置为`BearerTokenAuthentication`,其中包括一个简单的`OAuth2AuthenticatedPrincipal`、`Map`的属性,以及`Collection`的授予权限。 + +具体地说,它将包括一个`Map`,其键/值对为`sub`/`user`: + +爪哇 + +``` +assertThat((String) token.getTokenAttributes().get("sub")).isEqualTo("user"); +``` + +Kotlin + +``` +assertThat(token.tokenAttributes["sub"] as String?).isEqualTo("user") +``` + +而`Collection`只有一个权限的权限,`SCOPE_read`: + +爪哇 + +``` +assertThat(token.getAuthorities()).hasSize(1); +assertThat(token.getAuthorities()).containsExactly(new SimpleGrantedAuthority("SCOPE_read")); +``` + +Kotlin + +``` +assertThat(token.authorities).hasSize(1) +assertThat(token.authorities).containsExactly(SimpleGrantedAuthority("SCOPE_read")) +``` + +Spring 安全性做了必要的工作,以确保`BearerTokenAuthentication`实例可用于你的控制器方法。 + +### 配置权限 + +在许多情况下,你的方法受到过滤器或方法安全性的保护,并且需要你的`Authentication`拥有某些授权权限来允许请求。 + +在这种情况下,你可以使用`authorities()`方法提供你需要的授权: + +爪哇 + +``` +client + .mutateWith(mockOpaqueToken() + .authorities(new SimpleGrantedAuthority("SCOPE_message:read")) + ) + .get().uri("/endpoint").exchange(); +``` + +Kotlin + +``` +client + .mutateWith(mockOpaqueToken() + .authorities(SimpleGrantedAuthority("SCOPE_message:read")) + ) + .get().uri("/endpoint").exchange() +``` + +### 配置索赔 + +虽然在所有 Spring 安全性中,授予权限是非常常见的,但在 OAuth2.0 中,我们也有属性。 + +例如,假设你有一个`user_id`属性,该属性指示系统中用户的 ID。你可以像在控制器中那样访问它: + +爪哇 + +``` +@GetMapping("/endpoint") +public Mono foo(BearerTokenAuthentication authentication) { + String userId = (String) authentication.getTokenAttributes().get("user_id"); + // ... +} +``` + +Kotlin + +``` +@GetMapping("/endpoint") +fun foo(authentication: BearerTokenAuthentication): Mono { + val userId = authentication.tokenAttributes["user_id"] as String? + // ... +} +``` + +在这种情况下,你需要使用`attributes()`方法指定该属性: + +爪哇 + +``` +client + .mutateWith(mockOpaqueToken() + .attributes(attrs -> attrs.put("user_id", "1234")) + ) + .get().uri("/endpoint").exchange(); +``` + +Kotlin + +``` +client + .mutateWith(mockOpaqueToken() + .attributes { attrs -> attrs["user_id"] = "1234" } + ) + .get().uri("/endpoint").exchange() +``` + +### 附加配置 + +还有其他方法来进一步配置身份验证;它只是取决于控制器期望的数据。 + +其中一个是`principal(OAuth2AuthenticatedPrincipal)`,你可以使用它来配置作为`OAuth2AuthenticatedPrincipal`实例基础的完整`BearerTokenAuthentication`实例。 + +如果你: +1. 有你自己的`OAuth2AuthenticatedPrincipal`的实现,或者 +2. 想要指定不同的主体名称 + +例如,假设你的授权服务器发送`user_name`属性中的主体名称,而不是`sub`属性。在这种情况下,你可以手动配置`OAuth2AuthenticatedPrincipal`: + +爪哇 + +``` +Map attributes = Collections.singletonMap("user_name", "foo_user"); +OAuth2AuthenticatedPrincipal principal = new DefaultOAuth2AuthenticatedPrincipal( + (String) attributes.get("user_name"), + attributes, + AuthorityUtils.createAuthorityList("SCOPE_message:read")); + +client + .mutateWith(mockOpaqueToken().principal(principal)) + .get().uri("/endpoint").exchange(); +``` + +Kotlin + +``` +val attributes: Map = mapOf(Pair("user_name", "foo_user")) +val principal: OAuth2AuthenticatedPrincipal = DefaultOAuth2AuthenticatedPrincipal( + attributes["user_name"] as String?, + attributes, + AuthorityUtils.createAuthorityList("SCOPE_message:read") +) + +client + .mutateWith(mockOpaqueToken().principal(principal)) + .get().uri("/endpoint").exchange() +``` + +请注意,作为使用`mockOpaqueToken()`测试支持的一种替代方法,你还可以使用`OpaqueTokenIntrospector` Bean 本身来模拟`@MockBean`注释。 + +[测试 CSRF ](csrf.html)[WebFlux 安全性](../../configuration/webflux.html) + diff --git a/docs/spring-security/reactive-test-web-setup.md b/docs/spring-security/reactive-test-web-setup.md new file mode 100644 index 0000000000000000000000000000000000000000..16d742b3719b27a4d3a6c986d1c83187b8ae5a9c --- /dev/null +++ b/docs/spring-security/reactive-test-web-setup.md @@ -0,0 +1,29 @@ +# 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(); + } + // ... +} +``` + +[测试 Web 安全性](index.html)[测试身份验证](authentication.html) + diff --git a/docs/spring-security/reactive-test-web.md b/docs/spring-security/reactive-test-web.md new file mode 100644 index 0000000000000000000000000000000000000000..d27cdb3bda5428152c1c7eb7de5f80b6d27bdbf7 --- /dev/null +++ b/docs/spring-security/reactive-test-web.md @@ -0,0 +1,13 @@ +# 测试 Web 安全性 + +在本节中,我们将讨论测试 Web 应用程序端点。 + +## 章节摘要 + +* [WebTestClient 设置](setup.html) +* [测试身份验证](authentication.html) +* [测试 CSRF ](csrf.html) +* [测试 OAuth2.0](oauth2.html) + +[测试方法安全性](../method.html)[WebTestClient 设置](setup.html) + diff --git a/docs/spring-security/reactive-test.md b/docs/spring-security/reactive-test.md new file mode 100644 index 0000000000000000000000000000000000000000..ba023b8a79181ee73db8dc3eabc9038507d4016f --- /dev/null +++ b/docs/spring-security/reactive-test.md @@ -0,0 +1,11 @@ +# 反应测试支持 + +Spring 安全性支持用于测试反应性应用程序的两种基本模式。 + +## 章节摘要 + +* [测试方法安全性](method.html) +* [测试 Web 安全性](web/index.html) + +[RSocket](../integrations/rsocket.html)[测试方法安全性](method.html) + diff --git a/docs/spring-security/reactive.md b/docs/spring-security/reactive.md new file mode 100644 index 0000000000000000000000000000000000000000..b7b35e4d300b5c065ba67f46dacf0b26fe41f525 --- /dev/null +++ b/docs/spring-security/reactive.md @@ -0,0 +1,6 @@ +# 反应性应用 + +反应性应用程序的工作原理与[Servlet Applications](../servlet/index.html#servlet-applications)非常不同。本节讨论 Spring 安全性如何与反应性应用程序一起工作,这些应用程序通常使用 Spring 的 WebFlux 编写。 + +[FAQ](../servlet/appendix/faq.html)[开始](getting-started.html) + diff --git a/docs/spring-security/samples.md b/docs/spring-security/samples.md new file mode 100644 index 0000000000000000000000000000000000000000..a598c862c1635c71fe6ffc75aba9126d2950bb4b --- /dev/null +++ b/docs/spring-security/samples.md @@ -0,0 +1,9 @@ +# 样本 + +Spring 安全性包括许多[samples](https://github.com/spring-projects/spring-security-samples/tree/5.6.x)应用程序。 + +| |这些样本正在迁移到一个单独的项目中,但是,你仍然可以在[Spring Security repository](https://github.com/spring-projects/spring-security/tree/5.4.x/samples)的一个较旧的分支中找到
未迁移的样本。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +[项目模块](modules.html)[Servlet Applications](servlet/index.html) + diff --git a/docs/spring-security/servlet-appendix-database-schema.md b/docs/spring-security/servlet-appendix-database-schema.md new file mode 100644 index 0000000000000000000000000000000000000000..a98bc0f6f82505f2ae4da2131de3da9174a64c84 --- /dev/null +++ b/docs/spring-security/servlet-appendix-database-schema.md @@ -0,0 +1,365 @@ +# 安全数据库模式 + +给出了 HSQLDB 数据库的 DDL 语句。你可以使用这些作为指导方针来定义你正在使用的数据库的模式。 + +## 用户模式 + +`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 Security2.0 在`JdbcDaoImpl`中引入了对组权限的支持。如果启用了组,则表结构如下所示。你将需要调整此模式以匹配你正在使用的数据库方言。 + +``` +create table groups ( + id bigint generated by default as identity(start with 0) primary key, + group_name varchar_ignorecase(50) not null +); + +create table group_authorities ( + group_id bigint not null, + authority varchar(50) not null, + constraint fk_group_authorities_group foreign key(group_id) references groups(id) +); + +create table group_members ( + id bigint generated by default as identity(start with 0) primary key, + username varchar(50) not null, + group_id bigint not null, + constraint fk_group_members_group foreign key(group_id) references groups(id) +); +``` + +请记住,只有在使用所提供的 JDBC`UserDetailsService`实现时,这些表才是必需的。如果你自己编写或者选择在没有`UserDetailsService`的情况下实现`AuthenticationProvider`,那么你完全可以自由地决定如何存储数据,只要满足接口契约。 + +## 模式 + +此表用于存储由更安全的[持久令牌](../authentication/rememberme.html#remember-me-persistent-token)remember-me 实现使用的数据。如果你直接或通过名称空间使用`JdbcTokenRepositoryImpl`,那么你将需要这个表。请记住调整此模式以匹配你正在使用的数据库方言。 + +``` +create table persistent_logins ( + username varchar(64) not null, + series varchar(64) primary key, + token varchar(64) not null, + last_used timestamp not null +); +``` + +## ACL 模式 + +Spring Security[ACL](../authorization/acls.html#domain-acls)实现使用了四个表。 + +1. `acl_sid`存储 ACL 系统识别的安全标识。这些可以是唯一的主体或权威,可以适用于多个主体。 + +2. `acl_class`定义了 ACLS 应用的域对象类型。`class`列存储对象的 Java 类名。 + +3. `acl_object_identity`存储特定领域对象的对象标识定义。 + +4. `acl_entry`存储应用于特定对象标识和安全标识的 ACL 权限。 + +假定数据库将自动生成每个标识的主键。当`JdbcMutableAclService`在`acl_sid`或`acl_class`表中创建新行时,`JdbcMutableAclService`必须能够检索这些数据。它有两个属性,它们定义了检索这些值`classIdentityQuery`和`sidIdentityQuery`所需的 SQL。这两个都默认为`call identity()` + +ACL 工件 JAR 包含用于在 HypersQL、PostgreSQL、MySQL/MariaDB、Microsoft SQL Server 和 Oracle 数据库中创建 ACL 模式的文件。这些模式还将在下面的小节中进行演示。 + +### Hypersql + +默认模式与框架内的单元测试中使用的嵌入式 HSQLDB 数据库一起工作。 + +``` +create table acl_sid( + id bigint generated by default as identity(start with 100) not null primary key, + principal boolean not null, + sid varchar_ignorecase(100) not null, + constraint unique_uk_1 unique(sid,principal) +); + +create table acl_class( + id bigint generated by default as identity(start with 100) not null primary key, + class varchar_ignorecase(100) not null, + constraint unique_uk_2 unique(class) +); + +create table acl_object_identity( + id bigint generated by default as identity(start with 100) not null primary key, + object_id_class bigint not null, + object_id_identity varchar_ignorecase(36) not null, + parent_object bigint, + owner_sid bigint, + entries_inheriting boolean not null, + constraint unique_uk_3 unique(object_id_class,object_id_identity), + constraint foreign_fk_1 foreign key(parent_object)references acl_object_identity(id), + constraint foreign_fk_2 foreign key(object_id_class)references acl_class(id), + constraint foreign_fk_3 foreign key(owner_sid)references acl_sid(id) +); + +create table acl_entry( + id bigint generated by default as identity(start with 100) not null primary key, + acl_object_identity bigint not null, + ace_order int not null, + sid bigint not null, + mask integer not null, + granting boolean not null, + audit_success boolean not null, + audit_failure boolean not null, + constraint unique_uk_4 unique(acl_object_identity,ace_order), + constraint foreign_fk_4 foreign key(acl_object_identity) references acl_object_identity(id), + constraint foreign_fk_5 foreign key(sid) references acl_sid(id) +); +``` + +### PostgreSQL + +``` +create table acl_sid( + id bigserial not null primary key, + principal boolean not null, + sid varchar(100) not null, + constraint unique_uk_1 unique(sid,principal) +); + +create table acl_class( + id bigserial not null primary key, + class varchar(100) not null, + constraint unique_uk_2 unique(class) +); + +create table acl_object_identity( + id bigserial primary key, + object_id_class bigint not null, + object_id_identity varchar(36) not null, + parent_object bigint, + owner_sid bigint, + entries_inheriting boolean not null, + constraint unique_uk_3 unique(object_id_class,object_id_identity), + constraint foreign_fk_1 foreign key(parent_object)references acl_object_identity(id), + constraint foreign_fk_2 foreign key(object_id_class)references acl_class(id), + constraint foreign_fk_3 foreign key(owner_sid)references acl_sid(id) +); + +create table acl_entry( + id bigserial primary key, + acl_object_identity bigint not null, + ace_order int not null, + sid bigint not null, + mask integer not null, + granting boolean not null, + audit_success boolean not null, + audit_failure boolean not null, + constraint unique_uk_4 unique(acl_object_identity,ace_order), + constraint foreign_fk_4 foreign key(acl_object_identity) references acl_object_identity(id), + constraint foreign_fk_5 foreign key(sid) references acl_sid(id) +); +``` + +你必须将`classIdentityQuery`和`sidIdentityQuery`的属性分别设置为以下值: + +* `select currval(pg_get_serial_sequence('acl_class', 'id'))` + +* `select currval(pg_get_serial_sequence('acl_sid', 'id'))` + +### MySQL 和 MariaDB + +``` +CREATE TABLE acl_sid ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + principal BOOLEAN NOT NULL, + sid VARCHAR(100) NOT NULL, + UNIQUE KEY unique_acl_sid (sid, principal) +) ENGINE=InnoDB; + +CREATE TABLE acl_class ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + class VARCHAR(100) NOT NULL, + UNIQUE KEY uk_acl_class (class) +) ENGINE=InnoDB; + +CREATE TABLE acl_object_identity ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + object_id_class BIGINT UNSIGNED NOT NULL, + object_id_identity VARCHAR(36) NOT NULL, + parent_object BIGINT UNSIGNED, + owner_sid BIGINT UNSIGNED, + entries_inheriting BOOLEAN NOT NULL, + UNIQUE KEY uk_acl_object_identity (object_id_class, object_id_identity), + CONSTRAINT fk_acl_object_identity_parent FOREIGN KEY (parent_object) REFERENCES acl_object_identity (id), + CONSTRAINT fk_acl_object_identity_class FOREIGN KEY (object_id_class) REFERENCES acl_class (id), + CONSTRAINT fk_acl_object_identity_owner FOREIGN KEY (owner_sid) REFERENCES acl_sid (id) +) ENGINE=InnoDB; + +CREATE TABLE acl_entry ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + acl_object_identity BIGINT UNSIGNED NOT NULL, + ace_order INTEGER NOT NULL, + sid BIGINT UNSIGNED NOT NULL, + mask INTEGER UNSIGNED NOT NULL, + granting BOOLEAN NOT NULL, + audit_success BOOLEAN NOT NULL, + audit_failure BOOLEAN NOT NULL, + UNIQUE KEY unique_acl_entry (acl_object_identity, ace_order), + CONSTRAINT fk_acl_entry_object FOREIGN KEY (acl_object_identity) REFERENCES acl_object_identity (id), + CONSTRAINT fk_acl_entry_acl FOREIGN KEY (sid) REFERENCES acl_sid (id) +) ENGINE=InnoDB; +``` + +### Microsoft SQL Server + +``` +CREATE TABLE acl_sid ( + id BIGINT NOT NULL IDENTITY PRIMARY KEY, + principal BIT NOT NULL, + sid VARCHAR(100) NOT NULL, + CONSTRAINT unique_acl_sid UNIQUE (sid, principal) +); + +CREATE TABLE acl_class ( + id BIGINT NOT NULL IDENTITY PRIMARY KEY, + class VARCHAR(100) NOT NULL, + CONSTRAINT uk_acl_class UNIQUE (class) +); + +CREATE TABLE acl_object_identity ( + id BIGINT NOT NULL IDENTITY PRIMARY KEY, + object_id_class BIGINT NOT NULL, + object_id_identity VARCHAR(36) NOT NULL, + parent_object BIGINT, + owner_sid BIGINT, + entries_inheriting BIT NOT NULL, + CONSTRAINT uk_acl_object_identity UNIQUE (object_id_class, object_id_identity), + CONSTRAINT fk_acl_object_identity_parent FOREIGN KEY (parent_object) REFERENCES acl_object_identity (id), + CONSTRAINT fk_acl_object_identity_class FOREIGN KEY (object_id_class) REFERENCES acl_class (id), + CONSTRAINT fk_acl_object_identity_owner FOREIGN KEY (owner_sid) REFERENCES acl_sid (id) +); + +CREATE TABLE acl_entry ( + id BIGINT NOT NULL IDENTITY PRIMARY KEY, + acl_object_identity BIGINT NOT NULL, + ace_order INTEGER NOT NULL, + sid BIGINT NOT NULL, + mask INTEGER NOT NULL, + granting BIT NOT NULL, + audit_success BIT NOT NULL, + audit_failure BIT NOT NULL, + CONSTRAINT unique_acl_entry UNIQUE (acl_object_identity, ace_order), + CONSTRAINT fk_acl_entry_object FOREIGN KEY (acl_object_identity) REFERENCES acl_object_identity (id), + CONSTRAINT fk_acl_entry_acl FOREIGN KEY (sid) REFERENCES acl_sid (id) +); +``` + +### Oracle 数据库 + +``` +CREATE TABLE ACL_SID ( + ID NUMBER(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; +``` + +## OAuth2.0 客户端模式 + +[OAuth2 授权客户服务](../oauth2/client/core.html#oauth2Client-authorized-repo-service)(`JdbcOAuth2AuthorizedClientService`)的 JDBC 实现需要一个表来持久化`OAuth2AuthorizedClient`(s)。你将需要调整此模式以匹配你正在使用的数据库方言。 + +``` +CREATE TABLE oauth2_authorized_client ( + client_registration_id varchar(100) NOT NULL, + principal_name varchar(200) NOT NULL, + access_token_type varchar(100) NOT NULL, + access_token_value blob NOT NULL, + access_token_issued_at timestamp NOT NULL, + access_token_expires_at timestamp NOT NULL, + access_token_scopes varchar(1000) DEFAULT NULL, + refresh_token_value blob DEFAULT NULL, + refresh_token_issued_at timestamp DEFAULT NULL, + created_at timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL, + PRIMARY KEY (client_registration_id, principal_name) +); +``` + +[Appendix](index.html)[XML 命名空间](namespace/index.html) + diff --git a/docs/spring-security/servlet-appendix-faq.md b/docs/spring-security/servlet-appendix-faq.md new file mode 100644 index 0000000000000000000000000000000000000000..7a49740cb0372ab104d09e0aebd78898c80c5775 --- /dev/null +++ b/docs/spring-security/servlet-appendix-faq.md @@ -0,0 +1,535 @@ +# Spring 安全常见问题解答 + +* [一般性问题](#appendix-faq-general-questions) + +* [常见问题](#appendix-faq-common-problems) + +* [Spring 安全架构问题](#appendix-faq-architecture) + +* [共同的“howto”请求](#appendix-faq-howto) + +## 一般性问题 + +1. [安全性是否会满足我所有的应用程序安全性要求?](#appendix-faq-other-concerns) + +2. [为什么不直接使用 web.xml 安全性呢?](#appendix-faq-web-xml) + +3. [需要哪些 爪哇 和 Spring Framework 版本?](#appendix-faq-requirements) + +4. [I’m new to Spring Security and I need to build an application that supports CAS single sign-on over HTTPS, while allowing Basic authentication locally for certain URLs, authenticating against multiple back end user information sources (LDAP and JDBC).我复制了一些我发现的配置文件,但它不起作用。](#appendix-faq-start-simple) + +### Will Spring Security take care of all my application security requirements? + +Spring 安全性为你的身份验证和授权需求提供了一个非常灵活的框架,但是对于构建安全应用程序,还有许多其他考虑因素不在其范围内。Web 应用程序容易受到你应该熟悉的各种攻击,最好是在开始开发之前,这样你就可以从一开始就将它们设计和编写代码。查看[OWASP 网站](https://www.owasp.org/),了解有关 Web 应用程序开发人员面临的主要问题以及你可以针对他们使用的对策的信息。 + +### 为什么不直接使用 web.xml 安全性呢? + +让我们假设你正在基于 Spring 开发一个 Enterprise 应用程序。通常需要解决四个安全问题:身份验证、Web 请求安全、服务层安全(即实现业务逻辑的方法)和域对象实例安全(即不同的域对象具有不同的权限)。考虑到这些典型的需求: + +1. *认证*: Servlet 规范提供了一种身份验证方法。但是,你将需要配置容器以执行身份验证,这通常需要编辑特定于容器的“领域”设置。这使得配置不可移植,如果你需要编写一个实际的 爪哇 类来实现容器的身份验证接口,那么它就变得更加不可移植。有了 Spring 安全性,你就实现了完全的可移植性---直接到战争级别。 Spring 安全性还提供了经过生产验证的身份验证提供者和机制的选择,这意味着你可以在部署时切换身份验证方法。对于编写需要在未知目标环境中工作的产品的软件供应商来说,这一点尤其有价值。 + +2. *Web 请求安全性:* Servlet 规范提供了一种方法来保护你的请求 URI。然而,这些 URI 只能以 Servlet 规范自身的有限 URI 路径格式表示。 Spring 安全性提供了一种更全面的方法。例如,你可以使用 Ant 路径或正则表达式,你可以考虑 URI 的一部分,而不仅仅是所请求的页面(例如,你可以考虑 HTTP GET 参数),并且你可以实现自己的配置数据的运行时源。这意味着你的 Web 请求安全性可以在 WebApp 的实际执行过程中动态地进行更改。 + +3. *服务层和域对象安全性:* Servlet 规范中缺乏对服务层安全性或域对象实例安全性的支持,这代表了多层应用程序的严重限制。通常,开发人员要么忽略这些需求,要么在他们的 MVC 控制器代码中实现安全逻辑(甚至更糟的是,在视图中)。这种方法有严重的缺点: + + 1. *关注的分离:*授权是一个横切关注点,应该作为横切关注点来实现。实现授权代码的 MVC 控制器或视图使得对控制器和授权逻辑的测试更加困难,调试更加困难,并且通常会导致代码重复。 + + 2. *对富客户机和 Web 服务的支持:*如果最终必须支持另一种客户机类型,则嵌入在 Web 层中的任何授权代码都是不可重用的。应该考虑的是 Spring Remoting Exports 只输出服务层 bean(而不是 MVC 控制器)。因为这样的授权逻辑需要位于服务层中,以支持多个客户机类型。 + + 3. *分层问题:*MVC 控制器或视图仅仅是不正确的架构层,用于实现有关服务层方法或域对象实例的授权决策。虽然主体可以传递给服务层以使其能够做出授权决策,但这样做将在每个服务层方法上引入一个额外的参数。一种更优雅的方法是使用 ThreadLocal 来保存主体,尽管这可能会将开发时间增加到简单地使用专用安全框架会更经济(在成本效益的基础上)的程度。 + + 4. *授权代码质量:*人们常说 Web 框架“使做正确的事情变得更容易,做错误的事情变得更难”。安全框架也是一样,因为它们是以抽象的方式设计的,目的很广泛。从零开始编写自己的授权代码并不能提供框架所提供的“设计检查”,而内部授权代码通常不会像广泛部署、同行评审和新版本那样得到改进。 + +对于简单的应用程序, Servlet 规范安全性可能就足够了。尽管在考虑 Web 容器可移植性、配置需求、有限的 Web 请求安全灵活性以及不存在的服务层和域对象实例安全性的情况下,开发人员经常寻找替代解决方案的原因是显而易见的。 + +### What 爪哇 and Spring Framework versions are required? + +Spring 安全性 3.0 和 3.1 至少需要 JDK1.5,并且至少也需要 Spring 3.0.3。理想情况下,你应该使用最新的发布版本,以避免出现问题。 + +Spring Security2.0.x 至少需要 1.4 的 JDK 版本,并且是针对 Spring 2.0.x 构建的。它还应该与使用 Spring 2.5.x 的应用程序兼容。 + +### . I’ve copied some configuration files I found but it doesn’t work. + +可能出什么问题了? + +或者替代另一种复杂的场景… + +实际上,在成功地使用它们构建应用程序之前,你需要了解打算使用的技术。安全是复杂的。使用登录表单设置一个简单的配置,而使用 Spring Security 的名称空间设置一些硬编码的用户是相当简单的。转向使用 Backed JDBC 数据库也很容易。但是,如果你尝试直接跳到这样一个复杂的部署场景,你几乎肯定会感到沮丧。在建立 CAS 等系统、配置 LDAP 服务器和正确安装 SSL 证书所需的学习曲线中,有一个很大的跳跃。因此,你需要一步一步地做事情。 + +从 Spring 安全的角度来看,你应该做的第一件事是遵循网站上的“入门”指南。这将需要你通过一系列步骤来启动和运行,并了解框架如何运行。如果你使用的是你 AREN 不熟悉的其他技术,那么你应该做一些研究,并尝试确保在将它们组合到一个复杂的系统中之前可以单独使用它们。 + +## 常见问题 + +1. 认证 + + 1. [当我尝试登录时,我会收到一条错误消息,上面写着“不良凭证”。怎么了?](#appendix-faq-bad-credentials) + + 2. [当我尝试登录时,我的应用程序进入了一个“死循环”,这是怎么回事?](#appendix-faq-login-loop) + + 3. [我得到了一个异常消息“访问被拒绝(用户是匿名的);”。怎么了?](#appendix-faq-anon-access-denied) + + 4. [为什么我在退出应用程序后仍然可以看到一个安全的页面?](#appendix-faq-cached-secure-page) + + 5. [我得到了一个异常消息,即“在 SecurityContext 中未找到身份验证对象”。怎么了?](#auth-exception-credentials-not-found) + + 6. [我无法让 LDAP 身份验证工作。](#appendix-faq-ldap-authentication) + +2. 会话管理 + + 1. [我正在使用 Spring Security 的并发会话控件,以防止用户一次登录多次。](#appendix-faq-concurrent-session-same-browser) + + 2. [为什么当我通过 Spring 安全性进行身份验证时,会话ID 会发生变化?](#appendix-faq-new-session-on-authentication) + + 3. [I’m using Tomcat (or some other servlet container) 并为我的登录页面启用了 HTTPS,之后切换回 HTTP。](#appendix-faq-tomcat-https-session) + + 4. [我正在尝试使用 Concurrent会话-Control 支持,但它不允许我重新登录,即使我确定我已经退出并且没有超过允许的会话。](#appendix-faq-session-listener-missing) + + 5. [Spring 安全性正在某个地方创建一个会话,即使我已经将它配置为不是,通过将 create-会话属性设置为 never。](#appendix-faq-unwanted-session-creation) + +3. 杂项 + + 1. [我在发帖时收到了一封 403 的禁止信。](#appendix-faq-forbidden-csrf) + + 2. [我正在使用 RequestDispatcher 将一个请求转发到另一个 URL,但是我的安全约束 AREN 没有被应用。](#appendix-faq-no-security-on-forward) + + 3. [I have added Spring Security’s \ element to my application context but if I add security annotations to my Spring MVC controller beans (Struts actions etc.) 那么,它们似乎并没有产生效果。](#appendix-faq-method-security-in-web-context) + + 4. [我有一个用户肯定已经通过了身份验证,但是当我在某些请求中尝试访问 SecurityContextholder 时,身份验证是空的。](#appendix-faq-no-filters-no-context) + + 5. [当使用 URL 属性时,Authorized JSP 标记不尊重我的方法安全注释。](#appendix-faq-method-security-with-taglib) + +### 当我尝试登录时,我会收到一条错误消息,上面写着“不良凭证”。怎么了? + +这意味着身份验证失败。它没有说明原因,因为避免提供可能有助于攻击者猜测帐户名称或密码的详细信息是一种好的做法。 + +这也意味着,如果你在论坛上问这个问题,除非你提供额外的信息,否则你不会得到答案。对于任何问题,你都应该检查调试日志的输出,注意任何异常堆栈跟踪和相关消息。通过调试器中的代码,查看身份验证在哪里失败以及为什么失败。编写一个测试用例,在应用程序之外执行你的身份验证配置。通常情况下,故障是由于存储在数据库中的密码数据与用户输入的密码数据存在差异。如果使用散列密码,请确保存储在数据库中的值*完全正确*与应用程序中配置的`PasswordEncoder`产生的值相同。 + +### 当我尝试登录时,我的应用程序进入了一个“死循环”,这是怎么回事? + +无限循环和重定向到登录页面的常见用户问题是由于意外地将登录页面配置为“安全”资源而引起的。确保你的配置允许匿名访问登录页面,可以将其从安全筛选链中排除,也可以将其标记为需要角色 \_Anonymous。 + +如果你的 AccessDecisionManager 包含一个身份验证投票器,则可以使用属性“is\_Authenticated\_Anonymouly”。如果你使用标准的名称空间配置设置,这将自动可用。 + +从 Spring Security2.0.1 开始,当你使用基于名称空间的配置时,将在加载应用程序上下文时进行检查,如果你的登录页面似乎受到保护,则将记录一条警告消息。 + +### ;".怎么了? + +这是一条调试级别的消息,匿名用户第一次尝试访问受保护的资源时会发生该消息。 + +``` +DEBUG [ExceptionTranslationFilter] - Access is denied (user is anonymous); redirecting to authentication entry point +org.springframework.security.AccessDeniedException: Access is denied +at org.springframework.security.vote.AffirmativeBased.decide(AffirmativeBased.java:68) +at org.springframework.security.intercept.AbstractSecurityInterceptor.beforeInvocation(AbstractSecurityInterceptor.java:262) +``` + +这是正常的,不应该有什么可担心的。 + +### 为什么我在退出应用程序后仍然可以看到一个安全的页面? + +最常见的原因是,你的浏览器已经缓存了该页面,并且你正在看到一个副本,该副本正在从浏览器缓存中检索。通过检查浏览器是否真的在发送请求来验证这一点(检查你的服务器访问日志、调试日志,或者使用合适的浏览器调试插件,例如 Firefox 的“Tamper Data”)。这与 Spring 安全性无关,你应该将你的应用程序或服务器配置为设置适当的`Cache-Control`响应头。请注意,SSL 请求永远不会被缓存。 + +### 我得到了一个异常消息,即“在 SecurityContext 中未找到身份验证对象”。怎么了? + +这是另一种调试级别的消息,它发生在匿名用户第一次尝试访问受保护的资源时,但在你的筛选链配置中没有`AnonymousAuthenticationFilter`时。 + +``` +DEBUG [ExceptionTranslationFilter] - Authentication exception occurred; redirecting to authentication entry point +org.springframework.security.AuthenticationCredentialsNotFoundException: + An Authentication object was not found in the SecurityContext +at org.springframework.security.intercept.AbstractSecurityInterceptor.credentialsNotFound(AbstractSecurityInterceptor.java:342) +at org.springframework.security.intercept.AbstractSecurityInterceptor.beforeInvocation(AbstractSecurityInterceptor.java:254) +``` + +这是正常的,不应该有什么可担心的。 + +### 我无法让 LDAP 身份验证工作。 + +我的配置有什么问题? + +请注意,LDAP 目录的权限通常不允许你读取用户的密码。因此,通常不可能使用[什么是 UserDetailsService,我需要一个吗?](#appendix-faq-what-is-userdetailservice),其中 Spring 安全性将存储的密码与用户提交的密码进行比较。最常见的方法是使用 LDAP“bind”,这是[LDAP 协议](https://en.wikipedia.org/wiki/Lightweight_Directory_Access_Protocol)支持的操作之一。 Spring 使用这种方法,Security 通过尝试以用户身份对目录进行身份验证来验证密码。 + +LDAP 身份验证最常见的问题是缺乏对目录服务器树结构和配置的了解。这在不同的公司会有所不同,所以你必须自己去发现它。在向应用程序添加 Spring 安全性 LDAP 配置之前,使用标准的 爪哇 LDAP 代码编写一个简单的测试(不涉及 Spring 安全性)是一个好主意,并确保你可以首先让它工作。例如,要对用户进行身份验证,你可以使用以下代码: + +Java + +``` +@Test +public void ldapAuthenticationIsSuccessful() throws Exception { + Hashtable env = new Hashtable(); + env.put(Context.SECURITY_AUTHENTICATION, "simple"); + env.put(Context.SECURITY_PRINCIPAL, "cn=joe,ou=users,dc=mycompany,dc=com"); + env.put(Context.PROVIDER_URL, "ldap://mycompany.com:389/dc=mycompany,dc=com"); + env.put(Context.SECURITY_CREDENTIALS, "joespassword"); + env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); + + InitialLdapContext ctx = new InitialLdapContext(env, null); + +} +``` + +Kotlin + +``` +@Test +fun ldapAuthenticationIsSuccessful() { + val env = Hashtable() + env[Context.SECURITY_AUTHENTICATION] = "simple" + env[Context.SECURITY_PRINCIPAL] = "cn=joe,ou=users,dc=mycompany,dc=com" + env[Context.PROVIDER_URL] = "ldap://mycompany.com:389/dc=mycompany,dc=com" + env[Context.SECURITY_CREDENTIALS] = "joespassword" + env[Context.INITIAL_CONTEXT_FACTORY] = "com.sun.jndi.ldap.LdapCtxFactory" + val ctx = InitialLdapContext(env, null) +} +``` + +### 会话管理 + +会话管理问题是论坛问题的一个常见来源。如果你正在开发 Java Web 应用程序,那么你应该了解如何在 Servlet 容器和用户的浏览器之间维护会话。你还应该理解安全和非安全 Cookie 之间的区别,以及使用 HTTP/HTTPS 和在两者之间切换的含义。 Spring 安全性与维护会话或提供会话标识符无关。这完全由 Servlet 容器处理。 + +### I’m using Spring Security’s concurrent session control to prevent users from logging in more than once at a time. + +当我在登录后打开另一个浏览器窗口时,它并不会阻止我再次登录。为什么我可以登录不止一次? + +浏览器通常为每个浏览器实例维护一个会话。你不能同时进行两个单独的疗程。因此,如果你再次在另一个窗口或选项卡中登录,那么你只是在同一窗口中重新进行身份验证会话。服务器对选项卡、Windows 或浏览器实例一无所知。它只看到 HTTP 请求,并根据它们所包含的 JSessionID cookie 的值将这些请求与特定的会话绑定。当用户在会话期间进行身份验证时, Spring Security 的并发会话控件检查他们拥有的*其他经过身份验证的会话*的数量。如果已经用相同的方法对它们进行了身份验证,那么重新进行身份验证将不会有任何效果。 + +### Why does the session Id change when I authenticate through Spring Security? + +使用默认配置, Spring Security 在用户进行身份验证时会更改会话ID。如果你使用的是 Servlet 3.1 或更新的容器,只需更改会话ID 即可。如果你使用的是旧的容器, Spring Security 会使现有的会话失效,创建一个新的会话,并将会话数据传输到新的会话。以这种方式更改会话标识符可防止“会话-fixing”攻击。你可以在网上和参考手册中找到更多有关此的信息。 + +### and have enabled HTTPS for my login page, switching back to HTTP afterwards. + +它不起作用——我只是在进行了身份验证后回到了登录页面。 + +这是因为在 HTTPS 下创建的会话(会话cookie 被标记为“secure”)随后不能在 HTTP 下使用。浏览器不会将 cookie 发送回服务器,并且任何会话状态都将丢失(包括安全上下文信息)。首先在 HTTP 中启动会话应该可以工作,因为会话cookie 不会被标记为安全。然而, Spring 安全性的[Session Fixation Protection](https://docs.spring.io/spring-security/site/docs/3.1.x/reference/springsecurity-single.html#ns-session-fixation)可能会干扰这一点,因为它会导致新的会话ID cookie 被发送回用户的浏览器,通常带有安全标志。为了避免这种情况,你可以禁用会话固定保护,但是在较新的 Servlet 容器中,你还可以配置会话cookie,使其永远不使用安全标志。请注意,在 HTTP 和 HTTPS 之间切换通常不是一个好主意,因为任何使用 HTTP 的应用程序都容易受到中间人攻击。为了真正的安全,用户应该开始使用 HTTPS 访问你的网站,并继续使用它,直到他们退出。甚至从通过 HTTP 访问的页面中点击 HTTPS 链接也可能存在风险。如果你需要更多的说服力,请查看[sslstrip](https://github.com/moxie0/sslstrip/)之类的工具。 + +### 我没有在 HTTP 和 HTTPS 之间切换,但我的会话仍然迷路了。 + +会话可以通过交换会话cookie 或向 URL 添加`jsessionid`参数来维护(如果你使用 JSTL 输出 URL,或者在 URL 上调用`HttpServletResponse.encodeUrl`(例如,在重定向之前),这会自动发生。如果客户机禁用了 cookie,并且你没有重写 URL 以包含`jsessionid`,那么会话将丢失。请注意,出于安全原因,Cookie 的使用是首选的,因为它不会公开 URL 中的会话信息。 + +### I’m trying to use the concurrent session-control support but it won’t let me log back in, even if I’m sure I’ve logged out and haven’t exceeded the allowed sessions. + +确保已将侦听器添加到 web.xml 文件中。必须确保在会话被销毁时通知 Spring 安全会话登记册。没有它,会话信息将不会从注册表中删除。 + +``` + + org.springframework.security.web.session.HttpSessionEventPublisher + +``` + +### Spring Security is creating a session somewhere, even though I’ve configured it not to, by setting the create-session attribute to never. + +这通常意味着用户的应用程序正在某个地方创建一个会话,但他们 AREN 不知道这一点。最常见的罪魁祸首是 JSP。许多 AREN 的人并不知道 JSP 默认情况下会创建会话。要防止 JSP 创建会话,请将指令`<%@ page session="false" %>`添加到页面的顶部。 + +如果你在确定在哪里创建了会话时遇到了困难,那么可以添加一些调试代码来跟踪位置。一种方法是将`javax.servlet.http.HttpSessionListener`添加到应用程序中,该应用程序在`sessionCreated`方法中调用`Thread.dumpStack()`。 + +### 我在发帖时收到了一封 403 的禁止信。 + +如果 HTTP POST 返回了一个禁止的 HTTP403,但是对于 HTTPGET 有效,那么这个问题很可能与有关。提供 CSRF 令牌或禁用 CSRF 保护(不建议使用)。 + +### 我正在使用 RequestDispatcher 将一个请求转发到另一个 URL,但是我的安全约束 AREN 没有被应用。 + +过滤器在默认情况下不应用于转发或包含。如果确实希望将安全过滤器应用于转发和/或包含,则必须使用 \元素(\的子元素)在 web.xml 中显式地配置这些过滤器。 + +### then they don’t seem to have an effect. + +在 Spring Web 应用程序中,为 Dispatcher Servlet 保存 Spring MVC bean 的应用程序上下文通常与主应用程序上下文分离。它通常在一个名为`myapp-servlet.xml`的文件中定义,其中“myapp”是在`web.xml`中分配给 Spring `DispatcherServlet`的名称。一个应用程序可以有多个`DispatcherServlet`s,每个都有自己独立的应用程序上下文。这些“子”上下文中的 bean 对应用程序的其余部分不可见。“父”应用程序上下文由你在`web.xml`中定义的`ContextLoaderListener`加载,并且对所有子上下文都是可见的。这个父上下文通常是你定义安全配置的地方,包括``元素)。因此,不会强制执行应用于这些 Web bean 中的方法的任何安全约束,因为无法从`DispatcherServlet`上下文中看到这些 bean。你需要将``声明移动到 Web 上下文中,或者将你想要保护的 bean 移动到主应用程序上下文中。 + +通常,我们建议在服务层而不是在单个 Web 控制器上应用方法安全性。 + +### 我有一个用户肯定已经通过了身份验证,但是当我在某些请求中尝试访问 SecurityContextholder 时,身份验证是空的。 + +为什么我看不到用户信息? + +如果你使用与 URL 模式匹配的``元素中的属性`filters='none'`从安全筛选链中排除了该请求,那么将不会为该请求填充`SecurityContextHolder`。检查调试日志以查看请求是否正在通过筛选链。(你正在读取调试日志,对吗?)。 + +### 当使用 URL 属性时,Authorized JSP 标记不尊重我的方法安全注释。 + +当使用``中的`url`属性时,方法安全性不会隐藏链接,因为我们无法轻松地反向工程将什么 URL 映射到什么控制器端点,因为控制器可以依靠头、当前用户等来确定调用什么方法。 + +## Spring Security Architecture Questions + +1. [我如何知道 X 类在哪个包中?](#appendix-faq-where-is-class-x) + +2. [名称空间元素如何映射到传统的 Bean 配置?](#appendix-faq-namespace-to-bean-mapping) + +3. [“role\_”是什么意思,为什么我需要它在我的角色名称?](#appendix-faq-role-prefix) + +4. [我如何知道应该将哪些依赖项添加到我的应用程序中以使用 Spring 安全性?](#appendix-faq-what-dependencies) + +5. [运行嵌入式 ApacheDS LDAP 服务器需要哪些依赖项?](#appendix-faq-apacheds-deps) + +6. [什么是 UserDetailsService,我需要一个吗?](#appendix-faq-what-is-userdetailservice) + +### 我如何知道 X 类在哪个包中? + +定位类的最佳方法是在 IDE 中安装 Spring 安全源。该分布包括项目被划分到的每个模块的源 JAR。将这些添加到项目源路径中,你就可以直接导航到 Spring 安全类(`Ctrl-Shift-T`在 Eclipse 中)。这也使调试变得更容易,并允许你通过直接查看发生异常的代码来解决异常问题,从而了解发生了什么。 + +### How do the namespace elements map to conventional bean configurations? + +在参考指南的命名空间附录中,对命名空间创建的 bean 有一个大致的概述。在[blog.springsource.com](https://spring.io/blog/2010/03/06/behind-the-spring-security-namespace/)上还有一篇名为“ Spring 安全命名空间背后”的详细博客文章。如果想知道完整的详细信息,那么代码在 Spring Security3.0 发行版中的`spring-security-config`模块中。你可能应该先阅读标准 Spring 框架参考文档中有关名称空间解析的章节。 + +### “role\_”是什么意思,为什么我需要它在我的角色名称? + +Spring 安全性具有基于投票人的体系结构,这意味着访问决策是由一系列`AccessDecisionVoter`s 作出的。投票者根据为安全资源(例如方法调用)指定的“配置属性”进行操作。使用这种方法,并不是所有属性都与所有投票者相关,投票者需要知道什么时候应该忽略一个属性(弃权),什么时候应该基于属性值投票授予或拒绝访问。最常见的投票者是`RoleVoter`,在默认情况下,每当它发现带有“role\_”前缀的属性时,它都会投票。它将属性(例如“role\_user”)与当前用户被分配的权限的名称进行简单的比较。如果它找到一个匹配的(他们有一个名为“role\_user”的权限),它将投票授予访问权限,否则将投票拒绝访问。 + +可以通过设置`RoleVoter`的`rolePrefix`属性来更改前缀。如果你只需要在应用程序中使用角色,而不需要其他自定义投票者,那么你可以将前缀设置为一个空白字符串,在这种情况下,`RoleVoter`将把所有属性都视为角色。 + +### How do I know which dependencies to add to my application to work with Spring Security? + +这将取决于你正在使用的功能以及正在开发的应用程序类型。在 Spring Security3.0 中,项目 JAR 被划分为明显不同的功能区域,因此很容易从应用程序需求中找出你需要的 Spring 安全性 JAR。所有应用程序都需要`spring-security-core`jar。如果你正在开发一个 Web 应用程序,那么你需要`spring-security-web`jar。如果使用安全名称空间配置,则需要`spring-security-config`JAR,对于 LDAP 支持,则需要`spring-security-ldap`JAR 等等。 + +对于第三方罐子来说,情况并不总是那么明显。一个很好的起点是从一个预构建的示例应用程序 WEB-INF/lib 目录中复制这些应用程序。对于基本的应用程序,你可以从教程示例开始。如果你希望在嵌入式测试服务器中使用 LDAP,那么可以使用 LDAP 示例作为起点。参考手册还包括[附录](https://docs.spring.io/spring-security/site/docs/5.6.2/reference/html5/#modules)列出了每个 Spring 安全模块的第一级依赖关系,并提供了有关它们是否是可选的以及它们需要做什么的一些信息。 + +如果你使用 Maven 构建你的项目,那么将适当的 Spring 安全模块作为依赖项添加到 POM.xml 将自动获取框架所需的核心 JAR。在 Spring Security POM 文件中被标记为“可选的”的任何文件,如果你需要它们,都必须添加到你自己的 POM.xml 文件中。 + +### 运行嵌入式 ApacheDS LDAP 服务器需要哪些依赖项? + +如果正在使用 Maven,则需要在 POM 依赖项中添加以下内容: + +``` + + org.apache.directory.server + apacheds-core + 1.5.5 + runtime + + + org.apache.directory.server + apacheds-server-jndi + 1.5.5 + runtime + +``` + +其他所需的罐子应以传递式方式拉入。 + +### 什么是 UserDetailsService,我需要一个吗? + +`UserDetailsService`是用于加载特定于用户帐户的数据的 DAO 接口。它没有其他功能来加载该数据,以供框架内的其他组件使用。它不负责对用户进行身份验证。使用用户名/密码组合对用户进行身份验证最常见的方法是`DaoAuthenticationProvider`,它被注入`UserDetailsService`,以允许它为用户加载密码(和其他数据),以便将其与提交的值进行比较。请注意,如果你正在使用 LDAP,[这种方法可能行不通。](#appendix-faq-ldap-authentication)。 + +如果你想定制身份验证过程,那么你应该自己实现`AuthenticationProvider`。有关集成 Spring 安全性验证和 Google App Engine 的示例,请参见[博客文章](https://spring.io/blog/2010/08/02/spring-security-in-google-app-engine/)。 + +## 共同的“howto”请求 + +1. [我需要登录更多的信息,而不仅仅是用户名。](#appendix-faq-extra-login-fields) + +2. [在只有请求的 URL 的分段值不同(例如/foo#bar 和/foo#blah)的情况下,如何应用不同的截取 URL 约束?](#appendix-faq-matching-url-fragments) + +3. [如何在 UserDetailsService 中访问用户的 IP 地址(或其他 Web 请求数据)?](#appendix-faq-request-details-in-user-service) + +4. [我如何从 UserDetailsService 访问 HttpSession?](#appendix-faq-access-session-from-user-service) + +5. [我如何在用户详细服务中访问用户的密码?](#appendix-faq-password-in-user-service) + +6. [如何在应用程序中动态地定义安全的 URL?](#appendix-faq-dynamic-url-metadata) + +7. [如何针对 LDAP 进行身份验证,并从数据库加载用户角色?](#appendix-faq-ldap-authorities) + +8. [我想修改由名称空间创建的 Bean 的属性,但是模式中没有支持它的内容。](#appendix-faq-namespace-post-processor) + +### 我需要登录更多的信息,而不仅仅是用户名。 + +如何添加对额外登录字段(例如公司名称)的支持? + +这个问题在 Spring 安全论坛中反复出现,因此你将通过搜索文档(或通过 Google)在那里找到更多信息。 + +提交的登录信息由`UsernamePasswordAuthenticationFilter`实例处理。你将需要自定义这个类来处理额外的数据字段。一种选择是使用你自己定制的身份验证令牌类(而不是标准的`UsernamePasswordAuthenticationToken`),另一种选择是简单地将额外的字段与用户名连接起来(例如,使用一个“:”作为分隔符),并将它们传递到`UsernamePasswordAuthenticationToken`的用户名属性中。 + +你还需要自定义实际的身份验证过程。例如,如果你使用自定义身份验证令牌类,则必须编写`AuthenticationProvider`来处理它(或扩展标准`DaoAuthenticationProvider`)。如果你已将这些字段串联起来,则可以实现你自己的`UserDetailsService`,它将它们拆分并加载适当的用户数据以进行身份验证。 + +### 在只有请求的 URL 的分段值不同(例如/foo#bar 和/foo#blah)的情况下,如何应用不同的截取 URL 约束? + +你不能这样做,因为片段不会从浏览器传输到服务器。从服务器的角度来看,上面的 URL 是相同的。这是 GWT 用户的一个常见问题。 + +### 在用户详细服务中? + +显然,你不能(在不求助于线程局部变量的情况下),因为提供给接口的唯一信息是用户名。与其实现`UserDetailsService`,不如直接实现`AuthenticationProvider`,并从提供的`Authentication`令牌中提取信息。 + +在标准的 Web 设置中,`Authentication`对象上的`getDetails()`方法将返回`WebAuthenticationDetails`的实例。如果需要其他信息,可以将自定义`AuthenticationDetailsSource`插入到正在使用的身份验证筛选器中。如果你使用名称空间,例如使用``元素,那么你应该删除该元素,并将其替换为``声明,该声明指向显式配置的`UsernamePasswordAuthenticationFilter`。 + +### 我如何从 UserDetailsService 访问 HttpSession? + +你不能,因为`UserDetailsService`没有意识到 Servlet API。如果你想存储自定义用户数据,那么你应该自定义返回的`UserDetails`对象。然后可以在任何点通过线程本地`SecurityContextHolder`访问此内容。调用`SecurityContextHolder.getContext().getAuthentication().getPrincipal()`将返回此自定义对象。 + +如果你确实需要访问会话,那么它必须通过定制 Web 层来完成。 + +### 我如何在用户详细服务中访问用户的密码? + +你不能(也不应该)。你可能误解了它的目的。见上文“[什么是用户详细服务?](#appendix-faq-what-is-userdetailservice)”。 + +### 如何在应用程序中动态地定义安全的 URL? + +人们经常询问如何在数据库中存储安全 URL 和安全元数据属性之间的映射,而不是在应用程序上下文中。 + +你应该问自己的第一件事是,你是否真的需要这么做。如果应用程序需要安全保护,那么它还需要基于已定义的策略对安全性进行彻底测试。在将其推出到生产环境之前,可能需要进行审核和验收测试。具有安全意识的组织应该意识到,通过允许在运行时更改配置数据库中的一两行来修改安全设置,可以立即消除其勤奋的测试过程的好处。 Spring 如果你已经考虑到了这一点(可能在你的应用程序中使用了多个安全层),那么安全性允许你完全自定义安全元数据的来源。如果你愿意的话,你可以让它完全充满活力。 + +方法和 Web 安全性都由`AbstractSecurityInterceptor`的子类保护,该子类配置为`SecurityMetadataSource`,它从该子类获得特定方法或过滤器调用的元数据。对于 Web 安全,拦截器类是`FilterSecurityInterceptor`,它使用标记接口`FilterInvocationSecurityMetadataSource`。它所操作的“安全对象”类型是`FilterInvocation`。使用的默认实现(在名称空间``中以及显式配置拦截器时)将 URL 模式列表及其相应的“配置属性”列表(`ConfigAttribute`实例)存储在内存映射中。 + +要从替代源加载数据,必须使用显式声明的安全筛选链(通常是 Spring Security 的`FilterChainProxy`),以便自定义`FilterSecurityInterceptor` Bean。你不能使用名称空间。然后,你将实现`FilterInvocationSecurityMetadataSource`来为特定的`FilterInvocation`[1]任意加载数据。一个非常基本的大纲应该是这样的: + +Java + +``` + public class MyFilterSecurityMetadataSource implements FilterInvocationSecurityMetadataSource { + + public List getAttributes(Object object) { + FilterInvocation fi = (FilterInvocation) object; + String url = fi.getRequestUrl(); + String httpMethod = fi.getRequest().getMethod(); + List attributes = new ArrayList(); + + // Lookup your database (or other source) using this information and populate the + // list of attributes + + return attributes; + } + + public Collection getAllConfigAttributes() { + return null; + } + + public boolean supports(Class clazz) { + return FilterInvocation.class.isAssignableFrom(clazz); + } + } +``` + +Kotlin + +``` +class MyFilterSecurityMetadataSource : FilterInvocationSecurityMetadataSource { + override fun getAttributes(securedObject: Any): List { + val fi = securedObject as FilterInvocation + val url = fi.requestUrl + val httpMethod = fi.request.method + + // Lookup your database (or other source) using this information and populate the + // list of attributes + return ArrayList() + } + + override fun getAllConfigAttributes(): Collection? { + return null + } + + override fun supports(clazz: Class<*>): Boolean { + return FilterInvocation::class.java.isAssignableFrom(clazz) + } +} +``` + +有关更多信息,请查看`DefaultFilterInvocationSecurityMetadataSource`的代码。 + +### 如何针对 LDAP 进行身份验证,并从数据库加载用户角色? + +`LdapAuthenticationProvider` Bean(在 Spring Security 中处理正常的 LDAP 身份验证)被配置为两个独立的策略接口,一个用于执行身份验证,另一个用于加载用户权限,分别称为`LdapAuthenticator`和`LdapAuthoritiesPopulator`。`DefaultLdapAuthoritiesPopulator`从 LDAP 目录加载用户权限,并具有各种配置参数,允许你指定如何检索这些权限。 + +要使用 JDBC,你可以自己实现接口,使用适合你的模式的任何 SQL: + +Java + +``` + public class MyAuthoritiesPopulator implements LdapAuthoritiesPopulator { + @Autowired + JdbcTemplate template; + + List getGrantedAuthorities(DirContextOperations userData, String username) { + return template.query("select role from roles where username = ?", + new String[] {username}, + new RowMapper() { + /** + * We're assuming here that you're using the standard convention of using the role + * prefix "ROLE_" to mark attributes which are supported by Spring Security's RoleVoter. + */ + @Override + public GrantedAuthority mapRow(ResultSet rs, int rowNum) throws SQLException { + return new SimpleGrantedAuthority("ROLE_" + rs.getString(1)); + } + }); + } + } +``` + +Kotlin + +``` +class MyAuthoritiesPopulator : LdapAuthoritiesPopulator { + @Autowired + lateinit var template: JdbcTemplate + + override fun getGrantedAuthorities(userData: DirContextOperations, username: String): MutableList { + return template.query("select role from roles where username = ?", + arrayOf(username) + ) { rs, _ -> + /** + * We're assuming here that you're using the standard convention of using the role + * prefix "ROLE_" to mark attributes which are supported by Spring Security's RoleVoter. + */ + SimpleGrantedAuthority("ROLE_" + rs.getString(1)) + } + } +} +``` + +然后将这种类型的 Bean 添加到应用程序上下文中,并将其注入`LdapAuthenticationProvider`。在参考手册的 LDAP 章节中,关于使用显式 Spring bean 配置 LDAP 的部分介绍了这一点。请注意,在这种情况下,你不能使用名称空间进行配置。你还应该参考 Javadoc 获得相关的类和接口。 + +### I want to modify the property of a bean that is created by the namespace, but there is nothing in the schema to support it. + +除了放弃名称空间的使用,我还能做什么呢? + +名称空间功能是有意限制的,因此它不能涵盖使用普通 bean 所能做的所有事情。如果你想做一些简单的事情,比如修改一个 Bean,或者注入一个不同的依赖项,那么你可以通过在配置中添加一个`BeanPostProcessor`来做到这一点。更多信息请参见[Spring Reference Manual](https://docs.spring.io/spring/docs/3.0.x/spring-framework-reference/htmlsingle/spring-framework-reference.html#beans-factory-extension-bpp)。为了做到这一点,你需要了解一些有关创建了哪些 bean 的信息,因此你还应该阅读[how the namespace maps to Spring beans](#appendix-faq-namespace-to-bean-mapping)上的问题中的博客文章。 + +通常,你会将所需的功能添加到`postProcessBeforeInitialization`的`BeanPostProcessor`方法中。假设你希望自定义`UsernamePasswordAuthenticationFilter`使用的`AuthenticationDetailsSource`,(由`form-login`元素创建)。你希望从请求中提取一个名为`CUSTOM_HEADER`的特定标头,并在对用户进行身份验证时使用它。处理器类看起来是这样的: + +Java + +``` +public class CustomBeanPostProcessor implements BeanPostProcessor { + + public Object postProcessAfterInitialization(Object bean, String name) { + if (bean instanceof UsernamePasswordAuthenticationFilter) { + System.out.println("********* Post-processing " + name); + ((UsernamePasswordAuthenticationFilter)bean).setAuthenticationDetailsSource( + new AuthenticationDetailsSource() { + public Object buildDetails(Object context) { + return ((HttpServletRequest)context).getHeader("CUSTOM_HEADER"); + } + }); + } + return bean; + } + + public Object postProcessBeforeInitialization(Object bean, String name) { + return bean; + } +} +``` + +Kotlin + +``` +class CustomBeanPostProcessor : BeanPostProcessor { + override fun postProcessAfterInitialization(bean: Any, name: String): Any { + if (bean is UsernamePasswordAuthenticationFilter) { + println("********* Post-processing $name") + bean.setAuthenticationDetailsSource( + AuthenticationDetailsSource { context -> context.getHeader("CUSTOM_HEADER") }) + } + return bean + } + + override fun postProcessBeforeInitialization(bean: Any, name: String?): Any { + return bean + } +} +``` + +然后,你将在应用程序上下文中注册此 Bean。 Spring 将在应用程序上下文中定义的 bean 上自动调用它。 + +--- + +[1](#_footnoteref_1)。`FilterInvocation`对象包含`HttpServletRequest`,因此你可以获得 URL 或任何其他相关信息,你可以根据返回的属性列表将包含什么来做出决定。 + +[WebSocket Security](namespace/websocket.html)[反应性应用](../../reactive/index.html) + diff --git a/docs/spring-security/servlet-appendix-namespace-authentication-manager.md b/docs/spring-security/servlet-appendix-namespace-authentication-manager.md new file mode 100644 index 0000000000000000000000000000000000000000..f8bac7016a59efb7d8cb6391aca2119884feb8b5 --- /dev/null +++ b/docs/spring-security/servlet-appendix-namespace-authentication-manager.md @@ -0,0 +1,150 @@ +# 认证服务 + +这将创建 Spring Security 的`ProviderManager`类的实例,该实例需要配置一个或多个`AuthenticationProvider`实例的列表。这些可以使用名称空间提供的语法元素创建,也可以是标准 Bean 定义,标记为使用`authentication-provider`元素添加到列表中。 + +## \ + +每个使用命名空间的安全应用程序都必须在某个地方包含这个元素。它负责注册为应用程序提供身份验证服务的`AuthenticationManager`。所有创建`AuthenticationProvider`实例的元素都应该是这个元素的子元素。 + +### \属性 + +* **别名**此属性允许你为内部实例定义别名,以便在你自己的配置中使用。 + +* **擦除凭据**如果设置为 true,则一旦用户通过身份验证,身份验证管理器将尝试清除返回的身份验证对象中的任何凭据数据。从字面上看,它映射到[`eraseCredentialsAfterAuthentication`][`ProviderManager`](.../authentication/architecture.html# Servlet-authentication-providermanager)的`eraseCredentialsAfterAuthentication`属性。 + +* **身份证**此属性允许你为内部实例定义一个 ID,以便在你自己的配置中使用。它与 Alias 元素相同,但是对于使用 ID 属性的元素提供了更一致的体验。 + +### \的子元素 + +* [身份验证提供者](#nsa-authentication-provider) + +* [LDAP-身份验证-提供者](ldap.html#nsa-ldap-authentication-provider) + +## \ + +除非与`ref`属性一起使用,否则此元素是用于配置`DaoAuthenticationProvider`的简写。`DaoAuthenticationProvider`从`UserDetailsService`加载用户信息,并将用户名/密码组合与登录时提供的值进行比较。`UserDetailsService`实例可以通过使用可用的名称空间元素(`jdbc-user-service`)或通过使用`user-service-ref`属性指向应用程序上下文中其他地方定义的 Bean 来定义。 + +### \的父元素 + +* [身份验证管理器](#nsa-authentication-manager) + +### \属性 + +* **参考**定义了对实现`AuthenticationProvider`的 Spring Bean 的引用。 + +如果你已经编写了自己的`AuthenticationProvider`实现(或者出于某种原因想将 Spring Security 自己的实现之一配置为传统的 Bean,那么你可以使用以下语法将其添加到`ProviderManager`的内部列表中: + +``` + + + + +``` + +* **user-service-ref **对实现可使用标准 Bean 元素或自定义用户服务元素创建的 UserDetailsService 的 Bean 的引用。 + +### \的子元素 + +* [JDBC-用户服务](#nsa-jdbc-user-service) + +* [LDAP-用户服务](ldap.html#nsa-ldap-user-service) + +* [密码编码器](#nsa-password-encoder) + +* [用户服务](#nsa-user-service) + +## \ + +导致创建基于 JDBC 的 UserDetailsService。 + +### \属性 + +* **权威用户名查询**一种 SQL 语句,用于查询给定用户名的用户授予的权限。 + +默认值是 + +``` +select username, authority from authorities where username = ? +``` + +* **cache-ref **定义了对缓存的引用,以便与 UserDetailsService 一起使用。 + +* **data-source-ref **提供所需表的数据源的 Bean ID。 + +* **组-权限-按用户名-查询**一个 SQL 语句,用于查询给定用户名的用户组权限。默认值是 + + ``` + select + g.id, g.group_name, ga.authority + from + groups g, group_members gm, group_authorities ga + where + gm.username = ? and g.id = ga.group_id and g.id = gm.group_id + ``` + +* **身份证** Bean 标识符,用于在上下文的其他地方引用 Bean。 + +* **角色前缀**一个非空的字符串前缀,它将被添加到从持久存储加载的角色字符串中(默认为“role\_”)。在默认值为非空的情况下,使用值“none”表示无前缀。 + +* **用户按用户名查询**查询用户名、密码和给定用户名的已启用状态的 SQL 语句。默认值是 + + ``` + select username, password, enabled from users where username = ? + ``` + +## \ + +身份验证提供者可以可选地被配置为使用[密码存储](../../../features/authentication/password-storage.html#authentication-password-storage)中所述的密码编码器。这将导致 Bean 被注入适当的`PasswordEncoder`实例。 + +### \的父元素 + +* [身份验证提供者](#nsa-authentication-provider) + +* [密码-比较](#nsa-password-compare) + +### \属性 + +* **散列**定义了用于用户密码的散列算法。我们强烈建议不要使用 MD4,因为它是一种非常弱的散列算法。 + +* **参考**定义了对实现`PasswordEncoder`的 Spring Bean 的引用。 + +## \ + +从属性文件或“用户”子元素列表创建内存中的 UserDetailsService。用户名在内部被转换为小写字母,以允许不区分大小写的查找,因此如果需要区分大小写,则不应使用这种方法。 + +### \属性 + +* **身份证**一个 Bean 标识符,用于在上下文的其他地方引用 Bean。 + +* **属性**属性文件的位置,其中每行的格式为 + + ``` + username=password,grantedAuthority[,grantedAuthority][,enabled|disabled] + ``` + +### \的子元素 + +* [user](#nsa-user) + +## \ + +表示应用程序中的用户。 + +### \的父元素 + +* [用户服务](#nsa-user-service) + +### \属性 + +* **当局**授予用户的多个权限之一。用逗号(但没有空格)分隔权限。例如,“role\_user,role\_administrator” + +* **已禁用**可以设置为“true”,以将帐户标记为禁用和不可用。 + +* **锁定**可以设置为“true”,以标记帐户为锁定和不可用。 + +* **姓名**分配给用户的用户名。 + +* **密码**分配给用户的密码。如果相应的身份验证提供程序支持散列(请记住设置“user-service”元素的“hash”属性),则可能会进行散列。如果数据不用于身份验证,而仅用于访问权限,则省略此属性。如果省略,命名空间将生成一个随机值,从而防止意外地将其用于身份验证。不能是空的。 + +[XML 命名空间](index.html)[网络安全](http.html) + diff --git a/docs/spring-security/servlet-appendix-namespace-http.md b/docs/spring-security/servlet-appendix-namespace-http.md new file mode 100644 index 0000000000000000000000000000000000000000..f73cfada902568b6773869e07a703f48963e1975 --- /dev/null +++ b/docs/spring-security/servlet-appendix-namespace-http.md @@ -0,0 +1,1042 @@ +# Web 应用程序安全性 + +## \ + +启用 Spring 安全调试基础设施。这将提供人类可读的(多行)调试信息,以监视进入安全过滤器的请求。这可能包括敏感信息,例如请求参数或标题,并且应该仅在开发环境中使用。 + +## \ + +如果在应用程序中使用``元素,则会创建一个名为“SpringSecurityFilterChain”的`FilterChainProxy` Bean,并且该元素中的配置将用于在`FilterChainProxy`中构建过滤器链。从 Spring Security3.1 开始,额外的`http`元素可以用来添加额外的过滤链[1]用于如何从你的`web.xml`设置映射。一些核心过滤器总是在过滤器链中创建的,而另一些将根据存在的属性和子元素添加到堆栈中。标准过滤器的位置是固定的(参见名称空间介绍中的[过滤器订单表](../../configuration/xml-namespace.html#filter-stack)),当用户必须在`FilterChainProxy` Bean 中显式地配置过滤器链时,消除了以前版本框架的常见错误来源。当然,如果你需要完全控制配置,你仍然可以这样做。 + +所有需要引用[`AuthenticationManager`](../../authentication/architecture.html# Servlet-authentication-authenticationManager)的过滤器都将自动注入由名称空间配置创建的内部实例。 + +每个``名称空间块总是创建一个`SecurityContextPersistenceFilter`,一个`ExceptionTranslationFilter`和一个`FilterSecurityInterceptor`。这些都是固定的,不能用替代品代替。 + +### \属性 + +``元素上的属性控制核心过滤器上的一些属性。 + +* **access-decision-manager-ref**可选属性,指定用于授权 HTTP 请求的`AccessDecisionManager`实现的 ID。默认情况下,`AffirmativeBased`实现用于`RoleVoter`和`AuthenticatedVoter`。 + +* **认证-管理器-ref**对`AuthenticationManager`的引用,用于此 HTTP 元素创建的`FilterChain`。 + +* **自动配置**自动注册一个登录表单、基本身份验证、注销服务。如果设置为“true”,则会添加所有这些功能(尽管你仍然可以通过提供相应的元素来定制每个功能的配置)。如果未指定,默认为“false”。不建议使用此属性。使用显式配置元素来避免混淆。 + +* **create-session**控制着由 Spring 安全类创建 HTTP会话的渴望。选项包括: + + * `always`- Spring 安全性将主动创建一个会话如果一个不存在。 + + * `ifRequired`- Spring 安全性仅在需要会话时才会创建会话(默认值)。 + + * `never`- Spring 安全性永远不会创建会话,但是如果应用程序创建了,则会使用一个。 + + * `stateless`- Spring 安全性不会创建会话并忽略用于获得 Spring `Authentication`的会话。 + +* **禁用 URL 重写**防止将会话ID 追加到应用程序中的 URL。如果此属性设置为`true`,则客户端必须使用 cookie。默认值为`true`。 + +* **入口点-参考**通常使用的`AuthenticationEntryPoint`将根据已配置的身份验证机制进行设置。此属性允许通过定义一个定制的`AuthenticationEntryPoint` Bean 来重写此行为,该定义将启动身份验证过程。 + +* **JAAS-API-供应**如果可用,则以从`JaasAuthenticationToken`获取的`Subject`的形式运行请求,该请求通过向堆栈添加`JaasApiIntegrationFilter` Bean 来实现。默认值为`false`。 + +* **姓名**一个 Bean 标识符,用于在上下文的其他地方引用 Bean。 + +* **每次请求一次**对应于`observeOncePerRequest`的`FilterSecurityInterceptor`属性。默认值为`true`。 + +* **模式**为[http](#nsa-http)元素定义一个模式,控制将通过它定义的过滤器列表过滤的请求。解释依赖于配置的[请求匹配程序](#nsa-http-request-matcher)。如果没有定义模式,那么将匹配所有请求,因此应该首先声明最特定的模式。 + +* **领域**设置用于基本身份验证的领域名称(如果启用)。对应于`BasicAuthenticationEntryPoint`上的`realmName`属性。 + +* **请求匹配程序**定义了`RequestMatcher`策略中使用的`FilterChainProxy`和由`intercept-url`创建的 bean,以匹配传入的请求。对于 Spring MVC、 Ant、正则表达式和不区分大小写的正则表达式,当前的选项分别是`mvc`、`ant`、`regex`和`ciRegex`。使用[pattern](#nsa-intercept-url-pattern)、[method](#nsa-intercept-url-method)和[servlet-path](#nsa-intercept-url-servlet-path)属性为每个[截取-URL](#nsa-intercept-url)元素创建一个单独的实例。 Ant 路径是使用`AntPathRequestMatcher`进行匹配的,正则表达式是使用`RegexRequestMatcher`进行匹配的,并且对于 Spring MVC 路径是使用`MvcRequestMatcher`进行匹配的。有关这些类的更多详细信息,请参见 Javadoc。 Ant 路径是默认策略。 + +* **request-matcher-ref**对实现`RequestMatcher`的 Bean 的引用,该引用将确定是否应该使用此`FilterChain`。这是一个比[pattern](#nsa-http-pattern)更强大的替代方案。 + +* **安全**通过将此属性设置为`none`,可以将一个请求模式映射到一个空的过滤器链。将不会应用任何安全性,并且 Spring 安全性的任何功能都将不可用。 + +* **security-context-repository-ref**允许将自定义`SecurityContextRepository`注入到`SecurityContextPersistenceFilter`中。 + +* **servlet-api-provision**提供`HttpServletRequest`安全方法的版本,例如`isUserInRole()`和`getPrincipal()`,这些方法通过向堆栈中添加`SecurityContextHolderAwareRequestFilter` Bean 来实现。默认值为`true`。 + +* **使用表达式**在`access`属性中启用 el-表达式,如[基于表达式的访问控制](../../authorization/expression-based.html#el-access-web)一章中所述。默认值为 true。 + +### \的子元素 + +* [拒绝访问的处理程序](#nsa-access-denied-handler) + +* [anonymous](#nsa-anonymous) + +* [cors](#nsa-cors) + +* [csrf](#nsa-csrf) + +* [自定义过滤器](#nsa-custom-filter) + +* [表达式处理程序](#nsa-expression-handler) + +* [form-login](#nsa-form-login) + +* [headers](#nsa-headers) + +* [http-basic](#nsa-http-basic) + +* [截取-URL](#nsa-intercept-url) + +* [jee](#nsa-jee) + +* [logout](#nsa-logout) + +* [OAuth2-客户端](#nsa-oauth2-client) + +* [OAuth2-登录](#nsa-oauth2-login) + +* [OAuth2-资源服务器](#nsa-oauth2-resource-server) + +* [OpenID-登录](#nsa-openid-login) + +* [密码管理](#nsa-password-management) + +* [端口映射](#nsa-port-mappings) + +* [记住-我](#nsa-remember-me) + +* [请求-缓存](#nsa-request-cache) + +* [session-management](#nsa-session-management) + +* [x509](#nsa-x509) + +## \ + +这个元素允许你使用[error-page](#nsa-access-denied-handler-error-page)属性为`AccessDeniedHandler`使用的默认`AccessDeniedHandler`属性设置`errorPage`属性,或者使用[ref](#nsa-access-denied-handler-ref)属性提供你自己的实现。这将在[ExceptionTranslationFilter](../../architecture.html#servlet-exceptiontranslationfilter)一节中进行更详细的讨论。 + +### \的父元素 + +* [http](#nsa-http) + +### \属性 + +* **错误页面**被拒绝访问的页面,如果经过身份验证的用户请求一个他们没有权限访问的页面,该页面将被重定向到该页面。 + +* **参考**定义了对`AccessDeniedHandler`类型的 Spring Bean 的引用。 + +## \ + +这个元素允许配置`CorsFilter`。如果指定了 no`CorsFilter`或`CorsConfigurationSource`,并且 Spring MVC 在 Classpath 上,则使用`HandlerMappingIntrospector`作为`CorsConfigurationSource`。 + +### \属性 + +``元素上的属性控制 headers 元素。 + +* **参考**可选属性,指定`CorsFilter`的 Bean 名称。 + +* **CORS-configuration-source-ref**可选属性,该属性指定要注入到由 XML 命名空间创建的`CorsFilter`中的`CorsConfigurationSource`的 Bean 名称。 + +### \的父元素 + +* [http](#nsa-http) + +## \ + +该元素允许配置附加的(安全性)头,以便与响应一起发送。它支持几个头的简单配置,还允许通过[header](#nsa-header)元素设置自定义头。其他信息,可以在[安全标头](../../../features/exploits/headers.html#headers)部分的引用中找到。 + +* `Cache-Control`,`Pragma`,和`Expires`-可以使用[高速缓存控制](#nsa-cache-control)元素进行设置。这确保了浏览器不会缓存你的安全页面。 + +* `Strict-Transport-Security`-可以使用[hsts](#nsa-hsts)元素进行设置。这确保了浏览器自动为将来的请求请求请求 HTTPS。 + +* `X-Frame-Options`-可以使用[框架-选项](#nsa-frame-options)元素进行设置。[X-帧-选项](https://en.wikipedia.org/wiki/Clickjacking#X-Frame-Options)标头可用于防止单击劫持攻击。 + +* `X-XSS-Protection`-可以使用[XSS-保护](#nsa-xss-protection)元素进行设置。浏览器可以使用[X-XSS-保护](https://en.wikipedia.org/wiki/Cross-site_scripting)头来执行基本控制。 + +* `X-Content-Type-Options`-可以使用[内容类型选项](#nsa-content-type-options)元素进行设置。[X-Content-Type-Options](https://blogs.msdn.com/b/ie/archive/2008/09/02/ie8-security-part-vi-beta-2-update.aspx)头可以防止 Internet Explorer 从声明的 Content-Type MIME 嗅探响应。这也适用于谷歌 Chrome,当下载扩展时。 + +* `Public-Key-Pinning`或`Public-Key-Pinning-Report-Only`-可以使用[hpkp](#nsa-hpkp)元素进行设置。这使得 HTTPS 网站能够抵抗攻击者使用错误发行或其他欺诈性证书的假冒行为。 + +* `Content-Security-Policy`或`Content-Security-Policy-Report-Only`-可以使用[内容安全策略](#nsa-content-security-policy)元素进行设置。[内容安全策略](https://www.w3.org/TR/CSP2/)是一种机制,Web 应用程序可以利用该机制来减轻内容注入漏洞,例如跨站点脚本。 + +* `Referrer-Policy`-可以使用[推荐人-政策](#nsa-referrer-policy)元素进行设置,[推荐人-政策](https://www.w3.org/TR/referrer-policy/)是一种机制,Web 应用程序可以利用该机制来管理 Referrer 字段,该字段包含用户所在的最后一个页面。 + +* `Feature-Policy`-可以使用[特征-策略](#nsa-feature-policy)元素进行设置,[特征-策略](https://wicg.github.io/feature-policy/)是一种机制,允许 Web 开发人员选择性地启用、禁用和修改浏览器中某些 API 和 Web 功能的行为。 + +### \属性 + +``元素上的属性控制 headers 元素。 + +* **默认值-禁用**可选属性,该属性指定禁用默认的 Spring Security 的 HTTP 响应头。默认值为 false(包括默认的标头)。 + +* **已禁用**可选属性,指定禁用 Spring Security 的 HTTP 响应头。默认值为 false(已启用标头)。 + +### \的父元素 + +* [http](#nsa-http) + +### \的子元素 + +* [高速缓存控制](#nsa-cache-control) + +* [内容安全策略](#nsa-content-security-policy) + +* [内容类型选项](#nsa-content-type-options) + +* [特征-策略](#nsa-feature-policy) + +* [框架-选项](#nsa-frame-options) + +* [header](#nsa-header) + +* [hpkp](#nsa-hpkp) + +* [hsts](#nsa-hsts) + +* [权限策略](#nsa-permissions-policy) + +* [推荐人-政策](#nsa-referrer-policy) + +* [XSS-保护](#nsa-xss-protection) + +## \ + +添加`Cache-Control`、`Pragma`和`Expires`标题,以确保浏览器不会缓存你的安全页面。 + +### \属性 + +* **已禁用**指定是否应禁用缓存控制。默认为 false。 + +### \的父元素 + +* [headers](#nsa-headers) + +## \ + +启用时,将[严格的运输安全](https://tools.ietf.org/html/rfc6797)头添加到响应中,用于处理任何安全请求。这允许服务器指示浏览器在将来的请求中自动使用 HTTPS。 + +### \属性 + +* **已禁用**指定是否应禁用 strict-transport-security。默认为 false。 + +* **包含-子域**指定是否应包含子域。默认为 true。 + +* **最大年龄秒**指定主机应被视为已知 HSTS 主机的最大时间。违约一年。 + +* **request-matcher-ref**用于确定是否应设置标头的 requestmatcher 实例。默认情况是,如果 HttpServletRequest.isSecure()为真。 + +* **预加载**指定是否应包含预加载。默认为 false。 + +### \的父元素 + +* [headers](#nsa-headers) + +## \ + +启用时,将[HTTP 的公钥固定扩展](https://tools.ietf.org/html/rfc7469)头添加到响应中,用于处理任何安全请求。这使得 HTTPS 网站能够抵抗攻击者使用错误发行或其他欺诈性证书的假冒行为。 + +### \属性 + +* **已禁用**指定是否应禁用 HTTP 公钥锁定。默认为 true。 + +* **包含-子域**指定是否应包含子域。默认为 false。 + +* **最大年龄秒**设置 public-key-pins 头的 max-age 指令的值。默认 60 天。 + +* **仅报告**指定浏览器是否只应报告 PIN 验证失败。默认为 true。 + +* **报告-URI**指定浏览器应向其报告 PIN 验证失败的 URI。 + +### \的父元素 + +* [headers](#nsa-headers) + +## \ + +别针列表 + +### \的子元素 + +* [pin](#nsa-pin) + +## \ + +指定 PIN 时,使用 Base64 编码的 SPKI 指纹作为值,使用加密哈希算法作为属性 + +### \属性 + +* **算法**加密散列算法。默认值是 SHA256。 + +### \的父元素 + +* [pins](#nsa-pins) + +## \ + +启用时,将[内容安全策略](https://www.w3.org/TR/CSP2/)头添加到响应中。CSP 是一种机制,Web 应用程序可以利用它来减轻内容注入漏洞,例如跨站点脚本。 + +### \属性 + +* **政策指令**用于 content-security-policy 头的安全策略指令,或者如果将 report-only 设置为 true,则使用 content-security-policy-report-only 头。 + +* **仅报告**设置为 true,以启用 content-security-policy-report-only 头,仅用于报告违反策略的情况。默认为 false。 + +### \的父元素 + +* [headers](#nsa-headers) + +## \ + +启用时,将[推荐人政策](https://www.w3.org/TR/referrer-policy/)头添加到响应中。 + +### \属性 + +* **政策**referrer-policy 头的策略。默认的“无推荐人”。 + +### \的父元素 + +* [headers](#nsa-headers) + +## \ + +启用时,将[特征策略](https://wicg.github.io/feature-policy/)头添加到响应中。 + +### \属性 + +* **政策指令**用于 Feature-Policy 头的安全策略指令。 + +### \的父元素 + +* [headers](#nsa-headers) + +## \ + +启用后,将[X-frame-options 标头](https://tools.ietf.org/html/draft-ietf-websec-x-frame-options)添加到响应中,这允许较新的浏览器进行一些安全检查并防止[点击劫持](https://en.wikipedia.org/wiki/Clickjacking)攻击。 + +### \属性 + +* **已禁用**如果禁用,将不包括 X-frame-options 标头。默认为 false。 + +* **政策** + + * `DENY`该页面不能显示在一个框架中,无论站点试图这样做。这是在指定框架-选项-策略时的默认值。 + + * `SAMEORIGIN`页面只能在与页面本身位于同一原点的框架中显示 + + 换句话说,如果你指定了 deny,那么在从其他站点加载时,在帧中加载页面的尝试不仅会失败,而且在从同一站点加载时也会失败。另一方面,如果你指定了 SameOrigin,则仍然可以在一个框架中使用该页面,只要在一个框架中包含该页面的站点与为该页面提供服务的站点相同。 + +### \的父元素 + +* [headers](#nsa-headers) + +## \ + +将[权限-策略标头](https://w3c.github.io/webappsec-permissions-policy/)添加到响应中。 + +### \属性 + +* **政策**要为`Permissions-Policy`头写的策略值 + +### \的父元素 + +* [headers](#nsa-headers) + +## \ + +将[X-XSS-保护报头](https://blogs.msdn.com/b/ie/archive/2008/07/02/ie8-security-part-iv-the-xss-filter.aspx)添加到响应中,以帮助防止[reflected/type-1 跨站点脚本](https://en.wikipedia.org/wiki/Cross-site_scripting#Non-Persistent)攻击。这绝对是对 XSS 攻击的全面保护! + +### \属性 + +* **XSS-保护-禁用**不包括[reflected/type-1 跨站点脚本](https://en.wikipedia.org/wiki/Cross-site_scripting#Non-Persistent)保护的标题。 + +* **支持 XSS 保护**显式启用或禁用[reflected/type-1 跨站点脚本](https://en.wikipedia.org/wiki/Cross-site_scripting#Non-Persistent)保护。 + +* **xss-保护模块**当 true 和 xss-protection-enabled 为 true 时,将 mode=block 添加到 header 中。这向浏览器表明,该页面根本不应该加载。当 false 和启用 XSS-Protection-enabled 为 true 时,当检测到反射攻击时,仍将呈现该页面,但将修改响应以防止攻击。请注意,有时有一些绕过此模式的方法,这通常会使阻塞页面变得更理想。 + +### \的父元素 + +* [headers](#nsa-headers) + +## \ + +向响应添加带有 nosniff 值的 x-content-type-options 头。这[禁用 MIME 嗅探功能](https://blogs.msdn.com/b/ie/archive/2008/09/02/ie8-security-part-vi-beta-2-update.aspx)用于 IE8+ 和 Chrome 扩展。 + +### \属性 + +* **已禁用**指定是否应禁用内容类型选项。默认为 false。 + +### \的父元素 + +* [headers](#nsa-headers) + +## \ + +在响应中添加额外的头,需要同时指定名称和值。 + +### \属性 + +* **标头-Name**标题的`name`。 + +* **价值**要添加的标头的`value`。 + +* **参考**引用了`HeaderWriter`接口的自定义实现。 + +### \的父元素 + +* [headers](#nsa-headers) + +## \ + +将`AnonymousAuthenticationFilter`添加到堆栈,并将`AnonymousAuthenticationProvider`添加到堆栈。如果你正在使用`IS_AUTHENTICATED_ANONYMOUSLY`属性,则需要这样做。 + +### \的父元素 + +* [http](#nsa-http) + +### \属性 + +* **已启用**通过默认的名称空间设置,匿名“身份验证”功能将自动启用。你可以使用此属性禁用它。 + +* **授予-授权**应分配给匿名请求的授予权限。这通常用于分配匿名请求的特定角色,随后可以在授权决策中使用该角色。如果未设置,则默认为`ROLE_ANONYMOUS`。 + +* **钥匙**提供程序和过滤器之间共享的密钥。这通常不需要设置。如果未设置,它将默认为一个安全的随机生成值。这意味着在使用匿名功能时,设置该值可以提高启动时间,因为安全的随机值可能需要一段时间才能生成。 + +* **用户 Name**应该分配给匿名请求的用户名。这允许标识主体,这对于日志记录和审核可能很重要。如果未设置,则默认为`anonymousUser`。 + +## \ + +这个元素将向应用程序添加[跨站点请求伪造者](https://en.wikipedia.org/wiki/Cross-site_request_forgery)保护。它还更新了默认的 RequestCache,以便在身份验证成功时只重放“get”请求。其他信息可以在参考文献的[跨站点请求伪造](../../../features/exploits/csrf.html#csrf)部分中找到。 + +### \的父元素 + +* [http](#nsa-http) + +### \属性 + +* **已禁用**可选属性,指定禁用 Spring Security 的 CSRF 保护。默认值为 false(启用了 CSRF 保护)。强烈建议启用 CSRF 保护。 + +* **Token-Repository-ref**要使用的 csRFTokenRepository。默认值为`HttpSessionCsrfTokenRepository`。 + +* **request-matcher-ref**用于确定是否应用 CSRF 的请求匹配实例。默认是除了“get”、“trace”、“head”、“options”之外的任何 HTTP 方法。 + +## \ + +这个元素用于向过滤链添加过滤器。它不会创建任何额外的 bean,而是用于选择`javax.servlet.Filter`类型的 Bean,该类型已在应用程序上下文中定义,并在由 Spring Security 维护的筛选链中的特定位置添加该类型。详细信息可以在[名称空间章节](../../configuration/xml-namespace.html#ns-custom-filters)中找到。 + +### \的父元素 + +* [http](#nsa-http) + +### \属性 + +* **之后**紧随其后的过滤器,该自定义过滤器应放置在链中。该功能将仅由希望将自己的过滤器混合到安全过滤器链中并对标准 Spring 安全过滤器有一定了解的高级用户所需要。过滤器名称映射到特定的 Spring 安全实现过滤器。 + +* **在此之前**将自定义过滤器置于链中之前的过滤器 + +* **职务**将自定义过滤器放置在链中的明确位置。如果你正在更换标准过滤器,请使用。 + +* **参考**定义了对实现`Filter`的 Spring Bean 的引用。 + +## \ + +定义启用基于表达式的访问控制时将使用的`SecurityExpressionHandler`实例。如果没有提供,将使用缺省实现(不支持 ACL)。 + +### \的父元素 + +* [全局方法安全性](method-security.html#nsa-global-method-security) + +* [http](#nsa-http) + +* [方法-安全性](method-security.html#nsa-method-security) + +* [websocket-message-broker](websocket.html#nsa-websocket-message-broker) + +### \属性 + +* **参考**定义了对实现`SecurityExpressionHandler`的 Spring Bean 的引用。 + +## \ + +用于将`UsernamePasswordAuthenticationFilter`添加到筛选器堆栈,并将`LoginUrlAuthenticationEntryPoint`添加到应用程序上下文,以按需提供身份验证。这将始终优先于其他名称空间创建的入口点。如果不提供属性,将在 URL“/login”[2]自动生成登录页面。可以使用[``属性](#NSA-form-login-attributes)定制行为。 + +### \的父元素 + +* [http](#nsa-http) + +### \属性 + +* **总是-使用-默认-目标**如果设置为`true`,则用户将始终从[默认值-目标-URL](#nsa-form-login-default-target-url)给出的值开始,无论他们是如何到达登录页面的。映射到`UsernamePasswordAuthenticationFilter`的`alwaysUseDefaultTargetUrl`属性。默认值为`false`。 + +* **身份验证-详细信息-源代码-引用**引用将由身份验证过滤器使用的`AuthenticationDetailsSource` + +* **身份验证-失败-处理程序-ref**可以用作[身份验证-失败-URL](#nsa-form-login-authentication-failure-url)的替代方案,在身份验证失败后,你可以完全控制导航流。该值应该是应用程序上下文中`AuthenticationFailureHandler` Bean 的名称。 + +* **身份验证-失败-URL**映射到`authenticationFailureUrl`的`authenticationFailureUrl`属性。定义登录失败时浏览器将重定向到的 URL。默认为`/login?error`,这将由自动登录页面生成器自动处理,用错误消息重新呈现登录页面。 + +* **认证-成功-处理程序-引用**这可以作为[默认值-目标-URL](#nsa-form-login-default-target-url)和[总是-使用-默认-目标](#nsa-form-login-always-use-default-target)的替代方案,使你在成功的身份验证之后可以完全控制导航流。该值应该是应用程序上下文中`AuthenticationSuccessHandler` Bean 的名称。默认情况下,使用`SavedRequestAwareAuthenticationSuccessHandler`的实现并注入[默认值-目标-URL](#nsa-form-login-default-target-url)。 + +* **默认值-目标-URL**映射到`defaultTargetUrl`的`UsernamePasswordAuthenticationFilter`属性。如果未设置,默认值为“/”(应用程序根)。如果用户在试图访问安全资源时没有被要求登录,那么在登录后将被带到此 URL,这时他们将被带到最初请求的 URL。 + +* **登录页面**应该用来呈现登录页面的 URL。映射到`LoginUrlAuthenticationEntryPoint`的`loginFormUrl`属性。默认为“/login”。 + +* **登录-处理-URL**映射到`filterProcessesUrl`的`UsernamePasswordAuthenticationFilter`属性。默认值为“/login”。 + +* **密码参数**包含密码的请求参数的名称。默认为“密码”。 + +* **用户名参数**包含用户名的请求参数的名称。默认为“用户名”。 + +* **认证-成功-转发-URL**将`ForwardAuthenticationSuccessHandler`映射到`authenticationSuccessHandler`的`UsernamePasswordAuthenticationFilter`属性。 + +* **认证-失败-转发-URL**将`ForwardAuthenticationFailureHandler`映射到`authenticationFailureHandler`的属性`UsernamePasswordAuthenticationFilter`。 + +## \ + +[OAuth2.0 登录](../../oauth2/login/index.html#oauth2login)特性使用 OAuth2.0 和/或 OpenID Connect1.0 提供程序配置身份验证支持。 + +### \的父元素 + +* [http](#nsa-http) + +### \属性 + +* **客户端-注册-存储库-ref**引用`ClientRegistrationRepository`。 + +* **授权-客户端-存储库-ref**引用`OAuth2AuthorizedClientRepository`。 + +* **授权-客户-服务-参考**引用`OAuth2AuthorizedClientService`。 + +* **授权-请求-存储库-引用**引用`AuthorizationRequestRepository`。 + +* **授权-请求-解析器-ref**引用`OAuth2AuthorizationRequestResolver`。 + +* **access-token-response-client-ref**引用`OAuth2AccessTokenResponseClient`。 + +* **user-authorities-mapper-ref**引用`GrantedAuthoritiesMapper`。 + +* **user-service-ref**引用`OAuth2UserService`。 + +* **OIDC-user-service-ref**对 OpenID Connect 的引用`OAuth2UserService`。 + +* **登录-处理-URL**过滤器处理身份验证请求的 URI。 + +* **登录页面**发送用户登录的 URI。 + +* **认证-成功-处理程序-引用**引用`AuthenticationSuccessHandler`。 + +* **身份验证-失败-处理程序-ref**引用`AuthenticationFailureHandler`。 + +* **JWT-解码器-工厂-参考**引用`JwtDecoderFactory`使用的`OidcAuthorizationCodeAuthenticationProvider`。 + +## \ + +配置[OAuth2.0 客户端](../../oauth2/client/index.html#oauth2client)支持。 + +### \的父元素 + +* [http](#nsa-http) + +### \属性 + +* **客户端-注册-存储库-ref**引用`ClientRegistrationRepository`。 + +* **授权-客户端-存储库-ref**引用`OAuth2AuthorizedClientRepository`。 + +* **授权-客户-服务-参考**引用`OAuth2AuthorizedClientService`。 + +### \的子元素 + +* [授权-代码-授予](#nsa-authorization-code-grant) + +## \ + +配置[OAuth2.0 授权代码授予](../../oauth2/client/authorization-grants.html#oauth2Client-auth-grant-support)。 + +### \的父元素 + +* [OAuth2-客户端](#nsa-oauth2-client) + +### \属性 + +* **授权-请求-存储库-引用**引用`AuthorizationRequestRepository`。 + +* **授权-请求-解析器-ref**引用`OAuth2AuthorizationRequestResolver`。 + +* **access-token-response-client-ref**引用`OAuth2AccessTokenResponseClient`。 + +## \ + +为客户机注册的容器元素([客户登记](../../oauth2/client/index.html#oauth2Client-client-registration)),使用 OAuth2.0 或 OpenID Connect1.0 提供程序。 + +### \的子元素 + +* [客户-注册](#nsa-client-registration) + +* [provider](#nsa-provider) + +## \ + +表示在 OAuth2.0 或 OpenID Connect1.0 提供程序中注册的客户端。 + +### \的父元素 + +* [客户-注册](#nsa-client-registrations) + +### \属性 + +* **注册-ID**唯一标识`ClientRegistration`的 ID。 + +* **客户端-ID**客户端标识符。 + +* **客户端-机密**客户端秘密。 + +* **客户机-身份验证-方法**用于通过提供程序对客户端进行身份验证的方法。支持的值是**客户端 \_Secret\_BASIC**,**客户端 \_Secret\_post**,**Private\_Key\_JWT**,**客户端 \_Secret\_JWT**和**无**[(公众客户)](https://tools.ietf.org/html/rfc6749#section-2.1)。 + +* **授权-授予类型**OAuth2.0 授权框架定义了四种[授权授予](https://tools.ietf.org/html/rfc6749#section-1.3)类型。支持的值是`authorization_code`,`client_credentials`,`password`,以及扩展授权类型`urn:ietf:params:oauth:grant-type:jwt-bearer`。 + +* **重定向 URI**客户端注册的重定向 URI,在最终用户对客户端进行了身份验证和授权访问之后,*授权服务器*将最终用户的用户代理重定向到该 URI。 + +* **范围**客户端在授权请求流期间请求的范围,例如 OpenID、电子邮件或配置文件。 + +* **客户端-Name**用于客户机的描述性名称。该名称可以在某些场景中使用,例如在自动生成的登录页面中显示客户端的名称时。 + +* **提供者 ID**对关联提供者的引用。可以引用``元素或使用常见的提供者之一(Google,GitHub,Facebook,OKTA)。 + +## \ + +OAuth2.0 或 OpenID Connect1.0 提供程序的配置信息。 + +### \的父元素 + +* [客户-注册](#nsa-client-registrations) + +### \属性 + +* **提供者 ID**唯一标识提供者的 ID。 + +* **授权-URI**授权服务器的授权端点 URI。 + +* **Token-URI**授权服务器的令牌端点 URI。 + +* **User-info-uri**用于访问经过身份验证的最终用户的声明/属性的 userinfo 端点 URI。 + +* **用户信息认证方法**向 UserInfo 端点发送访问令牌时使用的身份验证方法。支持的值是**页眉**、**形式**和**查询**。 + +* **user-info-user-name-属性**在引用最终用户的名称或标识符的 userinfo 响应中返回的属性的名称。 + +* **JWK-SET-URI**用于从授权服务器检索[JSON Web Key](https://tools.ietf.org/html/rfc7517)集的 URI,其中包含用于验证 ID 令牌的[JSON Web 签名](https://tools.ietf.org/html/rfc7515)的加密密钥,以及可选的 userinfo 响应。 + +* **发行者-URI**最初使用发现 OpenID Connect 提供者的[配置端点](https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig)或授权服务器的[元数据端点](https://tools.ietf.org/html/rfc8414#section-3)来配置`ClientRegistration`的 URI。 + +## \ + +将`BearerTokenAuthenticationFilter`、`BearerTokenAuthenticationEntryPoint`和`BearerTokenAccessDeniedHandler`添加到配置中。此外,必须指定``或``。 + +### \的父元素 + +* [http](#nsa-http) + +### \的子元素 + +* [jwt](#nsa-jwt) + +* [不透明令牌](#nsa-opaque-token) + +### \属性 + +* **身份验证-管理器-解析器-ref**引用`AuthenticationManagerResolver`,它将在请求时解析`AuthenticationManager` + +* **承载-令牌-解析器-ref**引用一个`BearerTokenResolver`,它将从请求中检索不记名令牌 + +* **入口点-参考**引用将处理未授权请求的`AuthenticationEntryPoint` + +## \ + +表示将授权 JWTS 的 OAuth2.0 资源服务器 + +### \的父元素 + +* [OAuth2-资源服务器](#nsa-oauth2-resource-server) + +### \属性 + +* **jwt-认证-转换器-ref**引用`Converter` + +* **JWT-decoder-ref**引用`JwtDecoder`。这是一个覆盖`jwk-set-uri`的较大组件 + +* **JWK-SET-URI**用于从 OAuth2.0 授权服务器加载签名验证密钥的 JWK 设置 URI + +## \ + +表示将授权不透明令牌的 OAuth2.0 资源服务器 + +### \的父元素 + +* [OAuth2-资源服务器](#nsa-oauth2-resource-server) + +### \属性 + +* **内省-Ref**引用`OpaqueTokenIntrospector`。这是一个覆盖`introspection-uri`、`client-id`和`client-secret`的较大组件。 + +* **内省-URI**用于内省不透明令牌细节的内省 URI。应附上`client-id`和`client-secret`。 + +* **客户端-ID**针对所提供的`introspection-uri`用于客户端身份验证的客户端 ID。 + +* **客户端-机密**针对所提供的`introspection-uri`用于客户端身份验证的客户端秘密。 + +## \ + +将`BasicAuthenticationFilter`和`BasicAuthenticationEntryPoint`添加到配置中。只有在未启用基于表单的登录时,才将后者用作配置入口点。 + +### \的父元素 + +* [http](#nsa-http) + +### \属性 + +* **身份验证-详细信息-源代码-引用**引用将由身份验证过滤器使用的`AuthenticationDetailsSource` + +* **入口点-参考**设置`AuthenticationEntryPoint`所使用的`BasicAuthenticationFilter`。 + +## \元素 + +这是一个顶级元素,可用于将`HttpFirewall`的自定义实现注入到由名称空间创建的`FilterChainProxy`中。默认的实现应该适合大多数应用程序。 + +### \属性 + +* **参考**定义了对实现`HttpFirewall`的 Spring Bean 的引用。 + +## \ + +这个元素用于定义应用程序感兴趣的 URL 模式集,并配置它们应该如何处理。它用于构造`FilterInvocationSecurityMetadataSource`所使用的`FilterSecurityInterceptor`。例如,如果需要通过 HTTPS 访问特定的 URL,它还负责配置`ChannelProcessingFilter`。当将指定的模式与传入的请求匹配时,将按照声明元素的顺序进行匹配。因此,最具体的模式应该排在第一位,最一般的模式应该排在最后。 + +### \的父元素 + +* [过滤器-安全性-元数据-源](#nsa-filter-security-metadata-source) + +* [http](#nsa-http) + +### \属性 + +* **访问**列出了将存储在`FilterInvocationSecurityMetadataSource`中的用于定义的 URL 模式/方法组合的访问属性。这应该是一个以逗号分隔的安全配置属性列表(例如角色名称)。 + +* **方法**将与模式和 Servlet 路径(可选)结合使用的 HTTP 方法,以匹配传入请求。如果省略,任何方法都将匹配。如果在使用和不使用方法的情况下指定了相同的模式,则方法特定的匹配将优先。 + +* **模式**定义 URL 路径的模式。内容将依赖于包含 HTTP 元素的`request-matcher`属性,因此将默认为 Ant 路径语法。 + +* **request-matcher-ref**对`RequestMatcher`的引用,该引用将用于确定是否使用了``。 + +* **需要-通道**可以是“HTTP”或“HTTPS”,这取决于是否应该分别通过 HTTP 或 HTTPS 访问特定的 URL 模式。或者,当没有偏好时,可以使用值“any”。如果这个属性存在于任何``元素上,那么`ChannelProcessingFilter`将被添加到筛选器堆栈中,并将其附加的依赖项添加到应用程序上下文中。 + +如果添加了``配置,这将被`SecureChannelProcessor`和`InsecureChannelProcessor`bean 用于确定用于重定向到 HTTP/HTTPS 的端口。 + +| |此属性对[过滤器-安全性-元数据-源](#nsa-filter-security-metadata-source)无效| +|---|----------------------------------------------------------------------------------------------------| + +* **servlet-path**将与模式和 HTTP 方法结合使用的 Servlet 路径,以匹配传入请求。此属性仅在[请求匹配程序](#nsa-http-request-matcher)为“mvc”时才适用。另外,该值仅在以下 2 个用例中需要:1)在`ServletContext`中注册的`HttpServlet`中有 2 个或更多的`HttpServlet`具有以`'/'`开头的映射,并且它们是不同的;2)模式以注册的`HttpServlet`路径的相同值开始,排除默认(根)`HttpServlet``'/'`。 + +| |此属性对[过滤器-安全性-元数据-源](#nsa-filter-security-metadata-source)无效| +|---|----------------------------------------------------------------------------------------------------| + +## \ + +将 J2eepreAuthenticatedProcessingFilter 添加到筛选链中,以提供与容器身份验证的集成。 + +### \的父元素 + +* [http](#nsa-http) + +### \属性 + +* **可映射角色**在传入的 HttpServletRequest 中要查找的逗号分隔的角色列表。 + +* **user-service-ref**对用户服务(或 UserDetailsService Bean)ID 的引用 + +## \ + +将`LogoutFilter`添加到筛选器堆栈。这被配置为`SecurityContextLogoutHandler`。 + +### \的父元素 + +* [http](#nsa-http) + +### \属性 + +* **删除-cookies**用户注销时应删除的 cookie 名称的逗号分隔列表。 + +* **invalidate-session**映射到`invalidateHttpSession`的`SecurityContextLogoutHandler`。默认值为“true”,因此会话将在注销时无效。 + +* **注销-成功-URL**用户退出后将被带到的目标 URL。默认为 \/?注销(即/登录?注销) + + 设置此属性将注入`SessionManagementFilter`,并使用配置有该属性值的`SimpleRedirectInvalidSessionStrategy`。当提交了无效的会话ID 时,将调用该策略,将其重定向到配置的 URL。 + +* **注销-URL**将导致注销的 URL(即将由过滤器处理的 URL)。默认为“/注销”。 + +* **Success-Handler-Ref**可用于提供`LogoutSuccessHandler`的实例,该实例将在注销后被调用以控制导航。 + +## \ + +类似于``并具有相同的属性。`login-processing-url`的默认值是“/login/openid”。将注册`OpenIDAuthenticationFilter`和`OpenIDAuthenticationProvider`。后者需要引用`UserDetailsService`。同样,这可以由`id`指定,使用`user-service-ref`属性,或者将自动位于应用程序上下文中。 + +### \的父元素 + +* [http](#nsa-http) + +### \属性 + +* **总是-使用-默认-目标**用户登录后是否总是应该重定向到 default-target-url。 + +* **身份验证-详细信息-源代码-引用**引用验证过滤器将使用的验证详细信息源 + +* **身份验证-失败-处理程序-ref**引用验证失败处理程序 Bean,该处理程序应用于处理失败的验证请求。不应该与身份验证-失败-URL 组合使用,因为实现应该始终处理到后续目标的导航。 + +* **身份验证-失败-URL**登录失败页面的 URL。如果没有指定登录失败的 URL, Spring Security 将在/login?Login\_Error 自动创建一个失败的登录 URL 和一个相应的过滤器,以便在请求时呈现该登录失败的 URL。 + +* **认证-成功-转发-URL**将`ForwardAuthenticationSuccessHandler`映射到`authenticationSuccessHandler`的属性`UsernamePasswordAuthenticationFilter`。 + +* **认证-失败-转发-URL**将`ForwardAuthenticationFailureHandler`映射到`authenticationFailureHandler`的属性`UsernamePasswordAuthenticationFilter`。 + +* **认证-成功-处理程序-引用**引用一个验证成功处理了一个成功的验证请求的验证成功处理程序 Bean。不应与[默认值-目标-URL](#nsa-openid-login-default-target-url)(或[总是-使用-默认-目标](#nsa-openid-login-always-use-default-target))组合使用,因为实现应该始终处理到后续目标的导航 + +* **默认值-目标-URL**如果用户之前的操作无法恢复,则在身份验证成功后将被重定向到的 URL。如果用户访问登录页面而没有首先请求触发身份验证的安全操作,则通常会发生这种情况。如果未指定,则默认为应用程序的根。 + +* **登录页面**登录页面的 URL。如果没有指定登录 URL, Spring Security 将在/Login 处自动创建一个登录 URL 和一个相应的过滤器,以便在需要时呈现该登录 URL。 + +* **登录-处理-URL**登录表单发布到的 URL。如果未指定,则默认为/登录。 + +* **密码参数**包含密码的请求参数的名称。默认为“密码”。 + +* **user-service-ref**对用户服务(或 UserDetailsService Bean)ID 的引用 + +* **用户名参数**包含用户名的请求参数的名称。默认为“用户名”。 + +### \的子元素 + +* [属性交换](#nsa-attribute-exchange) + +## \ + +`attribute-exchange`元素定义了应该从标识提供程序请求的属性列表。在名称空间配置章节的[OpenID 支持](../../authentication/openid.html#servlet-openid)部分中可以找到一个示例。可以使用多个属性,在这种情况下,每个属性都必须具有`identifier-match`属性,其中包含一个正则表达式,该正则表达式与提供的 OpenID 标识符匹配。这允许从不同的提供商(谷歌、雅虎等)获取不同的属性列表。 + +### \的父元素 + +* [OpenID-登录](#nsa-openid-login) + +### \属性 + +* **标识符-匹配**当决定在身份验证期间使用哪个属性交换配置时,将与所要求的标识进行比较的正则表达式。 + +### \的子元素 + +* [OpenID-属性](#nsa-openid-attribute) + +## \ + +创建 OpenID AX 时使用的属性[获取请求](https://openid.net/specs/openid-attribute-exchange-1_0.html#fetch_request) + +### \的父元素 + +* [属性交换](#nsa-attribute-exchange) + +### \属性 + +* **计数**指定希望返回的属性的数量。例如,返回 3 封电子邮件。默认值是 1。 + +* **姓名**指定你希望返回的属性的名称。例如,电子邮件。 + +* **必需的**指定此属性是否是 OP 所必需的,但如果 OP 不返回该属性,则不会出错。默认值为 false。 + +* **类型**指定属性类型。例如,[https://axschema.org/contact/email](https://axschema.org/contact/email)。有关有效的属性类型,请参阅你的 OP 文档。 + +## \ + +此元素配置密码管理。 + +### \的父元素 + +* [http](#nsa-http) + +### \属性 + +* **更改-密码-页面**更改密码页面。默认为“/change-password”。 + +## \ + +默认情况下,`PortMapperImpl`的实例将被添加到配置中,用于重定向到安全和不安全的 URL。可以选择使用此元素重写该类定义的默认映射。每个子元素``定义了一对 HTTPS 端口。默认的映射是 80:443 和 8080:8443。可以在[重定向到 HTTPS](../../exploits/http.html#servlet-http-redirect)中找到覆盖这些内容的示例。 + +### \的父元素 + +* [http](#nsa-http) + +### \的子元素 + +* [端口映射](#nsa-port-mapping) + +## \ + +提供一种方法,在强制重定向时将 HTTP 端口映射到 HTTPS 端口。 + +### \的父元素 + +* [端口映射](#nsa-port-mappings) + +### \属性 + +* **HTTP**要使用的 HTTP 端口。 + +* **HTTPS**要使用的 HTTPS 端口。 + +## \ + +将`RememberMeAuthenticationFilter`添加到堆栈。这进而将被配置为使用`TokenBasedRememberMeServices`、`PersistentTokenBasedRememberMeServices`或根据属性设置实现`RememberMeServices`的用户指定的 Bean。 + +### \的父元素 + +* [http](#nsa-http) + +### \属性 + +* 如果需要自定义导航,**认证-成功-处理程序-引用**将在`RememberMeAuthenticationFilter`上设置`authenticationSuccessHandler`属性。该值应该是应用程序上下文中`AuthenticationSuccessHandler` Bean 的名称。 + +* **data-source-ref**对`DataSource` Bean 的引用。如果设置了这个值,将使用`PersistentTokenBasedRememberMeServices`并将其配置为`JdbcTokenRepositoryImpl`实例。 + +* **Remember-me-parameter**切换 Remember-Me 身份验证的请求参数的名称。默认值为“Remember-me”。映射到`AbstractRememberMeServices`的“参数”属性。 + +* **Remember-me-cookie**存储用于 Remember-Me 身份验证的令牌的 Cookie 的名称。默认值为“Remember-me”。映射到`AbstractRememberMeServices`的“cookiename”属性。 + +* **钥匙**映射到`AbstractRememberMeServices`的“键”属性。应该设置为唯一值,以确保 Remember-Me cookies 仅在一个应用程序[3]内有效。如果没有设置这个值,就会生成一个安全的随机值。由于生成安全的随机值可能需要一段时间,所以在使用 Remember-Me 功能时,显式地设置该值可以帮助提高启动时间。 + +* **服务-别名**将内部定义的`RememberMeServices`导出为 Bean 别名,允许它被应用程序上下文中的其他 bean 使用。 + +* **服务-参考**允许对过滤器将使用的`RememberMeServices`实现进行完全控制。在实现该接口的应用程序上下文中,值应该是 Bean 中的[gt r=“669”/>。如果正在使用注销过滤器,也应该实现`LogoutHandler`。 + +* **Token-Repository-ref**配置了`PersistentTokenBasedRememberMeServices`,但允许使用自定义的`PersistentTokenRepository` Bean。 + +* **令牌-有效性-秒**映射到`tokenValiditySeconds`的`AbstractRememberMeServices`属性。指定 rememe-me cookie 的有效时间(以秒为单位)。默认情况下,它的有效期为 14 天。 + +* **使用安全 cookie**建议 Remember-Me Cookie 仅通过 HTTPS 提交,因此应标记为“Secure”。默认情况下,如果发出登录请求的连接是安全的(应该是安全的),则将使用安全的 cookie。如果将此属性设置为`false`,则不会使用安全 cookie。将其设置为`true`将始终设置 cookie 上的安全标志。此属性映射到`AbstractRememberMeServices`的`useSecureCookie`属性。 + +* **user-service-ref**Remember-Me 服务实现需要访问`UserDetailsService`,因此必须在应用程序上下文中定义一个。如果只有一个,它将被命名空间配置自动选择和使用。如果有多个实例,则可以使用此属性显式地指定 Bean `id`。 + +## \元素 + +设置`RequestCache`实例,该实例将被`ExceptionTranslationFilter`用于在调用`AuthenticationEntryPoint`之前存储请求信息。 + +### \的父元素 + +* [http](#nsa-http) + +### \属性 + +* **参考**定义了对 Spring Bean 的引用,即`RequestCache`。 + +## \ + +会话-管理相关的功能是通过向过滤器堆栈添加`SessionManagementFilter`来实现的。 + +### \的父元素 + +* [http](#nsa-http) + +### \属性 + +* **invalid-session-url**设置此属性将为`SessionManagementFilter`注入一个配置有该属性值的`SimpleRedirectInvalidSessionStrategy`。当提交了无效的会话ID 时,将调用该策略,将其重定向到配置的 URL。 + +* **invalid-session-url**允许注入 SessionManagementFilter 使用的 InvalidSessionStrategy 实例。可以使用这个属性,也可以使用`invalid-session-url`属性,但不能同时使用这两个属性。 + +* **session-authentication-error-url**定义了错误页的 URL,当 SessionAuthenticationStrategy 引发异常时,应该显示错误页的 URL。如果未设置,将向客户端返回未经授权的(401)错误代码。请注意,如果错误发生在基于表单的登录过程中,则此属性不适用,在此过程中,验证失败的 URL 将优先处理。 + +* **session-authentication-strategy-ref**允许注入由 SessionManagementFilter 使用的 SessionAuthenticationStrategy 实例 + +* **session-fixation-protection**指示在用户进行身份验证时如何应用会话固定保护。如果设置为“无”,则不会应用任何保护。“NewSession”将创建一个新的空会话,其中只迁移了 Spring 与安全相关的属性。“migratesession”将创建一个新的会话并将所有会话属性复制到新的会话。在 Servlet 3.1(Java EE7)和较新的容器中,指定“ChangeSessionID”将保留现有的会话并使用容器提供的会话固定保护(HttpServletRequest#ChangeSessionID())。在 Servlet 3.1 和较新的容器中,默认为“ChangeSessionID”,在较旧的容器中为“MigrateSession”。如果在较旧的容器中使用了“changesessionID”,则抛出一个异常。 + + 如果会话固定保护被启用,则`SessionManagementFilter`被注入适当配置的`DefaultSessionAuthenticationStrategy`。有关此类的更多详细信息,请参见 Javadoc。 + +### \的子元素 + +* [并发控制](#nsa-concurrency-control) + +## \ + +会话增加了对并发控制的支持,允许对用户可以拥有的活动会话的数量进行限制。将创建一个`ConcurrentSessionFilter`,并将`ConcurrentSessionControlAuthenticationStrategy`与`SessionManagementFilter`一起使用。如果已经声明了`form-login`元素,那么策略对象也将被注入到创建的身份验证筛选器中。将创建一个`SessionRegistry`的实例(一个`SessionRegistryImpl`实例,除非用户希望使用自定义 Bean),以供该策略使用。 + +### \的父元素 + +* [session-management](#nsa-session-management) + +### \属性 + +* 如果将**超过最大值时的错误**设置为“true”,则当用户尝试超过允许的最大会话次数时,将引发`SessionAuthenticationException`。默认的行为是使原始的会话过期。 + +* **已过期的 URL**如果用户试图使用会话,则将被重定向到的 URL,该会话已被并发会话控制器“过期”,因为该用户已超过了允许的会话数量,并已在其他地方再次登录。应该设置,除非设置`exception-if-maximum-exceeded`。如果没有提供任何值,则只需将过期消息直接写回响应。 + +* **已过期的 URL**允许注入 ConcurrentSessionFilter 使用的 ExpiredSessionStrategy 实例 + +* **最大会话**映射到`maximumSessions`的`ConcurrentSessionControlAuthenticationStrategy`属性。指定`-1`作为支持无限会话的值。 + +* **session-registry-alias**在你自己的 bean 或管理接口中使用对内部会话注册表的引用也很有用。你可以使用`session-registry-alias`属性公开内部 Bean,从而为它提供一个可以在配置的其他地方使用的名称。 + +* **session-registry-ref**用户可以使用`session-registry-ref`属性提供他们自己的`SessionRegistry`实现。会话其他并发控制 bean 将被连接起来以使用它。 + +## \ + +添加对 X.509 身份验证的支持。一个`X509AuthenticationFilter`将被添加到堆栈中,并且将创建一个`Http403ForbiddenEntryPoint` Bean。后者仅在不使用其他身份验证机制的情况下使用(其唯一功能是返回 HTTP403 错误代码)。还将创建一个`PreAuthenticatedAuthenticationProvider`,它将用户权限的加载委托给`UserDetailsService`。 + +### \的父元素 + +* [http](#nsa-http) + +### \属性 + +* **身份验证-详细信息-源代码-引用**对`AuthenticationDetailsSource`的引用 + +* **主体-主体-正则表达式**定义了一个正则表达式,它将用于从证书中提取用户名(用于`UserDetailsService`)。 + +* 在配置了多个实例的情况下,**user-service-ref**允许在 X.509 中使用特定的`UserDetailsService`。如果没有设置,将尝试自动定位一个合适的实例并使用它。 + +## \ + +用于显式地配置带有 filterchainmap 的 filterchainproxy 实例 + +### \属性 + +* **请求匹配程序**定义了用于匹配传入请求的策略。目前的选项是“ Ant”(用于 Ant 路径模式),“regex”用于正则表达式,“ciregex”用于不区分大小写的正则表达式。 + +### \的子元素 + +* [过滤链](#nsa-filter-chain) + +## \ + +内用于定义特定的 URL 模式和应用于匹配该模式的 URL 的筛选器列表。当为了配置 FilterChainProxy 而将多个筛选链元素组合在一个列表中时,最特定的模式必须放在列表的顶部,而大多数一般的模式则放在底部。 + +### \的父元素 + +* [filter-chain-map](#nsa-filter-chain-map) + +### \属性 + +* **过滤器**对实现`Filter`的 Spring bean 的引用的逗号分隔列表。值“none”表示对于这个`FilterChain`应该使用 no`Filter`。 + +* **模式**结合[请求匹配程序](#nsa-filter-chain-map-request-matcher)创建请求匹配器的模式 + +* **request-matcher-ref**对`RequestMatcher`的引用,该引用将用于确定是否应该调用来自`filters`属性的任何`Filter`。 + +## \ + +用于显式地配置一个 FilterSecurityMetaDataSource Bean,以便与 FilterSecurityInterceptor 一起使用。通常仅在显式配置 FilterChainProxy 时才需要,而不是使用 \元素。使用的截取 URL 元素应该只包含模式、方法和访问属性。任何其他操作都会导致配置错误。 + +### \属性 + +* **身份证** Bean 标识符,用于在上下文的其他地方引用 Bean。 + +* **请求匹配程序**定义了用于匹配传入请求的策略。当前的选项是“ Ant”(用于 Ant 路径模式),“regex”用于正则表达式,“ciregex”用于不区分大小写的正则表达式。 + +* **使用表达式**允许在 \元素中的“access”属性中使用表达式,而不是使用传统的配置属性列表。默认值为“true”。如果启用,每个属性应该包含一个布尔表达式。如果表达式计算为“true”,则将授予访问权限。 + +### \的子元素 + +* [截取-URL](#nsa-intercept-url) + +--- + +[1](#_footnoteref_1)。参见 Xref: Servlet/configuration/xml-namespace.ADOC#ns-web-xml[介绍性章节] + +[2](#_footnoteref_2)。此功能实际上只是为了方便而提供的,并不是用于生产(在生产过程中,将选择一种视图技术,并可用于呈现定制的登录页面)。类`DefaultLoginPageGeneratingFilter`负责呈现登录页面,并在需要时为普通表单登录和/或 OpenID 提供登录表单。 + +[3](#_footnoteref_3)。这不会影响`PersistentTokenBasedRememberMeServices`的使用,在这里,令牌存储在服务器端。 + +[认证服务](authentication-manager.html)[方法安全性](method-security.html) + diff --git a/docs/spring-security/servlet-appendix-namespace-ldap.md b/docs/spring-security/servlet-appendix-namespace-ldap.md new file mode 100644 index 0000000000000000000000000000000000000000..98b80d46f3d6b585164b212aa72b5560cb84f816 --- /dev/null +++ b/docs/spring-security/servlet-appendix-namespace-ldap.md @@ -0,0 +1,114 @@ +# LDAP 命名空间选项 + +LDAP 实现广泛地使用 Spring LDAP,因此熟悉该项目的 API 可能是有用的。 + +## 使用以下命令定义 LDAP 服务器 + +``元素此元素设置一个 Spring LDAP`ContextSource`供其他 LDAP bean 使用,定义 LDAP 服务器的位置和用于连接它的其他信息(例如用户名和密码,如果它不允许匿名访问的话)。它还可以用于创建用于测试的嵌入式服务器。这两个选项的语法细节在[LDAP 章节](../../authentication/passwords/ldap.html#servlet-authentication-ldap)中都有涉及。实际的`ContextSource`实现是`DefaultSpringSecurityContextSource`,它扩展了 Spring LDAP 的`LdapContextSource`类。`manager-dn`和`manager-password`属性分别映射到后者的`userDn`和`password`属性。 + +如果在应用程序上下文中只定义了一个服务器,那么其他 LDAP 命名空间定义的 bean 将自动使用它。否则,你可以给元素一个“id”属性,并使用`server-ref`属性从其他名称空间 bean 引用它。这实际上是`id`实例的 Bean `id`,如果你想在其他传统的 Spring bean 中使用它的话。 + +### \属性 + +* **模式**显式指定应该使用哪个嵌入式 LDAP 服务器。值为`apacheds`和`unboundid`。默认情况下,这将取决于 Classpath 中的库是否可用。 + +* **身份证**一个 Bean 标识符,用于在上下文的其他地方引用 Bean。 + +* **LDIF**显式指定要加载到嵌入式 LDAP 服务器中的 LDIF 文件资源。LDIF 应该是一个 Spring 资源模式(即 Classpath:init.ldif)。默认值是 Classpath \*:\*.ldif + +* **Manager-dn**将用于对(非嵌入式)LDAP 服务器进行身份验证的“Manager”用户标识的用户名。如果省略,将使用匿名访问。 + +* **管理器-密码**Manager DN 的密码。如果指定了 Manager-DN,这是必需的。 + +* **港口**指定 IP 端口号。例如,用于配置嵌入式 LDAP 服务器。默认值是 33389。 + +* **根**嵌入式 LDAP 服务器的可选根后缀。默认为“dc=springframework,dc=org” + +* **网址**指定不使用嵌入式 LDAP 服务器时的 LDAP 服务器 URL。 + +## \ + +这个元素是创建`LdapAuthenticationProvider`实例的简写。默认情况下,这将被配置为`BindAuthenticator`实例和`DefaultAuthoritiesPopulator`实例。与所有名称空间身份验证提供者一样,它必须作为`authentication-provider`元素的子元素包含。 + +### \的父元素 + +* [身份验证管理器](authentication-manager.html#nsa-authentication-manager) + +### \属性 + +* **组-角色-属性**包含将在 Spring Security 中使用的角色名的 LDAP 属性名。映射到`DefaultLdapAuthoritiesPopulator`的`groupRoleAttribute`属性。默认为“cn”。 + +* **群组搜索基础**搜索组成员搜索的基础。映射到`DefaultLdapAuthoritiesPopulator`的`groupSearchBase`构造函数参数。默认值为“”(从根搜索)。 + +* **组搜索过滤器**组搜索过滤器。映射到`DefaultLdapAuthoritiesPopulator`的`groupSearchFilter`属性。默认值为`(uniqueMember={0})`。替换的参数是用户的 DN。 + +* **角色前缀**一个非空的字符串前缀,它将被添加到从 Persistent 加载的角色字符串中。映射到`DefaultLdapAuthoritiesPopulator`的`rolePrefix`属性。默认值为“role\_”。在默认值为非空的情况下,使用值“none”表示无前缀。 + +* **服务器-ref**可选使用的服务器。如果省略,并且注册了一个默认的 LDAP 服务器(使用没有 ID 的 \),则将使用该服务器。 + +* **user-context-mapper-ref**通过指定一个 UserDetailsContextMapper Bean,允许对加载的用户对象进行显式定制,该用户对象将根据用户目录条目中的上下文信息被调用 + +* **用户-详细信息-类**允许指定用户条目的 ObjectClass。如果设置了,框架将尝试将定义的类的标准属性加载到返回的 UserDetails 对象中 + +* **user-dn-pattern**如果你的用户在目录中的固定位置(即你可以直接从用户名计算出 DN,而无需进行目录搜索),则可以使用此属性直接映射到 DN。它直接映射到`userDnPatterns`的`AbstractLdapAuthenticator`属性。该值是用于构建用户 DN 的特定模式,例如`uid={0},ou=people`。键`{0}`必须存在,并将被用户名代替。 + +* **用户搜索基础**用户搜索的搜索基础。默认值为“”。仅与“用户搜索过滤器”一起使用。 + + 如果需要执行搜索以在目录中定位用户,则可以设置这些属性来控制搜索。`BindAuthenticator`将被配置为`FilterBasedLdapUserSearch`,并且属性值直接映射到该 Bean 构造函数的前两个参数。如果这些属性 AREN 没有设置,并且没有提供`user-dn-pattern`作为替代,那么将使用`user-search-filter="(uid={0})"`和`user-search-base=""`的默认搜索值。 + +* **用户搜索过滤器**用于搜索用户的 LDAP 过滤器(可选)。例如`(uid={0})`。替换的参数是用户的登录名。 + + 如果需要执行搜索以在目录中定位用户,则可以设置这些属性来控制搜索。`BindAuthenticator`将被配置为`FilterBasedLdapUserSearch`,并且属性值直接映射到该 Bean 构造函数的前两个参数。如果没有设置这些属性 AREN,并且没有提供`user-dn-pattern`作为替代选项,那么将使用`user-search-filter="(uid={0})"`和`user-search-base=""`的默认搜索值。 + +### \的子元素 + +* [密码-比较](#nsa-password-compare) + +## \ + +这被用作``的子元素,并将身份验证策略从`BindAuthenticator`切换到`PasswordComparisonAuthenticator`。 + +### \的父元素 + +* [LDAP-身份验证-提供者](#nsa-ldap-authentication-provider) + +### \属性 + +* **散列**定义了用于用户密码的散列算法。我们强烈建议不要使用 MD4,因为它是一种非常弱的散列算法。 + +* **密码属性**目录中包含用户密码的属性。默认值为“userpassword”。 + +### \的子元素 + +* [密码编码器](authentication-manager.html#nsa-password-encoder) + +## \ + +这个元素配置一个 LDAP`UserDetailsService`。所使用的类是`LdapUserDetailsService`,它是`FilterBasedLdapUserSearch`和`DefaultLdapAuthoritiesPopulator`的组合。它支持的属性具有与``中相同的用法。 + +### \属性 + +* **cache-ref**定义了对缓存的引用,以便与 UserDetailsService 一起使用。 + +* **组-角色-属性**包含将在 Spring Security 中使用的角色名的 LDAP 属性名。默认为“cn”。 + +* **群组搜索基础**搜索组成员搜索的基础。默认值为“”(从根搜索)。 + +* **组搜索过滤器**组搜索过滤器。默认值为`(uniqueMember={0})`。替换的参数是用户的 DN。 + +* **身份证** Bean 标识符,用于在上下文的其他地方引用 Bean。 + +* **角色前缀**一个非空的字符串前缀,它将被添加到从持久存储中加载的角色字符串中(例如“role\_”)。在默认值为非空的情况下,使用值“none”表示无前缀。 + +* **服务器-ref**可选使用的服务器。如果省略,并且注册了一个缺省的 LDAP 服务器(使用没有 ID 的 \),则将使用该服务器。 + +* **user-context-mapper-ref**通过指定一个 UserDetailsContextMapper Bean,允许对加载的用户对象进行显式定制,该用户对象将根据用户目录条目中的上下文信息被调用 + +* **用户-详细信息-类**允许指定用户条目的 ObjectClass。如果设置了,框架将尝试将已定义类的标准属性加载到返回的 UserDetails 对象中 + +* **用户搜索基础**用户搜索的搜索基础。默认值为“”。仅与“用户搜索过滤器”一起使用。 + +* **用户搜索过滤器**用于搜索用户的 LDAP 过滤器(可选)。例如`(uid={0})`。替换的参数是用户的登录名。 + +[方法安全性](method-security.html)[WebSocket Security](websocket.html) + diff --git a/docs/spring-security/servlet-appendix-namespace-method-security.md b/docs/spring-security/servlet-appendix-namespace-method-security.md new file mode 100644 index 0000000000000000000000000000000000000000..a9a40f5dc57619a5b1d024fe49d310a806c31c30 --- /dev/null +++ b/docs/spring-security/servlet-appendix-namespace-method-security.md @@ -0,0 +1,180 @@ +# 方法安全性 + +## \ + +这个元素是在 Spring Security bean 上添加对方法安全的支持的主要手段。方法可以通过使用注释(在接口或类级别上定义)或通过定义一组切入点来保护。 + +### \属性 + +* **pre-post-enabled **为此应用程序上下文启用 Spring Security 的 pre 和 post 调用注释(@prefilter、@preauthorize、@postfilter、@postauthorize)。默认值为“true”。 + +* **安全启用**为此应用程序上下文启用 Spring Security 的 @Secured 注释。默认为“false”。 + +* **支持 JSR250**为此应用程序上下文启用 JSR-250 授权注释(@roleslowled,@permitall,@denyall)。默认为“false”。 + +* **代理-目标-类**如果为真,将使用基于类的代理,而不是基于接口的代理。默认为“false”。 + +### \的子元素 + +* [表达式处理程序](http.html#nsa-expression-handler) + +## \ + +这个元素是在 Spring Security bean 上添加对方法安全的支持的主要手段。方法可以通过使用注释(在接口或类级别定义)或使用 AspectJ 语法将一组切入点定义为子元素来保护。 + +### \属性 + +* **access-decision-manager-ref **Method Security 使用与 Web Security 相同的`AccessDecisionManager`配置,但是可以使用此属性重写此配置。默认情况下,基于确认的实现用于 RoleVoter 和 AuthentidVoter。 + +* **认证-管理器-ref **对方法安全性应该使用的`AuthenticationManager`的引用。 + +* **JSR250-注释**指定是否使用 JSR-250 样式属性(例如“允许滚动”)。这将需要 Classpath 上的 javax.annotation.security 类。将此设置为 true 还会为`Jsr250Voter`添加`AccessDecisionManager`,因此,如果你正在使用自定义实现并希望使用这些注释,则需要确保执行此操作。 + +* **元数据-源-参考**可以提供一个外部`MethodSecurityMetadataSource`实例,该实例将优先于其他源(例如默认注释)。 + +* **模式**此属性可以设置为“AspectJ”,以指定应该使用 AspectJ,而不是默认的 Spring AOP。安全方法必须与`spring-security-aspects`模块中的`AnnotationSecurityAspect`交织在一起。 + +需要注意的是,AspectJ 遵循了 Java 的规则,即接口上的注释不会被继承。这意味着在接口上定义安全注释的方法将不受保护。相反,在使用 AspectJ 时,你必须在类上放置安全注释。 + +* **秩序**允许为方法安全性拦截器设置通知“order”。 + +* **pre-post 注释**指定是否应为此应用程序上下文启用 Spring Security 的 pre 和 post 调用注释(@prefilter、@preauthorize、@postfilter、@postauthorize)。默认值为“禁用”。 + +* **代理-目标-类**如果为真,将使用基于类的代理,而不是基于接口的代理。 + +* **Run-as-manager-ref **对可选`RunAsManager`实现的引用,该实现将由配置的`MethodSecurityInterceptor`使用 + +* **安全注释**指定是否应为此应用程序上下文启用 Spring Security 的 @Secured 注释。默认值为“禁用”。 + +### \的子元素 + +* [后调用-提供者](#nsa-after-invocation-provider) + +* [表达式处理程序](http.html#nsa-expression-handler) + +* [后注解前处理](#nsa-pre-post-annotation-handling) + +* [保护切入点](#nsa-protect-pointcut) + +## \ + +这个元素可以用来装饰`AfterInvocationProvider`,以便由``命名空间维护的安全拦截器使用。你可以在`global-method-security`元素中定义零个或更多个,每个元素都有一个`ref`属性,指向应用程序上下文中的`AfterInvocationProvider` Bean 实例。 + +### \的父元素 + +* [全局方法安全性](#nsa-global-method-security) + +### \属性 + +* **参考**定义了对实现`AfterInvocationProvider`的 Spring Bean 的引用。 + +## \ + +允许完全替换用于处理 Spring 安全性的 pre 和 post 调用注释(@prefilter、@preauthorize、@postfilter、@postauthorize)的缺省基于表达式的机制。仅在启用了这些注释的情况下才会应用。 + +### \的父元素 + +* [全局方法安全性](#nsa-global-method-security) + +### \的子元素 + +* [调用-属性-工厂](#nsa-invocation-attribute-factory) + +* [调用后-建议](#nsa-post-invocation-advice) + +* [调用前的建议](#nsa-pre-invocation-advice) + +## \ + +定义 prepostinvocationAttributeFactory 实例,该实例用于从带注释的方法生成 pre 和 post 调用元数据。 + +### \的父元素 + +* [后注解前处理](#nsa-pre-post-annotation-handling) + +### \属性 + +* **参考**定义了对 Spring Bean ID 的引用。 + +## \ + +将`PostInvocationAdviceProvider`与 ref 一起定制为 \`PostInvocationAuthorizationAdvice`元素的`PostInvocationAuthorizationAdvice`。 + +### \的父元素 + +* [后注解前处理](#nsa-pre-post-annotation-handling) + +### \属性 + +* **参考**定义了对 Spring Bean ID 的引用。 + +## \ + +将`PreInvocationAuthorizationAdviceVoter`与 ref 一起定制为 \`PreInvocationAuthorizationAdviceVoter`元素的`PreInvocationAuthorizationAdviceVoter`。 + +### \的父元素 + +* [后注解前处理](#nsa-pre-post-annotation-handling) + +### \属性 + +* **参考**定义了对 Spring Bean ID 的引用。 + +## 使用固定方法 + +``不是使用`@Secured`注释在单个方法或类的基础上定义安全属性,而是可以使用``元素在服务层中的整个方法和接口集合中定义跨领域的安全约束。你可以在[命名空间介绍](../../authorization/method-security.html#ns-protect-pointcut)中找到一个示例。 + +### \的父元素 + +* [全局方法安全性](#nsa-global-method-security) + +### \属性 + +* **访问**Access Configuration Attributes 列表,该列表应用于匹配切入点的所有方法,例如“role\_a,role\_b” + +* **表达式**AspectJ 表达式,包括`execution`关键字。例如,`execution(int com.foo.TargetObject.countLength(String))`。 + +## \ + +可以在 Bean 定义中使用,以便向 Bean 添加安全拦截器,并为 Bean 的方法设置访问配置属性 + +### \属性 + +* **access-decision-manager-ref **可选的 AccessDecisionManager Bean ID 将被创建的方法安全拦截器使用。 + +### \的子元素 + +* [protect](#nsa-protect) + +## \ + +创建一个 MethodSecurityMetaDataSource 实例 + +### \属性 + +* **身份证** Bean 标识符,用于在上下文的其他地方引用 Bean。 + +* **使用表达式**允许在 \元素中的“access”属性中使用表达式,而不是使用传统的配置属性列表。默认为“false”。如果启用,每个属性应该包含一个布尔表达式。如果表达式计算为“true”,则将授予访问权限。 + +### \的子元素 + +* [protect](#nsa-protect) + +## \ + +定义受保护的方法和应用于该方法的访问控制配置属性。我们强烈建议你不要将“protect”声明与“global-method-security”提供的任何服务混为一谈。 + +### \的父元素 + +* [截取方法](#nsa-intercept-methods) + +* [方法-安全性-元数据-源](#nsa-method-security-metadata-source) + +### \属性 + +* **访问**访问应用于该方法的配置属性列表,例如“role\_a,role\_b”。 + +* **方法**一个方法名 + +[网络安全](http.html)[LDAP 安全性](ldap.html) + diff --git a/docs/spring-security/servlet-appendix-namespace-websocket.md b/docs/spring-security/servlet-appendix-namespace-websocket.md new file mode 100644 index 0000000000000000000000000000000000000000..e86581563113e91ded99f49a670811f7f569934a --- /dev/null +++ b/docs/spring-security/servlet-appendix-namespace-websocket.md @@ -0,0 +1,50 @@ +# WebSocket 安全 + +Spring Security4.0+ 为授权消息提供了支持。这一点很有用的一个具体例子是在基于 WebSocket 的应用程序中提供授权。 + +## \ + +WebSocket-message-broker 元素有两种不同的模式。如果没有指定[[[email protected]](#NSA- WebSocket-message-broker-id),那么它将执行以下操作: + +* 确保任何 SimpAnnotationMethodMessageHandler 都将身份验证原则 argumentResolver 注册为自定义参数解析器。这允许使用`@AuthenticationPrincipal`来解析当前`Authentication`的主体 + +* 确保 SecurityContextChannelInterceptor 已自动注册为 ClientBoundChannel。这将用消息中找到的用户填充 SecurityContextholder + +* 确保通道安全拦截器已在客户端 BoundChannel 中注册。这允许为消息指定授权规则。 + +* 确保一个 CSRFChannelInterceptor 已在 ClientInboundChannel 中注册。这确保只启用来自原始域的请求。 + +* 确保 CSRFTokenhandShakeInterceptor 已注册到 WebSockettPrequesthandler、TransportHandlingSockJSService 或 DefaultSockJSService。这确保了预期的来自 HttpServletRequest 的 CSRFToken 被复制到 WebSocket 会话属性中。 + +如果需要额外的控制,可以指定 ID,并将为指定的 ID 分配一个通道 SecurityInterceptor。然后,所有与 Spring 的消息传递基础设施的连接都可以手动完成。这比较麻烦,但提供了对配置的更大控制。 + +### \属性 + +* **身份证**一个 Bean 标识符,用于在上下文的其他地方引用 ChannelSecurityInterceptor Bean。如果指定了, Spring 安全性要求在 Spring 消息传递中进行显式配置。如果没有指定, Spring 安全性将自动与消息传递基础设施集成,如[\](#nsa-websocket-message-broker)中所述 + +* **同源禁用**禁用在 stomp 标头中存在的 CSRF 令牌的要求(默认为 false)。如果有必要允许其他来源建立 Sockjs 连接,那么更改默认值是有用的。 + +### \的子元素 + +* [表达式处理程序](http.html#nsa-expression-handler) + +* [截取消息](#nsa-intercept-message) + +## \ + +为消息定义授权规则。 + +### \的父元素 + +* [websocket-message-broker](#nsa-websocket-message-broker) + +### \属性 + +* **模式**一种基于 Ant 的模式,它在消息目标上匹配。例如,“/**”匹配带有目的地的任何消息;“/admin/**”匹配带有以“/admin/\***”开头的目的地的任何消息。 + +* **类型**要匹配的消息类型。有效的值在 SimpMessageType 中定义(即 Connect、Connect\_ack、heartbeat、Message、Subscribe、Unsubscribe、Disconnect、Disconnect\_ack、Other)。 + +* **访问**用于保护消息的表达式。例如,“denyall”将拒绝对所有匹配消息的访问;“permitall”将授予对所有匹配消息的访问权限;"HasRole(’admin’)要求当前用户拥有用于匹配消息的角色’role\_admin’。 + +[LDAP 安全性](ldap.html)[FAQ](../faq.html) + diff --git a/docs/spring-security/servlet-appendix-namespace.md b/docs/spring-security/servlet-appendix-namespace.md new file mode 100644 index 0000000000000000000000000000000000000000..871b6862d88f7bb42a73ffd235b452ff51b87a44 --- /dev/null +++ b/docs/spring-security/servlet-appendix-namespace.md @@ -0,0 +1,14 @@ +# 安全命名空间 + +本附录提供了对安全名称空间中可用的元素的引用,以及关于它们创建的底层 bean 的信息(假定了解各个类以及它们如何一起工作-你可以在项目 Javadoc 和本文档中的其他地方找到更多信息)。如果你以前没有使用过名称空间,请阅读关于名称空间配置的[介绍性章节](../../configuration/xml-namespace.html#ns-config),因为这是对那里的信息的补充。建议在编辑基于模式的配置时使用高质量的 XML 编辑器,因为这将提供关于哪些元素和属性可用的上下文信息,以及解释其目的的注释。名称空间以[RELAX NG](https://relaxng.org/)紧凑格式编写,之后转换为 XSD 模式。如果你熟悉这种格式,你可能希望直接检查[模式文件](https://raw.githubusercontent.com/spring-projects/spring-security/main/config/src/main/resources/org/springframework/security/config/spring-security-5.6.rnc)。 + +## 章节摘要 + +* [认证服务](authentication-manager.html) +* [网络安全](http.html) +* [方法安全性](method-security.html) +* [LDAP 安全性](ldap.html) +* [WebSocket Security](websocket.html) + +[数据库模式](../database-schema.html)[认证服务](authentication-manager.html) + diff --git a/docs/spring-security/servlet-appendix.md b/docs/spring-security/servlet-appendix.md new file mode 100644 index 0000000000000000000000000000000000000000..e3728993836f6c8a5d70dcdf8e85c6d39e2a85ab --- /dev/null +++ b/docs/spring-security/servlet-appendix.md @@ -0,0 +1,12 @@ +# 附录 + +这是基于 Servlet 的 Spring 安全性的附录。它有以下几个部分: + +* [数据库模式](database-schema.html) + +* [XML 命名空间](namespace/index.html) + +* [FAQ](faq.html) + +[安全结果制造者](../test/mockmvc/result-handlers.html)[数据库模式](database-schema.html) + diff --git a/docs/spring-security/servlet-architecture.md b/docs/spring-security/servlet-architecture.md new file mode 100644 index 0000000000000000000000000000000000000000..5e5838e37b629e9f0b9f9ba0be91cdd61a41e658 --- /dev/null +++ b/docs/spring-security/servlet-architecture.md @@ -0,0 +1,232 @@ +# 建筑 + +本节讨论 Spring 基于 Servlet 的应用程序中的 Spring 安全性的高级体系结构。我们在参考的[认证](authentication/index.html#servlet-authentication)、[授权](authorization/index.html#servlet-authorization)、[保护免受剥削](exploits/index.html#servlet-exploits)部分中建立了这种高层次的理解。 + +## 《`Filter`s》评介 + +Spring 安全性的 Servlet 支持是基于 Servlet `Filter`s 的,因此通常首先查看`Filter`s 的作用是有帮助的。下图显示了单个 HTTP 请求的处理程序的典型分层。 + +![滤清链](../_images/servlet/architecture/filterchain.png) + +图 1。滤清链 + +客户机向应用程序发送一个请求,容器创建一个`FilterChain`,其中包含`Filter`s 和`Servlet`,它们应该基于请求 URI 的路径来处理`HttpServletRequest`。在 Spring MVC 应用程序中,`Servlet`是[`DispatcherServlet`](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/reference/html/web.html#mvc- Servlet)的一个实例。最多一个`Servlet`可以处理单个`HttpServletRequest`和`HttpServletResponse`。但是,可以使用多个`Filter`来: + +* 阻止调用下游`Filter`s 或`Servlet`。在这种情况下,`Filter`通常会写`HttpServletResponse`。 + +* 修改由下游`Filter`s 和`Servlet`使用的`HttpServletRequest`或`HttpServletResponse` + +`Filter`的幂来自传递到它的`FilterChain`。 + +例 1。`FilterChain`用法示例 + +爪哇 + +``` +public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) { + // do something before the rest of the application + chain.doFilter(request, response); // invoke the rest of the application + // do something after the rest of the application +} +``` + +Kotlin + +``` +fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) { + // do something before the rest of the application + chain.doFilter(request, response) // invoke the rest of the application + // do something after the rest of the application +} +``` + +由于`Filter`只会影响下游`Filter`s 和`Servlet`,因此每次调用`Filter`的顺序是极其重要的。 + +## 委托过滤代理 + +Spring 提供了名为[`Filter`](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/web/filter/delegatingfilterproxy.html)的`Filter`实现,该实现允许在 Servlet 容器的生命周期和 Spring 的`ApplicationContext`之间架桥。 Servlet 容器允许使用自己的标准注册`Filter`s,但它不知道 Spring 定义的 bean。`DelegatingFilterProxy`可以通过标准 Servlet 容器机制注册,但将所有工作委托给实现`Filter`的 Spring Bean。 + +下面是一张`DelegatingFilterProxy`如何与[`Filter`s 和`FilterChain`](# Servlet-filters-review)相匹配的图片。 + +![委托过滤代理](../_images/servlet/architecture/delegatingfilterproxy.png) + +图 2。委托过滤代理 + +`DelegatingFilterProxy`从`ApplicationContext`中查找*Bean Filter0*,然后调用*Bean Filter0*。下面可以看到`DelegatingFilterProxy`的伪代码。 + +例 2。`DelegatingFilterProxy`伪代码 + +爪哇 + +``` +public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) { + // Lazily get Filter that was registered as a Spring Bean + // For the example in DelegatingFilterProxy delegate is an instance of Bean Filter0 + Filter delegate = getFilterBean(someBeanName); + // delegate work to the Spring Bean + delegate.doFilter(request, response); +} +``` + +Kotlin + +``` +fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) { + // Lazily get Filter that was registered as a Spring Bean + // For the example in DelegatingFilterProxy delegate is an instance of Bean Filter0 + val delegate: Filter = getFilterBean(someBeanName) + // delegate work to the Spring Bean + delegate.doFilter(request, response) +} +``` + +`DelegatingFilterProxy`的另一个好处是,它允许延迟查找`Filter` Bean 实例。这一点很重要,因为容器在启动之前需要注册`Filter`实例。然而, Spring 通常使用`ContextLoaderListener`来加载 Spring bean,这在`Filter`实例需要注册之后才能完成。 + +## FilterchainProxy + +Spring Security 的 Servlet 支持包含在`FilterChainProxy`中。`FilterChainProxy`是 Spring Security 提供的一种特殊的`Filter`,它允许通过[`证券过滤链`](# Servlet-SecurityFilterchain)将许多`Filter`实例委托给多个实例。由于`FilterChainProxy`是 Bean,因此它通常被包装在[委托过滤代理](#servlet-delegatingfilterproxy)中。 + +![FilterchainProxy ](../_images/servlet/architecture/filterchainproxy.png) + +图 3。FilterchainProxy + +## SecurityFilterChain + +[`SecurityFilterChain`](https://DOCS. Spring.io/ Spring-security/site/DOCS/5.6.2/api/org/springframework/security/web/securityfilterchain.html)被[FilterchainProxy ](#servlet-filterchainproxy)用于确定应该为此请求调用哪个 Spring security`Filter`s。 + +![证券过滤链](../_images/servlet/architecture/securityfilterchain.png) + +图 4。证券过滤链 + +[安全过滤器](#servlet-security-filters)中的`SecurityFilterChain`通常是 bean,但它们是用`FilterChainProxy`而不是[委托过滤代理](#servlet-delegatingfilterproxy)注册的。`FilterChainProxy`提供了直接向 Servlet 容器或[委托过滤代理](#servlet-delegatingfilterproxy)注册的许多优点。首先,它为 Spring Security 的 Servlet 支持提供了一个起点。因此,如果你试图对 Spring Security 的 Servlet 支持进行故障排除,那么在`FilterChainProxy`中添加一个调试点是一个很好的起点。 + +其次,由于`FilterChainProxy`是 Spring 安全性使用的核心,因此它可以执行不被视为可选的任务。例如,它清除`SecurityContext`以避免内存泄漏。它还应用 Spring Security 的[`HttpFirewall`](exploits/firewall.html# Servlet-HttpFirewall)来保护应用程序免受某些类型的攻击。 + +此外,它在确定何时应该调用`SecurityFilterChain`时提供了更大的灵活性。在 Servlet 容器中,`Filter`s 仅基于 URL 被调用。但是,`FilterChainProxy`可以通过利用`RequestMatcher`接口,基于`HttpServletRequest`中的任何内容来确定调用。 + +事实上,`FilterChainProxy`可以用来确定应该使用哪些`SecurityFilterChain`。这允许为应用程序的不同*切片*提供完全独立的配置。 + +![多证券过滤链](../_images/servlet/architecture/multi-securityfilterchain.png) + +图 5。多重证券过滤链 + +在[多重证券过滤链](#servlet-multi-securityfilterchain-figure)图中,`FilterChainProxy`决定应该使用哪个`SecurityFilterChain`。只调用第一个匹配的`SecurityFilterChain`。如果请求`/api/messages/`的 URL,它将首先匹配`SecurityFilterChain0`的`/api/**`的模式,因此只有`SecurityFilterChain0`将被调用,即使它也匹配`SecurityFilterChainn`。如果请求一个`/messages/`的 URL,它将不匹配`SecurityFilterChain0`的`/api/**`的模式,因此`FilterChainProxy`将继续尝试每个`SecurityFilterChain`。假设没有其他的,`SecurityFilterChain`实例匹配`SecurityFilterChainn`将被调用。 + +请注意,`SecurityFilterChain0`只配置了三个安全`Filter`实例。但是,`SecurityFilterChainn`配置了四个`Filter`的安全性。需要注意的是,每个`SecurityFilterChain`都可以是唯一的,并且可以单独配置。实际上,如果应用程序希望 Spring 安全性忽略某些请求,则`SecurityFilterChain`可能具有零安全性`Filter`s。 + +## 安全过滤器 + +使用[证券过滤链](#servlet-securityfilterchain)API 将安全过滤器插入[FilterchainProxy ](#servlet-filterchainproxy)中。[`Filter`](# Servlet-过滤器-评审)的顺序很重要。通常不需要知道 Spring 证券的`Filter`s 的排序。 + +下面是 Spring 安全过滤器排序的综合列表: + +* 通道处理滤波器 + +* WebAsyncManagerIntegrationFilter + +* SecurityContextPersistenceFilter + +* HeaderWriterFilter + +* Corsfilter + +* Csrffilter + +* LogoutFilter + +* OAuth2AuthorizationRequestReDirectFilter + +* SAML2WebssoAuthenticationRequestFilter + +* X509 身份验证过滤器 + +* 抽象预先验证的处理过滤器 + +* CasauthenticationFilter + +* OAuth2 登录验证过滤器 + +* SAML2WebssoAuthenticationFilter + +* [`UsernamePasswordAuthenticationFilter`](身份验证/密码/form.html# Servlet-身份验证-usernamepasswordauthenticationfilter) + +* openidauthenticationfilter + +* DefaultLoginPageGeneratingFilter + +* DefaultLogOutpageGeneratingFilter + +* ConcurrentSessionFilter + +* [`DigestAuthenticationFilter`](authentication/passwords/digest.html# Servlet-authentication-digest) + +* BeareRtoKenAuthenticationFilter + +* [`BasicAuthenticationFilter`](authentication/passwords/basic.html# Servlet-authentication-basic) + +* RequestCacheAwareFilter + +* SecurityContextholderAwareRequestFilter + +* JaasapiIntegrationfilter + +* RememerMeAuthenticationFilter + +* 匿名身份验证过滤器 + +* OAuth2AuthorizationCodeGrantFilter + +* SessionManagementFilter + +* [`ExceptionTranslationFilter`](# Servlet-ExceptionTranslationFilter) + +* [`FilterSecurityInterceptor`](授权/authorization-requests.html# Servlet-authorization-filtersecurityinterceptor) + +* 切换 chuserfilter + +## 处理安全异常 + +[`ExceptionTranslationFilter`](https://DOCS. Spring.io/ Spring-security/site/DOCS/5.6.2/api/org/springframework/security/web/access/excess/exceptiontranslationfilter.html)允许将[`AccessDeniedException`](https://DOCS. Spring.io/ Spring.io/ Spring-cesecurity/site/DOCS/5.6.2/api/api/org/accessframework/secframework/securance/securance/securation/acception/acc + +`ExceptionTranslationFilter`作为[安全过滤器](#servlet-security-filters)中的一个插入到[FilterchainProxy ](#servlet-filterchainproxy)中。 + +![ExceptionTranslationFilter ](../_images/servlet/architecture/exceptiontranslationfilter.png) + +* ![number 1](../_images/icons/number_1.png)首先,`ExceptionTranslationFilter`调用`FilterChain.doFilter(request, response)`来调用应用程序的其余部分。 + +* ![number 2](../_images/icons/number_2.png)如果用户没有经过身份验证,或者它是`AuthenticationException`,那么*启动身份验证*。 + + * [SecurityContextholder ](authentication/architecture.html#servlet-authentication-securitycontextholder)已清除 + + * `HttpServletRequest`保存在[`RequestCache`](https://DOCS. Spring.io/ Spring-security/site/DOCS/5.6.2/api/org/springframework/security/web/savedrequest/requestcache.html)中。当用户成功进行身份验证时,将使用`RequestCache`重播原始请求。 + + * `AuthenticationEntryPoint`用于从客户机请求凭据。例如,它可能重定向到一个登录页面,或者发送一个`WWW-Authenticate`头。 + +* ![number 3](../_images/icons/number_3.png)否则如果是`AccessDeniedException`,则*访问被拒绝*。调用`AccessDeniedHandler`来处理拒绝访问。 + +| |如果应用程序不抛出`AccessDeniedException`或`AuthenticationException`,则`ExceptionTranslationFilter`不执行任何操作。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------| + +`ExceptionTranslationFilter`的伪代码如下所示: + +ExceptionTranslationFilter 伪码 + +``` +try { + filterChain.doFilter(request, response); (1) +} catch (AccessDeniedException | AuthenticationException ex) { + if (!authenticated || ex instanceof AuthenticationException) { + startAuthentication(); (2) + } else { + accessDenied(); (3) + } +} +``` + +|**1**|你可以从[对`Filter`s 的回顾](# Servlet-filters-review)中回忆起,调用`FilterChain.doFilter(request, response)`相当于调用应用程序的其余部分。
这意味着,如果应用程序的另一部分,(即[`FilterSecurityInterceptor`](Authorization/authorization-requests.html# Servlet-authorization-filterSecurityInterceptor)或方法安全性)抛出一个`AuthenticationException`或`AccessDeniedException`将在此捕获并处理。| +|-----|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +|**2**|如果用户没有经过身份验证,或者它是`AuthenticationException`,那么*启动身份验证*。| +|**3**|否则,*访问被拒绝*| + +[开始](getting-started.html)[认证](authentication/index.html) + diff --git a/docs/spring-security/servlet-authentication-anonymous.md b/docs/spring-security/servlet-authentication-anonymous.md new file mode 100644 index 0000000000000000000000000000000000000000..83cd7f1dc995f321c017d1c479aa4de8adcb735f --- /dev/null +++ b/docs/spring-security/servlet-authentication-anonymous.md @@ -0,0 +1,117 @@ +# 匿名身份验证 + +## 概述 + +通常认为,采用“默认拒绝”(deny-by-default)是一种良好的安全实践,在这种情况下,你可以明确指定允许的内容,而不允许其他任何内容。定义未经身份验证的用户可以访问的内容也是类似的情况,特别是对于 Web 应用程序。许多网站要求用户必须对除几个 URL(例如主页和登录页面)以外的任何内容进行身份验证。在这种情况下,最容易为这些特定的 URL 定义访问配置属性,而不是为每个安全资源定义访问配置属性。换句话说,有时说`ROLE_SOMETHING`是缺省情况下需要的,并且只允许此规则的某些例外情况,例如应用程序的登录、注销和主页,这是很好的。你还可以从筛选链中完全省略这些页面,从而绕过访问控制检查,但是由于其他原因,这可能是不希望的,特别是如果对于经过身份验证的用户,页面的行为不同。 + +这就是我们所说的匿名身份验证。请注意,“匿名身份验证”的用户和未经身份验证的用户之间没有真正的概念上的区别。 Spring 安全性的匿名身份验证为你提供了一种更方便的方式来配置访问控制属性。例如,对 Servlet API 的调用(例如`getCallerPrincipal`)仍将返回 null,即使在`SecurityContextHolder`中实际上存在匿名身份验证对象。 + +在其他情况下,匿名身份验证是有用的,例如,当审计拦截器查询`SecurityContextHolder`以确定哪个主体负责给定的操作时。如果类知道`SecurityContextHolder`总是包含一个`Authentication`对象,而不是`null`,则可以更可靠地编写类。 + +## 配置 + +匿名身份验证支持在使用 HTTP 配置 Spring Security3.0 时自动提供,并且可以使用``元素进行定制(或禁用)。你不需要配置这里描述的 bean,除非你使用的是传统的 Bean 配置。 + +三个类一起提供匿名身份验证功能。`AnonymousAuthenticationToken`是`Authentication`的一个实现,并存储适用于匿名主体的`GrantedAuthority`s。有一个对应的`AnonymousAuthenticationProvider`,它被链接到`ProviderManager`中,使得`AnonymousAuthenticationToken`s 被接受。最后,还有一个`AnonymousAuthenticationFilter`,它在正常的身份验证机制之后被链接,并且如果不存在`Authentication`,则自动将一个`AnonymousAuthenticationToken`添加到`SecurityContextHolder`中。过滤器和身份验证提供程序的定义如下: + +``` + + + + + + + + +``` + +`key`在过滤器和身份验证提供程序之间共享,因此前者创建的令牌被后者接受[1]。`userAttribute`以`usernameInTheAuthenticationToken,grantedAuthority[,grantedAuthority]`的形式表示。这与`InMemoryDaoImpl`的`userMap`属性的等号后面使用的语法相同。 + +正如前面所解释的,匿名身份验证的好处是,所有 URI 模式都可以对它们应用安全性。例如: + +``` + + + + + + + + + + + " + + + +``` + +## 身份验证 TrustResolver + +匿名身份验证讨论的一个补充是`AuthenticationTrustResolver`接口,其对应的`AuthenticationTrustResolverImpl`实现。这个接口提供了`isAnonymous(Authentication)`方法,它允许感兴趣的类考虑到这种特殊类型的身份验证状态。`ExceptionTranslationFilter`在处理`AccessDeniedException`s 时使用该接口。如果抛出了`AccessDeniedException`,并且身份验证是匿名类型的,那么过滤器将启动`AuthenticationEntryPoint`,以使主体能够正确地进行身份验证,而不是抛出 403(禁用)响应。这是一个必要的区别,否则主体将始终被视为“身份验证”,并且永远不会有机会通过 Form、Basic、Digest 或其他一些正常的身份验证机制登录。 + +在上面的拦截器配置中,你经常会看到`ROLE_ANONYMOUS`属性被替换为`IS_AUTHENTICATED_ANONYMOUSLY`,这在定义访问控制时实际上是相同的。这是`AuthenticatedVoter`的使用示例,我们将在[授权章](../authorization/architecture.html#authz-authenticated-voter)中看到它。它使用`AuthenticationTrustResolver`来处理这个特定的配置属性,并授予匿名用户访问权限。`AuthenticatedVoter`方法更强大,因为它允许你区分匿名、rememe-me 和完全验证的用户。但是,如果你不需要此功能,那么你可以坚持使用`ROLE_ANONYMOUS`,它将由 Spring Security 的标准`RoleVoter`处理。 + +## 利用 Spring MVC 获得匿名身份验证 + +[ Spring MVC 解析类型`Principal`]的参数(https://DOCS. Spring.io/ Spring-framework/DOCS/current/reference/html/web.html#mvc-ann-arguments)使用自己的参数解析器。 + +这意味着像这样的构造: + +爪哇 + +``` +@GetMapping("/") +public String method(Authentication authentication) { + if (authentication instanceof AnonymousAuthenticationToken) { + return "anonymous"; + } else { + return "not anonymous"; + } +} +``` + +Kotlin + +``` +@GetMapping("/") +fun method(authentication: Authentication?): String { + return if (authentication is AnonymousAuthenticationToken) { + "anonymous" + } else { + "not anonymous" + } +} +``` + +将始终返回“不匿名”,即使是匿名请求。原因是 Spring MVC 使用`HttpServletRequest#getPrincipal`解析参数,当请求是匿名的时,这是`null`。 + +如果你想在匿名请求中获得`Authentication`,请使用`@CurrentSecurityContext`: + +例 1。对匿名请求使用 CurrentSecurityContext + +爪哇 + +``` +@GetMapping("/") +public String method(@CurrentSecurityContext SecurityContext context) { + return context.getAuthentication().getName(); +} +``` + +Kotlin + +``` +@GetMapping("/") +fun method(@CurrentSecurityContext context : SecurityContext) : String = + context!!.authentication!!.name +``` + +--- + +[1](#_footnoteref_1)。在这里,使用`key`属性不应被视为提供了任何真正的安全性。这只是一次记账练习。如果你正在共享一个`ProviderManager`,其中包含一个`AnonymousAuthenticationProvider`,在这样的场景中,验证客户端可以构造`Authentication`对象(例如通过 RMI 调用),然后,恶意客户端可以提交它自己创建的`AnonymousAuthenticationToken`(使用所选的用户名和权限列表)。如果`key`是可以猜测的,或者可以找到,那么该令牌将被匿名提供者接受。这不是正常使用的问题,但是如果你正在使用 RMI,你最好使用定制的`ProviderManager`,它省略了匿名提供者,而不是共享你用于 HTTP 身份验证机制的提供者。 + +[OpenID](openid.html)[预先认证](preauth.html) + diff --git a/docs/spring-security/servlet-authentication-architecture.md b/docs/spring-security/servlet-authentication-architecture.md new file mode 100644 index 0000000000000000000000000000000000000000..e68bddbb87b05289cc815e7f37c80caee68e9467 --- /dev/null +++ b/docs/spring-security/servlet-authentication-architecture.md @@ -0,0 +1,190 @@ +# Servlet 身份验证体系结构 + +本讨论扩展到[Servlet Security: The Big Picture](../architecture.html#servlet-architecture),以描述在 Servlet 身份验证中使用的 Spring 安全性的主要体系结构组件。如果你需要具体的流程来解释这些部分是如何组合在一起的,请查看[认证机制](index.html#servlet-authentication-mechanisms)特定部分。 + +* [SecurityContextholder](#servlet-authentication-securitycontextholder)-`SecurityContextholder`是 Spring 安全性存储谁是[已认证](../../features/authentication/index.html#authentication)的详细信息的地方。 + +* [SecurityContext](#servlet-authentication-securitycontext)-是从`SecurityContextHolder`获得的,并且包含当前已验证用户的`认证`。 + +* [认证](#servlet-authentication-authentication)-可以是对`身份验证管理器`的输入,以提供用户已提供的用于验证的凭据或来自`SecurityContext`的当前用户。 + +* [大企业](#servlet-authentication-granted-authority)-在`Authentication`上授予主体的权限(即角色、范围等) + +* [身份验证管理器](#servlet-authentication-authenticationmanager)-定义 Spring 安全性过滤器如何执行[认证](../../features/authentication/index.html#authentication)的 API。 + +* [ProviderManager](#servlet-authentication-providermanager)-`AuthenticationManager`的最常见的实现。 + +* [身份验证提供者](#servlet-authentication-authenticationprovider)-用于`ProviderManager`执行特定类型的身份验证。 + +* [带有`AuthenticationEntryPoint`的请求凭据](# Servlet-authentication-authentryPoint)-用于从客户端请求凭据(即重定向到登录页面,发送`WWW-Authenticate`响应等) + +* [抽象处理过滤器](#servlet-authentication-abstractprocessingfilter)-用于身份验证的基`Filter`。这也为身份验证的高级流程以及各个部分如何协同工作提供了一个很好的思路。 + +## SecurityContextHolder + +hi Servlet/身份验证/体系结构 + +Spring 安全性的身份验证模型的核心是`SecurityContextHolder`。它包含[SecurityContext](#servlet-authentication-securitycontext)。 + +![SecurityContextholder](../../_images/servlet/authentication/architecture/securitycontextholder.png) + +在`SecurityContextHolder`中, Spring 安全性存储了谁是[已认证](../../features/authentication/index.html#authentication)的详细信息。 Spring 安全性并不关心`SecurityContextHolder`是如何填充的。如果它包含一个值,那么它将被用作当前经过身份验证的用户。 + +表示用户已通过身份验证的最简单方法是直接设置`SecurityContextHolder`。 + +例 1。设置`SecurityContextHolder` + +爪哇 + +``` +SecurityContext context = SecurityContextHolder.createEmptyContext(); (1) +Authentication authentication = + new TestingAuthenticationToken("username", "password", "ROLE_USER"); (2) +context.setAuthentication(authentication); + +SecurityContextHolder.setContext(context); (3) +``` + +Kotlin + +``` +val context: SecurityContext = SecurityContextHolder.createEmptyContext() (1) +val authentication: Authentication = TestingAuthenticationToken("username", "password", "ROLE_USER") (2) +context.authentication = authentication + +SecurityContextHolder.setContext(context) (3) +``` + +|**1**|我们首先创建一个空的`SecurityContext`。
重要的是创建一个新的`SecurityContext`实例,而不是使用`SecurityContextHolder.getContext().setAuthentication(authentication)`,以避免跨多个线程的竞争条件。| +|-----|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +|**2**|接下来我们创建一个新的[`Authentication`](# Servlet-authentication-authentication)对象。
Spring Security 并不关心在`Authentication`上设置了什么类型的`Authentication`实现。
这里我们使用`TestingAuthenticationToken`,因为它非常简单。
一个更常见的生产场景是。| +|**3**|最后,我们在`SecurityContextHolder`上设置`SecurityContext`。
Spring Security 将对[授权](../authorization/index.html#servlet-authorization)使用此信息。| + +如果你希望获得有关经过身份验证的主体的信息,可以通过访问`SecurityContextHolder`来实现。 + +例 2。访问当前通过身份验证的用户 + +爪哇 + +``` +SecurityContext context = SecurityContextHolder.getContext(); +Authentication authentication = context.getAuthentication(); +String username = authentication.getName(); +Object principal = authentication.getPrincipal(); +Collection authorities = authentication.getAuthorities(); +``` + +Kotlin + +``` +val context = SecurityContextHolder.getContext() +val authentication = context.authentication +val username = authentication.name +val principal = authentication.principal +val authorities = authentication.authorities +``` + +默认情况下,`SecurityContextHolder`使用`ThreadLocal`来存储这些细节,这意味着`SecurityContext`对于同一线程中的方法总是可用的,即使`SecurityContext`没有显式地作为参数传递给这些方法。以这种方式使用`ThreadLocal`是非常安全的,如果在处理当前主体的请求之后要注意清除线程。 Spring Security 的[FilterchainProxy](../architecture.html#servlet-filterchainproxy)确保始终清除`SecurityContext`。 + +一些应用程序 AREN 并不完全适合使用`ThreadLocal`,因为它们处理线程的方式很特殊。例如,Swing 客户机可能希望 Java 虚拟机中的所有线程使用相同的安全上下文。`SecurityContextHolder`可以在启动时配置一个策略,以指定如何存储上下文。对于独立的应用程序,你将使用`SecurityContextHolder.MODE_GLOBAL`策略。其他应用程序可能希望安全线程生成的线程也具有相同的安全标识。这是通过使用`SecurityContextHolder.MODE_INHERITABLETHREADLOCAL`实现的。你可以通过两种方式从默认的`SecurityContextHolder.MODE_THREADLOCAL`更改模式。第一种是设置一个系统属性,第二种是在`SecurityContextHolder`上调用一个静态方法。大多数应用程序不需要更改默认设置,但是如果需要更改,请查看`SecurityContextHolder`的 Javadoc 以了解更多信息。 + +## SecurityContext + +[`SecurityContext`](https://DOCS. Spring.io/ Spring-security/site/DOCS/5.6.2/api/org/springframework/security/core/context/securitycontext.html)是从[SecurityContextholder](#servlet-authentication-securitycontextholder)中获得的。`SecurityContext`包含一个[认证](#servlet-authentication-authentication)对象。 + +## Authentication + +[`Authentication`](https://DOCS. Spring.io/ Spring-security/site/DOCS/5.6.2/api/org/springframework/security/core/authentication.html)在 Spring 安全性中有两个主要目的: + +* 输入到[`AuthenticationManager`](# Servlet-authentication-authenticationManager),以提供用户提供的用于验证的凭据。在此场景中使用时,`isAuthenticated()`返回`false`。 + +* 表示当前经过身份验证的用户。当前的`Authentication`可以从[SecurityContext](#servlet-authentication-securitycontext)中得到。 + +`Authentication`包含: + +* `principal`-标识用户。当使用用户名/密码进行身份验证时,这通常是[`UserDetails`](passwords/user-details.html# Servlet-authentication-userdetails)的一个实例。 + +* `credentials`-通常是密码。在许多情况下,在用户通过身份验证以确保其不会泄漏后,将清除该漏洞。 + +* `authorities`-[`大企业`s](# Servlet-authentication-granted-authority)是授予用户的高级权限。几个例子是角色或范围。 + +## GrantedAuthority + +[`GrantedAuthority`s](https://DOCS. Spring.io/ Spring-security/site/DOCS/5.6.2/api/org/springframework/security/core/grantedauthority.html)是授予用户的高级权限。几个例子是角色或范围。 + +`GrantedAuthority`s 可以从[`Authentication.getAuthorities()`](# Servlet-authentication-authentication)方法获得。该方法提供了`Collection`的`GrantedAuthority`对象。a`GrantedAuthority`是授予委托人的权力,这并不奇怪。这种权威通常是“角色”,如`ROLE_ADMINISTRATOR`或`ROLE_HR_SUPERVISOR`。这些角色稍后将被配置用于 Web 授权、方法授权和域对象授权。 Spring 安全性的其他部分能够解释这些权威,并期望它们存在。当使用基于用户名/密码的身份验证时,`GrantedAuthority`s 通常是由[`UserDetailsService`]加载的(密码/user-details-service.html# Servlet-authentication-userDetailsService)。 + +通常`GrantedAuthority`对象是应用程序范围的权限。它们不是特定于给定域对象的。因此,你可能不需要`GrantedAuthority`来表示对`Employee`对象号 54 的权限,因为如果有数千个这样的权限,你将很快耗尽内存(或者至少导致应用程序花费很长时间来验证用户)。当然, Spring 安全性是专门为处理这一常见需求而设计的,但是你应该为此目的使用项目的域对象安全功能。 + +## AuthenticationManager + +[`AuthenticationManager`](https://DOCS. Spring.io/ Spring-security/site/DOCS/5.6.2/api/org/SpringFramework/security/Authentication/AuthenticationManager.html)是定义 Spring Security 的过滤器如何执行[认证](../../features/authentication/index.html#authentication)的 API。返回的[`Authentication`](# Servlet-authentication-authentication)然后由控制器在[SecurityContextholder](#servlet-authentication-securitycontextholder)上设置(即[ Spring Security 的`Filters`s](.../architecture.html# Servlet-security-gt)),该控制器调用<)。如果你没有与 * Spring Security 的`Filters`s* 集成,则可以直接设置`SecurityContextHolder`,并且不需要使用`AuthenticationManager`。 + +虽然`AuthenticationManager`的实现可以是任何东西,但最常见的实现是[`ProviderManager`](# Servlet-attentification-providermanager)。 + +## ProviderManager + +[`ProviderManager`](https://DOCS. Spring.io/ Spring-security/site/DOCS/5.6.2/api/org/springframework/security/authentication/providermanager.html)是最常用的[`AuthenticationManager`](# Servlet-authentication-authentication manager)的实现。`ProviderManager`委托给[`List`的[`身份验证提供者`s](# Servlet-authenticentication-authenticationprov 每个`AuthenticationProvider`都有机会指示身份验证应该成功、失败或指示它不能做出决定,并允许下游`AuthenticationProvider`进行决定。如果所有配置的`AuthenticationProvider`都不能进行身份验证,则使用`ProviderNotFoundException`进行身份验证将失败,这是一个特殊的`AuthenticationException`,表示`ProviderManager`未配置为支持传递到它的`Authentication`类型。 + +![ProviderManager](../../_images/servlet/authentication/architecture/providermanager.png) + +在实践中,每个`AuthenticationProvider`都知道如何执行特定类型的身份验证。例如,一个`AuthenticationProvider`可能能够验证用户名/密码,而另一个可能能够验证 SAML 断言。这允许每个`AuthenticationProvider`执行非常特定类型的身份验证,同时支持多种类型的身份验证,并且只公开单个`AuthenticationManager` Bean。 + +`ProviderManager`还允许配置一个可选的父`AuthenticationManager`,在没有`AuthenticationProvider`可以执行身份验证的情况下,可以查询该父`AuthenticationManager`。父可以是`AuthenticationManager`的任何类型,但它通常是`ProviderManager`的实例。 + +![ProviderManager 母公司](../../_images/servlet/authentication/architecture/providermanager-parent.png) + +实际上,多个`ProviderManager`实例可能共享同一个父`AuthenticationManager`。在多个[`SecurityFilterChain`](../architecture.html# Servlet-securityfilterchain)实例具有一些共同的身份验证(共享的父`AuthenticationManager`)的场景中,这种情况有些常见,但也存在不同的身份验证机制(不同的`ProviderManager`实例)。 + +![ProviderManagers 母公司](../../_images/servlet/authentication/architecture/providermanagers-parent.png) + +默认情况下,`ProviderManager`将尝试清除由成功的身份验证请求返回的`Authentication`对象中的任何敏感凭据信息。这可以防止像密码这样的信息在`HttpSession`中保留的时间超过必要的时间。 + +这可能会在使用用户对象的缓存时引起问题,例如,在无状态应用程序中提高性能。如果`Authentication`包含对缓存中某个对象的引用(例如`UserDetails`实例),并且它的凭据已被删除,那么它将不再可能根据缓存的值进行身份验证。如果你正在使用缓存,则需要考虑到这一点。一个明显的解决方案是首先在缓存实现中或在创建返回的`Authentication`对象的`AuthenticationProvider`中复制对象。或者,你可以在`ProviderManager`上禁用`eraseCredentialsAfterAuthentication`属性。有关更多信息,请参见[Javadoc](https://docs.spring.io/spring-security/site/docs/5.6.2/api/org/springframework/security/authentication/ProviderManager.html)。 + +## AuthenticationProvider + +多个[`AuthenticationProvider`s](https://DOCS. Spring.io/ Spring-security/site/DOCS/5.6.2/api/org/springframework/security/authentication/authenticationprovider.html)可以被注入到[`ProviderManager`](# Servlet-authentication-providermanager)中。每个`AuthenticationProvider`执行特定类型的身份验证。例如,[`DaoAuthenticationProvider`](passwords/dao-authentication-provider.html# Servlet-authentication-daoauthenticationprovider)支持基于用户名/密码的身份验证,而`JwtAuthenticationProvider`支持对 JWT 令牌进行身份验证。 + +## 使用`AuthenticationEntryPoint`请求凭据 + +[`AuthenticationEntryPoint`](https://DOCS. Spring.io/ Spring-security/site/DOCS/5.6.2/api/org/springframework/security/web/authenticationentrypoint.html)用于发送请求客户端凭据的 HTTP 响应。 + +有时,客户端会主动地包含一些凭据,例如用户名/密码,以请求资源。在这些情况下, Spring 安全性不需要提供一个 HTTP 响应来请求来自客户机的凭据,因为它们已经包含在其中。 + +在其他情况下,客户端将向未经授权访问的资源发出未经身份验证的请求。在这种情况下,`AuthenticationEntryPoint`的实现用于从客户端请求凭据。`AuthenticationEntryPoint`实现可以执行[重定向到登录页面](passwords/form.html#servlet-authentication-form),用[WWW-认证](passwords/basic.html#servlet-authentication-basic)报头进行响应,等等。 + +## 抽象处理过滤器 + +[`AbstractAuthenticationProcessingFilter`](https://DOCS. Spring.io/ Spring-security/site/DOCS/5.6.2/api/org/springframework/security/web/authentication/abstractothenticationprocessingfilter.html)被用作验证用户凭据的基础`Filter`。在能够对凭据进行身份验证之前, Spring Security 通常使用[](# Servlet-Authentication-AuthenticationEntryPoint)请求凭据。 + +接下来,`AbstractAuthenticationProcessingFilter`可以对提交给它的任何身份验证请求进行身份验证。 + +![抽象处理过滤器](../../_images/servlet/authentication/architecture/abstractauthenticationprocessingfilter.png) + +![number 1](../../_images/icons/number_1.png)当用户提交其凭据时,`AbstractAuthenticationProcessingFilter`从`Authentication`创建一个[`Authentication`](# Servlet-authentication-authentication)来进行身份验证。创建的`Authentication`类型取决于`AbstractAuthenticationProcessingFilter`的子类。例如,[`UsernamePasswordAuthenticationFilter`](passwords/form.html# Servlet-authentication-usernamepasswordauthenticationfilter)从*用户 Name*和*密码*中创建一个`UsernamePasswordAuthenticationToken`,它们在`HttpServletRequest`中提交。 + +![number 2](../../_images/icons/number_2.png)接下来,将[`Authentication`](# Servlet-authentication-authentication)传递到[`AuthenticationManager`](# Servlet-authentication-authenticationManager)中进行身份验证。 + +![number 3](../../_images/icons/number_3.png)如果身份验证失败,则*失败* + +* [SecurityContextholder](#servlet-authentication-securitycontextholder)被清除。 + +* 调用`RememberMeServices.loginFail`。如果 Remember Me 没有配置,这是一个禁止操作。 + +* 调用`AuthenticationFailureHandler`。 + +![number 4](../../_images/icons/number_4.png)如果身份验证成功,则*成功*。 + +* `SessionAuthenticationStrategy`被通知有一个新的登录。 + +* [认证](#servlet-authentication-authentication)设置在[SecurityContextholder](#servlet-authentication-securitycontextholder)上。稍后,`SecurityContextPersistenceFilter`将`SecurityContext`保存到`HttpSession`。 + +* 调用`RememberMeServices.loginSuccess`。如果 Remember Me 没有配置,这是一个禁止操作。 + +* `ApplicationEventPublisher`发布`InteractiveAuthenticationSuccessEvent`。 + +* 调用`AuthenticationSuccessHandler`。 + +[认证](index.html)[用户名/密码](passwords/index.html) + diff --git a/docs/spring-security/servlet-authentication-cas.md b/docs/spring-security/servlet-authentication-cas.md new file mode 100644 index 0000000000000000000000000000000000000000..86af5b003d5b07de1964da57b4e2ed36593f0ce0 --- /dev/null +++ b/docs/spring-security/servlet-authentication-cas.md @@ -0,0 +1,359 @@ +# CAS 认证 + +## 概述 + +JA-SIG 公司生产一种 Enterprise 范围内的单点登录系统,称为 CAS。与其他计划不同,JA-SIG 的中央身份验证服务是开源的,广泛使用,易于理解,独立于平台,并支持代理功能。 Spring 安全性完全支持 CAS,并提供了一种从 Spring 安全性的单应用程序部署到由 Enterprise 范围的 CAS 服务器保护的多应用程序部署的简单迁移路径。 + +你可以在[https://www.apereo.org](https://www.apereo.org)上了解有关 CAS 的更多信息。你还需要访问此站点来下载 CAS 服务器文件。 + +## CAS 的工作原理 + +虽然 CAS Web 站点包含详细介绍 CAS 体系结构的文档,但我们在 Spring 安全性的上下文中再次介绍一般概述。 Spring Security3.x 支持 Cas3。在编写本文时,CAS 服务器处于 3.4 版本。 + +在你的 Enterprise 的某个地方,你将需要设置一个 CAS 服务器。CAS 服务器只是一个标准的 WAR 文件,因此设置服务器没有什么困难。在 WAR 文件中,你将在显示给用户的页面上自定义登录和其他单个签名。 + +部署 CAS3.4 服务器时,还需要在 CAS 包含的`deployerConfigContext.xml`中指定`AuthenticationHandler`。`AuthenticationHandler`有一个简单的方法,该方法返回一个关于给定的凭据集是否有效的布尔值。你的`AuthenticationHandler`实现将需要链接到某种类型的后端身份验证存储库,例如 LDAP 服务器或数据库。CAS 本身包括许多开箱即用的`AuthenticationHandler`s,以帮助实现这一点。当你下载和部署服务器 WAR 文件时,设置它可以成功地对输入与其用户名匹配的密码的用户进行身份验证,这对于测试非常有用。 + +除了 CAS 服务器本身,其他关键的参与者当然是部署在整个 Enterprise 中的安全 Web 应用程序。这些 Web 应用程序被称为“服务”。有三种类型的服务。对服务票进行身份验证的,可以获得代理票的,以及对代理票进行身份验证的。对代理票据进行身份验证是不同的,因为必须对代理票据列表进行验证,并且通常情况下代理票据可以重复使用。 + +### Spring 安全性与 CAS 交互序列 + +Web 浏览器、CAS 服务器和 Spring 安全保护服务之间的基本交互如下: + +* 网络用户正在浏览该服务的公共页面。不涉及 CAS 或 Spring 安全性。 + +* 用户最终会请求一个安全的页面,或者它使用的某个 bean 是安全的。 Spring 安全性的`ExceptionTranslationFilter`将检测到`AccessDeniedException`或`AuthenticationException`。 + +* 因为用户的`Authentication`对象(或缺少它)导致了`AuthenticationException`,所以`ExceptionTranslationFilter`将调用已配置的`AuthenticationEntryPoint`。如果使用 CAS,这将是`CasAuthenticationEntryPoint`类。 + +* `CasAuthenticationEntryPoint`将把用户的浏览器重定向到 CAS 服务器。它还将指示一个`service`参数,这是 Spring 安全服务(你的应用程序)的回调 URL。例如,浏览器被重定向到的 URL 可能是[https://my.company.com/cas/login?service=https%3A%2F%2Fserver3.company.com%2Fwebapp%2Flogin/cas](https://my.company.com/cas/login?service=https%3A%2F%2Fserver3.company.com%2Fwebapp%2Flogin/cas)。 + +* 用户的浏览器重定向到 CAS 后,会提示他们输入用户名和密码。如果用户提供了一个会话cookie,表明他们以前已经登录过,那么将不会提示他们再次登录(这个过程有一个例外,我们将在后面进行讨论)。CAS 将使用上面讨论的`PasswordHandler`(如果使用 CAS3.0,则使用`AuthenticationHandler`)来决定用户名和密码是否有效。 + +* 成功登录后,CAS 将把用户的浏览器重定向到原始服务。它还将包括一个`ticket`参数,这是一个表示“服务票据”的不透明字符串。继续前面的示例,浏览器被重定向到的 URL 可能是[https://server3.company.com/webapp/login/cas?ticket=ST-0-ER94xMJmn6pha35CQRoZ](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`只响应包含 CAS 特定主体的`UsernamePasswordAuthenticationToken`s(例如`CasAuthenticationFilter.CAS_STATEFUL_IDENTIFIER`)和`CasAuthenticationToken`s(稍后讨论)。 + +* `CasAuthenticationProvider`将使用`TicketValidator`实现来验证服务票。这通常是`Cas20ServiceTicketValidator`,它是 CAS 客户库中包含的类之一。如果应用程序需要验证代理票,则使用`Cas20ProxyTicketValidator`。`TicketValidator`向 CAS 服务器发出 HTTPS 请求,以验证服务票据。它还可以包括一个代理回调 URL,它包含在这个示例中:[https://my.company.com/cas/proxyValidate?service=https%3A%2F%2Fserver3.company.com%2Fwebapp%2Flogin/cas&ticket=ST-0-ER94xMJmn6pha35CQRoZ&pgtUrl=https://server3.company.com/webapp/login/cas/proxyreceptor](https://my.company.com/cas/proxyValidate?service=https%3A%2F%2Fserver3.company.com%2Fwebapp%2Flogin/cas&ticket=ST-0-ER94xMJmn6pha35CQRoZ&pgtUrl=https://server3.company.com/webapp/login/cas/proxyreceptor)。 + +* 返回到 CAS 服务器上,将接收到验证请求。如果提供的服务票证与该票证被颁发给的服务 URL 相匹配,则 CAS 将以 XML 提供肯定的响应,指示用户名。如果身份验证中涉及任何代理(在下面讨论),则代理列表也将包含在 XML 响应中。 + +* [可选]如果对 CAS 验证服务的请求包括代理回调 URL(在`pgtUrl`参数中),CAS 将在 XML 响应中包括`pgtIou`字符串。此`pgtIou`表示代理授予票据借据。然后,CAS 服务器将创建自己的 HTTPS 连接,返回到`pgtUrl`。这是为了相互验证 CAS 服务器和声明的服务 URL。HTTPS 连接将用于向原始 Web 应用程序发送代理授权票。例如,[https://server3.company.com/webapp/login/cas/proxyreceptor?pgtIou=PGTIOU-0-R0zlgrl4pdAQwBvJWO3vnNpevwqStbSGcq3vKB2SqSFFRnjPHt&pgtId=PGT-1-si9YkkHLrtACBo64rmsi3v2nf7cpCResXg5MpESZFArbaZiOKH](https://server3.company.com/webapp/login/cas/proxyreceptor?pgtIou=PGTIOU-0-R0zlgrl4pdAQwBvJWO3vnNpevwqStbSGcq3vKB2SqSFFRnjPHt&pgtId=PGT-1-si9YkkHLrtACBo64rmsi3v2nf7cpCResXg5MpESZFArbaZiOKH)。 + +* `Cas20TicketValidator`将解析从 CAS 服务器接收的 XML。它将返回`CasAuthenticationProvider`a`TicketResponse`,其中包括用户名(强制使用)、代理列表(如果涉及任何代理)和代理授予票 iou(如果请求代理回调)。 + +* 接下来`CasAuthenticationProvider`将调用已配置的`CasProxyDecider`。`CasProxyDecider`指示`TicketResponse`中的代理列表是否为服务所接受。 Spring 安全性提供了几种实现方式:`RejectProxyTickets`、`AcceptAnyCasProxy`和`NamedCasProxyDecider`。除了`NamedCasProxyDecider`允许提供`List`的可信代理之外,这些名称基本上是不言自明的。 + +* `CasAuthenticationProvider`下一步将请求一个`AuthenticationUserDetailsService`来加载`GrantedAuthority`对象,该对象应用于`Assertion`中包含的用户。 + +* 如果没有问题,`CasAuthenticationProvider`构造一个`CasAuthenticationToken`,包括`TicketResponse`和`GrantedAuthority`s 中包含的细节。 + +* 控件然后返回`CasAuthenticationFilter`,这将创建的`CasAuthenticationToken`置于安全上下文中。 + +* 用户的浏览器被重定向到导致`AuthenticationException`的原始页面(或根据配置自定义的目标)。 + +你还在这儿真好!现在让我们来看一下如何配置 + +## CAS 客户端的配置 + +Spring 由于安全性,CAS 的 Web 应用程序端变得很容易。假设你已经了解了使用 Spring 安全性的基本知识,因此下面不再讨论这些内容。我们将假设正在使用基于名称空间的配置,并根据需要添加 CAS bean。每一节都是在前一节的基础上发展起来的。完整的 CAS 示例应用程序可以在 Spring Security[Samples](../../samples.html#samples)中找到。 + +### 服务票认证 + +本节描述如何设置 Spring 安全性以对服务票据进行身份验证。通常情况下,这就是 Web 应用程序所需要的。你将需要在应用程序上下文中添加`ServiceProperties` Bean。这表示你的 CAS 服务: + +``` + + + + +``` + +`service`必须等于将由`CasAuthenticationFilter`监视的 URL。`sendRenew`默认为 false,但如果你的应用程序特别敏感,则应将其设置为 true。这个参数的作用是告诉 CAS 登录服务,登录时的单一签名是不可接受的。相反,用户将需要重新输入他们的用户名和密码,以便获得对该服务的访问权限。 + +应该配置以下 bean 以启动 CAS 身份验证过程(假设你使用的是名称空间配置): + +``` + +... + + + + + + + + + + + +``` + +对于 CAS 操作,`ExceptionTranslationFilter`必须将其`authenticationEntryPoint`属性设置为`CasAuthenticationEntryPoint` Bean。使用[入口点-参考](../appendix/namespace/http.html#nsa-http-entry-point-ref)就可以很容易地做到这一点,就像上面示例中所做的那样。`CasAuthenticationEntryPoint`必须指`ServiceProperties` Bean(在上面讨论过),它为 Enterprise 的 CAS 登录服务器提供 URL。这就是用户的浏览器将被重定向的地方。 + +`CasAuthenticationFilter`具有与`UsernamePasswordAuthenticationFilter`(用于基于表单的登录)非常相似的属性。你可以使用这些属性来自定义一些事情,比如验证成功和失败的行为。 + +接下来,你需要添加一个`CasAuthenticationProvider`及其协作者: + +``` + + + + + + + + + + + + + + + + + + + + + + +... + +``` + +`CasAuthenticationProvider`使用`UserDetailsService`实例为用户加载权限,一旦这些权限已被 CAS 认证。我们在这里展示了一个简单的内存设置。请注意,`CasAuthenticationProvider`实际上并不使用密码进行身份验证,但它确实使用了授权。 + +如果你回顾[CAS 的工作原理](#cas-how-it-works)部分,那么这些 bean 都是合理的不言自明的。 + +这就完成了 CAS 的最基本配置。如果你没有犯任何错误,那么你的 Web 应用程序应该在 CAS 单点登录的框架内愉快地工作。 Spring 安全性的任何其他部分都不需要关注 CAS 处理身份验证的事实。在下面的小节中,我们将讨论一些(可选的)更高级的配置。 + +### 单次注销 + +CAS 协议支持单注销,并且可以很容易地添加到你的 Spring 安全配置中。下面是处理单注销的 Spring 安全配置的更新 + +``` + +... + + + + + + + + + + + + + + + + +``` + +`logout`元素将用户从本地应用程序中记录下来,但不会以 CAS 服务器或已登录的任何其他应用程序结束会话。`requestSingleLogoutFilter`过滤器将允许请求`/spring_security_cas_logout`的 URL 来将应用程序重定向到配置的 CAS 服务器注销 URL。然后,CAS 服务器将向所有签入的服务发送一个注销请求。`singleLogoutFilter`通过在静态`Map`中查找`HttpSession`来处理单个注销请求,然后使其无效。 + +这可能会让人困惑,为什么同时需要`logout`元素和`singleLogoutFilter`元素。首先在本地注销被认为是最佳实践,因为`SingleSignOutFilter`只是将`HttpSession`存储在静态`Map`中,以便在其上调用 Invalidate。有了上面的配置,注销流程将是: + +* 用户请求`/logout`,这将使用户退出本地应用程序,并将用户发送到注销成功页面。 + +* 注销成功页面`/cas-logout.jsp`应指示用户单击指向`/logout/cas`的链接,以便注销所有应用程序。 + +* 当用户单击链接时,用户将被重定向到 CAS 单注销 URL([https://localhost:9443/cas/logout](https://localhost:9443/cas/logout))。 + +* 在 CAS 服务器端,CAS 单注销 URL 然后向所有 CAS 服务提交单注销请求。在 CAS 服务端,Jasig 的`SingleSignOutFilter`通过使原始会话无效来处理注销请求。 + +下一步是将以下内容添加到 web.xml 中 + +``` + +characterEncodingFilter + + org.springframework.web.filter.CharacterEncodingFilter + + + encoding + UTF-8 + + + +characterEncodingFilter +/* + + + + org.jasig.cas.client.session.SingleSignOutHttpSessionListener + + +``` + +当使用 SingleSignoutFilter 时,你可能会遇到一些编码问题。因此,建议添加`CharacterEncodingFilter`,以确保在使用`SingleSignOutFilter`时字符编码是正确的。同样,请参考 Jasig 的文档了解详细信息。`SingleSignOutHttpSessionListener`确保当`HttpSession`过期时,用于单注销的映射将被删除。 + +### 使用 CAS 对无状态服务进行身份验证 + +本节描述如何使用 CAS 对服务进行身份验证。换句话说,本节将讨论如何设置一个使用 CAS 进行身份验证的服务的客户机。下一节介绍如何设置无状态服务以使用 CAS 进行身份验证。 + +#### 配置 CAS 以获得代理授予票 + +为了对无状态服务进行身份验证,应用程序需要获得代理授予票。本节描述如何配置 Spring 安全性,以便在 cas-st[Service Ticket Authentication]配置的基础上获得 PGT 构建。 + +第一步是在 Spring 安全性配置中包含`ProxyGrantingTicketStorage`。这用于存储由`CasAuthenticationFilter`获得的 PGT’s,以便它们可以用于获得代理票。下面显示了一个示例配置。 + +``` + + +``` + +下一步是更新`CasAuthenticationProvider`,以便能够获得代理票。为此,将`Cas20ServiceTicketValidator`替换为`Cas20ProxyTicketValidator`。应该将`proxyCallbackUrl`设置为应用程序将在某个位置接收 PGT 的 URL。最后,配置还应该引用`ProxyGrantingTicketStorage`,以便它可以使用 PGT 来获取代理票。你可以在下面找到应该进行的配置更改的示例。 + +``` + +... + + + + + + + + +``` + +最后一步是更新`CasAuthenticationFilter`以接受 PGT 并将其存储在`ProxyGrantingTicketStorage`中。重要的是`proxyReceptorUrl`与`Cas20ProxyTicketValidator`的`proxyCallbackUrl`匹配。下面显示了一个配置示例。 + +``` + + ... + + + +``` + +#### 使用代理票据调用无状态服务 + +既然 Spring Security 获得了 PGTs,那么你就可以使用它们来创建代理票,该代理票可用于对无状态服务进行身份验证。cas[示例应用程序](../../samples.html#samples)在`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"); +... +} +``` + +Kotlin + +``` +protected fun doGet(request: HttpServletRequest, response: HttpServletResponse?) { + // NOTE: The CasAuthenticationToken can also be obtained using + // SecurityContextHolder.getContext().getAuthentication() + val token = request.userPrincipal as CasAuthenticationToken + // proxyTicket could be reused to make calls to the CAS service even if the + // target url differs + val proxyTicket = token.assertion.principal.getProxyTicketFor(targetUrl) + + // Make a remote call using the proxy ticket + val serviceUrl: String = targetUrl + "?ticket=" + URLEncoder.encode(proxyTicket, "UTF-8") + val proxyResponse = CommonUtils.getResponseFromServer(serviceUrl, "UTF-8") +} +``` + +### 代理票身份验证 + +`CasAuthenticationProvider`区分了有状态客户机和无状态客户机。有状态客户机被认为是任何提交到`CasAuthenticationFilter`的`filterProcessUrl`的客户机。无状态客户端是指在`filterProcessUrl`以外的 URL 上向`CasAuthenticationFilter`提出身份验证请求的任何客户端。 + +由于远程处理协议无法在`HttpSession`的上下文中呈现自己,因此不可能依赖于在请求之间的会话中存储安全上下文的默认实践。此外,由于 CAS 服务器在票证经过`TicketValidator`验证后会使其无效,因此在后续请求中呈现相同的代理票证将不会工作。 + +一个明显的选择是完全不使用 CAS 来远程处理协议客户机。然而,这将消除 CAS 的许多理想特性。作为中间立场,`CasAuthenticationProvider`使用`StatelessTicketCache`。这仅用于使用等于`CasAuthenticationFilter.CAS_STATELESS_IDENTIFIER`的本金的无状态客户机。发生的情况是`CasAuthenticationProvider`将在`StatelessTicketCache`中存储结果`CasAuthenticationToken`,并在代理票上键入。因此,远程协议客户端可以呈现与`CasAuthenticationProvider`相同的代理票并且将不需要与 CAS 服务器联系以进行验证(除了第一个请求)。经过身份验证后,代理票据可以用于原始目标服务以外的 URL。 + +本节建立在前几节的基础上,以容纳代理票据身份验证。第一步是指定对所有工件进行身份验证,如下所示。 + +``` + +... + + +``` + +下一步是为`serviceProperties`指定`authenticationDetailsSource`,并为`CasAuthenticationFilter`指定`authenticationDetailsSource`。`serviceProperties`属性指示`CasAuthenticationFilter`尝试对所有工件进行身份验证,而不是仅对`filterProcessUrl`上存在的工件进行身份验证。`ServiceAuthenticationDetailsSource`创建了一个`ServiceAuthenticationDetails`,以确保在验证票据时使用基于`HttpServletRequest`的当前 URL 作为服务 URL。生成服务 URL 的方法可以通过注入一个自定义`AuthenticationDetailsSource`来定制,该方法返回一个自定义`ServiceAuthenticationDetails`。 + +``` + +... + + + + + + + +``` + +你还需要更新`CasAuthenticationProvider`以处理代理票。为此,将`Cas20ServiceTicketValidator`替换为`Cas20ProxyTicketValidator`。你将需要配置`statelessTicketCache`以及要接受的代理。你可以在下面找到一个接受所有代理所需的更新示例。 + +``` + +... + + + + + + + + + + + + + + + + + + + + + +``` + +[JAAS](jaas.html)[X509](x509.html) + diff --git a/docs/spring-security/servlet-authentication-events.md b/docs/spring-security/servlet-authentication-events.md new file mode 100644 index 0000000000000000000000000000000000000000..e6a4561ad0d60a72749427d20a4c763ddc9cc4d6 --- /dev/null +++ b/docs/spring-security/servlet-authentication-events.md @@ -0,0 +1,146 @@ +# 认证事件 + +对于每个成功或失败的身份验证,将分别触发`AuthenticationSuccessEvent`或`AbstractAuthenticationFailureEvent`。 + +要侦听这些事件,你必须首先发布`AuthenticationEventPublisher`。 Spring 安全性的`DefaultAuthenticationEventPublisher`可能做得很好: + +爪哇 + +``` +@Bean +public AuthenticationEventPublisher authenticationEventPublisher + (ApplicationEventPublisher applicationEventPublisher) { + return new DefaultAuthenticationEventPublisher(applicationEventPublisher); +} +``` + +Kotlin + +``` +@Bean +fun authenticationEventPublisher + (applicationEventPublisher: ApplicationEventPublisher?): AuthenticationEventPublisher { + return DefaultAuthenticationEventPublisher(applicationEventPublisher) +} +``` + +然后,你可以使用 Spring 的`@EventListener`支持: + +爪哇 + +``` +@Component +public class AuthenticationEvents { + @EventListener + public void onSuccess(AuthenticationSuccessEvent success) { + // ... + } + + @EventListener + public void onFailure(AbstractAuthenticationFailureEvent failures) { + // ... + } +} +``` + +Kotlin + +``` +@Component +class AuthenticationEvents { + @EventListener + fun onSuccess(success: AuthenticationSuccessEvent?) { + // ... + } + + @EventListener + fun onFailure(failures: AbstractAuthenticationFailureEvent?) { + // ... + } +} +``` + +虽然类似于`AuthenticationSuccessHandler`和`AuthenticationFailureHandler`,但它们很好,因为它们可以独立于 Servlet API 使用。 + +## 添加异常映射 + +默认情况下,`DefaultAuthenticationEventPublisher`将为以下事件发布`AbstractAuthenticationFailureEvent`: + +| Exception |事件| +|--------------------------------|----------------------------------------------| +| `BadCredentialsException` |`AuthenticationFailureBadCredentialsEvent`| +| `UsernameNotFoundException` |`AuthenticationFailureBadCredentialsEvent`| +| `AccountExpiredException` |`AuthenticationFailureExpiredEvent`| +| `ProviderNotFoundException` |`AuthenticationFailureProviderNotFoundEvent`| +| `DisabledException` |`AuthenticationFailureDisabledEvent`| +| `LockedException` |`AuthenticationFailureLockedEvent`| +|`AuthenticationServiceException`|`AuthenticationFailureServiceExceptionEvent`| +| `CredentialsExpiredException` |`AuthenticationFailureCredentialsExpiredEvent`| +| `InvalidBearerTokenException` |`AuthenticationFailureBadCredentialsEvent`| + +发布者进行完全的`Exception`匹配,这意味着这些异常的子类也不会产生事件。 + +为此,你可能希望通过`setAdditionalExceptionMappings`方法向发布服务器提供额外的映射: + +爪哇 + +``` +@Bean +public AuthenticationEventPublisher authenticationEventPublisher + (ApplicationEventPublisher applicationEventPublisher) { + Map, + Class> mapping = + Collections.singletonMap(FooException.class, FooEvent.class); + AuthenticationEventPublisher authenticationEventPublisher = + new DefaultAuthenticationEventPublisher(applicationEventPublisher); + authenticationEventPublisher.setAdditionalExceptionMappings(mapping); + return authenticationEventPublisher; +} +``` + +Kotlin + +``` +@Bean +fun authenticationEventPublisher + (applicationEventPublisher: ApplicationEventPublisher?): AuthenticationEventPublisher { + val mapping: Map, Class> = + mapOf(Pair(FooException::class.java, FooEvent::class.java)) + val authenticationEventPublisher = DefaultAuthenticationEventPublisher(applicationEventPublisher) + authenticationEventPublisher.setAdditionalExceptionMappings(mapping) + return authenticationEventPublisher +} +``` + +## 默认事件 + +并且,你可以在任何`AuthenticationException`的情况下提供一个包罗万象的事件: + +爪哇 + +``` +@Bean +public AuthenticationEventPublisher authenticationEventPublisher + (ApplicationEventPublisher applicationEventPublisher) { + AuthenticationEventPublisher authenticationEventPublisher = + new DefaultAuthenticationEventPublisher(applicationEventPublisher); + authenticationEventPublisher.setDefaultAuthenticationFailureEvent + (GenericAuthenticationFailureEvent.class); + return authenticationEventPublisher; +} +``` + +Kotlin + +``` +@Bean +fun authenticationEventPublisher + (applicationEventPublisher: ApplicationEventPublisher?): AuthenticationEventPublisher { + val authenticationEventPublisher = DefaultAuthenticationEventPublisher(applicationEventPublisher) + authenticationEventPublisher.setDefaultAuthenticationFailureEvent(GenericAuthenticationFailureEvent::class.java) + return authenticationEventPublisher +} +``` + +[Logout](logout.html)[授权](../authorization/index.html) + diff --git a/docs/spring-security/servlet-authentication-jaas.md b/docs/spring-security/servlet-authentication-jaas.md new file mode 100644 index 0000000000000000000000000000000000000000..c7f57c7069da19de0b52bbd9ce8f4cbbd16f76b8 --- /dev/null +++ b/docs/spring-security/servlet-authentication-jaas.md @@ -0,0 +1,127 @@ +# Java 身份验证和授权服务提供者 + +## 概述 + +Spring 安全性提供了一种包,能够将身份验证请求委托给 Java 身份验证和授权服务。下面将详细讨论这个包。 + +## 抽象 JaasAuthenticationProvider + +`AbstractJaasAuthenticationProvider`是所提供的 JAAS`AuthenticationProvider`实现的基础。子类必须实现创建`LoginContext`的方法。`AbstractJaasAuthenticationProvider`具有许多可以注入其中的依赖项,这些依赖项将在下面讨论。 + +### JAAS CallbackHandler + +大多数 JAAS`LoginModule`s 都需要某种类型的回调。这些回调通常用于从用户获得用户名和密码。 + +在 Spring 安全性部署中, Spring 安全性负责此用户交互(通过身份验证机制)。因此,在将身份验证请求委托给 JAAS 时, Spring Security 的身份验证机制将已经完全填充了一个`Authentication`对象,该对象包含 JAAS`LoginModule`所需的所有信息。 + +因此, Spring Security 的 JAAS 包提供了两个默认的回调处理程序,`JaasNameCallbackHandler`和`JaasPasswordCallbackHandler`。这些回调处理程序中的每一个都实现`JaasAuthenticationCallbackHandler`。在大多数情况下,这些回调处理程序可以在不了解内部机制的情况下简单地使用。 + +对于那些需要完全控制回调行为的人,内部`AbstractJaasAuthenticationProvider`将这些`JaasAuthenticationCallbackHandler`s 包装成`InternalCallbackHandler`。`InternalCallbackHandler`是实际实现 JAASNormal`CallbackHandler`接口的类。每当使用 JAAS`LoginModule`时,都会传递一个配置`InternalCallbackHandler`s 的应用程序上下文列表。如果`LoginModule`对`InternalCallbackHandler`s 请求回调,则回调依次传递给正在打包的`JaasAuthenticationCallbackHandler`s。 + +### Jaas AuthorityGranter + +JAAS 与校长一起工作。在 JAAS 中,甚至连“角色”也被表示为主体。 Spring 另一方面,安全性适用于`Authentication`对象。每个`Authentication`对象包含一个主体,以及多个`GrantedAuthority`s。为了便于在这些不同概念之间进行映射, Spring Security 的 JAAS 包包括一个`AuthorityGranter`接口。 + +一个`AuthorityGranter`负责检查一个 JAAS 主体,并返回一组`String`s,代表分配给主体的权限。对于每个返回的权限字符串,`AbstractJaasAuthenticationProvider`创建一个`JaasGrantedAuthority`(它实现 Spring Security 的`GrantedAuthority`接口),其中包含权限字符串和传递`AuthorityGranter`的 JAAS 主体。`AbstractJaasAuthenticationProvider`首先通过使用 JAAS`LoginModule`成功验证用户的凭据来获得 JAAS 主体,然后访问它返回的`LoginContext`。对`LoginContext.getSubject().getPrincipals()`进行了调用,将每个生成的主体传递给根据`AbstractJaasAuthenticationProvider.setAuthorityGranters(List)`属性定义的每个`AuthorityGranter`。 + +Spring 考虑到每个 JAAS 主体都具有特定于实现的含义,安全性不包括任何`AuthorityGranter`s 的产品。然而,在单元测试中有一个`TestAuthorityGranter`演示了一个简单的`AuthorityGranter`实现。 + +## DefaultJaasAuthenticationProvider + +`DefaultJaasAuthenticationProvider`允许将 JAAS`Configuration`对象作为依赖项注入其中。然后,它使用注入的 JAAS创建。这意味着`DefaultJaasAuthenticationProvider`不绑定`Configuration`作为`JaasAuthenticationProvider`的任何特定实现。 + +### 记忆组态 + +为了方便地将`Configuration`注入`DefaultJaasAuthenticationProvider`,提供了一个名为`InMemoryConfiguration`的默认内存中实现。实现构造函数接受一个`Map`,其中每个键表示一个登录配置名,该值表示`Array`的`AppConfigurationEntry`s。`InMemoryConfiguration`还支持一个`Array`的缺省`Array`的`AppConfigurationEntry`对象,如果在提供的`Map`中找不到映射,则将使用该对象。有关详细信息,请参阅`InMemoryConfiguration`的类级别 Javadoc。 + +### DefaultJaasAuthenticationProvider 示例配置 + +虽然`InMemoryConfiguration`的 Spring 配置可以比标准的 JAAS 配置文件更详细,但与`DefaultJaasAuthenticationProvider`一起使用它比`JaasAuthenticationProvider`更灵活,因为它不依赖于默认的`Configuration`实现。 + +下面提供了使用`InMemoryConfiguration`的`DefaultJaasAuthenticationProvider`的配置示例。请注意,`Configuration`的自定义实现也可以很容易地注入到`DefaultJaasAuthenticationProvider`中。 + +``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +## JaasAuthenticationProvider + +`JaasAuthenticationProvider`假设默认的`Configuration`是[配置文件](https://docs.oracle.com/javase/8/docs/jre/api/security/jaas/spec/com/sun/security/auth/login/ConfigFile.html)的实例。这个假设是为了尝试更新`Configuration`。然后`JaasAuthenticationProvider`使用默认的`Configuration`来创建`LoginContext`。 + +假设我们有一个 JAAS 登录配置文件`/WEB-INF/login.conf`,其内容如下: + +``` +JAASTest { + sample.SampleLoginModule required; +}; +``` + +与所有 Spring 安全 bean 一样,`JaasAuthenticationProvider`也是通过应用程序上下文配置的。以下定义对应于上面的 JAAS 登录配置文件: + +``` + + + + + + + + + + + + + + + +``` + +## 以跑步为主题 + +如果进行了配置,`JaasApiIntegrationFilter`将尝试在`JaasAuthenticationToken`上以`Subject`的形式运行。这意味着`Subject`可以通过以下方式访问: + +``` +Subject subject = Subject.getSubject(AccessController.getContext()); +``` + +可以使用[JAAS-API-供应](../appendix/namespace/http.html#nsa-http-jaas-api-provision)属性轻松配置此集成。当与依赖于填充的 JAAS 主题的遗留或外部 API 集成时,此功能非常有用。 + +[预先认证](preauth.html)[CAS](cas.html) + diff --git a/docs/spring-security/servlet-authentication-logout.md b/docs/spring-security/servlet-authentication-logout.md new file mode 100644 index 0000000000000000000000000000000000000000..3a2ff793876e1dddc5e0a61f2ed98159f34b98cf --- /dev/null +++ b/docs/spring-security/servlet-authentication-logout.md @@ -0,0 +1,122 @@ +# 处理注销 + +## 注销 爪哇/ Kotlin 配置 + +当使用`[WebSecurityConfigurerAdapter](https://docs.spring.io/spring-security/site/docs/5.6.2/api/org/springframework/security/config/annotation/web/configuration/WebSecurityConfigurerAdapter.html)`时,将自动应用注销功能。默认情况下,访问 URL`/logout`将通过以下方式将用户注销: + +* 使 HTTP会话无效 + +* 清除所有已配置的 RememberMe 身份验证 + +* 清除`SecurityContextHolder` + +* 重定向到`/login?logout` + +然而,与配置登录功能类似,你还可以使用各种选项来进一步定制你的注销需求: + +例 1。注销配置 + +Java + +``` +protected void configure(HttpSecurity http) throws Exception { + http + .logout(logout -> logout (1) + .logoutUrl("/my/logout") (2) + .logoutSuccessUrl("/my/index") (3) + .logoutSuccessHandler(logoutSuccessHandler) (4) + .invalidateHttpSession(true) (5) + .addLogoutHandler(logoutHandler) (6) + .deleteCookies(cookieNamesToClear) (7) + ) + ... +} +``` + +Kotlin + +``` +override fun configure(http: HttpSecurity) { + http { + logout { + logoutUrl = "/my/logout" (1) + logoutSuccessUrl = "/my/index" (2) + logoutSuccessHandler = customLogoutSuccessHandler (3) + invalidateHttpSession = true (4) + addLogoutHandler(logoutHandler) (5) + deleteCookies(cookieNamesToClear) (6) + } + } +} +``` + +|**1**|提供注销支持。
当使用`WebSecurityConfigurerAdapter`时,会自动应用此功能。| +|-----|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +|**2**|触发注销发生的 URL(默认值是`/logout`)。
如果启用了 CSRF 保护(默认值),那么请求也必须是 POST。
有关更多信息,请咨询[Javadoc](https://docs.spring.io/spring-security/site/docs/5.6.2/api/org/springframework/security/config/annotation/web/configurers/LogoutConfigurer.html#logoutUrl-java.lang.String-)。| +|**3**|发生注销后要重定向到的 URL。
默认值为`/login?logout`。
有关更多信息,请咨询[Javadoc](https://docs.spring.io/spring-security/site/docs/5.6.2/api/org/springframework/security/config/annotation/web/configurers/LogoutConfigurer.html#logoutSuccessUrl-java.lang.String-)。| +|**4**|让我们指定一个自定义`LogoutSuccessShandler`。
如果指定了这个,`logoutSuccessUrl()`将被忽略。
有关更多信息,请咨询[Javadoc](https://docs.spring.io/spring-security/site/docs/5.6.2/api/org/springframework/security/config/annotation/web/configurers/LogoutConfigurer.html#logoutSuccessHandler-org.springframework.security.web.authentication.logout.LogoutSuccessHandler-)。| +|**5**|指定是否在注销时使`HttpSession`无效。
默认情况下这是**true**。
配置了封面下的`SecurityContextLogouthandler`。
有关更多信息,请咨询[Javadoc](https://docs.spring.io/spring-security/site/docs/5.6.2/api/org/springframework/security/config/annotation/web/configurers/LogoutConfigurer.html#invalidateHttpSession-boolean-)。| +|**6**|添加`LogoutHandler`。默认情况下,`SecurityContextLogoutHandler`是作为最后一个`LogoutHandler`添加的。| +|**7**|允许指定注销成功后要删除的 cookie 的名称。
这是显式添加`CookieClearingLogoutHandler`的快捷方式。| + +| |当然,也可以使用 XML 命名空间符号来配置注销。
请参阅 Spring Security XML 命名空间小节中[注销元素](../appendix/namespace/http.html#nsa-logout)的文档以获取更多详细信息。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +通常,为了定制注销功能,可以添加`[LogoutHandler](https://docs.spring.io/spring-security/site/docs/5.6.2/api/org/springframework/security/web/authentication/logout/LogoutHandler.html)`和/或`[LogoutSuccessHandler](https://docs.spring.io/spring-security/site/docs/5.6.2/api/org/springframework/security/web/authentication/logout/LogoutSuccessHandler.html)`实现。对于许多常见的场景,在使用 Fluent API 时,这些处理程序会被隐藏起来。 + +## 注销 XML 配置 + +`logout`元素通过导航到特定的 URL,增加了对注销的支持。默认的注销 URL 是`/logout`,但你可以使用`logout-url`属性将其设置为其他内容。有关其他可用属性的更多信息,请参见名称空间附录。 + +## LogoutHandler + +通常,`[LogoutHandler](https://docs.spring.io/spring-security/site/docs/5.6.2/api/org/springframework/security/web/authentication/logout/LogoutHandler.html)`实现表示能够参与注销处理的类。预计将调用它们来执行必要的清理。因此,他们不应该抛出例外。提供了各种实现方式: + +* [坚持以 DREMEMBERMESERVICE 为基础](https://docs.spring.io/spring-security/site/docs/5.6.2/api/org/springframework/security/web/authentication/rememberme/PersistentTokenBasedRememberMeServices.html) + +* [TokenBaseDreMemberMeservices ](https://docs.spring.io/spring-security/site/docs/5.6.2/api/org/springframework/security/web/authentication/rememberme/TokenBasedRememberMeServices.html) + +* [CookieclearingLogouthandler ](https://docs.spring.io/spring-security/site/docs/5.6.2/api/org/springframework/security/web/authentication/logout/CookieClearingLogoutHandler.html) + +* [CSRFLOGOUTHANDLER ](https://docs.spring.io/spring-security/site/docs/5.6.2/api/org/springframework/security/web/csrf/CsrfLogoutHandler.html) + +* [SecurityContextLogouthandler ](https://docs.spring.io/spring-security/site/docs/5.6.2/api/org/springframework/security/web/authentication/logout/SecurityContextLogoutHandler.html) + +* [HeaderWriterLogouthandler ](https://docs.spring.io/spring-security/site/docs/5.6.2/api/org/springframework/security/web/authentication/logout/HeaderWriterLogoutHandler.html) + +详情请见[Remember-Me 接口和实现](rememberme.html#remember-me-impls)。 + +与其直接提供`LogoutHandler`实现,Fluent API 还提供了一些快捷方式,这些快捷方式在封面下提供了相应的`LogoutHandler`实现。例如:`deleteCookies()`允许指定注销成功后要删除的一个或多个 Cookie 的名称。与添加`CookieClearingLogoutHandler`相比,这是一个快捷方式。 + +## LogoutSuccessHandler + +`LogoutSuccessHandler`在`LogoutFilter`成功注销后调用`LogoutSuccessHandler`,以处理例如重定向或转发到适当的目的地。请注意,该接口几乎与`LogoutHandler`相同,但可能会引发异常。 + +提供了以下实现方式: + +* [SimpleurlLogoutSuccessShandler ](https://docs.spring.io/spring-security/site/docs/5.6.2/api/org/springframework/security/web/authentication/logout/SimpleUrlLogoutSuccessHandler.html) + +* HttpStatusReturningLogOutSuccessShandler + +如上所述,你不需要直接指定`SimpleUrlLogoutSuccessHandler`。相反,Fluent API 通过设置`logoutSuccessUrl()`提供了一个快捷方式。这将在封面下设置`SimpleUrlLogoutSuccessHandler`。注销发生后,提供的 URL 将被重定向到该 URL。默认值为`/login?logout`。 + +在 REST API 类型场景中,`HttpStatusReturningLogoutSuccessHandler`可能很有趣。这`LogoutSuccessHandler`不是在成功注销后重定向到 URL,而是允许你提供一个要返回的普通 HTTP 状态代码。如果未配置状态代码 200,将在默认情况下返回。 + +## 更多与注销相关的参考资料 + +* [注销处理](#ns-logout) + +* [测试注销](../test/mockmvc/logout.html#test-logout) + +* [HttpServletRequest.logout()](../integrations/servlet-api.html#servletapi-logout) + +* [Remember-Me 接口和实现](rememberme.html#remember-me-impls) + +* [注销](../exploits/csrf.html#servlet-considerations-csrf-logout)中的 csrf 警告 + +* section[单次注销](cas.html#cas-singlelogout)(CAS 协议) + +* Spring Security XML 名称空间部分中[注销元素](../appendix/namespace/http.html#nsa-logout)的文档 + +[Run-As](runas.html)[认证事件](events.html) + diff --git a/docs/spring-security/servlet-authentication-openid.md b/docs/spring-security/servlet-authentication-openid.md new file mode 100644 index 0000000000000000000000000000000000000000..415c92b38608fa9e476830db89520a1c65065100 --- /dev/null +++ b/docs/spring-security/servlet-authentication-openid.md @@ -0,0 +1,47 @@ +# OpenID 支持 + +| |OpenID1.0 和 2.0 协议已被弃用,并鼓励用户迁移到 Spring-security-oAuth2 支持的 OpenID Connect。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------| + +名称空间支持[OpenID](https://openid.net/)登录,而不是常规的基于表单的登录,或者除了基于表单的登录之外,只需要做一个简单的更改: + +``` + + + + +``` + +然后,你应该向 OpenID 提供者(例如 myopenid.com)注册自己,并将用户信息添加到内存``: + +``` + +``` + +你应该能够使用`myopenid.com`站点登录以进行身份验证。通过在`openid-login`元素上设置`user-service-ref`属性,也可以选择特定的`UserDetailsService` Bean 来使用 OpenID。请注意,我们在上面的用户配置中省略了 password 属性,因为这组用户数据仅用于为用户加载权限。将在内部生成一个随机密码,以防止你意外地将此用户数据用作配置中其他位置的验证源。 + +## 属性交换 + +支持 OpenID[属性交换](https://openid.net/specs/openid-attribute-exchange-1_0.html)。例如,下面的配置将试图从 OpenID 提供程序检索电子邮件和全名,以供应用程序使用: + +``` + + + + + + +``` + +每个 OpenID 属性的“type”都是一个 URI,由一个特定的模式决定,在本例中[https://axschema.org/](https://axschema.org/)。如果必须检索一个属性才能成功进行身份验证,则可以设置`required`属性。支持的确切模式和属性将取决于你的 OpenID 提供程序。属性值作为身份验证过程的一部分返回,之后可以使用以下代码对其进行访问: + +``` +OpenIDAuthenticationToken token = + (OpenIDAuthenticationToken)SecurityContextHolder.getContext().getAuthentication(); +List attributes = token.getAttributes(); +``` + +我们可以从[SecurityContextholder ](architecture.html#servlet-authentication-securitycontextholder)中得到`OpenIDAuthenticationToken`。`OpenIDAttribute`包含属性类型和检索到的值(或者在多值属性的情况下的值)。你可以提供多个`attribute-exchange`元素,在每个元素上使用`identifier-matcher`属性。这包含一个正则表达式,它将与用户提供的 OpenID 标识符进行匹配。参见代码库中的 OpenID 示例应用程序,以获得配置示例,该配置为 Google、Yahoo 和 MyOpenID 提供者提供了不同的属性列表。 + +[记住我](rememberme.html)[Anonymous](anonymous.html) + diff --git a/docs/spring-security/servlet-authentication-passwords-basic.md b/docs/spring-security/servlet-authentication-passwords-basic.md new file mode 100644 index 0000000000000000000000000000000000000000..66dcc51bf96394e07dcde5d53f8320927948935d --- /dev/null +++ b/docs/spring-security/servlet-authentication-passwords-basic.md @@ -0,0 +1,84 @@ +# 基本身份验证 + +本节详细介绍了 Spring 安全性如何为基于 Servlet 的应用程序提供[基本 HTTP 认证](https://tools.ietf.org/html/rfc7617)支持。 + +让我们来看看 HTTP Basic 身份验证在 Spring 安全性中是如何工作的。首先,我们看到[WWW-认证](https://tools.ietf.org/html/rfc7235#section-4.1)头被发送回未经验证的客户端。 + +![基本验证入口点](../../../_images/servlet/authentication/unpwd/basicauthenticationentrypoint.png) + +图 1。发送 WWW-身份验证报头 + +该图构建于我们的[`SecurityFilterChain`](../../architecture.html# Servlet-SecurityFilterchain)图。 + +![number 1](../../../_images/icons/number_1.png)首先,用户向资源`/private`发出未经授权的请求。 + +![number 2](../../../_images/icons/number_2.png) Spring security 的[`FilterSecurityInterceptor`](.../授权/authorization/authorization/authorization-requests.html# Servlet-authorization-filtersecurityinterceptor)通过抛出`AccessDeniedException`表示未经验证的请求是*拒绝*。 + +![number 3](../../../_images/icons/number_3.png)由于用户未经过身份验证,[`ExceptionTranslationFilter`](..../architecture.html# Servlet-ExceptionTranslationFilter)发起*启动身份验证*。配置的[`AuthenticationEntryPoint`](../architecture.html# Servlet-authentication-authentryPoint)是[`BasicAuthenticationEntryPoint`](https://DOCS. Spring.io/ Spring-security/site/DOCS/5.6.2/api/org/springframework/security/web/authentification/WWW/basicauthentrypoint.html)的一个实例,它发送一个 WWW-authenticate 报头。`RequestCache`通常是不保存请求的`NullRequestCache`,因为客户机能够重放它最初请求的请求。 + +当客户端接收到 WWW-Authenticate 报头时,它知道应该使用用户名和密码重试。下面是正在处理的用户名和密码的流程。 + +![基本验证过滤器](../../../_images/servlet/authentication/unpwd/basicauthenticationfilter.png) + +图 2。验证用户名和密码 + +这个图是基于我们的[`SecurityFilterChain`](../../architecture.html# Servlet-SecurityFilterchain)图构建的。 + +![number 1](../../../_images/icons/number_1.png)当用户提交他们的用户名和密码时,`BasicAuthenticationFilter`通过从`HttpServletRequest`中提取用户名和密码,创建一个`UsernamePasswordAuthenticationToken`,这是一种[`Authentication`](../architecture.html# Servlet-authentication-authentication)的类型。 + +![number 2](../../../_images/icons/number_2.png)接下来,将`UsernamePasswordAuthenticationToken`传递到`AuthenticationManager`中以进行身份验证。`AuthenticationManager`的详细内容取决于[用户信息被存储](index.html#servlet-authentication-unpwd-storage)的方式。 + +![number 3](../../../_images/icons/number_3.png)如果身份验证失败,则*失败* + +* [SecurityContextholder ](../architecture.html#servlet-authentication-securitycontextholder)被清除。 + +* 调用`RememberMeServices.loginFail`。如果 Remember Me 没有配置,这是一个禁止操作。 + +* 调用`AuthenticationEntryPoint`以触发再次发送 WWW-身份验证。 + +![number 4](../../../_images/icons/number_4.png)如果身份验证成功,则*成功*。 + +* [认证](../architecture.html#servlet-authentication-authentication)设置在[SecurityContextholder ](../architecture.html#servlet-authentication-securitycontextholder)上。 + +* 调用`RememberMeServices.loginSuccess`。如果 Remember Me 没有配置,这是一个禁止操作。 + +* `BasicAuthenticationFilter`调用`FilterChain.doFilter(request,response)`以继续应用程序逻辑的其余部分。 + +Spring 默认情况下启用了 Security 的 HTTP Basic 身份验证支持。然而,只要提供了任何基于 Servlet 的配置,就必须显式地提供 HTTP BASIC。 + +可以在下面找到最小的显式配置: + +例 1。显式 HTTP 基本配置 + +爪哇 + +``` +protected void configure(HttpSecurity http) { + http + // ... + .httpBasic(withDefaults()); +} +``` + +XML + +``` + + + + +``` + +Kotlin + +``` +fun configure(http: HttpSecurity) { + http { + // ... + httpBasic { } + } +} +``` + +[Form](form.html)[Digest](digest.html) + diff --git a/docs/spring-security/servlet-authentication-passwords-digest.md b/docs/spring-security/servlet-authentication-passwords-digest.md new file mode 100644 index 0000000000000000000000000000000000000000..074f777a3ac218bf2240aeb1fc5a2320ad2b6ee8 --- /dev/null +++ b/docs/spring-security/servlet-authentication-passwords-digest.md @@ -0,0 +1,74 @@ +# 摘要认证 + +本节提供了有关 Spring 安全性如何为[摘要认证](https://tools.ietf.org/html/rfc2617)提供支持的详细信息,`DigestAuthenticationFilter`提供了支持。 + +| |在现代应用程序中不应该使用摘要身份验证,因为它不被认为是安全的。
最明显的问题是,你必须以明文、加密或 MD5 格式存储密码,
所有这些存储格式都被认为是不安全的。,相反,
,你应该使用单向自适应密码散列(即 bcrypt、pbkdf2、scrypt 等)存储凭据,摘要身份验证不支持这种方式。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +摘要身份验证试图解决[基本身份验证](basic.html#servlet-authentication-basic)的许多缺点,特别是通过确保凭据永远不会以明文形式发送到网络上。许多[浏览器支持摘要身份验证](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Digest#Browser_compatibility)。 + +管理 HTTP 摘要身份验证的标准由[RFC 2617](https://tools.ietf.org/html/rfc2617)定义,该标准更新了[RFC 2069](https://tools.ietf.org/html/rfc2069)规定的摘要身份验证标准的早期版本。大多数用户代理实现 RFC2617。 Spring Security 的摘要身份验证支持与 RFC2617 规定的“auth”质量保护()兼容,后者还提供与 RFC2069 的向后兼容性。如果你需要使用未加密的 HTTP(即没有 TLS/HTTPS)并希望最大限度地提高身份验证过程的安全性,则摘要身份验证被视为更具吸引力的选项。但是,每个人都应该使用[HTTPS](../../../features/exploits/http.html#http)。 + +摘要身份验证的核心是“nonce”。这是服务器生成的值。 Spring Security 的 nonce 采用以下格式: + +例 1。摘要语法 + +``` +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 +``` + +你将需要使用`NoOpPasswordEncoder`来确保[configure](../../../features/authentication/password-storage.html#authentication-password-storage-configuration)不安全的纯文本[密码存储](../../../features/authentication/password-storage.html#authentication-password-storage)。下面提供了一个使用 爪哇 配置配置摘要身份验证的示例: + +例 2。摘要认证 + +Java + +``` +@Autowired +UserDetailsService userDetailsService; + +DigestAuthenticationEntryPoint entryPoint() { + DigestAuthenticationEntryPoint result = new DigestAuthenticationEntryPoint(); + result.setRealmName("My App Relam"); + result.setKey("3028472b-da34-4501-bfd8-a355c42bdf92"); +} + +DigestAuthenticationFilter digestAuthenticationFilter() { + DigestAuthenticationFilter result = new DigestAuthenticationFilter(); + result.setUserDetailsService(userDetailsService); + result.setAuthenticationEntryPoint(entryPoint()); +} + +protected void configure(HttpSecurity http) throws Exception { + http + // ... + .exceptionHandling(e -> e.authenticationEntryPoint(authenticationEntryPoint())) + .addFilterBefore(digestFilter()); +} +``` + +XML + +``` + + + + + + + + +``` + +[Basic](basic.html)[密码存储](storage.html) + diff --git a/docs/spring-security/servlet-authentication-passwords-form.md b/docs/spring-security/servlet-authentication-passwords-form.md new file mode 100644 index 0000000000000000000000000000000000000000..6194997691f9308e7d4b47664cb43709ab23191e --- /dev/null +++ b/docs/spring-security/servlet-authentication-passwords-form.md @@ -0,0 +1,208 @@ +# 表单登录 + +Spring 安全性为正在通过 HTML 表单提供的用户名和密码提供支持。本节详细介绍了 Spring 安全性中基于表单的身份验证的工作方式。 + +让我们来看看基于表单的登录在 Spring 安全性中是如何工作的。首先,我们来看看用户是如何被重定向到 Log In 表单的。 + +![LoginurlauthenticationEntryPoint ](../../../_images/servlet/authentication/unpwd/loginurlauthenticationentrypoint.png) + +图 1。重定向到登录页面 + +该图构建于我们的[`SecurityFilterChain`](../../architecture.html# Servlet-SecurityFilterchain)图。 + +![number 1](../../../_images/icons/number_1.png)首先,用户向资源`/private`发出未经授权的请求。 + +![number 2](../../../_images/icons/number_2.png) Spring security 的[`FilterSecurityInterceptor`](.../授权/authorization/authorization/authorization-requests.html# Servlet-authorization-filtersecurityinterceptor)通过抛出`AccessDeniedException`表示未经验证的请求是*拒绝*。 + +![number 3](../../../_images/icons/number_3.png)由于未对用户进行身份验证,[`ExceptionTranslationFilter`](..../architecture.html# Servlet-ExceptionTranslationFilter)启动*启动身份验证*,并用配置的[`AuthenticationEntryPoint`](../architecture.html# Servlet-authentication-authentrationEntryPoint)向登录页面发送重定向。在大多数情况下,`AuthenticationEntryPoint`是[`LoginUrlAuthenticationEntryPoint`](https://DOCS. Spring.io/ Spring-security/site/DOCS/5.6.2/api/org/springframework/security/web/authentication/loginurlauthenticationentrypoint.html)的一个实例。 + +![number 4](../../../_images/icons/number_4.png)然后浏览器将请求重定向到的登录页面。 + +![number 5](../../../_images/icons/number_5.png)应用程序中的某个内容,必须[呈现登录页面](#servlet-authentication-form-custom)。 + +当提交用户名和密码时,`UsernamePasswordAuthenticationFilter`将对用户名和密码进行身份验证。`UsernamePasswordAuthenticationFilter`扩展了[抽象处理过滤器](../architecture.html#servlet-authentication-abstractprocessingfilter),所以这个图看起来应该很相似。 + +![用户名 passwordauthenticationfilter ](../../../_images/servlet/authentication/unpwd/usernamepasswordauthenticationfilter.png) + +图 2。验证用户名和密码 + +该图构建于我们的[`SecurityFilterChain`](../../architecture.html# Servlet-SecurityFilterchain)图。 + +![number 1](../../../_images/icons/number_1.png)当用户提交他们的用户名和密码时,`UsernamePasswordAuthenticationFilter`通过从`HttpServletRequest`中提取用户名和密码,创建一个`UsernamePasswordAuthenticationToken`,这是一种[`Authentication`](../architecture.html# Servlet-authentication-authentication)的类型。 + +![number 2](../../../_images/icons/number_2.png)接下来,将`UsernamePasswordAuthenticationToken`传递到`AuthenticationManager`中以进行身份验证。`AuthenticationManager`的详细内容取决于[用户信息被存储](index.html#servlet-authentication-unpwd-storage)的方式。 + +![number 3](../../../_images/icons/number_3.png)如果身份验证失败,则*失败* + +* [SecurityContextholder ](../architecture.html#servlet-authentication-securitycontextholder)被清除。 + +* 调用`RememberMeServices.loginFail`。如果 Remember Me 没有配置,这是一个禁止操作。 + +* 调用`AuthenticationFailureHandler`。 + +![number 4](../../../_images/icons/number_4.png)如果身份验证成功,则*成功*。 + +* `SessionAuthenticationStrategy`被通知有一个新的登录。 + +* [认证](../architecture.html#servlet-authentication-authentication)设置在[SecurityContextholder ](../architecture.html#servlet-authentication-securitycontextholder)上。 + +* 调用`RememberMeServices.loginSuccess`。如果 Remember Me 没有配置,这是一个禁止操作。 + +* `ApplicationEventPublisher`发布`InteractiveAuthenticationSuccessEvent`。 + +* 调用`AuthenticationSuccessHandler`。通常这是一个`SimpleUrlAuthenticationSuccessHandler`,当我们重定向到登录页面时,它将重定向到由[`ExceptionTranslationFilter`](../../architecture.html# Servlet-ExceptionTranslationFilter)保存的请求。 + +Spring 默认情况下启用安全表单登录。然而,只要提供了任何基于 Servlet 的配置,就必须显式地提供基于表单的登录。可以在下面找到最小的、显式的 爪哇 配置: + +例 1。表单登录 + +爪哇 + +``` +protected void configure(HttpSecurity http) { + http + // ... + .formLogin(withDefaults()); +} +``` + +XML + +``` + + + + +``` + +Kotlin + +``` +fun configure(http: HttpSecurity) { + http { + // ... + formLogin { } + } +} +``` + +在这种配置 Spring 中,Security 将呈现一个默认的登录页面。大多数生产应用程序将需要一个自定义日志的形式。 + +下面的配置演示了如何在表单中提供自定义日志。 + +例 2。自定义登录表单配置 + +爪哇 + +``` +protected void configure(HttpSecurity http) throws Exception { + http + // ... + .formLogin(form -> form + .loginPage("/login") + .permitAll() + ); +} +``` + +XML + +``` + + + + + +``` + +Kotlin + +``` +fun configure(http: HttpSecurity) { + http { + // ... + formLogin { + loginPage = "/login" + permitAll() + } + } +} +``` + +当登录页面在 Spring 安全配置中指定时,你负责呈现该页面。下面是一个[Thymeleaf](https://www.thymeleaf.org/)模板,该模板生成符合`/login`登录页面的 HTML 登录表单: + +例 3。登录表单 + +SRC/main/resources/templates/login.html + +``` + + + + Please Log In + + +

Please Log In

+
+ Invalid username and password.
+
+ You have been logged out.
+ +
+ +
+
+ +
+ + + + +``` + +关于默认的 HTML 表单,有几个关键点: + +* 表单应该执行`post`到`/login` + +* 表单将需要包含一个[CSRF Token](../../exploits/csrf.html#servlet-csrf),由 ThymeLeaf 表示为[自动包含](../../exploits/csrf.html#servlet-csrf-include-form-auto)。 + +* 表单应该在名为`username`的参数中指定用户名。 + +* 表单应该在名为`password`的参数中指定密码。 + +* 如果发现 HTTP 参数错误,则表示用户未能提供有效的用户名/密码。 + +* 如果找到了 HTTP 参数注销,则表示用户已成功注销。 + +许多用户只需要定制登录页面就可以了。然而,如果需要的话,上面的所有内容都可以通过额外的配置进行定制。 + +如果你正在使用 Spring MVC,你将需要一个控制器,该控制器将`GET /login`映射到我们创建的登录模板。下面可以看到最小样本`LoginController`: + +例 4。登录控制器 + +Java + +``` +@Controller +class LoginController { + @GetMapping("/login") + String login() { + return "login"; + } +} +``` + +Kotlin + +``` +@Controller +class LoginController { + @GetMapping("/login") + fun login(): String { + return "login" + } +} +``` + +[读取用户名/密码](input.html)[Basic](basic.html) + diff --git a/docs/spring-security/servlet-authentication-passwords-input.md b/docs/spring-security/servlet-authentication-passwords-input.md new file mode 100644 index 0000000000000000000000000000000000000000..dd325c015f7a2fb7448916daf92782749f4ad17e --- /dev/null +++ b/docs/spring-security/servlet-authentication-passwords-input.md @@ -0,0 +1,12 @@ +# 读取用户名和密码 + +Spring 安全性提供了以下内置机制,用于从`HttpServletRequest`读取用户名和密码: + +## 章节摘要 + +* [Form](form.html) +* [Basic](basic.html) +* [Digest](digest.html) + +[用户名/密码](index.html)[Form](form.html) + diff --git a/docs/spring-security/servlet-authentication-passwords-storage-dao-authentication-provider.md b/docs/spring-security/servlet-authentication-passwords-storage-dao-authentication-provider.md new file mode 100644 index 0000000000000000000000000000000000000000..2cca388578bf250032450a8d69cc0623747cab0f --- /dev/null +++ b/docs/spring-security/servlet-authentication-passwords-storage-dao-authentication-provider.md @@ -0,0 +1,22 @@ +# DAoAuthenticationProvider + +[`DaoAuthenticationProvider`](https://DOCS. Spring.io/ Spring-security/site/DOCS/5.6.2/api/org/springframework/security/authentication/dao/daoauthenticationprovider.html)是一种[`AuthenticationProvider`](../architecture.html.html.html# Servlet-attentification-authnationprovider)实现,利用[<`UserDetailsService`](user-details-service.html# Servlet-html#[userauthentification]-html# Servlet- + +让我们来看看`DaoAuthenticationProvider`在 Spring 安全性中是如何工作的。该图详细说明了[读取用户名和密码](index.html#servlet-authentication-unpwd-input)图中的[`AuthenticationManager`](../architecture.html# Servlet-authentication-authenticationmanager)是如何工作的。 + +![DAoAuthenticationProvider ](../../../_images/servlet/authentication/unpwd/daoauthenticationprovider.png) + +图 1。`DaoAuthenticationProvider`用法 + +![number 1](../../../_images/icons/number_1.png)来自[读取用户名和密码](index.html#servlet-authentication-unpwd-input)的身份验证`Filter`将一个`UsernamePasswordAuthenticationToken`传递到`AuthenticationManager`,这是由[`ProviderManager`](../architecture.html# Servlet-assertification-providerManager)实现的。 + +![number 2](../../../_images/icons/number_2.png)`ProviderManager`被配置为使用[身份验证提供者](../architecture.html#servlet-authentication-authenticationprovider)类型的`DaoAuthenticationProvider`。 + +![number 3](../../../_images/icons/number_3.png)`DaoAuthenticationProvider`从`UserDetailsService`中查找`UserDetails`。 + +![number 4](../../../_images/icons/number_4.png)`DaoAuthenticationProvider`然后使用[`PasswordEncoder`](password-encoder.html# Servlet-authentication-password-storage)在上一步返回的`UserDetails`上验证密码。 + +![number 5](../../../_images/icons/number_5.png)当身份验证成功时,返回的[`Authentication`](../architecture.html# Servlet-Authentication-Authentication)类型为`UsernamePasswordAuthenticationToken`,并且具有一个主体,即配置的`UserDetailsService`返回的`UserDetails`。最终,返回的`UsernamePasswordAuthenticationToken`将由身份验证`Filter`设置在[`SecurityContextHolder`](../architecture.html# Servlet-authentication-securitycontextholder)上。 + +[PasswordEncoder ](password-encoder.html)[LDAP](ldap.html) + diff --git a/docs/spring-security/servlet-authentication-passwords-storage-in-memory.md b/docs/spring-security/servlet-authentication-passwords-storage-in-memory.md new file mode 100644 index 0000000000000000000000000000000000000000..4ec08c14a6c9aad9a6fac7f954f12a65244f9dab --- /dev/null +++ b/docs/spring-security/servlet-authentication-passwords-storage-in-memory.md @@ -0,0 +1,124 @@ +# 内存中身份验证 + +Spring Security 的`InMemoryUserDetailsManager`实现[UserDetailsService](user-details-service.html#servlet-authentication-userdetailsservice)以提供对存储在内存中的基于用户名/密码的身份验证的支持。`InMemoryUserDetailsManager`通过实现`UserDetailsManager`接口提供`UserDetails`的管理。 Spring Security 在将其配置为[接受用户名/密码](index.html#servlet-authentication-unpwd-input)以进行身份验证时使用基于`UserDetails`的身份验证。 + +在这个示例中,我们使用[Spring Boot CLI](../../../features/authentication/password-storage.html#authentication-password-storage-boot-cli)对`password`的密码进行编码,并获得`{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW`的编码密码。 + +例 1。InMemoryUserDetailsManager 爪哇 配置 + +爪哇 + +``` +@Bean +public UserDetailsService users() { + UserDetails user = User.builder() + .username("user") + .password("{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW") + .roles("USER") + .build(); + UserDetails admin = User.builder() + .username("admin") + .password("{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW") + .roles("USER", "ADMIN") + .build(); + return new InMemoryUserDetailsManager(user, admin); +} +``` + +XML + +``` + + + + +``` + +Kotlin + +``` +@Bean +fun users(): UserDetailsService { + val user = User.builder() + .username("user") + .password("{bcrypt}$2a$10\$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW") + .roles("USER") + .build() + val admin = User.builder() + .username("admin") + .password("{bcrypt}$2a$10\$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW") + .roles("USER", "ADMIN") + .build() + return InMemoryUserDetailsManager(user, admin) +} +``` + +上面的示例以安全的格式存储密码,但在入门体验方面还有很多不足之处。 + +在下面的示例中,我们利用[User.withDefaultPasswordEncoder](../../../features/authentication/password-storage.html#authentication-password-storage-dep-getting-started)来确保存储在内存中的密码受到保护。然而,它并不能防止通过解码源代码获取密码。因此,`User.withDefaultPasswordEncoder`只应用于“开始”,而不是用于生产。 + +例 2。在 MemoryUserDetailsManager 中使用 user.withDefaultPassWordEncoder + +Java + +``` +@Bean +public UserDetailsService users() { + // The builder will ensure the passwords are encoded before saving in memory + UserBuilder users = User.withDefaultPasswordEncoder(); + UserDetails user = users + .username("user") + .password("password") + .roles("USER") + .build(); + UserDetails admin = users + .username("admin") + .password("password") + .roles("USER", "ADMIN") + .build(); + return new InMemoryUserDetailsManager(user, admin); +} +``` + +Kotlin + +``` +@Bean +fun users(): UserDetailsService { + // The builder will ensure the passwords are encoded before saving in memory + val users = User.withDefaultPasswordEncoder() + val user = users + .username("user") + .password("password") + .roles("USER") + .build() + val admin = users + .username("admin") + .password("password") + .roles("USER", "ADMIN") + .build() + return InMemoryUserDetailsManager(user, admin) +} +``` + +对于基于 XML 的配置,没有简单的方法来使用`User.withDefaultPasswordEncoder`。对于演示或刚刚开始,你可以选择在密码前加上`{noop}`来表示[不应使用编码](../../../features/authentication/password-storage.html#authentication-password-storage-dpe-format)。 + +例 3。\`{noop}`XML 配置 + +``` + + + + +``` + +[密码存储](storage.html)[JDBC](jdbc.html) + diff --git a/docs/spring-security/servlet-authentication-passwords-storage-jdbc.md b/docs/spring-security/servlet-authentication-passwords-storage-jdbc.md new file mode 100644 index 0000000000000000000000000000000000000000..22c6f84d5bbae30d0f6f4346e0851a33d5ffa5a4 --- /dev/null +++ b/docs/spring-security/servlet-authentication-passwords-storage-jdbc.md @@ -0,0 +1,190 @@ +# JDBC 身份验证 + +Spring Security 的`JdbcDaoImpl`实现了[UserDetailsService](user-details-service.html#servlet-authentication-userdetailsservice),以提供对基于用户名/密码的身份验证的支持这是使用 JDBC 检索的。`JdbcUserDetailsManager`扩展`JdbcDaoImpl`以通过`UserDetailsManager`接口提供`UserDetails`的管理。当 Spring Security 将其配置为[接受用户名/密码](index.html#servlet-authentication-unpwd-input)以进行身份验证时,将使用基于`UserDetails`的身份验证。 + +在以下几节中,我们将进行讨论: + +* Spring 安全性 JDBC 身份验证使用的[默认模式](#servlet-authentication-jdbc-schema) + +* [建立数据源](#servlet-authentication-jdbc-datasource) + +* [JDBCuserDetailsManager Bean](#servlet-authentication-jdbc-bean) + +## 默认模式 + +Spring 安全性为基于 JDBC 的身份验证提供了默认查询。本节提供了与默认查询一起使用的相应的默认模式。你将需要调整模式,以匹配对查询和正在使用的数据库方言的任何自定义。 + +### 用户模式 + +`JdbcDaoImpl`要求表为用户加载密码、帐户状态(已启用或禁用)和权限列表(角色)。所需的默认模式可以在下面找到。 + +| |默认的模式也公开为 Classpath 名为`org/springframework/security/core/userdetails/jdbc/users.ddl`的资源。| +|---|--------------------------------------------------------------------------------------------------------------------------------| + +例 1。默认用户模式 + +``` +create table users( + username varchar_ignorecase(50) not null primary key, + password varchar_ignorecase(500) 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 是一个流行的数据库选择,但需要一个稍微不同的模式。你可以在下面找到用户的默认 Oracle 模式。 + +例 2。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; +``` + +### 组模式 + +如果你的应用程序正在利用组,那么你将需要提供组模式。可以在下面找到组的默认模式。 + +例 3。默认组模式 + +``` +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) +); +``` + +## 建立数据源 + +在配置`JdbcUserDetailsManager`之前,必须创建`DataSource`。在我们的示例中,我们将设置一个[嵌入式数据源](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/data-access.html#jdbc-embedded-database-support),它是用[默认用户模式](#servlet-authentication-jdbc-schema)初始化的。 + +例 4。嵌入式数据源 + +爪哇 + +``` +@Bean +DataSource dataSource() { + return new EmbeddedDatabaseBuilder() + .setType(H2) + .addScript("classpath:org/springframework/security/core/userdetails/jdbc/users.ddl") + .build(); +} +``` + +XML + +``` + + + +``` + +Kotlin + +``` +@Bean +fun dataSource(): DataSource { + return EmbeddedDatabaseBuilder() + .setType(H2) + .addScript("classpath:org/springframework/security/core/userdetails/jdbc/users.ddl") + .build() +} +``` + +在生产环境中,你将需要确保设置到外部数据库的连接。 + +## JdbcUserDetailsManager Bean + +在这个示例中,我们使用[Spring Boot CLI](../../../features/authentication/password-storage.html#authentication-password-storage-boot-cli)对`password`的密码进行编码,并获得`{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW`的编码密码。有关如何存储密码的更多详细信息,请参见[PasswordEncoder](../../../features/authentication/password-storage.html#authentication-password-storage)小节。 + +例 5。JDBCuserDetailsManager + +爪哇 + +``` +@Bean +UserDetailsManager users(DataSource dataSource) { + UserDetails user = User.builder() + .username("user") + .password("{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW") + .roles("USER") + .build(); + UserDetails admin = User.builder() + .username("admin") + .password("{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW") + .roles("USER", "ADMIN") + .build(); + JdbcUserDetailsManager users = new JdbcUserDetailsManager(dataSource); + users.createUser(user); + users.createUser(admin); + return users; +} +``` + +XML + +``` + + + + +``` + +Kotlin + +``` +@Bean +fun users(dataSource: DataSource): UserDetailsManager { + val user = User.builder() + .username("user") + .password("{bcrypt}$2a$10\$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW") + .roles("USER") + .build(); + val admin = User.builder() + .username("admin") + .password("{bcrypt}$2a$10\$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW") + .roles("USER", "ADMIN") + .build(); + val users = JdbcUserDetailsManager(dataSource) + users.createUser(user) + users.createUser(admin) + return users +} +``` + +[In Memory](in-memory.html)[用户详细信息](user-details.html) + diff --git a/docs/spring-security/servlet-authentication-passwords-storage-ldap.md b/docs/spring-security/servlet-authentication-passwords-storage-ldap.md new file mode 100644 index 0000000000000000000000000000000000000000..db0a4c02d83b0a8ae20a7af845c9664355d0b212 --- /dev/null +++ b/docs/spring-security/servlet-authentication-passwords-storage-ldap.md @@ -0,0 +1,524 @@ +# LDAP 身份验证 + +LDAP 通常被组织用作用户信息的中心存储库和身份验证服务。它还可以用于存储应用程序用户的角色信息。 + +Spring 当安全性被配置为[接受用户名/密码](index.html#servlet-authentication-unpwd-input)以进行身份验证时,安全性使用基于 LDAP 的身份验证。然而,尽管利用用户名/密码进行身份验证,但它并未使用`UserDetailsService`进行集成,因为在[绑定身份验证](#servlet-authentication-ldap-bind)中,LDAP 服务器不返回密码,因此应用程序无法对密码进行验证。 + +对于如何配置 LDAP 服务器,有许多不同的场景,因此 Spring Security 的 LDAP 提供者是完全可配置的。它使用独立的策略接口来进行身份验证和角色检索,并提供可配置为处理各种情况的默认实现。 + +## 先决条件 + +在尝试使用 Spring 安全性之前,你应该熟悉 LDAP。下面的链接很好地介绍了所涉及的概念,并提供了使用免费的 LDAP 服务器 OpenLDAP 设置目录的指南:[https://www.zytrax.com/books/ldap/](https://www.zytrax.com/books/ldap/)。熟悉一些用于从 爪哇 访问 LDAP 的 JNDI API 也可能是有用的。在 LDAP 提供程序中,我们不使用任何第三方 LDAP 库(Mozilla、JLDAP 等),但是大量使用了 Spring LDAP,因此,如果你计划添加自己的定制,那么熟悉该项目可能会很有用。 + +在使用 LDAP 身份验证时,确保正确配置 LDAP 连接池非常重要。如果你不熟悉如何做到这一点,你可以参考[爪哇 LDAP 文档](https://docs.oracle.com/javase/jndi/tutorial/ldap/connect/config.html)。 + +## 设置嵌入式 LDAP 服务器 + +你需要做的第一件事是确保你有一个 LDAP 服务器来指向你的配置。为了简单起见,通常最好从嵌入式 LDAP 服务器开始。 Spring 安全支持使用以下两种方式之一: + +* [嵌入式无 boundid 服务器](#servlet-authentication-ldap-unboundid) + +* [嵌入式 ApacheDS 服务器](#servlet-authentication-ldap-apacheds) + +在下面的示例中,我们以`users.ldif`作为 Classpath 资源公开以下内容,以初始化嵌入的 LDAP 服务器,其中用户`user`和`admin`的密码都为`password`。 + +users.ldif + +``` +dn: ou=groups,dc=springframework,dc=org +objectclass: top +objectclass: organizationalUnit +ou: groups + +dn: ou=people,dc=springframework,dc=org +objectclass: top +objectclass: organizationalUnit +ou: people + +dn: uid=admin,ou=people,dc=springframework,dc=org +objectclass: top +objectclass: person +objectclass: organizationalPerson +objectclass: inetOrgPerson +cn: Rod Johnson +sn: Johnson +uid: admin +userPassword: password + +dn: uid=user,ou=people,dc=springframework,dc=org +objectclass: top +objectclass: person +objectclass: organizationalPerson +objectclass: inetOrgPerson +cn: Dianne Emu +sn: Emu +uid: user +userPassword: password + +dn: cn=user,ou=groups,dc=springframework,dc=org +objectclass: top +objectclass: groupOfNames +cn: user +uniqueMember: uid=admin,ou=people,dc=springframework,dc=org +uniqueMember: uid=user,ou=people,dc=springframework,dc=org + +dn: cn=admin,ou=groups,dc=springframework,dc=org +objectclass: top +objectclass: groupOfNames +cn: admin +uniqueMember: uid=admin,ou=people,dc=springframework,dc=org +``` + +### 嵌入式无 boundid 服务器 + +如果你希望使用[UnboundID](https://ldap.com/unboundid-ldap-sdk-for-java/),那么请指定以下依赖项: + +例 1。无边界依赖项 + +Maven + +``` + + com.unboundid + unboundid-ldapsdk + 4.0.14 + runtime + +``` + +Gradle + +``` +depenendencies { + runtimeOnly "com.unboundid:unboundid-ldapsdk:4.0.14" +} +``` + +然后就可以配置嵌入式 LDAP 服务器了。 + +例 2。嵌入式 LDAP 服务器配置 + +爪哇 + +``` +@Bean +UnboundIdContainer ldapContainer() { + return new UnboundIdContainer("dc=springframework,dc=org", + "classpath:users.ldif"); +} +``` + +XML + +``` + +``` + +Kotlin + +``` +@Bean +fun ldapContainer(): UnboundIdContainer { + return UnboundIdContainer("dc=springframework,dc=org","classpath:users.ldif") +} +``` + +### 嵌入式 ApacheDS 服务器 + +| |Spring 安全性使用的是不再维护的 ApacheDS1.x。
遗憾的是,ApacheDS2.x 只发布了里程碑版本,没有稳定的发布。
一旦稳定的 ApacheDS2.x 版本可用,我们将考虑更新。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +如果你希望使用[Apache DS](https://directory.apache.org/apacheds/),那么请指定以下依赖项: + +例 3。Apacheds 依赖项 + +Maven + +``` + + org.apache.directory.server + apacheds-core + 1.5.5 + runtime + + + org.apache.directory.server + apacheds-server-jndi + 1.5.5 + runtime + +``` + +Gradle + +``` +depenendencies { + runtimeOnly "org.apache.directory.server:apacheds-core:1.5.5" + runtimeOnly "org.apache.directory.server:apacheds-server-jndi:1.5.5" +} +``` + +然后就可以配置嵌入式 LDAP 服务器了。 + +例 4。嵌入式 LDAP 服务器配置 + +爪哇 + +``` +@Bean +ApacheDSContainer ldapContainer() { + return new ApacheDSContainer("dc=springframework,dc=org", + "classpath:users.ldif"); +} +``` + +XML + +``` + +``` + +Kotlin + +``` +@Bean +fun ldapContainer(): ApacheDSContainer { + return ApacheDSContainer("dc=springframework,dc=org", "classpath:users.ldif") +} +``` + +## LDAP ContextSource + +一旦你有了一个 LDAP 服务器来指向你的配置,你就需要配置 Spring 安全性来指向一个应该用来验证用户身份的 LDAP 服务器。这是通过创建一个 LDAP`ContextSource`来完成的,它相当于一个 JDBC`DataSource`。 + +例 5。LDAP 上下文源代码 + +爪哇 + +``` +ContextSource contextSource(UnboundIdContainer container) { + return new DefaultSpringSecurityContextSource("ldap://localhost:53389/dc=springframework,dc=org"); +} +``` + +XML + +``` + +``` + +Kotlin + +``` +fun contextSource(container: UnboundIdContainer): ContextSource { + return DefaultSpringSecurityContextSource("ldap://localhost:53389/dc=springframework,dc=org") +} +``` + +## 认证 + +Spring Security 的 LDAP 支持不使用[UserDetailsService](user-details-service.html#servlet-authentication-userdetailsservice),因为 LDAP BIND 身份验证不允许客户端读取密码,甚至不允许读取密码的散列版本。这意味着不可能读取密码,然后由 Spring Security 进行身份验证。 + +因此,LDAP 支持是使用`LdapAuthenticator`接口实现的。`LdapAuthenticator`还负责检索任何必需的用户属性。这是因为属性的权限可能取决于所使用的身份验证类型。例如,如果绑定为用户,则可能需要使用用户自己的权限来读取它们。 + +有两个`LdapAuthenticator`实现提供了 Spring 安全性: + +* [使用绑定身份验证](#servlet-authentication-ldap-bind) + +* [使用密码身份验证](#servlet-authentication-ldap-pwd) + +## 使用绑定身份验证 + +[绑定身份验证](https://ldap.com/the-ldap-bind-operation/)是使用 LDAP 对用户进行身份验证的最常见机制。在 BIND 身份验证中,用户凭据(即用户名/密码)被提交给 LDAP 服务器,由该服务器对其进行身份验证。使用绑定身份验证的好处是,用户的秘密(即密码)不需要暴露给客户端,这有助于保护它们不被泄露。 + +可以在下面找到绑定身份验证配置的示例。 + +例 6。绑定身份验证 + +爪哇 + +``` +@Bean +BindAuthenticator authenticator(BaseLdapPathContextSource contextSource) { + BindAuthenticator authenticator = new BindAuthenticator(contextSource); + authenticator.setUserDnPatterns(new String[] { "uid={0},ou=people" }); + return authenticator; +} + +@Bean +LdapAuthenticationProvider authenticationProvider(LdapAuthenticator authenticator) { + return new LdapAuthenticationProvider(authenticator); +} +``` + +XML + +``` + +``` + +Kotlin + +``` +@Bean +fun authenticator(contextSource: BaseLdapPathContextSource): BindAuthenticator { + val authenticator = BindAuthenticator(contextSource) + authenticator.setUserDnPatterns(arrayOf("uid={0},ou=people")) + return authenticator +} + +@Bean +fun authenticationProvider(authenticator: LdapAuthenticator): LdapAuthenticationProvider { + return LdapAuthenticationProvider(authenticator) +} +``` + +这个简单的示例将通过在提供的模式中替换用户登录名并尝试将该用户与登录密码绑定为该用户来获得用户的 DN。如果你的所有用户都存储在目录中的一个节点下,则可以这样做。如果你希望配置一个 LDAP 搜索过滤器来定位用户,那么可以使用以下方法: + +例 7。用搜索筛选器绑定身份验证 + +爪哇 + +``` +@Bean +BindAuthenticator authenticator(BaseLdapPathContextSource contextSource) { + String searchBase = "ou=people"; + String filter = "(uid={0})"; + FilterBasedLdapUserSearch search = + new FilterBasedLdapUserSearch(searchBase, filter, contextSource); + BindAuthenticator authenticator = new BindAuthenticator(contextSource); + authenticator.setUserSearch(search); + return authenticator; +} + +@Bean +LdapAuthenticationProvider authenticationProvider(LdapAuthenticator authenticator) { + return new LdapAuthenticationProvider(authenticator); +} +``` + +XML + +``` + +``` + +Kotlin + +``` +@Bean +fun authenticator(contextSource: BaseLdapPathContextSource): BindAuthenticator { + val searchBase = "ou=people" + val filter = "(uid={0})" + val search = FilterBasedLdapUserSearch(searchBase, filter, contextSource) + val authenticator = BindAuthenticator(contextSource) + authenticator.setUserSearch(search) + return authenticator +} + +@Bean +fun authenticationProvider(authenticator: LdapAuthenticator): LdapAuthenticationProvider { + return LdapAuthenticationProvider(authenticator) +} +``` + +如果与`ContextSource`[以上定义](#servlet-authentication-ldap-contextsource)一起使用,则将在 dn`ou=people,dc=springframework,dc=org`下使用`(uid={0})`作为过滤器执行搜索。同样,用户登录名被过滤器名中的参数所代替,因此它将搜索具有`uid`属性的条目,该属性等于用户名。如果没有提供用户搜索库,搜索将从根目录执行。 + +## 使用密码身份验证 + +密码比较是指将用户提供的密码与存储库中存储的密码进行比较。这可以通过检索 Password 属性的值并在本地进行检查来完成,也可以通过执行 LDAP“Compare”操作来完成,在该操作中,提供的密码被传递给服务器进行比较,而不会检索到真正的密码值。当密码被随机 SALT 正确散列时,无法进行 LDAP 比较。 + +例 8。最小密码比较配置 + +爪哇 + +``` +@Bean +PasswordComparisonAuthenticator authenticator(BaseLdapPathContextSource contextSource) { + return new PasswordComparisonAuthenticator(contextSource); +} + +@Bean +LdapAuthenticationProvider authenticationProvider(LdapAuthenticator authenticator) { + return new LdapAuthenticationProvider(authenticator); +} +``` + +XML + +``` + + + +``` + +Kotlin + +``` +@Bean +fun authenticator(contextSource: BaseLdapPathContextSource): PasswordComparisonAuthenticator { + return PasswordComparisonAuthenticator(contextSource) +} + +@Bean +fun authenticationProvider(authenticator: LdapAuthenticator): LdapAuthenticationProvider { + return LdapAuthenticationProvider(authenticator) +} +``` + +可以在下面找到带有一些自定义功能的更高级配置。 + +例 9。密码比较配置 + +爪哇 + +``` +@Bean +PasswordComparisonAuthenticator authenticator(BaseLdapPathContextSource contextSource) { + PasswordComparisonAuthenticator authenticator = + new PasswordComparisonAuthenticator(contextSource); + authenticator.setPasswordAttributeName("pwd"); (1) + authenticator.setPasswordEncoder(new BCryptPasswordEncoder()); (2) + return authenticator; +} + +@Bean +LdapAuthenticationProvider authenticationProvider(LdapAuthenticator authenticator) { + return new LdapAuthenticationProvider(authenticator); +} +``` + +XML + +``` + + (1) + (2) + + + +``` + +Kotlin + +``` +@Bean +fun authenticator(contextSource: BaseLdapPathContextSource): PasswordComparisonAuthenticator { + val authenticator = PasswordComparisonAuthenticator(contextSource) + authenticator.setPasswordAttributeName("pwd") (1) + authenticator.setPasswordEncoder(BCryptPasswordEncoder()) (2) + return authenticator +} + +@Bean +fun authenticationProvider(authenticator: LdapAuthenticator): LdapAuthenticationProvider { + return LdapAuthenticationProvider(authenticator) +} +``` + +|**1**|将 password 属性指定为`pwd`| +|-----|---------------------------------------| +|**2**|使用`BCryptPasswordEncoder`| + +## ldapAuthoritiesPopulator + +Spring 安全性的`LdapAuthoritiesPopulator`用于确定将为用户返回什么授权。 + +例 10。ldapAuthoritiesPopulator 配置 + +Java + +``` +@Bean +LdapAuthoritiesPopulator authorities(BaseLdapPathContextSource contextSource) { + String groupSearchBase = ""; + DefaultLdapAuthoritiesPopulator authorities = + new DefaultLdapAuthoritiesPopulator(contextSource, groupSearchBase); + authorities.setGroupSearchFilter("member={0}"); + return authorities; +} + +@Bean +LdapAuthenticationProvider authenticationProvider(LdapAuthenticator authenticator, LdapAuthoritiesPopulator authorities) { + return new LdapAuthenticationProvider(authenticator, authorities); +} +``` + +XML + +``` + +``` + +Kotlin + +``` +@Bean +fun authorities(contextSource: BaseLdapPathContextSource): LdapAuthoritiesPopulator { + val groupSearchBase = "" + val authorities = DefaultLdapAuthoritiesPopulator(contextSource, groupSearchBase) + authorities.setGroupSearchFilter("member={0}") + return authorities +} + +@Bean +fun authenticationProvider(authenticator: LdapAuthenticator, authorities: LdapAuthoritiesPopulator): LdapAuthenticationProvider { + return LdapAuthenticationProvider(authenticator, authorities) +} +``` + +## 活动目录 + +Active Directory 支持其自己的非标准身份验证选项,并且正常的使用模式与标准`LdapAuthenticationProvider`不太匹配。通常,身份验证是使用域用户名(形式为`[[email protected]](/cdn-cgi/l/email-protection)`)执行的,而不是使用 LDAP 专有名称。 Spring 为了使这更容易,Security 有一个身份验证提供者,该提供者是为典型的活动目录设置定制的。 + +配置`ActiveDirectoryLdapAuthenticationProvider`非常简单。你只需要提供域名和一个 LDAP URL,该 URL 提供服务器的地址[1]。下面是一个配置示例: + +例 11。活动目录配置示例 + +Java + +``` +@Bean +ActiveDirectoryLdapAuthenticationProvider authenticationProvider() { + return new ActiveDirectoryLdapAuthenticationProvider("example.com", "ldap://company.example.com/"); +} +``` + +XML + +``` + + + + +``` + +Kotlin + +``` +@Bean +fun authenticationProvider(): ActiveDirectoryLdapAuthenticationProvider { + return ActiveDirectoryLdapAuthenticationProvider("example.com", "ldap://company.example.com/") +} +``` + +--- + +[1](#_footnoteref_1)。还可以使用 DNS 查找获得服务器的 IP 地址。这是目前不支持的,但希望将在未来的版本。 + +[DAoAuthenticationProvider](dao-authentication-provider.html)[Session Management](../session-management.html) + diff --git a/docs/spring-security/servlet-authentication-passwords-storage-password-encoder.md b/docs/spring-security/servlet-authentication-passwords-storage-password-encoder.md new file mode 100644 index 0000000000000000000000000000000000000000..b820f7f4d0861bc9c08eb22faff304eced44c11f --- /dev/null +++ b/docs/spring-security/servlet-authentication-passwords-storage-password-encoder.md @@ -0,0 +1,6 @@ +# PasswordEncoder + +Spring Security 的 Servlet 支持通过集成[`PasswordEncoder`](.../../Features/Authentication/Password-Storage.html#Authentication-Password-Storage)来安全地存储密码。自定义 Spring Security 使用的`PasswordEncoder`实现可以通过[公开`PasswordEncoder` Bean](.../.../Features/Authentication/password-storage.html#Authentication-password-storage-configuration)来完成。 + +[UserDetailsService ](user-details-service.html)[DAoAuthenticationProvider ](dao-authentication-provider.html) + diff --git a/docs/spring-security/servlet-authentication-passwords-storage-user-details-service.md b/docs/spring-security/servlet-authentication-passwords-storage-user-details-service.md new file mode 100644 index 0000000000000000000000000000000000000000..59277a3079bfc546611c78877feadfc33dd92a22 --- /dev/null +++ b/docs/spring-security/servlet-authentication-passwords-storage-user-details-service.md @@ -0,0 +1,35 @@ +# UserDetailsService + +[`UserDetailsService`](https://DOCS. Spring.io/ Spring-security/site/DOCS/5.6.2/api/org/springframework/security/core/userdetails/userdetails.html)是[`DaoAuthenticationProvider`](DAO-Authentication-provider.html# Servlet-Authentication-DAOAuthenticationationationProvider)用于检索用户名、密码和其他属性,以进行用户名和密码的身份验证。 Spring 安全性提供[in-memory](in-memory.html#servlet-authentication-inmemory)和[JDBC](jdbc.html#servlet-authentication-jdbc)`UserDetailsService`的实现。 + +可以通过将自定义`UserDetailsService`公开为 Bean 来定义自定义身份验证。例如,下面将在假设`CustomUserDetailsService`实现`UserDetailsService`的情况下定制身份验证: + +| |这仅在`AuthenticationManagerBuilder`尚未填充且没有定义`AuthenticationProviderBean`时使用。| +|---|------------------------------------------------------------------------------------------------------------------------------| + +例 1。自定义用户详细服务 Bean + +爪哇 + +``` +@Bean +CustomUserDetailsService customUserDetailsService() { + return new CustomUserDetailsService(); +} +``` + +XML + +``` + +``` + +Kotlin + +``` +@Bean +fun customUserDetailsService() = CustomUserDetailsService() +``` + +[用户详细信息](user-details.html)[PasswordEncoder ](password-encoder.html) + diff --git a/docs/spring-security/servlet-authentication-passwords-storage-user-details.md b/docs/spring-security/servlet-authentication-passwords-storage-user-details.md new file mode 100644 index 0000000000000000000000000000000000000000..3e36a420a623790f417e97a8734b8b0535548c7a --- /dev/null +++ b/docs/spring-security/servlet-authentication-passwords-storage-user-details.md @@ -0,0 +1,6 @@ +# 用户详细信息 + +[`UserDetails`](https://DOCS. Spring.io/ Spring-security/site/DOCS/5.6.2/api/org/springframework/security/core/userdetails/userdetails.html)是由[`UserDetailsService`](user-details-service.html# Servlet-attentification-userdetailssservice)返回的。[`DaoAuthenticationProvider`](DAO-Authentication-Provider.html# Servlet-Authentication-DaoAuthenticationProvider)验证`UserDetails`,然后返回一个[`Authentication`](../architecture.html# Servlet-Authentication-Authentication),其主体是由配置的`UserDetailsService`返回的`UserDetails`。 + +[JDBC](jdbc.html)[UserDetailsService ](user-details-service.html) + diff --git a/docs/spring-security/servlet-authentication-passwords-storage.md b/docs/spring-security/servlet-authentication-passwords-storage.md new file mode 100644 index 0000000000000000000000000000000000000000..259d8ccbd4ed1831b1b9c7c73e9450323fde0796 --- /dev/null +++ b/docs/spring-security/servlet-authentication-passwords-storage.md @@ -0,0 +1,24 @@ +# 存储机制 + +用于读取用户名和密码的每种受支持的机制都可以利用任何受支持的存储机制: + +* 带有[内存中身份验证](in-memory.html#servlet-authentication-inmemory)的简单存储 + +* 具有[JDBC 身份验证](jdbc.html#servlet-authentication-jdbc)的关系数据库 + +* 自定义数据存储[UserDetailsService ](user-details-service.html#servlet-authentication-userdetailsservice) + +* 具有[LDAP 身份验证](ldap.html#servlet-authentication-ldap)的 LDAP 存储 + +## 章节摘要 + +* [In Memory](in-memory.html) +* [JDBC](jdbc.html) +* [用户详细信息](user-details.html) +* [UserDetailsService ](user-details-service.html) +* [PasswordEncoder ](password-encoder.html) +* [DAoAuthenticationProvider ](dao-authentication-provider.html) +* [LDAP](ldap.html) + +[Digest](digest.html)[In Memory](in-memory.html) + diff --git a/docs/spring-security/servlet-authentication-passwords.md b/docs/spring-security/servlet-authentication-passwords.md new file mode 100644 index 0000000000000000000000000000000000000000..035efa05f6bdc5e5e67ace929fe9cdffd0a0d090 --- /dev/null +++ b/docs/spring-security/servlet-authentication-passwords.md @@ -0,0 +1,11 @@ +# 用户名/密码身份验证 + +验证用户身份的最常见方法之一是验证用户名和密码。因此, Spring 安全性为使用用户名和密码进行身份验证提供了全面的支持。 + +## 章节摘要 + +* [读取用户名/密码](input.html) +* [密码存储](storage.html) + +[认证体系结构](../architecture.html)[读取用户名/密码](input.html) + diff --git a/docs/spring-security/servlet-authentication-preauth.md b/docs/spring-security/servlet-authentication-preauth.md new file mode 100644 index 0000000000000000000000000000000000000000..1da982f90fa7a647e585935aea5d32015dbb269e --- /dev/null +++ b/docs/spring-security/servlet-authentication-preauth.md @@ -0,0 +1,112 @@ +# 预认证场景 + +示例包括 X.509、SiteMinder 和通过运行应用程序的 爪哇 EE 容器进行的身份验证。在使用预身份验证时, Spring 安全性必须 + +* 识别提出请求的用户。 + +* 为用户获取权限。 + +细节将取决于外部身份验证机制。在 X.509 的情况下,用户可以通过他们的证书信息来标识,在 SiteMinder 的情况下,用户可以通过 HTTP 请求头来标识。如果依赖于容器身份验证,则将通过在传入的 HTTP 请求上调用`getUserPrincipal()`方法来识别用户。在某些情况下,外部机制可以为用户提供角色/权限信息,但在另一些情况下,权限必须从单独的来源获得,例如`UserDetailsService`。 + +## 预认证框架类 + +因为大多数预身份验证机制遵循相同的模式, Spring Security 有一组类,它们为实现预身份验证的身份验证提供者提供了一个内部框架。这消除了重复,并允许以结构化的方式添加新的实现,而无需从头开始编写所有内容。如果你想使用[X.509 认证](x509.html#servlet-x509)之类的东西,那么你不需要了解这些类,因为它已经有了一个名称空间配置选项,该选项更易于使用和启动。如果你需要使用显式 Bean 配置,或者正在计划编写自己的实现,那么了解所提供的实现是如何工作的将是有用的。你将在`org.springframework.security.web.authentication.preauth`下找到类。我们只是在这里提供一个大纲,所以你应该在适当的地方参考 Javadoc 和源代码。 + +### 抽象预先验证的处理过滤器 + +这个类将检查安全上下文的当前内容,如果为空,它将尝试从 HTTP 请求中提取用户信息,并将其提交给`AuthenticationManager`。子类覆盖以下方法以获得此信息: + +例 1。覆盖抽象预认证处理过滤器 + +Java + +``` +protected abstract Object getPreAuthenticatedPrincipal(HttpServletRequest request); + +protected abstract Object getPreAuthenticatedCredentials(HttpServletRequest request); +``` + +Kotlin + +``` +protected abstract fun getPreAuthenticatedPrincipal(request: HttpServletRequest): Any? + +protected abstract fun getPreAuthenticatedCredentials(request: HttpServletRequest): Any? +``` + +在调用这些之后,筛选器将创建一个`PreAuthenticatedAuthenticationToken`,其中包含返回的数据,并将其提交以进行身份验证。这里的“身份验证”实际上只是指进一步的处理,以便可能加载用户的权限,但是遵循了标准的安全身份验证体系结构 Spring。 + +与其他 Spring 安全身份验证过滤器一样,预身份验证过滤器具有`authenticationDetailsSource`属性,该属性在默认情况下将创建`WebAuthenticationDetails`对象,以在`Authentication`对象的`details`属性中存储诸如会话-标识符和发起 IP 地址等附加信息。在可以从预身份验证机制获得用户角色信息的情况下,该数据也存储在该属性中,具有实现`GrantedAuthoritiesContainer`接口的细节。这使身份验证提供程序能够读取外部分配给用户的权限。接下来我们将看一个具体的例子。 + +#### J2EebasedPreAuthenticatedWebAuthenticationDetailsSource + +如果筛选器配置了`authenticationDetailsSource`,这是该类的一个实例,则通过为每个预先确定的“可映射角色”集合调用`isUserInRole(String role)`方法来获得权限信息。该类从配置的`MappableAttributesRetriever`获得这些。可能的实现方式包括在应用程序上下文中对列表进行硬编码,并从``文件中的`web.xml`信息中读取角色信息。预认证示例应用程序使用后一种方法。 + +还有一个额外的阶段,其中角色(或属性)使用配置的`Attributes2GrantedAuthoritiesMapper`映射到 Spring Security`GrantedAuthority`对象。默认值只会将通常的`ROLE_`前缀添加到名称中,但它使你可以完全控制行为。 + +### 预先验证 dauthenticationProvider + +预先验证的提供者除了为用户加载`UserDetails`对象外,几乎没有什么可做的。它通过委托给`AuthenticationUserDetailsService`来实现这一点。后者类似于标准`UserDetailsService`,但接受一个`Authentication`对象,而不仅仅是用户名: + +``` +public interface AuthenticationUserDetailsService { + UserDetails loadUserDetails(Authentication token) throws UsernameNotFoundException; +} +``` + +这个接口可能还有其他用途,但是通过预身份验证,它允许访问打包在`Authentication`对象中的权限,就像我们在上一节中看到的那样。`PreAuthenticatedGrantedAuthoritiesUserDetailsService`类实现了这一点。或者,可以通过`UserDetailsByNameServiceWrapper`实现将其委托给标准`UserDetailsService`。 + +### HTTP403 禁止 BiddenentryPoint + +[`AuthenticationEntryPoint`](architecture.html# Servlet-authentication-authentryPoint)负责启动未经身份验证的用户的身份验证过程(当他们试图访问受保护的资源时),但在预先验证的情况下,这不适用。如果你 AREN 不将预身份验证与其他身份验证机制结合使用,那么你将仅使用该类的实例来配置`ExceptionTranslationFilter`。如果用户被`AbstractPreAuthenticatedProcessingFilter`拒绝,导致空身份验证,则将调用它。如果调用它,它总是返回`403`-禁止的响应代码。 + +## 具体实现 + +X.509 身份验证包含在其[自己的章节](x509.html#servlet-x509)中。在这里,我们将介绍一些类,它们为其他预先验证的场景提供了支持。 + +### + +外部身份验证系统可以通过在 HTTP 请求上设置特定的头来向应用程序提供信息。一个著名的例子是 SiteMinder,它在一个名为`SM_USER`的头文件中传递用户名。这个机制得到了`RequestHeaderAuthenticationFilter`类的支持,它只是从头部提取用户名。默认情况下,它使用`SM_USER`作为标题名称。有关更多详细信息,请参见 Javadoc。 + +| |注意,当使用这样的系统时,框架根本不执行任何身份验证检查,它是*极端*重要的外部系统已正确配置,并保护了对应用程序的所有访问。
如果攻击者能够伪造其原始请求中的头而不被检测到,那么他们可能会选择他们想要的任何用户名。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### SiteMinder 示例配置 + +使用此筛选器的典型配置如下所示: + +``` + + + + + + + + + + + + + + + + + + + + + +``` + +这里我们假设[安全命名空间](../configuration/xml-namespace.html#ns-config)用于配置。还假定你在配置中添加了`UserDetailsService`(称为“UserDetailsService”)以加载用户的角色。 + +### Java EE 容器身份验证 + +类`J2eePreAuthenticatedProcessingFilter`将从`HttpServletRequest`的`userPrincipal`属性中提取用户名。这个过滤器的使用通常与上面[J2EebasedPreAuthenticatedWebAuthenticationDetailsSource ](#j2ee-preauth-details)中描述的 Java EE 角色的使用相结合。 + +在 Samples 项目中有一个[示例应用程序](https://github.com/spring-projects/spring-security/tree/5.4.x/samples/xml/preauth)使用这种方法,因此,如果你感兴趣的话,可以从 Github 获取代码并查看应用程序上下文文件。 + +[Anonymous](anonymous.html)[JAAS](jaas.html) + diff --git a/docs/spring-security/servlet-authentication-rememberme.md b/docs/spring-security/servlet-authentication-rememberme.md new file mode 100644 index 0000000000000000000000000000000000000000..5487a13801b42975db1597b380fd604130d44428 --- /dev/null +++ b/docs/spring-security/servlet-authentication-rememberme.md @@ -0,0 +1,113 @@ +# rememe-me 认证 + +## 概述 + +Remember-Me 或 Persistent-Login 身份验证是指网站能够在会话之间记住主体的身份。这通常是通过向浏览器发送一个 cookie 来实现的,在将来的会话中会检测到该 cookie,并导致自动登录。 Spring 安全性为这些操作的发生提供了必要的挂钩,并且具有两个具体的 Rememe-me 实现。一种是使用散列来保护基于 Cookie 的令牌的安全性,另一种是使用数据库或其他持久存储机制来存储生成的令牌。 + +注意,这两个实现都需要`UserDetailsService`。如果你使用的身份验证提供程序不使用`UserDetailsService`(例如,LDAP 提供程序),那么它将无法工作,除非你的应用程序上下文中也有`UserDetailsService` Bean。 + +## 一种简单的基于散列的令牌方法 + +这种方法使用散列来实现一个有用的 rememe-me 策略。本质上,在成功进行交互身份验证后,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 +``` + +因此,Rememe-Me 令牌仅在指定的时间段内有效,并且前提是用户名、密码和密钥不会更改。值得注意的是,这有一个潜在的安全问题,因为捕获的 Rememe-Me 令牌可以从任何用户代理使用,直到令牌过期为止。这是与摘要身份验证相同的问题。如果委托人知道某个令牌已被捕获,他们可以很容易地更改其密码,并立即使所有发行的 Rememe-Me 令牌无效。如果需要更重要的安全性,则应该使用下一节中描述的方法。或者,根本不应该使用 Rememe-me 服务。 + +如果你熟悉[名称空间配置](../configuration/xml-namespace.html#ns-config)一章中讨论的主题,那么只需添加``元素,就可以启用 rememe 身份验证: + +``` + +... + + +``` + +`UserDetailsService`通常会被自动选择。如果你的应用程序上下文中有多个应用程序,那么你需要指定应该使用`user-service-ref`属性中的哪个属性,其中的值是你的`UserDetailsService` Bean 的名称。 + +## 持久令牌方法 + +这种方法是以[http://jaspan.com/improved\_persistent\_login\_cookie\_best\_practice](https://web.archive.org/web/20180819014446/http://jaspan.com/improved_persistent_login_cookie_best_practice)为基础的,对[1]作了一些小的修改。要在名称空间配置中使用这种方法,你需要提供一个数据源引用: + +``` + +... + + +``` + +数据库应该包含一个`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) +``` + +## Remember-Me 接口和实现 + +remember-me 用于`UsernamePasswordAuthenticationFilter`,并通过`AbstractAuthenticationProcessingFilter`超类中的钩子实现。它也在`BasicAuthenticationFilter`中使用。钩子将在适当的时候调用一个具体的`RememberMeServices`。界面如下所示: + +``` +Authentication autoLogin(HttpServletRequest request, HttpServletResponse response); + +void loginFail(HttpServletRequest request, HttpServletResponse response); + +void loginSuccess(HttpServletRequest request, HttpServletResponse response, + Authentication successfulAuthentication); +``` + +请参考 Javadoc 以更全面地讨论这些方法的作用,尽管在此阶段注意`AbstractAuthenticationProcessingFilter`只调用`loginFail()`和`loginSuccess()`方法。当`SecurityContextHolder`不包含`Authentication`时,`autoLogin()`方法被`RememberMeAuthenticationFilter`调用。因此,该接口为底层的 Rememe-Me 实现提供了与身份验证相关的事件的充分通知,并在候选 Web 请求可能包含 Cookie 并希望被记住时将其委托给实现。这种设计允许任意数量的 rememe-me 实现策略。我们在上文中已经看到, Spring Security 提供了两种实现方式。我们将依次讨论这些问题。 + +### TokenBaseDreMemberMeservices + +该实现支持[一种简单的基于散列的令牌方法](#remember-me-hash-token)中描述的更简单的方法。`TokenBasedRememberMeServices`生成一个`RememberMeAuthenticationToken`,它由`RememberMeAuthenticationProvider`处理。在此身份验证提供程序和`TokenBasedRememberMeServices`之间共享`key`。此外,`TokenBasedRememberMeServices`需要一个 UserDetailsService,它可以从中检索用户名和密码以进行签名比较,并生成`RememberMeAuthenticationToken`以包含正确的`GrantedAuthority`s。应用程序应该提供某种类型的注销命令,如果用户请求,该命令会使 cookie 无效。`TokenBasedRememberMeServices`还实现了 Spring Security 的`LogoutHandler`接口,因此可以与`LogoutFilter`一起使用,以自动清除 cookie。 + +在应用程序上下文中启用 Remember-Me 服务所需的 bean 如下: + +``` + + + + + + + + + + + + + +``` + +不要忘记将`RememberMeServices`实现添加到`UsernamePasswordAuthenticationFilter.setRememberMeServices()`属性中,在`AuthenticationManager.setProviders()`列表中包含`RememberMeAuthenticationProvider`,并将`RememberMeAuthenticationFilter`添加到`FilterChainProxy`(通常在`UsernamePasswordAuthenticationFilter`之后)。 + +### 坚持以 DREMEMBERMESERVICE 为基础 + +这个类可以以与`TokenBasedRememberMeServices`相同的方式使用,但是它还需要配置一个`PersistentTokenRepository`来存储令牌。有两种标准的实现方式。 + +* `InMemoryTokenRepositoryImpl`仅用于测试。 + +* `JdbcTokenRepositoryImpl`,它将令牌存储在数据库中。 + +上面在[持久令牌方法](#remember-me-persistent-token)中描述了数据库模式。 + +--- + +[1](#_footnoteref_1)。本质上,用户名不包含在 cookie 中,以防止暴露有效的登录名。在本文的评论部分对此进行了讨论。 + +[Session Management](session-management.html)[OpenID](openid.html) + diff --git a/docs/spring-security/servlet-authentication-runas.md b/docs/spring-security/servlet-authentication-runas.md new file mode 100644 index 0000000000000000000000000000000000000000..312a143ce0bd7bd5199fef7c6c037893944cc131 --- /dev/null +++ b/docs/spring-security/servlet-authentication-runas.md @@ -0,0 +1,45 @@ +# run-as 身份验证替换 + +## 概述 + +在安全对象回调阶段,`AbstractSecurityInterceptor`可以在`SecurityContext`和`SecurityContextHolder`中临时替换`Authentication`对象。只有当原始`Authentication`对象被`AuthenticationManager`和`AccessDecisionManager`成功处理时,才会发生这种情况。`RunAsManager`将指示在`SecurityInterceptorCallback`期间应该使用的替换`Authentication`对象(如果有的话)。 + +通过在安全对象回调阶段临时替换`Authentication`对象,安全调用将能够调用需要不同身份验证和授权凭据的其他对象。它还能够对特定的`GrantedAuthority`对象执行任何内部安全检查。因为 Spring Security 提供了许多帮助器类,这些帮助器类根据`SecurityContextHolder`的内容自动配置远程协议,所以在调用远程 Web 服务时,这些 run-as 替换特别有用。 + +## 配置 + +一个`RunAsManager`接口由 Spring Security 提供: + +``` +Authentication buildRunAs(Authentication authentication, Object object, + List config); + +boolean supports(ConfigAttribute attribute); + +boolean supports(Class clazz); +``` + +第一个方法返回`Authentication`对象,该对象应该在方法调用的持续时间内替换现有的`Authentication`对象。如果该方法返回`null`,则表示不应进行替换。第二种方法由`AbstractSecurityInterceptor`使用,作为其配置属性的启动验证的一部分。安全拦截器实现调用`supports(Class)`方法,以确保配置的`RunAsManager`支持安全拦截器将呈现的安全对象类型。 + +一个`RunAsManager`的具体实现提供了 Spring 安全性。`RunAsManagerImpl`类返回一个替换`RunAsUserToken`,如果有`ConfigAttribute`以`RUN_AS_`开头。如果找到任何这样的`ConfigAttribute`,则替换的`RunAsUserToken`将包含与原始`Authentication`对象相同的主体、凭据和授予的权限,以及每个`RUN_AS_``ConfigAttribute`的新`SimpleGrantedAuthority`。每个新的`SimpleGrantedAuthority`都将被前缀为`ROLE_`,然后是`RUN_AS``ConfigAttribute`。例如,一个`RUN_AS_SERVER`将导致替换`RunAsUserToken`中包含一个`ROLE_RUN_AS_SERVER`授予的权限。 + +替换`RunAsUserToken`就像任何其他`Authentication`对象一样。它需要通过`AuthenticationManager`进行身份验证,很可能是通过将其委托给合适的`AuthenticationProvider`。`RunAsImplAuthenticationProvider`执行这种身份验证。它只是接受任何`RunAsUserToken`呈现为有效。 + +为了确保恶意代码不会创建`RunAsUserToken`,并将其呈现给`RunAsImplAuthenticationProvider`,密钥的散列存储在所有生成的令牌中。在 Bean 上下文中使用相同的键创建`RunAsManagerImpl`和`RunAsImplAuthenticationProvider`: + +``` + + + + + + + +``` + +通过使用相同的键,每个`RunAsUserToken`都可以验证它是由批准的`RunAsManagerImpl`创建的。出于安全原因,`RunAsUserToken`在创建后是不可变的 + +[X509](x509.html)[Logout](logout.html) + diff --git a/docs/spring-security/servlet-authentication-session-management.md b/docs/spring-security/servlet-authentication-session-management.md new file mode 100644 index 0000000000000000000000000000000000000000..3bc380a80221b9951185dbb0a0f969a46ae58ce9 --- /dev/null +++ b/docs/spring-security/servlet-authentication-session-management.md @@ -0,0 +1,256 @@ +# 会话管理 + +## 检测超时 + +你可以配置 Spring 安全性以检测无效会话ID 的提交,并将用户重定向到适当的 URL。这是通过`session-management`元素实现的: + +爪哇 + +``` +@Override +protected void configure(HttpSecurity http) throws Exception{ + http + .sessionManagement(session -> session + .invalidSessionUrl("/invalidSession.htm") + ); +} +``` + +XML + +``` + +... + + +``` + +请注意,如果你使用此机制来检测会话超时,那么如果用户注销并在不关闭浏览器的情况下重新登录,则可能会错误地报告错误。这是因为当你使会话无效时,会话cookie 将不会被清除,并且即使用户已经注销,它也将被重新提交。你可以在注销时显式地删除 JSessionID cookie,例如,在注销处理程序中使用以下语法: + +爪哇 + +``` +@Override +protected void configure(HttpSecurity http) throws Exception{ + http + .logout(logout -> logout + .deleteCookies("JSESSIONID") + ); +} +``` + +XML + +``` + + + +``` + +不幸的是,这不能保证在每个 Servlet 容器中都能正常工作,因此你需要在你的环境中对其进行测试。 + +| |如果你在代理背后运行你的应用程序,你还可以通过配置代理服务器来删除会话cookie。,
,例如,使用 Apache HTTPD 的 mod\_headers,下面的指令将删除`JSESSIONID`cookie,方法是在对注销请求的响应中使其过期(假设应用程序部署在路径`/tutorial`下):

```

Header always set Set-Cookie "JSESSIONID=;Path=/tutorial;Expires=Thu, 01 Jan 1970 00:00:00 GMT"

```| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +## 并行控制会话 + +如果你希望对单个用户登录到你的应用程序的能力进行限制, Spring Security 通过以下简单的添加来支持这一点。首先,需要将以下监听器添加到配置中,以使 Spring 安全性更新到有关会话生命周期事件的信息: + +爪哇 + +``` +@Bean +public HttpSessionEventPublisher httpSessionEventPublisher() { + return new HttpSessionEventPublisher(); +} +``` + +XML + +``` + + + org.springframework.security.web.session.HttpSessionEventPublisher + + +``` + +然后将以下行添加到你的应用程序上下文中: + +爪哇 + +``` +@Override +protected void configure(HttpSecurity http) throws Exception { + http + .sessionManagement(session -> session + .maximumSessions(1) + ); +} +``` + +XML + +``` + +... + + + + +``` + +这将防止用户多次登录——第二次登录将导致第一次登录无效。通常情况下,你希望防止第二次登录,在这种情况下,你可以使用 + +爪哇 + +``` +@Override +protected void configure(HttpSecurity http) throws Exception { + http + .sessionManagement(session -> session + .maximumSessions(1) + .maxSessionsPreventsLogin(true) + ); +} +``` + +XML + +``` + + + + + +``` + +然后,第二次登录将被拒绝。通过“拒绝”,我们的意思是,如果使用基于表单的登录,用户将被发送到`authentication-failure-url`。如果第二次身份验证是通过另一种非交互式机制进行的,例如“remember-me”,则将向客户端发送一个“未授权的”(401)错误。如果要使用错误页,则可以将属性`session-authentication-error-url`添加到`session-management`元素中。 + +如果你正在为基于表单的登录使用定制的身份验证过滤器,那么你必须显式地配置并发会话控制支持。更多细节可以在[Session Management chapter](#session-mgmt)中找到。 + +## 会话固定攻击保护 + +[Session fixation](https://en.wikipedia.org/wiki/Session_fixation)攻击是一种潜在的风险,在这种情况下,恶意攻击者可能通过访问网站来创建会话,然后说服另一个用户使用相同的会话登录(例如,通过向他们发送包含会话标识符的链接作为参数)。 Spring 安全性通过在用户登录时创建新的会话或以其他方式更改会话ID 来自动防止这种情况。如果你不需要此保护,或者它与某些其他需求冲突,则可以使用``上的`session-fixation-protection`属性来控制该行为,该属性有四个选项 + +* `none`-什么都别做。原文会话将予以保留。 + +* `newSession`-创建新的“clean”会话,而不复制现有的会话数据( Spring 安全性相关的属性仍将被复制)。 + +* `migrateSession`-创建一个新的会话并将所有现有的会话属性复制到新的会话。这是 Servlet 3.0 或更早版本容器中的默认设置。 + +* `changeSessionId`-不要新建会话。相反,使用由 Servlet 容器提供的会话固定保护(`HttpServletRequest#changeSessionId()`)。此选项仅在 Servlet 3.1(Java EE7)和更新的容器中可用。在较旧的容器中指定它将导致异常。这是 Servlet 3.1 和更新的容器中的默认设置。 + +会话发生固定保护时,会导致`SessionFixationProtectionEvent`在应用程序上下文中被发布。如果使用`changeSessionId`,此保护将导致任何*也是*的通知,因此,如果你的代码监听这两个事件,请小心。有关更多信息,请参见[Session Management](#session-mgmt)章。 + +## SessionManagementFilter + +`SessionManagementFilter`检查`SecurityContextRepository`中的内容与`SecurityContextHolder`中的当前内容之间的对比,以确定在当前请求期间是否已对用户进行了身份验证,通常通过一种非交互式的身份验证机制,例如预认证或 remember-me[1]。如果存储库包含安全上下文,则筛选器将不执行任何操作。如果不是,并且线程本地`SecurityContext`包含一个(非匿名的)`Authentication`对象,则筛选器假定它们已经由堆栈中的前一个筛选器进行了身份验证。然后它将调用配置的`SessionAuthenticationStrategy`。 + +如果用户当前未经过身份验证,则过滤器将检查是否请求了无效的会话ID(例如,由于超时),并将调用已配置的`InvalidSessionStrategy`(如果设置了)。最常见的行为是重定向到固定的 URL,这被封装在标准实现`SimpleRedirectInvalidSessionStrategy`中。后者也用于通过名称空间[如前所述](#session-mgmt)配置无效的会话URL。 + +## SessionAuthenticationStrategy + +`SessionAuthenticationStrategy`被`SessionManagementFilter`和`AbstractAuthenticationProcessingFilter`都使用,因此,例如,如果你正在使用定制的 Form-Login 类,则需要将其注入到这两个类中。在这种情况下,结合名称空间和自定义 bean 的典型配置可能如下所示: + +``` + + + + + + + + ... + + + +``` + +请注意,如果在实现`HttpSessionBindingListener`的会话中存储 bean(包括 Spring 会话-作用域 bean),则使用默认的`SessionFixationProtectionStrategy`可能会导致问题。有关此类的更多信息,请参见 Javadoc。 + +## 并发控制 + +Spring 安全性能够防止主体对同一应用程序的并行身份验证超过指定的次数。许多 ISV 利用这一点来执行许可,而网络管理员喜欢这个功能,因为它有助于防止人们共享登录名。例如,你可以阻止用户“Batman”从两个不同的会话登录到 Web 应用程序。你可以终止他们的上一次登录,或者在他们再次尝试登录时报告错误,从而阻止第二次登录。请注意,如果你正在使用第二种方法,则尚未显式退出的用户(例如,刚刚关闭其浏览器的用户)将无法再次登录,直到其原始会话到期为止。 + +名称空间支持并发控制,因此请检查前面的名称空间章节以获得最简单的配置。不过,有时你需要定制一些东西。 + +该实现使用`SessionAuthenticationStrategy`的专门版本,称为`ConcurrentSessionControlAuthenticationStrategy`。 + +| |以前,并发身份验证检查是由`ProviderManager`进行的,它可以被注入`ConcurrentSessionController`,
后者将检查用户是否试图超过允许的会话数量,
但是,这种方法要求预先创建一个 HTTP会话,
在 Spring Security3 中,首先由`AuthenticationManager`对用户进行身份验证,并且一旦他们被成功地进行了身份验证,则创建一个会话并进行检查是否允许他们打开另一个会话。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +要使用并发会话支持,你需要在`web.xml`中添加以下内容: + +``` + + + org.springframework.security.web.session.HttpSessionEventPublisher + + +``` + +此外,还需要将`ConcurrentSessionFilter`添加到`FilterChainProxy`中。`ConcurrentSessionFilter`需要两个构造函数参数,`sessionRegistry`,它通常指向`SessionRegistryImpl`的实例,以及`sessionInformationExpiredStrategy`,它定义了在会话过期时应用的策略。使用名称空间创建`FilterChainProxy`和其他默认 bean 的配置可能如下所示: + +``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +将侦听器添加到`web.xml`会导致每当`HttpSession`开始或结束时,将`ApplicationEvent`发布到 Spring `ApplicationContext`。这是关键的,因为它允许在会话结束时通知`SessionRegistryImpl`。如果没有它,一旦用户超出了他们的会话许可,他们将永远无法再次登录,即使他们退出了另一个会话或它超时。 + +### 为当前经过身份验证的用户及其会话查询 SessionRegistry + +通过名称空间或使用普通 bean 设置并发控制,其有益的副作用是为你提供了对`SessionRegistry`的引用,你可以在应用程序中直接使用该引用,因此,即使你不想限制用户可能拥有的会话数量,建立基础设施也是值得的。你可以将`maximumSession`属性设置为-1,以允许无限会话。如果使用名称空间,可以使用`session-registry-alias`属性为内部创建的`SessionRegistry`设置别名,提供一个引用,你可以将其注入到自己的 bean 中。 + +`getAllPrincipals()`方法为你提供了当前经过身份验证的用户的列表。你可以通过调用`getAllSessions(Object principal, boolean includeExpiredSessions)`方法来列出用户的会话,该方法返回`SessionInformation`对象的列表。你还可以通过在`SessionInformation`实例上调用`expireNow()`来使用户的会话过期。当用户返回应用程序时,他们将被阻止继续。例如,你可能会发现这些方法在管理应用程序中很有用。请查看 Javadoc 以获得更多信息。 + +--- + +[1](#_footnoteref_1)。通过在验证之后执行重定向的机制(例如 Form-Login)进行的验证不会被`SessionManagementFilter`检测到,因为在验证请求期间不会调用过滤器。会话-在这些情况下,管理功能必须单独处理。 + +[LDAP](passwords/ldap.html)[记住我](rememberme.html) + diff --git a/docs/spring-security/servlet-authentication-x509.md b/docs/spring-security/servlet-authentication-x509.md new file mode 100644 index 0000000000000000000000000000000000000000..c013734966bc1e5e41b4aba296fc8f53d9f94f5e --- /dev/null +++ b/docs/spring-security/servlet-authentication-x509.md @@ -0,0 +1,49 @@ +# X.509 认证 + +## 概述 + +X.509 证书身份验证最常见的用途是在使用 SSL 时验证服务器的身份,最常见的是在使用浏览器中的 HTTPS 时。浏览器将自动检查服务器提供的证书是否已由其维护的可信证书颁发机构列表中的一个颁发(即数字签名)。 + +你还可以使用带有“相互身份验证”的 SSL;然后,服务器将从客户机请求有效的证书,作为 SSL 握手的一部分。服务器将通过检查其证书是否由可接受的权威机构签名来对客户端进行身份验证。如果提供了有效的证书,则可以通过应用程序中的 Servlet API 获得该证书。 Spring 安全 X.509 模块使用一个过滤器提取证书。它将证书映射到应用程序用户,并加载该用户的一组授权权限,以便与标准 Spring 安全基础设施一起使用。 + +在尝试使用 Spring 安全性的容器之前,你应该熟悉使用证书和为 Servlet 容器设置客户端身份验证。大部分工作是创建和安装合适的证书和密钥。例如,如果你正在使用 Tomcat,那么请阅读这里的说明[https://tomcat.apache.org/tomcat-9.0-doc/ssl-howto.html](https://tomcat.apache.org/tomcat-9.0-doc/ssl-howto.html)。重要的是,在尝试使用 Spring 安全措施之前,你要让它发挥作用。 + +## 将 X.509 身份验证添加到 Web 应用程序中 + +启用 X.509 客户端身份验证非常简单。只需将``元素添加到你的 HTTP 安全名称空间配置中。 + +``` + +... + ; + +``` + +该元素有两个可选属性: + +* `subject-principal-regex`。用于从证书的主题名称中提取用户名的正则表达式。默认值如上所示。这是将传递给`UserDetailsService`的用户名,用于为用户加载权限。 + +* `user-service-ref`。这是用于 x.509 的`UserDetailsService`的 Bean ID。如果在你的应用程序上下文中只定义了一个,那么它就不需要了。 + +`subject-principal-regex`应该包含一个组。例如,默认表达式“cn=(.\*?),”与 Common Name 字段匹配。因此,如果证书中的主题名称是“CN=Jimi Hendrix,ou=…”,则将给出一个用户名为“Jimi Hendrix”。这些匹配不区分大小写。所以“emailaddress=(.\*?),”将匹配“emailaddress=[[[email protected]](/cdn-cgi/l/email-protection#244e4d494d644c414a40564d5c0a4b5643),cn=…”给用户名“[[email protected]](/cdn-cgi/l/email-protection#1a707377735a727f747e6873623475687d)”。如果客户机提供了一个证书,并且成功提取了一个有效的用户名,那么在安全上下文中应该有一个有效的`Authentication`对象。如果找不到证书,或者找不到相应的用户,那么安全上下文将保持为空。这意味着你可以轻松地使用 X.509 身份验证和其他选项,例如基于表单的登录。 + +## 在 Tomcat 中设置 SSL + +在[Spring Security Samples repository](https://github.com/spring-projects/spring-security-samples/tree/5.6.x/servlet/java-configuration/authentication/x509/server)中有一些预先生成的证书。如果你不想生成自己的 SSL,你可以使用这些工具来启用 SSL 进行测试。文件`server.jks`包含服务器证书、私钥和颁发证书的机构证书。示例应用程序中还为用户提供了一些客户机证书文件。你可以在浏览器中安装这些功能,以启用 SSL 客户机身份验证。 + +要在 SSL 支持下运行 Tomcat,将`server.jks`文件放入 Tomcat `conf`目录,并将以下连接器添加到`server.xml`文件中 + +``` + +``` + +`clientAuth`也可以设置为`want`,如果你仍然希望 SSL 连接成功,即使客户机不提供证书。除非使用非 X.509 身份验证机制(如表单身份验证),否则不提供证书的客户端将无法访问受 Spring 安全性保护的任何对象。 + +[CAS](cas.html)[Run-As](runas.html) + diff --git a/docs/spring-security/servlet-authentication.md b/docs/spring-security/servlet-authentication.md new file mode 100644 index 0000000000000000000000000000000000000000..33c9d313067a9238e0ab0eb73ff4311737f557e7 --- /dev/null +++ b/docs/spring-security/servlet-authentication.md @@ -0,0 +1,28 @@ +# 认证 + +Spring 安全性为[认证](../../features/authentication/index.html#authentication)提供了全面的支持。我们首先讨论整体[Servlet Authentication Architecture](../architecture.html#servlet-architecture)。正如你可能预期的那样,这一部分更抽象地描述了体系结构,而没有过多地讨论它如何应用于具体的流。 + +如果你愿意,你可以参考[认证机制](#servlet-authentication-mechanisms)来了解用户可以验证的具体方式。这些部分关注于你可能希望验证的特定方式,并指向体系结构部分来描述特定的流是如何工作的。 + +## 认证机制 + +* [用户名和密码](passwords/index.html#servlet-authentication-unpwd)-如何使用用户名/密码进行身份验证 + +* [OAuth2.0 登录](../oauth2/login/index.html#oauth2login)-使用 OpenID Connect 和非标准的 OAuth2.0 登录(即 GitHub) + +* [SAML2.0 登录](../saml2/index.html#servlet-saml2)-saml2.0 登录 + +* [中央认证服务器](cas.html#servlet-cas)-中央身份验证服务器支持 + +* [记住我](rememberme.html#servlet-rememberme)-如何记住过期的用户 + +* [JAAS 身份验证](jaas.html#servlet-jaas)-使用 JAAS 进行身份验证 + +* [OpenID](openid.html#servlet-openid)-OpenID 身份验证(不要与 OpenID Connect 混淆) + +* [预认证场景](preauth.html#servlet-preauth)-使用外部机制(如[SiteMinder](https://www.siteminder.com/)或 Java EE Security)进行身份验证,但仍使用 Spring Security 进行授权和防止常见的攻击。 + +* [X509 认证](x509.html#servlet-x509)-x509 身份验证 + +[建筑](../architecture.html)[认证体系结构](architecture.html) + diff --git a/docs/spring-security/servlet-authorization-.md b/docs/spring-security/servlet-authorization-.md new file mode 100644 index 0000000000000000000000000000000000000000..295c4f5b5cd0d2b94f7e668ab67d871f26357d45 --- /dev/null +++ b/docs/spring-security/servlet-authorization-.md @@ -0,0 +1,18 @@ +# 授权 + +Spring Security 中的高级授权功能是其受欢迎的最有说服力的原因之一。无论你选择如何进行身份验证--无论是使用 Spring 安全提供的机制和提供者,还是与容器或其他非 Spring 安全身份验证权限集成--你都会发现授权服务可以以一致且简单的方式在你的应用程序中使用。 + +在这一部分中,我们将探讨不同的`AbstractSecurityInterceptor`实现,这些实现在第一部分中介绍。然后,我们将继续研究如何通过使用域访问控制列表来微调授权。 + +## 章节摘要 + +* [授权体系结构](architecture.html) +* [授权 HTTP 请求](authorize-http-requests.html) +* [使用 FilterSecurityInterceptor 授权 HTTP 请求](authorize-requests.html) +* [基于表达式的访问控制](expression-based.html) +* [安全对象实现](secure-objects.html) +* [方法安全性](method-security.html) +* [域对象安全 ACLS](acls.html) + +[认证事件](../authentication/events.html)[授权体系结构](architecture.html) + diff --git a/docs/spring-security/servlet-authorization-acls.md b/docs/spring-security/servlet-authorization-acls.md new file mode 100644 index 0000000000000000000000000000000000000000..b96a344129706457f305c6818f01149bd56f70b6 --- /dev/null +++ b/docs/spring-security/servlet-authorization-acls.md @@ -0,0 +1,123 @@ +# ACLS + +## 概述 + +复杂的应用程序通常会发现需要定义访问权限,而不仅仅是在 Web 请求或方法调用级别。相反,安全决策需要包括 who(`Authentication`)、where(`MethodInvocation`)和 what(`SomeDomainObject`)。换句话说,授权决策还需要考虑方法调用的实际领域对象实例主题。 + +想象一下,你正在为一家宠物诊所设计一个应用程序。基于 Spring 的应用程序主要有两类用户:宠物诊所的工作人员,以及宠物诊所的客户。员工可以访问所有的数据,而你的客户只能看到他们自己的客户记录。为了让它更有趣,你的客户可以允许其他用户查看他们的客户记录,例如他们的“Puppy Prementary”导师或当地“Pony Club”的总裁。使用 Spring 安全性作为基础,你可以使用几种方法: + +* 编写你的业务方法以加强安全性。你可以查阅`Customer`域对象实例中的集合,以确定哪些用户具有访问权限。通过使用`SecurityContextHolder.getContext().getAuthentication()`,你将能够访问`Authentication`对象。 + +* 编写`AccessDecisionVoter`以从存储在`Authentication`对象中的`GrantedAuthority[]`s 强制执行安全性。这将意味着你的`AuthenticationManager`将需要用自定义的`GrantedAuthority[]`填充`Authentication`,表示主体可以访问的每个`Customer`域对象实例。 + +* 编写`AccessDecisionVoter`以强制执行安全性,并直接打开目标`Customer`域对象。这意味着你的投票者需要访问允许它检索`Customer`对象的 DAO。然后,它将访问`Customer`对象的已批准用户集合,并做出适当的决定。 + +这些方法中的每一种都是完全合法的。然而,第一种方法将你的授权检查与你的业务代码相结合。这样做的主要问题包括单元测试的难度增加,以及在其他地方重用`Customer`授权逻辑将更加困难。从`Authentication`对象获得`GrantedAuthority[]`s 也很好,但不会扩展到大量`Customer`s。如果用户可能能够访问 5,000 个`Customer`s(在这种情况下不太可能,但是想象一下,如果它是一个大型小马俱乐部的流行 VET!),则构建`Authentication`对象所消耗的内存量和所需的时间将是不希望的。最后一种方法是直接从外部代码打开`Customer`,这可能是三种方法中最好的。它实现了关注点的分离,并且不会滥用内存或 CPU 周期,但它仍然效率低下,因为`AccessDecisionVoter`和最终的业务方法本身都将执行对负责检索`Customer`对象的 DAO 的调用。每个方法调用两次访问显然是不希望的。此外,对于列出的每种方法,你都需要从头编写自己的访问控制列表持久性和业务逻辑。 + +幸运的是,还有另一种选择,我们将在下面讨论。 + +## 关键概念 + +Spring Security 的 ACL 服务以`spring-security-acl-xxx.jar`发送。你需要将此 JAR 添加到 Classpath 中,以使用 Spring Security 的域对象实例安全功能。 + +Spring Security 的域对象实例安全功能的核心是访问控制列表的概念。系统中的每个域对象实例都有自己的 ACL,ACL 记录了谁可以和不可以使用该域对象的详细信息。考虑到这一点, Spring Security 为你的应用程序提供了三种与 ACL 相关的主要功能: + +* 一种有效地检索所有域对象的 ACL 条目的方法(并修改这些 ACLS) + +* 在调用方法之前,一种确保给定的主体可以与对象一起工作的方法 + +* 在调用方法之后,一种确保给定的主体允许与你的对象(或它们返回的东西)一起工作的方法 + +如第一个要点所示, Spring 安全 ACL 模块的主要功能之一是提供一种检索 ACLS 的高性能方法。这种 ACL 存储库功能非常重要,因为系统中的每个域对象实例可能有几个访问控制条目,并且每个 ACL 可能以树状结构从其他 ACLS 继承( Spring Security 支持开箱即用,并且非常常用)。 Spring Security 的 ACL 功能经过精心设计,以提供对 ACLS 的高性能检索,以及可插拔的缓存、最小化死锁的数据库更新、独立于 ORM 框架(我们直接使用 JDBC)、适当的封装和透明的数据库更新。 + +给定数据库是 ACL 模块操作的核心,让我们研究一下实现中默认使用的四个主表。下面按照典型的 Spring 安全性 ACL 部署中的大小顺序列出了这些表,最后列出了行最多的表: + +* ACL\_SID 允许我们唯一地标识系统中的任何主体或权限(“SID”代表“安全标识”)。唯一的列是 ID、SID 的文本表示以及一个标志,用于指示文本表示是指主体名称还是`GrantedAuthority`。因此,对于每个唯一的主体有一个单行或`GrantedAuthority`。当用于接收许可的上下文时,SID 通常称为“收件人”。 + +* ACL\_CLASS 允许我们唯一地标识系统中的任意域对象类。唯一的列是 ID 和 爪哇 类名。因此,对于我们希望为其存储 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`s,并且知道`Acl`的所有者。ACL 不直接指向域对象,而是指`ObjectIdentity`。`Acl`存储在 ACL\_Object\_Identity 表中。 + +* `AccessControlEntry`:一个`Acl`包含多个`AccessControlEntry`s,这些在框架中通常被缩写为 ACES。每个 ACE 指的是`Permission`,`Sid`和`Acl`的特定元组。ACE 还可以是授予或不授予,并包含审计设置。ACE 存储在 ACL\_entry 表中。 + +* `Permission`:权限表示特定的不可变位掩码,并为位掩码和输出信息提供方便的功能。上面提供的基本权限(从 0 位到 4 位)包含在`BasePermission`类中。 + +* `Sid`:ACL 模块需要引用主体和`GrantedAuthority[]`s。间接级别由`Sid`接口提供,它是“安全标识”的缩写。常见的类包括`PrincipalSid`(表示`Authentication`对象内的主体)和`GrantedAuthoritySid`。安全标识信息存储在 ACL\_SID 表中。 + +* `ObjectIdentity`:每个域对象在 ACL 模块内部由`ObjectIdentity`表示。默认的实现称为`ObjectIdentityImpl`。 + +* `AclService`:检索适用于给定`ObjectIdentity`的`Acl`。在包含的实现中(`JdbcAclService`),检索操作被委托给`LookupStrategy`。`LookupStrategy`为检索 ACL 信息提供了一种高度优化的策略,它使用批检索(`BasicLookupStrategy`)和支持定制实现,这些实现利用物化视图、分层查询和类似的以性能为中心的非 ANSI SQL 功能。 + +* `MutableAclService`:允许为持久性而呈现修改后的`Acl`。如果你不想使用此接口,那么使用此接口并不是必需的。 + +请注意,我们的开箱即用 ACLService 和相关数据库类都使用 ANSI SQL。因此,这应该适用于所有主要的数据库。在撰写本文时,该系统已成功地使用高超音速 SQL、PostgreSQL、Microsoft SQL Server 和 Oracle 进行了测试。 + +两个示例附带 Spring 安全性,演示了 ACL 模块。第一个是[触点样品](https://github.com/spring-projects/spring-security-samples/tree/5.6.x/servlet/xml/java/contacts),另一个是[文档管理系统示例](https://github.com/spring-projects/spring-security-samples/tree/5.6.x/servlet/xml/java/dms)。我们建议看看这些例子。 + +## 开始 + +要开始使用 Spring Security 的 ACL 功能,你需要将 ACL 信息存储在某个地方。这需要使用 Spring 实例化`DataSource`。然后将`DataSource`注入到`JdbcMutableAclService`和`BasicLookupStrategy`实例中。后者提供高性能的 ACL 检索功能,前者提供 Mutator 功能。对于示例配置,请参考带有 Spring 安全性的示例之一。你还需要使用上一节中列出的四个特定于 ACL 的表填充数据库(对于相应的 SQL 语句,请参考 ACL 示例)。 + +一旦你创建了所需的模式并实例化了`JdbcMutableAclService`,接下来就需要确保你的域模型支持与 Spring Security ACL 包的互操作性。希望`ObjectIdentityImpl`将被证明是足够的,因为它提供了大量可以使用它的方法。大多数人都会拥有包含`public Serializable getId()`方法的域对象。如果返回类型是 long,或者与 long 兼容(例如,INT),你将发现不需要进一步考虑`ObjectIdentity`问题。ACL 模块的许多部分依赖于长标识符。如果你不使用 long(或 INT、字节等),那么你很有可能需要重新实现一些类。我们不打算在 Spring Security 的 ACL 模块中支持非长标识符,因为 Longs 已经与所有数据库序列(最常见的标识符数据类型)兼容,并且具有足够的长度来适应所有常见的使用场景。 + +下面的代码片段展示了如何创建`Acl`,或修改现有的`Acl`: + +Java + +``` +// 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); +``` + +Kotlin + +``` +val oi: ObjectIdentity = ObjectIdentityImpl(Foo::class.java, 44) +val sid: Sid = PrincipalSid("Samantha") +val p: Permission = BasePermission.ADMINISTRATION + +// Create or update the relevant ACL +var acl: MutableAcl? = null +acl = try { +aclService.readAclById(oi) as MutableAcl +} catch (nfe: NotFoundException) { +aclService.createAcl(oi) +} + +// Now grant some permissions via an access control entry (ACE) +acl!!.insertAce(acl.entries.size, p, sid, true) +aclService.updateAcl(acl) +``` + +在上面的示例中,我们检索与标识符为 44 的“foo”域对象相关联的 ACL。然后我们添加一个 ACE,这样一个名为“Samantha”的主体就可以“管理”这个对象。除了 insertace 方法之外,代码片段相对来说是不言自明的。INSERTACE 方法的第一个参数是确定将在 ACL 中的哪个位置插入新条目。在上面的例子中,我们只是将新的 ACE 放在现有 ACES 的末尾。最后一个论点是一个布尔值,它表明 ACE 是授予还是否认。在大多数情况下,它将授予(真),但如果它拒绝(假),则权限将被有效地阻止。 + +Spring 安全性不提供任何特殊的集成以自动创建、更新或删除 ACLS 作为 DAO 或存储库操作的一部分。相反,你将需要为你的各个域对象编写上面所示的代码。值得考虑在服务层使用 AOP 来自动将 ACL 信息与服务层操作集成在一起。在过去,我们发现这是一种非常有效的方法。 + +一旦使用了上述技术在数据库中存储了一些 ACL 信息,下一步就是实际使用 ACL 信息作为授权决策逻辑的一部分。你在这里有很多选择。你可以编写自己的`AccessDecisionVoter`或`AfterInvocationProvider`,它们分别在方法调用之前或之后触发。这样的类将使用`AclService`来检索相关的 ACL,然后调用`Acl.isGranted(Permission[] permission, Sid[] sids, boolean administrativeMode)`来决定是否授予权限。或者,你可以使用我们的`AclEntryVoter`、`AclEntryAfterInvocationProvider`或`AclEntryAfterInvocationCollectionFilteringProvider`类。所有这些类都提供了一种基于声明的方法来在运行时评估 ACL 信息,从而使你无需编写任何代码。请参考示例应用程序来了解如何使用这些类。 + +[方法安全性](method-security.html)[OAuth2](../oauth2/index.html) + diff --git a/docs/spring-security/servlet-authorization-architecture.md b/docs/spring-security/servlet-authorization-architecture.md new file mode 100644 index 0000000000000000000000000000000000000000..cf29d4df462b3bed9c040e8c3eda562ff1ff4b23 --- /dev/null +++ b/docs/spring-security/servlet-authorization-architecture.md @@ -0,0 +1,257 @@ +# 授权体系结构 + +## 当局 + +[`Authentication`](../authentication/architecture.html# Servlet-authentication-authentication),讨论了所有`Authentication`实现如何存储`GrantedAuthority`对象的列表。这些代表已授予校长的权力。`GrantedAuthority`对象由`AuthenticationManager`插入到`Authentication`对象中,然后在做出授权决策时由`AuthorizationManager`读取。 + +`GrantedAuthority`是一个只有一个方法的接口: + +``` +String getAuthority(); +``` + +这种方法允许`AuthorizationManager`s 得到精确的`String`表示的`GrantedAuthority`。通过以`String`的形式返回表示,大多数`AuthorizationManager`s 和`AccessDecisionManager`s 都可以轻松地“读取”`GrantedAuthority`s。如果`GrantedAuthority`不能精确地表示为`String`,则`GrantedAuthority`被认为是“复杂的”,而`getAuthority()`必须返回`null`。 + +“complex”`GrantedAuthority`的一个示例是一个实现,该实现存储了适用于不同客户帐号的操作和权限阈值的列表。将这个复杂的`GrantedAuthority`表示为`String`将非常困难,因此`getAuthority()`方法应该返回`null`。这将向任何`AuthorizationManager`表示,为了理解其内容,它将需要特别支持`GrantedAuthority`实现。 + +Spring 安全性包括一个具体的`GrantedAuthority`实现,`SimpleGrantedAuthority`。这允许将任何用户指定的`String`转换为`GrantedAuthority`。安全体系结构中包含的所有`AuthenticationProvider`都使用`SimpleGrantedAuthority`来填充`Authentication`对象。 + +## 调用前处理 + +Spring 安全性提供了拦截器,该拦截器控制对安全对象的访问,例如方法调用或 Web 请求。关于是否允许调用继续进行的调用前决定由`AccessDecisionManager`做出。 + +### 授权经理 + +`AuthorizationManager`同时取代[`AccessDecisionManager`和`AccessDecisionVoter`](#authz-legacy-note)。 + +鼓励定制`AccessDecisionManager`或`AccessDecisionVoter`的应用程序[更改为使用`AuthorizationManager`](#authz-voter-adaption)。 + +`AuthorizationManager`s 由[`AuthorizationFilter`]调用,并负责做出最终的访问控制决策。`AuthorizationManager`接口包含两种方法: + +``` +AuthorizationDecision check(Supplier authentication, Object secureObject); + +default AuthorizationDecision verify(Supplier authentication, Object secureObject) + throws AccessDeniedException { + // ... +} +``` + +将传递`AuthorizationManager`的`check`方法所需的所有相关信息,以便做出授权决定。特别地,传递 Secure`Object`使实际安全对象调用中包含的那些参数能够被检查。例如,假设安全对象是`MethodInvocation`。对于任何`Customer`参数,都可以很容易地查询`MethodInvocation`,然后在`AuthorizationManager`中实现某种安全逻辑,以确保主体被允许对该客户进行操作。如果授予访问,则预期实现将返回正的,如果拒绝访问,则返回负的,并且在不做出决定时返回空的。 + +`verify`调用`check`,然后在负数`AuthorizationDecision`的情况下抛出`AccessDeniedException`。 + +### 基于委托的授权管理器实现 + +虽然用户可以实现他们自己的`AuthorizationManager`来控制授权的所有方面, Spring 安全性提供了一个委托`AuthorizationManager`,它可以与单个`AuthorizationManager`s 协作。 + +`RequestMatcherDelegatingAuthorizationManager`将请求与最合适的委托匹配`AuthorizationManager`。对于方法安全性,可以使用`AuthorizationManagerBeforeMethodInterceptor`和`AuthorizationManagerAfterMethodInterceptor`。 + +[授权管理器实现](#authz-authorization-manager-implementations)说明了相关的类。 + +![授权层次结构](../../_images/servlet/authorization/authorizationhierarchy.png) + +图 1。授权管理器实现 + +使用这种方法,可以在授权决策上对`AuthorizationManager`实现的组合进行轮询。 + +#### AuthorityAuthorizationManager + +Spring 安全性提供的最常见的`AuthorizationManager`是`AuthorityAuthorizationManager`。它配置了一组给定的权限,以便在当前`Authentication`上查找。如果`Authentication`包含任何配置的权限,它将返回正的`AuthorizationDecision`。否则它将返回负值`AuthorizationDecision`。 + +#### AuthenticatedauthorizationManager + +另一个管理器是`AuthenticatedAuthorizationManager`。它可以用来区分匿名的,完全认证的和记住我认证的用户。许多网站在 Remember-Me 身份验证下允许某些有限的访问,但需要用户通过登录来确认其身份,以获得完全访问权限。 + +#### 自定义授权管理器 + +显然,你还可以实现一个自定义`AuthorizationManager`,并且你可以在其中放入你想要的任何访问控制逻辑。它可能是特定于你的应用程序的(与业务逻辑相关的),或者它可能实现一些安全管理逻辑。例如,你可以创建一个实现,该实现可以查询 Open Policy Agent 或你自己的授权数据库。 + +| |你将在 Spring 网站上找到[博客文章](https://spring.io/blog/2009/01/03/spring-security-customization-part-2-adjusting-secured-session-in-real-time),其中描述了如何使用遗留的`AccessDecisionVoter`来实时拒绝帐户已被暂停的用户的访问。
相反,你可以通过实现`AuthorizationManager`来实现相同的结果。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +## 适应 accessisodecisionmanager 和 accessidecisionvoters + +在`AuthorizationManager`之前, Spring 发布了安全性[`AccessDecisionManager`和`AccessDecisionVoter`](#authz-legacy-note)。 + +在某些情况下,例如迁移较旧的应用程序,可能需要引入一个调用`AuthorizationManager`或`AccessDecisionVoter`的`AuthorizationManager`。 + +要调用现有的`AccessDecisionManager`,可以执行以下操作: + +例 1。适应 accessisdecisionManager + +爪哇 + +``` +@Component +public class AccessDecisionManagerAuthorizationManagerAdapter implements AuthorizationManager { + private final AccessDecisionManager accessDecisionManager; + private final SecurityMetadataSource securityMetadataSource; + + @Override + public AuthorizationDecision check(Supplier authentication, Object object) { + try { + Collection attributes = this.securityMetadataSource.getAttributes(object); + this.accessDecisionManager.decide(authentication.get(), object, attributes); + return new AuthorizationDecision(true); + } catch (AccessDeniedException ex) { + return new AuthorizationDecision(false); + } + } + + @Override + public void verify(Supplier authentication, Object object) { + Collection attributes = this.securityMetadataSource.getAttributes(object); + this.accessDecisionManager.decide(authentication.get(), object, attributes); + } +} +``` + +然后把它连接到你的`SecurityFilterChain`。 + +或者只调用`AccessDecisionVoter`,你可以这样做: + +例 2。适应辅助决策投票人 + +爪哇 + +``` +@Component +public class AccessDecisionVoterAuthorizationManagerAdapter implements AuthorizationManager { + private final AccessDecisionVoter accessDecisionVoter; + private final SecurityMetadataSource securityMetadataSource; + + @Override + public AuthorizationDecision check(Supplier authentication, Object object) { + Collection attributes = this.securityMetadataSource.getAttributes(object); + int decision = this.accessDecisionVoter.vote(authentication.get(), object, attributes); + switch (decision) { + case ACCESS_GRANTED: + return new AuthorizationDecision(true); + case ACCESS_DENIED: + return new AuthorizationDecision(false); + } + return null; + } +} +``` + +然后把它连接到你的`SecurityFilterChain`。 + +## 等级角色 + +一个常见的要求是,应用程序中的特定角色应该自动“包含”其他角色。例如,在具有“管理员”和“用户”角色概念的应用程序中,你可能希望管理员能够执行普通用户可以执行的所有操作。要实现这一点,你可以确保所有管理用户也被分配为“用户”角色。或者,你可以修改要求“用户”角色也包括“管理员”角色的每个访问约束。如果你的应用程序中有很多不同的角色,那么这可能会变得非常复杂。 + +角色层次结构的使用允许你配置哪些角色(或权限)应该包括其他角色。 Spring Security 的`投票人`,`RoleHierarchyVoter`的扩展版本配置了`RoleHierarchy`,它从该版本获得分配给用户的所有“可访问权限”。一个典型的配置可能是这样的: + +例 3。分层角色配置 + +爪哇 + +``` +@Bean +AccessDecisionVoter hierarchyVoter() { + RoleHierarchy hierarchy = new RoleHierarchyImpl(); + hierarchy.setHierarchy("ROLE_ADMIN > ROLE_STAFF\n" + + "ROLE_STAFF > ROLE_USER\n" + + "ROLE_USER > ROLE_GUEST"); + return new RoleHierarcyVoter(hierarchy); +} +``` + +XML + +``` + + + + + + + ROLE_ADMIN > ROLE_STAFF + ROLE_STAFF > ROLE_USER + ROLE_USER > ROLE_GUEST + + + +``` + +在这里,我们在层次结构`ROLE_ADMIN ⇒ ROLE_STAFF ⇒ ROLE_USER ⇒ ROLE_GUEST`中有四个角色。使用`ROLE_ADMIN`进行身份验证的用户,在针对适用于调用上述`RoleHierarchyVoter`的`AuthorizationManager`的安全约束进行评估时,将表现为他们拥有所有四个角色。`>`符号可以被认为是“包括”的意思。 + +角色层次结构为简化应用程序的访问控制配置数据和/或减少需要分配给用户的权限数量提供了一种方便的方法。对于更复杂的需求,你可能希望在应用程序所需的特定访问权限和分配给用户的角色之间定义一个逻辑映射,在加载用户信息时在这两者之间转换。 + +## 遗留授权组件 + +| |Spring 安全性包含一些遗留组件。
由于它们尚未被删除,因此出于历史目的而包含文档。
它们的建议替换在上面。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### AccessDecisionManager + +`AccessDecisionManager`由`AbstractSecurityInterceptor`调用,并负责做出最终的访问控制决策。`AccessDecisionManager`接口包含三种方法: + +``` +void decide(Authentication authentication, Object secureObject, + Collection attrs) throws AccessDeniedException; + +boolean supports(ConfigAttribute attribute); + +boolean supports(Class clazz); +``` + +将传递`AccessDecisionManager`的`decide`方法所需的所有相关信息,以便做出授权决定。特别地,传递 Secure使那些包含在实际安全对象调用中的参数能够被检查。例如,假设安全对象是`MethodInvocation`。对于任何`Customer`参数,都可以很容易地查询`MethodInvocation`,然后在`AccessDecisionManager`中实现某种安全逻辑,以确保主体被允许对该客户进行操作。如果访问被拒绝,则期望实现抛出`AccessDeniedException`。 + +`supports(ConfigAttribute)`方法在启动时由`AbstractSecurityInterceptor`调用,以确定`AccessDecisionManager`是否可以处理传递的`ConfigAttribute`。安全拦截器实现调用`supports(Class)`方法,以确保配置的`AccessDecisionManager`支持安全拦截器将呈现的安全对象类型。 + +### 基于投票的 AccessDecisionManager 实现 + +虽然用户可以实现他们自己的`AccessDecisionManager`以控制授权的所有方面, Spring 安全性包括几个基于投票的`AccessDecisionManager`实现。[投票决策经理](#authz-access-voting)举例说明了相关的类。 + +![访问决定投票](../../_images/servlet/authorization/access-decision-voting.png) + +图 2。投票决策经理 + +使用这种方法,对一系列`AccessDecisionVoter`实现进行授权决策轮询。然后,`AccessDecisionManager`根据对选票的评估来决定是否抛出`AccessDeniedException`。 + +`AccessDecisionVoter`接口有三种方法: + +``` +int vote(Authentication authentication, Object object, Collection attrs); + +boolean supports(ConfigAttribute attribute); + +boolean supports(Class clazz); +``` + +具体实现返回`int`,可能的值反映在`AccessDecisionVoter`静态字段`ACCESS_ABSTAIN`、`ACCESS_DENIED`和`ACCESS_GRANTED`中。如果一个投票实现对授权决定没有意见,它将返回`ACCESS_ABSTAIN`。如果它确实有意见,它必须返回`ACCESS_DENIED`或`ACCESS_GRANTED`。 + +有三个具体的`AccessDecisionManager`s 提供了 Spring 安全,以统计选票。该`ConsensusBased`实现将基于非弃权投票的共识授予或拒绝访问。在票数相等或所有投票都弃权的情况下,提供属性以控制行为。如果收到一个或多个`ACCESS_GRANTED`投票,`AffirmativeBased`实现将授予访问权限(即,如果至少有一个授予投票,则拒绝投票将被忽略)。与`ConsensusBased`实现类似,如果所有投票者弃权,则有一个参数来控制行为。`UnanimousBased`提供者期望获得一致的`ACCESS_GRANTED`票,以授予访问权限,而忽略弃权。如果有任何`ACCESS_DENIED`投票,它将拒绝访问。与其他实现一样,如果所有投票者都弃权,则有一个控制行为的参数。 + +可以实现一个自定义`AccessDecisionManager`,该自定义可以以不同的方式对选票进行统计。例如,来自特定`AccessDecisionVoter`的投票可能会获得额外的权重,而来自特定选民的拒绝投票可能具有否决效果。 + +#### RoleVoter + +Spring 安全性提供的最常用的`AccessDecisionVoter`是简单的`RoleVoter`,它将配置属性视为简单的角色名称和投票,以在用户已被分配该角色时授予访问权限。 + +如果有`ConfigAttribute`开头的前缀`ROLE_`,则将进行投票。如果有一个`GrantedAuthority`返回一个`String`表示(通过`getAuthority()`方法)完全等于一个或多个`ConfigAttributes`,它将投票授予访问权限,该表示从前缀`ROLE_`开始。如果没有任何以`ROLE_`开头的`ConfigAttribute`完全匹配,则`RoleVoter`将投票拒绝访问。如果不`ConfigAttribute`以`ROLE_`开头,则投票人将弃权。 + +#### 已验证的投票人 + +我们隐式看到的另一个投票者是`AuthenticatedVoter`,它可以用来区分匿名的、完全验证的和通过 rememe 验证的用户。许多网站在 Remember-Me 身份验证下允许某些有限的访问,但需要用户通过登录来确认其身份,以获得完全访问权限。 + +当我们使用属性`IS_AUTHENTICATED_ANONYMOUSLY`授予匿名访问权限时,这个属性正在被`AuthenticatedVoter`处理。有关此类的更多信息,请参见 Javadoc。 + +#### 习俗投票人 + +显然,你还可以实现一个自定义`AccessDecisionVoter`,并且你可以在其中放入你想要的任何访问控制逻辑。它可能是特定于你的应用程序的(与业务逻辑相关的),或者它可能实现一些安全管理逻辑。例如,你将在 Spring 网站上找到一个[博客文章](https://spring.io/blog/2009/01/03/spring-security-customization-part-2-adjusting-secured-session-in-real-time),该网站描述了如何使用投票器实时拒绝帐户已被暂停的用户的访问。 + +![调用后](../../_images/servlet/authorization/after-invocation.png) + +图 3。调用实现之后 + +与 Spring 安全性的许多其他部分一样,`AfterInvocationManager`有一个具体的实现,`AfterInvocationProviderManager`,它轮询`AfterInvocationProvider`s 的列表。每个`AfterInvocationProvider`都被允许修改返回对象或抛出`AccessDeniedException`。实际上,多个提供者可以修改该对象,因为前一个提供者的结果被传递到列表中的下一个提供者。 + +请注意,如果你使用`AfterInvocationManager`,你仍然需要允许`MethodSecurityInterceptor`的`AccessDecisionManager`的配置属性来允许操作。如果你使用的是包括`AccessDecisionManager`实现的典型 Spring 安全性,那么没有为特定的安全方法调用定义配置属性将导致每个`AccessDecisionVoter`放弃投票。反过来,如果`AccessDecisionManager`属性“allowifallabstaindecisions”是`false`,则将抛出一个`AccessDeniedException`。你可以通过(i)将“AllowifallabstainDecisions”设置为`true`(尽管通常不建议这样做)或简单地确保至少有一个配置属性(`AccessDecisionVoter`将投票授予访问权限)来避免这个潜在的问题。后一种(推荐的)方法通常通过`ROLE_USER`或`ROLE_AUTHENTICATED`配置属性来实现。 + +[授权](index.html)[授权 HTTP 请求](authorize-http-requests.html) + diff --git a/docs/spring-security/servlet-authorization-authorize-http-requests.md b/docs/spring-security/servlet-authorization-authorize-http-requests.md new file mode 100644 index 0000000000000000000000000000000000000000..6c1c1ca39c747b41d78cd7c9365c8776a06ad783 --- /dev/null +++ b/docs/spring-security/servlet-authorization-authorize-http-requests.md @@ -0,0 +1,162 @@ +# 授权 HttpServletRequestwithAuthorizationFilter + +本节通过深入研究[授权](index.html#servlet-authorization)在基于 Servlet 的应用程序中的工作方式,构建了[Servlet Architecture and Implementation](../architecture.html#servlet-architecture)。 + +| |`AuthorizationFilter`取代[`FilterSecurityInterceptor`](authorize-requests.html# Servlet-authorization-filtersecurityinterceptor)。
要保持向后兼容,`FilterSecurityInterceptor`仍然是默认值。
本节讨论`AuthorizationFilter`如何工作以及如何覆盖默认配置。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +[`AuthorizationFilter`](https://DOCS. Spring.io/ Spring-security/site/DOCS/5.6.2/api/org/springframework/security/web/access/intercept/intercept/authorizationfilter.html)为`HttpServletRequest`s 提供[授权](index.html#servlet-authorization)。它作为[安全过滤器](../architecture.html#servlet-security-filters)中的一个插入到[FilterchainProxy](../architecture.html#servlet-filterchainproxy)中。 + +声明`SecurityFilterChain`时,可以重写默认值。不要使用[`authorizeRequests`](# Servlet-authorize-requests-defaults),而是使用`authorizeHttpRequests`,就像这样: + +例 1。使用 AuthorizeHttpRequests + +爪哇 + +``` +@Bean +SecurityFilterChain web(HttpSecurity http) throws AuthenticationException { + http + .authorizeHttpRequests((authorize) -> authorize + .anyRequest().authenticated(); + ) + // ... + + return http.build(); +} +``` + +这在以下几个方面改进了`authorizeRequests`: + +1. 使用简化的`AuthorizationManager`API,而不是元数据源、配置属性、决策管理器和投票者。这简化了重用和定制。 + +2. 延迟`Authentication`查找。而不是需要为每个请求查找身份验证,它只会在授权决策需要身份验证的请求中查找。 + +3. Bean-基于配置支持。 + +当使用`authorizeHttpRequests`而不是`authorizeRequests`时,则使用[`AuthorizationFilter`](https://DOCS. Spring.io/ Spring-security/site/DOCS/5.6.2/api/org/springframework/security/web/access/intercept/Authorizationfilter.html)代替[<<](authority-requests.html# Servlet-authority-filtersecurityptor)。 + +![授权过滤器](../../_images/servlet/authorization/authorizationfilter.png) + +图 1。授权 HttpServletRequest + +* ![number 1](../../_images/icons/number_1.png)首先,`AuthorizationFilter`从[SecurityContextholder](../authentication/architecture.html#servlet-authentication-securitycontextholder)得到[认证](../authentication/architecture.html#servlet-authentication-authentication)。它将此包在`Supplier`中,以延迟查找。 + +* ![number 2](../../_images/icons/number_2.png)秒,`AuthorizationFilter`从`HttpServletRequest`、`FilterInvocation`、`和`FilterInvocation`传递给[`AuthorizationManager`]。 + + * ![number 4](../../_images/icons/number_4.png)如果拒绝授权,将抛出`AccessDeniedException`。在这种情况下,[`ExceptionTranslationFilter`](../architecture.html# Servlet-ExceptionTranslationFilter)处理`AccessDeniedException`。 + + * ![number 5](../../_images/icons/number_5.png)如果访问被授予,`AuthorizationFilter`继续使用[滤清链](../architecture.html#servlet-filters-review),这允许应用程序正常处理。 + +通过按优先级顺序添加更多规则,我们可以将安全性配置为具有不同的规则。 + +例 2。授权请求 + +爪哇 + +``` +@Bean +SecurityFilterChain web(HttpSecurity http) throws Exception { + http + // ... + .authorizeHttpRequests(authorize -> authorize (1) + .mvcMatchers("/resources/**", "/signup", "/about").permitAll() (2) + .mvcMatchers("/admin/**").hasRole("ADMIN") (3) + .mvcMatchers("/db/**").access("hasRole('ADMIN') and hasRole('DBA')") (4) + .anyRequest().denyAll() (5) + ); + + return http.build(); +} +``` + +|**1**|指定了多个授权规则。
每个规则都按照它们被声明的顺序被考虑。| +|-----|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +|**2**|我们指定了任何用户都可以访问的多个 URL 模式。
具体来说,如果 URL 以“/resources/”开头,等于“/signup”或等于“/about”,则任何用户都可以访问请求。| +|**3**|任何以“/admin/”开头的 URL 都将被限制为具有角色“role\_admin”的用户。
你将注意到,由于我们正在调用`hasRole`方法,因此我们不需要指定“role\_”前缀。| +|**4**|任何以“/db/”开头的 URL 都要求用户同时具有“role\_admin”和“role\_DBA”。
你将注意到,由于我们使用的是`hasRole`表达式,因此我们不需要指定“role\_”前缀。| +|**5**|任何尚未匹配的 URL 都将被拒绝访问。
如果你不想意外地忘记更新授权规则,这是一个很好的策略。| + +你可以通过构建自己的[`RequestMatcherDelegatingAuthorizationManager`](architecture.html#authz-delegate-authorization-manager)来采用基于 Bean 的方法,如下所示: + +例 3。配置 RequestMatcherDelegatingAuthorizationManager + +爪哇 + +``` +@Bean +SecurityFilterChain web(HttpSecurity http, AuthorizationManager access) + throws AuthenticationException { + http + .authorizeHttpRequests((authorize) -> authorize + .anyRequest().access(access) + ) + // ... + + return http.build(); +} + +@Bean +AuthorizationManager requestMatcherAuthorizationManager(HandlerMappingIntrospector introspector) { + RequestMatcher permitAll = + new AndRequestMatcher( + new MvcRequestMatcher(introspector, "/resources/**"), + new MvcRequestMatcher(introspector, "/signup"), + new MvcRequestMatcher(introspector, "/about")); + RequestMatcher admin = new MvcRequestMatcher(introspector, "/admin/**"); + RequestMatcher db = new MvcRequestMatcher(introspector, "/db/**"); + RequestMatcher any = AnyRequestMatcher.INSTANCE; + AuthorizationManager manager = RequestMatcherDelegatingAuthorizationManager.builder() + .add(permitAll, (context) -> new AuthorizationDecision(true)) + .add(admin, AuthorityAuthorizationManager.hasRole("ADMIN")) + .add(db, AuthorityAuthorizationManager.hasRole("DBA")) + .add(any, new AuthenticatedAuthorizationManager()) + .build(); + return (context) -> manager.check(context.getRequest()); +} +``` + +你还可以为任何请求匹配器连接[你自己的自定义授权管理器](architecture.html#authz-custom-authorization-manager)。 + +下面是将自定义授权管理器映射到`my/authorized/endpoint`的示例: + +例 4。自定义授权管理器 + +爪哇 + +``` +@Bean +SecurityFilterChain web(HttpSecurity http) throws Exception { + http + .authorizeHttpRequests((authorize) -> authorize + .mvcMatchers("/my/authorized/endpoint").access(new CustomAuthorizationManager()); + ) + // ... + + return http.build(); +} +``` + +或者,你可以为所有请求提供它,如下所示: + +例 5。所有请求的自定义授权管理器 + +爪哇 + +``` +@Bean +SecurityFilterChain web(HttpSecurity http) throws Exception { + http + .authorizeHttpRequests((authorize) -> authorize + .anyRequest.access(new CustomAuthorizationManager()); + ) + // ... + + return http.build(); +} +``` + +[授权体系结构](architecture.html)[使用 FilterSecurityInterceptor 授权 HTTP 请求](authorize-requests.html) + diff --git a/docs/spring-security/servlet-authorization-authorize-requests.md b/docs/spring-security/servlet-authorization-authorize-requests.md new file mode 100644 index 0000000000000000000000000000000000000000..f4f07ace5031003f3f13aca28744413905f42000 --- /dev/null +++ b/docs/spring-security/servlet-authorization-authorize-requests.md @@ -0,0 +1,125 @@ +# 使用 FilterSecurityInterceptor 授权 HttpServletRequest + +| |`FilterSecurityInterceptor`正在被[`AuthorizationFilter`]替换。
考虑使用它。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------| + +本节通过深入研究[授权](index.html#servlet-authorization)在基于 Servlet 的应用程序中的工作方式,构建了[Servlet Architecture and Implementation](../architecture.html#servlet-architecture)。 + +[`FilterSecurityInterceptor`](https://DOCS. Spring.io/ Spring-security/site/DOCS/5.6.2/api/org/springframework/security/web/access/intercept/filtersecurityinterceptor.html)为`HttpServletRequest`s 提供[授权](index.html#servlet-authorization)。它作为[安全过滤器](../architecture.html#servlet-security-filters)中的一个插入到[FilterchainProxy](../architecture.html#servlet-filterchainproxy)中。 + +![过滤安全拦截器](../../_images/servlet/authorization/filtersecurityinterceptor.png) + +图 1。授权 HttpServletRequest + +* ![number 1](../../_images/icons/number_1.png)首先,`FilterSecurityInterceptor`从[SecurityContextholder](../authentication/architecture.html#servlet-authentication-securitycontextholder)得到一个[认证](../authentication/architecture.html#servlet-authentication-authentication)。 + +* ![number 2](../../_images/icons/number_2.png)第二,`FilterSecurityInterceptor`从`HttpServletRequest`、`HttpServletResponse`和`FilterChain`中创建一个[`FilterChain`(https://DOCS. Spring.io/ Spring-security/site/site/DOCS/5.6.2/api/org/springframework/security/web/filterinvocation.html),并传递到`HttpServletRequest`中的`HttpServletResponse`和`FilterChain`中。 + +* ![number 3](../../_images/icons/number_3.png)下一步,它将`FilterInvocation`传递到`SecurityMetadataSource`,得到`ConfigAttribute`s。 + +* ![number 4](../../_images/icons/number_4.png)最后,它将`Authentication`、`FilterInvocation`和`ConfigAttribute`s 传递给 Xref: Servlet/授权。ADOC#authz-access-decision-manager`AccessDecisionManager`。 + + * ![number 5](../../_images/icons/number_5.png)如果拒绝授权,将抛出`AccessDeniedException`。在这种情况下,[`ExceptionTranslationFilter`](../architecture.html# Servlet-ExceptionTranslationFilter)处理`AccessDeniedException`。 + + * ![number 6](../../_images/icons/number_6.png)如果访问被授予,`FilterSecurityInterceptor`继续使用[滤清链](../architecture.html#servlet-filters-review),这允许应用程序正常处理。 + +默认情况下, Spring Security 的授权将要求对所有请求进行身份验证。显式配置如下所示: + +例 1。每个请求都必须经过验证。 + +爪哇 + +``` +protected void configure(HttpSecurity http) throws Exception { + http + // ... + .authorizeRequests(authorize -> authorize + .anyRequest().authenticated() + ); +} +``` + +XML + +``` + + + + +``` + +Kotlin + +``` +fun configure(http: HttpSecurity) { + http { + // ... + authorizeRequests { + authorize(anyRequest, authenticated) + } + } +} +``` + +我们可以通过按优先级顺序添加更多规则来配置 Spring 安全性,使其具有不同的规则。 + +例 2。授权请求 + +爪哇 + +``` +protected void configure(HttpSecurity http) throws Exception { + http + // ... + .authorizeRequests(authorize -> authorize (1) + .mvcMatchers("/resources/**", "/signup", "/about").permitAll() (2) + .mvcMatchers("/admin/**").hasRole("ADMIN") (3) + .mvcMatchers("/db/**").access("hasRole('ADMIN') and hasRole('DBA')") (4) + .anyRequest().denyAll() (5) + ); +} +``` + +XML + +``` + (1) + + (2) + + + + + (3) + (4) + (5) + +``` + +Kotlin + +``` +fun configure(http: HttpSecurity) { + http { + authorizeRequests { (1) + authorize("/resources/**", permitAll) (2) + authorize("/signup", permitAll) + authorize("/about", permitAll) + + authorize("/admin/**", hasRole("ADMIN")) (3) + authorize("/db/**", "hasRole('ADMIN') and hasRole('DBA')") (4) + authorize(anyRequest, denyAll) (5) + } + } +} +``` + +|**1**|指定了多个授权规则。
每个规则都按照它们被声明的顺序被考虑。| +|-----|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +|**2**|我们指定了任何用户都可以访问的多个 URL 模式。
具体来说,如果 URL 以“/resources/”开头,等于“/signup”或等于“/about”,则任何用户都可以访问请求。| +|**3**|任何以“/admin/”开头的 URL 都将被限制为具有角色“role\_admin”的用户。
你将注意到,由于我们正在调用`hasRole`方法,因此我们不需要指定“role\_”前缀。| +|**4**|任何以“/db/”开头的 URL 都要求用户同时具有“role\_admin”和“role\_DBA”。
你将注意到,由于我们使用的是`hasRole`表达式,因此我们不需要指定“role\_”前缀。| +|**5**|任何尚未匹配的 URL 都将被拒绝访问。
如果你不想意外地忘记更新授权规则,这是一个很好的策略。| + +[授权 HTTP 请求](authorize-http-requests.html)[基于表达式的访问控制](expression-based.html) + diff --git a/docs/spring-security/servlet-authorization-expression-based.md b/docs/spring-security/servlet-authorization-expression-based.md new file mode 100644 index 0000000000000000000000000000000000000000..1b5e0fb98b8f38b2d12cb0e3955c8feec789ae7e --- /dev/null +++ b/docs/spring-security/servlet-authorization-expression-based.md @@ -0,0 +1,378 @@ +# 基于表达式的访问控制 + +## 概述 + +Spring 安全性使用 Spring EL 作为表达式支持,如果你有兴趣更深入地理解该主题,那么你应该研究它是如何工作的。表达式使用“根对象”作为求值上下文的一部分进行求值。 Spring 安全性使用用于 Web 和方法安全性的特定类作为根对象,以便提供内置的表达式和对诸如当前主体的值的访问。 + +### 常见的内置表达式 + +表达式根对象的基类是`SecurityExpressionRoot`。这提供了一些在 Web 和方法安全中都可用的公共表达式。 + +| Expression |说明| +|----------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `hasRole(String role)` |返回`true`如果当前主体具有指定的角色。

例如,`hasRole('admin')`

默认情况下,如果提供的角色不是以“role\_”开头,则将被添加。
这可以通过修改`DefaultWebSecurityExpressionHandler`上的`defaultRolePrefix`来定制。| +| `hasAnyRole(String…​ roles)` |返回`true`如果当前主体有任何提供的角色(以逗号分隔的字符串列表给出)。

例如,`hasAnyRole('admin', 'user')`

默认情况下,如果提供的角色不是以“role\_”开头,则将添加它。
这可以通过修改`DefaultWebSecurityExpressionHandler`上的`defaultRolePrefix`来定制。| +| `hasAuthority(String authority)` |如果当前主体具有指定的权限,则返回`true`。

例如,`hasAuthority('read')`| +| `hasAnyAuthority(String…​ authorities)` |返回`true`如果当前主体有任何提供的权限(以逗号分隔的字符串列表给出)

例如,`hasAnyAuthority('read', 'write')`| +| `principal` |允许直接访问表示当前用户的主体对象| +| `authentication` |允许直接访问从`SecurityContext`中获得的当前`Authentication`对象| +| `permitAll` |总是计算为`true`| +| `denyAll` |总是计算为`false`| +| `isAnonymous()` |如果当前主体是匿名用户,则返回`true`| +| `isRememberMe()` |如果当前主体是 rememe-me 用户,则返回`true`| +| `isAuthenticated()` |如果用户不是匿名的,则返回`true`| +| `isFullyAuthenticated()` |如果用户不是匿名者或 Remember-Me 用户,则返回`true`| +| `hasPermission(Object target, Object permission)` |返回`true`如果用户可以访问给定权限提供的目标。
例如,`hasPermission(domainObject, 'read')`| +|`hasPermission(Object targetId, String targetType, Object permission)`|返回`true`如果用户可以访问给定权限提供的目标。
例如,`hasPermission(1, 'com.example.domain.Message', 'read')`| + +## Web 安全表达式 + +要使用表达式来保护单个 URL,你首先需要将``元素中的`use-expressions`属性设置为`true`。 Spring 然后,安全性将期望`access`元素的``属性包含 Spring EL 表达式。表达式应该计算为布尔值,定义是否应该允许访问。例如: + +``` + + + ... + +``` + +在这里,我们定义了应用程序的“管理”区域(由 URL 模式定义)应该仅对具有授权“管理”且其 IP 地址与本地子网匹配的用户可用。在前面的部分中,我们已经看到了内置的`hasRole`表达式。表达式`hasIpAddress`是一个特定于 Web 安全的附加内置表达式。它是由`WebSecurityExpressionRoot`类定义的,当计算 Web-Access 表达式时,它的一个实例被用作表达式的根对象。该对象还直接公开了名为`request`的`HttpServletRequest`对象,因此你可以在表达式中直接调用该请求。如果正在使用表达式,则将把`WebExpressionVoter`添加到名称空间使用的`AccessDecisionManager`中。因此,如果你 AREN 不使用名称空间,而希望使用表达式,则必须将其中的一个添加到配置中。 + +### 在 Web 安全表达式中引用 bean + +如果你希望扩展可用的表达式,那么可以轻松地引用你公开的任何 Spring Bean。例如,假设你有一个名为`webSecurity`的 Bean,该 Bean 包含以下方法签名: + +爪哇 + +``` +public class WebSecurity { + public boolean check(Authentication authentication, HttpServletRequest request) { + ... + } +} +``` + +Kotlin + +``` +class WebSecurity { + fun check(authentication: Authentication?, request: HttpServletRequest?): Boolean { + // ... + } +} +``` + +你可以使用以下方法参考该方法: + +例 1。请参阅方法 + +爪哇 + +``` +http + .authorizeHttpRequests(authorize -> authorize + .antMatchers("/user/**").access("@webSecurity.check(authentication,request)") + ... + ) +``` + +XML + +``` + + + ... + +``` + +Kotlin + +``` +http { + authorizeRequests { + authorize("/user/**", "@webSecurity.check(authentication,request)") + } +} +``` + +### Web 安全表达式中的路径变量 + +有时,能够在 URL 中引用路径变量是一件好事。例如,考虑一个 RESTful 应用程序,该应用程序以`/user/{userId}`格式从 URL 路径中通过 ID 查找用户。 + +通过将路径变量放入模式中,你可以轻松地引用它。例如,如果你有一个名为`webSecurity`的 Bean,该 Bean 包含以下方法签名: + +爪哇 + +``` +public class WebSecurity { + public boolean checkUserId(Authentication authentication, int id) { + ... + } +} +``` + +Kotlin + +``` +class WebSecurity { + fun checkUserId(authentication: Authentication?, id: Int): Boolean { + // ... + } +} +``` + +你可以使用以下方法参考该方法: + +例 2。路径变量 + +爪哇 + +``` +http + .authorizeHttpRequests(authorize -> authorize + .antMatchers("/user/{userId}/**").access("@webSecurity.checkUserId(authentication,#userId)") + ... + ); +``` + +XML + +``` + + + ... + +``` + +Kotlin + +``` +http { + authorizeRequests { + authorize("/user/{userId}/**", "@webSecurity.checkUserId(authentication,#userId)") + } +} +``` + +在这种配置中,匹配的 URL 将传入 PATH 变量(并将其转换为 checkuserid 方法)。例如,如果 URL 是`/user/123/resource`,那么传入的 ID 将是`123`。 + +## 方法安全表达式 + +方法安全性比简单的允许或拒绝规则要复杂一些。 Spring Security3.0 引入了一些新的注释,以便允许对表达式的使用提供全面的支持。 + +### @pre 和 @post 注释 + +有四个注释支持表达式属性,以允许调用前和调用后的授权检查,还支持对提交的集合参数或返回值进行过滤。它们是`@PreAuthorize`,`@PreFilter`,`@PostAuthorize`和`@PostFilter`。它们的使用是通过`global-method-security`名称空间元素启用的: + +``` + +``` + +#### 使用 @preauthorize 和 @postauthorize 的访问控制 + +最明显有用的注释是`@PreAuthorize`,它决定是否可以实际调用方法。例如(来自[Contacts](https://github.com/spring-projects/spring-security-samples/tree/5.6.x/servlet/xml/java/contacts)示例应用程序) + +爪哇 + +``` +@PreAuthorize("hasRole('USER')") +public void create(Contact contact); +``` + +Kotlin + +``` +@PreAuthorize("hasRole('USER')") +fun create(contact: Contact?) +``` + +这意味着只允许角色为“role\_user”的用户访问。显然,使用传统配置和所需角色的简单配置属性可以轻松实现相同的事情。但是: + +爪哇 + +``` +@PreAuthorize("hasPermission(#contact, 'admin')") +public void deletePermission(Contact contact, Sid recipient, Permission permission); +``` + +Kotlin + +``` +@PreAuthorize("hasPermission(#contact, 'admin')") +fun deletePermission(contact: Contact?, recipient: Sid?, permission: Permission?) +``` + +在这里,我们实际上使用了一个方法参数作为表达式的一部分,来决定当前用户是否拥有给定联系人的“管理”权限。内置的`hasPermission()`表达式通过应用程序上下文链接到 Spring Security ACL 模块中,我们将[see below](#el-permission-evaluator)。你可以通过名称作为表达式变量来访问任何方法参数。 + +Spring 安全性可以通过多种方式来解决方法参数。 Spring 安全性使用`DefaultSecurityParameterNameDiscoverer`来发现参数名称。默认情况下,将对整个方法尝试以下选项。 + +* 如果 Spring Security 的`@P`注释出现在方法的单个参数上,则将使用该值。这对于在 JDK8 之前用 JDK 编译的接口非常有用,因为 JDK8 之前的接口不包含任何有关参数名称的信息。例如: + + 爪哇 + + ``` + import org.springframework.security.access.method.P; + + ... + + @PreAuthorize("#c.name == authentication.name") + public void doSomething(@P("c") Contact contact); + ``` + + Kotlin + + ``` + import org.springframework.security.access.method.P + + ... + + @PreAuthorize("#c.name == authentication.name") + fun doSomething(@P("c") contact: Contact?) + ``` + + 在幕后,这是使用`AnnotationParameterNameDiscoverer`实现的,它可以自定义以支持任何指定注释的 value 属性。 + +* 如果在该方法的至少一个参数上存在 Spring 数据的`@Param`注释,则将使用该值。这对于在 JDK8 之前用 JDK 编译的接口非常有用,因为 JDK8 之前的接口不包含任何有关参数名称的信息。例如: + + 爪哇 + + ``` + import org.springframework.data.repository.query.Param; + + ... + + @PreAuthorize("#n == authentication.name") + Contact findContactByName(@Param("n") String name); + ``` + + Kotlin + + ``` + import org.springframework.data.repository.query.Param + + ... + + @PreAuthorize("#n == authentication.name") + fun findContactByName(@Param("n") name: String?): Contact? + ``` + + 在幕后,这是使用`AnnotationParameterNameDiscoverer`实现的,它可以自定义以支持任何指定注释的值属性。 + +* 如果使用 JDK8 用-parameters 参数编译源代码,并且使用 Spring 4+,则使用标准的 JDK 反射 API 来发现参数名。这对类和接口都有效。 + +* 最后,如果代码是用调试符号编译的,那么将使用调试符号来发现参数名称。这将不适用于接口,因为它们没有关于参数名称的调试信息。对于接口,必须使用注释或 JDK8 方法。 + + + +表达式中的任何 Spring-EL 功能都是可用的,因此你也可以访问参数上的属性。例如,如果你希望一个特定的方法只允许访问用户名与联系人的用户名匹配的用户,那么你可以编写 + +爪哇 + +``` +@PreAuthorize("#contact.name == authentication.name") +public void doSomething(Contact contact); +``` + +Kotlin + +``` +@PreAuthorize("#contact.name == authentication.name") +fun doSomething(contact: Contact?) +``` + +这里我们正在访问另一个内置表达式`authentication`,它是存储在安全上下文中的`Authentication`。你还可以使用表达式`principal`直接访问其“principal”属性。该值通常是`UserDetails`实例,因此你可以使用`principal.username`或`principal.enabled`之类的表达式。 + + + +不太常见的情况是,你可能希望在方法被调用后执行访问控制检查。这可以使用`@PostAuthorize`注释来实现。要从方法访问返回值,请在表达式中使用内置名称`returnObject`。 + +#### 使用 @prefilter 和 @postfilter 进行过滤 + +Spring 安全性支持使用表达式对集合、阵列、地图和流进行过滤。这通常是在方法的返回值上执行的。例如: + +爪哇 + +``` +@PreAuthorize("hasRole('USER')") +@PostFilter("hasPermission(filterObject, 'read') or hasPermission(filterObject, 'admin')") +public List getAll(); +``` + +Kotlin + +``` +@PreAuthorize("hasRole('USER')") +@PostFilter("hasPermission(filterObject, 'read') or hasPermission(filterObject, 'admin')") +fun getAll(): List +``` + +当使用`@PostFilter`注释时, Spring Security 会遍历返回的集合或映射,并删除所提供的表达式为 false 的任何元素。对于数组,将返回一个包含筛选元素的新数组实例。名称`filterObject`是指集合中的当前对象。在使用映射的情况下,它将引用当前的`Map.Entry`对象,该对象允许在 expresion 中使用`filterObject.key`或`filterObject.value`。你也可以在方法调用之前使用`@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`对象)。第一种方法用于已经加载了访问权限的域对象的情况。然后,如果当前用户拥有该对象的给定权限,表达式将返回 true。第二个版本用于对象未加载但其标识符已知的情况。域对象还需要一个抽象的“类型”说明符,从而允许加载正确的 ACL 权限。传统上,这是对象的 爪哇 类,但只要与加载权限的方式一致,就不必这样做。 + +要使用`hasPermission()`表达式,你必须在应用程序上下文中显式地配置`PermissionEvaluator`。这看起来像这样: + +``` + + + + + + + +``` + +其中`myPermissionEvaluator`是实现`PermissionEvaluator`的 Bean。通常这将是来自 ACL 模块的实现,它被称为`AclPermissionEvaluator`。有关更多详细信息,请参见[Contacts](https://github.com/spring-projects/spring-security-samples/tree/5.6.x/servlet/xml/java/contacts)示例应用程序配置。 + +#### 方法安全元注释 + +你可以利用方法安全性的元注释来使你的代码更具可读性。如果你发现你在整个代码库中重复相同的复杂表达式,这一点特别方便。例如,考虑以下几点: + +``` +@PreAuthorize("#contact.name == authentication.name") +``` + +我们可以创建一个可以替代它的元注释,而不是在任何地方重复这一点。 + +Java + +``` +@Retention(RetentionPolicy.RUNTIME) +@PreAuthorize("#contact.name == authentication.name") +public @interface ContactPermission {} +``` + +Kotlin + +``` +@Retention(AnnotationRetention.RUNTIME) +@PreAuthorize("#contact.name == authentication.name") +annotation class ContactPermission +``` + +Spring 安全方法中的任何安全注释都可以使用元注释。为了保持与规范的兼容性,JSR-250 注释不支持元注释。 + +[使用 FilterSecurityInterceptor 授权 HTTP 请求](authorize-requests.html)[安全对象实现](secure-objects.html) + diff --git a/docs/spring-security/servlet-authorization-method-security.md b/docs/spring-security/servlet-authorization-method-security.md new file mode 100644 index 0000000000000000000000000000000000000000..a5e168493ec450806f95fff7555be74dd4fb27ca --- /dev/null +++ b/docs/spring-security/servlet-authorization-method-security.md @@ -0,0 +1,831 @@ +# 方法安全性 + +从版本 2.0 开始 Spring,安全性大大提高了对向服务层方法添加安全性的支持。它提供了对 JSR-250 注释安全性的支持,以及框架的原始`@Secured`注释。从 3.0 开始,你还可以使用 New[基于表达式的注释](expression-based.html#el-access)。你可以对单个 Bean 应用安全性,使用`intercept-methods`元素来装饰 Bean 声明,或者可以使用 AspectJ 样式的切入点在整个服务层中保护多个 bean。 + +## EnableMethodSecurity + +在 Spring Security5.6 中,我们可以在任何`@Configuration`实例上使用`@EnableMethodSecurity`注释来启用基于注释的安全性。 + +这在许多方面改进了`@启用全球方法安全`。`@EnableMethodSecurity`: + +1. 使用简化的`AuthorizationManager`API,而不是元数据源、配置属性、决策管理器和投票者。这简化了重用和定制。 + +2. 支持基于直接 Bean 的配置,而不是要求扩展`GlobalMethodSecurityConfiguration`来定制 bean + +3. 是使用本机 Spring AOP 构建的,删除了抽象,并允许你使用 Spring AOP 构建块来自定义 + +4. 检查相互冲突的注释,以确保安全配置的明确性. + +5. 符合 JSR-250 + +6. 默认情况下启用`@PreAuthorize`、`@PostAuthorize`、`@PreFilter`和`@PostFilter` + +| |对于更早的版本,请阅读[@enableGlobalMethodSecurity ](#jc-enable-global-method-security)的类似支持。| +|---|------------------------------------------------------------------------------------------------------------------------------| + +例如,下面将启用 Spring Security 的`@PreAuthorize`注释: + +例 1。方法安全配置 + +爪哇 + +``` +@EnableMethodSecurity +public class MethodSecurityConfig { + // ... +} +``` + +Kotlin + +``` +@EnableMethodSecurity +class MethodSecurityConfig { + // ... +} +``` + +XML + +``` + +``` + +然后,向方法(在类或接口上)添加注释将相应地限制对该方法的访问。 Spring Security 的本机注释支持为该方法定义了一组属性。这些将传递给`DefaultAuthorizationMethodInterceptorChain`,以便它做出实际的决定: + +例 2。方法安全注释的使用 + +爪哇 + +``` +public interface BankService { + @PreAuthorize("hasRole('USER')") + Account readAccount(Long id); + + @PreAuthorize("hasRole('USER')") + List findAccounts(); + + @PreAuthorize("hasRole('TELLER')") + Account post(Account account, Double amount); +} +``` + +Kotlin + +``` +interface BankService { + @PreAuthorize("hasRole('USER')") + fun readAccount(id : Long) : Account + + @PreAuthorize("hasRole('USER')") + fun findAccounts() : List + + @PreAuthorize("hasRole('TELLER')") + fun post(account : Account, amount : Double) : Account +} +``` + +你可以使用以下方法启用对 Spring Security 的`@Secured`注释的支持: + +例 3。@Secured Configuration + +爪哇 + +``` +@EnableMethodSecurity(securedEnabled = true) +public class MethodSecurityConfig { + // ... +} +``` + +Kotlin + +``` +@EnableMethodSecurity(securedEnabled = true) +class MethodSecurityConfig { + // ... +} +``` + +XML + +``` + +``` + +或 JSR-250 使用: + +例 4。JSR-250 配置 + +爪哇 + +``` +@EnableMethodSecurity(jsr250Enabled = true) +public class MethodSecurityConfig { + // ... +} +``` + +Kotlin + +``` +@EnableMethodSecurity(jsr250Enabled = true) +class MethodSecurityConfig { + // ... +} +``` + +XML + +``` + +``` + +### 自定义授权 + +Spring Security 的`@PreAuthorize`、`@PostAuthorize`、`@PreFilter`和`@PostFilter`提供了丰富的基于表达式的支持。 + +如果需要自定义表达式的处理方式,可以公开一个自定义`MethodSecurityExpressionHandler`,如下所示: + +例 5。自定义方法 SecurityExpressionHandler + +爪哇 + +``` +@Bean +static MethodSecurityExpressionHandler methodSecurityExpressionHandler() { + DefaultMethodSecurityExpressionHandler handler = new DefaultMethodSecurityExpressionHandler(); + handler.setTrustResolver(myCustomTrustResolver); + return handler; +} +``` + +Kotlin + +``` +companion object { + @Bean + fun methodSecurityExpressionHandler() : MethodSecurityExpressionHandler { + val handler = DefaultMethodSecurityExpressionHandler(); + handler.setTrustResolver(myCustomTrustResolver); + return handler; + } +} +``` + +XML + +``` + + + + + + + +``` + +| |我们使用`MethodSecurityExpressionHandler`方法公开`static`,以确保 Spring 在初始化 Spring Security 的方法 Security`@Configuration`类之前发布它| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +另外,对于基于角色的授权, Spring Security 添加了一个默认的`ROLE_`前缀,该前缀在计算`hasRole`之类的表达式时使用。 + +可以通过公开`GrantedAuthorityDefaults` Bean 来将授权规则配置为使用不同的前缀,如下所示: + +例 6。自定义方法 SecurityExpressionHandler + +爪哇 + +``` +@Bean +static GrantedAuthorityDefaults grantedAuthorityDefaults() { + return new GrantedAuthorityDefaults("MYPREFIX_"); +} +``` + +Kotlin + +``` +companion object { + @Bean + fun grantedAuthorityDefaults() : GrantedAuthorityDefaults { + return GrantedAuthorityDefaults("MYPREFIX_"); + } +} +``` + +XML + +``` + + + + + +``` + +| |我们使用`GrantedAuthorityDefaults`方法公开`static`,以确保 Spring 在初始化 Spring Security 的方法 Security`@Configuration`类之前发布它| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### 自定义授权管理器 + +方法授权是方法授权之前和方法授权之后的组合。 + +| |在方法被调用之前执行前方法授权。
如果该授权拒绝访问,则不调用该方法,并抛出一个`AccessDeniedException`
后方法授权是在方法被调用之后,但在方法返回调用者之前执行的。
如果该授权拒绝访问,不返回该值,并抛出一个`AccessDeniedException`| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +要重新创建添加`@EnableMethodSecurity`所做的默认操作,你需要发布以下配置: + +例 7。完整的 pre-post 方法安全配置 + +爪哇 + +``` +@EnableMethodSecurity(prePostEnabled = false) +class MethodSecurityConfig { + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + Advisor preFilterAuthorizationMethodInterceptor() { + return new PreFilterAuthorizationMethodInterceptor(); + } + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + Advisor preAuthorizeAuthorizationMethodInterceptor() { + return AuthorizationManagerBeforeMethodInterceptor.preAuthorize(); + } + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + Advisor postAuthorizeAuthorizationMethodInterceptor() { + return AuthorizationManagerAfterMethodInterceptor.postAuthorize(); + } + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + Advisor postFilterAuthorizationMethodInterceptor() { + return new PostFilterAuthorizationMethodInterceptor(); + } +} +``` + +Kotlin + +``` +@EnableMethodSecurity(prePostEnabled = false) +class MethodSecurityConfig { + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + fun preFilterAuthorizationMethodInterceptor() : Advisor { + return PreFilterAuthorizationMethodInterceptor(); + } + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + fun preAuthorizeAuthorizationMethodInterceptor() : Advisor { + return AuthorizationManagerBeforeMethodInterceptor.preAuthorize(); + } + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + fun postAuthorizeAuthorizationMethodInterceptor() : Advisor { + return AuthorizationManagerAfterMethodInterceptor.postAuthorize(); + } + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + fun postFilterAuthorizationMethodInterceptor() : Advisor { + return PostFilterAuthorizationMethodInterceptor(); + } +} +``` + +XML + +``` + + + + + + + + +``` + +请注意, Spring Security 的方法安全性是使用 Spring AOP 构建的。因此,拦截器是根据指定的顺序调用的。这可以通过在拦截器实例上调用`setOrder`进行自定义,如下所示: + +例 8。发布自定义顾问 + +爪哇 + +``` +@Bean +@Role(BeanDefinition.ROLE_INFRASTRUCTURE) +Advisor postFilterAuthorizationMethodInterceptor() { + PostFilterAuthorizationMethodInterceptor interceptor = new PostFilterAuthorizationMethodInterceptor(); + interceptor.setOrder(AuthorizationInterceptorOrders.POST_AUTHORIZE.getOrder() - 1); + return interceptor; +} +``` + +Kotlin + +``` +@Bean +@Role(BeanDefinition.ROLE_INFRASTRUCTURE) +fun postFilterAuthorizationMethodInterceptor() : Advisor { + val interceptor = PostFilterAuthorizationMethodInterceptor(); + interceptor.setOrder(AuthorizationInterceptorOrders.POST_AUTHORIZE.getOrder() - 1); + return interceptor; +} +``` + +XML + +``` + + + +``` + +你可能希望在应用程序中仅支持`@PreAuthorize`,在这种情况下,你可以执行以下操作: + +例 9。仅 @preauthorize 配置 + +爪哇 + +``` +@EnableMethodSecurity(prePostEnabled = false) +class MethodSecurityConfig { + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + Advisor preAuthorize() { + return AuthorizationManagerBeforeMethodInterceptor.preAuthorize(); + } +} +``` + +Kotlin + +``` +@EnableMethodSecurity(prePostEnabled = false) +class MethodSecurityConfig { + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + fun preAuthorize() : Advisor { + return AuthorizationManagerBeforeMethodInterceptor.preAuthorize() + } +} +``` + +XML + +``` + + + + + +``` + +或者,你可能有一个要添加到列表中的自定义 before-method`AuthorizationManager`。 + +在这种情况下,你需要告知 Spring 安全性`AuthorizationManager`以及你的授权管理器应用到哪些方法和类。 + +因此,可以将 Spring 安全性配置为在`@PreAuthorize`和`@PostAuthorize`之间调用`AuthorizationManager`,就像这样: + +例 10。顾问之前的自定义 + +爪哇 + +``` +@EnableMethodSecurity +class MethodSecurityConfig { + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + public Advisor customAuthorize() { + JdkRegexpMethodPointcut pattern = new JdkRegexpMethodPointcut(); + pattern.setPattern("org.mycompany.myapp.service.*"); + AuthorizationManager rule = AuthorityAuthorizationManager.isAuthenticated(); + AuthorizationManagerBeforeMethodInterceptor interceptor = new AuthorizationManagerBeforeMethodInterceptor(pattern, rule); + interceptor.setOrder(AuthorizationInterceptorsOrder.PRE_AUTHORIZE_ADVISOR_ORDER.getOrder() + 1); + return interceptor; + } +} +``` + +Kotlin + +``` +@EnableMethodSecurity +class MethodSecurityConfig { + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + fun customAuthorize() : Advisor { + val pattern = JdkRegexpMethodPointcut(); + pattern.setPattern("org.mycompany.myapp.service.*"); + val rule = AuthorityAuthorizationManager.isAuthenticated(); + val interceptor = AuthorizationManagerBeforeMethodInterceptor(pattern, rule); + interceptor.setOrder(AuthorizationInterceptorsOrder.PRE_AUTHORIZE_ADVISOR_ORDER.getOrder() + 1); + return interceptor; + } +} +``` + +XML + +``` + + + + + + + + + + + + + + + +``` + +| |你可以使用`AuthorizationInterceptorsOrder`中指定的顺序常量,将拦截器放置在 Spring 安全方法拦截器之间。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------| + +对于后方法授权也可以这样做。后方法授权通常涉及分析返回值以验证访问。 + +例如,你可能有一个方法来确认请求的帐户实际上属于登录用户,如下所示: + +例 11。@postauthorize 示例 + +爪哇 + +``` +public interface BankService { + + @PreAuthorize("hasRole('USER')") + @PostAuthorize("returnObject.owner == authentication.name") + Account readAccount(Long id); +} +``` + +Kotlin + +``` +interface BankService { + + @PreAuthorize("hasRole('USER')") + @PostAuthorize("returnObject.owner == authentication.name") + fun readAccount(id : Long) : Account +} +``` + +你可以提供自己的`AuthorizationMethodInterceptor`以自定义如何计算对返回值的访问。 + +例如,如果你有自己的自定义注释,那么可以这样配置它: + +例 12。自定义后的顾问 + +爪哇 + +``` +@EnableMethodSecurity +class MethodSecurityConfig { + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + public Advisor customAuthorize(AuthorizationManager rules) { + AnnotationMethodMatcher pattern = new AnnotationMethodMatcher(MySecurityAnnotation.class); + AuthorizationManagerAfterMethodInterceptor interceptor = new AuthorizationManagerAfterMethodInterceptor(pattern, rules); + interceptor.setOrder(AuthorizationInterceptorsOrder.POST_AUTHORIZE_ADVISOR_ORDER.getOrder() + 1); + return interceptor; + } +} +``` + +Kotlin + +``` +@EnableMethodSecurity +class MethodSecurityConfig { + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + fun customAuthorize(rules : AuthorizationManager) : Advisor { + val pattern = AnnotationMethodMatcher(MySecurityAnnotation::class.java); + val interceptor = AuthorizationManagerAfterMethodInterceptor(pattern, rules); + interceptor.setOrder(AuthorizationInterceptorsOrder.POST_AUTHORIZE_ADVISOR_ORDER.getOrder() + 1); + return interceptor; + } +} +``` + +XML + +``` + + + + + + + + + + + + + + + +``` + +它将在`@PostAuthorize`拦截器之后被调用。 + +## EnableGlobalMethodSecurity + +我们可以在任何`@Configuration`实例上使用`@EnableGlobalMethodSecurity`注释来启用基于注释的安全性。例如,下面将启用 Spring Security 的`@Secured`注释。 + +爪哇 + +``` +@EnableGlobalMethodSecurity(securedEnabled = true) +public class MethodSecurityConfig { +// ... +} +``` + +Kotlin + +``` +@EnableGlobalMethodSecurity(securedEnabled = true) +open 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); +} +``` + +Kotlin + +``` +interface BankService { + @Secured("IS_AUTHENTICATED_ANONYMOUSLY") + fun readAccount(id: Long): Account + + @Secured("IS_AUTHENTICATED_ANONYMOUSLY") + fun findAccounts(): Array + + @Secured("ROLE_TELLER") + fun post(account: Account, amount: Double): Account +} +``` + +可以使用以下工具启用对 JSR-250 注释的支持 + +爪哇 + +``` +@EnableGlobalMethodSecurity(jsr250Enabled = true) +public class MethodSecurityConfig { +// ... +} +``` + +Kotlin + +``` +@EnableGlobalMethodSecurity(jsr250Enabled = true) +open class MethodSecurityConfig { + // ... +} +``` + +这些是基于标准的,允许应用简单的基于角色的约束,但不具有 Spring Security 的本机注释的功能。要使用新的基于表达式的语法,你可以使用 + +爪哇 + +``` +@EnableGlobalMethodSecurity(prePostEnabled = true) +public class MethodSecurityConfig { +// ... +} +``` + +Kotlin + +``` +@EnableGlobalMethodSecurity(prePostEnabled = true) +open class MethodSecurityConfig { + // ... +} +``` + +与之对应的 爪哇 代码是 + +爪哇 + +``` +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); +} +``` + +Kotlin + +``` +interface BankService { + @PreAuthorize("isAnonymous()") + fun readAccount(id: Long): Account + + @PreAuthorize("isAnonymous()") + fun findAccounts(): Array + + @PreAuthorize("hasAuthority('ROLE_TELLER')") + fun post(account: Account, amount: Double): Account +} +``` + +## GlobalMethodSecurityConfiguration + +有时,你可能需要执行比`@EnableGlobalMethodSecurity`允许的注释更复杂的操作。对于这些实例,可以扩展`GlobalMethodSecurityConfiguration`,以确保子类上存在`@EnableGlobalMethodSecurity`注释。例如,如果希望提供自定义`MethodSecurityExpressionHandler`,则可以使用以下配置: + +爪哇 + +``` +@EnableGlobalMethodSecurity(prePostEnabled = true) +public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration { + @Override + protected MethodSecurityExpressionHandler createExpressionHandler() { + // ... create and return custom MethodSecurityExpressionHandler ... + return expressionHandler; + } +} +``` + +Kotlin + +``` +@EnableGlobalMethodSecurity(prePostEnabled = true) +open class MethodSecurityConfig : GlobalMethodSecurityConfiguration() { + override fun createExpressionHandler(): MethodSecurityExpressionHandler { + // ... create and return custom MethodSecurityExpressionHandler ... + return expressionHandler + } +} +``` + +有关可以重写的方法的其他信息,请参阅`GlobalMethodSecurityConfiguration`爪哇doc。 + +## \元素 + +此元素用于在应用程序中启用基于注释的安全性(通过在元素上设置适当的属性),还用于将安全性切入点声明组合在一起,这些声明将在整个应用程序上下文中应用。你应该只声明一个``元素。以下声明将支持 Spring Security 的`@Secured`: + +``` + +``` + +然后,向方法(在类或接口上)添加注释将相应地限制对该方法的访问。 Spring Security 的本机注释支持为该方法定义了一组属性。这些将被传递到`AccessDecisionManager`,以便它做出实际的决定: + +Java + +``` +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); +} +``` + +Kotlin + +``` +interface BankService { + @Secured("IS_AUTHENTICATED_ANONYMOUSLY") + fun readAccount(id: Long): Account + + @Secured("IS_AUTHENTICATED_ANONYMOUSLY") + fun findAccounts(): Array + + @Secured("ROLE_TELLER") + fun post(account: Account, amount: Double): Account +} +``` + +可以使用以下工具启用对 JSR-250 注释的支持 + +``` + +``` + +这些是基于标准的,允许应用简单的基于角色的约束,但不具有 Spring Security 的本机注释的功能。要使用新的基于表达式的语法,你可以使用 + +``` + +``` + +与之对应的 Java 代码是 + +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); +} +``` + +Kotlin + +``` +interface BankService { + @PreAuthorize("isAnonymous()") + fun readAccount(id: Long): Account + + @PreAuthorize("isAnonymous()") + fun findAccounts(): Array + + @PreAuthorize("hasAuthority('ROLE_TELLER')") + fun post(account: Account, amount: Double): Account +} +``` + +如果你需要定义简单的规则,而不是根据用户的权限列表检查角色名称,那么基于表达式的注释是一个不错的选择。 + +| |注释的方法将只对定义为 Spring bean 的实例(在启用方法安全性的相同应用程序上下文中)进行保护。
如果你想保护不是由 Spring 创建的实例(例如,使用`new`操作符),那么你需要使用 AspectJ。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |你可以在同一个应用程序中启用多个类型的注释,但是对于任何接口或类只应使用一种类型,因为在其他情况下,行为不会被很好地定义。,
如果发现两个注释适用于特定的方法,然后只应用其中的一个。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +## 使用 protect-pointcut 添加安全切入点 + +`protect-pointcut`的使用特别强大,因为它允许你只需简单的声明就可以将安全性应用于许多 bean。考虑以下示例: + +``` + + + +``` + +这将保护在应用程序上下文中声明的 bean 上的所有方法,这些 bean 的类在`com.mycompany`包中,其类名以“service”结尾。只有具有`ROLE_USER`角色的用户才能够调用这些方法。与 URL 匹配一样,最特定的匹配必须在切入点列表中排在第一位,因为将使用第一个匹配表达式。安全性注释优先于切入点。 + +[安全对象实现](secure-objects.html)[域对象安全 ACLS](acls.html) + diff --git a/docs/spring-security/servlet-authorization-secure-objects.md b/docs/spring-security/servlet-authorization-secure-objects.md new file mode 100644 index 0000000000000000000000000000000000000000..5fd0f20686383a0dbffebf05206bb1a6af3dd9f3 --- /dev/null +++ b/docs/spring-security/servlet-authorization-secure-objects.md @@ -0,0 +1,114 @@ +# 安全对象实现 + +## 安全拦截器 + +在 Spring Security2.0 之前,保护`MethodInvocation`s 需要相当多的锅炉板配置。现在推荐的方法安全性是使用[名称空间配置](../configuration/xml-namespace.html#ns-method-security)。通过这种方式,方法安全基础设施 bean 将自动为你配置,因此你实际上不需要了解实现类。我们将提供这里涉及的类的简要概述。 + +方法安全性是使用`MethodSecurityInterceptor`来实现的,它保护`MethodInvocation`s。根据配置方法,拦截器可以特定于单个 Bean 或在多个 bean 之间共享。拦截器使用`MethodSecurityMetadataSource`实例来获得配置属性应用于特定方法调用。`MapBasedMethodSecurityMetadataSource`用于存储由方法名(可以使用通配符)键控的配置属性,并且当在应用程序上下文中使用``或``元素定义属性时,将在内部使用。其他实现将用于处理基于注释的配置。 + +### 显式方法 SecurityInterceptor 配置 + +当然,你可以在应用程序上下文中直接配置`MethodSecurityInterceptor`,以便与 Spring AOP 的代理机制之一一起使用: + +``` + + + + + + + + + + + +``` + +## 安全拦截器 + +AspectJ 安全拦截器与上一节讨论的 AOP Alliance 安全拦截器非常相似。事实上,我们只在这一节讨论分歧。 + +AspectJ 拦截器被命名为`AspectJSecurityInterceptor`。 AOP Alliance Security Interceptor 依赖 Spring 应用程序上下文通过代理在安全拦截器中进行编织,而`AspectJSecurityInterceptor`则通过 AspectJ 编译器进行编织。在同一个应用程序中使用两种类型的安全拦截器并不少见,`AspectJSecurityInterceptor`用于域对象实例安全,而 AOP Alliance`MethodSecurityInterceptor`用于服务层安全。 + +让我们首先考虑在 Spring 应用程序上下文中如何配置`AspectJSecurityInterceptor`: + +``` + + + + + + + + + + + +``` + +正如你所看到的,除了类名,`AspectJSecurityInterceptor`与 AOP Alliance Security Interceptor 完全相同。实际上,这两个拦截器可以共享相同的`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,使用你认为合适的任何方法(例如`new Person();`),并且它们将应用安全拦截器。 + +[基于表达式的访问控制](expression-based.html)[方法安全性](method-security.html) + diff --git a/docs/spring-security/servlet-configuration-java.md b/docs/spring-security/servlet-configuration-java.md new file mode 100644 index 0000000000000000000000000000000000000000..00a3eb412f661977f1a34d096f0d14bda16ba069 --- /dev/null +++ b/docs/spring-security/servlet-configuration-java.md @@ -0,0 +1,322 @@ +# Java 配置 + +在 Spring 3.1 中,对[Java 配置](https://docs.spring.io/spring/docs/3.1.x/spring-framework-reference/html/beans.html#beans-java)的一般支持被添加到 Spring 框架中。自 Spring Security3.2 以来,已经有了 Spring Security Java 配置支持,它使用户能够轻松地配置 Spring 安全性,而无需使用任何 XML。 + +如果你熟悉[安全名称空间配置](xml-namespace.html#ns-config),那么你应该会发现它与安全 Java 配置支持之间有很多相似之处。 + +| |Spring 安全性提供了[大量的示例应用程序](https://github.com/spring-projects/spring-security-samples/tree/main/servlet/java-configuration),其中演示了 Spring 安全性 Java 配置的使用。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +## Hello Web 安全 Java 配置 + +第一步是创建我们的 Spring 安全 Java 配置。该配置创建了一个名为`springSecurityFilterChain`的 Servlet 过滤器,该过滤器负责应用程序中的所有安全性(保护应用程序的 URL,验证提交的用户名和密码,重定向到表单中的日志,等等)。你可以在下面找到 Spring 安全性 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 进行身份验证 + +* 为你生成一个登录表单 + +* 允许使用**用户 Name***User*和**密码***密码*的用户使用基于表单的身份验证进行身份验证 + +* 允许用户注销 + +* [CSRF 攻击](https://en.wikipedia.org/wiki/Cross-site_request_forgery)预防 + +* [Session Fixation](https://en.wikipedia.org/wiki/Session_fixation)保护 + +* 安全报头集成 + + * [HTTP 严格的传输安全](https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security)用于安全请求 + + * [X-Content-Type-Options](https://msdn.microsoft.com/en-us/library/ie/gg622941(v=vs.85).ASPX)集成 + + * 缓存控制(可以稍后由应用程序重写,以允许缓存静态资源) + + * [X-XSS-保护](https://msdn.microsoft.com/en-us/library/dd565647(v=vs.85).ASPX)积分 + + * x-frame-options 集成,帮助防止[点击劫持](https://en.wikipedia.org/wiki/Clickjacking) + +* 与以下 Servlet API 方法集成 + + * [HttpServletRequest#getRemoteUser()](https://docs.oracle.com/javaee/6/api/javax/servlet/http/HttpServletRequest.html#getRemoteUser()) + + * [HttpServletRequest#getUserPrincipal()](https://docs.oracle.com/javaee/6/api/javax/servlet/http/HttpServletRequest.html#getUserPrincipal()) + + * [HttpServletRequest#isUserInRole](https://docs.oracle.com/javaee/6/api/javax/servlet/http/HttpServletRequest.html#isUserInRole(java.lang.String)) + + * [HttpServletRequest#login(java.lang.string,java.lang.string)](https://docs.oracle.com/javaee/6/api/javax/servlet/http/HttpServletRequest.html#login(java.lang.String,%20java.lang.String)) + + * [HttpServletRequest#logout()](https://docs.oracle.com/javaee/6/api/javax/servlet/http/HttpServletRequest.html#logout()) + +### WebApplicationInitializer + +下一步是在 WAR 中注册`springSecurityFilterChain`。这可以在 Servlet 3.0+ 环境中使用[Spring’s WebApplicationInitializer support](https://docs.spring.io/spring/docs/3.2.x/spring-framework-reference/html/mvc.html#mvc-container-config)在 Java 配置中完成。 Spring Security 提供了一个基类`AbstractSecurityWebApplicationInitializer`,它将确保`springSecurityFilterChain`为你注册。我们使用`AbstractSecurityWebApplicationInitializer`的方式有所不同,这取决于我们是否已经在使用 Spring 或者 Spring 安全性是我们应用程序中唯一的 Spring 组件。 + +* [不存在的安全 WebApplicationInitializer Spring](#abstractsecuritywebapplicationinitializer-without-existing-spring)-如果你尚未使用 Spring,请使用以下说明 + +* [带有 Spring MVC 的安全 WebApplicationInitializer](#abstractsecuritywebapplicationinitializer-with-spring-mvc)-如果你已经在使用这些指令 Spring + +### AbstractSecurityWebApplicationInitializer without Existing Spring + +如果不使用 Spring 或 Spring MVC,则需要将`WebSecurityConfig`传递到超类中,以确保拾取配置。你可以在下面找到一个例子: + +``` +import org.springframework.security.web.context.*; + +public class SecurityWebApplicationInitializer + extends AbstractSecurityWebApplicationInitializer { + + public SecurityWebApplicationInitializer() { + super(WebSecurityConfig.class); + } +} +``` + +`SecurityWebApplicationInitializer`将执行以下操作: + +* 为应用程序中的每个 URL 自动注册 SpringSecurityFilterchain 过滤器 + +* 添加一个加载[WebSecurityConfig](#jc-hello-wsca)的 ContextLoaderListener。 + +### AbstractSecurityWebApplicationInitializer with Spring MVC + +如果我们在应用程序的其他地方使用 Spring,我们可能已经有一个`WebApplicationInitializer`正在加载我们的 Spring 配置。如果我们使用以前的配置,我们将得到一个错误。相反,我们应该使用现有的`ApplicationContext`注册 Spring 安全性。例如,如果我们使用 Spring MVC,我们的`SecurityWebApplicationInitializer`将如下所示: + +``` +import org.springframework.security.web.context.*; + +public class SecurityWebApplicationInitializer + extends AbstractSecurityWebApplicationInitializer { + +} +``` + +这只会为应用程序中的每个 URL 注册 SpringSecurityFilterchain 过滤器。在此之后,我们将确保`WebSecurityConfig`已加载到我们现有的应用程序初始化器中。例如,如果我们使用 Spring MVC,它将被添加到`getRootConfigClasses()`中 + +``` +public class MvcWebApplicationInitializer extends + AbstractAnnotationConfigDispatcherServletInitializer { + + @Override + protected Class[] getRootConfigClasses() { + return new Class[] { WebSecurityConfig.class }; + } + + // ... other overrides ... +} +``` + +## HttpSecurity + +到目前为止,我们的[WebSecurityConfig](#jc-hello-wsca)仅包含有关如何对用户进行身份验证的信息。 Spring 安全性如何知道我们希望要求对所有用户进行身份验证? Spring 安全性如何知道我们希望支持基于表单的身份验证?实际上,有一个正在幕后调用的配置类叫做`WebSecurityConfigurerAdapter`。它有一个名为`configure`的方法,其默认实现如下: + +``` +protected void configure(HttpSecurity http) throws Exception { + http + .authorizeRequests(authorize -> authorize + .anyRequest().authenticated() + ) + .formLogin(withDefaults()) + .httpBasic(withDefaults()); +} +``` + +上面的默认配置: + +* 确保对我们的应用程序的任何请求都需要对用户进行身份验证。 + +* 允许用户通过基于表单的登录进行身份验证 + +* 允许用户使用 HTTP Basic 身份验证进行身份验证 + +你将注意到此配置与 XML 名称空间配置非常相似: + +``` + + + + + +``` + +## 多重 HttpSecurity + +我们可以配置多个 HttpSecurity 实例,就像我们可以配置多个``块一样。关键是将`WebSecurityConfigurerAdapter`多次扩展。例如,下面是一个以`/api/`开头的不同 URL 配置的示例。 + +``` +@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) + .authorizeHttpRequests(authorize -> authorize + .anyRequest().hasRole("ADMIN") + ) + .httpBasic(withDefaults()); + } + } + + @Configuration (4) + public static class FormLoginWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .authorizeHttpRequests(authorize -> authorize + .anyRequest().authenticated() + ) + .formLogin(withDefaults()); + } + } +} +``` + +|**1**|将身份验证配置为常规身份验证| +|-----|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +|**2**|创建一个包含`@Order`的`WebSecurityConfigurerAdapter`实例,以指定应该首先考虑哪个`WebSecurityConfigurerAdapter`。| +|**3**|`http.antMatcher`声明此`HttpSecurity`将仅适用于以`/api/`开头的 URL| +|**4**|创建`WebSecurityConfigurerAdapter`的另一个实例。
如果 URL 不以`/api/`开始,将使用此配置。
此配置在`ApiWebSecurityConfigurationAdapter`之后被考虑,因为它在`1`之后有一个`@Order`值(不`@Order`默认为后)。| + +## 定制 DSL + +你可以在 Spring Security 中提供自己的自定义 DSL。例如,你可能有这样的东西: + +``` +public class MyCustomDsl extends AbstractHttpConfigurer { + private boolean flag; + + @Override + public void init(HttpSecurity http) throws Exception { + // any method that adds another configurer + // must be done in the init method + http.csrf().disable(); + } + + @Override + public void configure(HttpSecurity 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`s configure 方法中的代码 + +* 调用`MyCustomDsl`s init 方法中的代码 + +* 调用`MyCustomDsl`s configure 方法中的代码 + +如果需要,可以使用`SpringFactories`,在默认情况下将`WebSecurityConfigurerAdapter`添加到`MyCustomDsl`。例如,你将在 Classpath 上创建一个名为`META-INF/ Spring.工厂`的资源,其内容如下: + +META-INF/spring.factories + +``` +org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer = sample.MyCustomDsl +``` + +希望禁用默认值的用户可以显式地这样做。 + +``` +@EnableWebSecurity +public class Config extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .apply(customDsl()).disable() + ...; + } +} +``` + +## 已配置对象的后处理 + +Spring Security 的 Java 配置不会公开它配置的每个对象的每个属性。这简化了大多数用户的配置。毕竟,如果每个属性都被公开,那么用户可以使用标准的 Bean 配置。 + +尽管有充分的理由不直接公开每个属性,但用户可能仍然需要更高级的配置选项。 Spring 为了解决这一安全问题,引入了`ObjectPostProcessor`的概念,该概念可用于修改或替换由 Java 配置创建的许多对象实例。例如,如果要在`FilterSecurityInterceptor`上配置`filterSecurityPublishAuthorizationSuccess`属性,可以使用以下方法: + +``` +@Override +protected void configure(HttpSecurity http) throws Exception { + http + .authorizeRequests(authorize -> authorize + .anyRequest().authenticated() + .withObjectPostProcessor(new ObjectPostProcessor() { + public O postProcess( + O fsi) { + fsi.setPublishAuthorizationSuccess(true); + return fsi; + } + }) + ); +} +``` + +[JSP Taglib](../integrations/jsp-taglibs.html)[Kotlin Configuration](kotlin.html) + diff --git a/docs/spring-security/servlet-configuration-kotlin.md b/docs/spring-security/servlet-configuration-kotlin.md new file mode 100644 index 0000000000000000000000000000000000000000..6e5b4be50634ab85ffd644350683796001ca9e2b --- /dev/null +++ b/docs/spring-security/servlet-configuration-kotlin.md @@ -0,0 +1,91 @@ +# Kotlin 配置 + +| |Spring 安全性提供了[示例应用程序](https://github.com/spring-projects/spring-security-samples/tree/main/servlet/spring-boot/kotlin/hello-security),这演示了 Spring 安全性 Kotlin 配置的使用。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +## HttpSecurity + +Spring 安全性如何知道我们希望要求对所有用户进行身份验证? Spring 安全性如何知道我们希望支持基于表单的身份验证?有一个正在幕后调用的配置类,名为`WebSecurityConfigurerAdapter`。它有一个名为`configure`的方法,其默认实现如下: + +``` +fun configure(http: HttpSecurity) { + http { + authorizeRequests { + authorize(anyRequest, authenticated) + } + formLogin { } + httpBasic { } + } +} +``` + +上面的默认配置: + +* 确保对我们的应用程序的任何请求都需要对用户进行身份验证。 + +* 允许用户通过基于表单的登录进行身份验证 + +* 允许用户使用 HTTP Basic 身份验证进行身份验证 + +你将注意到此配置与 XML 名称空间配置非常相似: + +``` + + + + + +``` + +## 多重 HttpSecurity + +我们可以配置多个 HttpSecurity 实例,就像我们可以配置多个``块一样。关键是将`WebSecurityConfigurerAdapter`多次扩展。例如,下面是一个以`/api/`开头的不同 URL 配置的示例。 + +``` +@EnableWebSecurity +class MultiHttpSecurityConfig { + @Bean (1) + public fun userDetailsService(): UserDetailsService { + val users: User.UserBuilder = User.withDefaultPasswordEncoder() + val manager = 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) + class ApiWebSecurityConfigurationAdapter: WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + securityMatcher("/api/**") (3) + authorizeRequests { + authorize(anyRequest, hasRole("ADMIN")) + } + httpBasic { } + } + } + } + + @Configuration (4) + class FormLoginWebSecurityConfigurerAdapter: WebSecurityConfigurerAdapter() { + override fun configure(http: HttpSecurity) { + http { + authorizeRequests { + authorize(anyRequest, authenticated) + } + formLogin { } + } + } + } +} +``` + +|**1**|将身份验证配置为常规身份验证| +|-----|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +|**2**|创建一个包含`@Order`的`WebSecurityConfigurerAdapter`实例,以指定应该首先考虑哪个`WebSecurityConfigurerAdapter`。| +|**3**|`http.antMatcher`声明此`HttpSecurity`将仅适用于以`/api/`开头的 URL| +|**4**|创建`WebSecurityConfigurerAdapter`的另一个实例。
如果 URL 不以`/api/`开始,将使用此配置。
此配置在`ApiWebSecurityConfigurationAdapter`之后被考虑,因为它在`1`之后有一个`@Order`值(没有`@Order`默认为最后)。| + +[Java 配置](java.html)[名称空间配置](xml-namespace.html) +