6. Java Configuration

在 Spring 3.1 中,对Java Configuration的常规支持已添加到 Spring Framework 中。从 Spring Security 3.2 开始,Spring Security Java Configuration 支持使用户可以轻松配置 Spring Security,而无需使用任何 XML。

如果您熟悉第 7 章,安全命名空间配置,则应该在第 7 章,安全命名空间配置和 Security Java Configuration 支持之间找到很多相似之处。

Note

Spring Security 提供了许多示例应用程序,它们演示了 Spring Security Java 配置的使用。

6.1 Hello Web Security Java 配置

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

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

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

@EnableWebSecurity
public class WebSecurityConfig implements WebMvcConfigurer {

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

此配置的确没有太多,但是它做了很多。您可以找到以下功能的摘要:

6.1.1 AbstractSecurityWebApplicationInitializer

下一步是向 War 注册springSecurityFilterChain。可以在 Servlet 3.0 环境中使用Spring 的 WebApplicationInitializer 支持在 Java 配置中完成此操作。毫不奇怪,Spring Security 提供了一个 Base ClassAbstractSecurityWebApplicationInitializer,它将确保springSecurityFilterChain为您注册。使用AbstractSecurityWebApplicationInitializer的方式因我们是否已经在使用 Spring 或 Spring Security 是应用程序中唯一的 Spring 组件而异。

6.1.2 不存在 Spring 的 AbstractSecurityWebApplicationInitializer

如果您不使用 Spring 或 Spring MVC,则需要将WebSecurityConfig传递到超类中,以确保配置被接受。您可以在下面找到一个示例:

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

public class SecurityWebApplicationInitializer
    extends AbstractSecurityWebApplicationInitializer {

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

SecurityWebApplicationInitializer将执行以下操作:

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

  • 添加一个加载WebSecurityConfig的 ContextLoaderListener。

6.1.3 使用 Spring MVC 的 AbstractSecurityWebApplicationInitializer

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

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

public class SecurityWebApplicationInitializer
    extends AbstractSecurityWebApplicationInitializer {

}

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

public class MvcWebApplicationInitializer extends
        AbstractAnnotationConfigDispatcherServletInitializer {

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

    // ... other overrides ...
}

6.2 HttpSecurity

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

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

上面的默认配置:

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

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

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

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

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

使用and()方法表示等效于关闭 XML 标记的 Java 配置,该方法允许我们 continue 配置父级。如果您阅读该代码,这也很有意义。我要配置授权请求,并配置表单登录,并配置 HTTP 基本身份验证。

6.3 Java 配置和表单登录

您可能想知道提示您登录时登录表单的来源,因为我们没有提及任何 HTML 文件或 JSP。由于 Spring Security 的默认配置没有显式设置登录页面的 URL,因此 Spring Security 将基于已启用的功能并使用处理提交的登录的 URL 的标准值(用户将使用的默认目标 URL)自动生成一个 URL。登录后发送至。

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

protected void configure(HttpSecurity http) throws Exception {
    http
        .authorizeRequests()
            .anyRequest().authenticated()
            .and()
        .formLogin()
            .loginPage("/login") (1)
            .permitAll();        (2)
}
  • (1) 更新后的配置指定登录页面的位置。
  • (2) 我们必须授予所有用户(即未经身份验证的用户)访问我们登录页面的权限。 formLogin().permitAll()方法允许向所有用户授予与基于表单的登录相关的所有 URL 的访问权限。

下面是我们当前配置中使用 JSP 实现的示例登录页面:

Note

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

<c:url value="/login" var="loginUrl"/>
<form action="${loginUrl}" method="post">       (1)
    <c:if test="${param.error != null}">        (2)
        <p>
            Invalid username and password.
        </p>
    </c:if>
    <c:if test="${param.logout != null}">       (3)
        <p>
            You have been logged out.
        </p>
    </c:if>
    <p>
        <label for="username">Username</label>
        <input type="text" id="username" name="username"/>  (4)
    </p>
    <p>
        <label for="password">Password</label>
        <input type="password" id="password" name="password"/>  (5)
    </p>
    <input type="hidden"                        (6)
        name="${_csrf.parameterName}"
        value="${_csrf.token}"/>
    <button type="submit" class="btn">Log in</button>
</form>
  • (1)/login URL 的 POST 将尝试验证用户身份
  • (2) 如果查询参数error存在,则尝试认证失败
  • (3) 如果查询参数logout存在,则表明用户已成功注销
  • (4) 用户名必须作为名为* username *的 HTTP 参数存在
  • (5) 密码必须作为名为* password *的 HTTP 参数存在
  • (6) 我们必须名为“包含 CSRF 令牌”的部分要了解更多信息,请阅读参考的第 10.6 节“跨站请求伪造(CSRF)”部分

6.4 授权请求

我们的示例仅要求对用户进行身份验证,并且对应用程序中的每个 URL 都进行了身份验证。我们可以通过将多个子级添加到http.authorizeRequests()方法中来为 URL 指定自定义要求。例如:

protected void configure(HttpSecurity http) throws Exception {
    http
        .authorizeRequests()                                                                (1)
            .antMatchers("/resources/**", "/signup", "/about").permitAll()                  (2)
            .antMatchers("/admin/**").hasRole("ADMIN")                                      (3)
            .antMatchers("/db/**").access("hasRole('ADMIN') and hasRole('DBA')")            (4)
            .anyRequest().authenticated()                                                   (5)
            .and()
        // ...
        .formLogin();
}
  • (1) http.authorizeRequests()方法有多个子级,每个匹配器均按声明 Sequences 考虑。
  • (2) 我们指定了任何用户都可以访问的多个 URL 模式。具体来说,如果 URL 以“/resources /”开头,等于“/signup”或等于“/about”,则任何用户都可以访问请求。
  • (3) 任何以“/admin /”开头的 URL 都将限于角色为“ ROLE_ADMIN”的用户。您将注意到,由于我们正在调用hasRole方法,因此无需指定“ ROLE_”前缀。
  • (4) 任何以“/db /”开头的 URL 都要求用户同时具有“ ROLE_ADMIN”和“ ROLE_DBA”。您会注意到,由于我们使用的是hasRole表达式,因此无需指定“ ROLE_”前缀。
  • (5) 尚未匹配的任何 URL 仅要求对用户进行身份验证

6.5 处理注销

使用WebSecurityConfigurerAdapter时,将自动应用注销功能。默认设置是访问 URL /logout将通过以下方式注销用户:

  • 使 HTTP 会话无效

  • 清理配置的所有 RememberMe 身份验证

  • 清除SecurityContextHolder

  • 重定向到/login?logout

但是,与配置登录功能相似,您还可以使用各种选项来进一步自定义注销要求:

protected void configure(HttpSecurity http) throws Exception {
    http
        .logout()                                                                (1)
            .logoutUrl("/my/logout")                                                 (2)
            .logoutSuccessUrl("/my/index")                                           (3)
            .logoutSuccessHandler(logoutSuccessHandler)                              (4)
            .invalidateHttpSession(true)                                             (5)
            .addLogoutHandler(logoutHandler)                                         (6)
            .deleteCookies(cookieNamesToClear)                                       (7)
            .and()
        ...
}
  • (1) 提供注销支持。使用WebSecurityConfigurerAdapter时将自动应用。
  • (2) 触发注销的 URL 发生(默认为/logout)。如果启用了 CSRF 保护(默认),则请求也必须是 POST。有关更多信息,请咨询JavaDoc
  • (3) 发生注销后重定向到的 URL。默认值为/login?logout。有关更多信息,请咨询JavaDoc
  • (4) 让我们指定一个自定义LogoutSuccessHandler。如果指定此选项,则logoutSuccessUrl()将被忽略。有关更多信息,请咨询JavaDoc
  • (5) 指定注销时是否使HttpSession无效。默认情况下,这是“ true”。在幕后配置SecurityContextLogoutHandler。有关更多信息,请咨询JavaDoc
  • (6) 添加LogoutHandler。默认情况下,将SecurityContextLogoutHandler添加为最后的LogoutHandler
  • (7) 允许指定成功注销后将删除的 cookie 名称。这是显式添加CookieClearingLogoutHandler的快捷方式。

Note

===当然,也可以使用 XML 命名空间符号来配置注销。请参阅 Spring Security XML 命名空间部分中logout element的文档以获取更多详细信息。 ===

通常,为了自定义注销功能,您可以添加LogoutHandler和/或LogoutSuccessHandler实现。对于许多常见方案,使用流畅的 API 时会在后台应用这些处理程序。

6.5.1 LogoutHandler

通常,LogoutHandler实现表示能够参与注销处理的类。期望它们被调用以执行必要的清理。因此,它们不应引发异常。提供了各种实现:

有关详情,请参见第 10.5.4 节“记住我的接口和实现”

除了直接提供LogoutHandler实现之外,fluent API 还提供了快捷方式,这些快捷方式在幕后提供了各自的LogoutHandler实现。例如。 deleteCookies()允许指定注销成功后要删除的一个或多个 cookie 的名称。与添加CookieClearingLogoutHandler相比,这是捷径。

6.5.2 LogoutSuccessHandler

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

提供以下实现:

如上所述,您无需直接指定SimpleUrlLogoutSuccessHandler。相反,Fluent 的 API 通过设置logoutSuccessUrl()提供了快捷方式。这将在底下设置SimpleUrlLogoutSuccessHandler。提供的 URL 将在注销后重定向到。默认值为/login?logout

HttpStatusReturningLogoutSuccessHandler在 REST API 类型的场景中可能会很有趣。通过LogoutSuccessHandler,您可以提供要返回的纯 HTTP 状态代码,而不是在成功注销后重定向到 URL。如果未配置,默认情况下将返回状态码 200.

6.5.3 其他与注销有关的参考

6.6 OAuth 2.0Client 端

OAuth 2.0Client 端功能提供对OAuth 2.0 授权框架中定义的 Client 端角色的支持。

提供以下主要功能:

HttpSecurity.oauth2Client()提供了许多用于自定义 OAuth 2.0Client 端的配置选项。以下代码显示了可用于oauth2Client() DSL 的完整配置选项:

@EnableWebSecurity
public class OAuth2ClientSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .oauth2Client()
                .clientRegistrationRepository(this.clientRegistrationRepository())
                .authorizedClientRepository(this.authorizedClientRepository())
                .authorizedClientService(this.authorizedClientService())
                .authorizationCodeGrant()
                    .authorizationRequestRepository(this.authorizationRequestRepository())
                    .authorizationRequestResolver(this.authorizationRequestResolver())
                    .accessTokenResponseClient(this.accessTokenResponseClient());
    }
}

以下各节详细介绍了每个可用的配置选项:

6.6.1 ClientRegistration

ClientRegistration是向 OAuth 2.0 或 OpenID Connect 1.0 提供程序注册的 Client 端的表示。

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

ClientRegistration及其属性定义如下:

public final class ClientRegistration {
    private String registrationId;  (1)
    private String clientId;    (2)
    private String clientSecret;    (3)
    private ClientAuthenticationMethod clientAuthenticationMethod;  (4)
    private AuthorizationGrantType authorizationGrantType;  (5)
    private String redirectUriTemplate; (6)
    private Set<String> scopes; (7)
    private ProviderDetails providerDetails;
    private String clientName;  (8)

    public class ProviderDetails {
        private String authorizationUri;    (9)
        private String tokenUri;    https://www.docs4dev.com/images/spring-security/5.1.2.RELEASE/10.png
        private UserInfoEndpoint userInfoEndpoint;
        private String jwkSetUri;   https://www.docs4dev.com/images/spring-security/5.1.2.RELEASE/11.png
        private Map<String, Object> configurationMetadata;  https://www.docs4dev.com/images/spring-security/5.1.2.RELEASE/12.png

        public class UserInfoEndpoint {
            private String uri; https://www.docs4dev.com/images/spring-security/5.1.2.RELEASE/13.png
            private AuthenticationMethod authenticationMethod;  https://www.docs4dev.com/images/spring-security/5.1.2.RELEASE/14.png
            private String userNameAttributeName;   https://www.docs4dev.com/images/spring-security/5.1.2.RELEASE/15.png

        }
    }
}
  • (1) registrationId:唯一标识ClientRegistration的 ID。
  • (2) clientId:Client 端标识符。
  • (3) clientSecret:Client 端机密。
  • (4) clientAuthenticationMethod:用于通过提供者对 Client 端进行身份验证的方法。支持的值是 basicpost
  • (5) authorizationGrantType:OAuth 2.0 授权框架定义了四种Authorization Grant类型。支持的值是 authorization_code,implicit 和 client_credentials。
  • (6) redirectUriTemplate:在最终用户对 Client 端进行身份验证和授权访问后,“授权服务器”会将最终用户的用户代理重定向到的 Client 的注册重定向 URI。
  • (7) scopes:Client 端在授权请求流程中请求的范围,例如 openid,电子邮件或配置文件。
  • (8) clientName:用于 Client 端的描述性名称。该名称可能在某些情况下使用,例如在自动生成的登录页面中显示 Client 端名称时。
  • (9) authorizationUri:授权服务器的授权端点 URI。
  • (0) tokenUri:授权服务器的令牌端点 URI。
  • (1) jwkSetUri:用于从授权服务器中检索JSON Web 密钥(JWK)集的 URI,其中包含用于验证 ID 令牌JSON Web 签名(JWS)以及可选的 UserInfo 响应的加密密钥。
  • (2) configurationMetadataOpenID 提供程序配置信息。仅当配置了 Spring Boot 2.x 属性spring.security.oauth2.client.provider.[providerId].issuerUri时,此信息才可用。
  • (3) (userInfoEndpoint)uri:用于访问经过身份验证的最终用户的声明/属性的 UserInfo 端点 URI。
  • (4) (userInfoEndpoint)authenticationMethod:将访问令牌发送到 UserInfo 端点时使用的身份验证方法。支持的值为 headerformquery
  • (5) userNameAttributeName:在 UserInfo 响应中返回的属性名称,该属性引用了最终用户的名称或标识符。

6.6.2 ClientRegistrationRepository

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

Note

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

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

Note

ClientRegistrationRepository的默认实现是InMemoryClientRegistrationRepository

自动配置还会在ApplicationContext中将ClientRegistrationRepository注册为@Bean,以便在应用程序需要时可用于依赖项注入。

以下清单显示了一个示例:

@Controller
public class OAuth2ClientController {

    @Autowired
    private ClientRegistrationRepository clientRegistrationRepository;

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

        ...

        return "index";
    }
}

6.6.3 OAuth2AuthorizedClient

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

OAuth2AuthorizedClient用于将OAuth2AccessToken(和可选的OAuth2RefreshToken)与ClientRegistration(Client 端)和资源所有者相关联,该所有者是授予授权的Principal最终用户。

6.6.4 OAuth2AuthorizedClientRepository/OAuth2AuthorizedClientService

OAuth2AuthorizedClientRepository负责在 Web 请求之间保留OAuth2AuthorizedClientOAuth2AuthorizedClientService的主要作用是在应用程序级别 ManagementOAuth2AuthorizedClient

从开发人员的角度来看,OAuth2AuthorizedClientRepositoryOAuth2AuthorizedClientService提供了查找与 Client 端关联的OAuth2AccessToken的功能,以便可以将其用于发起受保护的资源请求。

Note

Spring Boot 2.x 自动配置在ApplicationContext中注册OAuth2AuthorizedClientRepository和/或OAuth2AuthorizedClientService @Bean

开发人员还可以在ApplicationContext中注册OAuth2AuthorizedClientRepositoryOAuth2AuthorizedClientService @Bean(覆盖 Spring Boot 2.x 自动配置),以便能够查找与特定ClientRegistration关联的OAuth2AccessToken(Client 端)。

以下清单显示了一个示例:

@Controller
public class OAuth2LoginController {

    @Autowired
    private OAuth2AuthorizedClientService authorizedClientService;

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

        OAuth2AccessToken accessToken = authorizedClient.getAccessToken();

        ...

        return "userinfo";
    }
}

6.6.5 RegisteredOAuth2AuthorizedClient

@RegisteredOAuth2AuthorizedClient注解提供了将方法参数解析为OAuth2AuthorizedClient类型的参数值的功能。与通过OAuth2AuthorizedClientService查找OAuth2AuthorizedClient相比,这是一个方便的选择。

@Controller
public class OAuth2LoginController {

    @RequestMapping("/userinfo")
    public String userinfo(@RegisteredOAuth2AuthorizedClient("google") OAuth2AuthorizedClient authorizedClient) {
        OAuth2AccessToken accessToken = authorizedClient.getAccessToken();

        ...

        return "userinfo";
    }
}

@RegisteredOAuth2AuthorizedClient注解由OAuth2AuthorizedClientArgumentResolver处理,并提供以下功能:

  • 如果尚未授权 Client 端,将自动请求OAuth2AccessToken

  • 对于authorization_code,这涉及触发授权请求重定向以启动流程

    • 对于client_credentials,使用DefaultClientCredentialsTokenResponseClient直接从令牌端点获取访问令牌。

6.6.6 AuthorizationRequestRepository

从发起授权请求到接收到授权响应(回调)的时间,AuthorizationRequestRepository负责OAuth2AuthorizationRequest的持久性。

Tip

OAuth2AuthorizationRequest用于关联和验证授权响应。

AuthorizationRequestRepository的默认实现是HttpSessionOAuth2AuthorizationRequestRepository,它将OAuth2AuthorizationRequest存储在HttpSession中。

如果您想提供一个AuthorizationRequestRepository的自定义实现,该实现将OAuth2AuthorizationRequest的属性存储在Cookie中,则可以按以下示例所示进行配置:

@EnableWebSecurity
public class OAuth2ClientSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .oauth2Client()
                .authorizationCodeGrant()
                    .authorizationRequestRepository(this.cookieAuthorizationRequestRepository())
                    ...
    }

    private AuthorizationRequestRepository<OAuth2AuthorizationRequest> cookieAuthorizationRequestRepository() {
        return new HttpCookieOAuth2AuthorizationRequestRepository();
    }
}

6.6.7 OAuth2AuthorizationRequestResolver

OAuth2AuthorizationRequestResolver的主要作用是从提供的 Web 请求中解析OAuth2AuthorizationRequest。默认实现DefaultOAuth2AuthorizationRequestResolver在(默认)路径/oauth2/authorization/{registrationId}上匹配,从而提取registrationId并使用它为关联的ClientRegistration构建OAuth2AuthorizationRequest

OAuth2AuthorizationRequestResolver可以实现的主要用例之一是能够使用 OAuth 2.0 授权框架中定义的标准参数之外的其他参数来自定义授权请求。

例如,OpenID Connect 为授权码流程定义了其他 OAuth 2.0 请求参数,该参数是在OAuth 2.0 授权框架中定义的标准参数的基础上扩展的。 prompt参数是这些扩展参数之一。

Note

可选的。用空格分隔的,区分大小写的 ASCII 字符串值列表,用于指定授权服务器是否提示最终用户进行重新认证和同意。定义的值是:无,登录,同意,select_account

以下示例显示了如何通过包含请求参数prompt=consent来实现针对oauth2Login()定制授权请求的OAuth2AuthorizationRequestResolver

@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private ClientRegistrationRepository clientRegistrationRepository;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .anyRequest().authenticated()
                .and()
            .oauth2Login()
                .authorizationEndpoint()
                    .authorizationRequestResolver(
                            new CustomAuthorizationRequestResolver(
                                    this.clientRegistrationRepository));    (1)
    }
}

public class CustomAuthorizationRequestResolver implements OAuth2AuthorizationRequestResolver {
    private final OAuth2AuthorizationRequestResolver defaultAuthorizationRequestResolver;

    public CustomAuthorizationRequestResolver(
            ClientRegistrationRepository clientRegistrationRepository) {

        this.defaultAuthorizationRequestResolver =
                new DefaultOAuth2AuthorizationRequestResolver(
                        clientRegistrationRepository, "/oauth2/authorization");
    }

    @Override
    public OAuth2AuthorizationRequest resolve(HttpServletRequest request) {
        OAuth2AuthorizationRequest authorizationRequest =
                this.defaultAuthorizationRequestResolver.resolve(request);  (2)

        return authorizationRequest != null ?   (3)
                customAuthorizationRequest(authorizationRequest) :
                null;
    }

    @Override
    public OAuth2AuthorizationRequest resolve(
            HttpServletRequest request, String clientRegistrationId) {

        OAuth2AuthorizationRequest authorizationRequest =
                this.defaultAuthorizationRequestResolver.resolve(
                    request, clientRegistrationId);    (4)

        return authorizationRequest != null ?   (5)
                customAuthorizationRequest(authorizationRequest) :
                null;
    }

    private OAuth2AuthorizationRequest customAuthorizationRequest(
            OAuth2AuthorizationRequest authorizationRequest) {

        Map<String, Object> additionalParameters =
                new LinkedHashMap<>(authorizationRequest.getAdditionalParameters());
        additionalParameters.put("prompt", "consent");  (6)

        return OAuth2AuthorizationRequest.from(authorizationRequest)    (7)
                .additionalParameters(additionalParameters) (8)
                .build();
    }
}
  • (1) 配置自定义OAuth2AuthorizationRequestResolver
  • (2) 尝试使用DefaultOAuth2AuthorizationRequestResolver解析OAuth2AuthorizationRequest
  • (3) 如果解决了OAuth2AuthorizationRequest,则返回自定义版本,否则返回null
  • (6) 将自定义参数添加到现有的OAuth2AuthorizationRequest.additionalParameters
  • (7) 创建默认值OAuth2AuthorizationRequest的副本,该副本将返回OAuth2AuthorizationRequest.Builder以作进一步修改
  • (8) 覆盖默认的additionalParameters

Tip

OAuth2AuthorizationRequest.Builder.build()构造OAuth2AuthorizationRequest.authorizationRequestUri,它表示完整的授权请求 URI,包括使用application/x-www-form-urlencoded格式的所有查询参数。

前面的示例显示了在标准参数之上添加自定义参数的常见用例。但是,如果您需要删除或更改标准参数,或者您的要求更高级,则可以通过完全覆盖OAuth2AuthorizationRequest.authorizationRequestUri属性来完全控制构建“授权请求 URI”。

下面的示例显示了customAuthorizationRequest()方法与上一示例的不同形式,而是覆盖了OAuth2AuthorizationRequest.authorizationRequestUri属性。

private OAuth2AuthorizationRequest customAuthorizationRequest(
        OAuth2AuthorizationRequest authorizationRequest) {

    String customAuthorizationRequestUri = UriComponentsBuilder
            .fromUriString(authorizationRequest.getAuthorizationRequestUri())
            .queryParam("prompt", "consent")
            .build(true)
            .toUriString();

    return OAuth2AuthorizationRequest.from(authorizationRequest)
            .authorizationRequestUri(customAuthorizationRequestUri)
            .build();
}

6.6.8 OAuth2AccessTokenResponseClient

OAuth2AccessTokenResponseClient的主要作用是在授权服务器的令牌端点处将访问授权证书的授权授权证书交换。

authorization_code授权的OAuth2AccessTokenResponseClient的默认实现是DefaultAuthorizationCodeTokenResponseClient,它使用RestOperations在令牌端点上交换访问令牌的授权代码。

DefaultAuthorizationCodeTokenResponseClient非常灵活,因为它允许您自定义令牌请求的预处理和/或令牌响应的后处理。

如果您需要自定义令牌请求的预处理,则可以为DefaultAuthorizationCodeTokenResponseClient.setRequestEntityConverter()提供自定义Converter<OAuth2AuthorizationCodeGrantRequest, RequestEntity<?>>。默认实现OAuth2AuthorizationCodeGrantRequestEntityConverter构建标准OAuth 2.0 访问令牌请求RequestEntity表示。但是,提供自定义Converter可以使您扩展标准令牌请求并添加一个自定义参数。

Tip

自定义Converter必须返回 OAuth 2.0 访问令牌请求的有效RequestEntity表示,预期的 OAuth 2.0 提供程序可以理解。

另一方面,如果您需要自定义令牌响应的后处理,则需要为DefaultAuthorizationCodeTokenResponseClient.setRestOperations()提供自定义配置的RestOperations。默认RestOperations配置如下:

RestTemplate restTemplate = new RestTemplate(Arrays.asList(
        new FormHttpMessageConverter(),
        new OAuth2AccessTokenResponseHttpMessageConverter()));

restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());

Tip

发送 OAuth 2.0 访问令牌请求时使用 Spring MVC FormHttpMessageConverter,因为它是必需的。

OAuth2AccessTokenResponseHttpMessageConverterHttpMessageConverter,代表 OAuth 2.0 访问令牌响应。您可以为OAuth2AccessTokenResponseHttpMessageConverter.setTokenResponseConverter()提供一个自定义Converter<Map<String, String>, OAuth2AccessTokenResponse>,该自定义Converter<Map<String, String>, OAuth2AccessTokenResponse>用于将 OAuth 2.0 访问令牌响应参数转换为OAuth2AccessTokenResponse

OAuth2ErrorResponseErrorHandlerResponseErrorHandler,可以处理 OAuth 2.0 错误(400 错误请求)。它使用OAuth2ErrorHttpMessageConverter将 OAuth 2.0 错误参数转换为OAuth2Error

无论您是自定义DefaultAuthorizationCodeTokenResponseClient还是提供自己的OAuth2AccessTokenResponseClient的实现,都需要按以下示例所示进行配置:

@EnableWebSecurity
public class OAuth2ClientSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .oauth2Client()
                .authorizationCodeGrant()
                    .accessTokenResponseClient(this.customAccessTokenResponseClient())
                    ...
    }

    private OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> customAccessTokenResponseClient() {
        ...
    }
}

6.7 OAuth 2.0 登录

OAuth 2.0 登录功能为应用程序提供了使用户能够通过使用他们在 OAuth 2.0 提供程序(例如 GitHub)或 OpenID Connect 1.0 提供程序(例如 Google)上的现有帐户登录到该应用程序的功能。 OAuth 2.0 登录实现了以下用例:“使用 Google 登录”或“使用 GitHub 登录”。

Note

OAuth 2.0 登录通过使用OAuth 2.0 授权框架OpenID Connect 核心 1.0中指定的 Authorization Code Grant 来实现。

6.7.1 Spring Boot 2.x 示例

Spring Boot 2.x 为 OAuth 2.0 登录带来了完整的自动配置功能。

本部分说明如何使用* Google 作为 Authentication Provider *配置OAuth 2.0 登录示例,并涵盖以下主题:

Initial setup

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

Note

请按照“设置 OAuth 2.0”部分开始的OpenID Connect页上的说明进行操作。

完成“获取 OAuth 2.0 凭据”说明后,您应该拥有一个新的 OAuthClient 端,其凭据由 Client 端 ID 和 Client 端密钥组成。

设置重定向 URI

重定向 URI 是最终用户的用户代理在通过 Google 身份验证并授予“同意”页面上的 OAuthClient 端*(在上一步中创建)*的访问权限后,将重定向到该应用程序中的路径。

在“设置重定向 URI”子部分中,确保“授权重定向 URI”字段设置为http://localhost:8080/login/oauth2/code/google

Tip

默认重定向 URI 模板为{baseUrl}/login/oauth2/code/{registrationId}。 *** registrationId** *是ClientRegistration的唯一标识符。

Configure application.yml

现在,您有了 Google 的新 OAuthClient 端,您需要配置应用程序以将 OAuthClient 端用于身份验证流程。为此:

  • 转到application.yml并设置以下配置:
spring:
  security:
    oauth2:
      client:
        registration:   (1)
          google:   (2)
            client-id: google-client-id
            client-secret: google-client-secret

实施例 6.1. OAuthClient 端属性

  • (1) spring.security.oauth2.client.registration是 OAuthClient 端属性的基本属性前缀。

  • (2) 基本属性前缀后面是ClientRegistration的 ID,例如 google。

  • client-idclient-secret属性中的值替换为您先前创建的 OAuth 2.0 凭据。

启动应用程序

启动 Spring Boot 2.x 示例并转到http://localhost:8080。然后,您将被重定向到默认的“自动生成”登录页面,该页面显示了 Google 的链接。

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

在使用您的 Google 帐户凭据进行身份验证之后,显示给您的下一个页面是“同意”屏幕。 “同意”屏幕要求您允许或拒绝访问您之前创建的 OAuthClient 端。点击“允许”以授权 OAuthClient 端访问您的电子邮件地址和基本 Profile 信息。

此时,OAuthClient 端会从UserInfo Endpoint检索您的电子邮件地址和基本 Profile 信息,并构建经过身份验证的会话。

6.7.2 Spring Boot 2.x 属性 Map

下表概述了 Spring Boot 2.x OAuthClient 端属性到ClientRegistration属性的 Map。

Spring Boot 2.xClientRegistration
spring.security.oauth2.client.registration.[registrationId]registrationId
spring.security.oauth2.client.registration.[registrationId].client-idclientId
spring.security.oauth2.client.registration.[registrationId].client-secretclientSecret
spring.security.oauth2.client.registration.[registrationId].client-authentication-methodclientAuthenticationMethod
spring.security.oauth2.client.registration.[registrationId].authorization-grant-typeauthorizationGrantType
spring.security.oauth2.client.registration.[registrationId].redirect-uriredirectUriTemplate
spring.security.oauth2.client.registration.[registrationId].scopescopes
spring.security.oauth2.client.registration.[registrationId].client-nameclientName
spring.security.oauth2.client.provider.[providerId].authorization-uriproviderDetails.authorizationUri
spring.security.oauth2.client.provider.[providerId].token-uriproviderDetails.tokenUri
spring.security.oauth2.client.provider.[providerId].jwk-set-uriproviderDetails.jwkSetUri
spring.security.oauth2.client.provider.[providerId].user-info-uriproviderDetails.userInfoEndpoint.uri
spring.security.oauth2.client.provider.[providerId].user-info-authentication-methodproviderDetails.userInfoEndpoint.authenticationMethod
spring.security.oauth2.client.provider.[providerId].userNameAttributeproviderDetails.userInfoEndpoint.userNameAttributeName

6.7.3 CommonOAuth2Provider

CommonOAuth2Providersched 义了许多知名提供商的默认 Client 端属性集:Google,GitHub,Facebook 和 Okta。

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

如前所述,当我们配置了 GoogleClient 端时,仅需要client-idclient-secret属性。

以下清单显示了一个示例:

spring:
  security:
    oauth2:
      client:
        registration:
          google:
            client-id: google-client-id
            client-secret: google-client-secret

Tip

Client 端属性的自动默认设置在这里可以无缝工作,因为registrationId(google)与CommonOAuth2Provider中的GOOGLE enum(不区分大小写)匹配。

对于您可能想要指定其他registrationId的情况(例如google-login),您仍然可以通过配置provider属性来利用 Client 端属性的自动默认设置。

以下清单显示了一个示例:

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()中设置的 Client 端属性的自动默认设置。

6.7.4 配置自定义提供程序属性

有些 OAuth 2.0 提供程序支持多租户,这会导致每个租户(或子域)使用不同的协议端点。

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

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

以下清单显示了一个示例:

spring:
  security:
    oauth2:
      client:
        registration:
          okta:
            client-id: okta-client-id
            client-secret: okta-client-secret
        provider:
          okta: (1)
            authorization-uri: https://your-subdomain.oktapreview.com/oauth2/v1/authorize
            token-uri: https://your-subdomain.oktapreview.com/oauth2/v1/token
            user-info-uri: https://your-subdomain.oktapreview.com/oauth2/v1/userinfo
            user-name-attribute: sub
            jwk-set-uri: https://your-subdomain.oktapreview.com/oauth2/v1/keys
  • (1) 基本属性(spring.security.oauth2.client.provider.okta)允许对协议端点位置进行自定义配置。

6.7.5 覆盖 Spring Boot 2.x 自动配置

用于 OAuthClient 端支持的 Spring Boot 2.x 自动配置类为OAuth2ClientAutoConfiguration

它执行以下任务:

  • 从已配置的 OAuthClient 端属性中注册由ClientRegistration个组成的ClientRegistrationRepository @Bean

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

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

注册一个 ClientRegistrationRepository @Bean

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

@Configuration
public class OAuth2LoginConfig {

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

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

提供一个 WebSecurityConfigurerAdapter

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

@EnableWebSecurity
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {

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

完全覆盖自动配置

以下示例显示如何通过注册ClientRegistrationRepository @Bean并提供WebSecurityConfigurerAdapter来完全覆盖自动配置。

@Configuration
public class OAuth2LoginConfig {

    @EnableWebSecurity
    public static class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {

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

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

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

6.7.6 没有 Spring Boot 2.x 的 Java 配置

如果您无法使用 Spring Boot 2.x,并且想在CommonOAuth2Provider中配置一个 sched 义的提供程序(例如 Google),请应用以下配置:

@Configuration
public class OAuth2LoginConfig {

    @EnableWebSecurity
    public static class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {

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

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

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

    @Bean
    public OAuth2AuthorizedClientRepository authorizedClientRepository(
            OAuth2AuthorizedClientService authorizedClientService) {
        return new AuthenticatedPrincipalOAuth2AuthorizedClientRepository(authorizedClientService);
    }

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

6.7.7 其他资源

以下其他资源描述了高级配置选项:

6.8 OAuth 2.0 资源服务器

Spring Security 支持使用JWT编码的 OAuth 2.0 Bearer Tokens保护端点。

在应用程序将其权限 Management 联合到authorization server(例如 Okta 或 Ping Identity)的情况下,这很方便。资源服务器可以咨询该授权服务器,以在处理请求时验证权限。

Note

完整的工作示例可以在OAuth 2.0 资源服务器 Servlet 示例中找到。

6.8.1 Dependencies

大多数资源服务器支持都收集在spring-security-oauth2-resource-server中。但是,对_JWT 进行解码和验证的支持在spring-security-oauth2-jose中,这意味着两者都必须具备,才能使工作的资源服务器支持 JWT 编码的承载令牌。

6.8.2 最低配置

使用Spring Boot时,将应用程序配置为资源服务器包括两个基本步骤。首先,包括所需的依赖关系,其次,指示授权服务器的位置。

指定授权服务器

要指定使用哪个授权服务器,只需执行以下操作:

security:
  oauth2:
    resourceserver:
      jwt:
        issuer-uri: https://idp.example.com

其中https://idp.example.comiss索赔中包含的授权服务器将发出的 JWT 令牌的值。资源服务器将使用此属性进行进一步的自我配置,发现授权服务器的公钥,然后验证传入的 JWT。

Note

要使用issuer-uri属性,还必须确保https://idp.example.com/.well-known/openid-configuration是授权服务器支持的端点。该端点称为Provider Configuration端点。

就是这样!

Startup Expectations

使用此属性和这些依赖关系时,资源服务器将自动配置自身以验证 JWT 编码的承载令牌。

它通过确定性的启动过程来实现:

  • 点击提供者配置端点https://idp.example.com/.well-known/openid-configuration,处理jwks_url属性的响应

  • 配置验证策略以查询jwks_url有效的公共密钥

  • 配置验证策略,以针对https://idp.example.com验证每个 JWT iss的声明。

此过程的结果是,授权服务器必须启动并接收请求,才能成功启动资源服务器。

Note

如果在资源服务器查询授权服务器时授权服务器已关闭(给出适当的超时),则启动将失败。

Runtime Expectations

应用程序启动后,资源服务器将尝试处理任何包含Authorization: BearerHeaders 的请求:

GET / HTTP/1.1
Authorization: Bearer some-token-value # Resource Server will process this

只要指示了此方案,资源服务器就会尝试根据 Bearer Token 规范处理请求。

给定格式正确的 JWT 令牌,资源服务器将

  • 根据启动期间从jwks_url端点获取并与 JWT 头匹配的公钥来验证其签名

  • 验证 JWT expnbf的时间戳以及 JWT iss的声明,并

  • 将每个范围 Map 到具有前缀SCOPE_的授权。

Note

当授权服务器提供新的密钥时,Spring Security 将自动旋转用于验证 JWT 令牌的密钥。

默认情况下,结果Authentication#getPrincipal是 Spring Security Jwt对象,并且Authentication#getNameMap 到 JWT 的sub属性(如果存在)。

从这里,考虑跳到:

如何在不将资源服务器启动绑定到授权服务器的可用性的情况下进行配置

如何在没有 Spring Boot 的情况下进行配置

6.8.3 直接指定授权服务器 JWK 设置 Uri

如果授权服务器不支持提供者配置端点,或者资源服务器必须能够独立于授权服务器启动,则可以将issuer-uri交换为jwk-set-uri

security:
  oauth2:
    resourceserver:
      jwt:
        jwk-set-uri: https://idp.example.com/.well-known/jwks.json

Note

JWK Set uri 尚未标准化,但是通常可以在授权服务器的文档中找到

因此,资源服务器在启动时不会对授权服务器执行 ping 操作。但是,它也将不再验证 JWT 中的iss声明(因为 Resource Server 不再知道发行者的值应该是什么)。

Note

此属性也可以直接在DSL上提供。

6.8.4 覆盖或替换引导自动配置

Spring Boot 代表资源服务器生成两个@Bean

第一个是WebSecurityConfigurerAdapter,它将应用程序配置为资源服务器:

protected void configure(HttpSecurity http) {
    http
        .authorizeRequests()
            .anyRequest().authenticated()
            .and()
        .oauth2ResourceServer()
            .jwt();
}

如果应用程序未公开WebSecurityConfigurerAdapter bean,那么 Spring Boot 将公开以上默认的WebSecurityConfigurerAdapter bean。

替换它就像在应用程序中公开 Bean 一样简单:

@EnableWebSecurity
public class MyCustomSecurityConfiguration extends WebSecurityConfigurerAdapter {
    protected void configure(HttpSecurity http) {
        http
            .authorizeRequests()
                .mvcMatchers("/messages/**").hasAuthority("SCOPE_message:read")
                .anyRequest().authenticated()
                .and()
            .oauth2ResourceServer()
                .jwt()
                    .jwtAuthenticationConverter(myConverter());
    }
}

以上要求以/messages/开头的任何 URL 的范围为message:read

oauth2ResourceServer DSL 上的方法还将覆盖或替换自动配置。

例如,第二个@Bean Spring Boot 创建的是JwtDecoder,它将String令牌解码为Jwt的经过验证的实例:

@Bean
public JwtDecoder jwtDecoder() {
    return JwtDecoders.fromOidcIssuerLocation(issuerUri);
}

如果应用程序未公开JwtDecoder bean,那么 Spring Boot 将公开以上默认的JwtDecoder bean。

可以使用jwkSetUri()覆盖其配置,也可以使用decoder()替换其配置。

Using jwkSetUri()

授权服务器的 JWK 设置 Uri 可以配置为作为配置属性,也可以在 DSL 中提供:

@EnableWebSecurity
public class DirectlyConfiguredJwkSetUri extends WebSecurityConfigurerAdapter {
    protected void configure(HttpSecurity http) {
        http
            .authorizeRequests()
                .anyRequest().authenticated()
                .and()
            .oauth2ResourceServer()
                .jwt()
                    .jwkSetUri("https://idp.example.com/.well-known/jwks.json");
    }
}

使用jwkSetUri()优先于任何配置属性。

Using decoder()

jwkSetUri()更强大的是decoder(),它将完全取代JwtDecoder的所有 Boot 自动配置:

@EnableWebSecurity
public class DirectlyConfiguredJwkSetUri extends WebSecurityConfigurerAdapter {
    protected void configure(HttpSecurity http) {
        http
            .authorizeRequests()
                .anyRequest().authenticated()
                .and()
            .oauth2ResourceServer()
                .jwt()
                    .decoder(myCustomDecoder());
    }
}

当需要更深的配置(例如validationmappingrequest timeouts)时,这非常方便。

公开 JwtDecoder @Bean

或者,暴露JwtDecoder @Beandecoder()具有相同的效果:

@Bean
public JwtDecoder jwtDecoder() {
    return new NimbusJwtDecoderJwkSupport(jwkSetUri);
}

6.8.5 配置授权

从 OAuth 2.0 授权服务器发出的 JWT 通常具有scopescp属性,指示已被授予的作用域(或权限),例如:

{ …, "scope" : "messages contacts"}

在这种情况下,资源服务器将尝试将这些作用域强制为已授予权限的列表,并为每个作用域添加字符串“ SCOPE_”作为前缀。

这意味着为了保护具有从 JWT 派生的作用域的端点或方法,相应的表达式应包含以下前缀:

@EnableWebSecurity
public class DirectlyConfiguredJwkSetUri extends WebSecurityConfigurerAdapter {
    protected void configure(HttpSecurity http) {
        http
            .authorizeRequests()
                .mvcMatchers("/contacts/**").hasAuthority("SCOPE_contacts")
                .mvcMatchers("/messages/**").hasAuthority("SCOPE_messages")
                .anyRequest().authenticated()
                .and()
            .oauth2ResourceServer()
                .jwt();
    }
}

或类似地具有方法安全性:

@PreAuthorize("hasAuthority('SCOPE_messages')")
public List<Message> getMessages(...) {}

手动提取权限

但是,在许多情况下,此默认设置不足。例如,某些授权服务器不使用scope属性,而是使用自己的自定义属性。或者,在其他时间,资源服务器可能需要将属性或属性组成调整为内部化的权限。

为此,DSL 公开了jwtAuthenticationConverter()

@EnableWebSecurity
public class DirectlyConfiguredJwkSetUri extends WebSecurityConfigurerAdapter {
    protected void configure(HttpSecurity http) {
        http
            .authorizeRequests()
                .anyRequest().authenticated()
                .and()
            .oauth2ResourceServer()
                .jwt()
                    .jwtAuthenticationConverter(grantedAuthoritiesExtractor());
    }
}

Converter<Jwt, AbstractAuthenticationToken> grantedAuthoritiesExtractor() {
    return new GrantedAuthoritiesExtractor();
}

负责将Jwt转换为Authentication

我们可以很简单地覆盖此方法,以更改授予权限的方式:

static class GrantedAuthoritiesExtractor extends JwtAuthenticationConverter {
    protected Collection<GrantedAuthorities> extractAuthorities(Jwt jwt) {
        Collection<String> authorities = (Collection<String>)
                jwt.getClaims().get("mycustomclaim");

        return authorities.stream()
                .map(SimpleGrantedAuthority::new)
                .collect(Collectors.toList());
    }
}

为了获得更大的灵 Active,DSL 支持使用实现Converter<Jwt, AbstractAuthenticationToken>的任何类完全替代转换器:

static class CustomAuthenticationConverter implements Converter<Jwt, AbstractAuthenticationToken> {
    public AbstractAuthenticationToken convert(Jwt jwt) {
        return new CustomAuthenticationToken(jwt);
    }
}

6.8.6 配置验证

使用指示授权服务器的发行者 uri,资源服务器将默认验证iss声明以及expnbf时间戳声明。

在需要自定义验证的情况下,资源服务器附带两个标准验证器,并且还接受自定义的OAuth2TokenValidator实例。

自定义时间戳验证

JWT 通常具有有效期窗口,该窗口的开始在nbf声明中指示,而结束在exp声明中指示。

但是,每台服务器都会经历时钟漂移,这可能导致令牌在一个服务器上显得过期,而在另一台服务器上过期。随着分布式系统中协作服务器数量的增加,这可能会导致某些实现上的胃口。

资源服务器使用JwtTimestampValidator来验证令牌的有效性窗口,并且可以将其配置为clockSkew来缓解上述问题:

@Bean
JwtDecoder jwtDecoder() {
     NimbusJwtDecoderJwkSupport jwtDecoder = (NimbusJwtDecoderJwkSupport)
             JwtDecoders.withOidcIssuerLocation(issuerUri);

     OAuth2TokenValidator<Jwt> withClockSkew = new DelegatingOAuth2TokenValidator<>(
            new JwtTimestampValidator(Duration.ofSeconds(60)),
            new IssuerValidator(issuerUri));

     jwtDecoder.setJwtValidator(withClockSkew);

     return jwtDecoder;
}

Note

默认情况下,资源服务器将时钟偏差配置为 30 秒。

配置自定义验证器

使用OAuth2TokenValidator API 可以为aud声明添加支票很简单:

public class AudienceValidator implements OAuth2TokenValidator<Jwt> {
    OAuth2Error error = new OAuth2Error("invalid_token", "The required audience is missing", null);

    public OAuth2TokenValidatorResult validate(Jwt jwt) {
        if (jwt.getAudience().contains("messaging")) {
            return OAuth2TokenValidatorResult.success();
        } else {
            return OAuth2TokenValidatorResult.failure(error);
        }
    }
}

然后,要添加到资源服务器中,只需指定JwtDecoder实例即可:

@Bean
JwtDecoder jwtDecoder() {
    NimbusJwtDecoderJwkSupport jwtDecoder = (NimbusJwtDecoderJwkSupport)
        JwtDecoders.withOidcIssuerLocation(issuerUri);

    OAuth2TokenValidator<Jwt> audienceValidator = new AudienceValidator();
    OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer(issuerUri);
    OAuth2TokenValidator<Jwt> withAudience = new DelegatingOAuth2TokenValidator<>(withIssuer, audienceValidator);

    jwtDecoder.setJwtValidator(withAudience);

    return jwtDecoder;
}

6.8.7 配置声明集 Map

Spring Security 使用Nimbus库来解析 JWT 并验证其签名。因此,Spring Security 受制于 Nimbus 对每个字段值以及如何将每个字段值强制转换为 Java 类型的解释。

例如,由于 Nimbus 仍与 Java 7 兼容,因此它不使用Instant来表示时间戳字段。

并且完全有可能使用其他库或进行 JWT 处理,这可能会自行做出需要调整的决策。

或者,很简单,出于特定于域的原因,资源服务器可能希望从 JWT 中添加或删除声明。

为此,Resource Server 支持将 JWT 声明集与MappedJwtClaimSetConverterMap。

自定义单个声明的转换

默认情况下,MappedJwtClaimSetConverter将尝试将声明强制转换为以下类型:

ClaimJava Type
audCollection<String>
expInstant
iatInstant
issString
jtiString
nbfInstant
subString

可以使用MappedJwtClaimSetConverter.withDefaults配置单个声明的转换策略:

@Bean
JwtDecoder jwtDecoder() {
    NimbusJwtDecoderJwkSupport jwtDecoder = new NimbusJwtDecoderJwkSupport(jwkSetUri);

    MappedJwtClaimSetConverter converter = MappedJwtClaimSetConverter
            .withDefaults(Collections.singletonMap("sub", this::lookupUserIdBySub));
    jwtDecoder.setJwtClaimSetConverter(converter);

    return jwtDecoder;
}

这将保留所有默认值,除了它将覆盖sub的默认声明转换器。

添加声明

MappedJwtClaimSetConverter还可以用于添加自定义声明,例如以适应现有系统:

MappedJwtClaimSetConverter.withDefaults(Collections.singletonMap("custom", custom -> "value"));

删除索赔

使用相同的 API 删除声明也很简单:

MappedJwtClaimSetConverter.withDefaults(Collections.singletonMap("legacyclaim", legacy -> null));

重命名声明

在更复杂的场景中,例如一次查询多个声明或重命名一个声明,Resource Server 接受实现Converter<Map<String, Object>, Map<String,Object>>的任何类:

public class UsernameSubClaimAdapter implements Converter<Map<String, Object>, Map<String, Object>> {
    private final MappedJwtClaimSetConverter delegate =
            MappedJwtClaimSetConverter.withDefaults(Collections.emptyMap());

    public Map<String, Object> convert(Map<String, Object> claims) {
        Map<String, Object> convertedClaims = this.delegate.convert(claims);

        String username = (String) convertedClaims.get("user_name");
        convertedClaims.put("sub", username);

        return convertedClaims;
    }
}

然后,可以像平常一样提供实例:

@Bean
JwtDecoder jwtDecoder() {
    NimbusJwtDecoderJwkSupport jwtDecoder = new NimbusJwtDecoderJwkSupport(jwkSetUri);
    jwtDecoder.setJwtClaimSetConverter(new UsernameSubClaimAdapter());
    return jwtDecoder;
}

6.8.8 配置超时

默认情况下,资源服务器使用 30 秒钟的连接和套接字超时来与授权服务器进行协调。

在某些情况下,这可能太短了。此外,它不考虑退避和发现等更复杂的模式。

要调整资源服务器连接到授权服务器的方式,NimbusJwtDecoderJwkSupport接受RestOperations的实例:

@Bean
public JwtDecoder jwtDecoder(RestTemplateBuilder builder) {
    RestOperations rest = builder
            .setConnectionTimeout(60000)
            .setReadTimeout(60000)
            .build();

    NimbusJwtDecoderJwkSupport jwtDecoder = new NimbusJwtDecoderJwkSupport(jwkSetUri);
    jwtDecoder.setRestOperations(rest);
    return jwtDecoder;
}

6.9 Authentication

到目前为止,我们只看了最基本的身份验证配置。让我们看一下配置身份验证的一些高级选项。

6.9.1 内存中身份验证

我们已经看到了为单个用户配置内存中身份验证的示例。下面是配置多个用户的示例:

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

6.9.2 JDBC 身份验证

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

@Autowired
private DataSource dataSource;

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

6.9.3 LDAP 认证

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

@Autowired
private DataSource dataSource;

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

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

users.ldif.

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

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

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

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

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

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

6.9.4 AuthenticationProvider

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

Note

仅在未填充AuthenticationManagerBuilder时使用

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

6.9.5 UserDetailsService

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

Note

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

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

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

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

6.10 多个 HttpSecurity

我们可以配置多个 HttpSecurity 实例,就像我们可以拥有多个<http>块一样。关键是要多次扩展WebSecurityConfigurationAdapter。例如,以下是对以/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)
                .authorizeRequests()
                    .anyRequest().hasRole("ADMIN")
                    .and()
                .httpBasic();
        }
    }

    @Configuration                                                   (4)
    public static class FormLoginWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {

        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                .authorizeRequests()
                    .anyRequest().authenticated()
                    .and()
                .formLogin();
        }
    }
}
  • (1) 正常配置身份验证
  • (2) 创建一个包含@OrderWebSecurityConfigurerAdapter实例,以指定应首先考虑哪个WebSecurityConfigurerAdapter
  • (3) http.antMatcher声明此HttpSecurity仅适用于以/api/开头的 URL
  • (4) 创建WebSecurityConfigurerAdapter的另一个实例。如果网址不是以/api/开头,则将使用此配置。在ApiWebSecurityConfigurationAdapter之后考虑此配置,因为它在1之后具有@Order值(没有@Order默认为最后)。

6.11 方法安全性

从 2.0 版开始,Spring Security 大大改善了对为服务层方法增加安全性的支持。它提供对 JSR-250Comments 安全性以及框架原始@SecuredComments 的支持。从 3.0 开始,您还可以使用新的expression-based annotations。您可以使用intercept-methods元素修饰 bean 声明,从而对单个 bean 应用安全性,或者可以使用 AspectJ 样式切入点在整个服务层中保护多个 bean。

6.11.1 EnableGlobalMethodSecurity

我们可以在任何@Configuration实例上使用@EnableGlobalMethodSecurityComments 启用基于 Comments 的安全性。例如,以下将启用 Spring Security 的@SecuredComments。

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

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

public interface BankService {

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

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

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

可以使用以下命令启用对 JSR-250 注解的支持

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

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

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

而等效的 Java 代码将是

public interface BankService {

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

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

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

6.11.2 GlobalMethodSecurityConfiguration

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

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

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

6.12 后处理配置的对象

Spring Security 的 Java 配置并未公开其配置的每个对象的每个属性。这简化了大多数用户的配置。毕竟,如果每个属性都公开,则用户可以使用标准 Bean 配置。

尽管有充分的理由不直接公开每个属性,但用户可能仍需要更多高级配置选项。为了解决这个问题,Spring Security 引入了ObjectPostProcessor的概念,该概念可用于修改或替换 Java 配置创建的许多 Object 实例。例如,如果要在FilterSecurityInterceptor上配置filterSecurityPublishAuthorizationSuccess属性,则可以使用以下命令:

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

6.13 自定义 DSL

您可以在 Spring Security 中提供自己的自定义 DSL。例如,您可能会有类似以下内容的内容:

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

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

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

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

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

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

Note

实际上就是实现HttpSecurity.authorizeRequests()之类的方法的方式。

然后可以像下面这样使用自定义 DSL:

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

该代码按以下 Sequences 调用:

  • 调用``的 configure 方法中的代码

  • MyCustomDsl 的 init 方法中的代码被调用

  • MyCustomDsl 的 configure 方法中的代码被调用

如果需要,可以使用SpringFactories默认情况下WebSecurityConfiguerAdapter添加MyCustomDsl。例如,您将在名为META-INF/spring.factories的 Classpath 上创建具有以下内容的资源:

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