diff --git a/pom.xml b/pom.xml index cc9f5ae..5a8983f 100644 --- a/pom.xml +++ b/pom.xml @@ -17,13 +17,15 @@ 17 - - org.springframework.boot - spring-boot-starter-data-redis - org.springframework.boot spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-json + + org.springframework.boot @@ -38,50 +40,63 @@ mybatis-spring-boot-starter 3.0.0 - + + org.springframework.boot + spring-boot-starter-data-redis + org.springframework.boot spring-boot-devtools runtime true - - com.mysql - mysql-connector-j - runtime - - - org.projectlombok - lombok - true - org.springframework.boot spring-boot-starter-test test + + com.mysql + mysql-connector-j + runtime + + + + org.projectlombok + lombok + true + + com.baomidou mybatis-plus-boot-starter 3.5.3 + org.springframework.boot spring-boot-starter-validation + com.github.penggle kaptcha 2.3.2 + com.alibaba.fastjson2 fastjson2 - 2.0.22 + 2.0.21 + + + com.alibaba.fastjson2 + fastjson2-extension + 2.0.21 @@ -96,6 +111,7 @@ 3.12.0 + io.jsonwebtoken jjwt-api @@ -113,6 +129,7 @@ 0.11.5 compile + diff --git a/src/main/java/xyz/playedu/api/bus/BackendBus.java b/src/main/java/xyz/playedu/api/bus/BackendBus.java new file mode 100644 index 0000000..149686d --- /dev/null +++ b/src/main/java/xyz/playedu/api/bus/BackendBus.java @@ -0,0 +1,16 @@ +package xyz.playedu.api.bus; + +import xyz.playedu.api.constant.BackendConstant; + +public class BackendBus { + + public static boolean inUnAuthWhitelist(String uri) { + for (int i = 0; i < BackendConstant.UN_AUTH_URI_WHITELIST.length; i++) { + if (uri.equals(BackendConstant.UN_AUTH_URI_WHITELIST[i])) { + return true; + } + } + return false; + } + +} diff --git a/src/main/java/xyz/playedu/api/config/RedisConfig.java b/src/main/java/xyz/playedu/api/config/RedisConfig.java index b4b71d9..8dc9acd 100644 --- a/src/main/java/xyz/playedu/api/config/RedisConfig.java +++ b/src/main/java/xyz/playedu/api/config/RedisConfig.java @@ -1,10 +1,10 @@ package xyz.playedu.api.config; +import com.alibaba.fastjson2.support.spring.data.redis.FastJsonRedisSerializer; 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 @@ -13,14 +13,17 @@ 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); + + // key值采用StringRedisSerializer序列化 + StringRedisSerializer stringRedisSerializer = new StringRedisSerializer(); redisTemplate.setKeySerializer(stringRedisSerializer); redisTemplate.setHashKeySerializer(stringRedisSerializer); - redisTemplate.setHashValueSerializer(JsonRedisSerializer); + + // value值采用fastjson2序列化 + FastJsonRedisSerializer fastJsonRedisSerializer = new FastJsonRedisSerializer(Object.class); + redisTemplate.setValueSerializer(fastJsonRedisSerializer); + redisTemplate.setHashValueSerializer(fastJsonRedisSerializer); return redisTemplate; } diff --git a/src/main/java/xyz/playedu/api/config/WebMvcConfig.java b/src/main/java/xyz/playedu/api/config/WebMvcConfig.java new file mode 100644 index 0000000..607e380 --- /dev/null +++ b/src/main/java/xyz/playedu/api/config/WebMvcConfig.java @@ -0,0 +1,68 @@ +package xyz.playedu.api.config; + +import com.alibaba.fastjson2.JSONReader; +import com.alibaba.fastjson2.JSONWriter; +import com.alibaba.fastjson2.support.config.FastJsonConfig; +import com.alibaba.fastjson2.support.spring.http.converter.FastJsonHttpMessageConverter; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.MediaType; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import xyz.playedu.api.middleware.AdminAuthMiddleware; + +import java.util.ArrayList; +import java.util.List; + +@Configuration +@Slf4j +public class WebMvcConfig implements WebMvcConfigurer { + + @Resource + private AdminAuthMiddleware adminAuthMiddleware; + + @Value("${playedu.cors.origins}") + private String ConfigOrigins; + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(adminAuthMiddleware).addPathPatterns("/backend/**"); + } + + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) { + WebMvcConfigurer.super.addResourceHandlers(registry); + } + + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**").allowedOrigins(ConfigOrigins).allowedHeaders("*").allowedMethods("GET", "POST", "DELETE", "PUT").maxAge(3600); + } + + @Override + public void configureMessageConverters(List> converters) { + converters.add(fastJsonHttpMessageConverter()); + } + + private HttpMessageConverter fastJsonHttpMessageConverter() { + FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter(); + + // 中文乱码解决方案 + List mediaTypes = new ArrayList<>(); + mediaTypes.add(MediaType.APPLICATION_JSON); + fastConverter.setSupportedMediaTypes(mediaTypes); + + // 配置 + FastJsonConfig fastJsonConfig = new FastJsonConfig(); + fastJsonConfig.setDateFormat("yyyy-MM-dd HH:mm:ss"); + fastJsonConfig.setWriterFeatures(JSONWriter.Feature.PrettyFormat); + fastConverter.setFastJsonConfig(fastJsonConfig); + + return fastConverter; + } +} diff --git a/src/main/java/xyz/playedu/api/constant/BackendConstant.java b/src/main/java/xyz/playedu/api/constant/BackendConstant.java new file mode 100644 index 0000000..bb0346d --- /dev/null +++ b/src/main/java/xyz/playedu/api/constant/BackendConstant.java @@ -0,0 +1,5 @@ +package xyz.playedu.api.constant; + +public class BackendConstant { + public final static String[] UN_AUTH_URI_WHITELIST = {"/backend/v1/system/image-captcha", "/backend/v1/auth/login",}; +} diff --git a/src/main/java/xyz/playedu/api/controller/admin/AdminUserController.java b/src/main/java/xyz/playedu/api/controller/backend/AdminUserController.java similarity index 77% rename from src/main/java/xyz/playedu/api/controller/admin/AdminUserController.java rename to src/main/java/xyz/playedu/api/controller/backend/AdminUserController.java index ed3be58..f413de2 100644 --- a/src/main/java/xyz/playedu/api/controller/admin/AdminUserController.java +++ b/src/main/java/xyz/playedu/api/controller/backend/AdminUserController.java @@ -1,24 +1,25 @@ -package xyz.playedu.api.controller.admin; +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.RequestParam; import org.springframework.web.bind.annotation.RestController; -import xyz.playedu.api.constant.SystemConstant; import xyz.playedu.api.domain.AdminUser; -import xyz.playedu.api.middleware.AuthMiddleware; import xyz.playedu.api.service.AdminUserService; import xyz.playedu.api.types.PaginationResult; import xyz.playedu.api.types.JsonResponse; @RestController +@Slf4j +@RequestMapping("/backend/v1/admin-user") public class AdminUserController { @Autowired private AdminUserService adminUserService; - @AuthMiddleware(prv = SystemConstant.JWT_PRV_ADMIN_USER) - @GetMapping("/admin/user/index") + @GetMapping("/index") 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/backend/LoginController.java similarity index 88% rename from src/main/java/xyz/playedu/api/controller/admin/LoginController.java rename to src/main/java/xyz/playedu/api/controller/backend/LoginController.java index 06036a8..2a55dd2 100644 --- a/src/main/java/xyz/playedu/api/controller/admin/LoginController.java +++ b/src/main/java/xyz/playedu/api/controller/backend/LoginController.java @@ -1,4 +1,4 @@ -package xyz.playedu.api.controller.admin; +package xyz.playedu.api.controller.backend; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; @@ -9,6 +9,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import xyz.playedu.api.constant.SystemConstant; import xyz.playedu.api.domain.AdminUser; +import xyz.playedu.api.exception.JwtLogoutException; import xyz.playedu.api.middleware.ImageCaptchaCheckMiddleware; import xyz.playedu.api.request.LoginRequest; import xyz.playedu.api.service.AdminUserService; @@ -22,7 +23,7 @@ import java.util.HashMap; @Slf4j @RestController -@RequestMapping("/admin/v1/auth") +@RequestMapping("/backend/v1/auth") public class LoginController { @Autowired @@ -57,8 +58,10 @@ public class LoginController { } @PostMapping("/logout") - public JsonResponse logout() { + public JsonResponse logout() throws JwtLogoutException { + jwtService.logout(RequestUtil.token(), SystemConstant.JWT_PRV_ADMIN_USER); 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/backend/SystemController.java similarity index 92% rename from src/main/java/xyz/playedu/api/controller/admin/SystemController.java rename to src/main/java/xyz/playedu/api/controller/backend/SystemController.java index 8fd084a..256c261 100644 --- a/src/main/java/xyz/playedu/api/controller/admin/SystemController.java +++ b/src/main/java/xyz/playedu/api/controller/backend/SystemController.java @@ -1,4 +1,4 @@ -package xyz.playedu.api.controller.admin; +package xyz.playedu.api.controller.backend; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; @@ -12,7 +12,7 @@ import java.io.IOException; import java.util.HashMap; @RestController -@RequestMapping("/admin/v1/system") +@RequestMapping("/backend/v1/system") public class SystemController { @Autowired diff --git a/src/main/java/xyz/playedu/api/middleware/AdminAuthMiddleware.java b/src/main/java/xyz/playedu/api/middleware/AdminAuthMiddleware.java new file mode 100644 index 0000000..f5df9f2 --- /dev/null +++ b/src/main/java/xyz/playedu/api/middleware/AdminAuthMiddleware.java @@ -0,0 +1,63 @@ +package xyz.playedu.api.middleware; + +import com.alibaba.fastjson2.JSON; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; +import xyz.playedu.api.PlayEduThreadLocal; +import xyz.playedu.api.bus.BackendBus; +import xyz.playedu.api.constant.SystemConstant; +import xyz.playedu.api.service.JWTService; +import xyz.playedu.api.types.JWTPayload; +import xyz.playedu.api.types.JsonResponse; +import xyz.playedu.api.util.RequestUtil; + +import java.io.IOException; + +@Component +@Slf4j +public class AdminAuthMiddleware implements HandlerInterceptor { + + @Autowired + private JWTService jwtService; + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + if (BackendBus.inUnAuthWhitelist(request.getRequestURI())) { + return HandlerInterceptor.super.preHandle(request, response, handler); + } + + String token = RequestUtil.token(); + if (token.length() == 0) { + responseTransform(response, 401, "请登录"); + return false; + } + + try { + JWTPayload payload = jwtService.parse(token, SystemConstant.JWT_PRV_ADMIN_USER); + + // 用户信息写入context + PlayEduThreadLocal.setAdminUserId(payload.getSub()); + + return HandlerInterceptor.super.preHandle(request, response, handler); + } catch (Exception e) { + responseTransform(response, 401, "请重新登录"); + return false; + } + } + + private void responseTransform(HttpServletResponse response, int code, String msg) throws IOException { + response.setStatus(code); + response.setContentType("application/json;charset=utf-8"); + response.getWriter().print(JSON.toJSONString(JsonResponse.error(msg))); + } + + @Override + public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { + PlayEduThreadLocal.remove(); + HandlerInterceptor.super.afterCompletion(request, response, handler, ex); + } +} diff --git a/src/main/java/xyz/playedu/api/middleware/AuthMiddleware.java b/src/main/java/xyz/playedu/api/middleware/AuthMiddleware.java deleted file mode 100644 index b324d28..0000000 --- a/src/main/java/xyz/playedu/api/middleware/AuthMiddleware.java +++ /dev/null @@ -1,12 +0,0 @@ -package xyz.playedu.api.middleware; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -@Target({ElementType.METHOD}) -@Retention(RetentionPolicy.RUNTIME) -public @interface AuthMiddleware { - String prv(); -} diff --git a/src/main/java/xyz/playedu/api/middleware/ImageCaptchaCheckMiddleware.java b/src/main/java/xyz/playedu/api/middleware/ImageCaptchaCheckMiddleware.java index 1e1651b..104d4e9 100644 --- a/src/main/java/xyz/playedu/api/middleware/ImageCaptchaCheckMiddleware.java +++ b/src/main/java/xyz/playedu/api/middleware/ImageCaptchaCheckMiddleware.java @@ -2,6 +2,7 @@ package xyz.playedu.api.middleware; import java.lang.annotation.*; +@Documented @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface ImageCaptchaCheckMiddleware { diff --git a/src/main/java/xyz/playedu/api/middleware/impl/AuthMiddlewareImpl.java b/src/main/java/xyz/playedu/api/middleware/impl/AuthMiddlewareImpl.java deleted file mode 100644 index 140641a..0000000 --- a/src/main/java/xyz/playedu/api/middleware/impl/AuthMiddlewareImpl.java +++ /dev/null @@ -1,34 +0,0 @@ -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.aspectj.lang.reflect.MethodSignature; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; -import xyz.playedu.api.middleware.AuthMiddleware; -import xyz.playedu.api.service.JWTService; - -@Aspect -@Component -@Slf4j -public class AuthMiddlewareImpl { - - @Autowired - private JWTService jwtService; - - @Pointcut("@annotation(xyz.playedu.api.middleware.AuthMiddleware)") - private void doPointcut() { - } - - @Around("doPointcut()") - public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable { - MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); - AuthMiddleware authMiddleware = methodSignature.getMethod().getAnnotation(AuthMiddleware.class); - log.info("prv的值:" + authMiddleware.prv()); - return joinPoint.proceed(); - } - -} diff --git a/src/main/java/xyz/playedu/api/service/JWTService.java b/src/main/java/xyz/playedu/api/service/JWTService.java index 6a937b4..a70eef0 100644 --- a/src/main/java/xyz/playedu/api/service/JWTService.java +++ b/src/main/java/xyz/playedu/api/service/JWTService.java @@ -1,7 +1,15 @@ package xyz.playedu.api.service; +import xyz.playedu.api.exception.JwtLogoutException; +import xyz.playedu.api.types.JWTPayload; import xyz.playedu.api.types.JwtToken; public interface JWTService { JwtToken generate(Integer userId, String iss, String prv); + + boolean isInBlack(String jti); + + void logout(String token, String prv) throws JwtLogoutException; + + JWTPayload parse(String token, String prv) throws JwtLogoutException; } diff --git a/src/main/java/xyz/playedu/api/util/RequestUtil.java b/src/main/java/xyz/playedu/api/util/RequestUtil.java index 19651e2..af76292 100644 --- a/src/main/java/xyz/playedu/api/util/RequestUtil.java +++ b/src/main/java/xyz/playedu/api/util/RequestUtil.java @@ -23,6 +23,18 @@ public class RequestUtil { return null; } + public static String token() { + HttpServletRequest request = RequestUtil.handler(); + if (request == null) { + return ""; + } + String token = request.getHeader("Authorization"); + if (token == null || token.length() == 0 || token.split(" ").length != 2) { + return ""; + } + return token.split(" ")[1]; + } + /** * 获取不带参请求URl * 示例: https://127.0.0.1:8082/api/system/menu/menus diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index b985512..0aa09bc 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -42,5 +42,7 @@ playedu: key: "eJTJSLPv13fw9twbuPoeicypLqnSfYWL" #32个字符,加密key用来加密jwt的数据[运行本系统之前请务必修改] expire: 1296000 #token有效期[单位:秒,默认15天] cache-black-prefix: "jwt:blk:" #主动注销的token黑名单缓存前缀 - + # CORS + cors: + origins: "*"