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的概况
在开始之前,重要的是我们正确理解一些基本概念。 建议先阅读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 |
使用这种方法,将不会与授权服务器进行交互,但是,当然,这意味着令牌签名配置在更改方面的灵活性较低。
与关键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存储库中找到这两种服务。