Spring Security OAuth2应用程序中的JWS + JWK

JWS + JWK in a Spring Security OAuth2 Application

1.概述

在本教程中,我们将学习JSON Web签名(JWS),以及如何在配置了Spring Security OAuth2的应用程序上使用JSON Web密钥(JWK)规范来实现它。

我们应该记住,即使Spring正在努力将所有Spring Security OAuth功能迁移到Spring Security框架,本指南仍然是了解这些规范的基本概念的一个很好的起点,并且在当时应该派上用场 在任何框架上实施它们。

首先,我们将尝试了解基本概念; 例如JWS和JWK,它们的用途以及我们如何轻松配置资源服务器以使用此OAuth解决方案。

然后,我们将进行更深入的研究,通过分析OAuth2 Boot在幕后所做的事情以及设置使用JWK的授权服务器来详细分析规范。

2.了解JWS和JWK的概况

 width=

 width=

在开始之前,重要的是我们正确理解一些基本概念。 建议先阅读OAuth和JWT文章,因为这些主题不在本教程的讨论范围之内。

JWS是由IETF创建的规范,描述了用于验证数据完整性的不同加密机制,即JSON Web令牌(JWT)中的数据,它定义了一个JSON结构,该结构包含必要的信息。

这是广泛使用的JWT规范中的一个关键方面,因为要求对声明进行签名或加密才能被认为是有效的。

在第一种情况下,JWT表示为JWS。 如果是加密的,则JWT将以JSON Web加密(JWE)结构进行编码。

使用OAuth时,最常见的情况是刚刚签署了JWT。 这是因为我们通常不需要"隐藏"信息,而只需验证数据的完整性。

当然,无论我们是处理签名的JWT还是加密的JWT,我们都需要正式的准则以能够有效地传输公共密钥。

这就是JWK的目的,JWK是一种JSON结构,它表示一个加密密钥,也由IETF定义。

许多身份验证提供程序都提供" JWK集"端点,该端点也在规范中定义。 有了它,其他应用程序可以找到有关公共密钥的信息以处理JWT。

例如,资源服务器使用JWT中存在的kid(Key Id)字段在JWK集中找到正确的密钥。

2.1。 使用JWK实施解决方案
<

通常,如果我们希望我们的应用程序以安全的方式提供资源,例如使用标准的安全协议(例如OAuth 2.0),则需要执行以下步骤:

  • 在授权服务器中注册客户端-在我们自己的服务中,或在Okta,Facebook或Github等知名提供商中

  • 这些客户端将按照我们可能已配置的任何OAuth策略向授权服务器请求访问令牌

  • 然后,他们将尝试访问将令牌(在本例中为JWT)呈现给资源服务器的资源

  • 资源服务器必须通过检查其签名来验证令牌是否未被操纵,并验证其声明。

  • 最后,我们的资源服务器检索资源,现在确保客户端具有正确的权限

  • 3. JWK和资源服务器配置

    稍后,我们将看到如何设置自己的授权服务器,该服务器为JWT和" JWK Set"端点提供服务。

    但是,在这一点上,我们将重点介绍最简单(也可能是最常见)的场景,该场景指向现有的授权服务器。

    我们要做的只是表明服务如何验证接收到的访问令牌,例如它应使用哪种公共密钥来验证JWT的签名。

    我们将使用Spring Security OAuth Autoconfigure功能,仅使用应用程序属性,以简单明了的方式实现这一目标。

    3.1。 Maven依赖

    我们需要将OAuth2自动配置依赖项添加到Spring应用程序的pom文件中:

    1
    2
    3
    4
    5
    <dependency>
        <groupId>org.springframework.security.oauth.boot</groupId>
        <artifactId>spring-security-oauth2-autoconfigure</artifactId>
        <version>2.1.6.RELEASE</version>
    </dependency>

    和往常一样,我们可以在Maven Central中检查工件的最新版本。

    请注意,该依赖项不是由Spring Boot管理的,因此我们需要指定其版本。

    无论如何,它应该与我们正在使用的Spring Boot的版本相匹配。

    3.2。 配置资源服务器

    接下来,我们必须使用@EnableResourceServer批注在我们的应用程序中启用资源服务器功能:

    1
    2
    3
    4
    5
    6
    7
    8
    @SpringBootApplication
    @EnableResourceServer
    public class ResourceServerApplication {

        public static void main(String[] args) {
            SpringApplication.run(ResourceServerApplication.class, args);
        }
    }

    现在,我们需要说明我们的应用程序如何获得必要的公钥,以验证其作为承载令牌收到的JWT的签名。

    OAuth2 Boot提供了不同的策略来验证令牌。

    如前所述,大多数授权服务器都通过一组密钥公开URI,其他服务可以使用这些密钥来验证签名。

    我们将配置本地授权服务器的JWK Set端点,我们将继续进行工作。

    让我们在application.properties中添加以下内容:

    1
    2
    security.oauth2.resource.jwk.key-set-uri=
      http://localhost:8081/sso-auth-server/.well-known/jwks.json

    在详细分析该主题时,我们将介绍其他策略。

    注意:新的Spring Security 5.1资源服务器仅支持JWK签名的JWT作为授权,并且Spring Boot还提供了一个非常相似的属性来配置JWK Set端点:

    1
    2
    spring.security.oauth2.resourceserver.jwk-set-uri=
      http://localhost:8081/sso-auth-server/.well-known/jwks.json

    3.3。 引擎盖下的弹簧配置

    我们之前添加的属性可以转换为创建几个Spring bean。

    更准确地说,OAuth2引导程序将创建:

  • 一个具有解码JWT并验证其签名的唯一能力的JwkTokenStore

  • aDefaultTokenServicesinstance使用以前的TokenStore

  • 4.授权服务器中的JWK设置端点

    现在,我们将更深入地研究该主题,在配置配置了颁发JWT并为其JWK Set端点提供服务的授权服务器时,将分析JWK和JWS的一些关键方面。

    请注意,由于Spring Security尚未提供用于设置授权服务器的功能,因此在此阶段,使用Spring Security OAuth功能创建服务器是唯一的选择。 不过,它将与Spring Security Resource Server兼容。

    4.1。 启用授权服务器功能

    第一步是将我们的授权服务器配置为在需要时发出访问令牌。

    我们还将像使用资源服务器一样添加spring-security-oauth2-autoconfigure依赖关系。

    首先,我们将使用@EnableAuthorizationServerannotation配置OAuth2授权服务器机制:

    1
    2
    3
    4
    5
    6
    7
    @Configuration
    @EnableAuthorizationServer
    public class JwkAuthorizationServerConfiguration {

        // ...

    }

    我们将使用属性注册OAuth 2.0客户端:

    1
    2
    security.oauth2.client.client-id=bael-client
    security.oauth2.client.client-secret=bael-secret

    有了这个,我们的应用程序将在请求时使用相应的凭据检索随机令牌:

    1
    2
    3
    4
    curl bael-client:bael-secret\
      @localhost:8081/sso-auth-server/oauth/token \
      -d grant_type=client_credentials \
      -d scope=any

    如我们所见,Spring Security OAuth默认检索随机字符串值,而不是JWT编码:

    1
    "access_token":"af611028-643f-4477-9319-b5aa8dc9408f"

    4.2。 发行JWT

    我们可以通过在上下文中创建JwtAccessTokenConverter bean来轻松更改此设置:

    1
    2
    3
    4
    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
        return new JwtAccessTokenConverter();
    }

    并在JwtTokenStore实例中使用它:

    1
    2
    3
    4
    @Bean
    public TokenStore tokenStore() {
        return new JwtTokenStore(accessTokenConverter());
    }

    因此,通过这些更改,让我们请求一个新的访问令牌,这一次,我们将获得准确编码为JWS的JWT。

    我们可以轻松地识别JWS; 它们的结构由以点分隔的三个字段(标头,有效负载和签名)组成:

    1
    2
    3
    4
    5
    "access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
      .
      eyJzY29wZSI6WyJhbnkiXSwiZXhwIjoxNTYxOTcy...
      .
      XKH70VUHeafHLaUPVXZI9E9pbFxrJ35PqBvrymxtvGI"

    默认情况下,Spring使用消息身份验证代码(MAC)方法对标头和有效负载进行签名。

    我们可以通过在许多JWT解码器/验证器在线工具之一中分析JWT来验证这一点。

    如果对获得的JWT进行解码,则会看到alg属性的值为HS256,这表明使用HMAC-SHA256算法对令牌进行签名。

    为了理解为什么我们不需要这种方法的JWK,我们必须了解MAC哈希函数是如何工作的。

    4.3。 默认的对称签名

    MAC哈希使用相同的密钥对消息进行签名并验证其完整性。 这是一个对称的哈希函数。

    因此,出于安全目的,该应用程序不能公开共享其签名密钥。

    仅出于学术原因,我们将公开Spring Security OAuth / oauth / token_key端点:

    1
    security.oauth2.authorization.token-key-access=permitAll()

    并且,当我们配置JwtAccessTokenConverterbean时,我们将自定义签名密钥值:

    1
    converter.setSigningKey("bael");

    确切知道正在使用哪个对称密钥。

    注意:即使我们不发布签名密钥,设置弱签名密钥也可能会对字典攻击造成威胁。

    一旦知道签名密钥,我们就可以使用前面提到的在线工具手动验证令牌的完整性。

    Spring Security OAuth库还配置了一个/ oauth / check_token端点,该端点验证并检索解码的JWT。

    此终结点还配置有denyAll()访问规则,应有意识地加以保护。 为此,我们可以像以前对令牌密钥那样使用security.oauth2.authorization.check-token-accessproperty。

    4.4。 资源服务器配置的替代方法

    根据我们的安全需求,我们可能认为适当保护最近提到的一个端点(同时使它们可被资源服务器访问)就足够了。

    如果是这种情况,那么我们可以按原样保留Authorization Server,然后为Resource Server选择另一种方法。

    资源服务器将期望授权服务器具有安全的端点,因此对于初学者来说,我们需要提供客户端凭据,并具有与授权服务器中使用的相同的属性:

    1
    2
    security.oauth2.client.client-id=bael-client
    security.oauth2.client.client-secret=bael-secret

    然后,我们可以选择使用/ oauth / check_token端点(也称为内省端点)或从/ oauth / token_key获取单个密钥:

    1
    2
    3
    4
    5
    6
    ## Single key URI:
    security.oauth2.resource.jwt.key-uri=
      http://localhost:8081/sso-auth-server/oauth/token_key
    ## Introspection endpoint:
    security.oauth2.resource.token-info-uri=
      http://localhost:8081/sso-auth-server/oauth/check_token

    或者,我们可以仅配置将用于验证资源服务中的令牌的密钥:

    1
    2
    ## Verifier Key
    security.oauth2.resource.jwt.key-value=bael

    使用这种方法,将不会与授权服务器进行交互,但是,当然,这意味着令牌签名配置在更改方面的灵活性较低。

    与关键URI策略一样,仅建议对非对称签名算法使用此后一种方法。

    4.5。 创建密钥库文件

    我们不要忘记我们的最终目标。 我们希望像最知名的提供者一样提供JWK Set端点。

    如果我们要共享密钥,那么最好使用非对称密码术(尤其是数字签名算法)来对令牌进行签名。

    第一步是创建密钥库文件。

    一种简单的方法是:

  • 在您方便使用的任何JDK或JRE的/ bin目录中打开命令行:

  • 1
    cd $JAVA_HOME/bin

  • 使用相应的参数运行keytool命令:

  • 1
    2
    3
    4
    5
    6
    ./keytool -genkeypair \
      -alias bael-oauth-jwt \
      -keyalg RSA \
      -keypass bael-pass \
      -keystore bael-jwt.jks \
      -storepass bael-pass

    请注意,我们在此处使用了非对称的RSA算法。

  • 回答交互式问题并生成密钥库文件

  • 4.6。 将密钥库文件添加到我们的应用程序

    我们必须将密钥库添加到我们的项目资源中。

    这是一个简单的任务,但是请记住,这是一个二进制文件。 这意味着它不能被过滤,否则将被损坏。

    如果使用的是Maven,另一种方法是将文本文件放在单独的文件夹中,并相应地配置pom.xml:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <build>
        <resources>
            <resource>
                <directory>src/main/resources</directory>
                <filtering>false</filtering>
            </resource>
            <resource>
                <directory>src/main/resources/filtered</directory>
                <filtering>true</filtering>
            </resource>
        </resources>
    </build>

    4.7。 配置令牌库

    下一步是使用密钥对配置TokenStore。 私人负责签署令牌,而公众负责验证完整性。

    我们将在类路径中使用密钥库文件创建一个KeyPairinstance,并在创建.jks文件时使用以下参数:

    1
    2
    3
    4
    5
    ClassPathResource ksFile =
      new ClassPathResource("bael-jwt.jks");
    KeyStoreKeyFactory ksFactory =
      new KeyStoreKeyFactory(ksFile,"bael-pass".toCharArray());
    KeyPair keyPair = ksFactory.getKeyPair("bael-oauth-jwt");

    然后,我们将在我们的JwtAccessTokenConverter bean中对其进行配置,删除任何其他配置:

    1
    converter.setKeyPair(keyPair);

    我们可以再次请求并解码JWT以检查已更改的alg参数。

    如果我们查看令牌密钥端点,我们将看到从密钥库获取的公共密钥。

    它可以通过PEM"封装边界"标头轻松识别。 以" --- BEGIN PUBLIC KEY ---"开头的字符串。

    4.8。 JWK设置端点依赖项

    Spring Security OAuth库不支持现成的JWK。

    因此,我们需要向我们的项目中添加另一个依赖项nimbus-jose-jwt,它提供一些基本的JWK实现:

    1
    2
    3
    4
    5
    <dependency>
        <groupId>com.nimbusds</groupId>
        <artifactId>nimbus-jose-jwt</artifactId>
        <version>7.3</version>
    </dependency>

    请记住,我们可以使用Maven Central知识库搜索引擎检查该图书馆的最新版本。

    4.9。 创建JWK设置端点

    首先,使用我们之前配置的KeyPair实例创建一个JWKSet bean:

    1
    2
    3
    4
    5
    6
    7
    8
    @Bean
    public JWKSet jwkSet() {
        RSAKey.Builder builder = new RSAKey.Builder((RSAPublicKey) keyPair().getPublic())
          .keyUse(KeyUse.SIGNATURE)
          .algorithm(JWSAlgorithm.RS256)
          .keyID("bael-key-id");
        return new JWKSet(builder.build());
    }

    现在创建端点非常简单:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @RestController
    public class JwkSetRestController {

        @Autowired
        private JWKSet jwkSet;

        @GetMapping("/.well-known/jwks.json")
        public Map<String, Object> keys() {
            return this.jwkSet.toJSONObject();
        }
    }

    我们在JWKSetinstance中配置的Key Id字段转换为kidparameter。

    此小子是密钥的任意别名,并且资源服务器通常使用它来从集合中选择正确的条目,因为JWT标头中应包含相同的密钥。

    我们现在面临一个新问题。 由于Spring Security OAuth不支持JWK,因此颁发的JWT将不包含kidHeader。

    让我们找到一种解决方法来解决此问题。

    4.10。 将孩子的价值添加到JWT标头中

    我们将创建一个新类,扩展我们一直在使用的JwtAccessTokenConverter,并允许将标头条目添加到JWT:

    1
    2
    3
    4
    5
    6
    public class JwtCustomHeadersAccessTokenConverter
      extends JwtAccessTokenConverter {

        // ...

    }

    首先,我们需要:

  • 像我们一直在配置父类,设置我们配置的KeyPair

  • 从密钥库中获取使用私钥的Signer对象

  • 当然,我们要添加到结构中的自定义标头的集合

  • 让我们基于此配置构造函数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    private Map<String, String> customHeaders = new HashMap<>();
    final RsaSigner signer;

    public JwtCustomHeadersAccessTokenConverter(
      Map<String, String> customHeaders,
      KeyPair keyPair) {
        super();
        super.setKeyPair(keyPair);
        this.signer = new RsaSigner((RSAPrivateKey) keyPair.getPrivate());
        this.customHeaders = customHeaders;
    }

    现在,我们将覆盖编码方法。 我们的实现与父代实现相同,唯一的区别是在创建String令牌时我们还将传递自定义标头:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    private JsonParser objectMapper = JsonParserFactory.create();

    @Override
    protected String encode(OAuth2AccessToken accessToken,
      OAuth2Authentication authentication) {
        String content;
        try {
            content = this.objectMapper
              .formatMap(getAccessTokenConverter()
              .convertAccessToken(accessToken, authentication));
        } catch (Exception ex) {
            throw new IllegalStateException(
             "Cannot convert access token to JSON", ex);
        }
        String token = JwtHelper.encode(
          content,
          this.signer,
          this.customHeaders).getEncoded();
        return token;
    }

    现在,在创建JwtAccessTokenConverter bean时使用此类:

    1
    2
    3
    4
    5
    6
    7
    8
    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
        Map<String, String> customHeaders =
          Collections.singletonMap("kid","bael-key-id");
        return new  JwtCustomHeadersAccessTokenConverter(
          customHeaders,
          keyPair());
    }

    我们准备出发了。 请记住,将资源服务器的属性改回来。 我们只需要使用在教程开始时设置的key-set-uri属性。

    我们可以要求一个访问令牌,检查它的孩子价值,并用它来请求资源。

    检索公用密钥后,资源服务器将其存储在内部,并将其映射到密钥ID以供将来请求。

    5.结论

    在这份有关JWT,JWS和JWK的综合指南中,我们学到了很多东西。 不仅是特定于Spring的配置,还包括一般的安全性概念,并通过一个实际的例子将其付诸实践。

    我们已经看到了使用JWK Set端点处理JWT的资源服务器的基本配置。

    最后,我们通过设置有效地公开JWK Set端点的授权服务器,扩展了Spring Security OAuth的基本功能。

    与往常一样,我们可以在OAuth Github存储库中找到这两种服务。