0Pricing
Spring Security 6 & JWT Authentication · 강의

API 접근 속도 제한

속도 제한을 사용하여 인증 엔드포인트와 API를 무차별 대입 공격 및 서비스 거부 공격으로부터 보호합니다.

API 접근 속도 제한은(는) CoddyKit의 무료 Spring Security 6 & JWT Authentication 강의입니다. 이것은 4개 중 2번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 Spring Security 6 & JWT Authentication 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. Spring Security 6 & JWT Authentication 강의에는 총 4개의 강의가 포함되어 있습니다.

이 강의의 일부는 아직 번역되지 않았으며 영어로 표시됩니다.

What is Rate Limiting?

Rate limiting is a crucial security measure that controls how often a user or system can repeat an action within a certain timeframe.

Think of it as a bouncer at a club, allowing only a certain number of people in per minute to prevent overcrowding.

It's vital for protecting your APIs and authentication endpoints from malicious activities.

Why APIs Need Rate Limiting

Without rate limiting, your application is vulnerable to several attacks:

  • Brute-Force Attacks: Repeated login attempts to guess passwords.
  • Denial-of-Service (DoS): Overwhelming your server with requests to make it unavailable.
  • Resource Exhaustion: Legitimate users might suffer if one user consumes too many resources.

Rate limiting helps maintain stability and security.

Rate Limiting Strategies

There are several ways to implement rate limiting:

  • Fixed Window: Allows N requests per window (e.g., 100 requests per minute). Simple, but can have a "burst" problem at window edges.
  • Sliding Window: A more sophisticated approach that smooths out traffic, preventing bursts.
  • Token Bucket/Leaky Bucket: More complex, often used for traffic shaping.

For our lesson, we'll focus on a simple fixed window approach.

Spring Boot Implementation

In Spring Boot, we can implement rate limiting using a custom Servlet Filter.

A filter intercepts incoming HTTP requests before they reach your controller methods. This allows us to check the request's rate limit and block it if necessary.

We'll create a dedicated service to manage the rate limiting logic.

Basic Rate Limiter Service

First, let's create a simple service to track request counts. This example uses an in-memory map for demonstration.

Try running this basic service setup:

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;

public class RateLimiterService {

    private final int MAX_REQUESTS;
    private final long TIME_WINDOW_MILLIS; // e.g., 1 minute = 60000ms

    // Stores request counts per client (e.g., IP address) within a window
    private final ConcurrentHashMap<String, RequestInfo> requestCounts = new ConcurrentHashMap<>();

    public RateLimiterService(int maxRequests, long timeWindowMillis) {
        this.MAX_REQUESTS = maxRequests;
        this.TIME_WINDOW_MILLIS = timeWindowMillis;
    }

    public boolean tryAcquire(String clientIdentifier) {
        long currentTime = System.currentTimeMillis();

        requestCounts.compute(clientIdentifier, (key, info) -> {
            if (info == null || currentTime - info.windowStartTime > TIME_WINDOW_MILLIS) {
                // New window or old window expired
                return new RequestInfo(currentTime, 1);
            } else {
                // Same window, increment count
                info.requestCount.incrementAndGet();
                return info;
            }
        });

        return requestCounts.get(clientIdentifier).requestCount.get() <= MAX_REQUESTS;
    }

    private static class RequestInfo {
        long windowStartTime;
        AtomicInteger requestCount;

        RequestInfo(long windowStartTime, int initialCount) {
            this.windowStartTime = windowStartTime;
            this.requestCount = new AtomicInteger(initialCount);
        }
    }

    public static void main(String[] args) {
        RateLimiterService limiter = new RateLimiterService(3, 10000); // 3 requests per 10 seconds

        System.out.println("Client A - Attempt 1: " + limiter.tryAcquire("clientA"));
        System.out.println("Client A - Attempt 2: " + limiter.tryAcquire("clientA"));
        System.out.println("Client A - Attempt 3: " + limiter.tryAcquire("clientA"));
        System.out.println("Client A - Attempt 4: " + limiter.tryAcquire("clientA")); // Should be false
    }
}

Understanding the Service

Our RateLimiterService uses a ConcurrentHashMap to store RequestInfo for each client identifier (like an IP address).

  • MAX_REQUESTS: The maximum number of requests allowed.
  • TIME_WINDOW_MILLIS: The duration of the fixed window.
  • tryAcquire(String clientIdentifier): This method checks if a client can make a request. It resets the window if expired or increments the count.

The main method demonstrates its usage.

Creating a Custom Filter

Now, let's create a custom OncePerRequestFilter. This filter will:

  • Intercept every incoming HTTP request.
  • Extract a client identifier (e.g., remote IP address).
  • Use our RateLimiterService to check if the request is allowed.
  • If not allowed, it will send an HTTP 429 (Too Many Requests) response.

This ensures the rate limit is enforced early in the request lifecycle.

Filter Code Example

Here's the full code for our custom RateLimitingFilter. Note how it injects the RateLimiterService and handles the response.

We'll manually add this filter to our Spring Security chain later.

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.HttpStatus;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;

public class RateLimitingFilter extends OncePerRequestFilter {

    private final RateLimiterService rateLimiterService;

    public RateLimitingFilter(RateLimiterService rateLimiterService) {
        this.rateLimiterService = rateLimiterService;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {

        String clientIp = request.getRemoteAddr(); // Simple IP-based identifier

        if (!rateLimiterService.tryAcquire(clientIp)) {
            response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
            response.getWriter().write("Too many requests from this IP. Please try again later.");
            return; // Stop the filter chain
        }

        filterChain.doFilter(request, response);
    }

    public static void main(String[] args) throws ServletException, IOException {
        // Simulate filter behavior for demo
        RateLimiterService limiter = new RateLimiterService(2, 5000); // 2 requests per 5 seconds
        RateLimitingFilter filter = new RateLimitingFilter(limiter);

        // Mock HttpServletRequest and HttpServletResponse for testing
        HttpServletRequest mockRequest = new MockHttpServletRequest("127.0.0.1");
        HttpServletResponse mockResponse = new MockHttpServletResponse();
        FilterChain mockChain = (req, res) -> System.out.println("Request processed by chain.");

        System.out.println("Attempt 1:");
        filter.doFilterInternal(mockRequest, mockResponse, mockChain);
        System.out.println("Response status: " + mockResponse.getStatus());

        System.out.println("Attempt 2:");
        mockResponse = new MockHttpServletResponse(); // Reset response for next attempt
        filter.doFilterInternal(mockRequest, mockResponse, mockChain);
        System.out.println("Response status: " + mockResponse.getStatus());

        System.out.println("Attempt 3 (should be blocked): ");
        mockResponse = new MockHttpServletResponse();
        filter.doFilterInternal(mockRequest, mockResponse, mockChain);
        System.out.println("Response status: " + mockResponse.getStatus());
    }

    // Simple mock classes for runnable example
    private static class MockHttpServletRequest implements HttpServletRequest {
        private String remoteAddr;
        public MockHttpServletRequest(String remoteAddr) { this.remoteAddr = remoteAddr; }
        @Override public String getRemoteAddr() { return remoteAddr; }
        @Override public String getMethod() { return "GET"; }
        @Override public String getRequestURI() { return "/test"; }
        @Override public String getHeader(String name) { return null; }
        @Override public java.util.Enumeration<String> getHeaderNames() { return null; }
        @Override public java.util.Enumeration<String> getHeaders(String name) { return null; }
        @Override public int getIntHeader(String name) { return -1; }
        @Override public String getContextPath() { return ""; }
        @Override public java.security.Principal getUserPrincipal() { return null; }
        @Override public boolean isUserInRole(String role) { return false; }
        @Override public String getAuthType() { return null; }
        @Override public String getRemoteUser() { return null; }
        @Override public String getPathInfo() { return null; }
        @Override public String getPathTranslated() { return null; }
        @Override public String getQueryString() { return null; }
        @Override public String getRemoteHost() { return null; }
        @Override public int getRemotePort() { return 0; }
        @Override public String getLocalAddr() { return null; }
        @Override public String getLocalName() { return null; }
        @Override public int getLocalPort() { return 0; }
        @Override public java.util.Locale getLocale() { return null; }
        @Override public java.util.Enumeration<java.util.Locale> getLocales() { return null; }
        @Override public String getScheme() { return "http"; }
        @Override public String getServerName() { return "localhost"; }
        @Override public int getServerPort() { return 8080; }
        @Override public java.io.BufferedReader getReader() throws IOException { return null; }
        @Override public String getRealPath(String path) { return null; }
        @Override public long getDateHeader(String name) { return -1; }
        @Override public java.io.InputStream getInputStream() throws IOException { return null; }
        @Override public int getContentLength() { return -1; }
        @Override public long getContentLengthLong() { return -1; }
        @Override public String getContentType() { return null; }
        @Override public String getCharacterEncoding() { return null; }
        @Override public void setCharacterEncoding(String env) throws java.io.UnsupportedEncodingException {}
        @Override public java.util.Map<String, String[]> getParameterMap() { return null; }
        @Override public String getParameter(String name) { return null; }
        @Override public java.util.Enumeration<String> getParameterNames() { return null; }
        @Override public String[] getParameterValues(String name) { return null; }
        @Override public String getProtocol() { return "HTTP/1.1"; }
        @Override public String getServletPath() { return ""; }
        @Override public jakarta.servlet.http.HttpSession getSession(boolean create) { return null; }
        @Override public jakarta.servlet.http.HttpSession getSession() { return null; }
        @Override public String getRequestedSessionId() { return null; }
        @Override public boolean isRequestedSessionIdValid() { return false; }
        @Override public boolean isRequestedSessionIdFromCookie() { return false; }
        @Override public boolean isRequestedSessionIdFromURL() { return false; }
        @Override public boolean isRequestedSessionIdFromUrl() { return false; }
        @Override public jakarta.servlet.RequestDispatcher getRequestDispatcher(String path) { return null; }
        @Override public String getForwardURI() { return null; }
        @Override public String getForwardQueryString() { return null; }
        @Override public String getForwardContextPath() { return null; }
        @Override public String getForwardServletPath() { return null; }
        @Override public String getForwardPathInfo() { return null; }
        @Override public String getForwardRequestURI() { return null; }
        @Override public String getDispatcherType() { return null; }
        @Override public Object getAttribute(String name) { return null; }
        @Override public java.util.Enumeration<String> getAttributeNames() { return null; }
        @Override public void setAttribute(String name, Object o) {}
        @Override public void removeAttribute(String name) {}
        @Override public jakarta.servlet.ServletContext getServletContext() { return null; }
        @Override public boolean isAsyncStarted() { return false; }
        @Override public boolean isAsyncSupported() { return false; }
        @Override public jakarta.servlet.AsyncContext startAsync() throws IllegalStateException { return null; }
        @Override public jakarta.servlet.AsyncContext startAsync(jakarta.servlet.ServletRequest servletRequest, jakarta.servlet.ServletResponse servletResponse) throws IllegalStateException { return null; }
        @Override public jakarta.servlet.AsyncContext getAsyncContext() { return null; }
    }

    private static class MockHttpServletResponse implements HttpServletResponse {
        private int status = SC_OK;
        private StringWriter writer = new StringWriter();
        private PrintWriter printWriter = new PrintWriter(writer);

        @Override public void setStatus(int sc) { this.status = sc; }
        @Override public int getStatus() { return status; }
        @Override public PrintWriter getWriter() throws IOException { return printWriter; }
        @Override public String toString() { return "Status: " + status + ", Body: " + writer.toString(); }
        @Override public void addCookie(jakarta.servlet.http.Cookie cookie) {}
        @Override public boolean containsHeader(String name) { return false; }
        @Override public String encodeURL(String url) { return url; }
        @Override public String encodeRedirectURL(String url) { return url; }
        @Override public String encodeUrl(String url) { return url; }
        @Override public String encodeRedirectUrl(String url) { return url; }
        @Override public void sendError(int sc, String msg) throws IOException {}
        @Override public void sendError(int sc) throws IOException {}
        @Override public void sendRedirect(String location) throws IOException {}
        @Override public void setDateHeader(String name, long date) {}
        @Override public void addDateHeader(String name, long date) {}
        @Override public void setHeader(String name, String value) {}
        @Override public void addHeader(String name, String value) {}
        @Override public void setIntHeader(String name, int value) {}
        @Override public void addIntHeader(String name, int value) {}
        @Override public void setContentType(String type) {}
        @Override public String getContentType() { return null; }
        @Override public void setCharacterEncoding(String charset) {}
        @Override public String getCharacterEncoding() { return null; }
        @Override public jakarta.servlet.ServletOutputStream getOutputStream() throws IOException { return null; }
        @Override public int getBufferSize() { return 0; }
        @Override public void setBufferSize(int size) {}
        @Override public void flushBuffer() throws IOException {}
        @Override public void resetBuffer() {}
        @Override public boolean isCommitted() { return false; }
        @Override public void reset() {}
        @Override public void setLocale(java.util.Locale loc) {}
        @Override public java.util.Locale getLocale() { return null; }
        @Override public void setContentLength(int len) {}
        @Override public void setContentLengthLong(long len) {}
        @Override public int getHeader(String name) { return -1; }
        @Override public java.util.Collection<String> getHeaders(String name) { return null; }
        @Override public java.util.Collection<String> getHeaderNames() { return null; }
        @Override public String getTrailerFields() { return null; }
        @Override public void setTrailerFields(String trailerFields) {}
        @Override public boolean isAsyncSupported() { return false; }
        @Override public jakarta.servlet.AsyncContext startAsync() throws IllegalStateException { return null; }
        @Override public jakarta.servlet.AsyncContext startAsync(jakarta.servlet.ServletRequest servletRequest, jakarta.servlet.ServletResponse servletResponse) throws IllegalStateException { return null; }
        @Override public jakarta.servlet.AsyncContext getAsyncContext() { return null; }
    }
}

Registering the Filter

To make our RateLimitingFilter active, we need to register it within Spring Security's configuration.

We typically do this in our SecurityFilterChain bean, ensuring it runs before other security filters, especially for unauthenticated endpoints like login.

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
public class SecurityConfig {

    private final RateLimiterService rateLimiterService;

    public SecurityConfig(RateLimiterService rateLimiterService) {
        this.rateLimiterService = rateLimiterService;
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable()) // Disable CSRF for simplicity in API examples
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/login").permitAll() // Allow login without auth
                .requestMatchers("/api/public/**").permitAll() // Public endpoints
                .anyRequest().authenticated() // All other requests need auth
            )
            // Add our custom rate limiting filter
            .addFilterBefore(new RateLimitingFilter(rateLimiterService),
                             UsernamePasswordAuthenticationFilter.class); // Or other relevant filter

        return http.build();
    }

    @Bean
    public RateLimiterService rateLimiterService() {
        // Configure max 5 requests per 60 seconds
        return new RateLimiterService(5, 60000);
    }

    public static void main(String[] args) {
        System.out.println("Spring Security configuration example.");
        System.out.println("RateLimiterService and RateLimitingFilter would be integrated here.");
    }
}

Example with a Controller

Let's imagine a simple login endpoint. With our filter in place, repeated requests from the same IP will be blocked after the limit is reached.

This is a simplified controller, but it shows where the filter would act before the actual login logic.

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.http.ResponseEntity;

// These classes (RateLimiterService, RateLimitingFilter) would be defined as @Component/@Configuration beans
// in a full Spring Boot application, as shown in previous scenes.

@RestController
class MyController {

    @GetMapping("/api/login")
    public ResponseEntity<String> login() {
        return ResponseEntity.ok("Login successful (simulated)!");
    }

    @GetMapping("/api/public/hello")
    public ResponseEntity<String> publicHello() {
        return ResponseEntity.ok("Hello from public endpoint!");
    }
}

// A simplified Spring Boot application to make it conceptually runnable
@SpringBootApplication
public class RateLimitApp {

    // The beans for RateLimiterService and RateLimitingFilter
    // would typically be defined in SecurityConfig or another @Configuration class.
    // We include them here for a self-contained runnable demonstration context.
    @Bean
    public RateLimiterService rateLimiterService() {
        return new RateLimiterService(3, 10000); // 3 requests per 10 seconds
    }

    @Bean
    public RateLimitingFilter rateLimitingFilter(RateLimiterService service) {
        return new RateLimitingFilter(service);
    }

    public static void main(String[] args) {
        System.out.println("This demonstrates a Spring Boot app structure.");
        System.out.println("The RateLimitingFilter would protect /api/login.");
        System.out.println("In a running app, repeated access to /api/login from the same IP");
        System.out.println("would result in HTTP 429 after 3 requests within 10 seconds.");
        // To actually run this, you'd uncomment:
        // SpringApplication.run(RateLimitApp.class, args);
    }
}

Testing Your Rate Limiter

Once configured, you can test your rate limiter by sending multiple requests to the protected endpoint (e.g., /api/login) from the same IP address.

You should observe the following:

  • Initial requests succeed (HTTP 200 OK).
  • After exceeding the limit, subsequent requests fail with an HTTP 429 (Too Many Requests) status code.

This confirms your rate limiter is actively protecting your API.

Quick Check

Rate limiting is an essential security practice. Which of the following are primary benefits of implementing rate limiting on API endpoints?

Recap: Rate Limiting

You've learned about the importance of rate limiting to protect your API endpoints from attacks like brute-force and DoS.

We explored how to implement a basic fixed-window rate limiter using a custom OncePerRequestFilter and a dedicated service in a Spring Boot application.

This fundamental security measure helps ensure your application's stability and resource availability.

자주 묻는 질문

“API 접근 속도 제한” 강의는 무료인가요?

네 — “API 접근 속도 제한” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 Spring Security 6 & JWT Authentication 강의 전체를 잠금 해제할 수 있습니다. Spring Security 6 & JWT Authentication 강의에는 총 4개의 강의가 포함되어 있습니다.

“API 접근 속도 제한”에서 뭘 배우나요?

속도 제한을 사용하여 인증 엔드포인트와 API를 무차별 대입 공격 및 서비스 거부 공격으로부터 보호합니다. 브라우저에서 직접 실행하는 실습 코드로 Spring Security 6 & JWT Authentication을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.

Spring Security 6 & JWT Authentication을(를) 시작하는 데 경험이 필요한가요?

사전 경험은 필요하지 않습니다. CoddyKit의 Spring Security 6 & JWT Authentication은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 2번째 강의입니다.

“API 접근 속도 제한” 강의는 얼마나 걸리나요?

대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.

이 Spring Security 6 & JWT Authentication 강의에서 코드를 작성하고 실행할 수 있나요?

네. 모든 Spring Security 6 & JWT Authentication 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.

이 강의의 모든 강의

  1. 다중 요소 인증 구현
  2. API 접근 속도 제한
  3. 사용자 지정 인증 이벤트 처리
  4. 계정 잠금 및 무차별 대입 공격 방어
← Spring Security 6 & JWT Authentication(으)로 돌아가기