feat: [auth] use CachingRequestBodyFilter fix multi read problem

This commit is contained in:
tjq 2024-02-11 23:52:35 +08:00
parent e18b9a8962
commit 3fdcc1e599
23 changed files with 758 additions and 28 deletions

View File

@ -30,4 +30,6 @@ public class OmsConstant {
public static final String HTTP_HEADER_CONTENT_TYPE = "Content-Type"; 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 JSON_MEDIA_TYPE = "application/json; charset=utf-8";
public static final String NULL = "null";
} }

View File

@ -7,6 +7,7 @@ import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.DateFormatUtils; import org.apache.commons.lang3.time.DateFormatUtils;
import java.util.Collection; import java.util.Collection;
import java.util.Date;
import java.util.UUID; import java.util.UUID;
import java.util.function.Supplier; import java.util.function.Supplier;
@ -147,6 +148,13 @@ public class CommonUtils {
return OmsConstant.NONE; return OmsConstant.NONE;
} }
public static String formatTime(Date date) {
if (date == null) {
return OmsConstant.NONE;
}
return formatTime(date.getTime());
}
/** /**
* 格式化字符串如果是 null 或空则显示 N/A * 格式化字符串如果是 null 或空则显示 N/A
* @param str 字符串 * @param str 字符串

View File

@ -8,7 +8,11 @@ package tech.powerjob.server.auth.common;
*/ */
public class AuthConstants { public class AuthConstants {
public static final String JWT_NAME = "power_jwt"; /**
* JWT key
* 前端 header 默认首字母大写保持一致方便处理
*/
public static final String JWT_NAME = "Power_jwt";
/** /**
* 前端跳转到指定页面指令 * 前端跳转到指定页面指令

View File

@ -1,8 +1,11 @@
package tech.powerjob.server.auth.interceptor; package tech.powerjob.server.auth.interceptor;
import tech.powerjob.server.auth.Permission; 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.DynamicPermission;
import tech.powerjob.server.auth.interceptor.dp.EmptyDynamicPermission; 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.ElementType;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
@ -24,6 +27,8 @@ public @interface ApiPermission {
*/ */
String name() default ""; String name() default "";
RoleScope roleScope() default RoleScope.APP;
/** /**
* 需要的权限 * 需要的权限
* @return 权限 * @return 权限
@ -35,4 +40,10 @@ public @interface ApiPermission {
* @return 动态权限 * @return 动态权限
*/ */
Class<? extends DynamicPermission> dynamicPermissionPlugin() default EmptyDynamicPermission.class; Class<? extends DynamicPermission> dynamicPermissionPlugin() default EmptyDynamicPermission.class;
/**
* 新增场景需要授权插件执行授权
* @return 授权插件
*/
Class<? extends GrantPermissionPlugin> grandPermissionPlugin() default EmptyGrantPermissionPlugin.class;
} }

View File

@ -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<? extends GrantPermissionPlugin> 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);
}
}
}

View File

@ -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) {
}
}

View File

@ -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);
}

View File

@ -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;
}
}

View File

@ -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<String, Object> 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<String, Object> 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();
}

View File

@ -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;
}
}

View File

@ -7,6 +7,7 @@ import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils; import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import tech.powerjob.common.OmsConstant;
import tech.powerjob.server.auth.PowerJobUser; import tech.powerjob.server.auth.PowerJobUser;
import tech.powerjob.server.auth.common.AuthConstants; import tech.powerjob.server.auth.common.AuthConstants;
import tech.powerjob.server.auth.common.AuthErrorCode; import tech.powerjob.server.auth.common.AuthErrorCode;
@ -140,6 +141,12 @@ public class PowerJobLoginServiceImpl implements PowerJobLoginService {
private Optional<String> parseUserName(HttpServletRequest httpServletRequest) { private Optional<String> parseUserName(HttpServletRequest httpServletRequest) {
// headercookie 都能获取 // headercookie 都能获取
String jwtStr = httpServletRequest.getHeader(AuthConstants.JWT_NAME); String jwtStr = httpServletRequest.getHeader(AuthConstants.JWT_NAME);
// 解决 window.localStorage.getItem null 的问题
if (OmsConstant.NULL.equalsIgnoreCase(jwtStr)) {
jwtStr = null;
}
if (StringUtils.isEmpty(jwtStr)) { if (StringUtils.isEmpty(jwtStr)) {
for (Cookie cookie : Optional.ofNullable(httpServletRequest.getCookies()).orElse(new Cookie[]{})) { for (Cookie cookie : Optional.ofNullable(httpServletRequest.getCookies()).orElse(new Cookie[]{})) {
if (cookie.getName().equals(AuthConstants.JWT_NAME)) { if (cookie.getName().equals(AuthConstants.JWT_NAME)) {

View File

@ -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<Long, Role> appId2Role, Multimap<Long, Role> namespaceId2Role) {
final String appIdStr = request.getHeader("appId"); final String appIdStr = request.getHeader("appId");
if (StringUtils.isEmpty(appIdStr)) { if (StringUtils.isEmpty(appIdStr)) {
throw new IllegalArgumentException("can't find appId in header, please refresh and try again!"); 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); Long appId = Long.valueOf(appIdStr);
final Permission requiredPermission = parsePermission(request, handler, apiPermission);
if (requiredPermission == Permission.NONE) {
return true;
}
final Collection<Role> appRoles = appId2Role.get(appId); final Collection<Role> appRoles = appId2Role.get(appId);
for (Role role : appRoles) { for (Role role : appRoles) {
if (role.getPermissions().contains(requiredPermission)) { if (role.getPermissions().contains(requiredPermission)) {
@ -101,8 +121,18 @@ public class PowerJobPermissionServiceImpl implements PowerJobPermissionService
} }
} }
// 检验全局穿透权限按使用频率排列检测顺序 app -> namespace -> global return false;
for (Role role : globalRoles) { }
private boolean checkNamespacePermission(HttpServletRequest request, Permission requiredPermission, Multimap<Long, Role> 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<Role> namespaceRoles = namespaceId2Role.get(namespaceId);
for (Role role : namespaceRoles) {
if (role.getPermissions().contains(requiredPermission)) { if (role.getPermissions().contains(requiredPermission)) {
return true; return true;
} }
@ -111,6 +141,8 @@ public class PowerJobPermissionServiceImpl implements PowerJobPermissionService
return false; return false;
} }
private static Permission parsePermission(HttpServletRequest request, Object handler, ApiPermission apiPermission) { private static Permission parsePermission(HttpServletRequest request, Object handler, ApiPermission apiPermission) {
Class<? extends DynamicPermission> dynamicPermissionPlugin = apiPermission.dynamicPermissionPlugin(); Class<? extends DynamicPermission> dynamicPermissionPlugin = apiPermission.dynamicPermissionPlugin();
if (EmptyDynamicPermission.class.equals(dynamicPermissionPlugin)) { if (EmptyDynamicPermission.class.equals(dynamicPermissionPlugin)) {

View File

@ -86,7 +86,7 @@ public class QueryConvertUtils {
}; };
} }
private static String convertLikeParams(Object o) { public static String convertLikeParams(Object o) {
String s = (String) o; String s = (String) o;
if (!s.startsWith("%")) { if (!s.startsWith("%")) {
s = "%" + s; s = "%" + s;

View File

@ -1,11 +1,12 @@
package tech.powerjob.server.persistence.remote.repository; 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.Page;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository; 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.List;
import java.util.Optional; import java.util.Optional;
@ -16,7 +17,7 @@ import java.util.Optional;
* @author tjq * @author tjq
* @since 2020/4/1 * @since 2020/4/1
*/ */
public interface AppInfoRepository extends JpaRepository<AppInfoDO, Long> { public interface AppInfoRepository extends JpaRepository<AppInfoDO, Long>, JpaSpecificationExecutor<AppInfoDO> {
Optional<AppInfoDO> findByAppName(String appName); Optional<AppInfoDO> findByAppName(String appName);

View File

@ -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;
}
}
}

View File

@ -3,9 +3,13 @@ package tech.powerjob.server.config;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry; 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.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.socket.config.annotation.EnableWebSocket; import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.server.standard.ServerEndpointExporter; import org.springframework.web.socket.server.standard.ServerEndpointExporter;
import tech.powerjob.server.auth.interceptor.PowerJobAuthInterceptor;
import javax.annotation.Resource;
/** /**
* CORS * CORS
@ -16,6 +20,10 @@ import org.springframework.web.socket.server.standard.ServerEndpointExporter;
@Configuration @Configuration
@EnableWebSocket @EnableWebSocket
public class WebConfig implements WebMvcConfigurer { public class WebConfig implements WebMvcConfigurer {
@Resource
private PowerJobAuthInterceptor powerJobAuthInterceptor;
@Override @Override
public void addCorsMappings(CorsRegistry registry) { public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**") registry.addMapping("/**")
@ -26,4 +34,19 @@ public class WebConfig implements WebMvcConfigurer {
public ServerEndpointExporter serverEndpointExporter() { public ServerEndpointExporter serverEndpointExporter() {
return new 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);
}
} }

View File

@ -1,23 +1,31 @@
package tech.powerjob.server.web.controller; 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 com.google.common.collect.Lists;
import lombok.Data; import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils; import org.springframework.beans.BeanUtils;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.util.CollectionUtils; import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.*; 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.Date;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
@ -42,7 +50,7 @@ public class AppInfoController {
private static final int MAX_APP_NUM = 200; private static final int MAX_APP_NUM = 200;
@PostMapping("/save") @PostMapping("/save")
public ResultDTO<Void> saveAppInfo(@RequestBody ModifyAppInfoRequest req) { public ResultDTO<AppInfoVO> saveAppInfo(@RequestBody ModifyAppInfoRequest req) {
req.valid(); req.valid();
AppInfoDO appInfoDO; AppInfoDO appInfoDO;
@ -62,8 +70,8 @@ public class AppInfoController {
BeanUtils.copyProperties(req, appInfoDO); BeanUtils.copyProperties(req, appInfoDO);
appInfoDO.setGmtModified(new Date()); appInfoDO.setGmtModified(new Date());
appInfoRepository.saveAndFlush(appInfoDO); AppInfoDO savedAppInfo = appInfoRepository.saveAndFlush(appInfoDO);
return ResultDTO.success(null); return ResultDTO.success(convert(Lists.newArrayList(savedAppInfo), false).get(0));
} }
@PostMapping("/assert") @PostMapping("/assert")
@ -86,18 +94,66 @@ public class AppInfoController {
}else { }else {
result = appInfoRepository.findByAppNameLike("%" + condition + "%", limit).getContent(); result = appInfoRepository.findByAppNameLike("%" + condition + "%", limit).getContent();
} }
return ResultDTO.success(convert(result)); return ResultDTO.success(convert(result, false));
} }
private static List<AppInfoVO> convert(List<AppInfoDO> data) { @PostMapping("/listByQuery")
public ResultDTO<PageResult<AppInfoVO>> listAppInfoByQuery(QueryAppInfoRequest queryAppInfoRequest) {
Pageable pageable = PageRequest.of(queryAppInfoRequest.getIndex(), queryAppInfoRequest.getPageSize());
// TODO: 我有权限的列表
Specification<AppInfoDO> specification = new Specification<AppInfoDO>() {
@Override
public Predicate toPredicate(Root<AppInfoDO> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) {
List<Predicate> 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<AppInfoDO> pageAppInfoResult = appInfoRepository.findAll(specification, pageable);
PageResult<AppInfoVO> pageRet = new PageResult<>(pageAppInfoResult);
List<AppInfoDO> appinfoDos = pageAppInfoResult.get().collect(Collectors.toList());
pageRet.setData(convert(appinfoDos, true));
return ResultDTO.success(pageRet);
}
private static List<AppInfoVO> convert(List<AppInfoDO> data, boolean fillDetail) {
if (CollectionUtils.isEmpty(data)) { if (CollectionUtils.isEmpty(data)) {
return Lists.newLinkedList(); return Lists.newLinkedList();
} }
return data.stream().map(appInfoDO -> { List<AppInfoVO> appInfoVOList = data.stream().map(appInfoDO -> {
AppInfoVO appInfoVO = new AppInfoVO(); AppInfoVO appInfoVO = new AppInfoVO();
BeanUtils.copyProperties(appInfoDO, appInfoVO); BeanUtils.copyProperties(appInfoDO, appInfoVO);
return appInfoVO; return appInfoVO;
}).collect(Collectors.toList()); }).collect(Collectors.toList());
if (fillDetail) {
// TODO: 补全权限等额外信息
}
return appInfoVOList;
} }
@Data @Data

View File

@ -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<NamespaceBaseVO> 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<List<NamespaceBaseVO>> listNamespace(@RequestBody QueryNamespaceRequest queryNamespaceRequest) {
List<NamespaceDO> allDos = namespaceRepository.findAll();
return ResultDTO.success(allDos.stream().map(NamespaceConverter::do2BaseVo).collect(Collectors.toList()));
}
private NamespaceDO fetchById(Long id) {
Optional<NamespaceDO> namespaceDoOpt = namespaceRepository.findById(id);
if (!namespaceDoOpt.isPresent()) {
throw new IllegalArgumentException("can't find namespace by id: " + id);
}
return namespaceDoOpt.get();
}
}

View File

@ -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;
}
}

View File

@ -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!");
}
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}