- JWT 简介
JWT(JSON Web Token)是一种开放标准(RFC 7519),用于在网络应用间传递声明。它由三部分组成:
· Header(头部):通常包含令牌类型(JWT)和签名算法(如 HMAC SHA256)。
· Payload(载荷):包含实体(通常是用户)的声明,例如用户 ID、角色、过期时间等。
· Signature(签名):用于验证消息在传输过程中未被篡改。
JWT 的优势在于无状态、跨语言、可扩展,非常适合微服务架构中的认证。
- 技术栈
· Spring Boot 2.x / 3.x
· Spring Security
· java-jwt(或 jjwt、auth0/java-jwt)
· Lombok(可选,简化代码)
- 项目初始化
创建 Spring Boot 项目,添加以下依赖(以 Maven 为例):
xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<!-- JWT 支持,这里使用 jjwt -->
<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>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
- 创建用户实体和 Repository
假设我们有一个简单的用户表。
java
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String password;
private String role; // 例如 "ROLE_USER", "ROLE_ADMIN"
}
对应的 Repository:
java
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByUsername(String username);
}
为了方便测试,可以在启动时插入一个用户。
java
@Component
public class DataLoader implements CommandLineRunner {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
public DataLoader(UserRepository userRepository, PasswordEncoder passwordEncoder) {
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
}
@Override
public void run(String... args) throws Exception {
if (userRepository.count() == 0) {
User user = new User();
user.setUsername("admin");
user.setPassword(passwordEncoder.encode("123456"));
user.setRole("ROLE_ADMIN");
userRepository.save(user);
}
}
}
- JWT 工具类
我们需要一个工具类来生成和解析 JWT。这里使用 jjwt。
java
@Component
public class JwtUtils {
@Value("${app.jwt.secret}")
private String jwtSecret;
@Value("${app.jwt.expiration-ms}")
private int jwtExpirationMs;
// 生成 JWT token
public String generateJwtToken(Authentication authentication) {
UserDetailsImpl userPrincipal = (UserDetailsImpl) authentication.getPrincipal();
return Jwts.builder()
.setSubject((userPrincipal.getUsername()))
.setIssuedAt(new Date())
.setExpiration(new Date((new Date()).getTime() + jwtExpirationMs))
.signWith(key(), SignatureAlgorithm.HS256)
.compact();
}
// 从 JWT 中提取用户名
public String getUserNameFromJwtToken(String token) {
return Jwts.parserBuilder().setSigningKey(key()).build()
.parseClaimsJws(token).getBody().getSubject();
}
// 签名密钥
private Key key() {
return Keys.hmacShaKeyFor(jwtSecret.getBytes(StandardCharsets.UTF_8));
}
// 验证 JWT
public boolean validateJwtToken(String authToken) {
try {
Jwts.parserBuilder().setSigningKey(key()).build().parse(authToken);
return true;
} catch (MalformedJwtException e) {
// log
} catch (ExpiredJwtException e) {
// log
} catch (UnsupportedJwtException e) {
// log
} catch (IllegalArgumentException e) {
// log
}
return false;
}
}
在 application.yml 中配置:
yaml
app:
jwt:
secret: mySecretKeyForJWTGenerationWithAtLeast32CharactersLong!
expiration-ms: 86400000 # 24小时
- Spring Security 配置
6.1 UserDetails 和 UserDetailsService
实现 Spring Security 的 UserDetailsService 从数据库加载用户。
java
@Data
public class UserDetailsImpl implements UserDetails {
private Long id;
private String username;
private String password;
private Collection<? extends GrantedAuthority> authorities;
public static UserDetailsImpl build(User user) {
List<GrantedAuthority> authorities = Collections.singletonList(new SimpleGrantedAuthority(user.getRole()));
return new UserDetailsImpl(user.getId(), user.getUsername(), user.getPassword(), authorities);
}
// 构造函数、getters、重写的方法...
}
Service:
java
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
@Transactional
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User Not Found with username: " + username));
return UserDetailsImpl.build(user);
}
}
6.2 认证过滤器
创建一个过滤器,用于拦截请求并验证 JWT。
java
@Component
public class AuthTokenFilter extends OncePerRequestFilter {
@Autowired
private JwtUtils jwtUtils;
@Autowired
private UserDetailsServiceImpl userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
try {
String jwt = parseJwt(request);
if (jwt != null && jwtUtils.validateJwtToken(jwt)) {
String username = jwtUtils.getUserNameFromJwtToken(jwt);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception e) {
// log
}
filterChain.doFilter(request, response);
}
private String parseJwt(HttpServletRequest request) {
String headerAuth = request.getHeader("Authorization");
if (StringUtils.hasText(headerAuth) && headerAuth.startsWith("Bearer ")) {
return headerAuth.substring(7);
}
return null;
}
}
6.3 认证入口点(处理未授权访问)
当用户尝试访问受保护资源但未提供有效凭证时,返回 401。
java
@Component
public class AuthEntryPointJwt implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Error: Unauthorized");
}
}
6.4 安全配置类
配置哪些路径需要认证,哪些可以公开,并添加过滤器。
java
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig {
@Autowired
private UserDetailsServiceImpl userDetailsService;
@Autowired
private AuthEntryPointJwt unauthorizedHandler;
@Autowired
private AuthTokenFilter authTokenFilter;
@Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService);
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {
return authConfig.getAuthenticationManager();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable()
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeRequests().antMatchers("/api/auth/**").permitAll()
.antMatchers("/api/test/**").permitAll()
.anyRequest().authenticated();
http.authenticationProvider(authenticationProvider());
http.addFilterBefore(authTokenFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
注意:Spring Security 5.7 之后推荐使用 SecurityFilterChain 替代 WebSecurityConfigurerAdapter。
- 认证控制器(登录)
创建控制器,接收用户名密码,验证成功后返回 JWT。
java
@RestController
@RequestMapping("/api/auth")
public class AuthController {
@Autowired
AuthenticationManager authenticationManager;
@Autowired
JwtUtils jwtUtils;
@Autowired
UserRepository userRepository;
@PostMapping("/signin")
public ResponseEntity<?> authenticateUser(@RequestBody LoginRequest loginRequest) {
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword()));
SecurityContextHolder.getContext().setAuthentication(authentication);
String jwt = jwtUtils.generateJwtToken(authentication);
UserDetailsImpl userDetails = (UserDetailsImpl) authentication.getPrincipal();
List<String> roles = userDetails.getAuthorities().stream()
.map(item -> item.getAuthority())
.collect(Collectors.toList());
return ResponseEntity.ok(new JwtResponse(jwt, userDetails.getId(), userDetails.getUsername(), roles));
}
}
其中 LoginRequest 和 JwtResponse 是简单的 DTO。
- 测试受保护接口
创建一个测试控制器,验证 Token 是否生效。
java
@RestController
@RequestMapping("/api/test")
public class TestController {
@GetMapping("/user")
@PreAuthorize("hasRole('USER') or hasRole('ADMIN')")
public String userAccess() {
return "User Content.";
}
@GetMapping("/admin")
@PreAuthorize("hasRole('ADMIN')")
public String adminAccess() {
return "Admin Board.";
}
}
注意:方法级权限需要 @EnableGlobalMethodSecurity(prePostEnabled = true)。
-
测试流程
-
启动应用。
-
发送 POST 请求 /api/auth/signin,携带 JSON:
json{ "username": "admin", "password": "123456" }返回 JWT token。
-
使用该 token 访问 /api/test/admin,在请求头中加入 Authorization: Bearer ,应能成功访问。
-
如果不带 token 或 token 无效,返回 401。
- 补充:刷新 Token 实现
通常 JWT 的有效期较短,可以配合 Refresh Token 实现无感刷新。常见做法是登录时返回 access_token(短效)和 refresh_token(长效),并提供刷新端点。
这里简述思路:
· 生成 access_token 和 refresh_token,refresh_token 存储在数据库(或 Redis)中。
· 刷新端点接收 refresh_token,验证后生成新的 access_token。
- 注意事项
· 密钥安全:生产环境应将 JWT 密钥通过环境变量或配置中心管理,不要硬编码。
· 无状态:JWT 认证是无状态的,但需要留意注销功能(通常结合黑名单)。
· HTTPS:生产环境务必使用 HTTPS 防止中间人攻击。
· Token 存储:前端可将 token 存储在内存、localStorage 或 httpOnly Cookie 中,根据安全需求选择。
· 过期时间:access_token 不宜过长(如 15-30 分钟),refresh_token 可长一些(如 7 天)。
通过以上步骤,你可以在 Spring Boot 中实现一套完整的基于 JWT 的认证系统。可以根据实际业务调整用户角色、权限控制粒度以及令牌的有效期。