Skip to content

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 的缺点?

  1. Token 一旦签发,无法撤销(除非加入黑名单)
  2. Payload 是 Base64 编码,可被解密(不放敏感信息)
  3. 较长,增加请求开销

Q2: Token 过期怎么办?

  1. 客户端使用 Refresh Token 换取新 Token
  2. 返回 401,客户端跳转到登录页

Q3: 如何防止 Token 被盗用?

  1. HTTPS 传输
  2. 不在 URL 中传递 Token
  3. 设置合理的过期时间
  4. Token 黑名单机制

八、下一章预告

下一章我们将学习 权限控制 RBAC

  • RBAC 模型
  • 动态权限加载
  • 方法级权限控制

基于 MIT 许可发布