13. Servlet 环境的 WebClient

Note

以下文档适用于 Servlet 环境。对于所有其他环境,请参考WebClient 反应式环境。

Spring Framework 内置了对设置 Bearer 令牌的支持。

webClient.get()
    .headers(h -> h.setBearerAuth(token))
    ...

Spring Security 在此支持的基础上提供了其他好处:

  • Spring Security 将自动刷新过期的令牌(如果存在刷新令牌)

  • 如果请求访问令牌但不存在,则 Spring Security 将自动请求访问令牌。

  • 对于 authorization_code,这涉及执行重定向,然后重播原始请求

    • 对于 client_credentials,只需请求并保存令牌
  • 支持透明包含当前 OAuth 令牌或明确选择应使用的令牌的功能。

13.1 WebClient OAuth2 设置

第一步是确保正确设置WebClient。在 servlet 环境中设置WebClient的示例可以在下面找到:

@Bean
WebClient webClient(ClientRegistrationRepository clientRegistrations,
        OAuth2AuthorizedClientRepository authorizedClients) {
    ServletOAuth2AuthorizedClientExchangeFilterFunction oauth =
            new ServletOAuth2AuthorizedClientExchangeFilterFunction(clientRegistrations, authorizedClients);
    // (optional) explicitly opt into using the oauth2Login to provide an access token implicitly
    // oauth.setDefaultOAuth2AuthorizedClient(true);
    // (optional) set a default ClientRegistration.registrationId
    // oauth.setDefaultClientRegistrationId("client-registration-id");
    return WebClient.builder()
            .apply(oauth2.oauth2Configuration())
            .build();
}

13.2 隐式 OAuth2AuthorizedClient

如果我们在设置中将defaultOAuth2AuthorizedClient设置为true并使用 oauth2Login(即 OIDC)进行了身份验证,则当前身份验证将用于自动提供访问令牌。另外,如果我们将defaultClientRegistrationId设置为有效的ClientRegistration id,则该注册将用于提供访问令牌。这很方便,但是在并非所有端点都应获取访问令牌的环境中,这样做很危险(您可能为端点提供了错误的访问令牌)。

Mono<String> body = this.webClient
        .get()
        .uri(this.uri)
        .retrieve()
        .bodyToMono(String.class);

13.3 显式 OAuth2AuthorizedClient

OAuth2AuthorizedClient可以通过在请求属性上设置来明确提供。在下面的示例中,我们使用 Spring WebFlux 或 Spring MVC 参数解析器支持来解析OAuth2AuthorizedClient。但是,OAuth2AuthorizedClient的解析方式无关紧要。

@GetMapping("/explicit")
Mono<String> explicit(@RegisteredOAuth2AuthorizedClient("client-id") OAuth2AuthorizedClient authorizedClient) {
    return this.webClient
            .get()
            .uri(this.uri)
            .attributes(oauth2AuthorizedClient(authorizedClient))
            .retrieve()
            .bodyToMono(String.class);
}

13.4 clientRegistrationId

或者,可以在请求属性上指定clientRegistrationId,而WebClient将尝试查找OAuth2AuthorizedClient。如果找不到,将自动获取一个。

Mono<String> body = this.webClient
        .get()
        .uri(this.uri)
        .attributes(clientRegistrationId("client-id"))
        .retrieve()
        .bodyToMono(String.class);

13.5 JSP 标记库

Spring Security 有自己的 taglib,它为访问安全信息和在 JSP 中应用安全约束提供了基本的支持。

13.5.1 声明 Taglib

要使用任何标记,必须在 JSP 中声明安全标记库:

<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %>

13.5.2 授权标签

该标签用于确定是否应评估其内容。在 Spring Security 3.0 中,可以两种方式使用[21]。第一种方法使用在标记的access属性中指定的web-security expression。表达式评估将委派给在应用程序上下文中定义的SecurityExpressionHandler<FilterInvocation>(您应在<http>命名空间配置中启用 Web 表达式,以确保该服务可用)。因此,例如,您可能有

<sec:authorize access="hasRole('supervisor')">

This content will only be visible to users who have the "supervisor" authority in their list of <tt>GrantedAuthority</tt>s.

</sec:authorize>

当与 Spring Security 的 PermissionEvaluator 结合使用时,该标签还可用于检查权限。例如:

<sec:authorize access="hasPermission(#domain,'read') or hasPermission(#domain,'write')">

This content will only be visible to users who have read or write permission to the Object found as a request attribute named "domain".

</sec:authorize>

通常的要求是,如果实际上允许用户单击某个链接,则仅显示该链接。我们如何预先确定是否允许某事?该标记还可以在替代模式下运行,该模式允许您将特定的 URL 定义为属性。如果允许用户调用该 URL,则将评估标记主体,否则将跳过该标记主体。所以你可能会喜欢

<sec:authorize url="/admin">

This content will only be visible to users who are authorized to send requests to the "/admin" URL.

</sec:authorize>

要使用此标记,您的应用程序上下文中还必须有一个WebInvocationPrivilegeEvaluator的实例。如果使用名称空间,将自动注册一个名称空间。这是DefaultWebInvocationPrivilegeEvaluator的实例,该实例为提供的 URL 创建一个虚拟 Web 请求并调用安全拦截器以查看该请求成功还是失败。这使您可以委派给使用<http>命名空间配置中的intercept-url声明定义的访问控制设置,并且省去了在 JSP 中重复信息(例如所需角色)的麻烦。此方法也可以与提供 HTTP 方法的method属性结合使用,以进行更具体的匹配。

通过将var属性设置为变量名称,可以将评估标记(无论是授予还是拒绝访问)的布尔结果存储在页面上下文范围变量中,而无需在复制代码的其他点重复和重新评估条件。页。

禁用测试的标签授权

在页面上为未经授权的用户隐藏链接不会阻止他们访问 URL。例如,他们可以直接将其键入浏览器中。在测试过程中,您可能希望显示隐藏区域,以检查链接在后端是否 true 固定。如果将系统属性spring.security.disableUISecurity设置为true,则authorize标记仍将运行,但不会隐藏其内容。默认情况下,它还将用<span class="securityHiddenUI">…</span>标签包围内容。这使您可以显示具有特定 CSS 样式(例如不同的背景颜色)的“隐藏”内容。例如,尝试在启用此属性的情况下运行“教程”示例应用程序。

如果要更改默认span标记中的周围文本(或使用空字符串将其完全删除),还可以设置属性spring.security.securedUIPrefixspring.security.securedUISuffix

13.5.3 身份验证标签

该标签允许访问存储在安全上下文中的当前Authentication对象。它直接在 JSP 中呈现对象的属性。因此,例如,如果Authenticationprincipal属性是 Spring Security 的UserDetails对象的实例,则使用<sec:authentication property="principal.username" />将呈现当前用户的名称。

当然,对于这种事情,不必使用 JSP 标记,并且某些人希望在视图中保持尽可能少的逻辑。您可以访问 MVC 控制器中的Authentication对象(通过调用SecurityContextHolder.getContext().getAuthentication()),并将数据直接添加到模型中以通过视图进行渲染。

13.5.4 accesscontrollist 标记

该标签仅在与 Spring Security 的 ACL 模块一起使用时才有效。它检查以逗号分隔的指定域对象的所需权限列表。如果当前用户拥有所有这些权限,则将评估标签正文。如果他们不这样做,它将被跳过。一个例子可能是

Warning

通常,应将此标签视为已弃用。而是使用第 13.5.2 节“授权标签”

<sec:accesscontrollist hasPermission="1,2" domainObject="${someObject}">

This will be shown if the user has all of the permissions represented by the values "1" or "2" on the given object.

</sec:accesscontrollist>

权限被传递到应用程序上下文中定义的PermissionFactory,将它们转换为 ACL Permission实例,因此它们可以是工厂支持的任何格式-它们不必是整数,它们可以是READWRITE之类的字符串。如果未找到PermissionFactory,将使用DefaultPermissionFactory的实例。应用程序上下文中的AclService将用于为所提供的对象加载Acl实例。将使用所需的权限调用Acl,以检查是否所有这些权限都被授予。

该标签还支持var属性,与authorize标签相同。

13.5.5 csrfInput 标签

如果启用了 CSRF 保护,则此标记将插入一个隐藏的表单字段,其中包含 CSRF 保护令牌的正确名称和值。如果未启用 CSRF 保护,则此标签不输出任何内容。

通常,Spring Security 会为您使用的任何<form:form>标签自动插入一个 CSRF 表单字段,但是如果由于某种原因您不能使用<form:form>,则csrfInput可以方便地替换。

您应该将此标记放置在 HTML <form></form>块中,通常将其放置在其他 Importing 字段中。请勿将此标签放在 Spring <form:form></form:form>块中。 Spring Security 自动处理 Spring 表单。

<form method="post" action="/do/something">
    <sec:csrfInput />
    Name:
<input type="text" name="name" /> ... </form>

13.5.6 csrfMetaTags 标签

如果启用了 CSRF 保护,则此标记将插入包含 CSRF 保护令牌形式字段,Headers 名称和 CSRF 保护令牌值的元标记。这些元标记对于在应用程序中的 JavaScript 中采用 CSRF 保护很有用。

您应将csrfMetaTags放置在 HTML <head></head>块中,通常将其放置在其他 meta 标签中。使用此标记后,您可以使用 JavaScript 轻松访问表单字段名称,标题名称和令牌值。在此示例中,使用 JQuery 简化了任务。

<!DOCTYPE html>
<html>
    <head>
        <title>CSRF Protected JavaScript Page</title>
        <meta name="description" content="This is the description for this page" />
        <sec:csrfMetaTags />
        <script type="text/javascript" language="javascript">

            var csrfParameter = $("meta[name='_csrf_parameter']").attr("content");
            var csrfHeader = $("meta[name='_csrf_header']").attr("content");
            var csrfToken = $("meta[name='_csrf']").attr("content");

            // using XMLHttpRequest directly to send an x-www-form-urlencoded request
            var ajax = new XMLHttpRequest();
            ajax.open("POST", "http://www.example.org/do/something", true);
            ajax.setRequestHeader("Content-Type", "application/x-www-form-urlencoded data");
            ajax.send(csrfParameter + "=" + csrfToken + "&name=John&...");

            // using XMLHttpRequest directly to send a non-x-www-form-urlencoded request
            var ajax = new XMLHttpRequest();
            ajax.open("POST", "http://www.example.org/do/something", true);
            ajax.setRequestHeader(csrfHeader, csrfToken);
            ajax.send("...");

            // using JQuery to send an x-www-form-urlencoded request
            var data = {};
            data[csrfParameter] = csrfToken;
            data["name"] = "John";
            ...
            $.ajax({
                url: "http://www.example.org/do/something",
                type: "POST",
                data: data,
                ...
            });

            // using JQuery to send a non-x-www-form-urlencoded request
            var headers = {};
            headers[csrfHeader] = csrfToken;
            $.ajax({
                url: "http://www.example.org/do/something",
                type: "POST",
                headers: headers,
                ...
            });

        <script>
    </head>
    <body>
        ...
    </body>
</html>

如果未启用 CSRF 保护,则csrfMetaTags不输出任何内容。

13.6 Java 身份验证和授权服务(JAAS)提供程序

13.6.1 Overview

Spring Security 提供了一个程序包,可以将身份验证请求委派给 Java 身份验证和授权服务(JAAS)。该软件包将在下面详细讨论。

13.6.2 AbstractJaasAuthenticationProvider

AbstractJaasAuthenticationProvider是所提供的 JAAS AuthenticationProvider实现的基础。子类必须实现创建LoginContext的方法。 AbstractJaasAuthenticationProvider具有许多可以注入到其中的依赖项,下面将对此进行讨论。

JAAS CallbackHandler

大多数 JAAS LoginModule都需要某种回调。这些回调通常用于从用户获取用户名和密码。

在 Spring Security 部署中,Spring Security 负责此用户交互(通过身份验证机制)。因此,在将身份验证请求委托给 JAAS 时,Spring Security 的身份验证机制将已经完全填充了Authentication对象,其中包含 JAAS LoginModule所需的所有信息。

因此,Spring Security 的 JAAS 包提供了两个默认的回调处理程序JaasNameCallbackHandlerJaasPasswordCallbackHandler。这些回调处理程序中的每一个都实现JaasAuthenticationCallbackHandler。在大多数情况下,无需了解内部机制即可简单地使用这些回调处理程序。

对于需要完全控制回调行为的用户,内部AbstractJaasAuthenticationProvider将这些JaasAuthenticationCallbackHandler换成InternalCallbackHandlerInternalCallbackHandler是实际实现 JAAS 常规CallbackHandler接口的类。每当使用 JAAS LoginModule时,都会向其传递配置了InternalCallbackHandler的应用程序上下文列表。如果LoginModule请求针对InternalCallbackHandler的回调,则该回调又传递给正在包装的JaasAuthenticationCallbackHandler

JAAS AuthorityGranter

JAAS 与校长合作。 JAAS 中甚至将“角色”表示为主体。另一方面,Spring Security 可以处理Authentication个对象。每个Authentication对象包含一个主体和多个GrantedAuthority。为了促进这些不同概念之间的 Map,Spring Security 的 JAAS 软件包包括一个AuthorityGranter接口。

AuthorityGranter负责检查 JAAS 委托人并返回一组String,代表分配给委托人的权限。对于每个返回的授权字符串,AbstractJaasAuthenticationProvider创建一个JaasGrantedAuthority(它实现 Spring Security 的GrantedAuthority接口),其中包含授权字符串和AuthorityGranter传递的 JAAS 主体。 AbstractJaasAuthenticationProvider首先通过使用 JAAS LoginModule成功验证用户的凭据,然后访问它返回的LoginContext,从而获得 JAAS 主体。调用LoginContext.getSubject().getPrincipals(),并将每个结果主体传递给针对AbstractJaasAuthenticationProvider.setAuthorityGranters(List)属性定义的每个AuthorityGranter

鉴于每个 JAAS 主体都具有特定于实现的含义,因此 Spring Security 不包含任何生产AuthorityGranter。但是,单元测试中有一个TestAuthorityGranter演示了一个简单的AuthorityGranter实现。

13.6.3 DefaultJaasAuthenticationProvider

DefaultJaasAuthenticationProvider允许将 JAAS Configuration对象作为依赖项注入到该对象中。然后,使用注入的 JAAS Configuration创建一个LoginContext。这意味着DefaultJaasAuthenticationProviderJaasAuthenticationProvider一样不受Configuration的任何特定实现的约束。

InMemoryConfiguration

为了便于将Configuration注入DefaultJaasAuthenticationProvider,提供了默认的内存实现InMemoryConfiguration。实现构造函数接受一个Map,其中每个键代表一个登录配置名称,该值代表一个AppConfigurationEntryArrayInMemoryConfiguration还支持AppConfigurationEntry对象的默认Array,如果在提供的Map中找不到 Map,则将使用它们。有关详细信息,请参考InMemoryConfiguration的类级别 javadoc。

DefaultJaasAuthenticationProvider 示例配置

尽管InMemoryConfiguration的 Spring 配置比标准 JAAS 配置文件更冗长,但与DefaultJaasAuthenticationProvider结合使用时,它比JaasAuthenticationProvider更灵活,因为它不依赖于默认的Configuration实现。

以下提供了使用InMemoryConfigurationDefaultJaasAuthenticationProvider的示例配置。请注意,Configuration的自定义实现也可以轻松地注入DefaultJaasAuthenticationProvider

<bean id="jaasAuthProvider"
class="org.springframework.security.authentication.jaas.DefaultJaasAuthenticationProvider">
<property name="configuration">
<bean class="org.springframework.security.authentication.jaas.memory.InMemoryConfiguration">
<constructor-arg>
    <map>
    <!--
    SPRINGSECURITY is the default loginContextName
    for AbstractJaasAuthenticationProvider
    -->
    <entry key="SPRINGSECURITY">
    <array>
    <bean class="javax.security.auth.login.AppConfigurationEntry">
        <constructor-arg value="sample.SampleLoginModule" />
        <constructor-arg>
        <util:constant static-field=
            "javax.security.auth.login.AppConfigurationEntry$LoginModuleControlFlag.REQUIRED"/>
        </constructor-arg>
        <constructor-arg>
        <map></map>
        </constructor-arg>
        </bean>
    </array>
    </entry>
    </map>
    </constructor-arg>
</bean>
</property>
<property name="authorityGranters">
<list>
    <!-- You will need to write your own implementation of AuthorityGranter -->
    <bean class="org.springframework.security.authentication.jaas.TestAuthorityGranter"/>
</list>
</property>
</bean>

13.6.4 JaasAuthenticationProvider

JaasAuthenticationProvider假定默认的ConfigurationConfigFile的实例。进行此假设是为了尝试更新ConfigurationJaasAuthenticationProvider然后使用默认的Configuration创建LoginContext

假设我们有一个 JAAS 登录配置文件/WEB-INF/login.conf,其内容如下:

JAASTest {
    sample.SampleLoginModule required;
};

像所有 Spring Security bean 一样,JaasAuthenticationProvider是通过应用程序上下文配置的。以下定义将对应于上述 JAAS 登录配置文件:

<bean id="jaasAuthenticationProvider"
class="org.springframework.security.authentication.jaas.JaasAuthenticationProvider">
<property name="loginConfig" value="/WEB-INF/login.conf"/>
<property name="loginContextName" value="JAASTest"/>
<property name="callbackHandlers">
<list>
<bean
    class="org.springframework.security.authentication.jaas.JaasNameCallbackHandler"/>
<bean
    class="org.springframework.security.authentication.jaas.JaasPasswordCallbackHandler"/>
</list>
</property>
<property name="authorityGranters">
    <list>
    <bean class="org.springframework.security.authentication.jaas.TestAuthorityGranter"/>
    </list>
</property>
</bean>

13.6.5 以主题身份运行

如果已配置,则JaasApiIntegrationFilter将尝试在JaasAuthenticationToken上以Subject的身份运行。这意味着可以使用以下命令访问Subject

Subject subject = Subject.getSubject(AccessController.getContext());

可以使用jaas-api-provision属性轻松配置此集成。与依赖于填充 JAAS 主题的旧版或外部 API 集成时,此功能很有用。

13.7 CAS 验证

13.7.1 Overview

JA-SIG 生成了企业范围的单点登录系统,称为 CAS。与其他计划不同,JA-SIG 的中央身份验证服务是开放源代码,广泛使用,易于理解,独立于平台并支持代理功能。 Spring Security 完全支持 CAS,并提供了从 Spring Security 的单应用程序部署到由企业范围的 CAS 服务器保护的多应用程序部署的简便迁移路径。

您可以在http://www.ja-sig.org/cas上了解有关 CAS 的更多信息。您还需要访问此站点以下载 CAS Server 文件。

13.7.2 CAS 的工作方式

尽管 CAS 网站包含详细介绍 CAS 体系结构的文档,但我们还是在 Spring Security 的上下文中再次介绍了总体概述。 Spring Security 3.x 支持 CAS3.在撰写本文时,CAS 服务器的版本为 3.4.

您需要在企业中的某个位置设置 CAS 服务器。 CAS 服务器只是一个标准的 WAR 文件,因此设置服务器没有任何困难。在 WAR 文件中,您将自定义显示给用户的登录页面和其他单一登录页面。

部署 CAS 3.4 服务器时,您还需要在 CAS 随附的deployerConfigContext.xml中指定AuthenticationHandlerAuthenticationHandler有一个简单的方法,该方法返回有关给定凭据集是否有效的布尔值。您的AuthenticationHandler实现将需要链接到某种类型的后端身份验证存储库,例如 LDAP 服务器或数据库。 CAS 本身提供了许多AuthenticationHandler来辅助此操作。在下载和部署服务器 war 文件时,该文件将设置为成功验证 Importing 与用户名匹配的密码的用户的身份,这对于测试非常有用。

除了 CAS 服务器本身之外,其他关键参与者当然是整个企业中部署的安全 Web 应用程序。这些 Web 应用程序称为“服务”。有三种类型的服务。那些对服务票证进行身份验证的人,那些可以获取代理票证的人以及那些对代理票证进行身份验证的人。验证代理票证的方式有所不同,因为必须验证代理列表,并且通常可以重复使用代理票证。

Spring Security 和 CAS 交互 Sequences

Web 浏览器,CAS 服务器和受 Spring Security 保护的服务之间的基本交互如下:

  • Web 用户正在浏览服务的公共页面。不涉及 CAS 或 Spring Security。

  • 用户最终请求的页面是安全的,或者它使用的其中一个 bean 是安全的。 Spring Security 的ExceptionTranslationFilter将检测到AccessDeniedExceptionAuthenticationException

  • 由于用户的Authentication对象(或缺少对象)导致了AuthenticationException,因此ExceptionTranslationFilter将调用已配置的AuthenticationEntryPoint。如果使用 CAS,则为CasAuthenticationEntryPoint类。

  • CasAuthenticationEntryPoint将用户的浏览器重定向到 CAS 服务器。它还将指示service参数,这是 Spring Security 服务(您的应用程序)的回调 URL。例如,浏览器重定向到的 URL 可能是https://my.company.com/cas/login?service=https%3A%2F%2Fserver3.company.com%2Fwebapp%2Flogin/cas

  • 用户浏览器重定向到 CAS 后,将提示他们 Importing 用户名和密码。如果用户提供的会话 cookie 指示他们先前已登录,则不会提示他们再次登录(此过程有一个 exception,稍后我们将进行介绍)。 CAS 将使用上面讨论的PasswordHandler(如果使用 CAS 3.0,则为AuthenticationHandler)来决定用户名和密码是否有效。

  • 成功登录后,CAS 会将用户的浏览器重定向回原始服务。它还将包含ticket参数,该参数是代表“服务票证”的不透明字符串。continue 前面的示例,浏览器重定向到的 URL 可能是https://server3.company.com/webapp/login/cas?ticket=ST-0-ER94xMJmn6pha35CQRoZ

  • 回到服务 Web 应用程序中,CasAuthenticationFilter始终在侦听对/login/cas的请求(这是可配置的,但是在本简介中将使用默认值)。处理过滤器将构造代表服务票证的UsernamePasswordAuthenticationToken。主体将等于CasAuthenticationFilter.CAS_STATEFUL_IDENTIFIER,而凭据将是服务票证的不透明值。然后,此身份验证请求将移交给已配置的AuthenticationManager

  • AuthenticationManager实现将是ProviderManager,而ProviderManager则配置为CasAuthenticationProviderCasAuthenticationProvider仅响应UsernamePasswordAuthenticationToken,其中包含 CAS 特定的主体(例如CasAuthenticationFilter.CAS_STATEFUL_IDENTIFIER)和CasAuthenticationToken s(稍后讨论)。

  • CasAuthenticationProvider将使用TicketValidator实现来验证服务票证。通常为Cas20ServiceTicketValidator,它是 CASClient 端库中包含的类之一。如果应用程序需要验证代理票证,则使用Cas20ProxyTicketValidatorTicketValidator向 CAS 服务器发出 HTTPS 请求,以验证服务票证。它还可能包含代理回调 URL,此示例中包括该 URL:https://my.company.com/cas/proxyValidate?service=https%3A%2F%2Fserver3.company.com%2Fwebapp%2Flogin/cas&ticket=ST-0-ER94xMJmn6pha35CQRoZ&pgtUrl=https://server3.company.com/webapp/login/cas/proxyreceptor

  • 返回 CAS 服务器,将收到验证请求。如果提供的服务票证与票证签发到的服务 URL 相匹配,则 CAS 将以 XML 形式提供肯定的响应,指示用户名。如果身份验证中涉及任何代理(如下所述),则代理列表也包含在 XML 响应中。

  • [可选]如果对 CAS 验证服务的请求包含代理回调 URL(在pgtUrl参数中),则 CAS 将在 XML 响应中包含pgtIou字符串。 pgtIou代表授权代理凭单 IOU。然后,CAS 服务器将创建自己的 HTTPS 连接回到pgtUrl。这是为了相互认证 CAS 服务器和要求保护的服务 URL。 HTTPS 连接将用于将代理授予票证发送到原始 Web 应用程序。例如https://server3.company.com/webapp/login/cas/proxyreceptor?pgtIou=PGTIOU-0-R0zlgrl4pdAQwBvJWO3vnNpevwqStbSGcq3vKB2SqSFFRnjPHt&pgtId=PGT-1-si9YkkHLrtACBo64rmsi3v2nf7cpCResXg5MpESZFArbaZiOKH

  • Cas20TicketValidator将解析从 CAS 服务器收到的 XML。它将返回到CasAuthenticationProvider a TicketResponse,其中包括用户名(必填),代理列表(如果涉及)和代理授予票证 IOU(如果请求了代理回调)。

  • 接下来的CasAuthenticationProvider将调用已配置的CasProxyDeciderCasProxyDecider指示TicketResponse中的代理列表是否对服务可接受。 Spring Security 提供了几种实现:RejectProxyTicketsAcceptAnyCasProxyNamedCasProxyDecider。这些名称在很大程度上是不言自明的,除了NamedCasProxyDecider允许提供List的受信任代理。

  • CasAuthenticationProvider接下来将请求AuthenticationUserDetailsService加载适用于Assertion中包含的用户的GrantedAuthority对象。

  • 如果没有问题,则CasAuthenticationProvider构造一个CasAuthenticationToken,其中包含TicketResponseGrantedAuthority中包含的详细信息。

  • 然后,控制返回到CasAuthenticationFilter,它将创建的CasAuthenticationToken放置在安全上下文中。

  • 用户的浏览器将重定向到导致AuthenticationException(或custom destination,具体取决于配置)的原始页面。

您还在这里真是太好了!现在让我们看一下它的配置方式

13.7.3 CASClient 端的配置

由于 Spring Security,使 CAS 的 Web 应用程序端变得容易。假定您已经知道使用 Spring Security 的基础知识,因此下面不再赘述。我们假设正在使用基于名称空间的配置,并根据需要添加 CAS Bean。每个部分都构建在上一部分的基础上。在 Spring Security Samples 中可以找到完整的CASsample 申请

服务票证身份验证

本节描述如何设置 Spring Security 来认证 Service Ticket。通常,这是 Web 应用程序所需的全部。您将需要在应用程序上下文中添加ServiceProperties bean。这代表您的 CAS 服务:

<bean id="serviceProperties"
    class="org.springframework.security.cas.ServiceProperties">
<property name="service"
    value="https://localhost:8443/cas-sample/login/cas"/>
<property name="sendRenew" value="false"/>
</bean>

service必须等于将由CasAuthenticationFilter监视的 URL。 sendRenew默认为 false,但是如果您的应用程序特别敏感,则应将其设置为 true。该参数的作用是告诉 CAS 登录服务单次登录是不可接受的。相反,用户将需要重新 Importing 其用户名和密码才能访问该服务。

应该配置以下 bean 以启动 CAS 身份验证过程(假设您使用的是名称空间配置):

<security:http entry-point-ref="casEntryPoint">
...
<security:custom-filter position="CAS_FILTER" ref="casFilter" />
</security:http>

<bean id="casFilter"
    class="org.springframework.security.cas.web.CasAuthenticationFilter">
<property name="authenticationManager" ref="authenticationManager"/>
</bean>

<bean id="casEntryPoint"
    class="org.springframework.security.cas.web.CasAuthenticationEntryPoint">
<property name="loginUrl" value="https://localhost:9443/cas/login"/>
<property name="serviceProperties" ref="serviceProperties"/>
</bean>

为了使 CAS 运行,ExceptionTranslationFilter必须将其authenticationEntryPoint属性设置为CasAuthenticationEntryPoint bean。可以使用entry-point-ref轻松完成此操作,就像上面的示例一样。 CasAuthenticationEntryPoint必须引用ServiceProperties bean(如上所述),该 bean 提供企业 CAS 登录服务器的 URL。这是将重定向用户浏览器的位置。

CasAuthenticationFilterUsernamePasswordAuthenticationFilter(用于基于表单的登录名)具有非常相似的属性。您可以使用这些属性来自定义行为,例如验证成功和失败的行为。

接下来,您需要添加一个CasAuthenticationProvider及其协作者:

<security:authentication-manager alias="authenticationManager">
<security:authentication-provider ref="casAuthenticationProvider" />
</security:authentication-manager>

<bean id="casAuthenticationProvider"
    class="org.springframework.security.cas.authentication.CasAuthenticationProvider">
<property name="authenticationUserDetailsService">
    <bean class="org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper">
    <constructor-arg ref="userService" />
    </bean>
</property>
<property name="serviceProperties" ref="serviceProperties" />
<property name="ticketValidator">
    <bean class="org.jasig.cas.client.validation.Cas20ServiceTicketValidator">
    <constructor-arg index="0" value="https://localhost:9443/cas" />
    </bean>
</property>
<property name="key" value="an_id_for_this_auth_provider_only"/>
</bean>

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

CasAuthenticationProvider使用UserDetailsService实例来为用户加载权限,一旦它们已被 CAS 验证。我们在这里显示了一个简单的内存设置。请注意,CasAuthenticationProvider实际上并未使用密码进行身份验证,但确实使用了权限。

如果您回头参考CAS 如何工作部分,那么所有这些 bean 都是不言自明的。

这样就完成了 CAS 的最基本配置。如果您没有犯任何错误,则您的 Web 应用程序应该在 CAS 单点登录框架内愉快地工作。 Spring Security 的其他部分无需关心 CAS 处理的身份验证这一事实。在以下各节中,我们将讨论一些(可选)更高级的配置。

Single Logout

CAS 协议支持单一注销,可以轻松添加到您的 Spring Security 配置中。以下是处理单点注销的 Spring Security 配置的更新

<security:http entry-point-ref="casEntryPoint">
...
<security:logout logout-success-url="/cas-logout.jsp"/>
<security:custom-filter ref="requestSingleLogoutFilter" before="LOGOUT_FILTER"/>
<security:custom-filter ref="singleLogoutFilter" before="CAS_FILTER"/>
</security:http>

<!-- This filter handles a Single Logout Request from the CAS Server -->
<bean id="singleLogoutFilter" class="org.jasig.cas.client.session.SingleSignOutFilter"/>

<!-- This filter redirects to the CAS Server to signal Single Logout should be performed -->
<bean id="requestSingleLogoutFilter"
    class="org.springframework.security.web.authentication.logout.LogoutFilter">
<constructor-arg value="https://localhost:9443/cas/logout"/>
<constructor-arg>
    <bean class=
        "org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler"/>
</constructor-arg>
<property name="filterProcessesUrl" value="/logout/cas"/>
</bean>

logout元素将用户从本地应用程序注销,但不会终止与 CAS 服务器或已登录的任何其他应用程序的会话。 requestSingleLogoutFilter过滤器将允许请求/spring_security_cas_logout的 URL,以将应用程序重定向到已配置的 CAS Server 注销 URL。然后,CAS 服务器将向已登录的所有服务发送“单一注销”请求。 singleLogoutFilter通过在静态Map中查找HttpSession然后使其无效来处理“单一注销”请求。

为什么同时需要logout元素和singleLogoutFilter可能令人困惑。最好先在本地注销,因为SingleSignOutFilter只是将HttpSession存储在静态Map中以便对其调用无效。使用上面的配置,注销流程将是:

  • 用户请求/logout,该用户将退出本地应用程序并将其发送到注销成功页面。

  • 注销成功页面/cas-logout.jsp应当指示用户单击指向/logout/cas的链接,以便注销所有应用程序。

  • 当用户单击链接时,该用户将被重定向到 CAS 单一注销 URL(https://localhost:9443/cas/logout)。

  • 然后,在 CAS 服务器端,CAS 单一注销 URL 向所有 CAS 服务提交单一注销请求。在 CAS Service 端,JASIG 的SingleSignOutFilter通过使原始会话无效来处理注销请求。

下一步是将以下内容添加到您的 web.xml 中

<filter>
<filter-name>characterEncodingFilter</filter-name>
<filter-class>
    org.springframework.web.filter.CharacterEncodingFilter
</filter-class>
<init-param>
    <param-name>encoding</param-name>
    <param-value>UTF-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>characterEncodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<listener>
<listener-class>
    org.jasig.cas.client.session.SingleSignOutHttpSessionListener
</listener-class>
</listener>

使用 SingleSignOutFilter 时,您可能会遇到一些编码问题。因此,建议添加CharacterEncodingFilter以确保使用SingleSignOutFilter时字符编码正确。同样,请参阅 JASIG 的文档以了解详细信息。 SingleSignOutHttpSessionListener确保HttpSession过期时,将删除用于单次注销的 Map。

通过 CAS 验证到 Stateless 服务

本节介绍如何使用 CAS 对服务进行身份验证。换句话说,本节讨论如何设置使用通过 CAS 进行身份验证的服务的 Client 端。下一节将介绍如何设置 Stateless 服务以使用 CAS 进行身份验证。

配置 CAS 以获取代理授予票证

为了向 Stateless 服务进行身份验证,应用程序需要获取代理授予票证(PGT)。本部分描述了如何配置 Spring Security,以便在 thencas-st [Service Ticket Authentication]配置上获得 PGT 构建。

第一步是在 Spring Security 配置中包含ProxyGrantingTicketStorage。这用于存储由CasAuthenticationFilter获得的 PGT,以便可以将其用于获取代理凭单。配置示例如下所示

<!--
NOTE: In a real application you should not use an in memory implementation.
You will also want to ensure to clean up expired tickets by calling
ProxyGrantingTicketStorage.cleanup()
-->
<bean id="pgtStorage" class="org.jasig.cas.client.proxy.ProxyGrantingTicketStorageImpl"/>

下一步是更新CasAuthenticationProvider以能够获取代理票证。为此,将Cas20ServiceTicketValidator替换为Cas20ProxyTicketValidatorproxyCallbackUrl应该设置为应用程序将在其上接收 PGT 的 URL。最后,配置还应引用ProxyGrantingTicketStorage,以便它可以使用 PGT 获得代理票证。您可以在下面找到配置更改的示例。

<bean id="casAuthenticationProvider"
    class="org.springframework.security.cas.authentication.CasAuthenticationProvider">
...
<property name="ticketValidator">
    <bean class="org.jasig.cas.client.validation.Cas20ProxyTicketValidator">
    <constructor-arg value="https://localhost:9443/cas"/>
        <property name="proxyCallbackUrl"
        value="https://localhost:8443/cas-sample/login/cas/proxyreceptor"/>
    <property name="proxyGrantingTicketStorage" ref="pgtStorage"/>
    </bean>
</property>
</bean>

最后一步是更新CasAuthenticationFilter以接受 PGT 并将其存储在ProxyGrantingTicketStorage中。 proxyReceptorUrlCas20ProxyTicketValidatorproxyCallbackUrl相匹配很重要。配置示例如下所示。

<bean id="casFilter"
        class="org.springframework.security.cas.web.CasAuthenticationFilter">
    ...
    <property name="proxyGrantingTicketStorage" ref="pgtStorage"/>
    <property name="proxyReceptorUrl" value="/login/cas/proxyreceptor"/>
</bean>
使用代理票证调用 Stateless 服务

现在,Spring Security 获得了 PGT,您可以使用它们来创建代理票证,该票证可用于对 Stateless 服务进行身份验证。 CASsample 申请ProxyTicketSampleServlet中包含一个工作示例。示例代码可以在下面找到:

protected void doGet(HttpServletRequest request, HttpServletResponse response)
    throws ServletException, IOException {
// NOTE: The CasAuthenticationToken can also be obtained using
// SecurityContextHolder.getContext().getAuthentication()
final CasAuthenticationToken token = (CasAuthenticationToken) request.getUserPrincipal();
// proxyTicket could be reused to make calls to the CAS service even if the
// target url differs
final String proxyTicket = token.getAssertion().getPrincipal().getProxyTicketFor(targetUrl);

// Make a remote call using the proxy ticket
final String serviceUrl = targetUrl+"?ticket="+URLEncoder.encode(proxyTicket, "UTF-8");
String proxyResponse = CommonUtils.getResponseFromServer(serviceUrl, "UTF-8");
...
}

代理票证身份验证

CasAuthenticationProvider区分有状态 Client 端和 StatelessClient 端。有状态 Client 端被认为是任何提交给CasAuthenticationFilterfilterProcessUrl的 Client 端。StatelessClient 端是在filterProcessUrl之外的 URL 上向CasAuthenticationFilter提出身份验证请求的 Client 端。

由于远程协议无法在HttpSession的上下文中表示自己,因此无法依赖默认的做法,即在请求之间的会话中存储安全上下文。此外,由于 CAS 服务器在TicketValidator验证票证后使票证无效,因此无法在后续请求中显示相同的代理票证。

一个明显的选择是根本不使用 CAS 远程协议 Client 端。但是,这将消除 CAS 的许多理想功能。作为中间立场,CasAuthenticationProvider使用StatelessTicketCache。这仅用于使用等于CasAuthenticationFilter.CAS_STATELESS_IDENTIFIER的主体的 StatelessClient 端。发生的情况是CasAuthenticationProvider将结果CasAuthenticationToken存储在StatelessTicketCache中,并在代理凭单上键入。因此,远程协议 Client 端可以提供相同的代理票证,并且CasAuthenticationProvider无需联系 CAS 服务器进行验证(除了第一个请求)。一旦通过身份验证,代理票证就可以用于原始目标服务以外的 URL。

本节以前面的部分为基础,以适应代理票证身份验证。第一步是指定对所有工件进行身份验证,如下所示。

<bean id="serviceProperties"
    class="org.springframework.security.cas.ServiceProperties">
...
<property name="authenticateAllArtifacts" value="true"/>
</bean>

下一步是为CasAuthenticationFilter指定servicePropertiesauthenticationDetailsSourceserviceProperties属性指示CasAuthenticationFilter尝试认证所有工件,而不是仅对filterProcessUrl上存在的工件进行认证。 ServiceAuthenticationDetailsSource创建一个ServiceAuthenticationDetails,以确保基于HttpServletRequest的当前 URL 在验证票证时用作服务 URL。可以通过注入返回ServiceAuthenticationDetails的自定义AuthenticationDetailsSource来自定义生成服务 URL 的方法。

<bean id="casFilter"
    class="org.springframework.security.cas.web.CasAuthenticationFilter">
...
<property name="serviceProperties" ref="serviceProperties"/>
<property name="authenticationDetailsSource">
    <bean class=
    "org.springframework.security.cas.web.authentication.ServiceAuthenticationDetailsSource">
    <constructor-arg ref="serviceProperties"/>
    </bean>
</property>
</bean>

您还需要更新CasAuthenticationProvider以处理代理凭单。为此,将Cas20ServiceTicketValidator替换为Cas20ProxyTicketValidator。您将需要配置statelessTicketCache以及要接受的代理。您可以在下面找到接受所有代理所需的更新示例。

<bean id="casAuthenticationProvider"
    class="org.springframework.security.cas.authentication.CasAuthenticationProvider">
...
<property name="ticketValidator">
    <bean class="org.jasig.cas.client.validation.Cas20ProxyTicketValidator">
    <constructor-arg value="https://localhost:9443/cas"/>
    <property name="acceptAnyProxy" value="true"/>
    </bean>
</property>
<property name="statelessTicketCache">
    <bean class="org.springframework.security.cas.authentication.EhCacheBasedTicketCache">
    <property name="cache">
        <bean class="net.sf.ehcache.Cache"
            init-method="initialise" destroy-method="dispose">
        <constructor-arg value="casTickets"/>
        <constructor-arg value="50"/>
        <constructor-arg value="true"/>
        <constructor-arg value="false"/>
        <constructor-arg value="3600"/>
        <constructor-arg value="900"/>
        </bean>
    </property>
    </bean>
</property>
</bean>

13.8 X.509 身份验证

13.8.1 Overview

X.509 证书身份验证的最常见用途是在使用 SSL 时(尤其是在浏览器中使用 HTTPS 时)验证服务器的身份。浏览器将自动检查服务器维护的受信任证书颁发机构列表之一是否已颁发(即进行数字签名)服务器提供的证书。

您还可以将 SSL 与“相互身份验证”结合使用;然后,服务器将作为 SSL 握手的一部分从 Client 端请求有效的证书。服务器将通过检查其证书是否由可接受的授权机构签名来对 Client 端进行身份验证。如果提供了有效证书,则可以通过应用程序中的 Servlet API 获得该证书。 Spring Security X.509 模块使用过滤器提取证书。它将证书 Map 到应用程序用户,并加载该用户的授权权限集以与标准 Spring Security 基础结构一起使用。

在尝试将其与 Spring Security 结合使用之前,您应该熟悉使用证书并为 Servlet 容器设置 Client 端身份验证。大多数工作是在创建和安装合适的证书和密钥。例如,如果您使用的是 Tomcat,请阅读http://tomcat.apache.org/tomcat-6.0-doc/ssl-howto.html的说明。在使用 Spring Security 进行尝试之前,一定要先做好这项工作,这一点很重要

13.8.2 向 Web 应用程序添加 X.509 身份验证

启用 X.509Client 端身份验证非常简单。只需将<x509/>元素添加到您的 http 安全名称空间配置中即可。

<http>
...
    <x509 subject-principal-regex="CN=(.*?)," user-service-ref="userService"/>;
</http>

元素具有两个可选属性:

  • subject-principal-regex。用于从证书的使用者名称中提取用户名的正则表达式。默认值如上所示。这是用户名,该用户名将传递给UserDetailsService以为用户加载权限。

  • user-service-ref。这是 X.509 所使用的UserDetailsService的 bean ID。如果您的应用程序上下文中仅定义了一个,则不需要。

subject-principal-regex应该包含一个组。例如,默认表达式“ CN =(.*?)”与公用名字段匹配。因此,如果证书中的主题名称为“ CN = Jimi Hendrix,OU = ...”,则用户名称为“ Jimi Hendrix”。匹配不区分大小写。因此,“ emailAddress =(.?)”将与“ EMAILADDRESS = [email protected],CN =…”匹配,并赋予用户名“ [email protected]”。如果 Client 端出示证书并且成功提取了有效的用户名,则安全上下文中应该有一个有效的Authentication对象。如果找不到证书,或者找不到相应的用户,则安全上下文将保持为空。这意味着您可以轻松地将 X.509 身份验证与其他选项(例如基于表单的登录名)一起使用。

13.8.3 在 Tomcat 中设置 SSL

Spring Security 项目的samples/certificate目录中有一些预先生成的证书。如果您不想生成自己的 SSL,则可以使用它们来启用 SSL 进行测试。文件server.jks包含服务器证书,私钥和颁发证书颁发机构的证书。示例应用程序中还为用户提供了一些 Client 端证书文件。您可以将这些安装在浏览器中以启用 SSLClient 端身份验证。

要运行具有 SSL 支持的 tomcat,请将server.jks文件拖放到 tomcat conf目录中,并将以下连接器添加到server.xml文件中

<Connector port="8443" protocol="HTTP/1.1" SSLEnabled="true" scheme="https" secure="true"
            clientAuth="true" sslProtocol="TLS"
            keystoreFile="${catalina.home}/conf/server.jks"
            keystoreType="JKS" keystorePass="password"
            truststoreFile="${catalina.home}/conf/server.jks"
            truststoreType="JKS" truststorePass="password"
/>

如果即使 Client 端不提供证书,您仍然希望 SSL 连接成功,也可以将clientAuth设置为want。除非您使用非 X.509 身份验证机制(例如表单身份验证),否则不提供证书的 Client 端将无法访问 Spring Security 保护的任何对象。

13.9 运行身份验证替换

13.9.1 Overview

AbstractSecurityInterceptor可以在安全对象回调阶段临时替换SecurityContextSecurityContextHolder中的Authentication对象。仅当AuthenticationManagerAccessDecisionManager成功处理了原始Authentication对象时,才会发生这种情况。 RunAsManager将指示在SecurityInterceptorCallback期间应使用的替换Authentication对象(如果有)。

通过在安全对象回调阶段临时替换Authentication对象,安全调用将能够调用需要不同身份验证和授权凭证的其他对象。它还将能够对特定的GrantedAuthority对象执行任何内部安全检查。因为 Spring Security 提供了许多帮助程序类,它们基于SecurityContextHolder的内容自动配置远程协议,所以这些运行方式替换在调用远程 Web 服务时特别有用。

13.9.2 Configuration

Spring Security 提供了一个RunAsManager接口:

Authentication buildRunAs(Authentication authentication, Object object,
    List<ConfigAttribute> config);

boolean supports(ConfigAttribute attribute);

boolean supports(Class clazz);

第一个方法返回Authentication对象,该对象应在方法调用期间替换现有的Authentication对象。如果该方法返回null,则表明不应进行任何替换。 AbstractSecurityInterceptor使用第二种方法作为配置属性启动验证的一部分。安全拦截器实现调用supports(Class)方法,以确保已配置的RunAsManager支持安全拦截器将呈现的安全对象的类型。

Spring Security 提供了RunAsManager的一种具体实现。如果ConfigAttributeRUN_AS_开头,则RunAsManagerImpl类返回替换RunAsUserToken。如果找到任何这样的ConfigAttribute,则替换RunAsUserToken将包含与原始Authentication对象相同的主体,凭据和授予的权限,以及每个RUN_AS_ ConfigAttribute的新SimpleGrantedAuthority。每个新的SimpleGrantedAuthority都将带有ROLE_前缀,然后是RUN_AS ConfigAttribute。例如,RUN_AS_SERVER将导致包含ROLE_RUN_AS_SERVER授予的权限的替换RunAsUserToken

替换RunAsUserToken就像其他任何Authentication对象一样。它需要由AuthenticationManager进行身份验证,可能需要通过委派给合适的AuthenticationProvider来进行。 RunAsImplAuthenticationProvider执行这种认证。它只是接受任何出现的RunAsUserToken作为有效值。

为了确保恶意代码不会创建RunAsUserToken并将其呈现出来以确保被RunAsImplAuthenticationProvider接受,密钥的哈希存储在所有生成的令牌中。 RunAsManagerImplRunAsImplAuthenticationProvider是在 bean 上下文中使用相同的键创建的:

<bean id="runAsManager"
    class="org.springframework.security.access.intercept.RunAsManagerImpl">
<property name="key" value="my_run_as_password"/>
</bean>

<bean id="runAsAuthenticationProvider"
    class="org.springframework.security.access.intercept.RunAsImplAuthenticationProvider">
<property name="key" value="my_run_as_password"/>
</bean>

通过使用相同的密钥,可以验证每个RunAsUserToken是由批准的RunAsManagerImpl创建的。出于安全原因,创建后RunAsUserToken是不可变的

13.10 Spring Security 加密模块

13.10.1 Introduction

Spring Security Crypto 模块提供对对称加密,密钥生成和密码编码的支持。该代码作为核心模块的一部分分发,但与任何其他 Spring Security(或 Spring)代码无关。

13.10.2 Encryptors

Encryptors 类提供了用于构造对称加密器的工厂方法。使用此类,您可以创建 ByteEncryptor 来以原始 byte []形式加密数据。您还可以构造 TextEncryptor 来加密文本字符串。加密器是线程安全的。

BytesEncryptor

使用 Encryptors.standard 工厂方法来构造“标准” BytesEncryptor:

Encryptors.standard("password", "salt");

“标准”加密方法是使用 PKCS#5 的 PBKDF2(基于密码的密钥派生功能#2)的 256 位 AES。此方法需要 Java6.用于生成 SecretKey 的密码应保存在安全的地方,并且不能共享。如果您的加密数据遭到破坏,该盐可用于防止针对密钥的字典攻击。还应用了 16 字节的随机初始化向量,因此每个加密的消息都是唯一的。

提供的盐应采用十六进制编码的字符串形式,并且是随机的,并且长度至少为 8 个字节。可以使用 KeyGenerator 生成这种盐:

String salt = KeyGenerators.string().generateKey(); // generates a random 8-byte salt that is then hex-encoded

TextEncryptor

使用 Encryptors.text 工厂方法构造一个标准的 TextEncryptor:

Encryptors.text("password", "salt");

TextEncryptor 使用标准的 BytesEncryptor 来加密文本数据。加密结果以十六进制编码的字符串形式返回,以便于存储在文件系统或数据库中。

使用 Encryptors.queryableText 工厂方法构造一个“可查询的” TextEncryptor:

Encryptors.queryableText("password", "salt");

可查询的 TextEncryptor 和标准 TextEncryptor 之间的区别与初始化向量(iv)处理有关。可查询 TextEncryptor#encrypt 操作中使用的 iv 是共享的或常量,并且不会随机生成。这意味着多次加密相同的文本将始终产生相同的加密结果。这不太安全,但是对于需要查询的加密数据是必需的。可查询的加密文本的一个示例是 OAuth apiKey。

13.10.3 密钥生成器

KeyGenerators 类为构造不同类型的密钥生成器提供了许多便利的工厂方法。使用此类,您可以创建一个 BytesKeyGenerator 来生成 byte []键。您还可以构造一个 StringKeyGenerator 来生成字符串键。 KeyGenerators 是线程安全的。

BytesKeyGenerator

使用 KeyGenerators.secureRandom 工厂方法来生成由 SecureRandom 实例支持的 BytesKeyGenerator:

BytesKeyGenerator generator = KeyGenerators.secureRandom();
byte[] key = generator.generateKey();

默认密钥长度为 8 个字节。还有一个 KeyGenerators.secureRandom 变体,可以控制密钥长度:

KeyGenerators.secureRandom(16);

使用 KeyGenerators.shared 工厂方法构造一个 BytesKeyGenerator,该每次调用总是返回相同的密钥:

KeyGenerators.shared(16);

StringKeyGenerator

使用 KeyGenerators.string 工厂方法来构造一个 8 字节的 SecureRandom KeyGenerator,该十六进制将每个密钥编码为字符串:

KeyGenerators.string();

13.10.4 密码编码

spring-security-crypto 模块的密码软件包提供了对密码编码的支持。 PasswordEncoder是中央服务接口,具有以下签名:

public interface PasswordEncoder {

String encode(String rawPassword);

boolean matches(String rawPassword, String encodedPassword);
}

如果 rawPassword 一旦编码,等于已编码的 Password,则 matchs 方法返回 true。此方法旨在支持基于密码的身份验证方案。

BCryptPasswordEncoder实现使用广泛支持的“ bcrypt”算法来对密码进行哈希处理。 Bcrypt 使用一个随机的 16 字节盐值,并且是一种故意慢速的算法,以阻止密码破解者。可以使用“ strength”参数调整它所做的工作量,该参数的取值范围为 4 到 31.值越大,计算散列就必须完成的工作就越多。默认值为 10.您可以在已部署的系统中更改此值,而不会影响现有密码,因为该值也存储在编码的哈希中。

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

Pbkdf2PasswordEncoder实现使用 PBKDF2 算法对密码进行哈希处理。为了破解密码破解,PBKDF2 是一种故意缓慢的算法,应调整为大约 0.5 秒才能验证系统上的密码。

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

13.11 并发支持

在大多数环境中,安全性是按Thread进行存储的。这意味着当在新的Thread上完成工作时,SecurityContext丢失了。 Spring Security 提供了一些基础架构来帮助用户轻松实现这一点。 Spring Security 提供了用于在多线程环境中使用 Spring Security 的底层抽象。实际上,这就是 Spring Security 与名为“ AsyncContext.start(Runnable)”的部分第 13.12.4 节“ Spring MVC 异步集成”集成的基础。

13.11.1 DelegatingSecurityContextRunnable

Spring Security 并发支持中最基本的构建块之一是DelegatingSecurityContextRunnable。它包装了一个委托Runnable,以便为该委托使用指定的SecurityContext初始化SecurityContextHolder。然后,它将调用委托 Runnable,以确保随后清除SecurityContextHolderDelegatingSecurityContextRunnable看起来像这样:

public void run() {
try {
    SecurityContextHolder.setContext(securityContext);
    delegate.run();
} finally {
    SecurityContextHolder.clearContext();
}
}

尽管非常简单,但可以无缝地将 SecurityContext 从一个线程传输到另一个线程。这很重要,因为在大多数情况下,SecurityContextHolder 会基于每个线程进行操作。例如,您可能已经使用 Spring Security 的名为“<global-method-security>”的部分支持来保护您的一项服务。现在,您可以轻松地将当前ThreadSecurityContext转移到调用安全服务的Thread。下面是如何执行此操作的示例:

Runnable originalRunnable = new Runnable() {
public void run() {
    // invoke secured service
}
};

SecurityContext context = SecurityContextHolder.getContext();
DelegatingSecurityContextRunnable wrappedRunnable =
    new DelegatingSecurityContextRunnable(originalRunnable, context);

new Thread(wrappedRunnable).start();

上面的代码执行以下步骤:

  • 创建一个Runnable来调用我们的安全服务。请注意,它不知道 Spring Security

  • SecurityContextHolder获得我们要使用的SecurityContext并初始化DelegatingSecurityContextRunnable

  • 使用DelegatingSecurityContextRunnable创建线程

  • 启动我们创建的线程

由于用SecurityContextHolder中的SecurityContext创建DelegatingSecurityContextRunnable很普遍,因此有一个快捷方式构造函数。以下代码与上面的代码相同:

Runnable originalRunnable = new Runnable() {
public void run() {
    // invoke secured service
}
};

DelegatingSecurityContextRunnable wrappedRunnable =
    new DelegatingSecurityContextRunnable(originalRunnable);

new Thread(wrappedRunnable).start();

我们拥有的代码易于使用,但仍然需要了解我们正在使用 Spring Security。在下一节中,我们将研究如何利用DelegatingSecurityContextExecutor隐藏我们正在使用 Spring Security 的事实。

13.11.2 DelegatingSecurityContextExecutor

在上一节中,我们发现使用DelegatingSecurityContextRunnable很容易,但是它并不理想,因为我们必须知道 Spring Security 才能使用它。让我们看一下DelegatingSecurityContextExecutor如何使我们的代码不受使用 Spring Security 的任何知识的影响。

DelegatingSecurityContextExecutor的设计与DelegatingSecurityContextRunnable的设计非常相似,不同之处在于它接受委托Executor而不是委托Runnable。您可以在下面查看如何使用它的示例:

SecurityContext context = SecurityContextHolder.createEmptyContext();
Authentication authentication =
    new UsernamePasswordAuthenticationToken("user","doesnotmatter", AuthorityUtils.createAuthorityList("ROLE_USER"));
context.setAuthentication(authentication);

SimpleAsyncTaskExecutor delegateExecutor =
    new SimpleAsyncTaskExecutor();
DelegatingSecurityContextExecutor executor =
    new DelegatingSecurityContextExecutor(delegateExecutor, context);

Runnable originalRunnable = new Runnable() {
public void run() {
    // invoke secured service
}
};

executor.execute(originalRunnable);

该代码执行以下步骤:

  • 创建用于DelegatingSecurityContextExecutorSecurityContext。请注意,在此示例中,我们仅手动创建了SecurityContext。但是,在何处或如何获取SecurityContext无关紧要(也就是说,如果需要,我们可以从SecurityContextHolder获取它)。

  • 创建一个负责执行提交的Runnable s 的委托 Actuator

  • 最后,我们创建一个DelegatingSecurityContextExecutor,它负责使用DelegatingSecurityContextRunnable来包装传递给 execute 方法的所有 Runnable。然后,它将包装的 Runnable 传递给委托 Actuator。在这种情况下,提交给我们DelegatingSecurityContextExecutor的每个 Runnable 将使用相同的SecurityContext。如果我们正在运行后台任务,而这些任务需要由特权较高的用户运行,那么这很好。

  • 此时,您可能会问自己:“这如何屏蔽我的代码,使其不了解 Spring Security?”代替在我们自己的代码中创建SecurityContextDelegatingSecurityContextExecutor,我们可以注入已初始化的DelegatingSecurityContextExecutor实例。

@Autowired
private Executor executor; // becomes an instance of our DelegatingSecurityContextExecutor

public void submitRunnable() {
Runnable originalRunnable = new Runnable() {
    public void run() {
    // invoke secured service
    }
};
executor.execute(originalRunnable);
}

现在我们的代码没有意识到SecurityContext正在传播到Thread,然后执行originalRunnable,然后清除了SecurityContextHolder。在此示例中,使用同一用户执行每个线程。如果我们想在调用executor.execute(Runnable)时使用SecurityContextHolder中的用户(即当前登录的用户)来处理originalRunnable怎么办?这可以通过从DelegatingSecurityContextExecutor构造函数中删除SecurityContext参数来完成。例如:

SimpleAsyncTaskExecutor delegateExecutor = new SimpleAsyncTaskExecutor();
DelegatingSecurityContextExecutor executor =
    new DelegatingSecurityContextExecutor(delegateExecutor);

现在,无论何时执行executor.execute(Runnable)SecurityContextHolder都会首先获得SecurityContext,然后使用SecurityContext来创建DelegatingSecurityContextRunnable。这意味着我们将使用与调用executor.execute(Runnable)代码相同的用户来执行Runnable

13.11.3 Spring Security 并发类

有关与 Java 并发 API 和 Spring Task 抽象的其他集成,请参考 Javadoc。一旦理解了先前的代码,它们就非常不言自明。

  • DelegatingSecurityContextCallable

  • DelegatingSecurityContextExecutor

  • DelegatingSecurityContextExecutorService

  • DelegatingSecurityContextRunnable

  • DelegatingSecurityContextScheduledExecutorService

  • DelegatingSecurityContextSchedulingTaskExecutor

  • DelegatingSecurityContextAsyncTaskExecutor

  • DelegatingSecurityContextTaskExecutor

13.12 Spring MVC 集成

Spring Security 提供了许多与 Spring MVC 的可选集成。本节将详细介绍集成。

13.12.1 @EnableWebMvcSecurity

Note

从 Spring Security 4.0 开始,不推荐使用@EnableWebMvcSecurity。替换为@EnableWebSecurity,它将根据 Classpath 确定添加 Spring MVC 功能。

要启用与 Spring MVC 的 Spring Security 集成,请在配置中添加@EnableWebSecurityComments。

Note

Spring Security 使用 Spring MVC 的WebMvcConfigurer提供配置。这意味着,如果您使用的是更高级的选项,例如直接与WebMvcConfigurationSupport集成,那么您将需要手动提供 Spring Security 配置。

13.12.2 MvcRequestMatcher

Spring Security 提供了 Spring MVC 如何匹配带有MvcRequestMatcher的 URL 的深度集成。这有助于确保您的安全规则与用于处理请求的逻辑相匹配。

为了使用MvcRequestMatcher,您必须将 Spring Security Configuration 与DispatcherServlet放在相同的ApplicationContext中。这是必需的,因为 Spring Security 的MvcRequestMatcher期望名称为mvcHandlerMappingIntrospectorHandlerMappingIntrospector bean 被用于执行匹配的 Spring MVC 配置注册。

对于web.xml,这意味着您应将配置放在DispatcherServlet.xml中。

<listener>
  <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

<!-- All Spring Configuration (both MVC and Security) are in /WEB-INF/spring/ -->
<context-param>
  <param-name>contextConfigLocation</param-name>
  <param-value>/WEB-INF/spring/*.xml</param-value>
</context-param>

<servlet>
  <servlet-name>spring</servlet-name>
  <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
  <!-- Load from the ContextLoaderListener -->
  <init-param>
    <param-name>contextConfigLocation</param-name>
    <param-value></param-value>
  </init-param>
</servlet>

<servlet-mapping>
  <servlet-name>spring</servlet-name>
  <url-pattern>/</url-pattern>
</servlet-mapping>

WebSecurityConfiguration以下位于DispatcherServlet s ApplicationContext中。

public class SecurityInitializer extends
    AbstractAnnotationConfigDispatcherServletInitializer {

  @Override
  protected Class<?>[] getRootConfigClasses() {
    return null;
  }

  @Override
  protected Class<?>[] getServletConfigClasses() {
    return new Class[] { RootConfiguration.class,
        WebMvcConfiguration.class };
  }

  @Override
  protected String[] getServletMappings() {
    return new String[] { "/" };
  }
}

Note

始终建议通过匹配HttpServletRequest和方法安全性来提供授权规则。

通过在HttpServletRequest上进行匹配来提供授权规则是很好的,因为它发生在代码路径的早期,并且有助于减少attack surface。方法安全性可确保如果有人绕过了 Web 授权规则,则您的应用程序仍然受到保护。这就是所谓的深度防御

考虑一个 Map 如下的控制器:

@RequestMapping("/admin")
public String admin() {

如果我们想将对这种控制器方法的访问限制为 Management 员用户,则开发人员可以通过在HttpServletRequest上匹配以下内容来提供授权规则:

protected configure(HttpSecurity http) throws Exception {
    http
        .authorizeRequests()
            .antMatchers("/admin").hasRole("ADMIN");
}

或 XML

<http>
    <intercept-url pattern="/admin" access="hasRole('ADMIN')"/>
</http>

无论采用哪种配置,URL /admin都将要求经过身份验证的用户为 Management 员用户。但是,根据我们的 Spring MVC 配置,URL /admin.html也将 Map 到我们的admin()方法。另外,根据我们的 Spring MVC 配置,URL /admin/也将 Map 到我们的admin()方法。

问题在于我们的安全规则仅保护/admin。我们可以为 Spring MVC 的所有排列添加其他规则,但这将非常冗长而乏味。

相反,我们可以利用 Spring Security 的MvcRequestMatcher。通过使用 Spring MVC 在 URL 上进行匹配,以下配置将保护 Spring MVC 将在其上进行匹配的相同 URL。

protected configure(HttpSecurity http) throws Exception {
    http
        .authorizeRequests()
            .mvcMatchers("/admin").hasRole("ADMIN");
}

或 XML

<http request-matcher="mvc">
    <intercept-url pattern="/admin" access="hasRole('ADMIN')"/>
</http>

13.12.3 @AuthenticationPrincipal

Spring Security 提供了AuthenticationPrincipalArgumentResolver,可以自动为 Spring MVC 参数解析当前的Authentication.getPrincipal()。通过使用@EnableWebSecurity,您将自动将其添加到 Spring MVC 配置中。如果使用基于 XML 的配置,则必须自己添加。例如:

<mvc:annotation-driven>
        <mvc:argument-resolvers>
                <bean class="org.springframework.security.web.method.annotation.AuthenticationPrincipalArgumentResolver" />
        </mvc:argument-resolvers>
</mvc:annotation-driven>

正确配置AuthenticationPrincipalArgumentResolver之后,您可以在 Spring MVC 层中与 Spring Security 完全脱钩。

考虑以下情况:自定义UserDetailsService返回实现UserDetails和您自己的CustomUser ObjectObject。可以使用以下代码访问当前已认证用户的CustomUser

@RequestMapping("/messages/inbox")
public ModelAndView findMessagesForUser() {
    Authentication authentication =
    SecurityContextHolder.getContext().getAuthentication();
    CustomUser custom = (CustomUser) authentication == null ? null : authentication.getPrincipal();

    // .. find messages for this user and return them ...
}

从 Spring Security 3.2 开始,我们可以通过添加 Comments 来更直接地解析参数。例如:

import org.springframework.security.core.annotation.AuthenticationPrincipal;

// ...

@RequestMapping("/messages/inbox")
public ModelAndView findMessagesForUser(@AuthenticationPrincipal CustomUser customUser) {

    // .. find messages for this user and return them ...
}

有时可能需要以某种方式转换主体。例如,如果CustomUser必须是最终的,则不能扩展。在这种情况下,UserDetailsService可能会返回实现UserDetails并提供名为getCustomUser的方法来访问CustomUserObject。例如,它可能看起来像:

public class CustomUserUserDetails extends User {
        // ...
        public CustomUser getCustomUser() {
                return customUser;
        }
}

然后,我们可以使用以Authentication.getPrincipal()作为根对象的SpEL expression来访问CustomUser

import org.springframework.security.core.annotation.AuthenticationPrincipal;

// ...

@RequestMapping("/messages/inbox")
public ModelAndView findMessagesForUser(@AuthenticationPrincipal(expression = "customUser") CustomUser customUser) {

    // .. find messags for this user and return them ...
}

我们还可以在 SpEL 表达式中引用 Bean。例如,如果我们使用 JPA 来 Management 用户,并且想要修改并保存当前用户的属性,则可以使用以下内容。

import org.springframework.security.core.annotation.AuthenticationPrincipal;

// ...

@PutMapping("/users/self")
public ModelAndView updateName(@AuthenticationPrincipal(expression = "@jpaEntityManager.merge(#this)") CustomUser attachedCustomUser,
        @RequestParam String firstName) {

    // change the firstName on an attached instance which will be persisted to the database
    attachedCustomUser.setFirstName(firstName);

    // ...
}

我们可以通过在自己的 Comments 上设置@AuthenticationPrincipal作为元 Comments 来进一步消除对 Spring Security 的依赖。下面我们演示如何在名为@CurrentUser的 Comments 上执行此操作。

Note

重要的是要意识到,为了消除对 Spring Security 的依赖,创建@CurrentUser的是消耗用户的应用程序。并非严格要求执行此步骤,但是可以帮助您将对 Spring Security 的依赖隔离到更中央的位置。

@Target({ElementType.PARAMETER, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@AuthenticationPrincipal
public @interface CurrentUser {}

现在已经指定了@CurrentUser,我们可以用它来通知解析当前已认证用户的CustomUser。我们还将对 Spring Security 的依赖关系隔离到一个文件中。

@RequestMapping("/messages/inbox")
public ModelAndView findMessagesForUser(@CurrentUser CustomUser customUser) {

    // .. find messages for this user and return them ...
}

13.12.4 Spring MVC 异步集成

Spring Web MVC 3.2 对异步请求处理提供了出色的支持。如果没有其他配置,Spring Security 将自动将SecurityContext设置为Thread,该Thread执行您的控制器返回的Callable。例如,以下方法将自动使用创建Callable时可用的SecurityContext执行其Callable

@RequestMapping(method=RequestMethod.POST)
public Callable<String> processUpload(final MultipartFile file) {

return new Callable<String>() {
    public Object call() throws Exception {
    // ...
    return "someView";
    }
};
}

Note

从技术上讲,Spring Security 与WebAsyncManager集成。用于处理CallableSecurityContext是在调用startCallableProcessingSecurityContextHolder上存在的SecurityContext

没有与控制器返回的DeferredResult自动集成。这是因为DeferredResult由用户处理,因此无法自动与其集成。但是,您仍然可以使用Concurrency Support提供与 Spring Security 的透明集成。

13.12.5 Spring MVC 和 CSRF 集成

自动令牌包含

Spring Security 将在使用Spring MVC 表单标签的表单中自动包括 CSRF 令牌。例如,以下 JSP:

<jsp:root xmlns:jsp="http://java.sun.com/JSP/Page"
    xmlns:c="http://java.sun.com/jsp/jstl/core"
    xmlns:form="http://www.springframework.org/tags/form" version="2.0">
    <jsp:directive.page language="java" contentType="text/html" />
<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
    <!-- ... -->

    <c:url var="logoutUrl" value="/logout"/>
    <form:form action="${logoutUrl}"
        method="post">
    <input type="submit"
        value="Log out" />
    <input type="hidden"
        name="${_csrf.parameterName}"
        value="${_csrf.token}"/>
    </form:form>

    <!-- ... -->
</html>
</jsp:root>

将输出类似于以下内容的 HTML:

<!-- ... -->

<form action="/context/logout" method="post">
<input type="submit" value="Log out"/>
<input type="hidden" name="_csrf" value="f81d4fae-7dec-11d0-a765-00a0c91e6bf6"/>
</form>

<!-- ... -->

解析 CsrfToken

Spring Security 提供了CsrfTokenArgumentResolver,可以自动为 Spring MVC 参数解析当前的CsrfToken。通过使用@EnableWebSecurity,您将自动将其添加到您的 Spring MVC 配置中。如果使用基于 XML 的配置,则必须自己添加。

正确配置CsrfTokenArgumentResolver之后,您就可以将CsrfToken暴露给基于静态 HTML 的应用程序。

@RestController
public class CsrfController {

    @RequestMapping("/csrf")
    public CsrfToken csrf(CsrfToken token) {
        return token;
    }
}

重要的是,要使CsrfToken成为其他域的 Secret。这意味着,如果您使用的是跨源共享(CORS),则应 不要CsrfToken公开给任何外部域。


[21] Spring Security 2.0 的旧选项也受支持,但不建议使用。