Keselamatan Spring dan Pengesahan JWT · Pelajaran

Mengehadkan Kadar Akses API

Lindungi titik akhir pengesahan dan API anda daripada serangan kekerasan dan penafian perkhidmatan menggunakan pengehad kadar.

Pelajaran 2 daripada 413 langkah

Mengehadkan Kadar Akses API ialah pelajaran Keselamatan Spring dan Pengesahan JWT percuma di CoddyKit. Ini ialah pelajaran 2 daripada 4. Sebanyak 3 pelajaran dalam laluan pembelajaran ini boleh dibaca sepenuhnya secara percuma — selepas itu, CoddyKit PRO membuka akses kepada semua pelajaran, serta latihan praktikal dengan penyunting kod terbina dalam dan tutor kecerdasan buatan yang tersedia 24/7. Pelajaran ini merupakan sebahagian daripada laluan pembelajaran Keselamatan Spring dan Pengesahan JWT, dan kemajuan anda disegerakkan merentas web serta aplikasi CoddyKit. Kursus Keselamatan Spring dan Pengesahan JWT merangkumi sejumlah 4 pelajaran.

Apakah Pengehadan Kadar?

Pengehadan kadar ialah langkah keselamatan penting yang mengawal kekerapan pengguna atau sistem boleh mengulangi sesuatu tindakan dalam tempoh tertentu.

Anggaplah ia seperti pengawal di sebuah kelab yang hanya membenarkan bilangan orang tertentu masuk setiap minit bagi mengelakkan kesesakan.

Ia penting untuk melindungi API dan titik akhir pengesahan anda daripada aktiviti berniat jahat.

Mengapa API Memerlukan Pengehadan Kadar

Tanpa pengehad kadar, aplikasi anda terdedah kepada beberapa serangan:

  • Serangan Paksaan Brute: Percubaan log masuk berulang untuk meneka kata laluan.
  • Penafian Perkhidmatan (DoS): Membebankan pelayan anda dengan permintaan sehingga perkhidmatan tidak tersedia.
  • Kehabisan Sumber: Pengguna yang sah mungkin terjejas jika seorang pengguna menggunakan terlalu banyak sumber.

Pengehadan kadar membantu mengekalkan kestabilan dan keselamatan.

Strategi Pengehadan Kadar

Terdapat beberapa cara untuk melaksanakan pengehad kadar:

  • Tetingkap Tetap: Membenarkan N permintaan bagi setiap tetingkap (contohnya, 100 permintaan seminit). Mudah, tetapi boleh mengalami masalah “lonjakan” di sempadan tetingkap.
  • Tetingkap Gelongsor: Pendekatan yang lebih canggih untuk melancarkan trafik dan menghalang lonjakan.
  • Baldi Token/Baldi Bocor: Lebih kompleks dan sering digunakan untuk membentuk trafik.

Dalam pelajaran ini, kita akan menumpukan pada pendekatan tetingkap tetap yang mudah.

Pelaksanaan Spring Boot

Dalam Spring Boot, kita boleh melaksanakan pengehad kadar menggunakan Penapis Servlet tersuai.

Penapis memintas permintaan HTTP masuk sebelum permintaan tersebut mencapai kaedah pengawal anda. Ini membolehkan kita menyemak had kadar permintaan dan menyekatnya jika perlu.

Kita akan mencipta perkhidmatan khusus untuk mengurus logik pengehadan kadar.

Perkhidmatan Pengehad Kadar Asas

Mula-mula, mari kita cipta perkhidmatan mudah untuk menjejak bilangan permintaan. Contoh ini menggunakan peta dalam memori untuk demonstrasi.

Cuba jalankan persediaan perkhidmatan asas ini:

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
    }
}

Memahami Perkhidmatan

RateLimiterService kita menggunakan ConcurrentHashMap untuk menyimpan RequestInfo bagi setiap pengecam pelanggan (seperti alamat IP).

  • MAX_REQUESTS: Bilangan maksimum permintaan yang dibenarkan.
  • TIME_WINDOW_MILLIS: Tempoh tetingkap tetap.
  • tryAcquire(String clientIdentifier): Kaedah ini menyemak sama ada pelanggan boleh membuat permintaan. Kaedah ini menetapkan semula tetingkap jika telah tamat atau menambah kiraan.

Kaedah main menunjukkan penggunaannya.

Mencipta Penapis Tersuai

Sekarang, mari kita cipta OncePerRequestFilter tersuai. Penapis ini akan:

  • Memintas setiap permintaan HTTP masuk.
  • Mengekstrak pengecam pelanggan (contohnya, alamat IP jauh).
  • Menggunakan RateLimiterService kita untuk menyemak sama ada permintaan dibenarkan.
  • Jika tidak dibenarkan, menghantar respons HTTP 429 (Terlalu Banyak Permintaan).

Ini memastikan had kadar dikuatkuasakan pada peringkat awal kitar hayat permintaan.

Contoh Kod Penapis

Berikut ialah kod penuh untuk RateLimitingFilter tersuai kita. Perhatikan cara penapis ini memasukkan RateLimiterService dan mengendalikan respons.

Kemudian, kita akan menambahkan penapis ini secara manual ke dalam rantaian Spring Security.

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; }
    }
}

Mendaftarkan Penapis

Untuk mengaktifkan RateLimitingFilter kita, kita perlu mendaftarkannya dalam konfigurasi Spring Security.

Biasanya, kita melakukan perkara ini dalam bean SecurityFilterChain, bagi memastikan penapis tersebut berjalan sebelum penapis keselamatan lain, terutamanya untuk titik akhir yang tidak memerlukan pengesahan seperti log masuk.

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.");
    }
}

Contoh dengan Pengawal

Bayangkan titik akhir log masuk yang mudah. Dengan penapis kita digunakan, permintaan berulang daripada IP yang sama akan disekat selepas had dicapai.

Ini ialah pengawal ringkas, tetapi ia menunjukkan tempat penapis akan bertindak sebelum logik log masuk sebenar.

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);
    }
}

Menguji Pengehad Kadar Anda

Setelah dikonfigurasikan, anda boleh menguji pengehad kadar dengan menghantar berbilang permintaan ke titik akhir yang dilindungi (contohnya, /api/login) daripada alamat IP yang sama.

Anda sepatutnya melihat perkara berikut:

  • Permintaan awal berjaya (HTTP 200 OK).
  • Selepas had dilampaui, permintaan seterusnya gagal dengan kod status HTTP 429 (Terlalu Banyak Permintaan).

Ini mengesahkan bahawa pengehad kadar anda sedang melindungi API secara aktif.

Semakan Pantas

Pengehadan kadar ialah amalan keselamatan yang penting. Manakah antara berikut merupakan manfaat utama pelaksanaan pengehadan kadar pada titik akhir API?

Imbas Kembali: Pengehadan Kadar

Anda telah mempelajari kepentingan pengehadan kadar untuk melindungi titik akhir API daripada serangan seperti paksaan brute dan DoS.

Kita telah meneroka cara melaksanakan pengehad kadar tetingkap tetap asas menggunakan OncePerRequestFilter tersuai dan perkhidmatan khusus dalam aplikasi Spring Boot.

Langkah keselamatan asas ini membantu memastikan kestabilan aplikasi dan ketersediaan sumber anda.

Percuma untuk bermula

Pelajari Java dengan tutor kecerdasan buatan — percuma

Tulis dan jalankan kod sebenar dalam pelayar anda, dapatkan bantuan segera daripada tutor kecerdasan buatan yang tersedia 24/7, dan sambung semula dari tempat anda berhenti di web atau dalam aplikasi.

Kursus
12
Pelajaran
48

Soalan Lazim

Adakah pelajaran “Mengehadkan Kadar Akses API” percuma?

Ya — sebanyak 3 pelajaran dalam laluan pembelajaran Keselamatan Spring dan Pengesahan JWT, termasuk “Mengehadkan Kadar Akses API”, boleh dibaca sepenuhnya secara percuma di web ini. Selepas itu, CoddyKit PRO membuka akses kepada semua pelajaran, serta latihan interaktif dengan penyunting kod terbina dalam dan tutor kecerdasan buatan yang tersedia 24/7. Kursus Keselamatan Spring dan Pengesahan JWT merangkumi sejumlah 4 pelajaran.

Apakah yang akan saya pelajari dalam “Mengehadkan Kadar Akses API”?

Lindungi titik akhir pengesahan dan API anda daripada serangan kekerasan dan penafian perkhidmatan menggunakan pengehad kadar. Anda berlatih Keselamatan Spring dan Pengesahan JWT menggunakan kod praktikal yang dijalankan terus dalam pelayar, manakala tutor kecerdasan buatan 24/7 menjawab soalan anda semasa anda mengikuti pelajaran.

Adakah saya memerlukan pengalaman untuk memulakan Keselamatan Spring dan Pengesahan JWT?

Tiada pengalaman terdahulu diperlukan. Pembelajaran Keselamatan Spring dan Pengesahan JWT di CoddyKit disusun untuk pelajar daripada peringkat pemula hingga lanjutan, jadi anda boleh bermula di sini atau dari awal dan belajar mengikut kadar anda sendiri. Ini ialah pelajaran 2 daripada 4.

Berapa lamakah pelajaran “Mengehadkan Kadar Akses API” diambil?

Kebanyakan pelajaran CoddyKit mengambil masa kira-kira 5–10 minit. Setiap pelajaran ringkas dan interaktif, jadi anda boleh membuat kemajuan secara berterusan dan menyambung tepat dari tempat anda berhenti di web atau aplikasi.

Bolehkah saya menulis dan menjalankan kod dalam pelajaran Keselamatan Spring dan Pengesahan JWT ini?

Ya. Setiap pelajaran Keselamatan Spring dan Pengesahan JWT menyertakan penyunting kod terbina dalam, jadi anda boleh menulis dan menjalankan kod sebenar terus dalam pelayar serta menerima maklum balas kecerdasan buatan serta-merta — tanpa memerlukan persediaan setempat.

Semua pelajaran dalam kursus ini

  1. Melaksanakan Pengesahan Berbilang Faktor
  2. Mengehadkan Kadar Akses API
  3. Pengendalian Peristiwa Pengesahan Tersuai
  4. Penguncian Akaun dan Perlindungan Daripada Serangan Brute Force
← Kembali ke Keselamatan Spring dan Pengesahan JWT