mirror of
https://github.com/PowerJob/PowerJob.git
synced 2025-07-17 00:00:04 +08:00
feat: [auth] use CachingRequestBodyFilter fix multi read problem
This commit is contained in:
parent
e18b9a8962
commit
3fdcc1e599
@ -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";
|
||||
}
|
||||
|
@ -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 字符串
|
||||
|
@ -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";
|
||||
|
||||
/**
|
||||
* 前端跳转到指定页面指令
|
||||
|
@ -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<? extends DynamicPermission> dynamicPermissionPlugin() default EmptyDynamicPermission.class;
|
||||
|
||||
/**
|
||||
* 新增场景,需要授权插件执行授权
|
||||
* @return 授权插件
|
||||
*/
|
||||
Class<? extends GrantPermissionPlugin> grandPermissionPlugin() default EmptyGrantPermissionPlugin.class;
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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) {
|
||||
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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<String> 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)) {
|
||||
|
@ -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");
|
||||
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<Role> 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<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)) {
|
||||
return true;
|
||||
}
|
||||
@ -111,6 +141,8 @@ public class PowerJobPermissionServiceImpl implements PowerJobPermissionService
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
private static Permission parsePermission(HttpServletRequest request, Object handler, ApiPermission apiPermission) {
|
||||
Class<? extends DynamicPermission> dynamicPermissionPlugin = apiPermission.dynamicPermissionPlugin();
|
||||
if (EmptyDynamicPermission.class.equals(dynamicPermissionPlugin)) {
|
||||
|
@ -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;
|
||||
|
@ -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<AppInfoDO, Long> {
|
||||
public interface AppInfoRepository extends JpaRepository<AppInfoDO, Long>, JpaSpecificationExecutor<AppInfoDO> {
|
||||
|
||||
Optional<AppInfoDO> findByAppName(String appName);
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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<Void> saveAppInfo(@RequestBody ModifyAppInfoRequest req) {
|
||||
public ResultDTO<AppInfoVO> 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<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)) {
|
||||
return Lists.newLinkedList();
|
||||
}
|
||||
return data.stream().map(appInfoDO -> {
|
||||
List<AppInfoVO> appInfoVOList = data.stream().map(appInfoDO -> {
|
||||
AppInfoVO appInfoVO = new AppInfoVO();
|
||||
BeanUtils.copyProperties(appInfoDO, appInfoVO);
|
||||
return appInfoVO;
|
||||
}).collect(Collectors.toList());
|
||||
|
||||
if (fillDetail) {
|
||||
// TODO: 补全权限等额外信息
|
||||
}
|
||||
|
||||
return appInfoVOList;
|
||||
}
|
||||
|
||||
@Data
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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!");
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user