一、Spring Boot 与 JWT 的概述
(一)Spring Boot 简介
Spring Boot 是一个开源的 Java 基于 Spring 框架的快速开发平台,它极大地简化了 Spring 应用的开发过程。通过自动配置 Spring 和第三方库,Spring Boot 让开发者能够快速搭建出一个功能强大的应用。它提供了大量的 “Starters”(启动器),这些启动器封装了各种常用的功能,例如数据库访问、安全认证、Web 开发等,开发者只需在项目中引入相应的依赖即可。
Spring Boot 的核心优势包括:
- 快速开发:通过自动配置,减少了大量的配置文件编写工作。
- 独立运行:Spring Boot 应用内嵌了 Tomcat、Jetty 等服务器,无需部署 WAR 文件。
- 简化依赖管理:通过 Maven 或 Gradle 的依赖管理,简化了依赖的配置和版本管理。
- 易于监控:提供了丰富的监控接口,方便对应用进行监控和管理。
(二)JWT 简介
JWT(JSON Web Token)是一种开放标准(RFC 7519),它允许在客户端和服务器之间以 JSON 对象安全地传递信息。JWT 的核心优势在于它的无状态性和可扩展性。由于 JWT 包含了用户的身份信息,服务器在验证用户身份时无需查询数据库,从而提高了系统的性能和可扩展性。JWT 通常用于身份认证和信息交换,它通过数字签名确保数据的完整性和安全性。
JWT 的结构包括三部分:
- Header(头部):通常包含令牌的类型(如 JWT)和所使用的签名算法(如 HMAC SHA256 或 RSA)。
- Payload(载荷):包含声明(Claims),这些声明是关于实体(通常是用户)和其他数据的声明。声明分为三种类型:
- Registered Claims(注册声明):预定义的声明,如
iss
(发行人)、exp
(过期时间)、sub
(主题)等。 - Public Claims(公共声明):自定义的声明,开发者可以根据需求添加任何信息。
- Private Claims(私有声明):自定义的声明,通常用于存储用户的身份信息。
- Signature(签名):用于验证消息的完整性和确保消息自签发后未被篡改。
二、Spring Boot 与 JWT 的结合优势
(一)无状态认证
JWT 是一种无状态的认证机制,它将用户的身份信息存储在令牌中,服务器无需保存用户的会话信息。这种方式使得系统更加轻量级,同时也便于扩展。在分布式系统中,无状态认证可以减少服务器之间的通信开销,提高系统的性能。
(二)安全性高
JWT 使用数字签名技术对令牌进行加密,确保令牌在传输过程中不会被篡改。此外,JWT 支持多种加密算法,开发者可以根据需求选择合适的加密方式。常见的加密算法包括:
- HMAC SHA256:使用密钥进行签名,适合对称加密。
- RSA:使用公钥和私钥进行签名,适合非对称加密。
(三)易于扩展
由于 JWT 是无状态的,它可以在分布式系统中轻松实现单点登录(SSO)。多个服务可以共享同一个 JWT,从而实现无缝的用户认证。这种方式特别适合微服务架构,每个服务都可以独立验证 JWT,而无需依赖中央认证服务器。
(四)跨域支持
JWT 令牌可以作为 HTTP 请求头的一部分发送,这种方式使得 JWT 在跨域请求中非常方便。客户端可以在跨域请求中携带 JWT,服务器通过验证 JWT 来确认用户的身份,从而实现跨域认证。
三、Spring Boot 与 JWT 的整合实现
(一)项目搭建与依赖准备
1. 创建 Spring Boot 项目
使用 Spring Initializr(https://start.spring.io/)快速创建一个 Spring Boot 项目。选择以下依赖:
- Spring Web
- Spring Security(可选,用于增强安全性)
- Lombok(可选,用于简化代码)
2. 添加 JWT 相关依赖
在 pom.xml
文件中添加 JWT 相关的依赖:
<dependencies><!-- Spring Web --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- JWT --><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-api</artifactId><version>0.11.5</version></dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-impl</artifactId><version>0.11.5</version><scope>runtime</scope></dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-jackson</artifactId><version>0.11.5</version><scope>runtime</scope></dependency><!-- Lombok --><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency>
</dependencies>
(二)JWT 配置类
在 Spring Boot 项目中,可以通过配置类来管理 JWT 的相关参数。以下是一个简单的 JWT 配置类示例:
package com.example.config;import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.Date;@Component
@ConfigurationProperties(prefix = "config.jwt")
public class JwtConfig {private String secret; // 加密密钥private long expire; // token 有效时长private String header; // header 名称public String createToken(String subject) {Date nowDate = new Date();Date expireDate = new Date(nowDate.getTime() + expire * 1000);return Jwts.builder().setHeaderParam("typ", "JWT").setSubject(subject).setIssuedAt(nowDate).setExpiration(expireDate).signWith(SignatureAlgorithm.HS512, secret).compact();}public Claims getTokenClaim(String token) {try {return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();} catch (Exception e) {return null;}}public boolean isTokenExpired(Date expirationTime) {return expirationTime.before(new Date());}public Date getExpirationDateFromToken(String token) {return getTokenClaim(token).getExpiration();}public String getUsernameFromToken(String token) {return getTokenClaim(token).getSubject();}public Date getIssuedAtDateFromToken(String token) {return getTokenClaim(token).getIssuedAt();}// Getter 和 Setter 方法
}
在 application.properties
文件中配置 JWT 相关参数:
config.jwt.secret=yourSecretKey
config.jwt.expire=3600
config.jwt.header=Authorization
(三)JWT 工具类
JWT 工具类用于生成和验证 JWT 令牌。以下是一个简单的 JWT 工具类示例:
package com.example.util;import com.example.config.JwtConfig;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;import java.util.Date;@Component
public class JwtUtil {@Autowiredprivate JwtConfig jwtConfig;public String generateToken(String username) {Date now = new Date();Date expiryDate = new Date(now.getTime() + jwtConfig.getExpire() * 1000);return Jwts.builder().setSubject(username).setIssuedAt(now).setExpiration(expiryDate).signWith(SignatureAlgorithm.HS512, jwtConfig.getSecret()).compact();}public String getUsernameFromToken(String token) {Claims claims = Jwts.parser().setSigningKey(jwtConfig.getSecret()).parseClaimsJws(token).getBody();return claims.getSubject();}public boolean validateToken(String token) {try {Jwts.parser().setSigningKey(jwtConfig.getSecret()).parseClaimsJws(token);return true;} catch (Exception e) {return false;}}
}
(四)JWT 拦截器
为了在每个请求中验证 JWT,可以创建一个拦截器。拦截器会在每个请求到达控制器之前验证 JWT 的有效性。
package com.example.interceptor;import com.example.config.JwtConfig;
import com.example.util.JwtUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;@Component
public class JwtInterceptor implements HandlerInterceptor {@Autowiredprivate JwtUtil jwtUtil;@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {final String authHeader = request.getHeader(JwtConfig.HEADER);if (authHeader != null && authHeader.startsWith("Bearer ")) {final String token = authHeader.substring(7);if (jwtUtil.validateToken(token)) {return true;}}response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");return false;}
}
在 Spring Boot 的配置类中注册拦截器:
package com.example.config;import com.example.interceptor.JwtInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;@Configuration
public class WebConfig implements WebMvcConfigurer {@Autowiredprivate JwtInterceptor jwtInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(jwtInterceptor).addPathPatterns("/api/**") // 拦截所有 /api 下的请求.excludePathPatterns("/api/auth/**"); // 排除登录接口}
}
(五)用户认证与 JWT 生成
在用户登录时,验证用户的用户名和密码,如果验证通过,则生成 JWT 并返回给客户端。
package com.example.controller;import com.example.entity.User;
import com.example.service.UserService;
import com.example.util.JwtUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;@RestController
public class AuthController {@Autowiredprivate UserService userService;@Autowiredprivate JwtUtil jwtUtil;@PostMapping("/api/auth/login")public ResponseEntity<?> login(@RequestBody User user) {User authenticatedUser = userService.authenticate(user.getUsername(), user.getPassword());if (authenticatedUser != null) {String token = jwtUtil.generateToken(authenticatedUser.getUsername());return ResponseEntity.ok().header("Authorization", "Bearer " + token).body("Login successful");} else {return ResponseEntity.status(401).body("Invalid credentials");}}
}
(六)用户服务类
用户服务类用于处理用户相关的逻辑,例如用户认证。
package com.example.service;import com.example.entity.User;
import com.example.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;@Service
public class UserService {@Autowiredprivate UserRepository userRepository;public User authenticate(String username, String password) {User user = userRepository.findByUsername(username);if (user != null && user.getPassword().equals(password)) {return user;}return null;}
}
(七)用户实体类
用户实体类用于表示用户信息。
package com.example.entity;import lombok.Data;import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;@Entity
@Data
public class User {@Id@GeneratedValue(strategy = GenerationType.IDENTITY)private Long id;private String username;private String password;
}
(八)用户仓库类
用户仓库类用于与数据库交互,获取用户信息。
package com.example.repository;import com.example.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;@Repository
public interface UserRepository extends JpaRepository<User, Long> {User findByUsername(String username);
}
四、Spring Boot 与 JWT 的高级应用
(一)JWT 刷新机制
在实际应用中,JWT 的有效期通常较短,例如 30 分钟。为了延长用户的会话时间,可以实现 JWT 刷新机制。当 JWT 过期时,客户端可以请求一个新的 JWT。
- 创建刷新令牌:在用户登录时,同时生成一个刷新令牌(Refresh Token),并将其存储在服务器端。
- 验证刷新令牌:当 JWT 过期时,客户端使用刷新令牌请求一个新的 JWT。
- 更新刷新令牌:每次生成新的 JWT 时,同时更新刷新令牌。
以下是一个简单的刷新令牌机制实现:
package com.example.controller;import com.example.entity.RefreshToken;
import com.example.service.RefreshTokenService;
import com.example.util.JwtUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;@RestController
@RequestMapping("/api/auth")
public class AuthController {@Autowiredprivate JwtUtil jwtUtil;@Autowiredprivate RefreshTokenService refreshTokenService;@PostMapping("/login")public ResponseEntity<?> login(@RequestBody User user) {// 用户认证逻辑User authenticatedUser = userService.authenticate(user.getUsername(), user.getPassword());if (authenticatedUser != null) {String token = jwtUtil.generateToken(authenticatedUser.getUsername());RefreshToken refreshToken = refreshTokenService.createRefreshToken(authenticatedUser.getId());return ResponseEntity.ok().body(new AuthResponse(token, refreshToken.getToken()));} else {return ResponseEntity.status(401).body("Invalid credentials");}}@PostMapping("/refresh")public ResponseEntity<?> refreshToken(@RequestBody RefreshTokenRequest refreshTokenRequest) {RefreshToken refreshToken = refreshTokenService.verifyRefreshToken(refreshTokenRequest.getRefreshToken());if (refreshToken != null) {String token = jwtUtil.generateToken(refreshToken.getUser().getUsername());return ResponseEntity.ok().body(new AuthResponse(token, refreshToken.getToken()));} else {return ResponseEntity.status(401).body("Invalid refresh token");}}
}
(二)JWT 黑名单机制
在某些情况下,可能需要提前使 JWT 失效,例如用户注销或修改密码。可以通过实现 JWT 黑名单机制来解决这个问题。当 JWT 被加入黑名单后,即使它未过期,也会被拒绝访问。
- 创建黑名单存储:可以使用内存数据库(如 Redis)存储黑名单中的 JWT。
- 验证 JWT:在拦截器中验证 JWT 时,检查它是否在黑名单中。
以下是一个简单的黑名单机制实现:
package com.example.service;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;import java.util.concurrent.TimeUnit;@Service
public class BlacklistService {@Autowiredprivate RedisTemplate<String, String> redisTemplate;public void addToBlacklist(String token) {redisTemplate.opsForValue().set(token, "blacklisted", 3600, TimeUnit.SECONDS);}public boolean isTokenBlacklisted(String token) {return redisTemplate.hasKey(token);}
}
在拦截器中验证 JWT 是否在黑名单中:
package com.example.interceptor;import com.example.config.JwtConfig;
import com.example.service.BlacklistService;
import com.example.util.JwtUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;@Component
public class JwtInterceptor implements HandlerInterceptor {@Autowiredprivate JwtUtil jwtUtil;@Autowiredprivate BlacklistService blacklistService;@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {final String authHeader = request.getHeader(JwtConfig.HEADER);if (authHeader != null && authHeader.startsWith("Bearer ")) {final String token = authHeader.substring(7);if (jwtUtil.validateToken(token) && !blacklistService.isTokenBlacklisted(token)) {return true;}}response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");return false;}
}
(三)JWT 的安全性增强
虽然 JWT 本身是安全的,但在实际应用中,还需要考虑一些额外的安全措施,以防止潜在的安全威胁。
- 使用 HTTPS:确保 JWT 在传输过程中使用 HTTPS,防止中间人攻击。
- 限制 JWT 的使用范围:在 JWT 的
Payload
中添加aud
(受众)声明,限制 JWT 的使用范围。 - 使用非对称加密:在敏感应用中,可以使用非对称加密算法(如 RSA)生成 JWT,以提高安全性。
- 限制 JWT 的有效期:将 JWT 的有效期设置为较短的时间,例如 30 分钟,以减少 JWT 被滥用的风险。
五、Spring Boot 与 JWT 的最佳实践
(一)合理设置 JWT 的有效期
JWT 的有效期不宜过长或过短。过长的有效期会增加 JWT 被滥用的风险,而过短的有效期会频繁要求用户重新登录。建议将 JWT 的有效期设置为 30 分钟到 1 小时之间,并结合刷新令牌机制延长用户的会话时间。
(二)保护 JWT 的密钥
JWT 的密钥是安全的关键,必须妥善保护。建议将密钥存储在环境变量或配置中心中,避免直接写在代码中。同时,定期更换密钥,以减少密钥泄露的风险。
(三)使用 HTTPS
在生产环境中,必须使用 HTTPS 来保护 JWT 的传输安全。HTTPS 可以防止中间人攻击,确保 JWT 在客户端和服务器之间安全传输。
(四)限制 JWT 的使用范围
在 JWT 的 Payload
中添加 aud
(受众)声明,限制 JWT 的使用范围。例如,可以指定 JWT 只能在特定的客户端或服务中使用,从而防止 JWT 被滥用。
(五)记录 JWT 的使用日志
记录 JWT 的生成、验证和失效日志,可以帮助开发者及时发现潜在的安全问题。例如,如果发现某个 JWT 被频繁验证,可能表明存在恶意攻击。
(六)实现 JWT 的黑名单机制
在某些情况下,可能需要提前使 JWT 失效,例如用户注销或修改密码。通过实现 JWT 的黑名单机制,可以将这些 JWT 加入黑名单,即使它们未过期,也会被拒绝访问。
六、Spring Boot 与 JWT 的常见问题与解决方案
(一)JWT 过期问题
当 JWT 过期时,客户端会收到 401 Unauthorized
错误。为了解决这个问题,可以实现 JWT 刷新机制,允许客户端使用刷新令牌请求一个新的 JWT。
(二)JWT 被篡改问题
JWT 的签名机制可以防止 JWT 被篡改。如果 JWT 被篡改,服务器在验证 JWT 时会抛出异常。为了解决这个问题,可以使用非对称加密算法(如 RSA)生成 JWT,以提高安全性。
(三)JWT 密钥泄露问题
如果 JWT 的密钥泄露,攻击者可以伪造 JWT。为了解决这个问题,必须妥善保护 JWT 的密钥,避免直接写在代码中。同时,定期更换密钥,以减少密钥泄露的风险。
(四)JWT 被滥用问题
如果 JWT 的有效期过长,可能会被滥用。为了解决这个问题,建议将 JWT 的有效期设置为较短的时间,并结合刷新令牌机制延长用户的会话时间。同时,限制 JWT 的使用范围,防止 JWT 被滥用。