Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Any example using Redis #160

Closed
ayoubAnbara opened this issue Apr 6, 2023 · 4 comments
Closed

Any example using Redis #160

ayoubAnbara opened this issue Apr 6, 2023 · 4 comments

Comments

@ayoubAnbara
Copy link

ayoubAnbara commented Apr 6, 2023

I am looking for an example using Redis.
Thank you.

@dgallego58
Copy link

dgallego58 commented Apr 12, 2023

i'm wondering the same, although i have found some example by doing it programatically and only with Redisson. I'd prefered to do it with lettuce due to ACL and permissions problems with AWS and some additional config that my team and me had to do with the VPC and the access connection via Role-Based Access Control (RBAC).

May this can help u:

What im using:

springBootVersion 3.0.5
java 17
awsSdkJavaV2

dependencies:

implementation 'org.redisson:redisson-spring-boot-starter:3.20.1'
implementation 'com.giffing.bucket4j.spring.boot.starter:bucket4j-spring-boot-starter:0.9.0'

as i said, i'd rather to do it with Lettuce Connection, and not with Redisson, but sadly spring does not provide a bean of
javax.cache.CacheManager but org.springframework.cache.CacheManager instead, so here is my config:

import com.giffing.bucket4j.spring.boot.starter.config.cache.SyncCacheResolver;
import com.giffing.bucket4j.spring.boot.starter.config.cache.jcache.JCacheCacheResolver;
import io.github.bucket4j.distributed.proxy.ProxyManager;
import io.github.bucket4j.grid.jcache.JCacheProxyManager;
import org.redisson.config.Config;
import org.redisson.config.ReadMode;
import org.redisson.jcache.configuration.RedissonConfiguration;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.boot.autoconfigure.cache.JCacheManagerCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.context.annotation.Profile;

import javax.cache.CacheManager;
import javax.cache.Caching;
import java.math.BigInteger;
import java.time.Duration;
import java.util.Collections;
import java.util.function.BinaryOperator;


@Configuration
@EnableCaching
public class RedisCacheRateLimitConfig{

    private static final String CACHE_NAME = "r_quota_";

    @Bean
    public Config envConfig(SecretsService secrets) throws JsonProcessingException {
        var port = secrets.get("port");
        var host = secrets.get("master_node");
        var hostread = secrets.get("replica_node");
        var username = secrets.get("username");
        var pass = secrets.get("password");
        BinaryOperator<String> getUrl = (h, p) -> String.format("redis://%s:%s", h, p);
        Config config = new Config();
        config.useReplicatedServers()
                .setReadMode(ReadMode.SLAVE)
                .setUsername(username)
                .setPassword(pass)
                .addNodeAddress(getUrl.apply(host, port))
                .addNodeAddress(getUrl.apply(hostread, port));
        return config;
    }

    @Bean(name = "javaxCacheManager") // naming this due to spring uses a bean named cacheManager so it cannot rise up some troubles
    public CacheManager cacheManager(Config config) {
        return Caching.getCachingProvider().getCacheManager();
    }

    @Bean
    public ProxyManager<String> proxyManager(CacheManager cache) {
        return new JCacheProxyManager<>(cache.getCache(CACHE_NAME));
    }

    // this rises an exception on start up since there is no resolver when there is a reactive stack to handle the app (netty, tomcat3 etc) when there are several cache names.
    @Bean
    @Primary
    public SyncCacheResolver bucket4jCacheResolver(CacheManager cacheManager) {
        return new JCacheCacheResolver(cacheManager);
    }

    /** this bean creates the cache where the keys are being associated to your bucket. im using here the customizer
   *  so if there is a spring bean which uses some redis connection or caching, can share the same config without rising up
   *  the exception
   *
   */
    @Bean
    public JCacheManagerCustomizer jCacheManagerCustomizer(Config config) {
        return cacheManager -> cacheManager.createCache(CACHE_NAME, RedissonConfiguration.fromConfig(config));
    }
}

once you get the connection you can just create the resolveBucket to handle your service.

in my case i used some configs based on Baeldung post - Rate Limit with Bucket4j

import io.github.bucket4j.Bandwidth;
import io.github.bucket4j.Bucket;
import io.github.bucket4j.BucketConfiguration;
import io.github.bucket4j.Refill;
import io.github.bucket4j.distributed.proxy.ProxyManager;
import org.springframework.stereotype.Service;

import java.time.Duration;
import java.util.function.Supplier;

@Service
public class RateLimitService {

    private static final int QUOTA = 20;
    private final ProxyManager<String> buckets;

    public RateLimitService(ProxyManager<String> buckets) {
        this.buckets = buckets;
    }

    public Bucket resolveBucket(String key) {
        Supplier<BucketConfiguration> confSupplier = () -> {
            var refill = Refill.intervally(QUOTA, Duration.ofHours(1));
            var limit = Bandwidth.classic(QUOTA, refill);
            return BucketConfiguration.builder()
                    .addLimit(limit)
                    .build();
        };
        return buckets.builder().build(key, confSupplier);
    }
}

then i use a Web Filter to handle request instead of an Interceptor, so i can order my filters with spring security's SecurityFilterChain.

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;
import java.math.BigInteger;

@Component //if u use this, it will be handler as global filter and will intercept every request
public class RateLimiterFilter extends OncePerRequestFilter {

    private static final String HEADER_LIMIT_REMAINING = "X-Rate-Limit-Remaining";
    private static final String HEADER_RETRY_AFTER = "X-Rate-Limit-Retry-After-Seconds";
    private static final int NANO_SECONDS = 1_000_000_000;
    private final RateLimitService rateLimitService;

    public RateLimiterFilter(RateLimitService rateLimitService) {
        super();
        this.rateLimitService = rateLimitService;
    }

    @Override
    protected void doFilterInternal(@NonNull HttpServletRequest request,
                                    @NonNull HttpServletResponse response,
                                    @NonNull FilterChain filterChain) throws ServletException, IOException {
        var key= request.getRemoteAddr();
        var bucket = rateLimitService.resolveBucket(key);
        var tokenConsumed = bucket.tryConsumeAndReturnRemaining(BigInteger.ONE.longValue());
        if (tokenConsumed.isConsumed()) {
            response.setHeader(HEADER_LIMIT_REMAINING, String.valueOf(tokenConsumed.getRemainingTokens()));
            filterChain.doFilter(request, response);
        } else {
            long waitToRefill = tokenConsumed.getNanosToWaitForRefill() / NANO_SECONDS;
            response.setContentType("application/json");
            response.addHeader(HEADER_LIMIT_REMAINING, String.valueOf(tokenConsumed.getRemainingTokens()));
            response.addHeader(HEADER_RETRY_AFTER, String.valueOf(waitToRefill));
            response.sendError(HttpStatus.TOO_MANY_REQUESTS.value(), "You have exhausted your API Request Quota");
        }
    }

}

hope it helps.

EDIT: spelling
EDIT 2: If u plan to use this as a Spring Security filter remember those filters shouldn't be Spring Beans (@service, @component... etc) see Registering Servlets, Filters, and Listeners as Spring Beans

@ayoubAnbara
Copy link
Author

ayoubAnbara commented Apr 20, 2023

Hi @dgallego58
I appreciate your help, I will test that.
Many thanks.

@ayoubAnbara ayoubAnbara reopened this Apr 20, 2023
MarcGiffing added a commit that referenced this issue Sep 29, 2023
@MarcGiffing
Copy link
Owner

I've updated the examples. Any suggestions for improvement are welcome.

@MarcGiffing
Copy link
Owner

Examples will be further improved in #202 and the overall spring-boot-starter experience including Redis maybe in #210

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants