Spring Boot 3.0 结合 Spring Security 与 MySQL 数据库实现 JWT 身份验证

在我们构建现代企业级应用时,安全性始终是首要考量。虽然传统的 Session 机制在单体应用中表现出色,但在面对微服务架构和分布式系统的挑战时,JWT(JSON Web Token)凭借其无状态和可扩展的特性,已成为行业标准。特别是展望 2026 年,随着云原生和边缘计算的普及,构建一个既安全又高效的认证系统比以往任何时候都更加关键。

在这个项目中,我们将一起构建一个基于 Spring Boot 3.0 的应用,融合 MySQL 数据库与 Spring Security。我们不仅会实现基础的注册登录,还会深入探讨在 2026 年的开发环境中,如何利用 AI 辅助工具(如 GitHub Copilot 或 Cursor)来加速开发,并确保我们的代码符合生产级标准。

步骤 1:创建项目与现代初始化

首先,让我们从 Spring Initializr 开始。在选择依赖时,我们不仅要满足当前需求,还要考虑到未来的可扩展性。以下是我们的核心依赖列表,以及为什么我们需要它们:

  • Spring Web: 构建 RESTful API 的基石。
  • Spring Security: 保护我们端点的强大框架。
  • MySQL Driver: 连接我们的持久化数据存储。
  • Spring Data JPA: 简化数据访问层的开发。
  • Lombok: 减少样板代码,让我们的代码更干净(虽然这在 2026 年可能因 Record 的普及而减少使用,但在遗留代码维护中依然有用)。

提示: 如果你正在使用 CursorWindsurf 这样的 AI 驱动 IDE,你可以直接在终端中使用 spring init 命令,或者让 AI 帮你生成初始的项目脚手架。

接下来是 JWT 的依赖。在 2026 年,我们可能会看到更多关于 JWK(JSON Web Key)的内置支持,但为了广泛的兼容性和对旧系统的迁移支持,我们依然坚持使用成熟的 jjwt 库。

请将以下代码段添加到你的 pom.xml 中。注意,我们使用了 Maven 属性来统一管理版本,这在多模块项目中是最佳实践。



    io.jsonwebtoken
    jjwt-api
    0.11.5


    io.jsonwebtoken
    jjwt-impl
    0.11.5
    runtime


    io.jsonwebtoken
    jjwt-jackson
    0.11.5
    runtime

步骤 2:深入数据库架构与领域模型

在 2026 年的视角下,我们不再仅仅把数据库视为数据存储,而是将其视为领域模型的一部分。让我们定义一个稳健的 User 实体。在生产环境中,我们通常希望考虑软删除和时间戳审计。

你可能已经注意到,我们在代码中加入了 INLINECODE53ca02fe 和 INLINECODE13bf595b。这是为了符合 合规性审计 要求,这在现代金融或医疗应用中是必须的。

package com.example.jwtsecurity.model;

import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import java.time.LocalDateTime;
import java.util.Set;

@Entity
@Table(name = "users", indexes = {@Index(columnList = "email")}) // 为高频查询字段添加索引
@EntityListeners(AuditingEntityListener.class)
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, unique = true, length = 100)
    private String username;

    @Column(nullable = false, unique = true, length = 100)
    private String email;

    @Column(nullable = false)
    private String password;

    @ElementCollection(fetch = FetchType.EAGER)
    @CollectionTable(name = "user_roles", joinColumns = @JoinColumn(name = "user_id"))
    @Column(name = "role")
    private Set roles;

    @CreatedDate
    @Column(nullable = false, updatable = false)
    private LocalDateTime createdAt;

    @LastModifiedDate
    @Column(nullable = false)
    private LocalDateTime updatedAt;
}

在上面的代码中,我们使用了 INLINECODEea85231d 而不是 INLINECODE38e31f50,这是 Spring Boot 3.0 基于 Jakarta EE 9+ 的一个重要变化。这一点在旧项目升级时尤其容易踩坑,我们的 AI 助手通常也能帮我们自动识别并修正这类导入错误。

步骤 3:构建无状态的安全配置

在现代 Spring Security 6(包含在 Spring Boot 3 中)中,配置方式发生了巨大变化。我们不再继承 WebSecurityConfigurerAdapter(因为它已被弃用),而是使用基于组件的配置。让我们来实现这一部分。

我们在配置类中禁用了 CSRF(因为我们使用的是无状态 JWT),并配置了 CORS 以允许前端应用(可能部署在不同的域名或端口下)进行通信。这是前后端分离架构中的标准做法。

package com.example.jwtsecurity.config;

import com.example.jwtsecurity.security.JwtAuthenticationEntryPoint;
import com.example.jwtsecurity.security.JwtAuthenticationFilter;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
@EnableMethodSecurity // 启用基于注解的方法级安全(如 @PreAuthorize)
@RequiredArgsConstructor
public class SecurityConfig {

    private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
    private final JwtAuthenticationFilter jwtAuthenticationFilter;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            // 禁用 CSRF,因为使用的是无状态 JWT
            .csrf(csrf -> csrf.disable())
            // 使用自定义的异常处理入口
            .exceptionHandling(exception -> exception
                .authenticationEntryPoint(jwtAuthenticationEntryPoint)
            )
            // 设置会话管理为无状态
            .sessionManagement(session -> session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            )
            // 配置授权规则
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/auth/**").permitAll() // 公开登录注册接口
                .requestMatchers("/api/admin/**").hasRole("ADMIN") // 仅管理员访问
                .anyRequest().authenticated() // 其他接口需认证
            );

        // 在 UsernamePasswordAuthenticationFilter 之前添加我们的 JWT 过滤器
        http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(); // 使用业界标准的 BCrypt 加密
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
        return configuration.getAuthenticationManager();
    }
}

在这个配置中,我们引入了 INLINECODEc4aec8e9 和 INLINECODE8fe35cad。这两个组件是我们 JWT 逻辑的核心。前者负责从请求头中提取 Token 并验证,后者负责在用户未认证时返回 401 错误,而不是默认的跳转登录页面行为。

步骤 4:JWT 工具类与令牌生成

让我们深入探讨 JWT 的生成与解析。在 2026 年,虽然可能会有更高级的加密标准出现,但 HMAC SHA-512 依然是性能与安全性平衡的最佳选择之一。

package com.example.jwtsecurity.security;

import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;

import java.security.Key;
import java.util.Date;

@Component
public class JwtTokenProvider {

    @Value("${app.jwt-secret}")
    private String jwtSecret;

    @Value("${app.jwt-expiration-milliseconds:86400000}") // 默认 24 小时
    private long jwtExpirationDate;

    // 生成密钥 Key
    private Key getSigningKey() {
        // 密钥必须足够长以满足加密算法要求(HMAC SHA-512 需要 64 字节以上)
        return Keys.hmacShaKeyFor(jwtSecret.getBytes());
    }

    // 生成 JWT Token
    public String generateToken(Authentication authentication) {
        String username = authentication.getName();
        Date currentDate = new Date();
        Date expireDate = new Date(currentDate.getTime() + jwtExpirationDate);

        return Jwts.builder()
                .setSubject(username)
                .setIssuedAt(new Date())
                .setExpiration(expireDate)
                .signWith(getSigningKey(), SignatureAlgorithm.HS512)
                .compact();
    }

    // 从 Token 获取用户名
    public String getUsername(String token) {
        Claims claims = Jwts.parserBuilder()
                .setSigningKey(getSigningKey())
                .build()
                .parseClaimsJws(token)
                .getBody();

        return claims.getSubject();
    }

    // 验证 Token
    public boolean validateToken(String token) {
        try {
            Jwts.parserBuilder()
                .setSigningKey(getSigningKey())
                .build()
                .parseClaimsJws(token);
            return true;
        } catch (SecurityException ex) {
            System.err.println("Invalid JWT signature");
        } catch (MalformedJwtException ex) {
            System.err.println("Invalid JWT token");
        } catch (ExpiredJwtException ex) {
            System.err.println("Expired JWT token");
        } catch (UnsupportedJwtException ex) {
            System.err.println("Unsupported JWT token");
        } catch (IllegalArgumentException ex) {
            System.err.println("JWT claims string is empty");
        }
        return false;
    }
}

在生产环境中,我们将密钥 jwt-secret 存储在环境变量或密钥管理服务(如 AWS Secrets Manager 或 HashiCorp Vault)中,而不是硬编码在代码里。这符合 安全左移 的理念。

步骤 5:JWT 过滤器与无状态认证

这是整个流程的守门员。每次用户请求受保护的资源时,这个过滤器都会先执行。它不会处理业务逻辑,只负责验证 Token。

package com.example.jwtsecurity.security;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final JwtTokenProvider jwtTokenProvider;
    private final CustomUserDetailsService customUserDetailsService;

    public JwtAuthenticationFilter(JwtTokenProvider jwtTokenProvider, CustomUserDetailsService customUserDetailsService) {
        this.jwtTokenProvider = jwtTokenProvider;
        this.customUserDetailsService = customUserDetailsService;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, 
                                    HttpServletResponse response, 
                                    FilterChain filterChain) throws ServletException, IOException {
        try {
            String jwt = getJwtFromRequest(request);

            if (StringUtils.hasText(jwt) && jwtTokenProvider.validateToken(jwt)) {
                String username = jwtTokenProvider.getUsername(jwt);

                UserDetails userDetails = customUserDetailsService.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);
    }

    private String getJwtFromRequest(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }
        return null;
    }
}

生产环境最佳实践与 2026 展望

在我们完成核心代码后,让我们思考一下如何将其部署到生产环境,并展望未来的技术趋势。

  • 可观测性:仅仅运行代码是不够的。我们需要知道认证失败率、Token 刷新频率等指标。我们可以使用 Spring Boot Actuator 配合 Prometheus 和 Grafana 来监控 /actuator/metrics/jwt。如果你使用的是 Agentic AI 辅助运维,这些指标可以触发自动修复机制。
  • 刷新令牌:上面的示例使用了固定有效期的 Token。在安全要求极高的系统中,我们应该实现 Refresh Token 机制。即 Access Token 有效期较短(如 15 分钟),而 Refresh Token 有效期较长(如 7 天)。这样即使 Access Token 泄露,由于时效短,风险也相对可控。
  • 技术债务管理:在长期维护中,我们发现自定义的 JWT 实现往往会引入安全漏洞。在 2026 年,如果团队预算允许,建议逐渐迁移到专用的身份认证服务(如 Keycloak 或 Auth0),让专业的人做专业的事,我们的应用只需作为 OAuth 2.0 的客户端即可。
  • 调试与故障排查:在开发过程中,你可能会遇到 403 Forbidden 错误。不要慌张,使用 AI 辅助 IDE 的调试功能,或者简单地在 INLINECODE427edefb 中添加断点,观察 INLINECODE9ada0c00 中是否真的包含了你的认证信息。通常,这是由于 Role 前缀(例如数据库中存储的是 INLINECODE06d53c7d 但 Spring Security 自动添加了 INLINECODE0fca1046 前缀)不匹配导致的。

通过以上步骤,我们构建了一个符合现代标准的安全认证系统。这不仅是代码的实现,更是对现代开发理念的一次实践。

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。如需转载,请注明文章出处豆丁博客和来源网址。https://shluqu.cn/47867.html
点赞
0.00 平均评分 (0% 分数) - 0