好嘞,各位看官老爷,今天咱们就来聊聊Spring Security JWT令牌这档子事儿!🚀 想象一下,咱们的网站就像一座戒备森严的城堡,用户想要进出,总得有个凭证吧?传统的Session-Cookie机制就像是给每个人发一张房卡,服务器还得记住每张卡对应谁,时间一长,卡多了,服务器就懵圈了,记不住了!🤯
这时候,JWT令牌就闪亮登场了!它就像是一种“自包含”的通行证,里面包含了用户的信息,服务器拿到通行证,不用查数据库,一看就知道你是谁,能干啥。这玩意儿,轻便、高效,简直是分布式架构下的完美搭档! 😎
一、啥是JWT?(这玩意儿怎么来的?)
JWT,全称JSON Web Token,是一种基于JSON的开放标准(RFC 7519),用于在各方之间安全地传输信息。 简单来说,它就是个字符串,但这个字符串里塞满了信息,而且还经过了加密处理,防止被人篡改。
你可以把JWT想象成一个封装精美的快递包裹📦,里面装了用户信息,外面贴了标签(头部信息和签名),确保包裹在运输过程中不会被拆开偷东西。
二、JWT长啥样?(拆开包裹看看!)
一个JWT令牌通常由三部分组成,用点号(.)分隔:
-
Header(头部): 描述令牌的元数据,通常包含令牌的类型(JWT)和使用的签名算法(例如HMAC SHA256或RSA)。
{ "alg": "HS256", "typ": "JWT" }这部分会进行Base64Url编码。
-
Payload(载荷): 包含令牌声明(Claims),这些声明是关于用户或其他实体以及额外数据的声明。 有三种类型的声明:
- Registered claims(注册声明): 一些预定义的声明,建议使用,但不是强制性的。 例如:
iss(签发者)、sub(主题)、aud(受众)、exp(过期时间)、nbf(生效时间)、iat(签发时间)、jti(JWT ID)。 - Public claims(公共声明): 可以自定义的声明,但为了避免冲突,建议在IANA JSON Web Token Registry注册。
- Private claims(私有声明): 自定义的声明,用于在应用程序之间共享信息。
{ "sub": "1234567890", "name": "John Doe", "admin": true, "iat": 1516239022 }这部分同样会进行Base64Url编码。
- Registered claims(注册声明): 一些预定义的声明,建议使用,但不是强制性的。 例如:
-
Signature(签名): 用于验证令牌的完整性,确保令牌没有被篡改。 签名通过以下方式计算:
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret )其中
secret是一个只有服务器知道的密钥。简单来说,就是用头部和载荷的信息,加上一个神秘的钥匙(secret),通过加密算法生成一个签名。
三、Spring Security + JWT:强强联合!
Spring Security是Java领域最流行的安全框架,它提供了强大的认证和授权功能。 结合JWT,我们可以构建出更加灵活、可扩展的安全系统。
1. 引入依赖
首先,你得在你的pom.xml文件中添加JWT相关的依赖,比如:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<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. 定义UserDetailsService
UserDetailsService是Spring Security用来加载用户信息的接口。 你需要实现这个接口,从数据库或其他数据源中获取用户信息。
@Service
public class MyUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository; // 假设你有一个UserRepository
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found with username: " + username));
// 将User对象转换成UserDetails对象
return new org.springframework.security.core.userdetails.User(
user.getUsername(),
user.getPassword(),
AuthorityUtils.createAuthorityList(user.getRole()) // 假设你的User类有个getRole()方法
);
}
}
3. JWT工具类
接下来,我们需要一个JWT工具类,用来生成和解析JWT令牌。
@Component
public class JwtUtil {
@Value("${jwt.secret}")
private String secret; // 从配置文件中读取密钥
@Value("${jwt.expiration}")
private long expiration; // 从配置文件中读取过期时间,单位是秒
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
// 可以添加自定义的claims,例如用户ID
claims.put("userId", "123");
return createToken(claims, userDetails.getUsername());
}
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() + expiration * 1000)) // 转换成毫秒
.signWith(Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)), SignatureAlgorithm.HS256) // 使用HS256算法
.compact();
}
public String getUsernameFromToken(String token) {
return getClaimFromToken(token, Claims::getSubject);
}
public Date getExpirationDateFromToken(String token) {
return getClaimFromToken(token, Claims::getExpiration);
}
public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
final Claims claims = getAllClaimsFromToken(token);
return claimsResolver.apply(claims);
}
private Claims getAllClaimsFromToken(String token) {
return Jwts.parserBuilder()
.setSigningKey(Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)))
.build()
.parseClaimsJws(token)
.getBody();
}
private Boolean isTokenExpired(String token) {
final Date expiration = getExpirationDateFromToken(token);
return expiration.before(new Date());
}
public Boolean validateToken(String token, UserDetails userDetails) {
final String username = getUsernameFromToken(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
}
4. JWT过滤器
我们需要一个JWT过滤器,拦截请求,从请求头中提取JWT令牌,并验证令牌的有效性。
@Component
public class JwtRequestFilter extends OncePerRequestFilter {
@Autowired
private MyUserDetailsService userDetailsService;
@Autowired
private JwtUtil jwtUtil;
@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); // 去掉"Bearer "前缀
try {
username = jwtUtil.getUsernameFromToken(jwt);
} catch (ExpiredJwtException e) {
// Token过期
System.out.println("JWT令牌已过期");
} catch (SignatureException e) {
// 签名不匹配
System.out.println("JWT签名无效");
} catch (Exception e) {
System.out.println("解析JWT令牌失败: " + e.getMessage());
}
}
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
if (jwtUtil.validateToken(jwt, userDetails)) {
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
usernamePasswordAuthenticationToken
.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
}
}
chain.doFilter(request, response);
}
}
5. Spring Security配置
最后,我们需要配置Spring Security,将JWT过滤器添加到过滤器链中。
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(securedEnabled = true, jsr250Enabled = true) // 启用方法级别的安全控制
public class SecurityConfig {
@Autowired
private JwtRequestFilter jwtRequestFilter;
@Autowired
private MyUserDetailsService userDetailsService;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(); // 使用BCryptPasswordEncoder进行密码加密
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests((authz) -> authz
.requestMatchers("/authenticate").permitAll() // 允许访问/authenticate接口
.requestMatchers("/register").permitAll() // 允许访问/register接口
.anyRequest().authenticated() // 其他所有请求都需要认证
)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 使用STATELESS,不创建session
)
.authenticationProvider(authenticationProvider()) // 添加自定义AuthenticationProvider
.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class); // 将JWT过滤器添加到UsernamePasswordAuthenticationFilter之前
return http.build();
}
@Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService);
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
}
6. 创建一个AuthenticationController
这个Controller用于处理用户登录,并返回JWT Token。
@RestController
public class AuthenticationController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JwtUtil jwtUtil;
@Autowired
private MyUserDetailsService userDetailsService;
@PostMapping("/authenticate")
public ResponseEntity<?> createAuthenticationToken(@RequestBody AuthenticationRequest authenticationRequest) throws Exception {
try {
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(authenticationRequest.getUsername(), authenticationRequest.getPassword())
);
} catch (DisabledException e) {
throw new Exception("USER_DISABLED", e);
} catch (BadCredentialsException e) {
throw new Exception("INVALID_CREDENTIALS", e);
}
final UserDetails userDetails = userDetailsService
.loadUserByUsername(authenticationRequest.getUsername());
final String token = jwtUtil.generateToken(userDetails);
return ResponseEntity.ok(new AuthenticationResponse(token));
}
}
// AuthenticationRequest 和 AuthenticationResponse 类需要自己定义
四、配置文件的秘密
别忘了在application.properties或application.yml文件中配置密钥和过期时间!
jwt.secret=YourSecretKey # 替换成一个安全的密钥,例如使用 UUID 生成
jwt.expiration=3600 # 单位是秒,这里表示1小时
五、优点和缺点(不能光看贼吃肉,也得看贼挨打!)
优点:
- 无状态: 服务器不需要存储会话信息,减轻了服务器的负担,易于扩展。
- 可扩展性: 适用于分布式系统和微服务架构。
- 安全性: 通过签名保证令牌的完整性,防止篡改。
- 跨域支持: 可以跨域共享认证信息。
缺点:
- 令牌撤销困难: 一旦JWT被签发,在过期之前都是有效的,无法主动撤销。 需要依赖短过期时间和黑名单机制来解决。
- 令牌大小: 相比于Session ID,JWT令牌的体积较大,可能会增加网络传输的负担。
- 密钥安全: 密钥的安全性至关重要,一旦泄露,所有令牌都可能被伪造。
六、实战演练(光说不练假把式!)
- 注册用户: 创建一个注册接口,将用户信息保存到数据库,并使用
BCryptPasswordEncoder对密码进行加密。 - 登录认证: 创建一个登录接口,验证用户的用户名和密码,如果验证成功,则生成JWT令牌并返回给客户端。
- 访问受保护的资源: 在客户端的每个请求头中添加
Authorization字段,值为Bearer <JWT令牌>。 服务器端的JWT过滤器会验证令牌的有效性,并授权用户访问受保护的资源。
七、注意事项(细节决定成败!)
- 密钥安全: 务必保护好你的密钥,不要将其暴露在代码中或公开的配置文件中。 建议使用环境变量或专门的密钥管理工具。
- 过期时间: 设置合理的过期时间,防止令牌被滥用。
- 黑名单机制: 可以使用Redis或其他缓存系统维护一个黑名单,用于存储被吊销的JWT令牌。
- 刷新令牌: 可以使用刷新令牌机制,允许用户在令牌过期后无需重新登录即可获取新的令牌。
八、总结(敲黑板,划重点!)
Spring Security + JWT 是一种强大的安全解决方案,可以帮助你构建出更加安全、可扩展的Web应用程序。 但是,你需要理解JWT的原理和优缺点,并根据你的实际需求进行合理的配置和优化。
希望这篇文章能帮助你更好地理解Spring Security JWT令牌,并在你的项目中成功应用! 如果你还有任何问题,欢迎在评论区留言,我会尽力解答! 😉