{"record":{"id":"4a3efb190e535a06","repo":"elunez/eladmin","slug":"error","errorCode":null,"errorMessage":"访问次数受限制","messagePattern":"访问次数受限制","errorType":"exception","errorClass":"BadRequestException","httpStatus":400,"severity":"warning","filePath":"eladmin-common/src/main/java/me/zhengjie/aspect/LimitAspect.java","lineNumber":81,"sourceCode":"        String key = limit.key();\n        if (StringUtils.isEmpty(key)) {\n            if (limitType == LimitType.IP) {\n                key = StringUtils.getIp(request);\n            } else {\n                key = signatureMethod.getName();\n            }\n        }\n\n        ImmutableList<Object> keys = ImmutableList.of(StringUtils.join(limit.prefix(), \"_\", key, \"_\", request.getRequestURI().replace(\"/\",\"_\")));\n\n        String luaScript = buildLuaScript();\n        RedisScript<Long> redisScript = new DefaultRedisScript<>(luaScript, Long.class);\n        Long count = redisTemplate.execute(redisScript, keys, limit.count(), limit.period());\n        if (ObjUtil.isNotNull(count) && count.intValue() <= limit.count()) {\n            logger.info(\"第{}次访问key为 {}，描述为 [{}] 的接口\", count, keys, limit.name());\n            return joinPoint.proceed();\n        } else {\n            throw new BadRequestException(\"访问次数受限制\");\n        }\n    }\n\n    /**\n     * 限流脚本\n     */\n    private String buildLuaScript() {\n        return \"local c\" +\n                \"\\nc = redis.call('get',KEYS[1])\" +\n                \"\\nif c and tonumber(c) > tonumber(ARGV[1]) then\" +\n                \"\\nreturn c;\" +\n                \"\\nend\" +\n                \"\\nc = redis.call('incr',KEYS[1])\" +\n                \"\\nif tonumber(c) == 1 then\" +\n                \"\\nredis.call('expire',KEYS[1],ARGV[2])\" +\n                \"\\nend\" +\n                \"\\nreturn c;\";\n    }","sourceCodeStart":63,"sourceCodeEnd":99,"githubUrl":"https://github.com/elunez/eladmin/blob/55fbf705956949697dbd68bf9003776609d3d029/eladmin-common/src/main/java/me/zhengjie/aspect/LimitAspect.java#L63-L99","documentation":"Thrown by the @Limit rate-limiting aspect (LimitAspect) when a Redis-backed Lua counter shows the caller has already hit an annotated interface more times than allowed within limit.period() seconds. The aspect builds a key from the limit prefix, the caller's IP/key, and the request URI, then increments/reads it in Redis. Exceeding the quota aborts the join point with BadRequestException instead of proceeding.","triggerScenarios":"Calling a REST endpoint annotated with @Limit (e.g. POST /auth/login with the default limit) more than limit.count() times within limit.period() seconds from the same key (IP or EL-TOKEN header per LimitType). Also thrown if Redis returns a count already greater than the configured threshold (the Lua script returns early when c > ARGV[1]).","commonSituations":"Automated login attempts or brute-force password guessing triggering the login rate limit; frontend retry storms hammering a limited endpoint; load tests without raising the limit; a misconfigured or shared Redis where counters from several instances accumulate on the same key.","solutions":["Wait for limit.period() seconds so the Redis key expires, then retry the request.","If the endpoint legitimately needs more throughput, raise count/period on its @Limit annotation (e.g. @Limit(key = \"login\", count = 20, period = 60)).","Verify the Redis connection (spring.redis.host/port) is correct and not shared with another environment that inflates the counter key.","For load/performance tests, call the endpoint with a different EL-TOKEN header key or bypass the aspect in the test profile."],"exampleFix":"// before\n@Limit(key = \"login\", period = 60, count = 5, name = \"登录接口限流\")\n@PostMapping(value = \"/login\")\npublic ResponseEntity<Object> login(...) { ... }\n\n// after (raise quota for legitimate traffic)\n@Limit(key = \"login\", period = 60, count = 20, name = \"登录接口限流\")\n@PostMapping(value = \"/login\")\npublic ResponseEntity<Object> login(...) { ... }","handlingStrategy":"retry","validationCode":"// Before a burst of calls, check remaining quota is plausible: simply space calls\n// so at most limit.count() requests per limit.period() seconds hit the endpoint.\n// e.g. for @Limit(count=5, period=60): at most 5 calls / 60s per key.\nlong minIntervalMs = (period * 1000L) / count;\nThread.sleep(minIntervalMs); // naive client-side pacing for scripts","typeGuard":null,"tryCatchPattern":"// HttpClient-ish pseudo: catch 400 with the rate-limit message, honor Retry-After-like delay\ntry {\n    return post(\"/auth/login\", body);\n} catch (BadRequestException e) {\n    if (e.getMessage().contains(\"访问次数受限制\")) {\n        return scheduleRetry(Duration.ofSeconds(limitPeriod)); // wait one window, then retry once\n    }\n    throw e;\n}","preventionTips":["Read the @Limit annotation (count/period/LimitType) on any endpoint you integrate with and pace clients below it.","For load tests, use distinct EL-TOKEN header keys per virtual user when LimitType is TOKEN-based.","Do not auto-retry instantly on 400 rate-limit responses — back off for at least limit.period() seconds.","Keep per-environment Redis instances separate so counters do not collide."],"tags":["rate-limit","redis","aop","eladmin","http-429"],"backgroundTag":null,"analyzedSha":"55fbf705956949697dbd68bf9003776609d3d029","analyzedAt":"2026-08-14T11:56:12.758Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}