Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Spring Security best practices for authn/authz, validation, CSRF, secrets, headers, rate limiting, and dependency security in Java Spring Boot services.
.claude/skills/loulanyue-springboot-security/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-14 | ✗→✓ | ▲ Improved | 67% | 0% |
| case-22 | ✓→✗ | ▼ Worse | 45% | 0% |
| case-01 | ✓→✓ | = Same ✓ | 83% | 0% |
| case-02 | ✓→✓ | = Same ✓ | 72% | 0% |
| case-03 | ✓→✓ | = Same ✓ | 209% | 0% |
Use when adding auth, handling input, creating endpoints, or dealing with secrets.
httpOnly, Secure, SameSite=Strict cookies for sessionsOncePerRequestFilter or resource serverjava@Component public class JwtAuthFilter extends OncePerRequestFilter { private final JwtService jwtService; public JwtAuthFilter(JwtService jwtService) { this.jwtService = jwtService; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { String header = request.getHeader(HttpHeaders.AUTHORIZATION); if (header != null && header.startsWith("Bearer ")) { String token = header.substring(7); Authentication auth = jwtService.authenticate(token); SecurityContextHolder.getContext().setAuthentication(auth); } chain.doFilter(request, response); } }
@EnableMethodSecurity@PreAuthorize("hasRole('ADMIN')") or @PreAuthorize("@authz.canEdit(#id)")java@RestController @RequestMapping("/api/admin") public class AdminController { @PreAuthorize("hasRole('ADMIN')") @GetMapping("/users") public List<UserDto> listUsers() { return userService.findAll(); } @PreAuthorize("@authz.isOwner(#id, authentication)") @DeleteMapping("/users/{id}") public ResponseEntity<Void> deleteUser(@PathVariable Long id) { userService.delete(id); return ResponseEntity.noContent().build(); } }
@Valid on controllers@NotBlank, @Email, @Size, custom validatorsjava// BAD: No validation @PostMapping("/users") public User createUser(@RequestBody UserDto dto) { return userService.create(dto); } // GOOD: Validated DTO public record CreateUserDto( @NotBlank @Size(max = 100) String name, @NotBlank @Email String email, @NotNull @Min(0) @Max(150) Integer age ) {} @PostMapping("/users") public ResponseEntity<UserDto> createUser(@Valid @RequestBody CreateUserDto dto) { return ResponseEntity.status(HttpStatus.CREATED) .body(userService.create(dto)); }
:param bindings; never concatenate stringsjava// BAD: String concatenation in native query @Query(value = "SELECT * FROM users WHERE name = '" + name + "'", nativeQuery = true) // GOOD: Parameterized native query @Query(value = "SELECT * FROM users WHERE name = :name", nativeQuery = true) List<User> findByName(@Param("name") String name); // GOOD: Spring Data derived query (auto-parameterized) List<User> findByEmailAndActiveTrue(String email);
PasswordEncoder bean, not manual hashingjava@Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(12); // cost factor 12 } // In service public User register(CreateUserDto dto) { String hashedPassword = passwordEncoder.encode(dto.password()); return userRepository.save(new User(dto.email(), hashedPassword)); }
javahttp .csrf(csrf -> csrf.disable()) .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
application.yml free of credentials; use placeholdersyaml# BAD: Hardcoded in application.yml spring: datasource: password: mySecretPassword123 # GOOD: Environment variable placeholder spring: datasource: password: ${DB_PASSWORD} # GOOD: Spring Cloud Vault integration spring: cloud: vault: uri: https://vault.example.com token: ${VAULT_TOKEN}
javahttp .headers(headers -> headers .contentSecurityPolicy(csp -> csp .policyDirectives("default-src 'self'")) .frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin) .xssProtection(Customizer.withDefaults()) .referrerPolicy(rp -> rp.policy(ReferrerPolicyHeaderWriter.ReferrerPolicy.NO_REFERRER)));
* in productionjava@Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration config = new CorsConfiguration(); config.setAllowedOrigins(List.of("https://app.example.com")); config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE")); config.setAllowedHeaders(List.of("Authorization", "Content-Type")); config.setAllowCredentials(true); config.setMaxAge(3600L); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/api/**", config); return source; } // In SecurityFilterChain: http.cors(cors -> cors.configurationSource(corsConfigurationSource()));
java// Using Bucket4j for per-endpoint rate limiting @Component public class RateLimitFilter extends OncePerRequestFilter { private final Map<String, Bucket> buckets = new ConcurrentHashMap<>(); private Bucket createBucket() { return Bucket.builder() .addLimit(Bandwidth.classic(100, Refill.intervally(100, Duration.ofMinutes(1)))) .build(); } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { String clientIp = request.getRemoteAddr(); Bucket bucket = buckets.computeIfAbsent(clientIp, k -> createBucket()); if (bucket.tryConsume(1)) { chain.doFilter(request, response); } else { response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value()); response.getWriter().write("{\"error\": \"Rate limit exceeded\"}"); } } }
Remember: Deny by default, validate inputs, least privilege, and secure-by-configuration first.
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | pass→pass | 12,185 | 8,981 | -26% | 1 | 1 | 0% | 1,991 | 3,647 | +83% | 0 | 0 | — |
case-02 | pass→pass | 9,409 | 5,092 | -46% | 1 | 1 | 0% | 1,670 | 2,872 | +72% | 0 | 0 | — |
case-03 | pass→pass | 5,297 | 5,130 | -3% | 1 | 1 | 0% | 944 | 2,919 | +209% | 0 | 0 | — |
case-04 | pass→pass | 8,423 | 4,100 | -51% | 1 | 1 | 0% | 1,590 | 2,743 | +73% | 0 | 0 | — |
case-05 | pass→pass | 10,040 | 6,901 | -31% | 1 | 1 | 0% | 1,814 | 3,286 | +81% | 0 | 0 | — |
case-06 | pass→pass | 3,753 | 3,208 | -15% | 1 | 1 | 0% | 674 | 2,547 | +278% | 0 | 0 | — |
case-07 | pass→pass | 10,672 | 6,761 | -37% | 1 | 1 | 0% | 1,810 | 3,219 | +78% | 0 | 0 | — |
case-08 | pass→pass | 9,675 | 5,367 | -45% | 1 | 1 | 0% | 1,671 | 2,897 | +73% | 0 | 0 | — |
case-09 | pass→pass | 11,413 | 5,159 | -55% | 1 | 1 | 0% | 2,061 | 2,986 | +45% | 0 | 0 | — |
case-10 | pass→pass | 10,993 | 6,425 | -42% | 1 | 1 | 0% | 1,975 | 3,198 | +62% | 0 | 0 | — |
case-11 | pass→pass | 10,531 | 6,669 | -37% | 1 | 1 | 0% | 1,867 | 3,102 | +66% | 0 | 0 | — |
case-12 | pass→pass | 8,547 | 3,686 | -57% | 1 | 1 | 0% | 1,557 | 2,716 | +74% | 0 | 0 | — |
case-13 | pass→pass | 7,321 | 5,768 | -21% | 1 | 1 | 0% | 1,362 | 3,018 | +122% | 0 | 0 | — |
case-14 | fail→pass | 10,805 | 7,955 | -26% | 1 | 1 | 0% | 2,074 | 3,465 | +67% | 0 | 0 | — |
case-15 | pass→pass | 12,732 | 7,172 | -44% | 1 | 1 | 0% | 2,468 | 3,328 | +35% | 0 | 0 | — |
case-16 | pass→pass | 12,477 | 8,402 | -33% | 1 | 1 | 0% | 2,346 | 3,646 | +55% | 0 | 0 | — |
case-17 | pass→pass | 8,138 | 3,043 | -63% | 1 | 1 | 0% | 1,506 | 2,500 | +66% | 0 | 0 | — |
case-18 | pass→pass | 12,260 | 7,216 | -41% | 1 | 1 | 0% | 2,156 | 3,194 | +48% | 0 | 0 | — |
case-19 | pass→pass | 17,360 | 12,406 | -29% | 1 | 1 | 0% | 2,864 | 4,150 | +45% | 0 | 0 | — |
case-20 | pass→pass | 7,819 | 7,649 | -2% | 1 | 1 | 0% | 1,477 | 3,386 | +129% | 0 | 0 | — |
case-21 | pass→pass | 8,703 | 7,908 | -9% | 1 | 1 | 0% | 1,661 | 3,399 | +105% | 0 | 0 | — |
case-22 | pass→fail | 16,381 | 12,834 | -22% | 1 | 1 | 0% | 3,189 | 4,637 | +45% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of 0 percentage points is the difference between those two pass rates over the 22 comparable cases. 1 case got worse with the skill loaded, and it is included in that figure.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.