From 2813e98d9ee9c5576f8c4e244bfdf3068bffdffb Mon Sep 17 00:00:00 2001 From: none Date: Wed, 15 Feb 2023 16:54:03 +0800 Subject: [PATCH] =?UTF-8?q?=E5=9B=BE=E5=BD=A2=E9=AA=8C=E8=AF=81=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 +- pom.xml | 28 + .../playedu/api/PlayeduApiApplication.java | 7 +- .../xyz/playedu/api/config/KaptchaConfig.java | 42 ++ .../xyz/playedu/api/config/PlayEduConfig.java | 5 + .../xyz/playedu/api/config/RedisConfig.java | 28 + .../api/controller/ExceptionController.java | 12 +- .../controller/admin/AdminUserController.java | 7 +- .../api/controller/admin/LoginController.java | 31 +- .../controller/admin/SystemController.java | 32 + .../xyz/playedu/api/domain/AdminUser.java | 8 + .../playedu/api/mapper/AdminUserMapper.java | 4 +- .../ImageCaptchaCheckMiddleware.java | 8 + .../impl/ImageCaptchaCheckMiddlewareImpl.java | 33 + .../xyz/playedu/api/request/LoginRequest.java | 37 +- .../playedu/api/service/AdminUserService.java | 12 +- .../api/service/ImageCaptchaService.java | 13 + .../service/impl/AdminUserServiceImpl.java | 14 +- .../service/impl/ImageCaptchaServiceImpl.java | 75 ++ .../types/ImageCaptchaRequestInterface.java | 8 + .../playedu/api/types/ImageCaptchaResult.java | 13 + .../xyz/playedu/api/types/JsonResponse.java | 49 +- .../java/xyz/playedu/api/util/Base64Util.java | 263 +++++++ .../java/xyz/playedu/api/util/EnvUtil.java | 37 + .../java/xyz/playedu/api/util/HttpUtil.java | 220 ++++++ .../java/xyz/playedu/api/util/IpUtil.java | 263 +++++++ .../java/xyz/playedu/api/util/MD5Util.java | 12 + .../java/xyz/playedu/api/util/RedisUtil.java | 662 ++++++++++++++++++ .../xyz/playedu/api/util/RequestUtil.java | 170 +++++ .../java/xyz/playedu/api/util/StringUtil.java | 538 ++++++++++++++ .../java/xyz/playedu/api/util/ToolUtil.java | 188 +++++ src/main/resources/application.example | 4 - src/main/resources/application.properties | 11 - src/main/resources/application.yml | 38 + src/main/resources/mapper/AdminUserMapper.xml | 7 +- 35 files changed, 2769 insertions(+), 112 deletions(-) create mode 100644 src/main/java/xyz/playedu/api/config/KaptchaConfig.java create mode 100644 src/main/java/xyz/playedu/api/config/PlayEduConfig.java create mode 100644 src/main/java/xyz/playedu/api/config/RedisConfig.java create mode 100644 src/main/java/xyz/playedu/api/controller/admin/SystemController.java create mode 100644 src/main/java/xyz/playedu/api/middleware/ImageCaptchaCheckMiddleware.java create mode 100644 src/main/java/xyz/playedu/api/middleware/impl/ImageCaptchaCheckMiddlewareImpl.java create mode 100644 src/main/java/xyz/playedu/api/service/ImageCaptchaService.java create mode 100644 src/main/java/xyz/playedu/api/service/impl/ImageCaptchaServiceImpl.java create mode 100644 src/main/java/xyz/playedu/api/types/ImageCaptchaRequestInterface.java create mode 100644 src/main/java/xyz/playedu/api/types/ImageCaptchaResult.java create mode 100644 src/main/java/xyz/playedu/api/util/Base64Util.java create mode 100644 src/main/java/xyz/playedu/api/util/EnvUtil.java create mode 100644 src/main/java/xyz/playedu/api/util/HttpUtil.java create mode 100644 src/main/java/xyz/playedu/api/util/IpUtil.java create mode 100644 src/main/java/xyz/playedu/api/util/MD5Util.java create mode 100644 src/main/java/xyz/playedu/api/util/RedisUtil.java create mode 100644 src/main/java/xyz/playedu/api/util/RequestUtil.java create mode 100644 src/main/java/xyz/playedu/api/util/StringUtil.java create mode 100644 src/main/java/xyz/playedu/api/util/ToolUtil.java delete mode 100644 src/main/resources/application.example delete mode 100644 src/main/resources/application.properties create mode 100644 src/main/resources/application.yml diff --git a/.gitignore b/.gitignore index b7b6d46..875f9d0 100644 --- a/.gitignore +++ b/.gitignore @@ -32,5 +32,5 @@ build/ ### VS Code ### .vscode/ -/src/main/resources/application-dev.properties +/src/main/resources/application-dev.yml /logs \ No newline at end of file diff --git a/pom.xml b/pom.xml index c7560a7..b395525 100644 --- a/pom.xml +++ b/pom.xml @@ -25,6 +25,10 @@ org.springframework.boot spring-boot-starter-web + + org.springframework.boot + spring-boot-starter-aop + org.springframework.boot spring-boot-starter-websocket @@ -67,6 +71,30 @@ org.springframework.boot spring-boot-starter-validation + + + com.github.penggle + kaptcha + 2.3.2 + + + + com.alibaba.fastjson2 + fastjson2 + 2.0.22 + + + + com.google.code.gson + gson + 2.10.1 + + + + org.apache.commons + commons-lang3 + 3.12.0 + diff --git a/src/main/java/xyz/playedu/api/PlayeduApiApplication.java b/src/main/java/xyz/playedu/api/PlayeduApiApplication.java index f1df994..7312ef1 100644 --- a/src/main/java/xyz/playedu/api/PlayeduApiApplication.java +++ b/src/main/java/xyz/playedu/api/PlayeduApiApplication.java @@ -2,12 +2,13 @@ package xyz.playedu.api; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.ComponentScan; @SpringBootApplication public class PlayeduApiApplication { - public static void main(String[] args) { - SpringApplication.run(PlayeduApiApplication.class, args); - } + public static void main(String[] args) { + SpringApplication.run(PlayeduApiApplication.class, args); + } } diff --git a/src/main/java/xyz/playedu/api/config/KaptchaConfig.java b/src/main/java/xyz/playedu/api/config/KaptchaConfig.java new file mode 100644 index 0000000..46b46ce --- /dev/null +++ b/src/main/java/xyz/playedu/api/config/KaptchaConfig.java @@ -0,0 +1,42 @@ +package xyz.playedu.api.config; + +import com.google.code.kaptcha.impl.DefaultKaptcha; +import com.google.code.kaptcha.util.Config; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.Properties; + +import static com.google.code.kaptcha.Constants.*; + +@Configuration +public class KaptchaConfig { + + @Bean(name = "captchaProducer") + public DefaultKaptcha getKaptchaBean() { + DefaultKaptcha defaultKaptcha = new DefaultKaptcha(); + Properties properties = new Properties(); + // 是否边框 + properties.setProperty(KAPTCHA_BORDER, "yes"); + // 字符颜色 + properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_COLOR, "black"); + // 图片宽度 + properties.setProperty(KAPTCHA_IMAGE_WIDTH, "120"); + // 图片高度 + properties.setProperty(KAPTCHA_IMAGE_HEIGHT, "40"); + // 字符大小 + properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_SIZE, "38"); + // 验证键码 + properties.setProperty(KAPTCHA_SESSION_CONFIG_KEY, "playedu"); + // 字符长度 + properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, "4"); + // 字体样式 + properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_NAMES, "Arial,Courier"); + // 图片样式 + properties.setProperty(KAPTCHA_OBSCURIFICATOR_IMPL, "com.google.code.kaptcha.impl.ShadowGimpy"); + + defaultKaptcha.setConfig(new Config(properties)); + + return defaultKaptcha; + } +} diff --git a/src/main/java/xyz/playedu/api/config/PlayEduConfig.java b/src/main/java/xyz/playedu/api/config/PlayEduConfig.java new file mode 100644 index 0000000..3f891fb --- /dev/null +++ b/src/main/java/xyz/playedu/api/config/PlayEduConfig.java @@ -0,0 +1,5 @@ +package xyz.playedu.api.config; + +public class PlayEduConfig { + public static String REDIS_PREFIX = "playedu:"; +} diff --git a/src/main/java/xyz/playedu/api/config/RedisConfig.java b/src/main/java/xyz/playedu/api/config/RedisConfig.java new file mode 100644 index 0000000..b4b71d9 --- /dev/null +++ b/src/main/java/xyz/playedu/api/config/RedisConfig.java @@ -0,0 +1,28 @@ +package xyz.playedu.api.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +@Configuration +public class RedisConfig { + + @Bean(name = "redisTemplate") + public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { + RedisTemplate redisTemplate = new RedisTemplate<>(); + StringRedisSerializer stringRedisSerializer = new StringRedisSerializer(); + GenericJackson2JsonRedisSerializer JsonRedisSerializer = new GenericJackson2JsonRedisSerializer(); + + redisTemplate.setConnectionFactory(redisConnectionFactory); + redisTemplate.setValueSerializer(JsonRedisSerializer); + redisTemplate.setKeySerializer(stringRedisSerializer); + redisTemplate.setHashKeySerializer(stringRedisSerializer); + redisTemplate.setHashValueSerializer(JsonRedisSerializer); + + return redisTemplate; + } + +} diff --git a/src/main/java/xyz/playedu/api/controller/ExceptionController.java b/src/main/java/xyz/playedu/api/controller/ExceptionController.java index 549db18..f097eb9 100644 --- a/src/main/java/xyz/playedu/api/controller/ExceptionController.java +++ b/src/main/java/xyz/playedu/api/controller/ExceptionController.java @@ -2,6 +2,7 @@ package xyz.playedu.api.controller; import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.validation.ObjectError; +import org.springframework.web.HttpRequestMethodNotSupportedException; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; @@ -19,17 +20,17 @@ public class ExceptionController { // } @ExceptionHandler(ServiceException.class) - public JsonResponse serviceExceptionHandler(ServiceException e) { + public JsonResponse serviceExceptionHandler(ServiceException e) { return JsonResponse.error(e.getMessage(), 1); } @ExceptionHandler(HttpMessageNotReadableException.class) - public JsonResponse serviceExceptionHandler(HttpMessageNotReadableException e) { + public JsonResponse serviceExceptionHandler(HttpMessageNotReadableException e) { return JsonResponse.error("参数为空", 406); } @ExceptionHandler(MethodArgumentNotValidException.class) - public JsonResponse serviceExceptionHandler(MethodArgumentNotValidException e) { + public JsonResponse serviceExceptionHandler(MethodArgumentNotValidException e) { StringBuffer errorMsg = new StringBuffer(); List allErrors = e.getBindingResult().getAllErrors(); for (ObjectError tmpError : allErrors) { @@ -39,4 +40,9 @@ public class ExceptionController { return JsonResponse.error(msg, 406); } + @ExceptionHandler(HttpRequestMethodNotSupportedException.class) + public JsonResponse serviceExceptionHandler(HttpRequestMethodNotSupportedException e) { + return JsonResponse.error("请求method错误", 400); + } + } diff --git a/src/main/java/xyz/playedu/api/controller/admin/AdminUserController.java b/src/main/java/xyz/playedu/api/controller/admin/AdminUserController.java index 3708fdf..ee989b0 100644 --- a/src/main/java/xyz/playedu/api/controller/admin/AdminUserController.java +++ b/src/main/java/xyz/playedu/api/controller/admin/AdminUserController.java @@ -5,8 +5,7 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import xyz.playedu.api.domain.AdminUser; -import xyz.playedu.api.exception.ServiceException; -import xyz.playedu.api.service.impl.AdminUserServiceImpl; +import xyz.playedu.api.service.AdminUserService; import xyz.playedu.api.types.PaginationResult; import xyz.playedu.api.types.JsonResponse; @@ -14,10 +13,10 @@ import xyz.playedu.api.types.JsonResponse; public class AdminUserController { @Autowired - private AdminUserServiceImpl adminUserService; + private AdminUserService adminUserService; @GetMapping("/admin/user/index") - public JsonResponse List(@RequestParam(name = "page", defaultValue = "1") Integer page, @RequestParam(name = "size", defaultValue = "10") Integer size) { + public JsonResponse List(@RequestParam(name = "page", defaultValue = "1") Integer page, @RequestParam(name = "size", defaultValue = "10") Integer size) { PaginationResult result = adminUserService.paginate(page, size, null); return JsonResponse.data(result); } diff --git a/src/main/java/xyz/playedu/api/controller/admin/LoginController.java b/src/main/java/xyz/playedu/api/controller/admin/LoginController.java index f9afae5..08b2f28 100644 --- a/src/main/java/xyz/playedu/api/controller/admin/LoginController.java +++ b/src/main/java/xyz/playedu/api/controller/admin/LoginController.java @@ -1,29 +1,44 @@ package xyz.playedu.api.controller.admin; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import xyz.playedu.api.domain.AdminUser; +import xyz.playedu.api.middleware.ImageCaptchaCheckMiddleware; import xyz.playedu.api.request.LoginRequest; +import xyz.playedu.api.service.AdminUserService; import xyz.playedu.api.types.JsonResponse; - -import java.util.HashMap; +import xyz.playedu.api.util.MD5Util; @RestController @RequestMapping("/admin/v1/auth") public class LoginController { + @Autowired + private AdminUserService adminUserService; + @PostMapping("/login") - public JsonResponse login(@RequestBody @Validated LoginRequest loginRequest) { - HashMap map = new HashMap<>(); - map.put("email", loginRequest.email); - map.put("password", loginRequest.password); - return JsonResponse.data(map); + @ImageCaptchaCheckMiddleware + public JsonResponse login(@RequestBody @Validated LoginRequest loginRequest) { + AdminUser adminUser = adminUserService.findByEmail(loginRequest.email); + if (adminUser == null) { + return JsonResponse.error("邮箱不存在"); + } + String password = MD5Util.md5(loginRequest.getPassword() + adminUser.getSalt()); + if (password != adminUser.getPassword()) { + return JsonResponse.error("密码错误"); + } + if (adminUser.getIsBanLogin() == 1) { + return JsonResponse.error("当前用户禁止登录"); + } + return JsonResponse.success("success"); } @PostMapping("/logout") - public JsonResponse logout() { + public JsonResponse logout() { return JsonResponse.success("success"); } diff --git a/src/main/java/xyz/playedu/api/controller/admin/SystemController.java b/src/main/java/xyz/playedu/api/controller/admin/SystemController.java new file mode 100644 index 0000000..8fd084a --- /dev/null +++ b/src/main/java/xyz/playedu/api/controller/admin/SystemController.java @@ -0,0 +1,32 @@ +package xyz.playedu.api.controller.admin; + +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.service.ImageCaptchaService; +import xyz.playedu.api.types.ImageCaptchaResult; +import xyz.playedu.api.types.JsonResponse; + +import java.io.IOException; +import java.util.HashMap; + +@RestController +@RequestMapping("/admin/v1/system") +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); + } + +} diff --git a/src/main/java/xyz/playedu/api/domain/AdminUser.java b/src/main/java/xyz/playedu/api/domain/AdminUser.java index 5a6f77d..dd0ba51 100644 --- a/src/main/java/xyz/playedu/api/domain/AdminUser.java +++ b/src/main/java/xyz/playedu/api/domain/AdminUser.java @@ -36,6 +36,11 @@ public class AdminUser implements Serializable { */ private String password; + /** + * Salt + */ + private String salt; + /** * 登录IP */ @@ -85,6 +90,7 @@ public class AdminUser implements Serializable { && (this.getName() == null ? other.getName() == null : this.getName().equals(other.getName())) && (this.getEmail() == null ? other.getEmail() == null : this.getEmail().equals(other.getEmail())) && (this.getPassword() == null ? other.getPassword() == null : this.getPassword().equals(other.getPassword())) + && (this.getSalt() == null ? other.getSalt() == null : this.getSalt().equals(other.getSalt())) && (this.getLoginIp() == null ? other.getLoginIp() == null : this.getLoginIp().equals(other.getLoginIp())) && (this.getLoginAt() == null ? other.getLoginAt() == null : this.getLoginAt().equals(other.getLoginAt())) && (this.getIsBanLogin() == null ? other.getIsBanLogin() == null : this.getIsBanLogin().equals(other.getIsBanLogin())) @@ -101,6 +107,7 @@ public class AdminUser implements Serializable { result = prime * result + ((getName() == null) ? 0 : getName().hashCode()); result = prime * result + ((getEmail() == null) ? 0 : getEmail().hashCode()); result = prime * result + ((getPassword() == null) ? 0 : getPassword().hashCode()); + result = prime * result + ((getSalt() == null) ? 0 : getSalt().hashCode()); result = prime * result + ((getLoginIp() == null) ? 0 : getLoginIp().hashCode()); result = prime * result + ((getLoginAt() == null) ? 0 : getLoginAt().hashCode()); result = prime * result + ((getIsBanLogin() == null) ? 0 : getIsBanLogin().hashCode()); @@ -120,6 +127,7 @@ public class AdminUser implements Serializable { sb.append(", name=").append(name); sb.append(", email=").append(email); sb.append(", password=").append(password); + sb.append(", salt=").append(salt); sb.append(", loginIp=").append(loginIp); sb.append(", loginAt=").append(loginAt); sb.append(", isBanLogin=").append(isBanLogin); diff --git a/src/main/java/xyz/playedu/api/mapper/AdminUserMapper.java b/src/main/java/xyz/playedu/api/mapper/AdminUserMapper.java index 1bb6ac2..e3c8410 100644 --- a/src/main/java/xyz/playedu/api/mapper/AdminUserMapper.java +++ b/src/main/java/xyz/playedu/api/mapper/AdminUserMapper.java @@ -4,13 +4,13 @@ import org.apache.ibatis.annotations.Mapper; import xyz.playedu.api.domain.AdminUser; import com.baomidou.mybatisplus.core.mapper.BaseMapper; +@Mapper /** * @author tengteng * @description 针对表【admin_users】的数据库操作Mapper -* @createDate 2023-01-30 16:22:00 +* @createDate 2023-02-11 10:58:52 * @Entity xyz.playedu.api.domain.AdminUser */ -@Mapper public interface AdminUserMapper extends BaseMapper { } diff --git a/src/main/java/xyz/playedu/api/middleware/ImageCaptchaCheckMiddleware.java b/src/main/java/xyz/playedu/api/middleware/ImageCaptchaCheckMiddleware.java new file mode 100644 index 0000000..1e1651b --- /dev/null +++ b/src/main/java/xyz/playedu/api/middleware/ImageCaptchaCheckMiddleware.java @@ -0,0 +1,8 @@ +package xyz.playedu.api.middleware; + +import java.lang.annotation.*; + +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface ImageCaptchaCheckMiddleware { +} diff --git a/src/main/java/xyz/playedu/api/middleware/impl/ImageCaptchaCheckMiddlewareImpl.java b/src/main/java/xyz/playedu/api/middleware/impl/ImageCaptchaCheckMiddlewareImpl.java new file mode 100644 index 0000000..0cce78e --- /dev/null +++ b/src/main/java/xyz/playedu/api/middleware/impl/ImageCaptchaCheckMiddlewareImpl.java @@ -0,0 +1,33 @@ +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.service.ImageCaptchaService; +import xyz.playedu.api.types.ImageCaptchaRequestInterface; +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/LoginRequest.java b/src/main/java/xyz/playedu/api/request/LoginRequest.java index b18c971..e0fdcd8 100644 --- a/src/main/java/xyz/playedu/api/request/LoginRequest.java +++ b/src/main/java/xyz/playedu/api/request/LoginRequest.java @@ -1,10 +1,13 @@ package xyz.playedu.api.request; import jakarta.validation.constraints.NotNull; +import lombok.Data; +import xyz.playedu.api.types.ImageCaptchaRequestInterface; import java.io.Serializable; -public class LoginRequest implements Serializable { +@Data +public class LoginRequest implements Serializable, ImageCaptchaRequestInterface { private static final long serialVersionUID = 1L; @@ -19,36 +22,4 @@ public class LoginRequest implements Serializable { @NotNull(message = "captchaKey参数为空") public String captchaKey; - - public String getEmail() { - return email; - } - - public void setEmail(String email) { - this.email = email; - } - - public String getPassword() { - return password; - } - - public void setPassword(String password) { - this.password = password; - } - - public String getCaptchaValue() { - return captchaValue; - } - - public void setCaptchaValue(String captchaValue) { - this.captchaValue = captchaValue; - } - - public String getCaptchaKey() { - return captchaKey; - } - - public void setCaptchaKey(String captchaKey) { - this.captchaKey = captchaKey; - } } diff --git a/src/main/java/xyz/playedu/api/service/AdminUserService.java b/src/main/java/xyz/playedu/api/service/AdminUserService.java index 004d1b5..0d5ead4 100644 --- a/src/main/java/xyz/playedu/api/service/AdminUserService.java +++ b/src/main/java/xyz/playedu/api/service/AdminUserService.java @@ -1,13 +1,17 @@ package xyz.playedu.api.service; +import com.baomidou.mybatisplus.core.conditions.Wrapper; import xyz.playedu.api.domain.AdminUser; import com.baomidou.mybatisplus.extension.service.IService; +import xyz.playedu.api.types.PaginationResult; /** -* @author tengteng -* @description 针对表【admin_users】的数据库操作Service -* @createDate 2023-01-30 16:22:00 -*/ + * @author tengteng + * @description 针对表【admin_users】的数据库操作Service + * @createDate 2023-02-11 10:58:52 + */ public interface AdminUserService extends IService { + PaginationResult paginate(int page, int size, Wrapper queryWrapper); + AdminUser findByEmail(String email); } diff --git a/src/main/java/xyz/playedu/api/service/ImageCaptchaService.java b/src/main/java/xyz/playedu/api/service/ImageCaptchaService.java new file mode 100644 index 0000000..574979b --- /dev/null +++ b/src/main/java/xyz/playedu/api/service/ImageCaptchaService.java @@ -0,0 +1,13 @@ +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/service/impl/AdminUserServiceImpl.java b/src/main/java/xyz/playedu/api/service/impl/AdminUserServiceImpl.java index 4af9314..0c4d57c 100644 --- a/src/main/java/xyz/playedu/api/service/impl/AdminUserServiceImpl.java +++ b/src/main/java/xyz/playedu/api/service/impl/AdminUserServiceImpl.java @@ -1,9 +1,10 @@ package xyz.playedu.api.service.impl; +import com.baomidou.mybatisplus.core.conditions.Wrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; -import com.baomidou.mybatisplus.core.conditions.Wrapper; import xyz.playedu.api.domain.AdminUser; import xyz.playedu.api.service.AdminUserService; import xyz.playedu.api.mapper.AdminUserMapper; @@ -13,10 +14,11 @@ import xyz.playedu.api.types.PaginationResult; /** * @author tengteng * @description 针对表【admin_users】的数据库操作Service实现 - * @createDate 2023-01-30 16:22:00 + * @createDate 2023-02-11 10:58:52 */ @Service -public class AdminUserServiceImpl extends ServiceImpl implements AdminUserService { +public class AdminUserServiceImpl extends ServiceImpl + implements AdminUserService { public PaginationResult paginate(int page, int size, Wrapper queryWrapper) { IPage userPage = new Page<>(page, size); @@ -29,6 +31,12 @@ public class AdminUserServiceImpl extends ServiceImpl queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("email", email); + return this.getBaseMapper().selectOne(queryWrapper); + } + } diff --git a/src/main/java/xyz/playedu/api/service/impl/ImageCaptchaServiceImpl.java b/src/main/java/xyz/playedu/api/service/impl/ImageCaptchaServiceImpl.java new file mode 100644 index 0000000..dbb83cb --- /dev/null +++ b/src/main/java/xyz/playedu/api/service/impl/ImageCaptchaServiceImpl.java @@ -0,0 +1,75 @@ +package xyz.playedu.api.service.impl; + +import com.google.code.kaptcha.impl.DefaultKaptcha; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.util.FastByteArrayOutputStream; +import xyz.playedu.api.service.ImageCaptchaService; +import xyz.playedu.api.types.ImageCaptchaResult; +import xyz.playedu.api.util.Base64Util; +import xyz.playedu.api.util.EnvUtil; +import xyz.playedu.api.util.RedisUtil; +import xyz.playedu.api.util.ToolUtil; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.IOException; + +@Slf4j +@Service +public class ImageCaptchaServiceImpl implements ImageCaptchaService { + + @Resource + private DefaultKaptcha defaultKaptcha; + + @Override + public ImageCaptchaResult generate() throws IOException { + ImageCaptchaResult imageCaptcha = new ImageCaptchaResult(); + BufferedImage image; + + // 图形验证码的key[api是无状态的需要key来锁定验证码的值] + String randomKey = ToolUtil.randomString(16); + imageCaptcha.setKey(randomKey); + + // 生成验证码 + imageCaptcha.setCode(defaultKaptcha.createText()); + image = defaultKaptcha.createImage(imageCaptcha.getCode()); + + // 写入到redis中 + RedisUtil.set(getCacheKey(randomKey), imageCaptcha.getCode(), getExpireSeconds()); + + FastByteArrayOutputStream os = new FastByteArrayOutputStream(); + ImageIO.write(image, "png", os); + + String base64 = "data:image/png;base64," + Base64Util.encode(os.toByteArray()); + imageCaptcha.setImage(base64); + + 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.equals(code); + + if (verifyResult) {//验证成功删除缓存->防止多次使用 + RedisUtil.del(cacheKey); + } + + return verifyResult; + } + + private String getCacheKey(String val) { + return EnvUtil.get("playedu.captcha.cache-name") + val; + } + + private Long getExpireSeconds() { + return Long.parseLong(EnvUtil.get("playedu.captcha.expire")); + } +} diff --git a/src/main/java/xyz/playedu/api/types/ImageCaptchaRequestInterface.java b/src/main/java/xyz/playedu/api/types/ImageCaptchaRequestInterface.java new file mode 100644 index 0000000..0b8670e --- /dev/null +++ b/src/main/java/xyz/playedu/api/types/ImageCaptchaRequestInterface.java @@ -0,0 +1,8 @@ +package xyz.playedu.api.types; + +public interface ImageCaptchaRequestInterface { + + String getCaptchaValue(); + + String getCaptchaKey(); +} diff --git a/src/main/java/xyz/playedu/api/types/ImageCaptchaResult.java b/src/main/java/xyz/playedu/api/types/ImageCaptchaResult.java new file mode 100644 index 0000000..a06ff7f --- /dev/null +++ b/src/main/java/xyz/playedu/api/types/ImageCaptchaResult.java @@ -0,0 +1,13 @@ +package xyz.playedu.api.types; + +import lombok.Data; + +@Data +public class ImageCaptchaResult { + + public String key; + + public String image; + + public String code; +} diff --git a/src/main/java/xyz/playedu/api/types/JsonResponse.java b/src/main/java/xyz/playedu/api/types/JsonResponse.java index 8b81831..b06cc7b 100644 --- a/src/main/java/xyz/playedu/api/types/JsonResponse.java +++ b/src/main/java/xyz/playedu/api/types/JsonResponse.java @@ -1,50 +1,33 @@ package xyz.playedu.api.types; -public class JsonResponse { +import lombok.Data; + +@Data +public class JsonResponse { private Integer code; private String msg; - private T data; + private Object data; - public Integer getCode() { - return code; - } - - public void setCode(Integer code) { - this.code = code; - } - - public String getMsg() { - return msg; - } - - public void setMsg(String msg) { - this.msg = msg; - } - - public T getData() { - return data; - } - - public void setData(T data) { - this.data = data; - } - - public JsonResponse(Integer code, String msg, T data) { + public JsonResponse(Integer code, String msg, Object data) { this.code = code; this.msg = msg; this.data = data; } - public static JsonResponse success(String msg) { - return new JsonResponse<>(0, msg, null); + public static JsonResponse success(String msg) { + return new JsonResponse(0, msg, null); } - public static JsonResponse data(Object data) { - return new JsonResponse<>(0, "", data); + public static JsonResponse data(Object data) { + return new JsonResponse(0, "", data); } - public static JsonResponse error(String msg, Integer code) { - return new JsonResponse<>(code, msg, null); + public static JsonResponse error(String msg, Integer code) { + return new JsonResponse(code, msg, null); + } + + public static JsonResponse error(String msg) { + return new JsonResponse(-1, msg, null); } } diff --git a/src/main/java/xyz/playedu/api/util/Base64Util.java b/src/main/java/xyz/playedu/api/util/Base64Util.java new file mode 100644 index 0000000..20f7e17 --- /dev/null +++ b/src/main/java/xyz/playedu/api/util/Base64Util.java @@ -0,0 +1,263 @@ +package xyz.playedu.api.util; + +/** + * Base64工具类 + */ +public final class Base64Util { + static private final int BASE_LENGTH = 128; + static private final int LOOK_UP_LENGTH = 64; + static private final int TWENTY_FOUR_BIT_GROUP = 24; + static private final int EIGHT_BIT = 8; + static private final int SIXTEEN_BIT = 16; + static private final int FOUR_BYTE = 4; + static private final int SIGN = -128; + static private final char PAD = '='; + static final private byte[] base64Alphabet = new byte[BASE_LENGTH]; + static final private char[] lookUpBase64Alphabet = new char[LOOK_UP_LENGTH]; + + static { + for (int i = 0; i < BASE_LENGTH; ++i) { + base64Alphabet[i] = -1; + } + + for (int i = 'Z'; i >= 'A'; i--) { + base64Alphabet[i] = (byte) (i - 'A'); + } + + for (int i = 'z'; i >= 'a'; i--) { + base64Alphabet[i] = (byte) (i - 'a' + 26); + } + + for (int i = '9'; i >= '0'; i--) { + base64Alphabet[i] = (byte) (i - '0' + 52); + } + + base64Alphabet['+'] = 62; + base64Alphabet['/'] = 63; + + for (int i = 0; i <= 25; i++) { + lookUpBase64Alphabet[i] = (char) ('A' + i); + } + + for (int i = 26, j = 0; i <= 51; i++, j++) { + lookUpBase64Alphabet[i] = (char) ('a' + j); + } + + for (int i = 52, j = 0; i <= 61; i++, j++) { + lookUpBase64Alphabet[i] = (char) ('0' + j); + } + + lookUpBase64Alphabet[62] = '+'; + lookUpBase64Alphabet[63] = '/'; + } + + /** + * 转码 + * + * @param binaryData 未转码数据 + * @return 转码后数据串 + */ + public static String encode(byte[] binaryData) { + if (binaryData == null) { + return null; + } + + int lengthDataBits = binaryData.length * EIGHT_BIT; + if (lengthDataBits == 0) { + return ""; + } + + int fewerThan24bits = lengthDataBits % TWENTY_FOUR_BIT_GROUP; + int numberTriplets = lengthDataBits / TWENTY_FOUR_BIT_GROUP; + int numberQuartet = fewerThan24bits != 0 ? numberTriplets + 1 : numberTriplets; + char[] encodedData; + + encodedData = new char[numberQuartet * 4]; + + byte k, l, b1, b2, b3; + int encodedIndex = 0; + int dataIndex = 0; + + for (int i = 0; i < numberTriplets; i++) { + b1 = binaryData[dataIndex++]; + b2 = binaryData[dataIndex++]; + b3 = binaryData[dataIndex++]; + + k = (byte) (b1 & 0x03); + l = (byte) (b2 & 0x0f); + + byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2) : (byte) ((b1) >> 2 ^ 0xc0); + byte val2 = ((b2 & SIGN) == 0) ? (byte) (b2 >> 4) : (byte) ((b2) >> 4 ^ 0xf0); + byte val3 = ((b3 & SIGN) == 0) ? (byte) (b3 >> 6) : (byte) ((b3) >> 6 ^ 0xfc); + + encodedData[encodedIndex++] = lookUpBase64Alphabet[val1]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[val2 | (k << 4)]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[(l << 2) | val3]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[b3 & 0x3f]; + } + + if (fewerThan24bits == EIGHT_BIT) { + b1 = binaryData[dataIndex]; + k = (byte) (b1 & 0x03); + byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2) : (byte) ((b1) >> 2 ^ 0xc0); + encodedData[encodedIndex++] = lookUpBase64Alphabet[val1]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[k << 4]; + encodedData[encodedIndex++] = PAD; + encodedData[encodedIndex++] = PAD; + } else if (fewerThan24bits == SIXTEEN_BIT) { + b1 = binaryData[dataIndex]; + b2 = binaryData[dataIndex + 1]; + l = (byte) (b2 & 0x0f); + k = (byte) (b1 & 0x03); + byte val1 = ((b1 & SIGN) == 0) ? (byte) (b1 >> 2) : (byte) ((b1) >> 2 ^ 0xc0); + byte val2 = ((b2 & SIGN) == 0) ? (byte) (b2 >> 4) : (byte) ((b2) >> 4 ^ 0xf0); + + encodedData[encodedIndex++] = lookUpBase64Alphabet[val1]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[val2 | (k << 4)]; + encodedData[encodedIndex++] = lookUpBase64Alphabet[l << 2]; + encodedData[encodedIndex++] = PAD; + } + return new String(encodedData); + } + + /** + * 解码 + * + * @param encoded 已转码数据 + * @return 解码后数据 + */ + public static byte[] decode(String encoded) { + if (encoded == null) { + return null; + } + + char[] base64Data = encoded.toCharArray(); + int len = removeWhiteSpace(base64Data); + + if (len % FOUR_BYTE != 0) { + return null; + } + + int numberQuadruple = (len / FOUR_BYTE); + if (numberQuadruple == 0) { + return new byte[0]; + } + + byte[] decodedData; + byte b1, b2, b3, b4; + char d1, d2, d3, d4; + + int i = 0; + int encodedIndex = 0; + int dataIndex = 0; + decodedData = new byte[(numberQuadruple) * 3]; + + for (; i < numberQuadruple - 1; i++) { + + if (!isData((d1 = base64Data[dataIndex++])) || !isData((d2 = base64Data[dataIndex++])) || !isData((d3 = base64Data[dataIndex++])) || !isData((d4 = base64Data[dataIndex++]))) { + return null; + } + + b1 = base64Alphabet[d1]; + b2 = base64Alphabet[d2]; + b3 = base64Alphabet[d3]; + b4 = base64Alphabet[d4]; + + decodedData[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4); + decodedData[encodedIndex++] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf)); + decodedData[encodedIndex++] = (byte) (b3 << 6 | b4); + } + + if (!isData((d1 = base64Data[dataIndex++])) || !isData((d2 = base64Data[dataIndex++]))) { + return null; + } + + b1 = base64Alphabet[d1]; + b2 = base64Alphabet[d2]; + d3 = base64Data[dataIndex++]; + d4 = base64Data[dataIndex++]; + if (!isData((d3)) || !isData((d4))) { + if (isPad(d3) && isPad(d4)) { + if ((b2 & 0xf) != 0) { + return null; + } + byte[] tmp = new byte[i * 3 + 1]; + System.arraycopy(decodedData, 0, tmp, 0, i * 3); + tmp[encodedIndex] = (byte) (b1 << 2 | b2 >> 4); + return tmp; + } else if (!isPad(d3) && isPad(d4)) { + b3 = base64Alphabet[d3]; + if ((b3 & 0x3) != 0) { + return null; + } + byte[] tmp = new byte[i * 3 + 2]; + System.arraycopy(decodedData, 0, tmp, 0, i * 3); + tmp[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4); + tmp[encodedIndex] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf)); + return tmp; + } else { + return null; + } + } else { + b3 = base64Alphabet[d3]; + b4 = base64Alphabet[d4]; + decodedData[encodedIndex++] = (byte) (b1 << 2 | b2 >> 4); + decodedData[encodedIndex++] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf)); + decodedData[encodedIndex++] = (byte) (b4 | b3 << 6); + + } + return decodedData; + } + + /** + * 是否空白 + * + * @param octEct 位 + * @return boolean + */ + private static boolean isWhiteSpace(char octEct) { + return (octEct == 0x20 || octEct == 0xd || octEct == 0xa || octEct == 0x9); + } + + /** + * 是否填充 + * + * @param octEct 位 + * @return boolean + */ + private static boolean isPad(char octEct) { + return (octEct == PAD); + } + + /** + * 是否数据 + * + * @param octEct 位 + * @return Boolean + */ + private static Boolean isData(char octEct) { + return (octEct < BASE_LENGTH && base64Alphabet[octEct] != -1); + } + + /** + * 移除空白 + * + * @param data 数据 + * @return int + */ + private static int removeWhiteSpace(char[] data) { + if (data == null) { + return 0; + } + + int newSize = 0; + int len = data.length; + for (int i = 0; i < len; i++) { + if (!isWhiteSpace(data[i])) { + data[newSize++] = data[i]; + } + } + return newSize; + } + +} diff --git a/src/main/java/xyz/playedu/api/util/EnvUtil.java b/src/main/java/xyz/playedu/api/util/EnvUtil.java new file mode 100644 index 0000000..313d46a --- /dev/null +++ b/src/main/java/xyz/playedu/api/util/EnvUtil.java @@ -0,0 +1,37 @@ +package xyz.playedu.api.util; + +import org.springframework.context.EnvironmentAware; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Component; + +/** + * 系统文件配置操作工具 + */ +@Component +public class EnvUtil implements EnvironmentAware { + + private static Environment env; + + /** + * 设置环境变量 + * + * @author fzr + * @param environment 环境变量 + */ + @Override + public void setEnvironment(Environment environment) { + EnvUtil.env = environment; + } + + /** + * 根据Key获取值 + * + * @author fzr + * @param key 键 + * @return String + */ + public static String get(String key) { + return env.getProperty(key); + } + +} diff --git a/src/main/java/xyz/playedu/api/util/HttpUtil.java b/src/main/java/xyz/playedu/api/util/HttpUtil.java new file mode 100644 index 0000000..3399a68 --- /dev/null +++ b/src/main/java/xyz/playedu/api/util/HttpUtil.java @@ -0,0 +1,220 @@ +package xyz.playedu.api.util; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.net.ssl.*; +import java.io.*; +import java.net.*; +import java.nio.charset.StandardCharsets; +import java.security.cert.X509Certificate; + +/** + * HTTP工具类 + */ +public class HttpUtil { + + private static final Logger log = LoggerFactory.getLogger(HttpUtil.class); + + /** + * 向指定URL发送GET方法的请求 (不带参) + * + * @param url 发送请求的 URL + * @return 所代表远程资源的响应结果 + */ + public static String sendGet(String url) { + return sendGet(url, StringUtil.EMPTY); + } + + /** + * 向指定URL发送GET方法的请求 (带参固定编码) + * + * @param url 发送请求的 URL + * @param param 请求参数,请求参数应该是 name1=value1&name2=value2 的形式。 + * @return 所代表远程资源的响应结果 + */ + public static String sendGet(String url, String param) { + return sendGet(url, param, "UTF-8"); + } + + /** + * 向指定URL发送GET方法的请求 (带参指定编码) + * + * @param url 发送请求的 URL + * @param param 请求参数,请求参数应该是 name1=value1&name2=value2 的形式。 + * @param contentType 编码类型 + * @return 所代表远程资源的响应结果 + */ + public static String sendGet(String url, String param, String contentType) { + StringBuilder result = new StringBuilder(); + BufferedReader in = null; + try { + String urlNameString = StringUtil.isNotBlank(param) ? url + "?" + param : url; + log.info("sendGet - {}", urlNameString); + URL realUrl = new URL(urlNameString); + URLConnection connection = realUrl.openConnection(); + connection.setRequestProperty("accept", "*/*"); + connection.setRequestProperty("connection", "Keep-Alive"); + connection.setRequestProperty("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)"); + connection.connect(); + in = new BufferedReader(new InputStreamReader(connection.getInputStream(), contentType)); + String line; + while ((line = in.readLine()) != null) { + result.append(line); + } + log.info("rev - {}", result); + } catch (ConnectException e) { + log.error("调用HttpUtils.sendGet ConnectException, url=" + url + ",param=" + param, e); + } catch (SocketTimeoutException e) { + log.error("调用HttpUtils.sendGet SocketTimeoutException, url=" + url + ",param=" + param, e); + } catch (IOException e) { + log.error("调用HttpUtils.sendGet IOException, url=" + url + ",param=" + param, e); + } catch (Exception e) { + log.error("调用HttpsUtil.sendGet Exception, url=" + url + ",param=" + param, e); + } finally { + try { + if (in != null) { + in.close(); + } + } catch (Exception ex) { + log.error("调用in.close Exception, url=" + url + ",param=" + param, ex); + } + } + return result.toString(); + } + + /** + * 向指定 URL 发送POST方法的请求 + * + * @param url 发送请求的 URL + * @param param 请求参数,请求参数应该是 name1=value1&name2=value2 的形式。 + * @return 所代表远程资源的响应结果 + * @author fzr + */ + public static String sendPost(String url, String param) { + PrintWriter out = null; + BufferedReader in = null; + StringBuilder result = new StringBuilder(); + try { + log.info("sendPost - {}", url); + URL realUrl = new URL(url); + URLConnection conn = realUrl.openConnection(); + conn.setRequestProperty("accept", "*/*"); + conn.setRequestProperty("connection", "Keep-Alive"); + conn.setRequestProperty("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)"); + conn.setRequestProperty("Accept-Charset", "utf-8"); + conn.setRequestProperty("contentType", "utf-8"); + conn.setDoOutput(true); + conn.setDoInput(true); + out = new PrintWriter(conn.getOutputStream()); + out.print(param); + out.flush(); + in = new BufferedReader(new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8)); + String line; + while ((line = in.readLine()) != null) { + result.append(line); + } + log.info("rev - {}", result); + } catch (ConnectException e) { + log.error("调用HttpUtils.sendPost ConnectException, url=" + url + ",param=" + param, e); + } catch (SocketTimeoutException e) { + log.error("调用HttpUtils.sendPost SocketTimeoutException, url=" + url + ",param=" + param, e); + } catch (IOException e) { + log.error("调用HttpUtils.sendPost IOException, url=" + url + ",param=" + param, e); + } catch (Exception e) { + log.error("调用HttpsUtil.sendPost Exception, url=" + url + ",param=" + param, e); + } finally { + try { + if (out != null) { + out.close(); + } + if (in != null) { + in.close(); + } + } catch (IOException ex) { + log.error("调用in.close Exception, url=" + url + ",param=" + param, ex); + } + } + return result.toString(); + } + + /** + * 发送SSL的POST请求 + * + * @param url 请求地址 + * @param param 请求参数 + * @return String + * @author fzr + */ + public static String sendSSLPost(String url, String param) { + StringBuilder result = new StringBuilder(); + String urlNameString = url + "?" + param; + try { + log.info("sendSSLPost - {}", urlNameString); + SSLContext sc = SSLContext.getInstance("SSL"); + sc.init(null, new TrustManager[]{new TrustAnyTrustManager()}, new java.security.SecureRandom()); + URL console = new URL(urlNameString); + HttpsURLConnection conn = (HttpsURLConnection) console.openConnection(); + conn.setRequestProperty("connection", "Keep-Alive"); + conn.setRequestProperty("accept", "*/*"); + conn.setRequestProperty("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)"); + conn.setRequestProperty("Accept-Charset", "utf-8"); + conn.setRequestProperty("contentType", "utf-8"); + conn.setDoOutput(true); + conn.setDoInput(true); + + conn.setSSLSocketFactory(sc.getSocketFactory()); + conn.setHostnameVerifier(new TrustAnyHostnameVerifier()); + conn.connect(); + InputStream is = conn.getInputStream(); + BufferedReader br = new BufferedReader(new InputStreamReader(is)); + String ret; + while ((ret = br.readLine()) != null) { + if (!"".equals(ret.trim())) { + result.append(new String(ret.getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8)); + } + } + log.info("rev - {}", result); + conn.disconnect(); + br.close(); + } catch (ConnectException e) { + log.error("调用HttpUtils.sendSSLPost ConnectException, url=" + url + ",param=" + param, e); + } catch (SocketTimeoutException e) { + log.error("调用HttpUtils.sendSSLPost SocketTimeoutException, url=" + url + ",param=" + param, e); + } catch (IOException e) { + log.error("调用HttpUtils.sendSSLPost IOException, url=" + url + ",param=" + param, e); + } catch (Exception e) { + log.error("调用HttpsUtil.sendSSLPost Exception, url=" + url + ",param=" + param, e); + } + return result.toString(); + } + + /** + * SSL证书协议接口 + */ + private static class TrustAnyTrustManager implements X509TrustManager { + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) { + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) { + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[]{}; + } + } + + /** + * POST请求安全接口 + */ + private static class TrustAnyHostnameVerifier implements HostnameVerifier { + @Override + public boolean verify(String hostname, SSLSession session) { + return true; + } + } + +} diff --git a/src/main/java/xyz/playedu/api/util/IpUtil.java b/src/main/java/xyz/playedu/api/util/IpUtil.java new file mode 100644 index 0000000..9f6c216 --- /dev/null +++ b/src/main/java/xyz/playedu/api/util/IpUtil.java @@ -0,0 +1,263 @@ +package xyz.playedu.api.util; + +import com.alibaba.fastjson2.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jakarta.servlet.http.HttpServletRequest; +import java.net.InetAddress; +import java.net.UnknownHostException; + +public class IpUtil { + + private static final Logger log = LoggerFactory.getLogger(IpUtil.class); + + /** + * 获取客户端IP + * + * @return IP地址 (113.67.10.194) + * @author fzr + */ + public static String getIpAddress() { + HttpServletRequest request = RequestUtil.handler(); + if (request == null) { + return "unknown"; + } + String ip = request.getHeader("x-forwarded-for"); + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("Proxy-Client-IP"); + } + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("X-Forwarded-For"); + } + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("WL-Proxy-Client-IP"); + } + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("X-Real-IP"); + } + + if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { + ip = request.getRemoteAddr(); + } + + return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : getMultistageReverseProxyIp(ip); + } + + /** + * 根据IP获取所在地址 + * + * @param ip Ip地址 + * @return String (广州省 广州市) + * @author fzr + */ + public static String getRealAddressByIP(String ip) { + String IP_URL = "https://whois.pconline.com.cn/ipJson.jsp"; + String UNKNOWN = "XX XX"; + + // 内网不查询 + if (IpUtil.internalIp(ip)) { + return "内网IP"; + } + + try { + String rspStr = HttpUtil.sendGet(IP_URL, "ip=" + ip + "&json=true", "GBK"); + if (StringUtil.isEmpty(rspStr)) { + log.error("获取地理位置异常 {}", ip); + return UNKNOWN; + } + JSONObject obj = JSONObject.parseObject(rspStr); + String region = obj.getString("pro"); + String city = obj.getString("city"); + return String.format("%s %s", region, city); + } catch (Exception e) { + log.error("获取地理位置异常 {}", ip); + } + + return UNKNOWN; + } + + /** + * 检查是否为内部IP地址 + * + * @param ip IP地址 + * @return 结果 + */ + public static boolean internalIp(String ip) { + byte[] address = textToNumericFormatV4(ip); + return internalIp(address) || "127.0.0.1".equals(ip); + } + + /** + * 检查是否为内部IP地址 + * + * @param address byte地址 + * @return 结果 + * @author fzr + */ + private static boolean internalIp(byte[] address) { + if (address == null || address.length < 2) { + return true; + } + final byte b0 = address[0]; + final byte b1 = address[1]; + // 10.x.x.x/8 + final byte SECTION_1 = 0x0A; + // 172.16.x.x/12 + final byte SECTION_2 = (byte) 0xAC; + final byte SECTION_3 = (byte) 0x10; + final byte SECTION_4 = (byte) 0x1F; + // 192.168.x.x/16 + final byte SECTION_5 = (byte) 0xC0; + final byte SECTION_6 = (byte) 0xA8; + switch (b0) { + case SECTION_1: + return true; + case SECTION_2: + if (b1 >= SECTION_3 && b1 <= SECTION_4) { + return true; + } + case SECTION_5: + if (b1 == SECTION_6) { + return true; + } + default: + return false; + } + } + + /** + * 将IPv4地址转换成字节 + * + * @param text IPv4地址 + * @return byte 字节 + * @author fzr + */ + public static byte[] textToNumericFormatV4(String text) { + if (text.length() == 0) { + return null; + } + + byte[] bytes = new byte[4]; + String[] elements = text.split("\\.", -1); + try { + long l; + int i; + switch (elements.length) { + case 1: + l = Long.parseLong(elements[0]); + if ((l < 0L) || (l > 4294967295L)) { + return null; + } + bytes[0] = (byte) (int) (l >> 24 & 0xFF); + bytes[1] = (byte) (int) ((l & 0xFFFFFF) >> 16 & 0xFF); + bytes[2] = (byte) (int) ((l & 0xFFFF) >> 8 & 0xFF); + bytes[3] = (byte) (int) (l & 0xFF); + break; + case 2: + l = Integer.parseInt(elements[0]); + if ((l < 0L) || (l > 255L)) { + return null; + } + bytes[0] = (byte) (int) (l & 0xFF); + l = Integer.parseInt(elements[1]); + if ((l < 0L) || (l > 16777215L)) { + return null; + } + bytes[1] = (byte) (int) (l >> 16 & 0xFF); + bytes[2] = (byte) (int) ((l & 0xFFFF) >> 8 & 0xFF); + bytes[3] = (byte) (int) (l & 0xFF); + break; + case 3: + for (i = 0; i < 2; ++i) { + l = Integer.parseInt(elements[i]); + if ((l < 0L) || (l > 255L)) { + return null; + } + bytes[i] = (byte) (int) (l & 0xFF); + } + l = Integer.parseInt(elements[2]); + if ((l < 0L) || (l > 65535L)) { + return null; + } + bytes[2] = (byte) (int) (l >> 8 & 0xFF); + bytes[3] = (byte) (int) (l & 0xFF); + break; + case 4: + for (i = 0; i < 4; ++i) { + l = Integer.parseInt(elements[i]); + if ((l < 0L) || (l > 255L)) { + return null; + } + bytes[i] = (byte) (int) (l & 0xFF); + } + break; + default: + return null; + } + } catch (NumberFormatException e) { + return null; + } + return bytes; + } + + /** + * 获取IP地址 + * + * @return 本地IP地址 + * @author fzr + */ + public static String getHostIp() { + try { + return InetAddress.getLocalHost().getHostAddress(); + } catch (UnknownHostException ignored) { + } + return "127.0.0.1"; + } + + /** + * 获取主机名 + * + * @return 本地主机名 + * @author fzr + */ + public static String getHostName() { + try { + return InetAddress.getLocalHost().getHostName(); + } catch (UnknownHostException ignored) { + } + return "未知"; + } + + /** + * 从多级反向代理中获得第一个非unknown IP地址 + * + * @param ip 获得的IP地址 + * @return 第一个非unknown IP地址 + * @author fzr + */ + public static String getMultistageReverseProxyIp(String ip) { + if (ip != null && ip.indexOf(",") > 0) { + final String[] ips = ip.trim().split(","); + for (String subIp : ips) { + if (!isUnknown(subIp)) { + ip = subIp; + break; + } + } + } + return ip; + } + + /** + * 检测给定字符串是否为未知,多用于检测HTTP请求相关 + * + * @param checkString 被检测的字符串 + * @return 是否未知 + * @author fzr + */ + public static boolean isUnknown(String checkString) { + return StringUtil.isBlank(checkString) || "unknown".equalsIgnoreCase(checkString); + } + +} diff --git a/src/main/java/xyz/playedu/api/util/MD5Util.java b/src/main/java/xyz/playedu/api/util/MD5Util.java new file mode 100644 index 0000000..c613098 --- /dev/null +++ b/src/main/java/xyz/playedu/api/util/MD5Util.java @@ -0,0 +1,12 @@ +package xyz.playedu.api.util; + +import org.springframework.util.DigestUtils; + +import java.nio.charset.StandardCharsets; + +public class MD5Util { + + public static String md5(String text) { + return DigestUtils.md5DigestAsHex(text.getBytes(StandardCharsets.UTF_8)); + } +} diff --git a/src/main/java/xyz/playedu/api/util/RedisUtil.java b/src/main/java/xyz/playedu/api/util/RedisUtil.java new file mode 100644 index 0000000..641a486 --- /dev/null +++ b/src/main/java/xyz/playedu/api/util/RedisUtil.java @@ -0,0 +1,662 @@ +package xyz.playedu.api.util; + +import jakarta.annotation.Resource; +import org.springframework.data.redis.connection.RedisConnection; +import org.springframework.data.redis.core.Cursor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ScanOptions; +import org.springframework.data.redis.serializer.RedisSerializer; +import org.springframework.stereotype.Component; +import org.springframework.util.CollectionUtils; +import xyz.playedu.api.config.PlayEduConfig; + +import java.util.*; +import java.util.concurrent.TimeUnit; + +@Component +public class RedisUtil { + + private static RedisTemplate redisTemplate; + private static final String redisPrefix = PlayEduConfig.REDIS_PREFIX; + + /** + * 注入Redis + * + * @param redisTemplate Redis对象 + * @author fzr + */ + @Resource + public void setRedisTemplate(RedisTemplate redisTemplate) { + RedisUtil.redisTemplate = redisTemplate; + } + + /** + * 对象句柄 + * + * @return RedisTemplate + * @author fzr + */ + public static RedisTemplate handler() { + return redisTemplate; + } + + /** + * 指定缓存失效时间 + * + * @param key 键 + * @param second 时间(秒) + * @author fzr + */ + public static void expire(String key, Long second) { + key = redisPrefix + key; + redisTemplate.expire(key, second, TimeUnit.SECONDS); + } + + /** + * 指定缓存失效时间 + * + * @param key 键 + * @param millisecond 时间(毫秒) + * @author fzr + */ + public static void pExpire(String key, Long millisecond) { + key = redisPrefix + key; + redisTemplate.expire(key, millisecond, TimeUnit.MILLISECONDS); + } + + /** + * 指定缓存永久有效 + * + * @param key 键 + * @author fzr + */ + public static void persist(String key) { + key = redisPrefix + key; + redisTemplate.persist(key); + } + + /** + * 根据key获取过期时间 + * + * @param key 键不能为null + * @return 返回0代表为永久有效(秒) + * @author fzr + */ + public static Long ttl(String key) { + key = redisPrefix + key; + return redisTemplate.getExpire(key, TimeUnit.SECONDS); + } + + /** + * 根据key获取过期时间 + * + * @param key 键不能为null + * @return 返回0代表为永久有效(毫秒) + * @author fzr + */ + public static Long pTtl(String key) { + key = redisPrefix + key; + return redisTemplate.getExpire(key, TimeUnit.MILLISECONDS); + } + + /** + * 判断key是否存在 + * + * @param key 键 + * @return true=存在,false=不存在 + * @author fzr + */ + public static Boolean exists(String key) { + key = redisPrefix + key; + return redisTemplate.hasKey(key); + } + + /** + * 删除1个或多个键 + * + * @param key 键(一个或多个) + * @author fzr + */ + @SuppressWarnings("unchecked") + public static void del(String... key) { + if (key.length == 1) { + key[0] = redisPrefix + key[0]; + redisTemplate.delete(key[0]); + } else { + for (int i = 0; key.length > i; i++) { + key[i] = redisPrefix + key[i]; + } + redisTemplate.delete((Collection) CollectionUtils.arrayToList(key)); + } + } + + /** + * 给key赋值一个新的key名 + * + * @param oldKey 旧的key + * @param newKey 新的key + * @author fzr + */ + public static void rename(String oldKey, String newKey) { + oldKey = redisPrefix + oldKey; + newKey = redisPrefix + newKey; + redisTemplate.rename(oldKey, newKey); + } + + /** + * 将当前数据库的key移动到给定的数据库db当中 + * + * @param key 键 + * @param db 库 + * @return Boolean + * @author fzr + */ + public static Boolean move(String key, int db) { + key = redisPrefix + key; + return redisTemplate.move(key, db); + } + + /** + * 获取匹配的key值 + * + * @param pattern 通配符(*, ?, []) + * @return Set + * @author fzr + * @author fzr + */ + public static Set keys(String pattern) { + return redisTemplate.keys(pattern); + } + + /** + * 随机返回一个key + * + * @return String + * @author fzr + * @author fzr + */ + public static String randomKey() { + return redisTemplate.randomKey(); + } + + /* ***************** common end *************** */ + + /** + * 按匹配获取或有KEY + * + * @param pattern 规则 + * @return Set + * @author fzr + */ + public static Set matchSet(String pattern) { + Set keys = new LinkedHashSet<>(); + RedisUtil.handler().execute((RedisConnection connection) -> { + try (Cursor cursor = connection.scan( + ScanOptions.scanOptions() + .count(Long.MAX_VALUE) + .match(pattern) + .build() + )) { + cursor.forEachRemaining(item -> { + keys.add(RedisSerializer.string().deserialize(item)); + }); + return null; + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + + return keys; + } + + /** + * 获取key的值 + * + * @param key 键 + * @return Object + * @author fzr + */ + public static Object get(String key) { + key = redisPrefix + key; + return redisTemplate.opsForValue().get(key); + } + + /** + * 获取旧值并设置新值 + * + * @param key 键 + * @param newVal 新值 + * @return Object + * @author fzr + */ + public static Object getSet(String key, Object newVal) { + key = redisPrefix + key; + return redisTemplate.opsForValue().getAndSet(key, newVal); + } + + /** + * 设置键值对 + * + * @param key 键 + * @param value 值 + * @author fzr + */ + public static void set(String key, Object value) { + key = redisPrefix + key; + redisTemplate.opsForValue().set(key, value); + } + + /** + * 设置键值对并设置时间 + * + * @param key 键 + * @param value 值 + * @param time time要大于0 如果time小于等于0 将设置无限期 + * @author fzr + */ + public static void set(String key, Object value, long time) { + key = redisPrefix + key; + if (time > 0) { + redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS); + } else { + set(key, value); + } + } + + /** + * 递增 + * + * @param key 键 + * @param delta 要增加几(大于0) + * @return Long + * @author fzr + */ + public static Long incr(String key, long delta) { + if (delta < 0) { + throw new RuntimeException("递增因子必须大于0"); + } + key = redisPrefix + key; + return redisTemplate.opsForValue().increment(key, delta); + } + + /** + * 递减 + * + * @param key 键 + * @param delta 要减少几(小于0) + * @return Long + * @author fzr + */ + public static Long decr(String key, long delta) { + if (delta < 0) { + throw new RuntimeException("递减因子必须大于0"); + } + key = redisPrefix + key; + return redisTemplate.opsForValue().increment(key, -delta); + } + + /* ***************** String end *************** */ + + /** + * 获取key中field域的值 + * + * @param key 键 不能为null + * @param field 项 不能为null + * @return 值 + * @author fzr + */ + public static Object hGet(String key, String field) { + key = redisPrefix + key; + return redisTemplate.opsForHash().get(key, field); + } + + /** + * 判断key中有没有field域名 + * + * @param key 键 + * @param field 字段 + * @return Boolean + * @author fzr + */ + public static Boolean hExists(String key, Object field) { + key = redisPrefix + key; + return redisTemplate.opsForHash().hasKey(key, field); + } + + /** + * 获取hashKey对应的所有键值 + * + * @param key 键 + * @return 对应的多个键值 + * @author fzr + */ + public Map hmGet(String key) { + key = redisPrefix + key; + return redisTemplate.opsForHash().entries(key); + } + + /** + * 设置field1->N个域,对应的值是value1->N + * + * @param key 键 + * @param map 对应多个键值 + * @author fzr + */ + public static void hmSet(String key, Map map) { + key = redisPrefix + key; + redisTemplate.opsForHash().putAll(key, map); + } + + /** + * HashSet 并设置时间 + * + * @param key 键 + * @param map 对应多个键值 + * @param time 时间(秒) + * @author fzr + */ + public static void hmSet(String key, Map map, long time) { + key = redisPrefix + key; + redisTemplate.opsForHash().putAll(key, map); + if (time > 0) { + expire(key, time); + } + } + + /** + * 向一张hash表中放入数据,如果不存在将创建 + * + * @param key 键 + * @param item 项 + * @param value 值 + * @author fzr + */ + public static void hSet(String key, String item, Object value) { + key = redisPrefix + key; + redisTemplate.opsForHash().put(key, item, value); + } + + /** + * 向一张hash表中放入数据,如果不存在将创建 + * + * @param key 键 + * @param item 项 + * @param value 值 + * @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间 + * @return true 成功 false失败 + * @author fzr + */ + public static boolean hSet(String key, String item, Object value, long time) { + key = redisPrefix + key; + redisTemplate.opsForHash().put(key, item, value); + if (time > 0) { + expire(key, time); + } + return true; + } + + /** + * 删除hash表中的值 + * + * @param key 键 不能为null + * @param item 项 可以使多个 不能为null + * @author fzr + */ + public static void hDel(String key, Object... item) { + key = redisPrefix + key; + redisTemplate.opsForHash().delete(key, item); + } + + /** + * 判断hash表中是否有该项的值 + * + * @param key 键 不能为null + * @param item 项 不能为null + * @return true 存在 false不存在 + * @author fzr + */ + public static boolean hHasKey(String key, String item) { + key = redisPrefix + key; + return redisTemplate.opsForHash().hasKey(key, item); + } + + /** + * hash递增 如果不存在,就会创建一个并把新增后的值返回 + * + * @param key 键 + * @param item 项 + * @param by 要增加几(大于0) + * @return double + * @author fzr + */ + public static double hIncr(String key, String item, long by) { + key = redisPrefix + key; + return redisTemplate.opsForHash().increment(key, item, by); + } + + /** + * hash递减 + * + * @param key 键 + * @param item 项 + * @param by 要减少记(小于0) + * @return double + * @author fzr + */ + public static double hDecr(String key, String item, long by) { + key = redisPrefix + key; + return redisTemplate.opsForHash().increment(key, item, -by); + } + + /* ***************** Map end *************** */ + + /** + * 根据key获取Set中的所有值 + * + * @param key 键 + * @return Set + * @author fzr + */ + public static Set sGet(String key) { + key = redisPrefix + key; + return redisTemplate.opsForSet().members(key); + } + + /** + * 根据value从一个set中查询,是否存在 + * + * @param key 键 + * @param value 值 + * @return true 存在 false不存在 + * @author fzr + */ + public Boolean sHasKey(String key, Object value) { + key = redisPrefix + key; + return redisTemplate.opsForSet().isMember(key, value); + } + + /** + * 将数据放入set缓存 + * + * @param key 键 + * @param values 值 可以是多个 + * @return 成功个数 + * @author fzr + */ + public static Long sSet(String key, Object... values) { + key = redisPrefix + key; + return redisTemplate.opsForSet().add(key, values); + } + + /** + * 将set数据放入缓存 + * + * @param key 键 + * @param time 时间(秒) + * @param values 值 可以是多个 + * @return 成功个数 + * @author fzr + */ + public Long sSetAndTime(String key, long time, Object... values) { + key = redisPrefix + key; + return redisTemplate.opsForSet().add(key, values); + } + + /** + * 获取set缓存的长度 + * + * @param key 键 + * @return Long + * @author fzr + */ + public Long sGetSetSize(String key) { + key = redisPrefix + key; + return redisTemplate.opsForSet().size(key); + } + + /** + * 移除值为value的 + * + * @param key 键 + * @param values 值 可以是多个 + * @return 移除的个数 + * @author fzr + */ + public Long setRemove(String key, Object... values) { + key = redisPrefix + key; + return redisTemplate.opsForSet().remove(key, values); + } + + /* ***************** Set end *************** */ + + /** + * 获取list缓存的内容 + * + * @param key 键 + * @param start 开始 + * @param end 结束 0 到 -1代表所有值 + * @return List + * @author fzr + */ + public List lGet(String key, long start, long end) { + key = redisPrefix + key; + return redisTemplate.opsForList().range(key, start, end); + } + + /** + * 获取list缓存的长度 + * + * @param key 键 + * @return Long + * @author fzr + */ + public Long lGetListSize(String key) { + key = redisPrefix + key; + return redisTemplate.opsForList().size(key); + } + + /** + * 通过索引获取list中的值 + * + * @param key 键 + * @param index 索引 index>=0时,0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推 + * @return Object + * @author fzr + */ + public Object lGetIndex(String key, long index) { + key = redisPrefix + key; + return redisTemplate.opsForList().index(key, index); + } + + /** + * 将list放入缓存 + * + * @param key 键 + * @param value 值 + * @return boolean + * @author fzr + */ + public boolean lSet(String key, Object value) { + key = redisPrefix + key; + redisTemplate.opsForList().rightPush(key, value); + return true; + } + + /** + * 将list放入缓存 + * + * @param key 键 + * @param value 值 + * @param second 时间(秒) + * @return boolean + * @author fzr + */ + public boolean lSet(String key, Object value, long second) { + key = redisPrefix + key; + redisTemplate.opsForList().rightPush(key, value); + if (second > 0) + expire(key, second); + return true; + } + + /** + * 将list放入缓存 + * + * @param key 键 + * @param value 值 + * @return boolean + * @author fzr + */ + public boolean lSet(String key, List value) { + key = redisPrefix + key; + redisTemplate.opsForList().rightPushAll(key, value); + return true; + } + + /** + * 将list放入缓存 + * + * @param key 键 + * @param value 值 + * @param time 时间(秒) + * @return boolean + * @author fzr + */ + public boolean lSet(String key, List value, Long time) { + key = redisPrefix + key; + redisTemplate.opsForList().rightPushAll(key, value); + if (time > 0) + expire(key, time); + return true; + } + + /** + * 根据索引修改list中的某条数据 + * + * @param key 键 + * @param index 索引 + * @param value 值 + * @return boolean + * @author fzr + */ + public boolean lUpdateIndex(String key, Long index, Object value) { + key = redisPrefix + key; + redisTemplate.opsForList().set(key, index, value); + return true; + } + + /** + * 移除N个值为value + * + * @param key 键 + * @param count 移除多少个 + * @param value 值 + * @return 移除的个数 + * @author fzr + */ + public Long lRemove(String key, Long count, Object value) { + key = redisPrefix + key; + return redisTemplate.opsForList().remove(key, count, value); + } + +} diff --git a/src/main/java/xyz/playedu/api/util/RequestUtil.java b/src/main/java/xyz/playedu/api/util/RequestUtil.java new file mode 100644 index 0000000..2c3dc1f --- /dev/null +++ b/src/main/java/xyz/playedu/api/util/RequestUtil.java @@ -0,0 +1,170 @@ +package xyz.playedu.api.util; + +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import java.util.Arrays; +import java.util.List; + +public class RequestUtil { + + /** + * 获取请求对象 + * + * @return HttpServletRequest + * @author fzr + */ + public static HttpServletRequest handler() { + ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + if (servletRequestAttributes != null) { + return servletRequestAttributes.getRequest(); + } + return null; + } + + /** + * 获取不带参请求URl + * 示例: https://127.0.0.1:8082/api/system/menu/menus + * + * @return String + * @author fzr + */ + public static String url() { + HttpServletRequest request = RequestUtil.handler(); + if (request != null) { + return request.getRequestURL().toString(); + } + return ""; + } + + /** + * 获取带端口的请求地址 + * 示例: https://127.0.0.1:8082 + * + * @return String + * @author fzr + */ + public static String uri() { + String domain = RequestUtil.domain(); + if (!Arrays.asList(443, 80, 0).contains(RequestUtil.port())) { + domain += ":" + RequestUtil.port(); + } + + return domain; + } + + /** + * 获取请求路由 + * 示例: /api/system/menu/menus + * + * @return String + * @author fzr + */ + public static String route() { + HttpServletRequest request = RequestUtil.handler(); + if (request != null) { + return request.getRequestURI(); + } + return ""; + } + + /** + * 获取请求端口 + * 示例: 443/80 + * + * @return Integer + * @author fzr + */ + public static Integer port() { + HttpServletRequest request = RequestUtil.handler(); + if (request != null) { + return request.getServerPort(); + } + return 0; + } + + /** + * 获取请求域名 + * 示例: https://127.0.0.1 + * + * @return String + * @author fzr + */ + public static String domain() { + HttpServletRequest request = RequestUtil.handler(); + if (request != null) { + String requestUrl = request.getRequestURL().toString(); + List urls = Arrays.asList(requestUrl.split("/")); + + String agree = "http:"; + if (request.getServerPort() == 443) { + agree = "https:"; + } + + return agree + "//" + urls.get(2).split(":")[0]; + } + return null; + } + + /** + * 判断是否是GET请求 + * + * @return Boolean + * @author fzr + */ + public static Boolean isGet() { + ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + if (servletRequestAttributes != null) { + HttpServletRequest request = servletRequestAttributes.getRequest(); + return request.getMethod().equals("GET"); + } + return false; + } + + /** + * 判断是否是POST请求 + * + * @return Boolean + * @author fzr + */ + public static Boolean isPost() { + ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + if (servletRequestAttributes != null) { + HttpServletRequest request = servletRequestAttributes.getRequest(); + return request.getMethod().equals("POST"); + } + return false; + } + + /** + * 判断是否是PUT请求 + * + * @return Boolean + * @author fzr + */ + public static Boolean isPUT() { + ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + if (servletRequestAttributes != null) { + HttpServletRequest request = servletRequestAttributes.getRequest(); + return request.getMethod().equals("PUT"); + } + return false; + } + + /** + * 判断是否是DELETE请求 + * + * @return Boolean + * @author fzr + */ + public static Boolean isDelete() { + ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); + if (servletRequestAttributes != null) { + HttpServletRequest request = servletRequestAttributes.getRequest(); + return request.getMethod().equals("DELETE"); + } + return false; + } + +} diff --git a/src/main/java/xyz/playedu/api/util/StringUtil.java b/src/main/java/xyz/playedu/api/util/StringUtil.java new file mode 100644 index 0000000..4895b35 --- /dev/null +++ b/src/main/java/xyz/playedu/api/util/StringUtil.java @@ -0,0 +1,538 @@ +package xyz.playedu.api.util; + +import org.springframework.util.AntPathMatcher; + +import java.util.*; + +/** + * 字符串工具类 + */ +public class StringUtil extends org.apache.commons.lang3.StringUtils { + /** + * 空字符串 + */ + private static final String NULL_STR = ""; + + /** + * 下划线 + */ + private static final char SEPARATOR = '_'; + + /** + * 获取参数不为空值 + * + * @param value defaultValue 要判断的value + * @return value 返回值 + */ + public static T nvl(T value, T defaultValue) { + return value != null ? value : defaultValue; + } + + /** + * 判断一个Collection是否为空,包含List, Set, Queue + * + * @param coll 要判断的Collection + * @return true=为空, false=非空 + */ + public static boolean isEmpty(Collection coll) { + return isNull(coll) || coll.isEmpty(); + } + + /** + * 判断一个Collection是否非空,包含List, Set, Queue + * + * @param coll 要判断的Collection + * @return true=非空, false=空 + */ + public static boolean isNotEmpty(Collection coll) { + return !isEmpty(coll); + } + + /** + * 判断一个对象数组是否为空 + * + * @param objects 要判断的对象数组 + * @return true=为空, false=非空 + */ + public static boolean isEmpty(Object[] objects) { + return isNull(objects) || (objects.length == 0); + } + + /** + * 判断一个对象数组是否非空 + * + * @param objects 要判断的对象数组 + * @return true=非空, false=空 + */ + public static boolean isNotEmpty(Object[] objects) { + return !isEmpty(objects); + } + + /** + * 判断一个Map是否为空 + * + * @param map 要判断的Map + * @return true=为空, false=非空 + */ + public static boolean isEmpty(Map map) { + return isNull(map) || map.isEmpty(); + } + + /** + * 判断一个Map是否为空 + * + * @param map 要判断的Map + * @return true=非空, false=空 + */ + public static boolean isNotEmpty(Map map) { + return !isEmpty(map); + } + + /** + * 判断一个字符串是否为空串 + * + * @param str String + * @return true=为空, false=非空 + */ + public static boolean isEmpty(String str) { + return isNull(str) || NULL_STR.equals(str.trim()); + } + + /** + * 判断一个字符串是否为非空串 + * + * @param str String + * @return true=非空串, false=空串 + */ + public static boolean isNotEmpty(String str) { + return !isEmpty(str); + } + + /** + * 判断一个对象是否为空 + * + * @param object Object + * @return true=为空, false=非空 + */ + public static boolean isNull(Object object) { + return object == null; + } + + /** + * 判断一个对象是否非空 + * + * @param object Object + * @return true=非空, false=空 + */ + public static boolean isNotNull(Object object) { + return !isNull(object); + } + + /** + * 判断一个对象是否是数组类型(Java基本型别的数组) + * + * @param object 对象 + * @return true=是数组, false=不是数组 + */ + public static boolean isArray(Object object) { + return isNotNull(object) && object.getClass().isArray(); + } + + /** + * 去空格 + */ + public static String trim(String str) { + return (str == null ? "" : str.trim()); + } + + /** + * 截取字符串 + * + * @param str 字符串 + * @param start 开始 + * @return 结果 + */ + public static String substring(final String str, int start) { + if (str == null) { + return NULL_STR; + } + + if (start < 0) { + start = str.length() + start; + } + + if (start < 0) { + start = 0; + } + + if (start > str.length()) { + return NULL_STR; + } + + return str.substring(start); + } + + /** + * 截取字符串 + * + * @param str 字符串 + * @param start 开始 + * @param end 结束 + * @return 结果 + */ + public static String substring(final String str, int start, int end) { + if (str == null) { + return NULL_STR; + } + + if (end < 0) { + end = str.length() + end; + } + + if (start < 0) { + start = str.length() + start; + } + + if (end > str.length()) { + end = str.length(); + } + + if (start > end) { + return NULL_STR; + } + + if (start < 0) { + start = 0; + } + + if (end < 0) { + end = 0; + } + + return str.substring(start, end); + } + + + /** + * 字符串转set + * + * @param str 字符串 + * @param sep 分隔符 + * @return set集合 + */ + public static Set str2Set(String str, String sep) { + return new HashSet(str2List(str, sep, true, false)); + } + + /** + * 字符串转list + * + * @param str 字符串 + * @param sep 分隔符 + * @param filterBlank 过滤纯空白 + * @param trim 去掉首尾空白 + * @return list集合 + */ + public static List str2List(String str, String sep, boolean filterBlank, boolean trim) { + List list = new ArrayList(); + if (StringUtil.isEmpty(str)) { + return list; + } + + // 过滤空白字符串 + if (filterBlank && StringUtil.isBlank(str)) { + return list; + } + + String[] split = str.split(sep); + for (String string : split) { + if (filterBlank && StringUtil.isBlank(string)) { + continue; + } + if (trim) { + string = string.trim(); + } + list.add(string); + } + + return list; + } + + /** + * 查找指定字符串是否包含指定字符串列表中的任意一个字符串同时串忽略大小写 + * + * @param cs 指定字符串 + * @param searchCharSequences 需要检查的字符串数组 + * @return 是否包含任意一个字符串 + */ + public static boolean containsAnyIgnoreCase(CharSequence cs, CharSequence... searchCharSequences) { + if (isEmpty(cs) || isEmpty(searchCharSequences)) + { + return false; + } + for (CharSequence testStr : searchCharSequences) + { + if (containsIgnoreCase(cs, testStr)) + { + return true; + } + } + return false; + } + + /** + * 驼峰转下划线命名 + */ + public static String toUnderScoreCase(String str) { + if (str == null) { + return null; + } + StringBuilder sb = new StringBuilder(); + // 前置字符是否大写 + boolean preCharIsUpperCase = true; + // 当前字符是否大写 + boolean cureCharIsUpperCase = true; + // 下一字符是否大写 + boolean nextCharIsUpperCase = true; + for (int i = 0; i < str.length(); i++) { + char c = str.charAt(i); + if (i > 0) { + preCharIsUpperCase = Character.isUpperCase(str.charAt(i - 1)); + } else { + preCharIsUpperCase = false; + } + + cureCharIsUpperCase = Character.isUpperCase(c); + + if (i < (str.length() - 1)) { + nextCharIsUpperCase = Character.isUpperCase(str.charAt(i + 1)); + } + + if (preCharIsUpperCase && cureCharIsUpperCase && !nextCharIsUpperCase) { + sb.append(SEPARATOR); + } else if ((i != 0 && !preCharIsUpperCase) && cureCharIsUpperCase) + { + sb.append(SEPARATOR); + } + sb.append(Character.toLowerCase(c)); + } + + return sb.toString(); + } + + /** + * 是否包含字符串 + * + * @param str 验证字符串 + * @param strArr 字符串组 + * @return 包含返回true + */ + public static boolean inStringIgnoreCase(String str, String... strArr) { + if (str != null && strArr != null) { + for (String s : strArr) { + if (str.equalsIgnoreCase(trim(s))) { + return true; + } + } + } + return false; + } + + /** + * 将下划线大写方式命名的字符串转换为驼峰式。 + * 如果转换前的下划线大写方式命名的字符串为空, + * 则返回空字符串。 例如:HELLO_WORLD->HelloWorld + * + * @param name 转换前的下划线大写方式命名的字符串 + * @return 转换后的驼峰式命名的字符串 + */ + public static String convertToCamelCase(String name) { + StringBuilder result = new StringBuilder(); + // 快速检查 + if (name == null || name.isEmpty()) { + // 没必要转换 + return ""; + } + else if (!name.contains("_")) { + // 不含下划线,仅将首字母大写 + return name.substring(0, 1).toUpperCase() + name.substring(1); + } + // 用下划线将原始字符串分割 + String[] camels = name.split("_"); + for (String camel : camels) { + // 跳过原始字符串中开头、结尾的下换线或双重下划线 + if (camel.isEmpty()) { + continue; + } + // 首字母大写 + result.append(camel.substring(0, 1).toUpperCase()); + result.append(camel.substring(1).toLowerCase()); + } + return result.toString(); + } + + /** + * 驼峰式命名法 例如:user_name->userName + * + * @param s 字符串 + * @return 驼峰字符串 + */ + public static String toCamelCase(String s) { + if (s == null) { + return null; + } + s = s.toLowerCase(); + StringBuilder sb = new StringBuilder(s.length()); + boolean upperCase = false; + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + + if (c == SEPARATOR) { + upperCase = true; + } else if (upperCase) { + sb.append(Character.toUpperCase(c)); + upperCase = false; + } else { + sb.append(c); + } + } + return sb.toString(); + } + + /** + * 查找指定字符串是否匹配指定字符串列表中的任意一个字符串 + * + * @param str 指定字符串 + * @param strArr 需要检查的字符串数组 + * @return 是否匹配 + */ + public static boolean matches(String str, List strArr) { + if (isEmpty(str) || isEmpty(strArr)) { + return false; + } + for (String pattern : strArr) { + if (isMatch(pattern, str)) { + return true; + } + } + return false; + } + + /** + * 判断url是否与规则配置: + * ? 表示单个字符; + * * 表示一层路径内的任意字符串,不可跨层级; + * ** 表示任意层路径; + * + * @param pattern 匹配规则 + * @param url 需要匹配的url + * @return boolean + */ + public static boolean isMatch(String pattern, String url) { + AntPathMatcher matcher = new AntPathMatcher(); + return matcher.match(pattern, url); + } + + /** + * 数字左边补齐0,使之达到指定长度。 + * 注意,如果数字转换为字符串后,长度大于size,则只保留 最后size个字符。 + * + * @param num 数字对象 + * @param size 字符串指定长度 + * @return 返回数字的字符串格式,该字符串为指定长度。 + */ + public static String padL(final Number num, final int size) { + return padL(num.toString(), size, '0'); + } + + /** + * 字符串左补齐 + * 如果原始字符串s长度大于size,则只保留最后size个字符。 + * + * @param s 原始字符串 + * @param size 字符串指定长度 + * @param c 用于补齐的字符 + * @return 返回指定长度的字符串,由原字符串左补齐或截取得到。 + */ + public static String padL(final String s, final int size, final char c) { + final StringBuilder sb = new StringBuilder(size); + if (s != null) { + final int len = s.length(); + if (s.length() <= size) { + for (int i = size - len; i > 0; i--) { + sb.append(c); + } + sb.append(s); + } else { + return s.substring(len - size, len); + } + } else { + for (int i = size; i > 0; i--) { + sb.append(c); + } + } + return sb.toString(); + } + + /** + * 格式化文本, {} 表示占位符
+ * 此方法只是简单将占位符 {} 按照顺序替换为参数
+ * 如果想输出 {} 使用 \\转义 { 即可,如果想输出 {} 之前的 \ 使用双转义符 \\\\ 即可
+ * 例:
+ * 通常使用:format("this is {} for {}", "a", "b") -> this is a for b
+ * 转义{}: format("this is \\{} for {}", "a", "b") -> this is \{} for a
+ * 转义\: format("this is \\\\{} for {}", "a", "b") -> this is \a for b
+ * + * @param strPattern 文本模板,被替换的部分用 {} 表示 + * @param argArray 参数值 + * @return 格式化后的文本 + */ + public static String format(String strPattern, Object... argArray) { + String EMPTY_JSON = "{}"; + char C_BACKSLASH = '\\'; + char C_DELIM_START = '{'; + + if (isEmpty(argArray) || isEmpty(strPattern)) { + return strPattern; + } + + final int strPatternLength = strPattern.length(); + StringBuilder sbuf = new StringBuilder(strPatternLength + 50); + int handledPosition = 0; + int delimIndex; + for (int argIndex = 0; argIndex < argArray.length; argIndex++) { + delimIndex = strPattern.indexOf(EMPTY_JSON, handledPosition); + if (delimIndex == -1) { + if (handledPosition == 0) { + return strPattern; + } else { + sbuf.append(strPattern, handledPosition, strPatternLength); + return sbuf.toString(); + } + } else { + if (delimIndex > 0 && strPattern.charAt(delimIndex - 1) == C_BACKSLASH) { + if (delimIndex > 1 && strPattern.charAt(delimIndex - 2) == C_BACKSLASH) { + sbuf.append(strPattern, handledPosition, delimIndex - 1); + sbuf.append(argArray[argIndex]); + handledPosition = delimIndex + 2; + } else { + // 占位符被转义 + argIndex--; + sbuf.append(strPattern, handledPosition, delimIndex - 1); + sbuf.append(C_DELIM_START); + handledPosition = delimIndex + 1; + } + } else { + // 正常占位符 + sbuf.append(strPattern, handledPosition, delimIndex); + sbuf.append(argArray[argIndex]); + handledPosition = delimIndex + 2; + } + } + } + + sbuf.append(strPattern, handledPosition, strPattern.length()); + return sbuf.toString(); + } + +} \ No newline at end of file diff --git a/src/main/java/xyz/playedu/api/util/ToolUtil.java b/src/main/java/xyz/playedu/api/util/ToolUtil.java new file mode 100644 index 0000000..1c2eab9 --- /dev/null +++ b/src/main/java/xyz/playedu/api/util/ToolUtil.java @@ -0,0 +1,188 @@ +package xyz.playedu.api.util; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.google.gson.reflect.TypeToken; + +import java.io.*; +import java.lang.reflect.Type; +import java.net.URL; +import java.net.URLConnection; +import java.util.*; + +public class ToolUtil { + + /** + * 制作UUID + * + * @return String + * @author fzr + */ + public static String uuid() { + return UUID.randomUUID().toString().replaceAll("-", "").substring(0, 32); + } + + /** + * 返回随机字符串 + * + * @param length 要生成的长度 + * @return String + * @author fzr + */ + public static String randomString(int length) { + Random random = new Random(); + StringBuilder stringBuffer = new StringBuilder(); + String str = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; + int strLength = str.length(); + for (int i = 0; i < length; i++) { + int index = random.nextInt(strLength); + stringBuffer.append(str.charAt(index)); + } + return stringBuffer.toString(); + } + + /** + * 返回随机数字字符串 + * + * @param length 要生成的长度 + * @return String + * @author fzr + */ + public static String randomInt(int length) { + Random random = new Random(); + StringBuilder stringBuffer = new StringBuilder(); + String str = "0123456789"; + for (int i = 0; i < length; i++) { + int index = random.nextInt(10); + stringBuffer.append(str.charAt(index)); + } + return stringBuffer.toString(); + } + + /** + * 转换存储单位: KB MB GB TB + * + * @return String + * @author fzr + */ + public static String storageUnit(Long size) { + if (size == null) { + return "0B"; + } + if (size < 1024) { + return size + "B"; + } else { + size = size / 1024; + } + if (size < 1024) { + return size + "KB"; + } else { + size = size / 1024; + } + if (size < 1024) { + size = size * 100; + return (size / 100) + "." + (size % 100) + "MB"; + } else { + size = size * 100 / 1024; + return (size / 100) + "." + (size % 100) + "GB"; + } + } + + /** + * 下载文件 + * + * @param urlString (文件网址) + * @param savePath (保存路径,如: /www/uploads) + * @param filename (保存名称,如: aa.png) + * @throws IOException 异常 + * @author fzr + */ + public static void download(String urlString, String savePath, String filename) throws IOException { + URL url = new URL(urlString); + URLConnection con = url.openConnection(); + con.setConnectTimeout(20 * 1000); + File sf = new File(savePath); + if (!sf.exists()) { + if (sf.mkdirs()) { + throw new IOException("创建目录失败"); + } + } + try (InputStream in = con.getInputStream(); OutputStream out = new FileOutputStream(sf.getPath() + "\\" + filename)) { + byte[] buff = new byte[1024]; + int n; + while ((n = in.read(buff)) >= 0) { + out.write(buff, 0, n); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * JSON转 Map + * + * @param json 对象 + * @return Map + * @author fzr + */ + public static Map jsonToMap(String json) { + Type type = new TypeToken>() { + }.getType(); + return JSON.parseObject(json, type); + } + + /** + * JSON转 Map + * + * @param json 对象 + * @return Map + * @author fzr + */ + public static Map jsonToMapAsObj(String json) { + Type type = new TypeToken>() { + }.getType(); + return JSON.parseObject(json, type); + } + + /** + * JSON转Map + * + * @param object 对象 + * @return Map + * @author fzr + */ + public static Map objectToMap(Object object) { + Type type = new TypeToken>() { + }.getType(); + return JSON.parseObject(JSONObject.toJSONString(object), type); + } + + /** + * 对象类型Map合并 + * + * @param map 对象 + * @return Object + * @author fzr + */ + public static Map mergeMapByObj(Map map, Map map1) { + HashMap map2 = new HashMap<>(); + map2.putAll(map); + map2.putAll(map1); + return map2; + } + + /** + * 字符串类型Map合并 + * + * @param map 对象 + * @return Object + * @author fzr + */ + public static Map mergeMapByStr(Map map, Map map1) { + HashMap map2 = new HashMap<>(); + map2.putAll(map); + map2.putAll(map1); + return map2; + } + +} diff --git a/src/main/resources/application.example b/src/main/resources/application.example deleted file mode 100644 index bcfd4be..0000000 --- a/src/main/resources/application.example +++ /dev/null @@ -1,4 +0,0 @@ -spring.datasource.url=jdbc:mysql://127.0.0.1:33060/playedu -spring.datasource.username=root -spring.datasource.password=root -spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties deleted file mode 100644 index 67e4f82..0000000 --- a/src/main/resources/application.properties +++ /dev/null @@ -1,11 +0,0 @@ -# ???? -spring.profiles.active=dev - -#mysql -spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver - -#mybatis -mybatis.mapper-locations=classpath:mapper/*.xml - -#mybatis-plus -mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..376e155 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,38 @@ +spring: + profiles: + active: "dev" + aop: + auto: true + proxy-target-class: true + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: "jdbc:mysql://127.0.0.1:33060/java-meedu?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=false" + username: "root" + password: "root" + # Redis配置 + data: + redis: + host: "127.0.0.1" + port: 6379 + password: + database: 0 + timeout: 5000 + lettuce: + pool: + max-wait: 30000 # 连接池最大阻塞等待时间(使用负数表示没有限制,默认-1) + max-active: 100 # 连接池最大连接数(使用负数表示没有限制,默认8) + max-idle: 20 # 连接池中的最大空闲连接(默认8) + min-idle: 5 # 连接池中的最小空闲连接(默认0) + +mybatis: + mapper-locations: classpath:mapper/*.xml + +mybatis-plus: + configuration: + log-impl: org.apache.ibatis.logging.stdout.StdOutImpl + +playedu: + captcha: + expire: 300 #分5钟 + cache-name: "captcha:key:" + diff --git a/src/main/resources/mapper/AdminUserMapper.xml b/src/main/resources/mapper/AdminUserMapper.xml index 9fd2235..6bda21b 100644 --- a/src/main/resources/mapper/AdminUserMapper.xml +++ b/src/main/resources/mapper/AdminUserMapper.xml @@ -9,6 +9,7 @@ + @@ -19,8 +20,8 @@ id,name,email, - password,login_ip,login_at, - is_ban_login,login_times,created_at, - updated_at + password,salt,login_ip, + login_at,is_ban_login,login_times, + created_at,updated_at