返回介绍

15.8 使用 JWT

发布于 2025-04-26 13:16:51 字数 6439 浏览 0 评论 0 收藏

在前面的案例中,我们一直都是使用的不透明令牌(Opaque Token),在实际开发中,JWT 令牌目前使用较多,因此本节我们来看一下如何在 OAuth2 中使用 JWT。

15.8.1 JWT

JWT 全称为 Json Web Token,它是一种 JSON 风格的轻量级授权和身份认证规范,可实现无状态、分布式的 Web 应用授权。

JWT 作为一种规范,并没有和某一种语言绑定在一起,开发者可以使用任何语言来实现 JWT。Java 中 JWT 相关的开源库也比较多,例如 jjwt、nimbus-jose-jwt 等。

15.8.2 JWT 数据格式

JWT 包含三部分数据:Header、Payload 与 Signature。

Header

头部,通常头部有两部分信息:

- 声明类型,这里是 JWT。

- 加密算法,自定义。

我们会对头部进行 Base64Url 编码(可解码),得到第一部分数据。

Payload

载荷,就是有效数据,在官方文档中(RFC7519)给了 7 个示例信息:

- iss (issuer):签发人。

- exp (expiration time):过期时间。

- sub (subject):主题。

- aud (audience):受众。

- nbf (Not Before):生效时间。

- iat (Issued At):签发时间。

- jti (JWT ID):编号。

这部分也会采用 Base64Url 编码,得到第二部分数据。

Signature

签名,是整个数据的认证信息。一般根据前两步的数据,再加上服务的密钥 secret(密钥保存在服务端,不能泄漏给客户端)。这个密钥通过 Header 中配置的加密算法生成,用于验证整个数据完整性和可靠性。

生成的数据格式如图 15-20 所示。

图 15-20 JWT 数据格式

注意

这里的数据使用“.”隔开成了三部分,分别对应前面提到的三部分。另外,这里数据是不换行的,图片换行只是为了展示方便而已。

15.8.3 OAuth2 中使用 JWT

Spring Security 官方推荐使用 nimbus-jose-jwt 来生成和解析 JWT 令牌,该库同时支持对称加密和非对称加密两种方式处理 JWT,本小节使用目前通用的非对称加密(RSA)来处理 JWT。

非对称加密有两种使用场景:

- 加密场景:公钥负责加密,私钥负责解密。

- 签名场景:私钥负责签名,公钥负责验证。

我们在 JWT 中使用的非对称加密属于签名场景。如果要使用 JWT,我们首先需要创建一个证书文件,这里使用 Java 自带的 keytool 工具来生成 jks 证书文件,该工具在 JDK 的 bin 目录下,生成过程如图 15-21 所示。

图 15-21 证书生成过程

生成证书命令中,我们设置了生成证书的别名是 jwt,生成的证书文件是 jwt.jks。接下来输入密码以及其他信息即可,命令执行完成后,会在当前目录下生成一个 jwt.jks 文件,将该文件拷贝到 auth_server 项目的 resources 目录下,如图 15-22 所示。

图 15-22 将生成的 jwt.jks 文件复制到 resources 目录下

接下来在 auth_server 项目中添加 JWT 依赖,代码如下(nimbus-jose-jwt 在资源服务器中有提供,所以只需要在授权服务器中添加即可):

    <dependency>
       <groupId>org.springframework.security</groupId>
       <artifactId>spring-security-jwt</artifactId>
       <version>1.1.0.RELEASE</version>
    </dependency>
    <dependency>
       <groupId>com.nimbusds</groupId>
       <artifactId>nimbus-jose-jwt</artifactId>
    </dependency>

接下来进行 JWT 配置,首先对密钥进行配置,代码如下:

    class KeyConfig {
       private static final String KEY_STORE_FILE = "jwt.jks";
       private static final String KEY_STORE_PASSWORD = "123456";
       private static final String KEY_ALIAS = "jwt";
       private static KeyStoreKeyFactory KEY_STORE_KEY_FACTORY =
               new KeyStoreKeyFactory(new ClassPathResource(KEY_STORE_FILE),
                                                 KEY_STORE_PASSWORD.toCharArray());
       static RSAPublicKey getVerifierKey() {
           return (RSAPublicKey) getKeyPair().getPublic();
       }
       static RSAPrivateKey getSignerKey() {
           return (RSAPrivateKey) getKeyPair().getPrivate();
       }
       private static KeyPair getKeyPair() {
           return KEY_STORE_KEY_FACTORY.getKeyPair(KEY_ALIAS);
       }
    }

KEY_STORE_FILE 就是生成的证书文件名,KEY_STORE_PASSWORD 则是生成证书时输入的密码,KEY_ALIAS 指证书别名,然后再通过 getVerifierKey 和 getSignerKey 两个方法分别返回公钥和私钥。

接下来配置 TokenStore,代码如下:

    @Configuration
    public class AccessTokenConfig {
       @Bean
       TokenStore tokenStore() {
           return new JwtTokenStore(jwtAccessTokenConverter());
       }
       @Bean
       public JwtAccessTokenConverter jwtAccessTokenConverter() {
           RsaSigner signer = new RsaSigner(KeyConfig.getSignerKey());
           JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
           converter.setSigner(signer);
           converter.setVerifier(new RsaVerifier(KeyConfig.getVerifierKey()));
           return converter;
       }
       @Bean
       public JWKSet jwkSet() {
          RSAKey.Builder builder = new RSAKey
                  .Builder(KeyConfig.getVerifierKey())
                  .keyUse(KeyUse.SIGNATURE)
                  .algorithm(JWSAlgorithm.RS256);
          return new JWKSet(builder.build());
       }
    }

此时提供的 TokenStore 实例是 JwtTokenStore,创建该实例时需要一个 JwtAccessToken Converter 对象,该对象是一个令牌生成工具。JwtAccessTokenConverter 对象在创建时,配置一下签名以及验证者即可。最后还需要提供一个包含公钥的 JWKSet 对象,该对象接下来要暴露给资源服务器。

接下来配置 AuthorizationServer,主要在 AuthorizationServerTokenServices 实例中进行配置,代码如下:

    @Autowired
    TokenStore tokenStore;
    @Autowired
    JwtAccessTokenConverter jwtAccessTokenConverter;
    @Bean
    AuthorizationServerTokenServices tokenServices() {
       DefaultTokenServices services = new DefaultTokenServices();
       services.setClientDetailsService(clientDetailsService);
       services.setSupportRefreshToken(true);
       services.setTokenStore(tokenStore);
       TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
       tokenEnhancerChain
                   .setTokenEnhancers(Arrays.asList(jwtAccessTokenConverter));
       services.setTokenEnhancer(tokenEnhancerChain);
       return services;
    }
    //省略其他

主要是在 DefaultTokenServices 中配置 TokenEnhancer,将之前的 JwtAccessTokenConverter 注入进来即可。

最后我们还需要提供一个公钥接口,资源服务器将从该接口中获取到公钥,进而完成对 JWT 的校验:

    @GetMapping(value = "/oauth2/keys")
    public String keys() {
       return jwkSet.toString();
    }

至此,我们的 auth_server 就改造完成了,接下来对 res_server 进行改造。

当采用 JWT 之后,资源服务器就不需要每次拿到令牌后都去调用授权服务器校验令牌,资源服务器只需要调用授权服务器接口获取到公钥即可。有了公钥,资源服务器就可以自己校验 JWT 令牌了。所以,对资源服务器的改动很简单,代码如下:

    @Configuration
    public class OAuth2ResourceServerSecurityConfiguration extends
                                                     WebSecurityConfigurerAdapter {
       @Override
       protected void configure(HttpSecurity http) throws Exception {
           http.authorizeRequests()
                   .anyRequest().authenticated()
                   .and()
                   .oauth2ResourceServer().jwt()
                   .jwkSetUri("http://auth.javaboy.org:8881/oauth2/keys");
       }
    }

开启 JWT 并设置获取 JwkSet 的地址即可。

配置完成后,分别启动 auth_server 和 res_server,测试客户端依然使用 15.5.2.3 小节搭建的客户端,具体的测试过程这里就不再赘述了。

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。