Java JWT集成指南:在Spring Boot中使用jjwt实现认证授权

【免费下载链接】jjwt Java JWT: JSON Web Token for Java and Android 【免费下载链接】jjwt 项目地址: https://gitcode.com/gh_mirrors/jj/jjwt

引言:JWT认证的痛点与解决方案

你是否还在为Spring Boot应用的认证授权方案而困扰?传统的Session认证在分布式系统中面临诸多挑战,如服务器内存占用高、水平扩展困难等问题。而JSON Web Token(JWT,JSON网络令牌)作为一种轻量级的认证机制,正逐渐成为解决这些问题的首选方案。

本文将详细介绍如何使用Java JWT库(jjwt)在Spring Boot应用中实现高效、安全的认证授权功能。通过阅读本文,你将能够:

  • 理解JWT的基本概念和工作原理
  • 掌握在Spring Boot中集成jjwt库的方法
  • 实现基于JWT的用户登录和令牌验证
  • 构建自定义的JWT认证过滤器
  • 处理令牌过期、刷新等常见问题
  • 了解JWT安全最佳实践

JWT简介

JWT的定义与结构

JSON Web Token(JWT)是一种开放标准(RFC 7519),定义了一种紧凑的、自包含的方式,用于在各方之间以JSON对象安全地传输信息。这些信息可以被验证和信任,因为它们是经过数字签名的。

JWT通常由三部分组成,用点(.)分隔:

  • Header(头部)
  • Payload(负载)
  • Signature(签名)

因此,一个典型的JWT看起来是这样的:xxxxx.yyyyy.zzzzz

JWT的工作流程

mermaid

JWT vs 传统Session认证

特性 JWT认证 Session认证
存储位置 客户端 服务器端
服务器负担 轻量 较重,需要存储Session
扩展性 良好,无状态 较差,需要Session共享
跨域支持 良好 较差
安全性 依赖签名验证 依赖Cookie安全性
令牌大小 通常较小 Session ID较小
过期处理 自包含过期信息 服务器端控制

准备工作:环境搭建与依赖配置

系统要求

  • Java 8或更高版本
  • Spring Boot 2.0或更高版本
  • Maven或Gradle构建工具

添加依赖

在Spring Boot项目的pom.xml文件中添加以下依赖:

<!-- Spring Boot Starter Web -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- Spring Boot Starter Security -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

<!-- JJWT API -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.12.3</version>
</dependency>

<!-- JJWT Impl -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.12.3</version>
    <scope>runtime</scope>
</dependency>

<!-- JJWT Jackson Support -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.12.3</version>
    <scope>runtime</scope>
</dependency>

配置JWT参数

在application.properties或application.yml中添加JWT相关配置:

# JWT配置
jwt:
  # 签名密钥,实际使用时应使用更复杂的密钥并妥善保管
  secret: your-secret-key-should-be-very-long-and-secure-for-production-use
  # 令牌过期时间(毫秒),此处设置为24小时
  expiration: 86400000
  # 刷新令牌过期时间(毫秒),此处设置为7天
  refresh-expiration: 604800000
  # 令牌前缀
  token-prefix: Bearer 
  # 请求头名称
  header-string: Authorization

JJWT核心API详解

jjwt库提供了丰富的API来处理JWT令牌的创建、解析和验证。下面介绍一些核心类和接口:

Jwts类

Jwts是jjwt库的入口类,提供了创建JWT构建器、解析器等的静态方法。

// 创建JWT构建器
JwtBuilder builder = Jwts.builder();

// 创建JWT解析器构建器
JwtParserBuilder parserBuilder = Jwts.parser();

JwtBuilder接口

JwtBuilder用于构建JWT令牌,支持设置各种声明和签名算法。

String jwt = Jwts.builder()
    .header()                    // 设置头部信息
        .add("alg", "HS256")     // 设置算法
        .and()
    .claims()                    // 设置负载声明
        .issuer("myapp")         // 签发者
        .subject("user123")      // 主题
        .issuedAt(new Date())    // 签发时间
        .expiration(new Date(System.currentTimeMillis() + 86400000)) // 过期时间
        .and()
    .signWith(Keys.hmacShaKeyFor(secretKey.getBytes()), Jwts.SIG.HS256) // 设置签名
    .compact();                  // 生成紧凑的JWT字符串

JwtParser接口

JwtParser用于解析和验证JWT令牌。

Jws<Claims> jws = Jwts.parser()
    .verifyWith(Keys.hmacShaKeyFor(secretKey.getBytes())) // 设置验证密钥
    .build()
    .parseSignedClaims(jwt);     // 解析JWT并获取声明

Claims claims = jws.getBody();  // 获取负载部分
String subject = claims.getSubject(); // 获取主题
Date expiration = claims.getExpiration(); // 获取过期时间

签名算法

jjwt支持多种签名算法,通过Jwts.SIG类可以方便地访问这些算法:

// HMAC算法
Jwts.SIG.HS256 // HMAC-SHA256
Jwts.SIG.HS384 // HMAC-SHA384
Jwts.SIG.HS512 // HMAC-SHA512

// RSA算法
Jwts.SIG.RS256 // RSA-SHA256
Jwts.SIG.RS384 // RSA-SHA384
Jwts.SIG.RS512 // RSA-SHA512

// ECDSA算法
Jwts.SIG.ES256 // ECDSA-SHA256
Jwts.SIG.ES384 // ECDSA-SHA384
Jwts.SIG.ES512 // ECDSA-SHA512

// EdDSA算法
Jwts.SIG.EdDSA // EdDSA

实现JWT工具类

创建JWT工具类

首先,创建一个JWT工具类,封装令牌的生成、解析和验证等操作:

@Component
public class JwtTokenProvider {
    
    @Value("${jwt.secret}")
    private String secretKey;
    
    @Value("${jwt.expiration}")
    private long expirationTime;
    
    @Value("${jwt.refresh-expiration}")
    private long refreshExpirationTime;
    
    // 生成访问令牌
    public String generateAccessToken(UserDetails userDetails) {
        return generateToken(userDetails, expirationTime);
    }
    
    // 生成刷新令牌
    public String generateRefreshToken(UserDetails userDetails) {
        return generateToken(userDetails, refreshExpirationTime);
    }
    
    // 生成令牌的通用方法
    private String generateToken(UserDetails userDetails, long expiration) {
        // 获取用户角色信息
        List<String> roles = userDetails.getAuthorities().stream()
                .map(GrantedAuthority::getAuthority)
                .collect(Collectors.toList());
        
        // 创建自定义声明
        Map<String, Object> claims = new HashMap<>();
        claims.put("roles", roles);
        
        // 构建并返回JWT令牌
        return Jwts.builder()
                .claims(claims)
                .subject(userDetails.getUsername())
                .issuedAt(new Date())
                .expiration(new Date(System.currentTimeMillis() + expiration))
                .signWith(Keys.hmacShaKeyFor(secretKey.getBytes()), Jwts.SIG.HS512)
                .compact();
    }
    
    // 从令牌中获取用户名
    public String getUsernameFromToken(String token) {
        Jws<Claims> jws = Jwts.parser()
                .verifyWith(Keys.hmacShaKeyFor(secretKey.getBytes()))
                .build()
                .parseSignedClaims(token);
        
        return jws.getBody().getSubject();
    }
    
    // 从令牌中获取角色
    @SuppressWarnings("unchecked")
    public List<String> getRolesFromToken(String token) {
        Jws<Claims> jws = Jwts.parser()
                .verifyWith(Keys.hmacShaKeyFor(secretKey.getBytes()))
                .build()
                .parseSignedClaims(token);
        
        return (List<String>) jws.getBody().get("roles");
    }
    
    // 验证令牌是否有效
    public boolean validateToken(String token) {
        try {
            Jwts.parser()
                    .verifyWith(Keys.hmacShaKeyFor(secretKey.getBytes()))
                    .build()
                    .parseSignedClaims(token);
            return true;
        } catch (SignatureException ex) {
            log.error("Invalid JWT signature");
        } catch (MalformedJwtException ex) {
            log.error("Invalid JWT token");
        } catch (ExpiredJwtException ex) {
            log.error("Expired JWT token");
        } catch (UnsupportedJwtException ex) {
            log.error("Unsupported JWT token");
        } catch (IllegalArgumentException ex) {
            log.error("JWT claims string is empty");
        }
        return false;
    }
}

实现用户认证

创建用户服务

实现用户详情服务,用于加载用户信息:

@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    
    private final UserRepository userRepository;
    
    @Autowired
    public UserDetailsServiceImpl(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
    
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username)
                .orElseThrow(() -> new UsernameNotFoundException("User not found with username: " + username));
        
        return new org.springframework.security.core.userdetails.User(
                user.getUsername(),
                user.getPassword(),
                user.getRoles().stream()
                        .map(role -> new SimpleGrantedAuthority(role.getName()))
                        .collect(Collectors.toList())
        );
    }
}

实现认证控制器

创建认证控制器,处理用户登录请求并生成JWT令牌:

@RestController
@RequestMapping("/api/auth")
public class AuthController {
    
    private final AuthenticationManager authenticationManager;
    private final UserDetailsService userDetailsService;
    private final JwtTokenProvider jwtTokenProvider;
    
    @Autowired
    public AuthController(AuthenticationManager authenticationManager, 
                          UserDetailsService userDetailsService, 
                          JwtTokenProvider jwtTokenProvider) {
        this.authenticationManager = authenticationManager;
        this.userDetailsService = userDetailsService;
        this.jwtTokenProvider = jwtTokenProvider;
    }
    
    @PostMapping("/login")
    public ResponseEntity<?> authenticateUser(@Valid @RequestBody LoginRequest loginRequest) {
        // 认证用户
        Authentication authentication = authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(
                        loginRequest.getUsername(),
                        loginRequest.getPassword()
                )
        );
        
        // 设置认证信息到上下文
        SecurityContextHolder.getContext().setAuthentication(authentication);
        
        // 加载用户详情
        UserDetails userDetails = userDetailsService.loadUserByUsername(loginRequest.getUsername());
        
        // 生成JWT令牌
        String accessToken = jwtTokenProvider.generateAccessToken(userDetails);
        String refreshToken = jwtTokenProvider.generateRefreshToken(userDetails);
        
        // 返回令牌
        return ResponseEntity.ok(new JwtResponse(accessToken, refreshToken));
    }
    
    @PostMapping("/refresh")
    public ResponseEntity<?> refreshToken(@Valid @RequestBody RefreshTokenRequest request) {
        String refreshToken = request.getRefreshToken();
        
        if (jwtTokenProvider.validateToken(refreshToken)) {
            String username = jwtTokenProvider.getUsernameFromToken(refreshToken);
            UserDetails userDetails = userDetailsService.loadUserByUsername(username);
            
            String newAccessToken = jwtTokenProvider.generateAccessToken(userDetails);
            
            return ResponseEntity.ok(new JwtResponse(newAccessToken, refreshToken));
        } else {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid refresh token");
        }
    }
}

实现JWT认证过滤器

创建JWT认证过滤器,用于拦截请求并验证JWT令牌:

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    
    private final JwtTokenProvider jwtTokenProvider;
    private final UserDetailsService userDetailsService;
    
    @Autowired
    public JwtAuthenticationFilter(JwtTokenProvider jwtTokenProvider, 
                                   UserDetailsService userDetailsService) {
        this.jwtTokenProvider = jwtTokenProvider;
        this.userDetailsService = userDetailsService;
    }
    
    @Override
    protected void doFilterInternal(HttpServletRequest request, 
                                    HttpServletResponse response, 
                                    FilterChain filterChain) throws ServletException, IOException {
        try {
            // 从请求中获取JWT令牌
            String jwt = getJwtFromRequest(request);
            
            // 如果令牌存在且有效,则进行认证
            if (StringUtils.hasText(jwt) && jwtTokenProvider.validateToken(jwt)) {
                String username = jwtTokenProvider.getUsernameFromToken(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 ex) {
            logger.error("Could not set user authentication in security context", ex);
        }
        
        // 继续过滤器链
        filterChain.doFilter(request, response);
    }
    
    // 从请求头中获取JWT令牌
    private String getJwtFromRequest(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }
        return null;
    }
}

配置Spring Security

创建安全配置类

配置Spring Security,启用JWT认证:

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
    
    private final UserDetailsService userDetailsService;
    private final JwtAuthenticationFilter jwtAuthenticationFilter;
    
    @Autowired
    public SecurityConfig(UserDetailsService userDetailsService, 
                          JwtAuthenticationFilter jwtAuthenticationFilter) {
        this.userDetailsService = userDetailsService;
        this.jwtAuthenticationFilter = jwtAuthenticationFilter;
    }
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable()) // 禁用CSRF保护,对于API通常不需要
            .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // 无状态会话
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/auth/**").permitAll() // 允许所有用户访问认证接口
                .requestMatchers("/api/public/**").permitAll() // 允许所有用户访问公开接口
                .requestMatchers("/api/admin/**").hasRole("ADMIN") // 仅管理员可访问管理员接口
                .anyRequest().authenticated() // 其他请求需要认证
            )
            .exceptionHandling(ex -> ex
                .authenticationEntryPoint(unauthorizedHandler) // 未认证处理
                .accessDeniedHandler(accessDeniedHandler) // 权限不足处理
            );
        
        // 添加JWT过滤器
        http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
        
        return http.build();
    }
    
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {
        return authConfig.getAuthenticationManager();
    }
    
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

实现资源访问控制

创建示例控制器

创建一个示例控制器,演示基于角色的访问控制:

@RestController
@RequestMapping("/api")
public class ApiController {
    
    // 公开接口
    @GetMapping("/public/hello")
    public String publicHello() {
        return "Hello, Public!";
    }
    
    // 认证用户可访问的接口
    @GetMapping("/hello")
    public String hello() {
        return "Hello, User!";
    }
    
    // 仅管理员可访问的接口
    @GetMapping("/admin/hello")
    @PreAuthorize("hasRole('ADMIN')")
    public String adminHello() {
        return "Hello, Admin!";
    }
    
    // 使用方法级权限控制示例
    @GetMapping("/users")
    @PreAuthorize("hasAnyRole('ADMIN', 'USER_MANAGER')")
    public List<String> getUsers() {
        // 返回用户列表
        return Arrays.asList("user1", "user2", "user3");
    }
}

JWT高级特性

自定义声明

除了标准声明外,jjwt还支持添加自定义声明:

String jwt = Jwts.builder()
    .claims()
        .issuer("myapp")
        .subject("user123")
        .add("userId", 123)         // 自定义用户ID
        .add("roles", Arrays.asList("USER", "ADMIN")) // 自定义角色
        .add("permissions", Arrays.asList("read", "write")) // 自定义权限
        .and()
    .signWith(Keys.hmacShaKeyFor(secretKey.getBytes()), Jwts.SIG.HS512)
    .compact();

令牌压缩

jjwt支持对JWT负载进行压缩,减少令牌大小:

String jwt = Jwts.builder()
    .claims()
        .issuer("myapp")
        .subject("user123")
        .and()
    .compressWith(CompressionCodecs.GZIP) // 使用GZIP压缩
    .signWith(Keys.hmacShaKeyFor(secretKey.getBytes()), Jwts.SIG.HS512)
    .compact();

加密JWT (JWE)

除了签名JWT(JWS)外,jjwt还支持加密JWT(JWE),提供更高的安全性:

// 生成加密密钥
SecretKey encryptionKey = Keys.secretKeyFor(Jwts.ENC.A256GCM);

// 创建JWE
String jwe = Jwts.builder()
    .claims()
        .issuer("myapp")
        .subject("user123")
        .and()
    .encryptWith(encryptionKey, Jwts.KEY.A256KW, Jwts.ENC.A256GCM)
    .compact();

// 解析JWE
Jwe<Claims> jwe = Jwts.parser()
    .decryptWith(encryptionKey)
    .build()
    .parseEncryptedClaims(jwe);

Claims claims = jwe.getPayload();

JWT安全最佳实践1. 使用安全的签名算法

选择足够强度的签名算法,如HS256、RS256或ES256,避免使用不安全的算法。

// 推荐使用的签名算法
Jwts.SIG.HS256 // HMAC-SHA256,至少256位密钥
Jwts.SIG.RS256 // RSA-SHA256,至少2048位密钥
Jwts.SIG.ES256 // ECDSA-SHA256,使用P-256曲线

2. 保护签名密钥

签名密钥应保持机密,避免硬编码在代码中。在生产环境中,应使用安全的密钥管理服务。

// 不推荐:硬编码密钥
String secretKey = "my-secret-key";

// 推荐:从环境变量或安全配置服务获取密钥
String secretKey = System.getenv("JWT_SECRET_KEY");

3. 设置合理的令牌过期时间

访问令牌的过期时间不宜过长,建议设置为15分钟到1小时。对于长期访问,可以使用刷新令牌机制。

// 设置合理的过期时间(1小时)
long expirationTime = 3600000; // 毫秒

4. 使用HTTPS传输

始终使用HTTPS协议传输JWT令牌,防止中间人攻击和令牌被窃听。

5. 实现令牌撤销机制

尽管JWT本身不支持撤销,但可以通过以下方式实现类似功能:

  • 维护令牌黑名单
  • 使用短期令牌并结合刷新令牌机制
  • 在关键声明中包含版本号或令牌ID

6. 验证所有必要的声明

在解析JWT时,确保验证所有必要的声明,如签发者、受众、过期时间等。

Jws<Claims> jws = Jwts.parser()
    .verifyWith(verificationKey)
    .requireIssuer("myapp") // 验证签发者
    .requireAudience("myaudience") // 验证受众
    .build()
    .parseSignedClaims(jwt);

常见问题与解决方案

令牌过期处理

当JWT令牌过期时,客户端需要重新登录或使用刷新令牌获取新的访问令牌。

// 前端处理令牌过期的示例代码
axios.interceptors.response.use(
    response => response,
    error => {
        const originalRequest = error.config;
        
        // 如果是401错误且未尝试刷新令牌
        if (error.response.status === 401 && !originalRequest._retry) {
            originalRequest._retry = true;
            
            // 尝试使用刷新令牌获取新的访问令牌
            return axios.post('/api/auth/refresh', { refreshToken: localStorage.getItem('refreshToken') })
                .then(response => {
                    // 存储新的访问令牌
                    localStorage.setItem('accessToken', response.data.accessToken);
                    
                    // 使用新令牌重试原始请求
                    originalRequest.headers['Authorization'] = 'Bearer ' + response.data.accessToken;
                    return axios(originalRequest);
                })
                .catch(err => {
                    // 刷新令牌也过期,需要重新登录
                    localStorage.removeItem('accessToken');
                    localStorage.removeItem('refreshToken');
                    window.location.href = '/login';
                    return Promise.reject(err);
                });
        }
        
        return Promise.reject(error);
    }
);

令牌吊销

实现令牌吊销的一种常见方法是维护一个令牌黑名单:

@Service
public class TokenBlacklistService {
    
    private final Set<String> blacklist = ConcurrentHashMap.newKeySet();
    private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
    
    // 将令牌加入黑名单,并设置自动移除时间
    public void blacklistToken(String token, long expirationTime) {
        blacklist.add(token);
        
        // 令牌过期后自动从黑名单中移除
        scheduler.schedule(() -> blacklist.remove(token), expirationTime, TimeUnit.MILLISECONDS);
    }
    
    // 检查令牌是否在黑名单中
    public boolean isTokenBlacklisted(String token) {
        return blacklist.contains(token);
    }
}

然后在JWT认证过滤器中添加黑名单检查:

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) 
        throws ServletException, IOException {
    try {
        String jwt = getJwtFromRequest(request);
        
        if (StringUtils.hasText(jwt)) {
            // 检查令牌是否在黑名单中
            if (tokenBlacklistService.isTokenBlacklisted(jwt)) {
                response.sendError(HttpStatus.UNAUTHORIZED.value(), "Token has been revoked");
                return;
            }
            
            if (jwtTokenProvider.validateToken(jwt)) {
                // 令牌有效,设置认证信息
                // ...
            }
        }
    } catch (Exception ex) {
        // 处理异常
        // ...
    }
    
    filterChain.doFilter(request, response);
}

总结与展望

本文详细介绍了如何在Spring Boot应用中使用jjwt库实现JWT认证授权。通过集成jjwt,我们可以构建出安全、高效、无状态的认证系统,非常适合分布式应用和微服务架构。

主要内容回顾:

  1. JWT的基本概念和工作原理
  2. 在Spring Boot中集成jjwt库
  3. 实现JWT工具类处理令牌的创建、解析和验证
  4. 构建基于JWT的认证和授权系统
  5. JWT高级特性和安全最佳实践
  6. 常见问题如令牌过期、刷新和吊销的解决方案

未来展望:

  • 探索更高级的JWT特性,如JWE加密、声明加密等
  • 结合OAuth2.0和OpenID Connect,构建更完整的身份认证系统
  • 使用分布式缓存(如Redis)优化令牌黑名单和刷新令牌的存储
  • 实现更细粒度的权限控制,如基于属性的访问控制(ABAC)

通过合理使用JWT,我们可以构建出更加安全、灵活和可扩展的认证授权系统,为用户提供更好的体验,同时简化系统架构和维护成本。

参考资料

  1. JSON Web Token (JWT) - RFC 7519
  2. JJWT官方文档
  3. Spring Security官方文档
  4. Spring Boot官方文档
  5. OWASP JWT安全最佳实践

【免费下载链接】jjwt Java JWT: JSON Web Token for Java and Android 【免费下载链接】jjwt 项目地址: https://gitcode.com/gh_mirrors/jj/jjwt

更多推荐