在我们构建现代企业级应用时,安全性始终是首要考量。虽然传统的 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 的普及而减少使用,但在遗留代码维护中依然有用)。
提示: 如果你正在使用 Cursor 或 Windsurf 这样的 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 前缀)不匹配导致的。
通过以上步骤,我们构建了一个符合现代标准的安全认证系统。这不仅是代码的实现,更是对现代开发理念的一次实践。