JWT 实现登录
一、JWT 概述
JWT 结构
┌─────────────────────────────────────────────────────────────────┐
│ JWT 结构 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
│ ────────────────┬──────────────────┬────────────────────────────│
│ Header │ Payload │ Signature │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────────┐│
│ │ { │ │ { │ │ ││
│ │ "alg": │ │ "sub": │ │ HMACSHA256( ││
│ │ "HS256",│ │ "123", │ │ base64(header) + "." + ││
│ │ "typ": │ │ "name": │ │ base64(payload), ││
│ │ "JWT" │ │ "John" │ │ secret ││
│ │ } │ │ "iat": │ │ ) ││
│ │ │ │ 123, │ │ ││
│ │ │ │ "exp": │ │ ││
│ │ │ │ 1234 │ │ ││
│ │ │ │ } │ │ ││
│ └─────────────┘ └─────────────┘ └─────────────────────────────┘│
│ │
└─────────────────────────────────────────────────────────────────┘JWT 组成部分
json
// Header
{
"alg": "HS256", // 签名算法
"typ": "JWT" // 类型
}
// Payload
{
"sub": "1234567890", // 主题(用户ID)
"name": "John Doe", // 自定义 claims
"role": "admin",
"iat": 1516239022, // 签发时间
"exp": 1516242622 // 过期时间
}
// Signature
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)二、JJWT 使用
Maven 依赖
xml
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.3</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.3</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.3</version>
<scope>runtime</scope>
</dependency>工具类
java
@Component
public class JwtUtils {
private static final String SECRET = "your-256-bit-secret-key-here-must-be-at-least-32-chars";
private static final long EXPIRATION = 24 * 60 * 60 * 1000; // 24小时
// 生成 Token
public String generateToken(UserDetails user) {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + EXPIRATION);
return Jwts.builder()
.subject(user.getUsername())
.claim("role", user.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.joining(",")))
.issuedAt(now)
.expiration(expiryDate)
.signWith(Keys.hmacShaKeyFor(SECRET.getBytes()))
.compact();
}
// 解析 Token
public Claims parseToken(String token) {
return Jwts.parser()
.verifyWith(Keys.hmacShaKeyFor(SECRET.getBytes()))
.build()
.parseSignedClaims(token)
.getPayload();
}
// 获取用户名
public String getUsername(String token) {
return parseToken(token).getSubject();
}
// 获取角色
public String getRole(String token) {
return parseToken(token).get("role", String.class);
}
// 验证 Token
public boolean validateToken(String token) {
try {
parseToken(token);
return true;
} catch (JwtException | IllegalArgumentException e) {
return false;
}
}
// 判断是否过期
public boolean isTokenExpired(String token) {
try {
Date expiration = parseToken(token).getExpiration();
return expiration.before(new Date());
} catch (Exception e) {
return true;
}
}
}三、Spring Security + JWT
JWT 过滤器
java
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtUtils jwtUtils;
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
String token = extractToken(request);
if (StringUtils.hasText(token) && jwtUtils.validateToken(token)) {
String username = jwtUtils.getUsername(token);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.getAuthorities()
);
authentication.setDetails(
new WebAuthenticationDetailsSource().buildDetails(request)
);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
chain.doFilter(request, response);
}
private String extractToken(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}Security 配置
java
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
private JwtAuthenticationFilter jwtFilter;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/login", "/register").permitAll()
.anyRequest().authenticated()
)
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}四、登录接口
java
@RestController
public class AuthController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JwtUtils jwtUtils;
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest request) {
try {
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
request.getUsername(),
request.getPassword()
)
);
SecurityContextHolder.getContext().setAuthentication(authentication);
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
String token = jwtUtils.generateToken(userDetails);
return ResponseEntity.ok(Map.of(
"token", token,
"username", userDetails.getUsername()
));
} catch (AuthenticationException e) {
return ResponseEntity.status(401).body(Map.of(
"message", "用户名或密码错误"
));
}
}
}五、Token 刷新
刷新机制
java
// 请求头携带 Refresh Token
@PostMapping("/refresh")
public ResponseEntity<?> refresh(@RequestHeader("Authorization") String authHeader) {
String oldToken = authHeader.substring(7);
if (jwtUtils.isTokenExpired(oldToken)) {
return ResponseEntity.status(401).body(Map.of(
"message", "Token 已过期,请重新登录"
));
}
String username = jwtUtils.getUsername(oldToken);
UserDetails user = userDetailsService.loadUserByUsername(username);
String newToken = jwtUtils.generateToken(user);
return ResponseEntity.ok(Map.of("token", newToken));
}六、Token 黑名单
java
@Service
public class TokenBlacklistService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
private static final String BLACKLIST_PREFIX = "token:blacklist:";
// 加入黑名单
public void blacklist(String token, long expirationMillis) {
String key = BLACKLIST_PREFIX + token;
redisTemplate.opsForValue().set(key, "1",
Duration.ofMillis(expirationMillis));
}
// 检查是否在黑名单
public boolean isBlacklisted(String token) {
String key = BLACKLIST_PREFIX + token;
return Boolean.TRUE.equals(redisTemplate.hasKey(key));
}
}
// JWT 过滤器中检查
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) {
String token = extractToken(request);
if (StringUtils.hasText(token)) {
// 检查黑名单
if (tokenBlacklistService.isBlacklisted(token)) {
chain.doFilter(request, response);
return;
}
// ... 验证逻辑
}
}七、面试高频问题
Q1: JWT 的缺点?
- Token 一旦签发,无法撤销(除非加入黑名单)
- Payload 是 Base64 编码,可被解密(不放敏感信息)
- 较长,增加请求开销
Q2: Token 过期怎么办?
- 客户端使用 Refresh Token 换取新 Token
- 返回 401,客户端跳转到登录页
Q3: 如何防止 Token 被盗用?
- HTTPS 传输
- 不在 URL 中传递 Token
- 设置合理的过期时间
- Token 黑名单机制
八、下一章预告
下一章我们将学习 权限控制 RBAC:
- RBAC 模型
- 动态权限加载
- 方法级权限控制