API Secure Design Practices
API Secure Design Practices
🔧 Implementation Steps:
1. Set up a Keycloak server and configure a Realm, Client, Roles, and Users.
2. Add Keycloak Spring Boot dependencies.
3. Configure Keycloak properties in application.yml.
4. Protect endpoints using Spring Security + OAuth2 resource server.
5. Decode and validate JWTs.
📜 Dependencies (Maven):
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-
server</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-spring-boot-starter</artifactId>
</dependency>
⚙️ application.yml
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: http://localhost:8080/realms/myrealm
🔐 SecurityConfig.java
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http)
throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**").permitAll()
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2.jwt());
return http.build();
}
}
🔧 Implementation Steps:
<dependency>
<groupId>com.github.vladimir-bukhtoyarov</groupId>
<artifactId>bucket4j-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
📦 RateLimiterFilter.java
@Component
public class RateLimiterFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String ip = request.getRemoteAddr();
Bucket bucket = redisTemplate.opsForValue().get(ip);
if (bucket == null) {
Refill refill = Refill.greedy(60, Duration.ofMinutes(1));
Bandwidth limit = Bandwidth.classic(60, refill);
bucket = Bucket.builder().addLimit(limit).build();
redisTemplate.opsForValue().set(ip, bucket);
}
if (bucket.tryConsume(1)) {
filterChain.doFilter(request, response);
} else {
response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
response.getWriter().write("Too many requests");
}
}
}
🔧 Implementation Steps:
🧱 Entity: ApiKey.java
@Entity
public class ApiKey {
@Id
private String key;
private String owner;
private boolean active;
}
🧩 ApiKeyInterceptor.java
@Component
public class ApiKeyInterceptor implements HandlerInterceptor {
@Autowired private ApiKeyRepository apiKeyRepository;
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) throws IOException {
String apiKey = request.getHeader("X-API-KEY");
if (apiKey == null
|| !apiKeyRepository.existsByIdAndActiveTrue(apiKey)) {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.getWriter().write("Invalid API Key");
return false;
}
return true;
}
}
🧬 WebConfig.java
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired ApiKeyInterceptor apiKeyInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(apiKeyInterceptor);
}
}
🔧 Implementation Steps:
🧠 AuditAspect.java
@Aspect
@Component
public class AuditAspect {
private static final Logger logger =
LoggerFactory.getLogger("AUDIT");
@Before("execution(* com.example..*.*(..))")
public void logBefore(JoinPoint joinPoint) {
HttpServletRequest request = ((ServletRequestAttributes)
RequestContextHolder.currentRequestAttributes()).getRequest();
logger.info("API called: {} by IP: {}",
joinPoint.getSignature(), request.getRemoteAddr());
}
}
🔧 Implementation Steps:
🔐 application.yml
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: http://localhost:8080/realms/myrealm
🔍 Example Controller
@RestController
@RequestMapping("/employee")
public class EmployeeController {
@PreAuthorize("hasAuthority('SCOPE_read')")
@GetMapping("/me")
public ResponseEntity<?> getMyProfile() {
return ResponseEntity.ok("Your profile");
}
@PreAuthorize("hasAuthority('SCOPE_admin')")
@GetMapping("/all")
public ResponseEntity<?> getAllProfiles() {
return ResponseEntity.ok("All profiles");
}
}