From 3fdcc1e599e1afc1babb0ebfd971d427ff8d4906 Mon Sep 17 00:00:00 2001 From: tjq Date: Sun, 11 Feb 2024 23:52:35 +0800 Subject: [PATCH] feat: [auth] use CachingRequestBodyFilter fix multi read problem --- .../tech/powerjob/common/OmsConstant.java | 2 + .../powerjob/common/utils/CommonUtils.java | 8 ++ .../server/auth/common/AuthConstants.java | 6 +- .../auth/interceptor/ApiPermission.java | 11 +++ .../auth/interceptor/ApiPermissionAspect.java | 66 +++++++++++++ .../gp/EmptyGrantPermissionPlugin.java | 16 ++++ .../interceptor/gp/GrantPermissionPlugin.java | 21 ++++ .../gp/SaveAppGrantPermissionPlugin.java | 16 ++++ .../gp/SaveGrantPermissionPlugin.java | 81 ++++++++++++++++ .../SaveNamespaceGrantPermissionPlugin.java | 16 ++++ .../login/impl/PowerJobLoginServiceImpl.java | 7 ++ .../PowerJobPermissionServiceImpl.java | 46 +++++++-- .../server/persistence/QueryConvertUtils.java | 2 +- .../remote/repository/AppInfoRepository.java | 9 +- .../config/CachingRequestBodyFilter.java | 96 +++++++++++++++++++ .../powerjob/server/config/WebConfig.java | 23 +++++ .../web/controller/AppInfoController.java | 86 ++++++++++++++--- .../web/controller/NamespaceController.java | 85 ++++++++++++++++ .../web/converter/NamespaceConverter.java | 26 +++++ .../web/request/ModifyNamespaceRequest.java | 47 +++++++++ .../web/request/QueryAppInfoRequest.java | 41 ++++++++ .../web/request/QueryNamespaceRequest.java | 23 +++++ .../server/web/response/NamespaceBaseVO.java | 52 ++++++++++ 23 files changed, 758 insertions(+), 28 deletions(-) create mode 100644 powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/interceptor/ApiPermissionAspect.java create mode 100644 powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/interceptor/gp/EmptyGrantPermissionPlugin.java create mode 100644 powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/interceptor/gp/GrantPermissionPlugin.java create mode 100644 powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/interceptor/gp/SaveAppGrantPermissionPlugin.java create mode 100644 powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/interceptor/gp/SaveGrantPermissionPlugin.java create mode 100644 powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/interceptor/gp/SaveNamespaceGrantPermissionPlugin.java create mode 100644 powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/config/CachingRequestBodyFilter.java create mode 100644 powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/NamespaceController.java create mode 100644 powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/converter/NamespaceConverter.java create mode 100644 powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/request/ModifyNamespaceRequest.java create mode 100644 powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/request/QueryAppInfoRequest.java create mode 100644 powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/request/QueryNamespaceRequest.java create mode 100644 powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/response/NamespaceBaseVO.java diff --git a/powerjob-common/src/main/java/tech/powerjob/common/OmsConstant.java b/powerjob-common/src/main/java/tech/powerjob/common/OmsConstant.java index f589e869..4d40d323 100644 --- a/powerjob-common/src/main/java/tech/powerjob/common/OmsConstant.java +++ b/powerjob-common/src/main/java/tech/powerjob/common/OmsConstant.java @@ -30,4 +30,6 @@ public class OmsConstant { public static final String HTTP_HEADER_CONTENT_TYPE = "Content-Type"; public static final String JSON_MEDIA_TYPE = "application/json; charset=utf-8"; + + public static final String NULL = "null"; } diff --git a/powerjob-common/src/main/java/tech/powerjob/common/utils/CommonUtils.java b/powerjob-common/src/main/java/tech/powerjob/common/utils/CommonUtils.java index 498d5b73..60507c6d 100644 --- a/powerjob-common/src/main/java/tech/powerjob/common/utils/CommonUtils.java +++ b/powerjob-common/src/main/java/tech/powerjob/common/utils/CommonUtils.java @@ -7,6 +7,7 @@ import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.time.DateFormatUtils; import java.util.Collection; +import java.util.Date; import java.util.UUID; import java.util.function.Supplier; @@ -147,6 +148,13 @@ public class CommonUtils { return OmsConstant.NONE; } + public static String formatTime(Date date) { + if (date == null) { + return OmsConstant.NONE; + } + return formatTime(date.getTime()); + } + /** * 格式化字符串,如果是 null 或空则显示 N/A * @param str 字符串 diff --git a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/common/AuthConstants.java b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/common/AuthConstants.java index 8bb4547f..af14d880 100644 --- a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/common/AuthConstants.java +++ b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/common/AuthConstants.java @@ -8,7 +8,11 @@ package tech.powerjob.server.auth.common; */ public class AuthConstants { - public static final String JWT_NAME = "power_jwt"; + /** + * JWT key + * 前端 header 默认首字母大写,保持一致方便处理 + */ + public static final String JWT_NAME = "Power_jwt"; /** * 前端跳转到指定页面指令 diff --git a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/interceptor/ApiPermission.java b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/interceptor/ApiPermission.java index c8efe157..c4f77d83 100644 --- a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/interceptor/ApiPermission.java +++ b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/interceptor/ApiPermission.java @@ -1,8 +1,11 @@ package tech.powerjob.server.auth.interceptor; import tech.powerjob.server.auth.Permission; +import tech.powerjob.server.auth.RoleScope; import tech.powerjob.server.auth.interceptor.dp.DynamicPermission; import tech.powerjob.server.auth.interceptor.dp.EmptyDynamicPermission; +import tech.powerjob.server.auth.interceptor.gp.EmptyGrantPermissionPlugin; +import tech.powerjob.server.auth.interceptor.gp.GrantPermissionPlugin; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; @@ -24,6 +27,8 @@ public @interface ApiPermission { */ String name() default ""; + RoleScope roleScope() default RoleScope.APP; + /** * 需要的权限 * @return 权限 @@ -35,4 +40,10 @@ public @interface ApiPermission { * @return 动态权限 */ Class dynamicPermissionPlugin() default EmptyDynamicPermission.class; + + /** + * 新增场景,需要授权插件执行授权 + * @return 授权插件 + */ + Class grandPermissionPlugin() default EmptyGrantPermissionPlugin.class; } diff --git a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/interceptor/ApiPermissionAspect.java b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/interceptor/ApiPermissionAspect.java new file mode 100644 index 00000000..4b57e2a2 --- /dev/null +++ b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/interceptor/ApiPermissionAspect.java @@ -0,0 +1,66 @@ +package tech.powerjob.server.auth.interceptor; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.AfterReturning; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.stereotype.Component; +import tech.powerjob.server.auth.interceptor.gp.GrantPermissionPlugin; + +import java.lang.reflect.Method; + +/** + * ApiPermission 切面 + * 主要用于执行授权插件,完成创建后授权 + * + * @author tjq + * @since 2024/2/11 + */ +@Slf4j +@Aspect +@Component +public class ApiPermissionAspect { + + @Pointcut("@annotation(ApiPermission)") + public void apiPermissionPointcut() { + // 定义切入点 + } + + /** + * 后置返回 + * 如果第一个参数为JoinPoint,则第二个参数为返回值的信息 + * 如果第一个参数不为JoinPoint,则第一个参数为returning中对应的参数 + * returning:限定了只有目标方法返回值与通知方法参数类型匹配时才能执行后置返回通知,否则不执行, + * 参数为Object类型将匹配任何目标返回值 + * After注解标注的方法会在目标方法执行后运行,无论目标方法是正常完成还是抛出异常。它相当于finally块,因为它总是执行,所以适用于释放资源等清理活动。@After注解不能访问目标方法的返回值。 + * AfterReturning注解标注的方法仅在目标方法成功执行后(即正常返回)运行。它可以访问目标方法的返回值。使用@AfterReturning可以在方法正常返回后执行一些逻辑,比如对返回值进行处理或验证。 + */ + @AfterReturning(value = "apiPermissionPointcut()", returning = "result") + public void doAfterReturningAdvice1(JoinPoint joinPoint, Object result) { + + // 入参 + Object[] args = joinPoint.getArgs(); + + // 获取目标方法 + MethodSignature signature = (MethodSignature) joinPoint.getSignature(); + Method method = signature.getMethod(); + + ApiPermission annotationAnno = AnnotationUtils.getAnnotation(method, ApiPermission.class); + + assert annotationAnno != null; + + Class grandPermissionPluginClz = annotationAnno.grandPermissionPlugin(); + + try { + GrantPermissionPlugin grandPermissionPlugin = grandPermissionPluginClz.getDeclaredConstructor().newInstance(); + grandPermissionPlugin.grant(args, result, method, joinPoint.getTarget()); + } catch (Exception e) { + log.error("[ApiPermissionAspect] process ApiPermission grant failed", e); + ExceptionUtils.rethrow(e); + } + } +} diff --git a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/interceptor/gp/EmptyGrantPermissionPlugin.java b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/interceptor/gp/EmptyGrantPermissionPlugin.java new file mode 100644 index 00000000..b50b4599 --- /dev/null +++ b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/interceptor/gp/EmptyGrantPermissionPlugin.java @@ -0,0 +1,16 @@ +package tech.powerjob.server.auth.interceptor.gp; + +import java.lang.reflect.Method; + +/** + * do nothing + * + * @author tjq + * @since 2024/2/11 + */ +public class EmptyGrantPermissionPlugin implements GrantPermissionPlugin { + @Override + public void grant(Object[] args, Object result, Method method, Object originBean) { + + } +} diff --git a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/interceptor/gp/GrantPermissionPlugin.java b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/interceptor/gp/GrantPermissionPlugin.java new file mode 100644 index 00000000..746cd000 --- /dev/null +++ b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/interceptor/gp/GrantPermissionPlugin.java @@ -0,0 +1,21 @@ +package tech.powerjob.server.auth.interceptor.gp; + +import java.lang.reflect.Method; + +/** + * 授予权限插件 + * + * @author tjq + * @since 2024/2/11 + */ +public interface GrantPermissionPlugin { + + /** + * 授权 + * @param args 入参 + * @param result 响应 + * @param method 被调用方法 + * @param originBean 原始对象 + */ + void grant(Object[] args, Object result, Method method, Object originBean); +} diff --git a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/interceptor/gp/SaveAppGrantPermissionPlugin.java b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/interceptor/gp/SaveAppGrantPermissionPlugin.java new file mode 100644 index 00000000..2e123582 --- /dev/null +++ b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/interceptor/gp/SaveAppGrantPermissionPlugin.java @@ -0,0 +1,16 @@ +package tech.powerjob.server.auth.interceptor.gp; + +import tech.powerjob.server.auth.RoleScope; + +/** + * desc + * + * @author tjq + * @since 2024/2/11 + */ +public class SaveAppGrantPermissionPlugin extends SaveGrantPermissionPlugin { + @Override + protected RoleScope fetchRuleScope() { + return RoleScope.APP; + } +} diff --git a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/interceptor/gp/SaveGrantPermissionPlugin.java b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/interceptor/gp/SaveGrantPermissionPlugin.java new file mode 100644 index 00000000..5ac22f86 --- /dev/null +++ b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/interceptor/gp/SaveGrantPermissionPlugin.java @@ -0,0 +1,81 @@ +package tech.powerjob.server.auth.interceptor.gp; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.MapUtils; +import tech.powerjob.common.response.ResultDTO; +import tech.powerjob.common.serialize.JsonUtils; +import tech.powerjob.server.auth.LoginUserHolder; +import tech.powerjob.server.auth.PowerJobUser; +import tech.powerjob.server.auth.Role; +import tech.powerjob.server.auth.RoleScope; +import tech.powerjob.server.common.utils.SpringUtils; +import tech.powerjob.server.persistence.remote.model.UserRoleDO; +import tech.powerjob.server.persistence.remote.repository.UserRoleRepository; + +import java.lang.reflect.Method; +import java.util.Date; +import java.util.Map; + +/** + * WEB 类保存&修改一体型请求-授权插件 + * + * @author tjq + * @since 2024/2/11 + */ +@Slf4j +public abstract class SaveGrantPermissionPlugin implements GrantPermissionPlugin { + + private static final String KEY_ID = "id"; + + @Override + public void grant(Object[] args, Object result, Method method, Object originBean) { + + if (args == null || args.length != 1) { + throw new IllegalArgumentException("[GrantPermission] args not match, maybe there has some bug"); + } + + // 理论上不可能,前置已完成判断 + PowerJobUser powerJobUser = LoginUserHolder.get(); + if (powerJobUser == null) { + throw new IllegalArgumentException("[GrantPermission] user not login, can't grant permission"); + } + + // 解析ID,非空代表更新,不授权 + Map saveRequest = JsonUtils.parseMap(JsonUtils.toJSONString(args[0])); + Long id = MapUtils.getLong(saveRequest, KEY_ID); + if (id != null) { + return; + } + + if (!(result instanceof ResultDTO)) { + throw new IllegalArgumentException("[GrantPermission] result not instanceof ResultDTO, maybe there has some bug"); + } + + ResultDTO resultDTO = (ResultDTO) result; + + if (!resultDTO.isSuccess()) { + log.warn("[GrantPermission] result not success, skip grant permission!"); + return; + } + + Map saveResult = JsonUtils.parseMap(JsonUtils.toJSONString(resultDTO.getData())); + Long savedId = MapUtils.getLong(saveResult, KEY_ID); + if (savedId == null) { + throw new IllegalArgumentException("[GrantPermission] result success but id not exits, maybe there has some bug, please fix it!!!"); + } + + UserRoleRepository userRoleRepository = SpringUtils.getBean(UserRoleRepository.class); + UserRoleDO userRoleDO = new UserRoleDO(); + + userRoleDO.setUserId(powerJobUser.getId()); + userRoleDO.setRole(Role.ADMIN.getV()); + userRoleDO.setScope(fetchRuleScope().getV()); + userRoleDO.setTarget(savedId); + userRoleDO.setGmtCreate(new Date()); + userRoleDO.setGmtModified(new Date()); + + userRoleRepository.saveAndFlush(userRoleDO); + } + + protected abstract RoleScope fetchRuleScope(); +} diff --git a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/interceptor/gp/SaveNamespaceGrantPermissionPlugin.java b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/interceptor/gp/SaveNamespaceGrantPermissionPlugin.java new file mode 100644 index 00000000..d9fbe752 --- /dev/null +++ b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/interceptor/gp/SaveNamespaceGrantPermissionPlugin.java @@ -0,0 +1,16 @@ +package tech.powerjob.server.auth.interceptor.gp; + +import tech.powerjob.server.auth.RoleScope; + +/** + * namespace 授权插件 + * + * @author tjq + * @since 2024/2/11 + */ +public class SaveNamespaceGrantPermissionPlugin extends SaveGrantPermissionPlugin { + @Override + protected RoleScope fetchRuleScope() { + return RoleScope.NAMESPACE; + } +} diff --git a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/service/login/impl/PowerJobLoginServiceImpl.java b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/service/login/impl/PowerJobLoginServiceImpl.java index 4244fe84..1003b266 100644 --- a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/service/login/impl/PowerJobLoginServiceImpl.java +++ b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/service/login/impl/PowerJobLoginServiceImpl.java @@ -7,6 +7,7 @@ import org.apache.commons.lang3.StringUtils; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import tech.powerjob.common.OmsConstant; import tech.powerjob.server.auth.PowerJobUser; import tech.powerjob.server.auth.common.AuthConstants; import tech.powerjob.server.auth.common.AuthErrorCode; @@ -140,6 +141,12 @@ public class PowerJobLoginServiceImpl implements PowerJobLoginService { private Optional parseUserName(HttpServletRequest httpServletRequest) { // header、cookie 都能获取 String jwtStr = httpServletRequest.getHeader(AuthConstants.JWT_NAME); + + // 解决 window.localStorage.getItem 为 null 的问题 + if (OmsConstant.NULL.equalsIgnoreCase(jwtStr)) { + jwtStr = null; + } + if (StringUtils.isEmpty(jwtStr)) { for (Cookie cookie : Optional.ofNullable(httpServletRequest.getCookies()).orElse(new Cookie[]{})) { if (cookie.getName().equals(AuthConstants.JWT_NAME)) { diff --git a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/service/permission/PowerJobPermissionServiceImpl.java b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/service/permission/PowerJobPermissionServiceImpl.java index 9753ace1..3b0ea263 100644 --- a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/service/permission/PowerJobPermissionServiceImpl.java +++ b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/service/permission/PowerJobPermissionServiceImpl.java @@ -68,7 +68,32 @@ public class PowerJobPermissionServiceImpl implements PowerJobPermissionService } } + // 前置判断需要的权限(新增场景还没有 appId or namespaceId) + final Permission requiredPermission = parsePermission(request, handler, apiPermission); + if (requiredPermission == Permission.NONE) { + return true; + } + + // 检验全局穿透权限 + for (Role role : globalRoles) { + if (role.getPermissions().contains(requiredPermission)) { + return true; + } + } + // 无超级管理员权限,校验普通权限 + if (RoleScope.APP.equals(apiPermission.roleScope())) { + return checkAppPermission(request, requiredPermission, appId2Role, namespaceId2Role); + } + + if (RoleScope.NAMESPACE.equals(apiPermission.roleScope())) { + return checkNamespacePermission(request, requiredPermission, namespaceId2Role); + } + + return false; + } + + private boolean checkAppPermission(HttpServletRequest request, Permission requiredPermission, Multimap appId2Role, Multimap namespaceId2Role) { final String appIdStr = request.getHeader("appId"); if (StringUtils.isEmpty(appIdStr)) { throw new IllegalArgumentException("can't find appId in header, please refresh and try again!"); @@ -76,11 +101,6 @@ public class PowerJobPermissionServiceImpl implements PowerJobPermissionService Long appId = Long.valueOf(appIdStr); - final Permission requiredPermission = parsePermission(request, handler, apiPermission); - if (requiredPermission == Permission.NONE) { - return true; - } - final Collection appRoles = appId2Role.get(appId); for (Role role : appRoles) { if (role.getPermissions().contains(requiredPermission)) { @@ -101,8 +121,18 @@ public class PowerJobPermissionServiceImpl implements PowerJobPermissionService } } - // 检验全局穿透权限(按使用频率排列检测顺序,即 app -> namespace -> global) - for (Role role : globalRoles) { + return false; + } + + private boolean checkNamespacePermission(HttpServletRequest request, Permission requiredPermission, Multimap namespaceId2Role) { + final String namespaceIdStr = request.getHeader("namespaceId"); + if (StringUtils.isEmpty(namespaceIdStr)) { + throw new IllegalArgumentException("can't find namespace in header, please refresh and try again!"); + } + Long namespaceId = Long.valueOf(namespaceIdStr); + + Collection namespaceRoles = namespaceId2Role.get(namespaceId); + for (Role role : namespaceRoles) { if (role.getPermissions().contains(requiredPermission)) { return true; } @@ -111,6 +141,8 @@ public class PowerJobPermissionServiceImpl implements PowerJobPermissionService return false; } + + private static Permission parsePermission(HttpServletRequest request, Object handler, ApiPermission apiPermission) { Class dynamicPermissionPlugin = apiPermission.dynamicPermissionPlugin(); if (EmptyDynamicPermission.class.equals(dynamicPermissionPlugin)) { diff --git a/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/QueryConvertUtils.java b/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/QueryConvertUtils.java index 31d0bbbf..3f4fe41d 100644 --- a/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/QueryConvertUtils.java +++ b/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/QueryConvertUtils.java @@ -86,7 +86,7 @@ public class QueryConvertUtils { }; } - private static String convertLikeParams(Object o) { + public static String convertLikeParams(Object o) { String s = (String) o; if (!s.startsWith("%")) { s = "%" + s; diff --git a/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/repository/AppInfoRepository.java b/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/repository/AppInfoRepository.java index a7e6d09d..bfdc0f5a 100644 --- a/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/repository/AppInfoRepository.java +++ b/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/repository/AppInfoRepository.java @@ -1,11 +1,12 @@ package tech.powerjob.server.persistence.remote.repository; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; -import tech.powerjob.server.persistence.remote.model.AppInfoDO; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import tech.powerjob.server.persistence.remote.model.AppInfoDO; import java.util.List; import java.util.Optional; @@ -16,7 +17,7 @@ import java.util.Optional; * @author tjq * @since 2020/4/1 */ -public interface AppInfoRepository extends JpaRepository { +public interface AppInfoRepository extends JpaRepository, JpaSpecificationExecutor { Optional findByAppName(String appName); diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/config/CachingRequestBodyFilter.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/config/CachingRequestBodyFilter.java new file mode 100644 index 00000000..e41136fb --- /dev/null +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/config/CachingRequestBodyFilter.java @@ -0,0 +1,96 @@ +package tech.powerjob.server.config; + +import org.springframework.stereotype.Component; + +import javax.servlet.*; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import java.io.*; + +/** + * 解决 HttpServletRequest 只能被读取一次的问题,方便全局日志 & 鉴权,切面提前读取数据 + * 在请求进入Servlet容器之前,先经过Filter的过滤器链。在请求进入Controller之前,先经过 HandlerInterceptor 的拦截器链。Filter 一定先于 HandlerInterceptor 执行 + * + * @author tjq + * @since 2024/2/11 + */ +@Component +public class CachingRequestBodyFilter implements Filter { + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + if (request instanceof HttpServletRequest) { + CustomHttpServletRequestWrapper wrappedRequest = new CustomHttpServletRequestWrapper((HttpServletRequest) request); + chain.doFilter(wrappedRequest, response); + } else { + chain.doFilter(request, response); + } + } + + // Implement other required methods like init() and destroy() if necessary + + + public static class CustomHttpServletRequestWrapper extends HttpServletRequestWrapper { + + private final String body; + + public CustomHttpServletRequestWrapper(HttpServletRequest request) throws IOException { + super(request); + StringBuilder stringBuilder = new StringBuilder(); + BufferedReader bufferedReader = null; + try { + InputStream inputStream = request.getInputStream(); + if (inputStream != null) { + bufferedReader = new BufferedReader(new InputStreamReader(inputStream)); + char[] charBuffer = new char[128]; + int bytesRead = -1; + while ((bytesRead = bufferedReader.read(charBuffer)) > 0) { + stringBuilder.append(charBuffer, 0, bytesRead); + } + } + } finally { + if (bufferedReader != null) { + bufferedReader.close(); + } + } + body = stringBuilder.toString(); + } + + @Override + public ServletInputStream getInputStream() throws IOException { + final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body.getBytes()); + + return new ServletInputStream() { + public int read() throws IOException { + return byteArrayInputStream.read(); + } + + @Override + public boolean isFinished() { + return byteArrayInputStream.available() == 0; + } + + @Override + public boolean isReady() { + return true; + } + + @Override + public void setReadListener(ReadListener readListener) { + throw new UnsupportedOperationException("Not implemented"); + } + }; + } + + @Override + public BufferedReader getReader() throws IOException { + return new BufferedReader(new InputStreamReader(this.getInputStream())); + } + + public String getBody() { + return this.body; + } + } + +} diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/config/WebConfig.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/config/WebConfig.java index c1e819ad..28427a0e 100644 --- a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/config/WebConfig.java +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/config/WebConfig.java @@ -3,9 +3,13 @@ package tech.powerjob.server.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.springframework.web.socket.config.annotation.EnableWebSocket; import org.springframework.web.socket.server.standard.ServerEndpointExporter; +import tech.powerjob.server.auth.interceptor.PowerJobAuthInterceptor; + +import javax.annotation.Resource; /** * CORS @@ -16,6 +20,10 @@ import org.springframework.web.socket.server.standard.ServerEndpointExporter; @Configuration @EnableWebSocket public class WebConfig implements WebMvcConfigurer { + + @Resource + private PowerJobAuthInterceptor powerJobAuthInterceptor; + @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") @@ -26,4 +34,19 @@ public class WebConfig implements WebMvcConfigurer { public ServerEndpointExporter serverEndpointExporter() { return new ServerEndpointExporter(); } + + @Override + public void addInterceptors(InterceptorRegistry registry) { + /* + 可以添加多个拦截器 + addPathPatterns("/**") 表示对所有请求都拦截 + .excludePathPatterns("/base/index") 表示排除对/base/index请求的拦截 + 多个拦截器可以设置order顺序,值越小,preHandle越先执行,postHandle和afterCompletion越后执行 + order默认的值是0,如果只添加一个拦截器,可以不显示设置order的值 + */ + registry.addInterceptor(powerJobAuthInterceptor) + .addPathPatterns("/**") + .excludePathPatterns("/css/**", "/js/**", "/images/**", "/img/**", "/fonts/**", "/favicon.ico") + .order(0); + } } diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/AppInfoController.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/AppInfoController.java index f50e9a90..2901332d 100644 --- a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/AppInfoController.java +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/AppInfoController.java @@ -1,23 +1,31 @@ package tech.powerjob.server.web.controller; -import lombok.RequiredArgsConstructor; -import tech.powerjob.common.exception.PowerJobException; -import tech.powerjob.common.response.ResultDTO; -import tech.powerjob.server.persistence.remote.model.AppInfoDO; -import tech.powerjob.server.persistence.remote.repository.AppInfoRepository; -import tech.powerjob.server.core.service.AppInfoService; -import tech.powerjob.server.web.request.AppAssertRequest; -import tech.powerjob.server.web.request.ModifyAppInfoRequest; import com.google.common.collect.Lists; import lombok.Data; +import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.BeanUtils; +import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.domain.Specification; import org.springframework.util.CollectionUtils; import org.springframework.web.bind.annotation.*; +import tech.powerjob.common.exception.PowerJobException; +import tech.powerjob.common.response.ResultDTO; +import tech.powerjob.server.core.service.AppInfoService; +import tech.powerjob.server.persistence.PageResult; +import tech.powerjob.server.persistence.QueryConvertUtils; +import tech.powerjob.server.persistence.remote.model.AppInfoDO; +import tech.powerjob.server.persistence.remote.repository.AppInfoRepository; +import tech.powerjob.server.web.request.AppAssertRequest; +import tech.powerjob.server.web.request.ModifyAppInfoRequest; +import tech.powerjob.server.web.request.QueryAppInfoRequest; -import javax.annotation.Resource; +import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.CriteriaQuery; +import javax.persistence.criteria.Predicate; +import javax.persistence.criteria.Root; import java.util.Date; import java.util.List; import java.util.Objects; @@ -42,7 +50,7 @@ public class AppInfoController { private static final int MAX_APP_NUM = 200; @PostMapping("/save") - public ResultDTO saveAppInfo(@RequestBody ModifyAppInfoRequest req) { + public ResultDTO saveAppInfo(@RequestBody ModifyAppInfoRequest req) { req.valid(); AppInfoDO appInfoDO; @@ -62,8 +70,8 @@ public class AppInfoController { BeanUtils.copyProperties(req, appInfoDO); appInfoDO.setGmtModified(new Date()); - appInfoRepository.saveAndFlush(appInfoDO); - return ResultDTO.success(null); + AppInfoDO savedAppInfo = appInfoRepository.saveAndFlush(appInfoDO); + return ResultDTO.success(convert(Lists.newArrayList(savedAppInfo), false).get(0)); } @PostMapping("/assert") @@ -86,18 +94,66 @@ public class AppInfoController { }else { result = appInfoRepository.findByAppNameLike("%" + condition + "%", limit).getContent(); } - return ResultDTO.success(convert(result)); + return ResultDTO.success(convert(result, false)); } - private static List convert(List data) { + @PostMapping("/listByQuery") + public ResultDTO> listAppInfoByQuery(QueryAppInfoRequest queryAppInfoRequest) { + + Pageable pageable = PageRequest.of(queryAppInfoRequest.getIndex(), queryAppInfoRequest.getPageSize()); + + // TODO: 我有权限的列表 + Specification specification = new Specification() { + @Override + public Predicate toPredicate(Root root, CriteriaQuery query, CriteriaBuilder criteriaBuilder) { + List predicates = Lists.newArrayList(); + + + Long appId = queryAppInfoRequest.getAppId(); + Long namespaceId = queryAppInfoRequest.getNamespaceId(); + + if (appId != null) { + predicates.add(criteriaBuilder.equal(root.get("id"), appId)); + } + + if (namespaceId != null) { + predicates.add(criteriaBuilder.equal(root.get("namespaceId"), namespaceId)); + } + + if (StringUtils.isNotEmpty(queryAppInfoRequest.getAppName())) { + predicates.add(criteriaBuilder.like(root.get("appName"), QueryConvertUtils.convertLikeParams(queryAppInfoRequest.getAppName()))); + } + + return query.where(predicates.toArray(new Predicate[0])).getRestriction(); + } + }; + + Page pageAppInfoResult = appInfoRepository.findAll(specification, pageable); + + PageResult pageRet = new PageResult<>(pageAppInfoResult); + + List appinfoDos = pageAppInfoResult.get().collect(Collectors.toList()); + pageRet.setData(convert(appinfoDos, true)); + + return ResultDTO.success(pageRet); + } + + + private static List convert(List data, boolean fillDetail) { if (CollectionUtils.isEmpty(data)) { return Lists.newLinkedList(); } - return data.stream().map(appInfoDO -> { + List appInfoVOList = data.stream().map(appInfoDO -> { AppInfoVO appInfoVO = new AppInfoVO(); BeanUtils.copyProperties(appInfoDO, appInfoVO); return appInfoVO; }).collect(Collectors.toList()); + + if (fillDetail) { + // TODO: 补全权限等额外信息 + } + + return appInfoVOList; } @Data diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/NamespaceController.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/NamespaceController.java new file mode 100644 index 00000000..456d3471 --- /dev/null +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/NamespaceController.java @@ -0,0 +1,85 @@ +package tech.powerjob.server.web.controller; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; +import tech.powerjob.common.response.ResultDTO; +import tech.powerjob.server.auth.RoleScope; +import tech.powerjob.server.auth.interceptor.ApiPermission; +import tech.powerjob.server.auth.interceptor.dp.ModifyOrCreateDynamicPermission; +import tech.powerjob.server.auth.interceptor.gp.SaveNamespaceGrantPermissionPlugin; +import tech.powerjob.server.common.constants.SwitchableStatus; +import tech.powerjob.server.persistence.remote.model.NamespaceDO; +import tech.powerjob.server.persistence.remote.repository.NamespaceRepository; +import tech.powerjob.server.web.converter.NamespaceConverter; +import tech.powerjob.server.web.request.ModifyNamespaceRequest; +import tech.powerjob.server.web.request.QueryNamespaceRequest; +import tech.powerjob.server.web.response.NamespaceBaseVO; + +import javax.annotation.Resource; +import java.util.Date; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.stream.Collectors; + +/** + * 命名空间 Controller + * + * @author tjq + * @since 2023/9/3 + */ +@Slf4j +@RestController +@RequestMapping("/namespace") +public class NamespaceController { + + @Resource + private NamespaceRepository namespaceRepository; + + @ResponseBody + @PostMapping("/save") + @ApiPermission(name = "Namespace-Save", roleScope = RoleScope.NAMESPACE, dynamicPermissionPlugin = ModifyOrCreateDynamicPermission.class, grandPermissionPlugin = SaveNamespaceGrantPermissionPlugin.class) + public ResultDTO save(@RequestBody ModifyNamespaceRequest req) { + + req.valid(); + + Long id = req.getId(); + NamespaceDO namespaceDO; + if (id == null) { + namespaceDO = new NamespaceDO(); + namespaceDO.setGmtCreate(new Date()); + + // code 单独拷贝 + namespaceDO.setCode(req.getCode()); + // 创建时生成 token + namespaceDO.setToken(UUID.randomUUID().toString()); + + } else { + namespaceDO = fetchById(id); + } + + // 拷贝通用变更属性(code 不允许更改) + namespaceDO.setName(req.getName()); + namespaceDO.setExtra(req.getExtra()); + namespaceDO.setStatus(Optional.ofNullable(req.getStatus()).orElse(SwitchableStatus.ENABLE.getV())); + + namespaceDO.setGmtModified(new Date()); + NamespaceDO savedNamespace = namespaceRepository.save(namespaceDO); + return ResultDTO.success(NamespaceConverter.do2BaseVo(savedNamespace)); + } + + @PostMapping("/list") + public ResultDTO> listNamespace(@RequestBody QueryNamespaceRequest queryNamespaceRequest) { + List allDos = namespaceRepository.findAll(); + return ResultDTO.success(allDos.stream().map(NamespaceConverter::do2BaseVo).collect(Collectors.toList())); + } + + private NamespaceDO fetchById(Long id) { + Optional namespaceDoOpt = namespaceRepository.findById(id); + if (!namespaceDoOpt.isPresent()) { + throw new IllegalArgumentException("can't find namespace by id: " + id); + } + return namespaceDoOpt.get(); + } + +} diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/converter/NamespaceConverter.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/converter/NamespaceConverter.java new file mode 100644 index 00000000..01f968f8 --- /dev/null +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/converter/NamespaceConverter.java @@ -0,0 +1,26 @@ +package tech.powerjob.server.web.converter; + +import org.springframework.beans.BeanUtils; +import tech.powerjob.common.utils.CommonUtils; +import tech.powerjob.server.common.constants.SwitchableStatus; +import tech.powerjob.server.persistence.remote.model.NamespaceDO; +import tech.powerjob.server.web.response.NamespaceBaseVO; + +/** + * NamespaceConverter + * + * @author tjq + * @since 2023/9/4 + */ +public class NamespaceConverter { + + public static NamespaceBaseVO do2BaseVo(NamespaceDO d) { + NamespaceBaseVO v = new NamespaceBaseVO(); + BeanUtils.copyProperties(d, v); + v.setGmtCreateStr(CommonUtils.formatTime(d.getGmtCreate())); + v.setGmtModifiedStr(CommonUtils.formatTime(d.getGmtModified())); + v.setStatusStr(SwitchableStatus.of(d.getStatus()).name()); + return v; + } + +} diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/request/ModifyNamespaceRequest.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/request/ModifyNamespaceRequest.java new file mode 100644 index 00000000..1d614fb8 --- /dev/null +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/request/ModifyNamespaceRequest.java @@ -0,0 +1,47 @@ +package tech.powerjob.server.web.request; + +import lombok.Data; +import org.apache.commons.lang3.StringUtils; +import tech.powerjob.common.exception.PowerJobException; +import tech.powerjob.common.utils.CommonUtils; + +/** + * ModifyNamespaceRequest + * + * @author tjq + * @since 2023/9/3 + */ +@Data +public class ModifyNamespaceRequest { + + private Long id; + + /** + * 空间唯一标识 + */ + private String code; + + /** + * 空间名称,比如中文描述(XX部门XX空间) + */ + private String name; + + /** + * 标签,扩展性之王,多值逗号分割 + */ + private String tags; + + private Integer status; + + /** + * 扩展字段 + */ + private String extra; + + public void valid() { + CommonUtils.requireNonNull(code, "namespace code can't be empty"); + if (StringUtils.containsWhitespace(code)) { + throw new PowerJobException("namespace code can't contains white space!"); + } + } +} diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/request/QueryAppInfoRequest.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/request/QueryAppInfoRequest.java new file mode 100644 index 00000000..625d2459 --- /dev/null +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/request/QueryAppInfoRequest.java @@ -0,0 +1,41 @@ +package tech.powerjob.server.web.request; + +import lombok.Data; + +/** + * 查询应用信息 + * + * @author tjq + * @since 2024/2/11 + */ +@Data +public class QueryAppInfoRequest { + + /** + * appId 精确查旋 + */ + private Long appId; + /** + * namespaceId + */ + private Long namespaceId; + /** + * 任务名称 + */ + private String appName; + + /** + * 查询与我相关的任务(我有直接权限的) + */ + private Boolean showMyRelated; + + /** + * 当前页码 + */ + private Integer index; + /** + * 页大小 + */ + private Integer pageSize; + +} diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/request/QueryNamespaceRequest.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/request/QueryNamespaceRequest.java new file mode 100644 index 00000000..a47d6015 --- /dev/null +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/request/QueryNamespaceRequest.java @@ -0,0 +1,23 @@ +package tech.powerjob.server.web.request; + +import lombok.Data; + +/** + * 查询 namespace 请求 + * + * @author tjq + * @since 2024/2/11 + */ +@Data +public class QueryNamespaceRequest { + + /** + * code 模糊查询 + */ + private String code; + + /** + * 名称模糊查询 + */ + private String name; +} diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/response/NamespaceBaseVO.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/response/NamespaceBaseVO.java new file mode 100644 index 00000000..b0e85d75 --- /dev/null +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/response/NamespaceBaseVO.java @@ -0,0 +1,52 @@ +package tech.powerjob.server.web.response; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +import java.io.Serializable; +import java.util.Date; + +/** + * 基础版本的命名空间 + * + * @author tjq + * @since 2023/9/3 + */ +@Getter +@Setter +@ToString +public class NamespaceBaseVO implements Serializable { + + private Long id; + + /** + * 空间唯一标识 + */ + private String code; + + /** + * 空间名称,比如中文描述(XX部门XX空间) + */ + private String name; + + private Integer status; + private String statusStr; + + /** + * 扩展字段 + */ + private String extra; + + private Date gmtCreate; + + private String gmtCreateStr; + + private Date gmtModified; + + private String gmtModifiedStr; + + private String creator; + + private String modifier; +}