OAuth2:使用JWT和加密签名


1、简介

使用加密签名验证令牌的优点是允许资源服务器验证令牌,而不需要直接调用授权服务器,也不需要共享数据库。这种令牌验证的方法通常用于使用OAuth2实现身份验证和授权的系统。

2、使用JWT以及对称密钥签名的令牌

用于令牌签名的最简单的方法是使用对称密钥。在这种方法中,使用相同的密钥,既可以签署一个令牌,又可以验证它的签名。使用对称密钥的优点是更简单、速度更快。它也有缺点。不能总是与身份验证过程中涉及的所有应用程序共享用于签名令牌的密钥。

现在我们创建两个项目,一个授权服务器,一个资源服务器。

2.1 使用JWT

JWT是一个令牌实现。令牌由三部分组成:头信息、主体和签名。头信息和主体中的详情用JSON表示,并且它们是Base64编码的。第三部分是签名,这是一种加密算法生成的,该算法使用头信息和主体作为其输入。密码算法还意味着需要密钥。密钥就像一个密码。拥有真正密钥的所有者可以签署令牌或验证签名的真实性。如果令牌上的签名是真实的,就可以确保在签名之后没有人修改令牌。

2.2 实现授权服务器以颁发JWT

这里将会实现一个授权服务器,该服务器会向客户端颁发JWT以进行授权。管理令牌的组件是TokenStore。这里要做的是使用Spring Security提供的TokenStore的另一种实现。这里要使用的实现的名称是JwtTokenStore,它会管理JWT。稍后,我们将实现一个资源服务器,并开发一个使用JWT的完整系统。可以两种方式使用JWT实现令牌验证:

  • 如果使用相同的密钥对令牌进行签名和验证签名,就可以说该密钥是对称的。

  • 如果使用一个密钥签名令牌,但使用另一个密钥验证签名,则可以说使用的是一个非对称密钥对。

这里使用对称密钥实现签名。这种方法意味着授权服务器和资源服务器都知道并使用相同的密钥。授权服务器使用密钥对令牌进行签名,资源服务器使用相同的密钥验证签名。

2.2.1 项目依赖

 <dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-security</artifactId>
 </dependency>
 <dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-web</artifactId>
 </dependency>
 <dependency>
     <groupId>org.springframework.cloud</groupId>
     <artifactId>spring-cloud-starter-oauth2</artifactId>
 </dependency>

2.2.2 配置JwtTokenStore

这里还需要定义一个JwtAccessTokenConverter类型的对象。使用JwtAccessTokenConverter就可以配置授权服务器验证令牌的方式。

 @Configuration
 @EnableAuthorizationServer
 public class AuthServerConfig
         extends AuthorizationServerConfigurerAdapter {
     //从application.properties配置文件中获取对称密钥的值
     @Value("${jwt.key}")
     private String jwtKey;
 
     @Autowired
     private AuthenticationManager authenticationManager;
 
     @Override
     public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
         clients.inMemory()
                .withClient("client")
                .secret("secret")
                .authorizedGrantTypes("password", "refresh_token")
                .scopes("read");
    }
 
     @Override
     public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
         endpoints
          .authenticationManager(authenticationManager)
                 //配置令牌存储和访问令牌转换器对象
          .tokenStore(tokenStore())
          .accessTokenConverter(jwtAccessTokenConverter());
    }
 
     @Bean
     public TokenStore tokenStore() {
         //创建带与之有关联的访问令牌转换器的令牌存储
         return new JwtTokenStore(jwtAccessTokenConverter());
    }
 
     @Bean
     public JwtAccessTokenConverter jwtAccessTokenConverter() {
         var converter = new JwtAccessTokenConverter();
         //设置访问令牌转换器对象的对称密钥的值
         converter.setSigningKey(jwtKey);
         return converter;
    }
 }

这里在application.properties文件中存储了这个示例的对称密钥的值,如下面的代码所示。现实场景中请不要暴露此数据。

 jwt.key=ymLTU8rq83j4fmJZj60wh4OrMNuntIj4fmJ

2.2.3 为授权服务器配置用户管理

 @Configuration
 public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
 
     @Bean
     public UserDetailsService uds() {
         var uds = new InMemoryUserDetailsManager();
 
         var u = User.withUsername("john")
                .password("12345")
                .authorities("read")
                .build();
 
         uds.createUser(u);
 
         return uds;
    }
 
     @Bean
     public PasswordEncoder passwordEncoder() {
         return NoOpPasswordEncoder.getInstance();
    }
 
     //在Spring上下文中将AuthenticationManager实例作为bean添加
     @Override
     @Bean
     public AuthenticationManager authenticationManagerBean() throws Exception {
         return super.authenticationManagerBean();
    }
 
 }

有了用户需要将用户关联到授权服务器配置即可。为此,需要在Spring上下文中将AuthenticationManager暴露为一个bean,然后在AuthServerConfig类中使用它。

2.2.4 启动项目获取访问令牌

接下来可以启动授权服务器并调用/oauth/token端点来获取访问令牌。下面的截图展示了用于调用/oauth/token端点的命令

OAuth2:使用JWT和加密签名

其响应体如下:

{

“access_token”: “eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2NDY0NTI1OTgsInVzZXJfbmFtZSI6ImpvaG4iLCJhdXRob3JpdGllcyI6WyJyZWFkIl0sImp0aSI6ImZmMGI3OTViLTc2YjEtNDUyZS1hZTZlLTljNTAwMzk4OTMxZSIsImNsaWVudF9pZCI6ImNsaWVudCIsInNjb3BlIjpbInJlYWQiXX0.SM_BXxsLzIE9FcYuc0RIwdN7CDoPw5rFOvJRaZ74WIU”,

“token_type”: “bearer”,

“refresh_token”: “eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJqb2huIiwic2NvcGUiOlsicmVhZCJdLCJhdGkiOiJmZjBiNzk1Yi03NmIxLTQ1MmUtYWU2ZS05YzUwMDM5ODkzMWUiLCJleHAiOjE2NDkwMDEzOTgsImF1dGhvcml0aWVzIjpbInJlYWQiXSwianRpIjoiZTQwNzI5ZjQtMzY4YS00ZGYzLTgxYmUtOWJkOGQzNWU0OGYzIiwiY2xpZW50X2lkIjoiY2xpZW50In0.rwCPNjL_ceLwFZtfbJgX-wG4FXcXmKZKsEGy42sWFIo”,

“expires_in”: 43199,

“scope”: “read”,

“jti”: “ff0b795b-76b1-452e-ae6e-9c500398931e”

}

可以在响应中观察到,访问和刷新令牌现在都是JWT。我们使用JWT在线解密工具查看

OAuth2:使用JWT和加密签名

设置好授权服务器之后,接下来就可以实现资源服务器了。

2.3 实现使用JWT的资源服务器

2.3.1 项目依赖

 <dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
 </dependency>
 <dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-web</artifactId>
 </dependency>
 <dependency>
     <groupId>org.springframework.cloud</groupId>
     <artifactId>spring-cloud-starter-oauth2</artifactId>
 </dependency>

2.3.2 添加测试端点

 @RestController
 public class HelloController {
 
     @GetMapping("/hello")
     public String hello() {
         return "Hello!";
    }
 }

现在我们已经有了一个要保护的端点,接下来可以声明配置类,其中要配置TokenStore。需要为资源服务器配置TokenStore,就像为授权服务器所做的配置一样。最重要的方面是确保为密钥使用相同的值。资源服务器需要该密钥用来验证令牌的签名。

2.3.3 资源服务器的配置类

 @Configuration
 @EnableResourceServer
 public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
 
     @Value("${jwt.key}")
     private String jwtKey;
 
     @Override
     public void configure(ResourceServerSecurityConfigurer resources) {
         //配置TokenStore
         resources.tokenStore(tokenStore());
    }
 
     @Bean
     public TokenStore tokenStore() {
         //声明TokenStore并将其添加到Spring上下文
         return new JwtTokenStore(jwtAccessTokenConverter());
    }
 
     @Bean
     public JwtAccessTokenConverter jwtAccessTokenConverter() {
         //创建访问令牌转换器并设置用于验证令牌签名的对称密钥
         var converter = new JwtAccessTokenConverter();
         converter.setSigningKey(jwtKey);
         return converter;
    }
 }

不要忘记在application.properties文件中设置密钥值。

因为这里在同一台机器上运行授权服务器和资源服务器,所以需要为这个应用程序配置不同的端口。下面的代码片段展示了application.properties文件的内容:

 server.port=9090
 
 jwt.key=ymLTU8rq83j4fmJZj60wh4OrMNuntIj4fmJ

2.3.4 测试

现在可以启动该资源服务器,并使用签名从授权服务器获得的有效JWT调用/hello端点。在这个示例中,必须将令牌添加到以单词”Bearer”为前缀的请求的Authorization HTTP头信息中。调用方式如下:

OAuth2:使用JWT和加密签名

3、使用通过JWT和非对称密钥签名的令牌

这里将会实现OAuth2身份验证的一个示例,其中授权服务器和资源服务器会使用一个非对称密钥对来对令牌签名和验证令牌。通常,如果授权服务器和资源服务器不是由同一个组织开发的就会发生这种情况。在这种情况下,就可以认为授权服务器不“信任”资源服务器,因此我们不希望授权服务器与资源服务器共享密钥。而且,使用对称密钥,资源服务器就拥有了过多的功能:不仅可以验证令牌,还可以对它们签名。

3.1 什么是非对称密钥对?

这个概念很简单。非对称密钥对有两个密钥:一个称为私钥,另一个称为公钥。授权服务器将使用私钥对令牌进行签名,而其他人只能使用私钥对令牌进行签名。

公钥和私钥是结合在一起的,这就是我们将其称为一对的原因。但是公钥只能用于验证签名。没有人可以使用公钥对令牌进行签名。

3.2 生成密钥对

生成一个私钥:

要生成私钥,可以运行下面代码片段中的keytool命令。它将在名为ssia.jks的文件中生成一个私钥。这里还是用了密码“ssia123”保护私钥,并使用别名“ssia”为密钥指定一个名称。在下面的命令中,可以看到用来生成密钥的算法,即RSA。

keytool -genkeypair -alias ssia -keyalg RSA -keypass ssia123 -keystore ssia.jks -storepass ssia123

获取公钥

要获取先前所生成私钥的公钥,可以运行这个keytool命令:

keytool -list -rfc --keystore ssia.jks | openssl x509 -inform pem -punkey

在生成公钥时,系统会提示我们输入密码;这里使用的是ssia123。然后应该可以在输出中找到公钥和证书。这个密钥应该类似于下面的代码片段:

-----BEGIN PUBLIC KEY-----MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAhORXDLLrdozoNFsIyaY48NwZaSP2f94JobhEV1CYw4ImqOH7My+odLyI063aDu0HLOeV0yGUj+oZVRNM/8Y5Qhl/fIRZeCtCDVybT7yJdBz/WvzAulfI4aGWSdjGUCwS88z5Af2BJUKGv7bkwRtaF+btTq8OEC/ke0GKOkWh2nGDKeHK645OOv59qLEoa8v6Ns/SveQCfB93Zx7V+utuV6Xjp8jqUN2X5MtM9+AQ2eihhTuLGCfZm0c51QXUihXYx4GH4kLMOULOXvI3uCSdrgkF6heTFRhN6sPCex1TEWB1mbGpCDGkRZ6Q0IeSKb5fcuW+LhUqfTwCKz6cvXT6kwIDAQAB
-----END PUBLIC KEY-----

现在我们有了一个用于JWT签名的私钥和一个用于验证签名的公钥。接下来只需要在授权和资源服务器中配置它们即可。

3.3 实现使用私钥的授权服务器

这里复制了私钥文件ssia.jks,它位于应用程序的resources文件夹中。需要将私钥添加到resources文件夹中,因为直接从类路径读取它会更容易。但是,将其放入类路径中的做法并不是强制性的。在application.properties文件中,存储了文件名、密钥的别名,以及用于保护私钥而生成的密码。我们需要这些详细信息用来配置JwtTokenStore。下面的代码片段展示了application.properties文件的内容:

password=ssia123
privateKey=ssia.jks
alias=ssia

与为授权服务器使用对称密钥所作的配置相比,唯一更改的是JwtAccessTokenCOnverter对象的定义。这里仍然使用JwtTokenStore。

3.3.1 授权服务器和私钥的配置类

@Configuration
@EnableAuthorizationServer
public class AuthServerConfig
extends AuthorizationServerConfigurerAdapter {

@Value("${password}")
private String password;

@Value("${privateKey}")
private String privateKey;

@Value("${alias}")
private String alias;

@Autowired
private AuthenticationManager authenticationManager;

@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("client")
.secret("secret")
.authorizedGrantTypes("password", "refresh_token")
.scopes("read");
}

@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints
.authenticationManager(authenticationManager)
.tokenStore(tokenStore())
.accessTokenConverter(jwtAccessTokenConverter());
}

@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}

@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
var converter = new JwtAccessTokenConverter();

//设置KeyStoreKeyFactory对象,以便从类路径读取私钥文件
KeyStoreKeyFactory keyStoreKeyFactory =
new KeyStoreKeyFactory(
new ClassPathResource(privateKey),
password.toCharArray());
//使用KeyStoreKeyFactory对象检索密钥对,并将密钥对设置为JwtAccessTokenConverter对象
converter.setKeyPair(keyStoreKeyFactory.getKeyPair(alias));

return converter;
}
}

3.3.2 生成令牌测试

现在可以启动该授权服务器并调用/oauth/token端点来生成一个新的访问令牌。当然,这里只是创建了一个普通的JWT,但是现在的区别是,要验证它的签名,需要使用密钥对中的公钥。顺便说一下,不要忘记令牌只是做了签名处理,并没有加密。下面展示了如何调用/oauth/token端点。

OAuth2:使用JWT和加密签名

3.4 实现使用公钥的资源服务器

这里实现一个资源服务器,该服务器使用公钥验证令牌的签名。当我们完成本示例时,将拥有一个完整的系统,该系统通过OAuth2实现身份验证,并用公钥对签名进行验证。注意,使用密钥只是为了给令牌签名,而不是加密它们。

资源服务器需要这对密钥的公钥来验证令牌的签名,因此需要将这个密钥添加到application.properties文件中。application.properties文件内容如下:

server.port=9090

publicKey=-----BEGIN PUBLIC KEY-----MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAhORXDLLrdozoNFsIyaY48NwZaSP2f94JobhEV1CYw4ImqOH7My+odLyI063aDu0HLOeV0yGUj+oZVRNM/8Y5Qhl/fIRZeCtCDVybT7yJdBz/WvzAulfI4aGWSdjGUCwS88z5Af2BJUKGv7bkwRtaF+btTq8OEC/ke0GKOkWh2nGDKeHK645OOv59qLEoa8v6Ns/SveQCfB93Zx7V+utuV6Xjp8jqUN2X5MtM9+AQ2eihhTuLGCfZm0c51QXUihXYx4GH4kLMOULOXvI3uCSdrgkF6heTFRhN6sPCex1TEWB1mbGpCDGkRZ6Q0IeSKb5fcuW+LhUqfTwCKz6cvXT6kwIDAQAB-----END PUBLIC KEY-----

3.4.1 资源服务器和公钥的配置类

@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

@Value("${publicKey}")
private String publicKey;

@Override
public void configure(ResourceServerSecurityConfigurer resources) {
resources.tokenStore(tokenStore());
}

@Bean
public TokenStore tokenStore() {
//创建JwtTokenStore并将其添加到Spring上下文中
return new JwtTokenStore(jwtAccessTokenConverter());
}

@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
var converter = new JwtAccessTokenConverter();
//设置令牌存储用于验证令牌的公钥
converter.setVerifierKey(publicKey);
return converter;
}
}

3.4.2 添加控制器

@RestController
public class HelloController {

@GetMapping("/hello")
public String hello() {
return "Hello!";
}
}

3.4.3 测试资源服务器

运行并调用端点来测试资源服务器,token就用我们之前授权服务器生成的。

OAuth2:使用JWT和加密签名

3.5 使用一个暴露公钥的端点

前面使用了公私密钥对来对令牌进行签名和验证。其中在资源服务器断配置了公钥。资源服务器使用该公钥验证JWT。但是,如果像更改密钥对,会发生什么情况呢?

最好不要永远保持同一份密钥对,应该定期旋转密钥。

到目前为止,我们已经在授权服务器端配置了私钥,在资源服务器端配置了公钥。而将密钥设置在两个地方使得密钥管理更困难。不过如果只在一端配置它们,则可以更容易地管理键。解决方案是将整个密钥对迁移至授权服务器端,并允许授权服务器使用端点暴露公钥。

接下来将使用一个单独的应用程序来证明如何使用Spring Security实现此配置。

授权服务器和资源服务器的依赖同上。只需要确保能够访问端点即可,也就是要暴露公钥。的确,Spring Boot已经配置了这样的端点,但也只是这样而已。默认情况下,对该端点的所有请求都会被拒绝。我们需要重写该端点的配置,并允许任何具有客户端凭据的人访问它。下面的代码展示了需要对授权服务器的配置类进行的更改。这些配置将允许任何具有有效客户端凭据的人调用端点以获得公钥。

3.5.1 用于暴露公钥的授权服务器的配置类

@Configuration
@EnableAuthorizationServer
public class AuthServerConfig
extends AuthorizationServerConfigurerAdapter {

@Value("${password}")
private String password;

@Value("${privateKey}")
private String privateKey;

@Value("${alias}")
private String alias;

@Autowired
private AuthenticationManager authenticationManager;

@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("client")
.secret("secret")
.authorizedGrantTypes("password", "refresh_token")
.scopes("read")
.and()
//添加资源服务器用于调用端点的客户端凭据,该端点会暴露公钥
.withClient("resourceserver")
.secret("resourceserversecret");
}

@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints
.authenticationManager(authenticationManager)
.tokenStore(tokenStore())
.tokenEnhancer(jwtAccessTokenConverter());
}

@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}

@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
var converter = new JwtAccessTokenConverter();

KeyStoreKeyFactory keyStoreKeyFactory =
new KeyStoreKeyFactory(
new ClassPathResource(privateKey),
password.toCharArray());
converter.setKeyPair(keyStoreKeyFactory.getKeyPair(alias));

return converter;
}

@Override
public void configure(AuthorizationServerSecurityConfigurer security) {
//配置授权服务器以便暴露提供公钥的端点,从而用于任何使用有效客户端凭据进行身份验证的请求
security.tokenKeyAccess("isAuthenticated()");
}
}

3.5.2 测试

现在可以启动该授权服务器并调用/oauth/token_key端点来确保正确实现了此配置。

OAuth2:使用JWT和加密签名

为了让资源服务器可以使用此端点并获得公钥,只需要在其属性文件中配置该端点和凭据即可。下面的代码片段定义了资源服务器的application.properties文件:

server.port=9090

security.oauth2.resource.jwt.key-uri=http://localhost:8080/oauth/token_key

security.oauth2.client.client-id=resourceserver
security.oauth2.client.client-secret=resourceserversecret

因为资源服务器现在从授权服务器的/oauth/token_key端点获取私钥,所以现在不需要在资源服务器配置类中配置它。资源服务器的配置类可以保持为空,如下面的代码片段所示:

OAuth2:使用JWT和加密签名

现在也可以启动资源服务器,并调用它暴露的/hello端点,以查看整个设置是否按预期工作。下面展示如何调用/hello端点。在这里将获得一个令牌,并且要是用它调用资源服务器的测试端点。

OAuth2:使用JWT和加密签名

4、将自定义信息添加到JWT

在现实场景中,可能会遇到在令牌中添加详细信息的需求。本示例将会展示如何更改授权服务器以便在JWT上添加自定义详细信息,还会展示如何更改资源服务器以读取这些详细信息。如果使用前面示例中生成的令牌之一对其进行解码,就可以看到Spring Security向令牌添加的默认值。

4.1 配置授权服务器以便向令牌添加自定义详细信息

这里假设的需求是添加授权服务器本身的时区。要向令牌添加额外的详细信息,需要创建一个TokenEnhancer类型的对象。

4.1.1 自定义令牌增强器

/**
* 自定义令牌增强器
*/
public class CustomTokenEnhancer implements TokenEnhancer {//实现TokenEnhancer接口

//重写enhance()方法,该方法将接收当前令牌并返回增强后的令牌
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken oAuth2AccessToken,
OAuth2Authentication oAuth2Authentication) {
//基于接收到的令牌创建一个新的令牌对象
var token = new DefaultOAuth2AccessToken(oAuth2AccessToken);

//将希望添加到令牌的详细信息定义为一个Map
Map<String, Object> info =
Map.of("generatedInZone", ZoneId.systemDefault().toString());
//将额外的详细信息添加到令牌
token.setAdditionalInformation(info);
//返回包含额外详细信息的令牌
return token;
}
}

TokenEnhancer对象的enhance()方法会接收要增强的令牌作为参数,并返回该增强后的令牌,其中包含额外的详细信息。这里和上面使用的是同一个应用程序,只不过更改了configure()方法以应用令牌增强器。

4.1.2 配置TokenEnhancer对象

@Configuration
@EnableAuthorizationServer
public class AuthServerConfig
extends AuthorizationServerConfigurerAdapter {

@Value("${password}")
private String password;

@Value("${privateKey}")
private String privateKey;

@Value("${alias}")
private String alias;

@Autowired
private AuthenticationManager authenticationManager;

@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("client")
.secret("secret")
.authorizedGrantTypes("password", "refresh_token")
.scopes("read");
}

@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
//定义一个TokenEnhancerChain
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();

//将这两个令牌增强器对象添加到一个列表中
var tokenEnhancers =
List.of(new CustomTokenEnhancer(),
jwtAccessTokenConverter());

//将令牌增强器的列表添加到增强器链中
tokenEnhancerChain.setTokenEnhancers(tokenEnhancers);

endpoints
.authenticationManager(authenticationManager)
.tokenStore(tokenStore())
.tokenEnhancer(tokenEnhancerChain); //配置令牌增强器对象
}

@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}

@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
var converter = new JwtAccessTokenConverter();

KeyStoreKeyFactory keyStoreKeyFactory =
new KeyStoreKeyFactory(
new ClassPathResource(privateKey),
password.toCharArray());
converter.setKeyPair(keyStoreKeyFactory.getKeyPair(alias));

return converter;
}
}

如上所示,配置自定义令牌增强器有点复杂。其中必须创建一个令牌增强器链,并设置了整个链,而不是只设置一个对象,因为访问令牌转换器对象也是一个令牌增强器。如果只配置自定义令牌增强器,则要重写访问令牌转换器的行为。我们转而要将两者都添加到职责链中,并配置包含这两个对象的链。

4.2 启动授权服务器,生成新的访问令牌

OAuth2:使用JWT和加密签名

4.3 增强的JWT的主体

用jwt解密工具查看

OAuth2:使用JWT和加密签名

4.4 配置资源服务器以读取JWT的自定义详细信息

这里需要对资源服务器进行更改,以便读取到添加到JWT的额外详细信息。

之前已经讨论过,AccessTokenConverter是将令牌转换为Authentication对象。这就是需要更改的对象,以便在其处理中纳入令牌中的自定义详细信息。之前,我们创建了一个类型为JwtAccessTokenConverter类型的bean,如下面的代码所示:

 @Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
var converter = new JwtAccessTokenConverter();
//设置令牌存储用于验证令牌的公钥
converter.setSigningKey(jwtKey);
return converter;
}

这里要使用这个令牌设置资源服务器用于令牌验证的密钥。此处创建了JwtAccessTokenConverter的自定义实现,其中还纳入了令牌上新的详细信息。最简单的方法就是扩展这个类并重写extractAuthentication()方法。此方法将转换Authentication对象中的令牌。

4.5 创建一个自定义AccessTokenConverter

public class AdditionalClaimsAccessTokenConverter
extends JwtAccessTokenConverter {

@Override
public OAuth2Authentication extractAuthentication(Map<String, ?> map) {
//应用JwtAccessTokenConverter类实现的逻辑并获取初始身份验证对象
var authentication = super.extractAuthentication(map);
//将自定义详细信息添加到身份验证
authentication.setDetails(map);
//返回身份验证对象
return authentication;
}
}

在资源服务器的配置类中,现在可以使用自定义访问令牌转换器了。

4.6 定义新的AccessTokenConverter bean

@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

@Value("${publicKey}")
private String publicKey;

@Override
public void configure(ResourceServerSecurityConfigurer resources) {
resources.tokenStore(tokenStore());
}

@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}

@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
//创建新的AccessTokenConverter对象的一个实例
var converter = new AdditionalClaimsAccessTokenConverter();
converter.setVerifierKey(publicKey);
return converter;
}
}

测试这些变更的一种简单方法是将它们注入控制器类中,并在HTTP响应中返回它们。

4.7 控制器类

@RestController
public class HelloController {

@GetMapping("/hello")
public String hello(OAuth2Authentication authentication) {
//获取添加到Authentication对象的额外详细信息
OAuth2AuthenticationDetails details =
(OAuth2AuthenticationDetails) authentication.getDetails();

//在HTTP响应中返回该详细信息
return "Hello! " + details.getDecodedDetails();
}
}

4.8 测试

现在可以启动该资源服务器并使用包含自定义详细信息的JWT来测试端点。下面展示了如何调用/hello端点和调用的结果。getDecodedDetails()方法将返回一个包含令牌详细信息的Map。在本示例中,为了保持简单,这里直接打印了getDecodedDetails()返回的整个值。如果只需要使用特定的值,则可以检查返回的Map,并使用键获取所需的值。

OAuth2:使用JWT和加密签名

可以看到,时区属性generatedInZone已经获取到了。

5、总结

  • 如果对称密钥在实现中不可用,则使用非对称密钥对来实现令牌签名和验证。

  • 为了简化密钥旋转,可以在授权服务器端配置密钥,并允许资源服务器在特定端点读取它们。

  • 可以通过根据实现的需求向JWT主体添加详细信息来自定义JWT。授权服务器会将自定义详细信息添加到令牌主体中,而资源服务器则使用这些详细信息进行授权。

原文始发于微信公众号(全栈开发那些事):OAuth2:使用JWT和加密签名

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/91686.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!