diff --git a/pom.xml b/pom.xml index 862fc39..67366cb 100644 --- a/pom.xml +++ b/pom.xml @@ -5,12 +5,12 @@ org.springframework.boot spring-boot-starter-parent - 3.1.0 + 3.1.1 xyz.playedu playedu-api - 1.0-beta.7 + 1.1 playedu-api playedu-api @@ -132,11 +132,6 @@ hutool-core 5.8.16 - - cn.hutool - hutool-captcha - 5.8.16 - diff --git a/src/main/java/xyz/playedu/api/config/PlayEduConfig.java b/src/main/java/xyz/playedu/api/config/PlayEduConfig.java index 35d5d46..c514dba 100644 --- a/src/main/java/xyz/playedu/api/config/PlayEduConfig.java +++ b/src/main/java/xyz/playedu/api/config/PlayEduConfig.java @@ -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; } diff --git a/src/main/java/xyz/playedu/api/config/RedisConfig.java b/src/main/java/xyz/playedu/api/config/RedisConfig.java index 0ef34a7..cbcc1d6 100644 --- a/src/main/java/xyz/playedu/api/config/RedisConfig.java +++ b/src/main/java/xyz/playedu/api/config/RedisConfig.java @@ -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 rateLimiterScript() { + DefaultRedisScript script = new DefaultRedisScript<>(); + script.setScriptSource( + new ResourceScriptSource(new ClassPathResource("lua/RateLimiterScript.lua"))); + script.setResultType(Long.class); + return script; + } } diff --git a/src/main/java/xyz/playedu/api/controller/backend/AdminRoleController.java b/src/main/java/xyz/playedu/api/controller/backend/AdminRoleController.java index e64a4c7..b5abc18 100644 --- a/src/main/java/xyz/playedu/api/controller/backend/AdminRoleController.java +++ b/src/main/java/xyz/playedu/api/controller/backend/AdminRoleController.java @@ -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 diff --git a/src/main/java/xyz/playedu/api/controller/backend/CourseController.java b/src/main/java/xyz/playedu/api/controller/backend/CourseController.java index d4b9130..b67efd3 100644 --- a/src/main/java/xyz/playedu/api/controller/backend/CourseController.java +++ b/src/main/java/xyz/playedu/api/controller/backend/CourseController.java @@ -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") diff --git a/src/main/java/xyz/playedu/api/controller/backend/DepartmentController.java b/src/main/java/xyz/playedu/api/controller/backend/DepartmentController.java index f3bb170..b19a623 100644 --- a/src/main/java/xyz/playedu/api/controller/backend/DepartmentController.java +++ b/src/main/java/xyz/playedu/api/controller/backend/DepartmentController.java @@ -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") diff --git a/src/main/java/xyz/playedu/api/controller/backend/LoginController.java b/src/main/java/xyz/playedu/api/controller/backend/LoginController.java index e37fc7b..92499d7 100644 --- a/src/main/java/xyz/playedu/api/controller/backend/LoginController.java +++ b/src/main/java/xyz/playedu/api/controller/backend/LoginController.java @@ -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()); diff --git a/src/main/java/xyz/playedu/api/controller/backend/SystemController.java b/src/main/java/xyz/playedu/api/controller/backend/SystemController.java index 6e0f18f..c197630 100644 --- a/src/main/java/xyz/playedu/api/controller/backend/SystemController.java +++ b/src/main/java/xyz/playedu/api/controller/backend/SystemController.java @@ -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 data = new HashMap<>(); - data.put("key", imageCaptchaResult.getKey()); - data.put("image", imageCaptchaResult.getImage()); - - return JsonResponse.data(data); - } - @GetMapping("/config") public JsonResponse config() { Map configData = BCtx.getConfig(); diff --git a/src/main/java/xyz/playedu/api/controller/frontend/IndexController.java b/src/main/java/xyz/playedu/api/controller/frontend/IndexController.java index 6de4065..af4a71f 100644 --- a/src/main/java/xyz/playedu/api/controller/frontend/IndexController.java +++ b/src/main/java/xyz/playedu/api/controller/frontend/IndexController.java @@ -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 "系统正在运行中..."; } } diff --git a/src/main/java/xyz/playedu/api/controller/frontend/LoginController.java b/src/main/java/xyz/playedu/api/controller/frontend/LoginController.java index aaec818..92075b6 100644 --- a/src/main/java/xyz/playedu/api/controller/frontend/LoginController.java +++ b/src/main/java/xyz/playedu/api/controller/frontend/LoginController.java @@ -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("当前学员已锁定无法登录"); } diff --git a/src/main/java/xyz/playedu/api/controller/frontend/SystemController.java b/src/main/java/xyz/playedu/api/controller/frontend/SystemController.java index f2d729e..49c5d36 100644 --- a/src/main/java/xyz/playedu/api/controller/frontend/SystemController.java +++ b/src/main/java/xyz/playedu/api/controller/frontend/SystemController.java @@ -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 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 data = new HashMap<>(); - data.put("key", imageCaptchaResult.getKey()); - data.put("image", imageCaptchaResult.getImage()); - - return JsonResponse.data(data); - } } diff --git a/src/main/java/xyz/playedu/api/controller/frontend/UserController.java b/src/main/java/xyz/playedu/api/controller/frontend/UserController.java index 97b7fab..8acf4f1 100644 --- a/src/main/java/xyz/playedu/api/controller/frontend/UserController.java +++ b/src/main/java/xyz/playedu/api/controller/frontend/UserController.java @@ -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 diff --git a/src/main/java/xyz/playedu/api/middleware/AdminMiddleware.java b/src/main/java/xyz/playedu/api/middleware/AdminMiddleware.java index dc81cbb..359d365 100644 --- a/src/main/java/xyz/playedu/api/middleware/AdminMiddleware.java +++ b/src/main/java/xyz/playedu/api/middleware/AdminMiddleware.java @@ -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 systemConfig = configService.keyValues(); BCtx.setConfig(systemConfig); diff --git a/src/main/java/xyz/playedu/api/middleware/BackendPermissionMiddleware.java b/src/main/java/xyz/playedu/api/middleware/BackendPermissionMiddleware.java index c1a865c..92aea9a 100644 --- a/src/main/java/xyz/playedu/api/middleware/BackendPermissionMiddleware.java +++ b/src/main/java/xyz/playedu/api/middleware/BackendPermissionMiddleware.java @@ -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) diff --git a/src/main/java/xyz/playedu/api/middleware/FrontMiddleware.java b/src/main/java/xyz/playedu/api/middleware/FrontMiddleware.java index 1036770..dd52ba9 100644 --- a/src/main/java/xyz/playedu/api/middleware/FrontMiddleware.java +++ b/src/main/java/xyz/playedu/api/middleware/FrontMiddleware.java @@ -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); } diff --git a/src/main/java/xyz/playedu/api/middleware/ImageCaptchaCheckMiddleware.java b/src/main/java/xyz/playedu/api/middleware/ImageCaptchaCheckMiddleware.java deleted file mode 100644 index 799834b..0000000 --- a/src/main/java/xyz/playedu/api/middleware/ImageCaptchaCheckMiddleware.java +++ /dev/null @@ -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 {} diff --git a/src/main/java/xyz/playedu/api/middleware/impl/BackendPermissionMiddlewareImpl.java b/src/main/java/xyz/playedu/api/middleware/impl/BackendPermissionMiddlewareImpl.java index 1d14025..69ce647 100644 --- a/src/main/java/xyz/playedu/api/middleware/impl/BackendPermissionMiddlewareImpl.java +++ b/src/main/java/xyz/playedu/api/middleware/impl/BackendPermissionMiddlewareImpl.java @@ -32,11 +32,6 @@ import xyz.playedu.api.types.JsonResponse; import java.util.HashMap; -/** - * @Author 杭州白书科技有限公司 - * - * @create 2023/2/21 16:42 - */ @Aspect @Component @Slf4j diff --git a/src/main/java/xyz/playedu/api/middleware/impl/ImageCaptchaCheckMiddlewareImpl.java b/src/main/java/xyz/playedu/api/middleware/impl/ImageCaptchaCheckMiddlewareImpl.java deleted file mode 100644 index 50501ca..0000000 --- a/src/main/java/xyz/playedu/api/middleware/impl/ImageCaptchaCheckMiddlewareImpl.java +++ /dev/null @@ -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(); - } -} diff --git a/src/main/java/xyz/playedu/api/request/backend/LoginRequest.java b/src/main/java/xyz/playedu/api/request/backend/LoginRequest.java index 51745fb..05799bd 100644 --- a/src/main/java/xyz/playedu/api/request/backend/LoginRequest.java +++ b/src/main/java/xyz/playedu/api/request/backend/LoginRequest.java @@ -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; } diff --git a/src/main/java/xyz/playedu/api/request/frontend/LoginPasswordRequest.java b/src/main/java/xyz/playedu/api/request/frontend/LoginPasswordRequest.java index 3d69777..9a31af1 100644 --- a/src/main/java/xyz/playedu/api/request/frontend/LoginPasswordRequest.java +++ b/src/main/java/xyz/playedu/api/request/frontend/LoginPasswordRequest.java @@ -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; } diff --git a/src/main/java/xyz/playedu/api/service/ImageCaptchaService.java b/src/main/java/xyz/playedu/api/service/ImageCaptchaService.java deleted file mode 100644 index b1b2dd5..0000000 --- a/src/main/java/xyz/playedu/api/service/ImageCaptchaService.java +++ /dev/null @@ -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); -} diff --git a/src/main/java/xyz/playedu/api/request/backend/types/ImageCaptchaRequestInterface.java b/src/main/java/xyz/playedu/api/service/RateLimiterService.java similarity index 80% rename from src/main/java/xyz/playedu/api/request/backend/types/ImageCaptchaRequestInterface.java rename to src/main/java/xyz/playedu/api/service/RateLimiterService.java index e84f959..16d3097 100644 --- a/src/main/java/xyz/playedu/api/request/backend/types/ImageCaptchaRequestInterface.java +++ b/src/main/java/xyz/playedu/api/service/RateLimiterService.java @@ -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); } diff --git a/src/main/java/xyz/playedu/api/service/impl/ImageCaptchaServiceImpl.java b/src/main/java/xyz/playedu/api/service/impl/ImageCaptchaServiceImpl.java deleted file mode 100644 index 9b7be22..0000000 --- a/src/main/java/xyz/playedu/api/service/impl/ImageCaptchaServiceImpl.java +++ /dev/null @@ -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; - } -} diff --git a/src/main/java/xyz/playedu/api/bus/AppBus.java b/src/main/java/xyz/playedu/api/service/impl/RateLimiterServiceImpl.java similarity index 51% rename from src/main/java/xyz/playedu/api/bus/AppBus.java rename to src/main/java/xyz/playedu/api/service/impl/RateLimiterServiceImpl.java index de9dc5f..09648c5 100644 --- a/src/main/java/xyz/playedu/api/bus/AppBus.java +++ b/src/main/java/xyz/playedu/api/service/impl/RateLimiterServiceImpl.java @@ -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 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; } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 27b21d4..220cd27 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -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 diff --git a/src/main/resources/lua/RateLimiterScript.lua b/src/main/resources/lua/RateLimiterScript.lua new file mode 100644 index 0000000..8c8ba50 --- /dev/null +++ b/src/main/resources/lua/RateLimiterScript.lua @@ -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 \ No newline at end of file