1. 概述

JSON Web Tokens (JWT) 是无状态应用安全性的事实标准。Spring Security框架提供了集成JWT以保护REST API的方法。生成令牌的关键过程之一是应用签名以保证真实性。

在这个教程中,我们将探讨一个利用JWT身份验证的无状态Spring Boot应用。我们将设置必要的组件,并创建一个加密的SecretKey实例,用于对JWT进行签名和验证。

2. 项目设置

首先,让我们使用Spring Security和JWT令牌初始化一个无状态的Spring Boot应用。为了简洁,我们不会展示完整的设置代码。

2.1. Maven依赖项

首先,添加到pom.xml的依赖项有:spring-boot-starter-webspring-boot-starter-securityspring-boot-starter-data-jpa,以及h2数据库:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <version>3.2.3</version> 
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId> 
    <version>3.2.3</version> 
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
    <version>3.2.3</version>  
</dependency>
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <version>2.2.224</version>
</dependency>

Spring Boot Starter Web提供构建REST API的功能,而Spring Boot Starter Security帮助实现身份验证和授权。我们添加了一个内存数据库,以便快速原型开发。

接下来,向pom.xml添加jjwt-apijjwt-impljjwt-jackson依赖:

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.12.5</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.12.5</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.12.5</version>
</dependency>

这些依赖提供了一个生成和签名JWT并将其集成到Spring Security的API。

2.2. JWT配置

首先,创建一个认证入口点:

@Component
class AuthEntryPointJwt implements AuthenticationEntryPoint {
    // ...
}

这里,我们创建了一个类,用于处理使用JWT身份验证的Spring Security应用程序中的授权尝试。它充当看门人,确保只有具有有效权限的用户可以访问受保护的资源。

然后,创建名为AuthTokenFilter的类,它拦截入站请求,验证JWT令牌,并在存在有效令牌时进行用户身份验证:

class AuthTokenFilter extends OncePerRequestFilter {
    // ...
}

最后,创建名为JwtUtil的类,它提供创建和验证令牌的方法:

@Component
class JwtUtils {
    // ... 
}

这个类包含使用signWith()方法的逻辑。

2.3. 安全配置

最后,定义SecurityConfiguration类并集成JWT:

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
class SecurityConfiguration {
    // ...

    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.csrf(AbstractHttpConfigurer::disable)
          .cors(AbstractHttpConfigurer::disable)
          .authorizeHttpRequests(req -> req.requestMatchers(WHITE_LIST_URL)
            .permitAll()
            .anyRequest()
            .authenticated())
          .exceptionHandling(ex -> ex.authenticationEntryPoint(unauthorizedHandler))
          .sessionManagement(session -> session.sessionCreationPolicy(STATELESS))
          .authenticationProvider(authenticationProvider())
          .addFilterBefore(
              authenticationJwtTokenFilter(), 
              UsernamePasswordAuthenticationFilter.class
           );

        return http.build();
    }

    // ...
}

在上面的代码中,我们整合了JWT入口点和过滤器以激活JWT身份验证。

3. signWith() 方法

JJWT库提供了signWith()方法,用于使用特定的加密算法和密钥签名JWT。这个签名过程对于确保JWT的完整性和真实性至关重要。

signWith()方法接受KeySecretKey实例和签名算法作为参数。哈希消息认证码(HMAC)算法是常用的签名算法之一

重要的是,该方法需要一个签名过程使用的秘密密钥,通常是一个字节数组。我们可以使用KeySecretKey实例将字符串形式的秘密转换为密钥。

值得注意的是,我们可以传递一个普通的字符串作为密钥。但这缺乏加密KeySecretKey实例的安全性和随机性

使用SecretKey实例确保JWT的完整性和真实性。

4. 签名JWT

我们可以使用KeySecretKey实例创建一个强大的密钥来签署JWT。

4.1. 使用Key实例

本质上,我们可以将秘密字符串转换为Key实例,然后再使用它来加密并签署JWT。

首先,确保秘密字符串进行了Base64编码

private String jwtSecret = "4261656C64756E67";

接下来,创建一个Key对象:

private Key getSigningKey() {
    byte[] keyBytes = Decoders.BASE64.decode(this.jwtSecret);
    return Keys.hmacShaKeyFor(keyBytes);
}

在上面的代码中,我们首先将jwtSecret解码为一个字节数组。然后,我们在Keys实例上调用hmacShaKeyFor(),传入keyBytes参数。这基于HMAC算法生成一个密钥。

如果密钥未进行Base64编码,我们可以直接在纯字符串上调用getByte()方法:

private Key getSigningKey() {
    byte[] keyBytes = this.jwtSecret.getBytes(StandardCharsets.UTF_8);
    return Keys.hmacShaKeyFor(keyBytes);
}

然而,这不是推荐的做法,因为秘密字符串可能格式不正确,且可能包含非UTF-8字符。因此,我们必须确保密钥字符串已进行Base64编码,再从其中生成密钥。

4.2. 使用SecretKey实例

此外,我们也可以使用HMAC-SHA算法创建一个强大的SecretKey实例来形成密钥。让我们创建一个返回密钥的SecretKey实例:

SecretKey getSigningKey() {
    return Jwts.SIG.HS256.key().build();
}

在这里,我们直接使用HMAC-SHA算法,无需使用字节数组。这形成了一个强大的签名密钥。接下来,我们可以更新signWith()方法,将getSigningKey()作为参数传递。

另一种选择是,从Base16编码的字符串创建一个SecretKey实例:

SecretKey getSigningKey() {
    byte[] keyBytes = Decoders.BASE64.decode(jwtSecret);
    return Keys.hmacShaKeyFor(keyBytes);
}

这将生成一个用于签署和验证JWT的强大SecretKey类型。

值得注意的是,建议使用SecretKey实例而不是Key实例,因为新的verifyWith()方法接受SecretKey类型的参数来验证令牌。

4.3. 应用密钥

现在,让我们将秘密密钥应用于我们应用的JWT签名:

String generateJwtToken(Authentication authentication) {
    UserDetailsImpl userPrincipal = (UserDetailsImpl) authentication.getPrincipal();

    return Jwts.builder()
      .subject((userPrincipal.getUsername()))
      .issuedAt(new Date())
      .expiration(new Date((new Date()).getTime() + jwtExpirationMs))
      .signWith(key)
      .compact();
}

signWith()方法接受SecretKey实例作为参数,为令牌附加一个唯一的签名。

5. 总结

在这篇文章中,我们学习了如何使用Java的KeySecretKey实例创建密钥。我们也了解了一个利用JWT令牌保证令牌完整性的无状态Spring Boot应用,并展示了如何使用KeySecretKey实例对其进行签名和验证。使用普通字符串不再被推荐。

如往常一样,示例代码的完整源代码可在GitHub上获取。