13 Mart 2022 Pazar

JSF CSRF protection

CSRF(Cross Site Request Forgery); kötü niyetli bir web sitesi, e-posta, blog, anlık mesaj veya program aracılığı ile kullanıcının kullandığı web tarayıcısı üzerinden, kimliği doğrulanmış olan web uygulamasından bir eylem gerçekleştirmesine neden olduğunda meydana gelen bir saldırı türüdür. JSF 2.2 in ci sürümden itibaren CSRF koruması gelmiş bulunmaktadır. CSRF saldırısı genelde phishing(e-dolandırıcılık) saldırısı ile yapılmaktadır. Senaryo alttaki şekilde olduğu gibi gerçekleşmektedir.

Bir JSF web uygulamasını kullanan kurban sisteme login olur. Yetkili session bilgileri kurbanın localinde yetkisine göre artık vardır. Saldırgan kurban a bir oltalama maili gönderir. Mail deki link saldırganın yazdığı form post işleminin yapıldığı siteye yönlendirilmiştir. Link açılır açılmaz, kurbanın aktif yetkili session bilgileri kullanılarak, kurbanın şifresi değiştirilmekte, kullanıcı adı ve şifre saldırgana mail olarak gitmektedir. Saldırgan çapraz bir sitedeki form post request i ile kurbanın tüm giriş bilgilerini ele geçirmiş bulunmaktadır. Başarılı bir CSRF saldırısının etkisi, savunmasız uygulamanın maruz kaldığı yeteneklerle ve kurbanların ayrıcalıklarıyla sınırlıdır.

Bu tarz bir istek sahteciliğini önleyebilmek için; Client – Server arası yapılacak her requestin güvenli hale getirilmesi gereklidir. Tavsiye edilen yöntemler;

1) Kullandığınız frameworklerde durum değişikliğine neden olan requestler için CSRF koruması olup olmadığını kontrol edin.

2) Eğer kullandığınız frameworkte CSRF koruması yok ise, Stateful(durum bilgisinin tutulduğu) synchronizer token pattern kullanın. Bu pattern da GET dışındaki her request için bir CSRF token üretip, doğrulamanız gereklidir.

3) Double Submit Cookie ile güçlü bir rastgele üretilmiş olan değeri, hem tanımlama bilgisi hem de istek bilgisi olarak cookie de client a gönderip, server site da doğrulayabilirsiniz.

4) Same Site Cookie Attribute: Yeni browser larda farklı sitelerden gelen isteklerde cookie lerin güvenlik seviyesi olarak yeni bir güvenlik attribute u olan “SameSite” eklenmiştir.

Set-Cookie: JSESSIONID=xxxxx; SameSite=Strict

Set-Cookie: JSESSIONID=xxxxx; SameSite=Lax

Set-Cookie: JSESSIONID=xxxxx; SameSite=None

SameSite ın desteklenen browser lardaki default değeri browserlara göre farklılık gösterir. Bu yüzden bu attribute i kendinizin ayarlamanız tavsiye edilir. (Browser desteği ve default değerleri için; https://caniuse.com/?search=samesite linkini inceleyebilirsiniz). “None” da hiçbir koruma yoktur. “Lax” ta farklı domainlerden gelen sadece GET request lerindeki cookielerin geçişine izin verilir. Güvenlik seviyesini en üst seviyede tutmak isterseniz bu değeri “Strict” yapmanız gerekir. Bu sayede farklı dns ler için cookie değerlerinin geçişine izin verilmez. Tabi bu da CSRF saldırılarını önlemede bir miktar daha katkı sağlamış olur. Yukarıda cookie de tutulan session id değerinin farklı domainlerden yapılan requestlerde izin verilip verilmemesi ile ilgili yetkilendirme yapılmaktadır.

5) Kullanıcı etkileşimli korumalar(Yetkilendirerek yeniden kimlik doğrulama, tek kullanımlık token lar, yeni CAPTCHA versiyonlarının kullanımı).

6) Header bilgileri özelleştirilmiş request(istek) kullanabilirsiniz.

7) Standart header da origin kontrolünü yapabilirsiniz(CORS filter).

8) XSS ile ilgili alınan tedbirler de CSRF saldırılarını önlemede yardımcıdır.

9) State(durum) değişikliğine neden olan tüm request lerde GET kullanmayın.

Benim CSRF güvenlik seviyesi ile ilgili olmazsa olmaz önerim 1. veya 2. öneri ile beraber 9. öneriyi web uygulamanız için implemente etmeniz olacaktır. Sonrasında 1 veya 2 yi uyguladıysanız ve güvenlik seviyenizi artırmak istiyorsanız ise; 4, 7 ve 8 inci önerileri sırayla uygulamanızı öneririm.

1. Öneride bahsedilen JSF frameworkte CSRF koruması nasıl çalışır ona bakalım. Yukarıda , JSF 2.2 den itibaren CSRF koruması geldi demiştik. Bununla ilgili implementation örneğini Oracle ‘ın kendi white paper larından alıp deneyebilirsiniz(https://www.oracle.com/webfolder/technetwork/tutorials/obe/java/JSF-CSRF-Demo/JSF2.2CsrfDemo.html). Buradaki örnekte GET dışındaki tüm request lerde CSRF koruması default olarak aktif tir. GET requestlerindeki koruma için; “faces-config.xml” dosyasının içinde “protected-views” taglar i arasında GET request koruması yapılacak jsf sayfalarının pathleri tanımlanmalıdır. Örn.:

<protected-views>

            <url-pattern>/csrf_protected_page.xhtml</url-pattern>

</protected-views>

            Her form verisinin en altında “javax.faces.ViewState” isminde hidden olan bir input değeri otomatik olarak eklenir. Bu view state değerinin default değeri server side da tutulmaktadır. Ancak bazen gerek performans için gerekse de clustered web application larda sticky session kullanıldığından tüm sunucularda bu değerin ortak kullanılmasına ihtiyaç duyulmaktadır. Bunun için “web.xml” içerisinde aşağıdaki config değerlerinin girilmesi gerekir;

<env-entry>

    <env-entry-name>jsf/ClientSideSecretKey</env-entry-name>

    <env-entry-type>java.lang.String</env-entry-type>

    <env-entry-value>[AES key in Base64 format]</env-entry-value>

</env-entry>

<context-param>

    <param-name>javax.faces.STATE_SAVING_METHOD</param-name>

    <param-value>client</param-value>

</context-param>

Burada gireceğiniz AES key değerini en az 128 bit key olarak generate ettikten sonra mutlaka UTF8 Base64 string olarak encode etmeyi unutmayın. Bu config değerinden sonra artık client ta form içindeki viewstate hidden değeri browser tarafında gösterilmeye başlanacaktır. Her clustered sunucuda ki application aynı aes key değerini kullanarak viewstate değerini generate ettiği için sticky session ile ilgili bir sorun yaşanmayacaktır.. Bu projenin github örnek kaynak kodunu “https://github.com/ariferol/jsf-csrf-simple.git” linkinden inceleyebilirsiniz.

Peki, JSF 2.2 default CSRF protection dışında, JSF için başka bir koruma seçeneği var mıdır? Evet var. Özellikle JSF 2.2 den önceki sürümler için kendi özel çözümünüzü üretmeniz gerekmektedir. Özel çözüm üretecekseniz benim önerim; OWASP(Open Web Application Security Project) ta JEE application lar için önerilen CSRF Guard API sini kullanmanızdır. CSRF Guard API nin open source github repository sine “https://github.com/OWASP/www-project-csrfguard” linkinden ulaşabilirsiniz.

API nin JSF teki implementation u için;

1) Maven POM file in içine dependency lerini ekleyin;

        <dependency>

            <groupId>org.owasp</groupId>

            <artifactId>csrfguard</artifactId>

            <version>4.1.2</version>

        </dependency>

 

        <!-- Stateful web application support -->

        <dependency>

            <groupId>org.owasp</groupId>

            <artifactId>csrfguard-extension-session</artifactId>

            <version>4.1.2</version>

        </dependency>

 

        <!-- JSP TAG support -->

        <dependency>

            <groupId>org.owasp</groupId>

            <artifactId>csrfguard-jsp-tags</artifactId>

            <version>4.1.2</version>

        </dependency>

2) “src/main/resources” folder inin içine “csrfguard.properties” configrasyon dosyasını kopyalayın.

3) “web.xml” dosyası içerisine listenerları, context parametrelerini, filter ve koruma altına alınacak url pattern pathini, javascript servlet ve bu servletin url pattern ını aşağıdaki gibi ekleyin.

<servlet-mapping>

            <servlet-name>Faces Servlet</servlet-name>

            <url-pattern>/other-context-url/*</url-pattern>

</servlet-mapping>

    <listener>

        <listener-class>org.owasp.csrfguard.CsrfGuardServletContextListener</listener-class>

    </listener>

    <listener>

        <listener-class>org.owasp.csrfguard.CsrfGuardHttpSessionListener</listener-class>

    </listener>

 

    <context-param>

        <param-name>Owasp.CsrfGuard.Config</param-name>

        <param-value>/csrfguard.properties</param-value>

    </context-param>   

    <context-param>

        <param-name>Owasp.CsrfGuard.Config.Print</param-name>

        <param-value>true</param-value>

    </context-param>

   

    <filter>

        <filter-name>CSRFGuard</filter-name>

        <filter-class>org.owasp.csrfguard.CsrfGuardFilter</filter-class>

    </filter>

    <filter-mapping>

        <filter-name>CSRFGuard</filter-name>

        <url-pattern>/other-context-url/content/*</url-pattern>

    </filter-mapping>

 

    <servlet>

        <servlet-name>JavaScriptServlet</servlet-name>

        <servlet-class>org.owasp.csrfguard.servlet.JavaScriptServlet</servlet-class>

    </servlet>

    <servlet-mapping>

        <servlet-name>JavaScriptServlet</servlet-name>

        <url-pattern>/JavaScriptServlet</url-pattern>

    </servlet-mapping>

4) “csrfguard.properties” config dosyasının içinde;

org.owasp.csrfguard.UnprotectedMethods = GET (GET dışındaki tüm methodlarda POST,PUT,DELETE gibi CSRF korumasını açık hale getirin)

org.owasp.csrfguard.unprotected.Error = %servletContext%/error.html (context-root un olduğu giriş sayfasının olduğu pathe error.html isimli bir kullanıcı bilgilendirme sayfası ekleyip, hata durumunda yönlendirilmesini sağlayın)

5) Koruma altına alınacak her form bulunan jsf sayfasının head tag inin içerisine;

<h:head>

            <title>Page Title </title>

            <script src="/context-root/other-context-url/JavaScriptServlet"></script>       

</h:head>

Şeklinde apinin içinden çağırılan javascript kütüphanesi import bildirimini ekleyin. Eğer single page app kullanıyorsanız, dashboard ın olduğu ana sayfaya bu bildirimi bir kez eklemeniz yeterli olacaktır.Dashboard dan dallanılan tüm sayfa url leri için de dashboard sayfası render olduğu için diğer sayfalara eklemenize gerek kalmayacaktır.

Web uygulaması çalıştırıldığında jsf sayfasının browserda ki kaynak koduna baktığınızda formun en altında hidden field olarak aşağıdaki gibi CSRF token ın generate edildiğini göreceksiniz;

<input type="hidden" name="OWASP-CSRFTOKEN" value="S5T3-UWOD-QGHK-S37C-U0OZ-T22Q-PTMA-5D1V">

Kaynaklar:


27 Ocak 2022 Perşembe

Netbeans RSA SSH gitlab repository connection

Netbeans RSA asimetrik şifreleme algoritması ile  generate edilen public SSH key ile gitlab repository clone işlemi:

Bu sorunu PEM key generation seçeneği ile çözebiliyoruz.

Önce localde RSA ssh key generate ediyoruz;

ssh-keygen -t rsa -b 2048 -m PEM -C "email@hostname.com" (rsa key generate eder)


Sonra pulic key i, git bash console dan clipboard a alıyoruz;

cat ~/.ssh/id_rsa.pub | clip (public rsa ssh key i clippboard a atar)

eval $(ssh-agent -s) (ssh agenti baslatir)

 

Sonra gitlab repository deki profile ayarlarındaki SSH sekmesinden, clipboarddaki public SSH key i “Key” textbox ının içine focuslanıp, paste yaptıktan sonra “Add key” butonu ile ekliyoruz.

 


Sonra client pc de CLI dan connection testi yapıyoruz;

ssh -T git@xxx.com.tr (repository connection test eder)

 

Sorun yoksa Netbeans de, Team/git/clone… şeklinde new repository clone yapabiliriz;

 


Not: ~/.ssh/config file in icinde;

"KexAlgorithms +diffie-hellman-group1-sha1"

bu satir in olmasi gerekiyor. 

 

Kaynaklar:

·        https://docs.gitlab.com/ee/ssh/#troubleshooting

·         

12 Aralık 2021 Pazar

Hız Limit Algoritmaları

Hız Limit Algoritmaları(Rate Limit Algorithms)

Çatlak kova(Leaky Bucket) algoritması:

Bu algoritmada; saniyede ne kadar request in çalışmasına izin verilecekse, bir saniyelik zaman dilimi o kadar parçaya bölünür. (İstek aralığı dilimi(milisaniye) = 1000 / maksimum istek sayısı) Sonra, bir saniye içerisinde bu istek aralığı zaman dilimi sınırları kontrol edilir. Gelen istekler bu zaman dilimi sınırından küçük veya eşit ise isteğin çalışması sağlanıp, zaman dilimi sınırı bir sonraki dilimi sınırına kaydırılır. Artık kontrol noktası bir sonraki zaman dilimi olmuştur. Yani her istek bölümlenmiş zaman dilimi aralıklarında çalışmaya zorlanmış olur. Belirlenenden fazla request geldiğinde ise reject edilmiş olur.

public abstract class RateLimiter {

   
protected final int maxRequestPerSec;

    protected
RateLimiter(int maxRequestPerSec) {
        
this.maxRequestPerSec = maxRequestPerSec;
   
}

   
abstract boolean allow();
}

public class LeakyBucket extends RateLimiter {

   
private long nextAllowedTime;

    private final long
REQUEST_INTERVAL_MILLIS;

    protected
LeakyBucket(int maxRequestPerSec) {
       
super(maxRequestPerSec);
       
/*Bir saniyeyi, max request sayisine bolerek, saniyedeki bir request in zaman araligini buluyoruz*/
       
REQUEST_INTERVAL_MILLIS = 1000 / maxRequestPerSec;

       
/*Sonraki izin verilen zamanin ilk degerini belirliyoruz*/
       
nextAllowedTime = System.currentTimeMillis();
   
}

   
@Override
   
boolean allow() {
       
long curTime = System.currentTimeMillis();
        synchronized
(this) {
           
/*Thread safe olarak, izin verilen sonraki zaman dilimi degeri; su anki zaman diliminden kucuk veya esit ise, request e izin veriyoruz.
            * Eger yuk cok fazla ise, sonraki izin verilen zaman dilimi degeri artıp tasacagindan, su andaki zaman degerinden buyuk olacagindan, max request sayisindan fazlasina izin vermemis olacagiz */
           
if (nextAllowedTime <= curTime) {
               
/*Requeste izin verdigimizde , izin verilen sonraki zaman dilimi degerini; belirledigimiz saniyedeki bir requestin zaman araligi kadar artiriyoruz.*/
               
nextAllowedTime = curTime + REQUEST_INTERVAL_MILLIS;
                return true;
           
}
           
return false;
       
}
    }
}

 

import java.util.concurrent.CountDownLatch;
import
java.util.concurrent.TimeUnit;

public class
Main {
   
public static void main(String[] args) throws InterruptedException {
       
final int MAX_REQUESTS_PER_SEC = 10;
       
/*Kova nin bir saniyedeki max boyutunu tanimliyoruz;*/
       
RateLimiter rateLimiter = new LeakyBucket(MAX_REQUESTS_PER_SEC);       

       
Thread requestThread = new Thread(() -> {
           
/*10 requesti 1 saniyede calistirmak istiyoruz;*/
           
sendRequest(rateLimiter, 10, 1);
       
});

       
requestThread.start();
       
requestThread.join();
   
}

   
private static void sendRequest(RateLimiter rateLimiter, int totalRequest, int requestPerSec) {
       
long startTime = System.currentTimeMillis();
       
/*totalCount kadar thread counter i baslatiyoruz;*/
       
CountDownLatch threadCounter = new CountDownLatch(totalRequest);
        for
(int i = 0; i < totalRequest; i++) {
           
try {
               
new Thread(() -> {
                   
while (!rateLimiter.allow()) {
                       
try {
                            TimeUnit.
MILLISECONDS.sleep(10);
                       
} catch (InterruptedException e) {
                            e.printStackTrace()
;
                       
}
                    }
                   
threadCounter.countDown();
               
}).start();
               
TimeUnit.MILLISECONDS.sleep(1000 / requestPerSec);
           
} catch (Exception e) {
                e.printStackTrace()
;
           
}
        }
       
try {
            threadCounter.await()
;
       
} catch (InterruptedException e) {
            e.printStackTrace()
;
       
}
       
double duration = (System.currentTimeMillis() - startTime) / 1000.0;
       
System.out.println("-------------------------------------------------------------------------------------");
       
System.out.println(totalRequest + " adet request " + duration + " saniye icerisinde islendi. ");
       
System.out.println("Hız: Saniyede ; " + (double) totalRequest / duration + " adet request islendi.");
       
System.out.println("-------------------------------------------------------------------------------------");
   
}
}

 

10 adet requestin 1 saniyede işlenmesini istiyoruz;

Hız limiti vermeden önce ki sonuç:

-------------------------------------------------------------------------------------

10 adet request 0.003 saniye icerisinde islendi.

Hız: Saniyede ; 3333.3333333333335 adet request islendi.

 

Hız limiti verdikten sonraki sonuç;

-------------------------------------------------------------------------------------

10 adet request 10.048 saniye icerisinde islendi.

Hız: Saniyede ; 0.9952229299363057 adet request islendi.

 

Jeton Kovası(Token Bucket) Algoritması:

Bu algoritmayı gerçek hayata uyarlayarak ifade etmek istersek; bir jeton kovası içine jeton(token) attığımızı düşünelim. Birim zamanda kova içindeki jeton sayısı kadar request e izin verilmektedir. Kovadaki birim zamandaki jeton lar bittiğinde gelen request ler reject edilmektedir.

import java.util.concurrent.TimeUnit;

public class
TokenBucket extends RateLimiter {

   
private int tokens;

    public
TokenBucket(int maxRequestsPerSec) {
       
super(maxRequestsPerSec);
        this
.tokens = maxRequestsPerSec;
        new
Thread(() -> {
           
while (true) {
               
try {
                    TimeUnit.
SECONDS.sleep(1);
               
} catch (InterruptedException e) {
                    e.printStackTrace()
;
               
}
                refillTokens(
maxRequestsPerSec);
           
}
        }).start()
;
   
}

   
@Override
   
public boolean allow() {
       
synchronized (this) {
           
if (tokens == 0) {
               
return false;
           
}
           
tokens--;
            return true;
       
}
    }

   
private void refillTokens(int cnt) {
       
synchronized (this) {
           
tokens = Math.min(tokens + cnt, maxRequestPerSec);
           
notifyAll();
       
}
    }
}

 

Request:

sendRequest(tokenBucketLimiter, 10, 1);

 

Output:

-------------------------------------------------------------------------------------

10 adet request 10.052 saniye icerisinde islendi.

Hız: Saniyede ; 0.9948269001193792 adet request islendi.

-------------------------------------------------------------------------------------

Bu örnek kodda iki thread var. Birisi token ları verilen saniyedeki request sayısına göre sürekli dolduruyor(yeniliyor). Diğeri request thread lerini başlatıyor. Burada aynı anda gelecek thread leri sınırlandırmak için; sürekli olarak saniyedeki max request sayısı kadar token ilave ediliyor. İzin verilmiş saniyedeki request sayısından fazlası gelirse reject ediliyor. Yani bir saniyede aynı anda gelen requestlerin sayısı, parametre olarak verdiğimiz max request sayısını geçmeyen istekler allow ediliyor(izin veriliyor).

         Yukarıdaki kodda kovaya doldurulan jetonları sabit doldurmak yerine, gelen request lere bağlı olarak, biraz gecikmeli şekilde, tembel(lazy) olarak da doldurabiliriz;

public class TokenBucketLazyRefill extends RateLimiter {

   
private int tokens;

    private long
lastRefillTime;

    public
TokenBucketLazyRefill(int maxRequestPerSec) {
       
super(maxRequestPerSec);
        this
.tokens = maxRequestPerSec;
        this
.lastRefillTime = System.currentTimeMillis();
   
}

   
@Override
   
public boolean allow() {
       
synchronized (this) {
            refillTokens()
;
            if
(tokens == 0) {
               
return false;
           
}
           
tokens--;
            return true;
       
}
    }

   
private void refillTokens() {
       
long curTime = System.currentTimeMillis();
        double
secSinceLastRefill = (curTime - lastRefillTime) / 1000.0;
        int
cnt = (int) (secSinceLastRefill * maxRequestPerSec);
        if
(cnt > 0) {
            
tokens = Math.min(tokens + cnt, maxRequestPerSec);
           
lastRefillTime = curTime;
       
}
    }
}

 

Request:

sendRequest(tokenBucketLazyLimiter, 10, 1);

 

Output:

-------------------------------------------------------------------------------------

10 adet request 10.048 saniye icerisinde islendi.

Hız: Saniyede ; 0.9952229299363057 adet request islendi.

-------------------------------------------------------------------------------------

Bu örnek kod da da, TokenBucket algoritmasına benzer şekilde saniyede izin verilen token sayısı kadar requestin çalışması sağlanmaktadır. Ancak tokenların doldurulma, yenilenme süresi gelen requestlere göre biraz geciktirilmektedir.Token doldurma süresi ve doldurulacak token miktarı; son token doldurulma(yenilenme) süresi nin saniye cinsinden değerinin, saniyede izin verilen max request sayısı ile çarpımının sıfırdan büyük olması şartına göre değişkenlik göstermektedir. (Yeniden doldurulacak token sayısı = (milisaniye olarak şu an - milisaniye olarak son tekrar token doldurma zamanı) * birim zamanda çalışmasına izin verilen max token sayısı) Bu işlem de token ların dolma süresini biraz geciktirmektedir.

 

Sabit Pencere Sayacı(Fixed Window Counter) Algoritması:

Bu algoritmada anlık requestler, izin verilen istek sayısı kadar pencereye “ConcurrentHashMap” nesnesi yardımıyla bölünür.

Her penceredeki sayaç limit i dolduğunda, gelen istekler reddedilmektedir.

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

public class
FixedWindowCounter extends RateLimiter {

   
// TODO: Burada onceki gunden kalan eski pencerelerin silinmesi islemi yapilabilir
   
private final ConcurrentMap<Long, AtomicInteger> windows = new ConcurrentHashMap<>();

    protected
FixedWindowCounter(int maxRequestPerSec) {
       
super(maxRequestPerSec);
   
}

   
@Override
   
boolean allow() {
       
long windowKey = System.currentTimeMillis() / 1000 * 1000;
       
windows.putIfAbsent(windowKey, new AtomicInteger(0));
        return
windows.get(windowKey).incrementAndGet() <= maxRequestPerSec;
   
}
}

Request:

sendRequest(fixedWindowCounterLimiter, 10, 1);

 

Output:

-----------------------------FixedWindowCounter---------------------------------

10 adet request 10.054 saniye icerisinde islendi.

Hız: Saniyede ; 0.9946290033817385 adet request islendi.

-------------------------------------------------------------------------------------

Yukarıdaki kodda, t anındaki zaman sayacı(windows nesnesi) sabit pencerelere bölünmektedir. Yani, her allow isteği geldiğinde bir concurrent hash map e, request timestamp(zaman damgası) flag i key olarak, sabit şekilde atılıp, bu key değerine ait olan counter, o an için AtomicInteger nesnesi ile incremental olarak artırılıyor. N inci kez aynı zaman diliminde gelen thread e ait request in değeri, parametre olarak verilmiş olan max request size ından fazla olduğu zaman, allow değeri reject edilerek false dönüyor. Yani aynı anda fix olarak sınırları kesin belirlenmiş bir pencere gibi max request sınırı ConcurrentHashMap nesnesi içinde kısıtlanmış olmaktadır.

 

Sürgülü Pencere Günlüğü(Sliding Window Log) Algoritması:

Bu algoritmada her allow isteği geldiğinde, t anı ve bir önceki saniye aralığının log u tutulur.

import java.util.Queue;

public class
SlidingWindowLog extends RateLimiter {

   
private final Queue<Long> log = new LinkedList<>();

    protected
SlidingWindowLog(int maxRequestPerSec) {
       
super(maxRequestPerSec);
   
}

   
@Override
   
boolean allow() {
       
long curTime = System.currentTimeMillis();
        long
boundary = curTime - 1000;
        synchronized
(log) {
           
while (!log.isEmpty() && log.element() <= boundary) {
               
log.poll();
           
}
           
log.add(curTime);
            return
log.size() <= maxRequestPerSec;
       
}
    }
}

Request:

endRequest(slidingWindowLogLimiter, 10, 1);

 

Output:

-----------------------------SlidingWindowLog---------------------------------

10 adet request 10.055 saniye icerisinde islendi.

Hız: Saniyede ; 0.9945300845350572 adet request islendi.

-------------------------------------------------------------------------------------

Yukarıdaki kodda; log isimli LinkedList nesnesinin içinde daima şu an ve bir saniye öncesi zaman diliminin logu tutulmaktadır. Fixed window dan farkı, bir önceki saniye yi de kapsayacak şekilde, t anındaki zaman sayacının pencerelere bölünmesidir. T anı ve bir saniye öncesindeki tutulan log, linked list içerisinde tutulur, bir saniye öncesindeki loglar ise log listesinden çıkarılır. T anı ve bir saniye öncesindeki gelen toplam request sayısı, max request sayısından fazla ise gelen requestler reject edilir.

 

Sürgülü Pencere(Sliding Window) Algoritması:

Bu algoritma da Sabit Pencere Sayacı(Fixed Window Counter) na benzer. Bir pencerenin kendinden önceki requestlerinin %75 ile aktif pencerenin isteklerinin toplamı max request sayısını aşmadıysa allow edilir.

 

Yani üstteki örneğe göre İstek sayısı = 9*(1 - %25) + 5 = 11.75 tir. Bu sayı max istek sayısını aştığı için(11.75 > 10 olduğu için) 5 request gelen ikinci pencere nin %25 lik zaman diliminde gelen request ler reject edilmektedir. Her bölümlenmiş pencere arasında böyle bir tolerans bırakmak daha pratik bulunmuştur.

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

public class
SlidingWindow extends RateLimiter {

   
// TODO: Burada onceki zamandan kalan eski pencerelerin silinmesi islemi yapilabilir
   
private final ConcurrentMap<Long, AtomicInteger> windows = new ConcurrentHashMap<>();

    protected
SlidingWindow(int maxRequestPerSec) {
       
super(maxRequestPerSec);
   
}

   
@Override
   
boolean allow() {
       
long curTime = System.currentTimeMillis();
        long
curWindowKey = curTime / 1000 * 1000;
       
windows.putIfAbsent(curWindowKey, new AtomicInteger(0));
        long
preWindowKey = curWindowKey - 1000;
       
AtomicInteger preCount = windows.get(preWindowKey);
        if
(preCount == null) {
           
return windows.get(curWindowKey).incrementAndGet() <= maxRequestPerSec;
       
}

       
double preWeight = 1 - (curTime - curWindowKey) / 1000.0;
        long
count = (long) (preCount.get() * preWeight
                +
windows.get(curWindowKey).incrementAndGet());
        return
count <= maxRequestPerSec;
   
}
}

 

Request:

sendRequest(slidingWindowLimiter, 10, 1);

 

Output:

-----------------------------SlidingWindow---------------------------------

10 adet request 10.056 saniye icerisinde islendi.

Hız: Saniyede ; 0.9944311853619731 adet request islendi.

-------------------------------------------------------------------------------------

Yukarıdaki kodda, sabit pencere sayacında olduğu gibi, ConcurrentHashMap içinde gene her pencere içindeki request tutulmakta. Ancak kendinden önceki penceredeki requestlerin %75 i ile kendi penceresindeki requestin toplamı max izin verilen request ten küçük eşit ise allow methodunda requestin çalışmasına izin verilmektedir. Bu da her pencerede izin verilen request sayısının değerini azaltmış bulunmaktadır.

 

Guava Limiter API:

Google ın Rate Limit API olarak sunmuş olduğu Guava library ile de hız limit sınırlarını kontrol altında tutmak mümkündür.

Aşağıdaki örnekte, Guava API de, bir saniyede bir requeste izin verecek şekilde limit veriliyor. 10 request multi thread olarak, saniyede dörderli request çalışacak şekilde istekte bulunuluyor. Sonuçta 10 request ten 2 si başarılı olarak kabul edilip, kalanlar reject ediliyor.

Eklenen Dependency:

<dependency>
    <groupId>
com.google.guava</groupId>
    <artifactId>
guava</artifactId>
    <version>
29.0-jre</version>
</dependency>

 

Source Code:

import java.time.Duration;
import
java.util.concurrent.CountDownLatch;
import
java.util.concurrent.TimeUnit;

public class
GuavaTest {
   
private static int successCount;

    public static void
main(String[] args) throws InterruptedException {
       
final int MAX_REQUESTS_PER_SEC = 100;
       
/** Google un Guava Rate Limit API sinin örneğidir;
        * Örnek olarak, Bir saniyede 1 request çalışsın diye tanımlıyoruz
        * */
       
com.google.common.util.concurrent.RateLimiter guavaLimiter =
                com.google.common.util.concurrent.RateLimiter
                        .create(
1d, Duration.ofSeconds(1));

       
Thread requestThread = new Thread(() -> {
           
/*10 requesti 1 saniyede calistiralım;*/
           
sendRequestGuava(guavaLimiter, 10, 1);
       
});

       
requestThread.start();
       
requestThread.join();
   
}

   
private static void sendRequestGuava(com.google.common.util.concurrent.RateLimiter rateLimiter, int totalRequest, int requestPerSec) {
       
long startTime = System.currentTimeMillis();
       
/*totalCount kadar thread counter i baslatiyoruz;*/
       
CountDownLatch threadCounter = new CountDownLatch(totalRequest);
        for
(int i = 0; i < totalRequest; i++) {
           
try {
               
new Thread(() -> {
                   
boolean allow = rateLimiter.tryAcquire();
                    
System.out.println("guavaLimiter.tryAcquire() is " + allow);
                    if
(allow) {
                       
try {
                           
successCount++;
                           
System.out.println(" -> request calisti");
                       
} catch (Exception e) {
                            e.printStackTrace()
;
                       
}
                    }
                   
threadCounter.countDown();

               
}).start();
               
/**
                 * Thread leri bir saniyenin
4 te biri kadar bekletip, saneyide yaklaşık 3 request çalışmasını sağlıyoruz.
                 * */
               
TimeUnit.MILLISECONDS.sleep(250);
           
} catch (Exception e) {
                e.printStackTrace()
;
           
}
        }
       
try {
            threadCounter.await()
;
       
} catch (InterruptedException e) {
            e.printStackTrace()
;
       
}
       
double duration = (System.currentTimeMillis() - startTime) / 1000.0;
       
System.out.println("-------------------------------------------------------------------------------------");
       
System.out.println("Bir Saniyede ; " + (double) totalRequest / duration + " adet request istegi gonderildi.");
       
System.out.println(totalRequest + " adet request " + duration + " saniye icerisinde islendi. ");
       
System.out.println("Rate limit olarak; 1 sn de 1 requeste izin verildigi icin; "+totalRequest +" adet request ten " + successCount + " tanesi basarili oldu. Gerisi reject edildi.");
       
System.out.println("-------------------------------------------------------------------------------------");
   
}
}

 

Output:

guavaLimiter.tryAcquire() is true

 -> request calisti

guavaLimiter.tryAcquire() is false

guavaLimiter.tryAcquire() is false

guavaLimiter.tryAcquire() is false

guavaLimiter.tryAcquire() is false

guavaLimiter.tryAcquire() is false

guavaLimiter.tryAcquire() is true

 -> request calisti

guavaLimiter.tryAcquire() is false

guavaLimiter.tryAcquire() is false

guavaLimiter.tryAcquire() is false

----------------------------------------------------------------------------------------

Bir Saniyede ; 3.9277297721916735 adet request istegi gonderildi.

10 adet request 2.546 saniye icerisinde islendi.

Rate limit olarak; 1 sn de 1 requeste izin verildigi icin; 10 adet request ten 2 tanesi basarili oldu. Gerisi reject edildi.

-------------------------------------------------------------------------------------------

 

Sonuç :

Hız sınırlama algoritmaları; saniye başına transaction lisanslama kısıtı için, web servislerde aşırı yüklenme yi kontrol altına alabilmek için, her hangi bir uygulamanın kaynaklarını kullanıcılara adil olarak dağıtabilmek için, yada bir uygulamayı DDOS(Distributed Denial Of Services) saldırılarına karşı korumak için kullanılabilir. Örneğin REST web servislerinde ki requestleri kontrol altına almak için yukarıdaki basit kod örnekleri verilen algoritma ve API ler; yazılacak bir request filter classı içerisinde yada, bir RestController layer da kullanılabilir. Bu sayede hizmet veren web servislerin bottleneck e düşmesi, hang olması, kilitlenmesi, hizmet veremez hale gelmesi önlenmiş olur. Tabi yukarıda bahsettiğim hız limiti algoritma ve API si dışında da; Resilience4j API, Bucket4j API, open source API Gateway librarylerinden biri olan Kong Gateway, bunların dışında harici kullanılacak API Gateway ler, …vs. gibi hız limiti için çözüm yolları tercih edilebilir.

Kaynaklar:

·         https://medium.com/@aayushbhatnagar_10462/rate-limiting-implementation-example-in-java-7831923e5de3

·         https://konghq.com/blog/how-to-design-a-scalable-rate-limiting-algorithm/

·         https://hechao.li/2018/06/25/Rate-Limiter-Part1/

·         https://hechao.li/2018/06/27/Rate-Limiter-Part2/

·         https://www.figma.com/blog/an-alternative-approach-to-rate-limiting/

·         https://www.baeldung.com/spring-bucket4j

·         https://www.baeldung.com/guava-rate-limiter

·         https://github.com/google/guava

·         https://medium.com/teamarimac/implementing-throttling-in-java-spring-boot-ec4723cfce9f