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-web,spring-boot-starter-security,spring-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-api,jjwt-impl和jjwt-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()
方法接受Key
或SecretKey
实例和签名算法作为参数。哈希消息认证码(HMAC)算法是常用的签名算法之一。
重要的是,该方法需要一个签名过程使用的秘密密钥,通常是一个字节数组。我们可以使用Key
或SecretKey
实例将字符串形式的秘密转换为密钥。
值得注意的是,我们可以传递一个普通的字符串作为密钥。但这缺乏加密Key
或SecretKey
实例的安全性和随机性。
使用SecretKey
实例确保JWT的完整性和真实性。
4. 签名JWT
我们可以使用Key
和SecretKey
实例创建一个强大的密钥来签署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的Key
和SecretKey
实例创建密钥。我们也了解了一个利用JWT令牌保证令牌完整性的无状态Spring Boot应用,并展示了如何使用Key
或SecretKey
实例对其进行签名和验证。使用普通字符串不再被推荐。
如往常一样,示例代码的完整源代码可在GitHub上获取。