mirror of
https://github.com/PlayEdu/PlayEdu
synced 2025-06-08 02:04:04 +08:00
parent
881e03310a
commit
192326bf7e
9
pom.xml
9
pom.xml
@ -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>
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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")
|
||||
|
@ -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")
|
||||
|
@ -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());
|
||||
|
@ -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();
|
||||
|
@ -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 "系统正在运行中...";
|
||||
}
|
||||
}
|
||||
|
@ -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("当前学员已锁定无法登录");
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
@ -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)
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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 {}
|
@ -32,11 +32,6 @@ import xyz.playedu.api.types.JsonResponse;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
/**
|
||||
* @Author 杭州白书科技有限公司
|
||||
*
|
||||
* @create 2023/2/21 16:42
|
||||
*/
|
||||
@Aspect
|
||||
@Component
|
||||
@Slf4j
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
@ -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);
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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
|
||||
|
6
src/main/resources/lua/RateLimiterScript.lua
Normal file
6
src/main/resources/lua/RateLimiterScript.lua
Normal 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
|
Loading…
x
Reference in New Issue
Block a user