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

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

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值