!2 移除图形验证码 && api限流 && 账户限流

* 账户登录限流
* added: api限流
* 移除图形验证码
This commit is contained in:
白书科技 2023-07-03 06:31:53 +00:00
parent 881e03310a
commit 192326bf7e
26 changed files with 111 additions and 325 deletions

View File

@ -5,12 +5,12 @@
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.0</version>
<version>3.1.1</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>xyz.playedu</groupId>
<artifactId>playedu-api</artifactId>
<version>1.0-beta.7</version>
<version>1.1</version>
<name>playedu-api</name>
<description>playedu-api</description>
<properties>
@ -132,11 +132,6 @@
<artifactId>hutool-core</artifactId>
<version>5.8.16</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-captcha</artifactId>
<version>5.8.16</version>
</dependency>
<!-- Sa-Token 权限认证在线文档https://sa-token.cc -->
<dependency>

View File

@ -26,4 +26,10 @@ public class PlayEduConfig {
@Value("${spring.profiles.active}")
private String env;
@Value("${playedu.limiter.duration}")
private Long limiterDuration;
@Value("${playedu.limiter.limit}")
private Long limiterLimit;
}

View File

@ -17,10 +17,14 @@ package xyz.playedu.api.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.scripting.support.ResourceScriptSource;
@Configuration
public class RedisConfig {
@ -43,4 +47,13 @@ public class RedisConfig {
return redisTemplate;
}
@Bean(name = "rateLimiterScript")
public RedisScript<Long> rateLimiterScript() {
DefaultRedisScript<Long> script = new DefaultRedisScript<>();
script.setScriptSource(
new ResourceScriptSource(new ClassPathResource("lua/RateLimiterScript.lua")));
script.setResultType(Long.class);
return script;
}
}

View File

@ -38,11 +38,6 @@ import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* @Author 杭州白书科技有限公司
*
* @create 2023/2/21 15:56
*/
@RestController
@RequestMapping("/backend/v1/admin-role")
@Slf4j

View File

@ -40,11 +40,6 @@ import java.text.ParseException;
import java.util.*;
import java.util.stream.Collectors;
/**
* @Author 杭州白书科技有限公司
*
* @create 2023/2/24 14:16
*/
@RestController
@Slf4j
@RequestMapping("/backend/v1/course")

View File

@ -44,11 +44,6 @@ import xyz.playedu.api.types.paginate.UserPaginateFilter;
import java.util.*;
import java.util.stream.Collectors;
/**
* @Author 杭州白书科技有限公司
*
* @create 2023/2/19 10:33
*/
@RestController
@Slf4j
@RequestMapping("/backend/v1/department")

View File

@ -26,14 +26,15 @@ import xyz.playedu.api.constant.BPermissionConstant;
import xyz.playedu.api.domain.AdminUser;
import xyz.playedu.api.event.AdminUserLoginEvent;
import xyz.playedu.api.middleware.BackendPermissionMiddleware;
import xyz.playedu.api.middleware.ImageCaptchaCheckMiddleware;
import xyz.playedu.api.request.backend.LoginRequest;
import xyz.playedu.api.request.backend.PasswordChangeRequest;
import xyz.playedu.api.service.AdminUserService;
import xyz.playedu.api.service.BackendAuthService;
import xyz.playedu.api.service.RateLimiterService;
import xyz.playedu.api.types.JsonResponse;
import xyz.playedu.api.util.HelperUtil;
import xyz.playedu.api.util.IpUtil;
import xyz.playedu.api.util.RedisUtil;
import xyz.playedu.api.util.RequestUtil;
import java.util.HashMap;
@ -50,20 +51,31 @@ public class LoginController {
@Autowired private ApplicationContext ctx;
@Autowired private RateLimiterService rateLimiterService;
@PostMapping("/login")
@ImageCaptchaCheckMiddleware
public JsonResponse login(@RequestBody @Validated LoginRequest loginRequest) {
AdminUser adminUser = adminUserService.findByEmail(loginRequest.email);
if (adminUser == null) {
return JsonResponse.error("邮箱或密码错误");
}
String limitKey = "admin-login-limit:" + loginRequest.getEmail();
Long reqCount = rateLimiterService.current(limitKey, 3600L);
if (reqCount > 5) {
return JsonResponse.error("连续五次错误,请稍后再试");
}
String password =
HelperUtil.MD5(loginRequest.getPassword() + adminUser.getSalt()).toLowerCase();
if (!adminUser.getPassword().equals(password)) {
return JsonResponse.error("邮箱或密码错误");
}
RedisUtil.del(limitKey);
if (adminUser.getIsBanLogin().equals(1)) {
return JsonResponse.error("当前用户已禁止登录");
return JsonResponse.error("当前管理员已禁止登录");
}
String token = authService.loginUsingId(adminUser.getId(), RequestUtil.url());

View File

@ -17,19 +17,15 @@ package xyz.playedu.api.controller.backend;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import xyz.playedu.api.BCtx;
import xyz.playedu.api.constant.CConfig;
import xyz.playedu.api.service.ImageCaptchaService;
import xyz.playedu.api.types.ImageCaptchaResult;
import xyz.playedu.api.types.JsonResponse;
import xyz.playedu.api.util.RequestUtil;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@ -40,19 +36,6 @@ import java.util.Map;
@Slf4j
public class SystemController {
@Autowired private ImageCaptchaService imageCaptchaService;
@GetMapping("/image-captcha")
public JsonResponse imageCaptcha() throws IOException {
ImageCaptchaResult imageCaptchaResult = imageCaptchaService.generate();
HashMap<String, String> data = new HashMap<>();
data.put("key", imageCaptchaResult.getKey());
data.put("image", imageCaptchaResult.getImage());
return JsonResponse.data(data);
}
@GetMapping("/config")
public JsonResponse config() {
Map<String, String> configData = BCtx.getConfig();

View File

@ -18,17 +18,10 @@ package xyz.playedu.api.controller.frontend;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import xyz.playedu.api.types.JsonResponse;
/**
* @Author 杭州白书科技有限公司
*
* @create 2023/3/24 17:42
*/
@RestController
public class IndexController {
@GetMapping("/")
public JsonResponse index() {
return JsonResponse.success();
public String index() {
return "系统正在运行中...";
}
}

View File

@ -28,13 +28,14 @@ import xyz.playedu.api.domain.User;
import xyz.playedu.api.event.UserLoginEvent;
import xyz.playedu.api.event.UserLogoutEvent;
import xyz.playedu.api.exception.LimitException;
import xyz.playedu.api.middleware.ImageCaptchaCheckMiddleware;
import xyz.playedu.api.request.frontend.LoginPasswordRequest;
import xyz.playedu.api.service.FrontendAuthService;
import xyz.playedu.api.service.RateLimiterService;
import xyz.playedu.api.service.UserService;
import xyz.playedu.api.types.JsonResponse;
import xyz.playedu.api.util.HelperUtil;
import xyz.playedu.api.util.IpUtil;
import xyz.playedu.api.util.RedisUtil;
import xyz.playedu.api.util.RequestUtil;
import java.util.HashMap;
@ -49,8 +50,9 @@ public class LoginController {
@Autowired private ApplicationContext ctx;
@Autowired private RateLimiterService rateLimiterService;
@PostMapping("/password")
@ImageCaptchaCheckMiddleware
public JsonResponse password(@RequestBody @Validated LoginPasswordRequest req)
throws LimitException {
String email = req.getEmail();
@ -59,9 +61,19 @@ public class LoginController {
if (user == null) {
return JsonResponse.error("邮箱或密码错误");
}
String limitKey = "login-limit:" + req.getEmail();
Long reqCount = rateLimiterService.current(limitKey, 600L);
if (reqCount >= 10) {
return JsonResponse.error("登录失败次数过多,请稍候再试");
}
if (!HelperUtil.MD5(req.getPassword() + user.getSalt()).equals(user.getPassword())) {
return JsonResponse.error("邮箱或密码错误");
}
RedisUtil.del(limitKey);
if (user.getIsLock() == 1) {
return JsonResponse.error("当前学员已锁定无法登录");
}

View File

@ -22,27 +22,17 @@ import org.springframework.web.bind.annotation.RestController;
import xyz.playedu.api.constant.CConfig;
import xyz.playedu.api.service.AppConfigService;
import xyz.playedu.api.service.ImageCaptchaService;
import xyz.playedu.api.types.ImageCaptchaResult;
import xyz.playedu.api.types.JsonResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
* @Author 杭州白书科技有限公司
*
* @create 2023/3/13 11:26
*/
@RestController
@RequestMapping("/api/v1/system")
public class SystemController {
@Autowired private AppConfigService appConfigService;
@Autowired private ImageCaptchaService imageCaptchaService;
@GetMapping("/config")
public JsonResponse config() {
Map<String, String> configs = appConfigService.keyValues();
@ -65,15 +55,4 @@ public class SystemController {
return JsonResponse.data(data);
}
@GetMapping("/image-captcha")
public JsonResponse imageCaptcha() throws IOException {
ImageCaptchaResult imageCaptchaResult = imageCaptchaService.generate();
HashMap<String, String> data = new HashMap<>();
data.put("key", imageCaptchaResult.getKey());
data.put("image", imageCaptchaResult.getImage());
return JsonResponse.data(data);
}
}

View File

@ -37,11 +37,6 @@ import xyz.playedu.api.util.PrivacyUtil;
import java.util.*;
import java.util.stream.Collectors;
/**
* @Author 杭州白书科技有限公司
*
* @create 2023/3/13 09:21
*/
@RestController
@RequestMapping("/api/v1/user")
@Slf4j

View File

@ -25,14 +25,16 @@ import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import xyz.playedu.api.BCtx;
import xyz.playedu.api.bus.AppBus;
import xyz.playedu.api.bus.BackendBus;
import xyz.playedu.api.config.PlayEduConfig;
import xyz.playedu.api.domain.AdminUser;
import xyz.playedu.api.service.AdminUserService;
import xyz.playedu.api.service.AppConfigService;
import xyz.playedu.api.service.BackendAuthService;
import xyz.playedu.api.service.RateLimiterService;
import xyz.playedu.api.types.JsonResponse;
import xyz.playedu.api.util.HelperUtil;
import xyz.playedu.api.util.IpUtil;
import java.io.IOException;
import java.util.Map;
@ -45,12 +47,14 @@ public class AdminMiddleware implements HandlerInterceptor {
@Autowired private AdminUserService adminUserService;
@Autowired private AppBus appBus;
@Autowired private BackendBus backendBus;
@Autowired private AppConfigService configService;
@Autowired private RateLimiterService rateLimiterService;
@Autowired private PlayEduConfig playEduConfig;
@Override
public boolean preHandle(
HttpServletRequest request, HttpServletResponse response, Object handler)
@ -59,6 +63,12 @@ public class AdminMiddleware implements HandlerInterceptor {
return HandlerInterceptor.super.preHandle(request, response, handler);
}
String reqCountKey = "api-limiter:" + IpUtil.getIpAddress();
Long reqCount = rateLimiterService.current(reqCountKey, playEduConfig.getLimiterDuration());
if (reqCount > playEduConfig.getLimiterLimit()) {
return responseTransform(response, 429, "太多请求");
}
// 读取全局配置
Map<String, String> systemConfig = configService.keyValues();
BCtx.setConfig(systemConfig);

View File

@ -17,11 +17,6 @@ package xyz.playedu.api.middleware;
import java.lang.annotation.*;
/**
* @Author 杭州白书科技有限公司
*
* @create 2023/2/21 16:40
*/
@Documented
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)

View File

@ -25,12 +25,15 @@ import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import xyz.playedu.api.FCtx;
import xyz.playedu.api.config.PlayEduConfig;
import xyz.playedu.api.constant.FrontendConstant;
import xyz.playedu.api.domain.User;
import xyz.playedu.api.service.FrontendAuthService;
import xyz.playedu.api.service.RateLimiterService;
import xyz.playedu.api.service.UserService;
import xyz.playedu.api.types.JsonResponse;
import xyz.playedu.api.util.HelperUtil;
import xyz.playedu.api.util.IpUtil;
import java.io.IOException;
@ -42,6 +45,10 @@ public class FrontMiddleware implements HandlerInterceptor {
@Autowired private UserService userService;
@Autowired private RateLimiterService rateLimiterService;
@Autowired private PlayEduConfig playEduConfig;
@Override
public boolean preHandle(
HttpServletRequest request, HttpServletResponse response, Object handler)
@ -50,6 +57,12 @@ public class FrontMiddleware implements HandlerInterceptor {
return HandlerInterceptor.super.preHandle(request, response, handler);
}
String reqCountKey = "api-limiter:" + IpUtil.getIpAddress();
Long reqCount = rateLimiterService.current(reqCountKey, playEduConfig.getLimiterDuration());
if (reqCount > playEduConfig.getLimiterLimit()) {
return responseTransform(response, 429, "太多请求");
}
if (FrontendConstant.UN_AUTH_URI_WHITELIST.contains(request.getRequestURI())) {
return HandlerInterceptor.super.preHandle(request, response, handler);
}

View File

@ -1,23 +0,0 @@
/*
* Copyright (C) 2023 杭州白书科技有限公司
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.playedu.api.middleware;
import java.lang.annotation.*;
@Documented
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ImageCaptchaCheckMiddleware {}

View File

@ -32,11 +32,6 @@ import xyz.playedu.api.types.JsonResponse;
import java.util.HashMap;
/**
* @Author 杭州白书科技有限公司
*
* @create 2023/2/21 16:42
*/
@Aspect
@Component
@Slf4j

View File

@ -1,49 +0,0 @@
/*
* Copyright (C) 2023 杭州白书科技有限公司
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.playedu.api.middleware.impl;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import xyz.playedu.api.request.backend.types.ImageCaptchaRequestInterface;
import xyz.playedu.api.service.ImageCaptchaService;
import xyz.playedu.api.types.JsonResponse;
@Aspect
@Component
@Slf4j
public class ImageCaptchaCheckMiddlewareImpl {
@Autowired private ImageCaptchaService imageCaptchaService;
@Pointcut("@annotation(xyz.playedu.api.middleware.ImageCaptchaCheckMiddleware)")
private void doPointcut() {}
@Around("doPointcut()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
ImageCaptchaRequestInterface request =
(ImageCaptchaRequestInterface) joinPoint.getArgs()[0];
if (!imageCaptchaService.verify(request.getCaptchaKey(), request.getCaptchaValue())) {
return JsonResponse.error("图形验证码错误");
}
return joinPoint.proceed();
}
}

View File

@ -15,19 +15,15 @@
*/
package xyz.playedu.api.request.backend;
import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import xyz.playedu.api.request.backend.types.ImageCaptchaRequestInterface;
import java.io.Serial;
import java.io.Serializable;
@Data
public class LoginRequest implements Serializable, ImageCaptchaRequestInterface {
public class LoginRequest implements Serializable {
@Serial private static final long serialVersionUID = 1L;
@ -36,12 +32,4 @@ public class LoginRequest implements Serializable, ImageCaptchaRequestInterface
@NotNull(message = "请输入密码")
public String password;
@NotNull(message = "请输入图形验证码")
@JsonProperty("captcha_value")
public String captchaValue;
@NotNull(message = "captchaKey参数为空")
@JsonProperty("captcha_key")
public String captchaKey;
}

View File

@ -15,33 +15,16 @@
*/
package xyz.playedu.api.request.frontend;
import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import xyz.playedu.api.request.backend.types.ImageCaptchaRequestInterface;
/**
* @Author 杭州白书科技有限公司
*
* @create 2023/3/10 13:13
*/
@Data
public class LoginPasswordRequest implements ImageCaptchaRequestInterface {
public class LoginPasswordRequest {
@NotBlank(message = "请输入邮箱")
private String email;
@NotBlank(message = "请输入密码")
private String password;
@NotBlank(message = "请输入验证码")
@JsonProperty("captcha_key")
private String captchaKey;
@NotBlank(message = "请输入验证码")
@JsonProperty("captcha_val")
private String captchaValue;
}

View File

@ -1,27 +0,0 @@
/*
* Copyright (C) 2023 杭州白书科技有限公司
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.playedu.api.service;
import xyz.playedu.api.types.ImageCaptchaResult;
import java.io.IOException;
public interface ImageCaptchaService {
ImageCaptchaResult generate() throws IOException;
boolean verify(String key, String code);
}

View File

@ -13,11 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.playedu.api.request.backend.types;
package xyz.playedu.api.service;
public interface ImageCaptchaRequestInterface {
public interface RateLimiterService {
String getCaptchaValue();
String getCaptchaKey();
public Long current(String key, Long seconds);
}

View File

@ -1,80 +0,0 @@
/*
* Copyright (C) 2023 杭州白书科技有限公司
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.playedu.api.service.impl;
import cn.hutool.captcha.CaptchaUtil;
import cn.hutool.captcha.LineCaptcha;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import xyz.playedu.api.service.ImageCaptchaService;
import xyz.playedu.api.types.ImageCaptchaResult;
import xyz.playedu.api.util.HelperUtil;
import xyz.playedu.api.util.RedisUtil;
@Slf4j
@Service
public class ImageCaptchaServiceImpl implements ImageCaptchaService {
@Value("${playedu.captcha.cache-prefix}")
private String ConfigCachePrefix;
@Value("${playedu.captcha.expire}")
private Long ConfigExpire;
@Override
public ImageCaptchaResult generate() {
ImageCaptchaResult imageCaptcha = new ImageCaptchaResult();
// 生成验证码
LineCaptcha lineCaptcha = CaptchaUtil.createLineCaptcha(240, 100, 4, 1);
// 图形验证码的key[api是无状态的需要key来锁定验证码的值]
String randomKey = HelperUtil.randomString(16);
imageCaptcha.setKey(randomKey);
imageCaptcha.setCode(lineCaptcha.getCode());
imageCaptcha.setImage("data:image/png;base64," + lineCaptcha.getImageBase64());
// 写入到redis中
RedisUtil.set(getCacheKey(imageCaptcha.getKey()), imageCaptcha.getCode(), ConfigExpire);
return imageCaptcha;
}
@Override
public boolean verify(String key, String code) {
String cacheKey = getCacheKey(key);
Object queryResult = RedisUtil.get(cacheKey);
if (queryResult == null) { // 未查找到[已过期 | 不存在]
return false;
}
String cacheValue = (String) queryResult;
boolean verifyResult = cacheValue.equalsIgnoreCase(code);
if (verifyResult) { // 验证成功删除缓存->防止多次使用
RedisUtil.del(cacheKey);
}
return verifyResult;
}
private String getCacheKey(String val) {
return ConfigCachePrefix + val;
}
}

View File

@ -13,25 +13,32 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.playedu.api.bus;
package xyz.playedu.api.service.impl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Component;
import xyz.playedu.api.config.PlayEduConfig;
import xyz.playedu.api.constant.SystemConstant;
import xyz.playedu.api.service.RateLimiterService;
import xyz.playedu.api.util.RedisUtil;
import java.util.Arrays;
/**
* @Author 杭州白书科技有限公司
*
* @create 2023/2/19 12:06
*/
@Component
public class AppBus {
@Slf4j
public class RateLimiterServiceImpl implements RateLimiterService {
@Autowired private PlayEduConfig playEduConfig;
@Autowired
@Qualifier("rateLimiterScript")
private RedisScript<Long> redisScript;
public boolean isDev() {
return !playEduConfig.getEnv().equals(SystemConstant.ENV_PROD);
@Override
public Long current(String key, Long seconds) {
Long current = RedisUtil.handler().execute(redisScript, Arrays.asList(key, seconds + ""));
log.info("key={},count={}", key, current);
return current;
}
}

View File

@ -64,10 +64,7 @@ sa-token:
jwt-secret-key: "playeduxyz"
token-prefix: "Bearer"
# PlayEdu
playedu:
# 图形验证码
captcha:
expire: 300 #有效期[单位:秒,默认5分钟]
cache-prefix: "captcha:key:" #存储key的前缀
limiter:
duration: 60
limit: 120

View File

@ -0,0 +1,6 @@
local current
current = redis.call("incr", KEYS[1])
if tonumber(current) == 1 then
redis.call("expire", KEYS[1], KEYS[2])
end
return current