mirror of
https://github.com/PowerJob/PowerJob.git
synced 2025-07-17 00:00:04 +08:00
feat: [auth] PowerJobPermissionService
This commit is contained in:
parent
0caa854409
commit
a1c12bf1c7
@ -28,4 +28,13 @@ public enum RoleScope {
|
||||
;
|
||||
|
||||
private final int v;
|
||||
|
||||
public static RoleScope of(int vv) {
|
||||
for (RoleScope rs : values()) {
|
||||
if (vv == rs.v) {
|
||||
return rs;
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("unknown RoleScope: " + vv);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,38 @@
|
||||
package tech.powerjob.server.auth.interceptor;
|
||||
|
||||
import tech.powerjob.server.auth.Permission;
|
||||
import tech.powerjob.server.auth.interceptor.dp.DynamicPermission;
|
||||
import tech.powerjob.server.auth.interceptor.dp.EmptyDynamicPermission;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* API 权限
|
||||
*
|
||||
* @author tjq
|
||||
* @since 2023/3/20
|
||||
*/
|
||||
@Target(ElementType.METHOD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface ApiPermission {
|
||||
/**
|
||||
* API 名称
|
||||
* @return 空使用服务.方法名代替
|
||||
*/
|
||||
String name() default "";
|
||||
|
||||
/**
|
||||
* 需要的权限
|
||||
* @return 权限
|
||||
*/
|
||||
Permission requiredPermission() default Permission.GLOBAL_SU;
|
||||
|
||||
/**
|
||||
* 固定权限不支持的场景,需要使用动态权限
|
||||
* @return 动态权限
|
||||
*/
|
||||
Class<? extends DynamicPermission> dynamicPermissionPlugin() default EmptyDynamicPermission.class;
|
||||
}
|
@ -0,0 +1,93 @@
|
||||
package tech.powerjob.server.auth.interceptor;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
import tech.powerjob.common.exception.PowerJobException;
|
||||
import tech.powerjob.server.auth.LoginUserHolder;
|
||||
import tech.powerjob.server.auth.PowerJobUser;
|
||||
import tech.powerjob.server.auth.service.login.PowerJobLoginService;
|
||||
import tech.powerjob.server.auth.service.permission.PowerJobPermissionService;
|
||||
import tech.powerjob.server.common.Loggers;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* login auth and permission check
|
||||
*
|
||||
* @author tjq
|
||||
* @since 2023/3/25
|
||||
*/
|
||||
@Component
|
||||
public class PowerJobAuthInterceptor implements HandlerInterceptor {
|
||||
@Resource
|
||||
private PowerJobLoginService powerJobLoginService;
|
||||
@Resource
|
||||
private PowerJobPermissionService powerJobPermissionService;
|
||||
|
||||
@Override
|
||||
public boolean preHandle(@NonNull HttpServletRequest request,@NonNull HttpServletResponse response,@NonNull Object handler) throws Exception {
|
||||
if (!(handler instanceof HandlerMethod)) {
|
||||
return true;
|
||||
}
|
||||
HandlerMethod handlerMethod = (HandlerMethod) handler;
|
||||
final Method method = handlerMethod.getMethod();
|
||||
final ApiPermission apiPermissionAnno = method.getAnnotation(ApiPermission.class);
|
||||
|
||||
// 无注解代表不需要权限,无需登陆直接访问
|
||||
if (apiPermissionAnno == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 尝试直接解析登陆
|
||||
final Optional<PowerJobUser> loginUserOpt = powerJobLoginService.ifLogin(request);
|
||||
|
||||
// 未登录前先使用302重定向到登录页面
|
||||
if (!loginUserOpt.isPresent()) {
|
||||
response.setStatus(302);
|
||||
response.setHeader("location", request.getContextPath() + "/login");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 登陆用户进行权限校验
|
||||
final PowerJobUser powerJobUser = loginUserOpt.get();
|
||||
|
||||
// 写入上下文
|
||||
LoginUserHolder.set(powerJobUser);
|
||||
|
||||
final boolean hasPermission = powerJobPermissionService.hasPermission(request, handler, powerJobUser, apiPermissionAnno);
|
||||
if (hasPermission) {
|
||||
return true;
|
||||
}
|
||||
|
||||
final String resourceName = parseResourceName(apiPermissionAnno, handlerMethod);
|
||||
Loggers.WEB.info("[PowerJobAuthInterceptor] user[{}] has no permission to access: {}", powerJobUser.getUsername(), resourceName);
|
||||
|
||||
throw new PowerJobException("Permission denied!");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterCompletion(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull Object handler, Exception ex) throws Exception {
|
||||
LoginUserHolder.clean();
|
||||
}
|
||||
|
||||
private static String parseResourceName(ApiPermission apiPermission, HandlerMethod handlerMethod) {
|
||||
final String name = apiPermission.name();
|
||||
if (StringUtils.isNotEmpty(name)) {
|
||||
return name;
|
||||
}
|
||||
try {
|
||||
final String clzName = handlerMethod.getBean().getClass().getSimpleName();
|
||||
final String methodName = handlerMethod.getMethod().getName();
|
||||
return String.format("%s_%s", clzName, methodName);
|
||||
} catch (Exception ignore) {
|
||||
}
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
package tech.powerjob.server.auth.interceptor.dp;
|
||||
|
||||
import tech.powerjob.server.auth.Permission;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
/**
|
||||
* 动态权限
|
||||
*
|
||||
* @author tjq
|
||||
* @since 2023/9/3
|
||||
*/
|
||||
public interface DynamicPermission {
|
||||
Permission calculate(HttpServletRequest request, Object handler);
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
package tech.powerjob.server.auth.interceptor.dp;
|
||||
|
||||
import tech.powerjob.server.auth.Permission;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
/**
|
||||
* NotUseDynamicPermission
|
||||
*
|
||||
* @author tjq
|
||||
* @since 2023/9/3
|
||||
*/
|
||||
public class EmptyDynamicPermission implements DynamicPermission {
|
||||
@Override
|
||||
public Permission calculate(HttpServletRequest request, Object handler) {
|
||||
return null;
|
||||
}
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
package tech.powerjob.server.auth.interceptor.dp;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.util.StreamUtils;
|
||||
import tech.powerjob.common.serialize.JsonUtils;
|
||||
import tech.powerjob.server.auth.Permission;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 针对 namespace 和 app 两大鉴权纬度,创建不需要任何权限,但任何修改操作都需要 WRITE 权限
|
||||
* 创建不需要权限,修改需要校验权限
|
||||
*
|
||||
* @author tjq
|
||||
* @since 2023/9/3
|
||||
*/
|
||||
@Slf4j
|
||||
public class ModifyOrCreateDynamicPermission implements DynamicPermission {
|
||||
@Override
|
||||
public Permission calculate(HttpServletRequest request, Object handler) {
|
||||
|
||||
try {
|
||||
//获取请求body
|
||||
byte[] bodyBytes = StreamUtils.copyToByteArray(request.getInputStream());
|
||||
String body = new String(bodyBytes, request.getCharacterEncoding());
|
||||
|
||||
Map<String, Object> inputParams = JsonUtils.parseMap(body);
|
||||
|
||||
Object id = inputParams.get("id");
|
||||
|
||||
// 创建,不需要权限
|
||||
if (id == null) {
|
||||
return Permission.NONE;
|
||||
}
|
||||
|
||||
return Permission.WRITE;
|
||||
} catch (Exception e) {
|
||||
log.error("[ModifyOrCreateDynamicPermission] check permission failed, please fix the bug!!!");
|
||||
}
|
||||
|
||||
// 异常情况先放行,不影响功能使用,后续修复 BUG
|
||||
return Permission.NONE;
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
package tech.powerjob.server.auth.service.permission;
|
||||
|
||||
import tech.powerjob.server.auth.PowerJobUser;
|
||||
import tech.powerjob.server.auth.interceptor.ApiPermission;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
/**
|
||||
* PowerJob 鉴权服务
|
||||
*
|
||||
* @author tjq
|
||||
* @since 2024/2/11
|
||||
*/
|
||||
public interface PowerJobPermissionService {
|
||||
|
||||
/**
|
||||
* 判断用户是否有访问权限
|
||||
* @param request 上下文请求
|
||||
* @param handler hander
|
||||
* @param user 用户
|
||||
* @param apiPermission 权限描述
|
||||
* @return true or false
|
||||
*/
|
||||
boolean hasPermission(HttpServletRequest request, Object handler, PowerJobUser user, ApiPermission apiPermission);
|
||||
}
|
@ -0,0 +1,128 @@
|
||||
package tech.powerjob.server.auth.service.permission;
|
||||
|
||||
import com.google.common.collect.ArrayListMultimap;
|
||||
import com.google.common.collect.Multimap;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.compress.utils.Lists;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.exception.ExceptionUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
import tech.powerjob.common.exception.ImpossibleException;
|
||||
import tech.powerjob.server.auth.Permission;
|
||||
import tech.powerjob.server.auth.PowerJobUser;
|
||||
import tech.powerjob.server.auth.Role;
|
||||
import tech.powerjob.server.auth.RoleScope;
|
||||
import tech.powerjob.server.auth.interceptor.ApiPermission;
|
||||
import tech.powerjob.server.auth.interceptor.dp.DynamicPermission;
|
||||
import tech.powerjob.server.auth.interceptor.dp.EmptyDynamicPermission;
|
||||
import tech.powerjob.server.persistence.remote.model.AppInfoDO;
|
||||
import tech.powerjob.server.persistence.remote.model.UserRoleDO;
|
||||
import tech.powerjob.server.persistence.remote.repository.AppInfoRepository;
|
||||
import tech.powerjob.server.persistence.remote.repository.UserRoleRepository;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* PowerJobPermissionService
|
||||
*
|
||||
* @author tjq
|
||||
* @since 2024/2/11
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class PowerJobPermissionServiceImpl implements PowerJobPermissionService {
|
||||
|
||||
@Resource
|
||||
private AppInfoRepository appInfoRepository;
|
||||
@Resource
|
||||
private UserRoleRepository userRoleRepository;
|
||||
|
||||
@Override
|
||||
public boolean hasPermission(HttpServletRequest request, Object handler, PowerJobUser user, ApiPermission apiPermission) {
|
||||
final List<UserRoleDO> userRoleList = Optional.ofNullable(userRoleRepository.findAllByUserId(user.getId())).orElse(Collections.emptyList());
|
||||
|
||||
Multimap<Long, Role> appId2Role = ArrayListMultimap.create();
|
||||
Multimap<Long, Role> namespaceId2Role = ArrayListMultimap.create();
|
||||
|
||||
List<Role> globalRoles = Lists.newArrayList();
|
||||
|
||||
for (UserRoleDO userRole : userRoleList) {
|
||||
final Role role = Role.of(userRole.getRole());
|
||||
RoleScope roleScope = RoleScope.of(userRole.getScope());
|
||||
|
||||
// 处理全局权限
|
||||
if (RoleScope.GLOBAL.equals(roleScope)) {
|
||||
if (Role.ADMIN.equals(role)) {
|
||||
return true;
|
||||
}
|
||||
globalRoles.add(role);
|
||||
}
|
||||
|
||||
if (Objects.equals(userRole.getScope(), RoleScope.NAMESPACE.getV())) {
|
||||
namespaceId2Role.put(userRole.getTarget(), role);
|
||||
}
|
||||
if (Objects.equals(userRole.getScope(), RoleScope.APP.getV())) {
|
||||
appId2Role.put(userRole.getTarget(), role);
|
||||
}
|
||||
}
|
||||
|
||||
// 无超级管理员权限,校验普通权限
|
||||
final String appIdStr = request.getHeader("appId");
|
||||
if (StringUtils.isEmpty(appIdStr)) {
|
||||
throw new IllegalArgumentException("can't find appId in header, please refresh and try again!");
|
||||
}
|
||||
|
||||
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)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 校验 namespace 穿透权限
|
||||
Optional<AppInfoDO> appInfoOpt = appInfoRepository.findById(appId);
|
||||
if (!appInfoOpt.isPresent()) {
|
||||
throw new IllegalArgumentException("can't find appInfo by appId in permission check: " + appId);
|
||||
}
|
||||
Long namespaceId = Optional.ofNullable(appInfoOpt.get().getNamespaceId()).orElse(-1L);
|
||||
Collection<Role> namespaceRoles = namespaceId2Role.get(namespaceId);
|
||||
for (Role role : namespaceRoles) {
|
||||
if (role.getPermissions().contains(requiredPermission)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 检验全局穿透权限(按使用频率排列检测顺序,即 app -> namespace -> global)
|
||||
for (Role role : globalRoles) {
|
||||
if (role.getPermissions().contains(requiredPermission)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static Permission parsePermission(HttpServletRequest request, Object handler, ApiPermission apiPermission) {
|
||||
Class<? extends DynamicPermission> dynamicPermissionPlugin = apiPermission.dynamicPermissionPlugin();
|
||||
if (EmptyDynamicPermission.class.equals(dynamicPermissionPlugin)) {
|
||||
return apiPermission.requiredPermission();
|
||||
}
|
||||
try {
|
||||
DynamicPermission dynamicPermission = dynamicPermissionPlugin.getDeclaredConstructor().newInstance();
|
||||
return dynamicPermission.calculate(request, handler);
|
||||
} catch (Throwable t) {
|
||||
log.error("[PowerJobAuthService] process dynamicPermissionPlugin failed!", t);
|
||||
ExceptionUtils.rethrow(t);
|
||||
}
|
||||
throw new ImpossibleException();
|
||||
}
|
||||
}
|
@ -22,6 +22,11 @@ public class AppInfoDO {
|
||||
@GenericGenerator(name = "native", strategy = "native")
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 命名空间ID,外键关联
|
||||
*/
|
||||
private Long namespaceId;
|
||||
|
||||
private String appName;
|
||||
/**
|
||||
* 应用分组密码
|
||||
|
@ -0,0 +1,54 @@
|
||||
package tech.powerjob.server.persistence.remote.model;
|
||||
|
||||
import lombok.Data;
|
||||
import org.hibernate.annotations.GenericGenerator;
|
||||
|
||||
import javax.persistence.*;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 命名空间,用于组织管理 App
|
||||
*
|
||||
* @author tjq
|
||||
* @since 2023/9/3
|
||||
*/
|
||||
@Data
|
||||
@Entity
|
||||
@Table(uniqueConstraints = {@UniqueConstraint(name = "uidx01_namespace", columnNames = {"code"})})
|
||||
public class NamespaceDO {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.AUTO, generator = "native")
|
||||
@GenericGenerator(name = "native", strategy = "native")
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 空间唯一标识
|
||||
*/
|
||||
private String code;
|
||||
|
||||
/**
|
||||
* 空间名称,比如中文描述(XX部门XX空间)
|
||||
*/
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* 鉴权 token,后续 OpenAPI 调用需要
|
||||
*/
|
||||
private String token;
|
||||
|
||||
private Integer status;
|
||||
|
||||
/**
|
||||
* 扩展字段
|
||||
*/
|
||||
private String extra;
|
||||
|
||||
private Date gmtCreate;
|
||||
|
||||
private Date gmtModified;
|
||||
|
||||
private String creator;
|
||||
|
||||
private String modifier;
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
package tech.powerjob.server.persistence.remote.repository;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import tech.powerjob.server.persistence.remote.model.NamespaceDO;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 命名空间
|
||||
*
|
||||
* @author tjq
|
||||
* @since 2023/9/3
|
||||
*/
|
||||
public interface NamespaceRepository extends JpaRepository<NamespaceDO, Long> {
|
||||
|
||||
Optional<NamespaceDO> findByCode(String code);
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user