Spring Boot JWT Token 认证

  1. JWT 简介

JWT(JSON Web Token)是一种开放标准(RFC 7519),用于在网络应用间传递声明。它由三部分组成:

· Header(头部):通常包含令牌类型(JWT)和签名算法(如 HMAC SHA256)。

· Payload(载荷):包含实体(通常是用户)的声明,例如用户 ID、角色、过期时间等。

· Signature(签名):用于验证消息在传输过程中未被篡改。

JWT 的优势在于无状态、跨语言、可扩展,非常适合微服务架构中的认证。


  1. 技术栈

· Spring Boot 2.x / 3.x

· Spring Security

· java-jwt(或 jjwt、auth0/java-jwt)

· Lombok(可选,简化代码)


  1. 项目初始化

创建 Spring Boot 项目,添加以下依赖(以 Maven 为例):

xml 复制代码
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
        <scope>runtime</scope>
    </dependency>
    <!-- JWT 支持,这里使用 jjwt -->
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-api</artifactId>
        <version>0.11.5</version>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-impl</artifactId>
        <version>0.11.5</version>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-jackson</artifactId>
        <version>0.11.5</version>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>

  1. 创建用户实体和 Repository

假设我们有一个简单的用户表。

java 复制代码
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String username;
    private String password;
    private String role; // 例如 "ROLE_USER", "ROLE_ADMIN"
}

对应的 Repository:

java 复制代码
public interface UserRepository extends JpaRepository<User, Long> {
    Optional<User> findByUsername(String username);
}

为了方便测试,可以在启动时插入一个用户。

java 复制代码
@Component
public class DataLoader implements CommandLineRunner {
    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;

    public DataLoader(UserRepository userRepository, PasswordEncoder passwordEncoder) {
        this.userRepository = userRepository;
        this.passwordEncoder = passwordEncoder;
    }

    @Override
    public void run(String... args) throws Exception {
        if (userRepository.count() == 0) {
            User user = new User();
            user.setUsername("admin");
            user.setPassword(passwordEncoder.encode("123456"));
            user.setRole("ROLE_ADMIN");
            userRepository.save(user);
        }
    }
}

  1. JWT 工具类

我们需要一个工具类来生成和解析 JWT。这里使用 jjwt。

java 复制代码
@Component
public class JwtUtils {
    @Value("${app.jwt.secret}")
    private String jwtSecret;

    @Value("${app.jwt.expiration-ms}")
    private int jwtExpirationMs;

    // 生成 JWT token
    public String generateJwtToken(Authentication authentication) {
        UserDetailsImpl userPrincipal = (UserDetailsImpl) authentication.getPrincipal();

        return Jwts.builder()
                .setSubject((userPrincipal.getUsername()))
                .setIssuedAt(new Date())
                .setExpiration(new Date((new Date()).getTime() + jwtExpirationMs))
                .signWith(key(), SignatureAlgorithm.HS256)
                .compact();
    }

    // 从 JWT 中提取用户名
    public String getUserNameFromJwtToken(String token) {
        return Jwts.parserBuilder().setSigningKey(key()).build()
                .parseClaimsJws(token).getBody().getSubject();
    }

    // 签名密钥
    private Key key() {
        return Keys.hmacShaKeyFor(jwtSecret.getBytes(StandardCharsets.UTF_8));
    }

    // 验证 JWT
    public boolean validateJwtToken(String authToken) {
        try {
            Jwts.parserBuilder().setSigningKey(key()).build().parse(authToken);
            return true;
        } catch (MalformedJwtException e) {
            // log
        } catch (ExpiredJwtException e) {
            // log
        } catch (UnsupportedJwtException e) {
            // log
        } catch (IllegalArgumentException e) {
            // log
        }
        return false;
    }
}

在 application.yml 中配置:

yaml 复制代码
app:
  jwt:
    secret: mySecretKeyForJWTGenerationWithAtLeast32CharactersLong!
    expiration-ms: 86400000  # 24小时

  1. Spring Security 配置

6.1 UserDetails 和 UserDetailsService

实现 Spring Security 的 UserDetailsService 从数据库加载用户。

java 复制代码
@Data
public class UserDetailsImpl implements UserDetails {
    private Long id;
    private String username;
    private String password;
    private Collection<? extends GrantedAuthority> authorities;

    public static UserDetailsImpl build(User user) {
        List<GrantedAuthority> authorities = Collections.singletonList(new SimpleGrantedAuthority(user.getRole()));
        return new UserDetailsImpl(user.getId(), user.getUsername(), user.getPassword(), authorities);
    }

    // 构造函数、getters、重写的方法...
}

Service:

java 复制代码
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    @Autowired
    private UserRepository userRepository;

    @Override
    @Transactional
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username)
                .orElseThrow(() -> new UsernameNotFoundException("User Not Found with username: " + username));
        return UserDetailsImpl.build(user);
    }
}

6.2 认证过滤器

创建一个过滤器,用于拦截请求并验证 JWT。

java 复制代码
@Component
public class AuthTokenFilter extends OncePerRequestFilter {
    @Autowired
    private JwtUtils jwtUtils;
    @Autowired
    private UserDetailsServiceImpl userDetailsService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        try {
            String jwt = parseJwt(request);
            if (jwt != null && jwtUtils.validateJwtToken(jwt)) {
                String username = jwtUtils.getUserNameFromJwtToken(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 e) {
            // log
        }
        filterChain.doFilter(request, response);
    }

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

6.3 认证入口点(处理未授权访问)

当用户尝试访问受保护资源但未提供有效凭证时,返回 401。

java 复制代码
@Component
public class AuthEntryPointJwt implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response,
                         AuthenticationException authException) throws IOException, ServletException {
        response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Error: Unauthorized");
    }
}

6.4 安全配置类

配置哪些路径需要认证,哪些可以公开,并添加过滤器。

java 复制代码
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig {
    @Autowired
    private UserDetailsServiceImpl userDetailsService;
    @Autowired
    private AuthEntryPointJwt unauthorizedHandler;
    @Autowired
    private AuthTokenFilter authTokenFilter;

    @Bean
    public DaoAuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
        authProvider.setUserDetailsService(userDetailsService);
        authProvider.setPasswordEncoder(passwordEncoder());
        return authProvider;
    }

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

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.cors().and().csrf().disable()
                .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                .authorizeRequests().antMatchers("/api/auth/**").permitAll()
                .antMatchers("/api/test/**").permitAll()
                .anyRequest().authenticated();

        http.authenticationProvider(authenticationProvider());
        http.addFilterBefore(authTokenFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }
}

注意:Spring Security 5.7 之后推荐使用 SecurityFilterChain 替代 WebSecurityConfigurerAdapter。


  1. 认证控制器(登录)

创建控制器,接收用户名密码,验证成功后返回 JWT。

java 复制代码
@RestController
@RequestMapping("/api/auth")
public class AuthController {
    @Autowired
    AuthenticationManager authenticationManager;
    @Autowired
    JwtUtils jwtUtils;
    @Autowired
    UserRepository userRepository;

    @PostMapping("/signin")
    public ResponseEntity<?> authenticateUser(@RequestBody LoginRequest loginRequest) {
        Authentication authentication = authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword()));

        SecurityContextHolder.getContext().setAuthentication(authentication);
        String jwt = jwtUtils.generateJwtToken(authentication);

        UserDetailsImpl userDetails = (UserDetailsImpl) authentication.getPrincipal();
        List<String> roles = userDetails.getAuthorities().stream()
                .map(item -> item.getAuthority())
                .collect(Collectors.toList());

        return ResponseEntity.ok(new JwtResponse(jwt, userDetails.getId(), userDetails.getUsername(), roles));
    }
}

其中 LoginRequest 和 JwtResponse 是简单的 DTO。


  1. 测试受保护接口

创建一个测试控制器,验证 Token 是否生效。

java 复制代码
@RestController
@RequestMapping("/api/test")
public class TestController {
    @GetMapping("/user")
    @PreAuthorize("hasRole('USER') or hasRole('ADMIN')")
    public String userAccess() {
        return "User Content.";
    }

    @GetMapping("/admin")
    @PreAuthorize("hasRole('ADMIN')")
    public String adminAccess() {
        return "Admin Board.";
    }
}

注意:方法级权限需要 @EnableGlobalMethodSecurity(prePostEnabled = true)。


  1. 测试流程

  2. 启动应用。

  3. 发送 POST 请求 /api/auth/signin,携带 JSON:

    json 复制代码
    {
      "username": "admin",
      "password": "123456"
    }

    返回 JWT token。

  4. 使用该 token 访问 /api/test/admin,在请求头中加入 Authorization: Bearer ,应能成功访问。

  5. 如果不带 token 或 token 无效,返回 401。


  1. 补充:刷新 Token 实现

通常 JWT 的有效期较短,可以配合 Refresh Token 实现无感刷新。常见做法是登录时返回 access_token(短效)和 refresh_token(长效),并提供刷新端点。

这里简述思路:

· 生成 access_token 和 refresh_token,refresh_token 存储在数据库(或 Redis)中。

· 刷新端点接收 refresh_token,验证后生成新的 access_token。


  1. 注意事项

· 密钥安全:生产环境应将 JWT 密钥通过环境变量或配置中心管理,不要硬编码。

· 无状态:JWT 认证是无状态的,但需要留意注销功能(通常结合黑名单)。

· HTTPS:生产环境务必使用 HTTPS 防止中间人攻击。

· Token 存储:前端可将 token 存储在内存、localStorage 或 httpOnly Cookie 中,根据安全需求选择。

· 过期时间:access_token 不宜过长(如 15-30 分钟),refresh_token 可长一些(如 7 天)。


通过以上步骤,你可以在 Spring Boot 中实现一套完整的基于 JWT 的认证系统。可以根据实际业务调整用户角色、权限控制粒度以及令牌的有效期。

相关推荐
程序员清风2 小时前
2026年必学:Vibe Coding几个实用技巧,老手都在偷偷用!
java·后端·面试
夕除2 小时前
js--24
java
AC赳赳老秦2 小时前
多模态 AI 驱动办公智能化变革:DeepSeek 赋能图文转写与视频摘要的高效实践
java·ide·人工智能·python·prometheus·ai-native·deepseek
iambooo2 小时前
系统健康巡检脚本的设计思路与落地实践
java·大数据·linux
blockrock2 小时前
Tomcat
java·tomcat
wangbing11252 小时前
开发指南143-扩展类功能
java·开发语言
何中应2 小时前
从零搭建Maven私服(Nexus)
java·运维·maven
loserwang2 小时前
拆解 NIO 核心:脱离 Selector 视角,详解 Channel、Buffer 与 Netty 的进阶优化
java
zihan03213 小时前
若依(RuoYi)框架升级适配 JDK 21 和 SpringBoot 3.5.10
java·spring boot·spring·若依·若依升级jdk21