JWT详解与Spring Boot集成实战
一、JWT简介
JSON Web Token (JWT) 是一种开放标准(RFC 7519),用于在各方之间安全地传输声明。JWT通常用于身份验证和信息交换,其特点是自包含性、紧凑性和可扩展性。
1. JWT结构
JWT由三部分组成,用点号(.)分隔:
xxxxx.yyyyy.zzzzz
- Header (头部): 包含令牌类型和使用的签名算法
{
"alg": "HS256",
"typ": "JWT"
} - Payload (负载): 包含声明(claims),分为三种类型:
- 注册声明 (如 iss, sub, aud, exp, nbf, iat, jti)
- 公开声明 (自定义)
- 私有声明 (双方约定的)
{
"sub": "1234567890",
"name": "John Doe",
"admin": true,
"iat": 1516239022
} - Signature (签名): 通过加密头部和负载生成,用于验证令牌完整性和真实性
2. JWT特点
- 无状态:服务端不保存会话状态,适合分布式系统
- 跨语言:支持多种编程语言实现
- 紧凑:体积小,传输效率高
- 自包含:负载中包含所有必要信息
二、JWT工作流程
- 客户端通过用户名和密码登录
- 服务端验证成功后生成JWT并返回给客户端
- 客户端在后续请求中将JWT放在Authorization头部发送给服务端
- 服务端验证JWT的有效性后处理请求
三、JWT安全性注意事项
- 不要在JWT中存储敏感信息(如密码)
- 使用强加密算法(如HS256或RS256)
- 设置合理的过期时间
- 妥善保管密钥
- 实现令牌刷新机制
四、Spring Boot集成JWT实战
1. 添加依赖
在pom.xml
中添加以下依赖:
<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>
2. JWT工具类
创建JwtUtil.java
:
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
@Component
public class JwtUtil {
// 密钥,实际生产环境应使用更安全的方式存储
private final SecretKey key = Keys.secretKeyFor(SignatureAlgorithm.HS256);
// Token有效期(毫秒),这里设置为7天
private final long expirationTime = 7 * 24 * 60 * 60 * 1000;
// 生成Token
public String generateToken(String username) {
Map<String, Object> claims = new HashMap<>();
return createToken(claims, username);
}
private String createToken(Map<String, Object> claims, String subject) {
return Jwts.builder()
.setClaims(claims)
.setSubject(subject)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + expirationTime))
.signWith(key)
.compact();
}
// 从Token中获取用户名
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}
// 检查Token是否过期
public boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}
public Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}
public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}
private Claims extractAllClaims(String token) {
return Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.getBody();
}
// 验证Token
public boolean validateToken(String token) {
try {
Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
return !isTokenExpired(token);
} catch (JwtException | IllegalArgumentException e) {
return false;
}
}
}
3. 过滤器配置
创建JwtRequestFilter.java
:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class JwtRequestFilter extends OncePerRequestFilter {
@Autowired
private JwtUtil jwtUtil;
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
final String authorizationHeader = request.getHeader("Authorization");
String username = null;
String jwt = null;
if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
jwt = authorizationHeader.substring(7);
try {
username = jwtUtil.extractUsername(jwt);
} catch (Exception e) {
// Token无效,继续处理请求
}
}
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
if (jwtUtil.validateToken(jwt)) {
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
usernamePasswordAuthenticationToken
.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
}
}
chain.doFilter(request, response);
}
}
4. 安全配置
修改SecurityConfig.java
:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtRequestFilter jwtRequestFilter;
@Autowired
private CustomUserDetailsService customUserDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(customUserDetailsService).passwordEncoder(passwordEncoder());
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(List.of("*")); // 根据实际情况设置
configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
configuration.setAllowedHeaders(List.of("*"));
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable()
.authorizeRequests()
.antMatchers("/api/auth/**").permitAll() // 登录注册等公开接口
.anyRequest().authenticated()
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
// 添加JWT过滤器
http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
}
}
5. 控制器示例
创建AuthController.java
:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.userdetails.UserDetails;
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 {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private CustomUserDetailsService userDetailsService;
@Autowired
private JwtUtil jwtUtil;
@PostMapping("/api/auth/login")
public ResponseEntity<?> login(@RequestBody LoginRequest loginRequest) {
try {
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword())
);
} catch (BadCredentialsException e) {
return ResponseEntity.badRequest().body("Invalid credentials");
}
final UserDetails userDetails = userDetailsService
.loadUserByUsername(loginRequest.getUsername());
final String jwt = jwtUtil.generateToken(userDetails.getUsername());
return ResponseEntity.ok(new JwtResponse(jwt));
}
}
// 请求和响应DTO类
class LoginRequest {
private String username;
private String password;
// getters and setters
}
class JwtResponse {
private String jwt;
public JwtResponse(String jwt) {
this.jwt = jwt;
}
// getter
}
6. 用户详情服务
创建CustomUserDetailsService.java
:
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 这里应该从数据库获取用户信息
// 示例中使用硬编码数据
if ("admin".equals(username)) {
return new User("admin", "$2a$10$slYQmyNdGzTn7ZLBXBChFOC9f6kFjAqPhccnP6DxlWXx2lPk1C3G6", new ArrayList<>());
} else {
throw new UsernameNotFoundException("User not found with username: " + username);
}
}
}
五、前端使用示例
在实际应用中,前端登录成功后需要将JWT存储在客户端(如localStorage或cookie),并在后续请求中通过Authorization头部发送:
// 登录请求示例
fetch('/api/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
username: 'admin',
password: 'password'
})
})
.then(response => response.json())
.then(data => {
if (data.jwt) {
// 存储JWT
localStorage.setItem('jwt', data.jwt);
// 后续请求添加JWT到头部
fetch('/api/protected', {
method: 'GET',
headers: {
'Authorization': 'Bearer ' + localStorage.getItem('jwt')
}
});
}
});
六、最佳实践
- 密钥管理:生产环境中应使用安全的密钥管理方案,如环境变量或密钥管理服务
- Token有效期:根据业务需求设置合理的过期时间
- 刷新机制:实现令牌刷新功能,避免频繁登录
- 敏感信息:不要在JWT中存储敏感信息
- HTTPS:始终使用HTTPS传输JWT
- 防护措施:实现速率限制,防止暴力破解
通过以上步骤,你可以在Spring Boot应用中成功集成JWT认证机制,实现无状态、安全的身份验证方案。
太涨知识了,宝藏博主