$npx -y skills add rrezartprebreza/spring-boot-skills --skill spring-security-jwtUse when implementing authentication, authorization, JWT tokens, security filters, password encoding, or any Spring Security configuration. Covers stateless JWT auth, token rotation, RBAC, and method-level security.
| 1 | # Spring Security — JWT |
| 2 | |
| 3 | ## Dependencies |
| 4 | |
| 5 | ```xml |
| 6 | <dependency> |
| 7 | <groupId>org.springframework.boot</groupId> |
| 8 | <artifactId>spring-boot-starter-security</artifactId> |
| 9 | </dependency> |
| 10 | <dependency> |
| 11 | <groupId>io.jsonwebtoken</groupId> |
| 12 | <artifactId>jjwt-api</artifactId> |
| 13 | <version>0.12.6</version> |
| 14 | </dependency> |
| 15 | <dependency> |
| 16 | <groupId>io.jsonwebtoken</groupId> |
| 17 | <artifactId>jjwt-impl</artifactId> |
| 18 | <version>0.12.6</version> |
| 19 | <scope>runtime</scope> |
| 20 | </dependency> |
| 21 | <dependency> |
| 22 | <groupId>io.jsonwebtoken</groupId> |
| 23 | <artifactId>jjwt-jackson</artifactId> |
| 24 | <version>0.12.6</version> |
| 25 | <scope>runtime</scope> |
| 26 | </dependency> |
| 27 | ``` |
| 28 | |
| 29 | ## Security Configuration |
| 30 | |
| 31 | ```java |
| 32 | @Configuration |
| 33 | @EnableWebSecurity |
| 34 | @EnableMethodSecurity |
| 35 | @RequiredArgsConstructor |
| 36 | public class SecurityConfig { |
| 37 | |
| 38 | private final JwtAuthenticationFilter jwtAuthFilter; |
| 39 | private final UserDetailsService userDetailsService; |
| 40 | |
| 41 | @Bean |
| 42 | public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { |
| 43 | return http |
| 44 | .csrf(AbstractHttpConfigurer::disable) |
| 45 | .sessionManagement(s -> s.sessionCreationPolicy(STATELESS)) |
| 46 | .authorizeHttpRequests(auth -> auth |
| 47 | .requestMatchers("/api/v1/auth/**").permitAll() |
| 48 | .requestMatchers("/actuator/health").permitAll() |
| 49 | .requestMatchers("/api/v1/admin/**").hasRole("ADMIN") |
| 50 | .anyRequest().authenticated() |
| 51 | ) |
| 52 | .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class) |
| 53 | .build(); |
| 54 | } |
| 55 | |
| 56 | @Bean |
| 57 | public AuthenticationProvider authenticationProvider() { |
| 58 | var provider = new DaoAuthenticationProvider(); |
| 59 | provider.setUserDetailsService(userDetailsService); |
| 60 | provider.setPasswordEncoder(passwordEncoder()); |
| 61 | return provider; |
| 62 | } |
| 63 | |
| 64 | @Bean |
| 65 | public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { |
| 66 | return config.getAuthenticationManager(); |
| 67 | } |
| 68 | |
| 69 | @Bean |
| 70 | public PasswordEncoder passwordEncoder() { |
| 71 | return new BCryptPasswordEncoder(12); |
| 72 | } |
| 73 | } |
| 74 | ``` |
| 75 | |
| 76 | ## JWT Service |
| 77 | |
| 78 | ```java |
| 79 | @Service |
| 80 | public class JwtService { |
| 81 | |
| 82 | @Value("${app.jwt.secret}") |
| 83 | private String secretKey; |
| 84 | |
| 85 | @Value("${app.jwt.access-token-expiry:900000}") // 15 min default |
| 86 | private long accessTokenExpiry; |
| 87 | |
| 88 | @Value("${app.jwt.refresh-token-expiry:604800000}") // 7 days default |
| 89 | private long refreshTokenExpiry; |
| 90 | |
| 91 | public String generateAccessToken(UserDetails user) { |
| 92 | return generateToken(Map.of("type", "access"), user, accessTokenExpiry); |
| 93 | } |
| 94 | |
| 95 | public String generateRefreshToken(UserDetails user) { |
| 96 | return generateToken(Map.of("type", "refresh"), user, refreshTokenExpiry); |
| 97 | } |
| 98 | |
| 99 | private String generateToken(Map<String, Object> claims, UserDetails user, long expiry) { |
| 100 | return Jwts.builder() |
| 101 | .claims(claims) |
| 102 | .subject(user.getUsername()) |
| 103 | .issuedAt(new Date()) |
| 104 | .expiration(new Date(System.currentTimeMillis() + expiry)) |
| 105 | .signWith(getSigningKey()) |
| 106 | .compact(); |
| 107 | } |
| 108 | |
| 109 | public boolean isTokenValid(String token, UserDetails user) { |
| 110 | return extractUsername(token).equals(user.getUsername()) && !isExpired(token); |
| 111 | } |
| 112 | |
| 113 | public String extractUsername(String token) { |
| 114 | return extractClaim(token, Claims::getSubject); |
| 115 | } |
| 116 | |
| 117 | private boolean isExpired(String token) { |
| 118 | return extractClaim(token, Claims::getExpiration).before(new Date()); |
| 119 | } |
| 120 | |
| 121 | private <T> T extractClaim(String token, Function<Claims, T> resolver) { |
| 122 | return resolver.apply(Jwts.parser().verifyWith(getSigningKey()).build().parseSignedClaims(token).getPayload()); |
| 123 | } |
| 124 | |
| 125 | private SecretKey getSigningKey() { |
| 126 | return Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretKey)); |
| 127 | } |
| 128 | } |
| 129 | ``` |
| 130 | |
| 131 | ## JWT Filter |
| 132 | |
| 133 | ```java |
| 134 | @Component |
| 135 | @RequiredArgsConstructor |
| 136 | public class JwtAuthenticationFilter extends OncePerRequestFilter { |
| 137 | |
| 138 | private final JwtService jwtService; |
| 139 | private final UserDetailsService userDetailsService; |
| 140 | |
| 141 | @Override |
| 142 | protected void doFilterInternal(HttpServletRequest request, |
| 143 | HttpServletResponse response, |
| 144 | FilterChain chain) throws ServletException, IOException { |
| 145 | String authHeader = request.getHeader("Authorization"); |
| 146 | if (authHeader == null || !authHeader.startsWith("Bearer ")) { |
| 147 | chain.doFilter(request, response); |
| 148 | return; |
| 149 | } |
| 150 | |
| 151 | String token = authHeader.substring(7); |
| 152 | try { |
| 153 | String username = jwtService.extractUsername(token); |
| 154 | |
| 155 | if (username != null && SecurityContex |