Secure Coding Standards for Spring Boot APIs
Use this page as an engineering standard during implementation and code review. Each section shows a common insecure pattern and a secure replacement you can adopt immediately.
1. Authentication and Authorization
Rule:
- Every protected endpoint must enforce both authentication and authorization.
- Authorization must include business and resource constraints, not only route-level roles.
Bad pattern:
@GetMapping("/api/orders/{id}")
public Order getOrder(@PathVariable String id) {
// No authz check; any authenticated caller can reach this path.
return repository.findById(id).orElseThrow();
}
Good pattern:
@GetMapping("/api/orders/{id}")
@PreAuthorize("hasAuthority('SCOPE_orders.read')")
public Order getOrder(@PathVariable String id, JwtAuthenticationToken auth) {
String tenantId = auth.getToken().getClaimAsString("tenant_id");
return repository.findByIdAndTenantId(id, tenantId)
.orElseThrow(() -> new AccessDeniedException("Order not accessible"));
}
2. Input Validation and DTO Safety
Rule:
- Validate all external input at API boundaries.
- Never bind request payloads directly to persistence entities.
Bad pattern:
@PostMapping("/api/users")
public User create(@RequestBody User user) {
return userRepository.save(user);
}
Good pattern:
public record CreateUserRequest(
@NotBlank String email,
@NotBlank String displayName
) {}
@PostMapping("/api/users")
@PreAuthorize("hasAuthority('SCOPE_users.write')")
public UserDto create(@Valid @RequestBody CreateUserRequest req) {
User user = new User();
user.setEmail(req.email());
user.setDisplayName(req.displayName());
return mapper.toDto(userRepository.save(user));
}
3. Data Access and Tenant Isolation
Rule:
- Sensitive reads and writes must be tenant-aware.
- Do not load broad datasets and filter in memory.
Bad pattern:
Order order = repository.findById(id).orElseThrow();
if (!order.getTenantId().equals(tenantId)) {
throw new AccessDeniedException("Denied");
}
Good pattern:
Order order = repository.findByIdAndTenantId(id, tenantId)
.orElseThrow(() -> new AccessDeniedException("Not found or not allowed"));
Repository standard:
public interface OrderRepository extends JpaRepository<Order, String> {
Optional<Order> findByIdAndTenantId(String id, String tenantId);
}
4. Secrets and Configuration Hygiene
Rule:
- No secrets in source code, YAML, or workflow files.
- Use environment injection and Secret Manager.
Bad pattern:
Good pattern:
Operational standard:
- Store secrets in GCP Secret Manager.
- Grant access with least privilege service identity.
- Rotate on schedule and on incident.
5. Logging and Error Handling
Rule:
- Logs should support incident response without leaking sensitive values.
- Error responses should be safe and consistent.
Bad pattern:
Good pattern:
log.warn("Authorization failed. traceId={} path={}", traceId, request.getRequestURI());
return ResponseEntity.status(HttpStatus.FORBIDDEN)
.body(Map.of("error", "forbidden", "traceId", traceId));
6. Serialization and Data Exposure
Rule:
- Never return persistence entities directly to API clients.
- Use response DTOs with explicit allow-list fields.
Bad pattern:
@GetMapping("/api/accounts/{id}")
public Account get(@PathVariable String id) {
return accountRepository.findById(id).orElseThrow();
}
Good pattern:
public record AccountResponse(String id, String displayName, String tier) {}
@GetMapping("/api/accounts/{id}")
public AccountResponse get(@PathVariable String id) {
Account a = accountRepository.findById(id).orElseThrow();
return new AccountResponse(a.getId(), a.getDisplayName(), a.getTier());
}
7. HTTP Security Defaults
Rule:
- Explicitly configure CORS, CSRF policy, and security headers per API profile.
- Do not use broad CORS wildcard in production APIs.
Bad pattern:
Good pattern:
@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration c = new CorsConfiguration();
c.setAllowedOrigins(List.of("https://app.example.com"));
c.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
c.setAllowedHeaders(List.of("Authorization", "Content-Type"));
UrlBasedCorsConfigurationSource s = new UrlBasedCorsConfigurationSource();
s.registerCorsConfiguration("/**", c);
return s;
}
8. CI Security Gates for Every Change
Minimum gate before merge:
semgrep --config auto src/
trivy fs --severity HIGH,CRITICAL .
gitleaks detect --source . --no-banner
Score gate (already in this repo):
trivy fs --format json --output trivy.json .
semgrep --config auto --json --output semgrep.json .
bash scripts/security-score.sh trivy.json semgrep.json
cat security-score.json
9. Definition of Done for Secure Features
A feature is not done until all are true:
- AuthN and authZ controls are implemented.
- Negative tests cover unauthorized and cross-tenant paths.
- Input validation is enforced with DTO constraints.
- Secrets are externalized.
- CI security checks pass.
- Security-relevant logs include correlation identifiers.
10. Team Adoption Plan
- Add this checklist to PR templates.
- Start with high-risk APIs first (auth, billing, data export, admin).
- Convert repeated review comments into automated tests or static rules.
- Review exceptions weekly and attach expiry dates.
Related pages: