diff --git a/pom.xml b/pom.xml index 3e6d540..5755458 100644 --- a/pom.xml +++ b/pom.xml @@ -49,6 +49,11 @@ spring-boot-starter-web + + org.springframework.boot + spring-boot-starter-aop + + org.springframework.boot spring-boot-starter-test diff --git a/src/main/java/com/xuxd/kafka/console/KafkaConsoleUiApplication.java b/src/main/java/com/xuxd/kafka/console/KafkaConsoleUiApplication.java index aa36b29..73d9a44 100644 --- a/src/main/java/com/xuxd/kafka/console/KafkaConsoleUiApplication.java +++ b/src/main/java/com/xuxd/kafka/console/KafkaConsoleUiApplication.java @@ -6,6 +6,9 @@ import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.web.servlet.ServletComponentScan; import org.springframework.scheduling.annotation.EnableScheduling; +/** + * @author 晓东哥哥 + */ @MapperScan("com.xuxd.kafka.console.dao") @SpringBootApplication @EnableScheduling diff --git a/src/main/java/com/xuxd/kafka/console/aspect/ControllerLogAspect.java b/src/main/java/com/xuxd/kafka/console/aspect/ControllerLogAspect.java new file mode 100644 index 0000000..139fbf8 --- /dev/null +++ b/src/main/java/com/xuxd/kafka/console/aspect/ControllerLogAspect.java @@ -0,0 +1,117 @@ +package com.xuxd.kafka.console.aspect; + +import com.xuxd.kafka.console.aspect.annotation.ControllerLog; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.locks.ReentrantLock; + +/** + * @author 晓东哥哥 + */ +@Slf4j +@Order(-1) +@Aspect +@Component +public class ControllerLogAspect { + + private Map descMap = new HashMap<>(); + + private ReentrantLock lock = new ReentrantLock(); + + @Pointcut("@annotation(com.xuxd.kafka.console.aspect.annotation.ControllerLog)") + private void pointcut() { + + } + + @Around("pointcut()") + public Object around(ProceedingJoinPoint joinPoint) throws Throwable { + StringBuilder params = new StringBuilder("["); + try { + String methodName = getMethodFullName(joinPoint.getTarget().getClass().getName(), joinPoint.getSignature().getName()); + + if (!descMap.containsKey(methodName)) { + cacheDescInfo(joinPoint); + } + + Object[] args = joinPoint.getArgs(); + long startTime = System.currentTimeMillis(); + Object res = joinPoint.proceed(); + long endTime = System.currentTimeMillis(); + + for (int i = 0; i < args.length; i++) { + params.append(args[i]); + } + params.append("]"); + + String resStr = "[" + (res != null ? res.toString() : "") + "]"; + + StringBuilder sb = new StringBuilder(); + String shortMethodName = descMap.getOrDefault(methodName, ".-"); + shortMethodName = shortMethodName.substring(shortMethodName.lastIndexOf(".") + 1); + sb.append("[").append(shortMethodName) + .append("调用完成: ") + .append("请求参数=").append(params).append(", ") + .append("响应值=").append(resStr).append(", ") + .append("耗时=").append(endTime - startTime) + .append(" ms"); + log.info(sb.toString()); + return res; + } catch (Throwable e) { + log.error("调用方法异常, 请求参数:" + params, e); + throw e; + } + } + + private void cacheDescInfo(ProceedingJoinPoint joinPoint) { + lock.lock(); + try { + String methodName = joinPoint.getSignature().getName(); + Class aClass = joinPoint.getTarget().getClass(); + + Method method = null; + try { + Object[] args = joinPoint.getArgs(); + + Class[] clzArr = new Class[args.length]; + for (int i = 0; i < args.length; i++) { + clzArr[i] = args[i].getClass(); + } + method = aClass.getDeclaredMethod(methodName, clzArr); + + } catch (Exception e) { + log.warn("cacheDescInfo error: {}", e.getMessage()); + } + + String fullMethodName = getMethodFullName(aClass.getName(), methodName); + String desc = "[" + fullMethodName + "]"; + if (method == null) { + descMap.put(fullMethodName, desc); + return; + } + + ControllerLog controllerLog = method.getAnnotation(ControllerLog.class); + String value = controllerLog.value(); + if (StringUtils.isBlank(value)) { + descMap.put(fullMethodName, desc); + } else { + descMap.put(fullMethodName, value); + } + } finally { + lock.unlock(); + } + } + + private String getMethodFullName(String className, String methodName) { + return className + "#" + methodName; + } +} diff --git a/src/main/java/com/xuxd/kafka/console/aspect/PermissionAspect.java b/src/main/java/com/xuxd/kafka/console/aspect/PermissionAspect.java new file mode 100644 index 0000000..29dfafd --- /dev/null +++ b/src/main/java/com/xuxd/kafka/console/aspect/PermissionAspect.java @@ -0,0 +1,127 @@ +package com.xuxd.kafka.console.aspect; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.xuxd.kafka.console.aspect.annotation.Permission; +import com.xuxd.kafka.console.beans.Credentials; +import com.xuxd.kafka.console.beans.dos.SysUserDO; +import com.xuxd.kafka.console.cache.RolePermCache; +import com.xuxd.kafka.console.config.AuthConfig; +import com.xuxd.kafka.console.dao.SysPermissionMapper; +import com.xuxd.kafka.console.dao.SysRoleMapper; +import com.xuxd.kafka.console.dao.SysUserMapper; +import com.xuxd.kafka.console.exception.UnAuthorizedException; +import com.xuxd.kafka.console.filter.CredentialsContext; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.aspectj.lang.annotation.Pointcut; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +import java.lang.reflect.Method; +import java.util.*; +import java.util.stream.Collectors; + +/** + * @author: xuxd + * @date: 2023/5/17 22:32 + **/ +@Slf4j +@Order(1) +@Aspect +@Component +public class PermissionAspect { + + + private Map> permMap = new HashMap<>(); + + private final AuthConfig authConfig; + + private final SysUserMapper userMapper; + + private final SysRoleMapper roleMapper; + + private final SysPermissionMapper permissionMapper; + + private final RolePermCache rolePermCache; + + public PermissionAspect(AuthConfig authConfig, + SysUserMapper userMapper, + SysRoleMapper roleMapper, + SysPermissionMapper permissionMapper, + RolePermCache rolePermCache) { + this.authConfig = authConfig; + this.userMapper = userMapper; + this.roleMapper = roleMapper; + this.permissionMapper = permissionMapper; + this.rolePermCache = rolePermCache; + } + + @Pointcut("@annotation(com.xuxd.kafka.console.aspect.annotation.Permission)") + private void pointcut() { + + } + + @Before(value = "pointcut()") + public void before(JoinPoint joinPoint) { + if (!authConfig.isEnable()) { + return; + } + MethodSignature signature = (MethodSignature) joinPoint.getSignature(); + Method method = signature.getMethod(); + Permission permission = method.getAnnotation(Permission.class); + if (permission == null) { + return; + } + String[] value = permission.value(); + if (value == null || value.length == 0) { + return; + } + String name = method.getName() + "@" + method.hashCode(); + + Map> pm = checkPermMap(name, value); + + Set allowPermSet = pm.get(name); + if (allowPermSet == null) { + log.error("解析权限出现意外啦!!!"); + return; + } + + Credentials credentials = CredentialsContext.get(); + if (credentials == null || credentials.isInvalid()) { + throw new UnAuthorizedException("credentials is invalid"); + } + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("username", credentials.getUsername()); + SysUserDO userDO = userMapper.selectOne(queryWrapper); + if (userDO == null) { + throw new UnAuthorizedException(credentials.getUsername() + ":" + allowPermSet); + } + + String roleIds = userDO.getRoleIds(); + List roleIdList = Arrays.stream(roleIds.split(",")).map(String::trim).filter(StringUtils::isNotEmpty).map(Long::valueOf).collect(Collectors.toList()); + for (Long roleId : roleIdList) { + Set permSet = rolePermCache.getRolePermCache().getOrDefault(roleId, Collections.emptySet()); + for (String p : allowPermSet) { + if (permSet.contains(p)) { + return; + } + } + } + throw new UnAuthorizedException(credentials.getUsername() + ":" + allowPermSet); + } + + private Map> checkPermMap(String methodName, String[] value) { + if (!permMap.containsKey(methodName)) { + Map> map = new HashMap<>(permMap); + map.put(methodName, new HashSet<>(Arrays.asList(value))); + permMap = map; + return map; + } + return permMap; + } + +} diff --git a/src/main/java/com/xuxd/kafka/console/aspect/annotation/ControllerLog.java b/src/main/java/com/xuxd/kafka/console/aspect/annotation/ControllerLog.java new file mode 100644 index 0000000..b74a2bb --- /dev/null +++ b/src/main/java/com/xuxd/kafka/console/aspect/annotation/ControllerLog.java @@ -0,0 +1,15 @@ +package com.xuxd.kafka.console.aspect.annotation; + +import java.lang.annotation.*; + +/** + * 该注解用到controller层的方法上 + * @author 晓东哥哥 + */ +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface ControllerLog { + + String value() default ""; +} diff --git a/src/main/java/com/xuxd/kafka/console/aspect/annotation/Permission.java b/src/main/java/com/xuxd/kafka/console/aspect/annotation/Permission.java new file mode 100644 index 0000000..344634c --- /dev/null +++ b/src/main/java/com/xuxd/kafka/console/aspect/annotation/Permission.java @@ -0,0 +1,17 @@ +package com.xuxd.kafka.console.aspect.annotation; + +import java.lang.annotation.*; + +/** + * 权限注解,开启认证的时候拥有该权限的用户才能访问对应接口. + * + * @author: xuxd + * @date: 2023/5/17 22:30 + **/ +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface Permission { + + String[] value() default {}; +} diff --git a/src/main/java/com/xuxd/kafka/console/beans/Credentials.java b/src/main/java/com/xuxd/kafka/console/beans/Credentials.java new file mode 100644 index 0000000..fe6db9c --- /dev/null +++ b/src/main/java/com/xuxd/kafka/console/beans/Credentials.java @@ -0,0 +1,21 @@ +package com.xuxd.kafka.console.beans; + +import lombok.Data; + +/** + * @author: xuxd + * @date: 2023/5/14 19:37 + **/ +@Data +public class Credentials { + + public static final Credentials INVALID = new Credentials(); + + private String username; + + private long expiration; + + public boolean isInvalid() { + return this == INVALID; + } +} diff --git a/src/main/java/com/xuxd/kafka/console/beans/LoginResult.java b/src/main/java/com/xuxd/kafka/console/beans/LoginResult.java new file mode 100644 index 0000000..030fe17 --- /dev/null +++ b/src/main/java/com/xuxd/kafka/console/beans/LoginResult.java @@ -0,0 +1,17 @@ +package com.xuxd.kafka.console.beans; + +import lombok.Data; + +import java.util.List; + +/** + * @author: xuxd + * @date: 2023/5/14 20:44 + **/ +@Data +public class LoginResult { + + private String token; + + private List permissions; +} diff --git a/src/main/java/com/xuxd/kafka/console/beans/RolePermUpdateEvent.java b/src/main/java/com/xuxd/kafka/console/beans/RolePermUpdateEvent.java new file mode 100644 index 0000000..8c6d64e --- /dev/null +++ b/src/main/java/com/xuxd/kafka/console/beans/RolePermUpdateEvent.java @@ -0,0 +1,22 @@ +package com.xuxd.kafka.console.beans; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import org.springframework.context.ApplicationEvent; + +/** + * @author: xuxd + * @date: 2023/5/18 15:49 + **/ +@ToString +public class RolePermUpdateEvent extends ApplicationEvent { + + @Getter + @Setter + private boolean reload = false; + + public RolePermUpdateEvent(Object source) { + super(source); + } +} diff --git a/src/main/java/com/xuxd/kafka/console/beans/dos/SysPermissionDO.java b/src/main/java/com/xuxd/kafka/console/beans/dos/SysPermissionDO.java new file mode 100644 index 0000000..2548878 --- /dev/null +++ b/src/main/java/com/xuxd/kafka/console/beans/dos/SysPermissionDO.java @@ -0,0 +1,29 @@ +package com.xuxd.kafka.console.beans.dos; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +/** + * @author: xuxd + * @date: 2023/4/11 21:17 + **/ +@Data +@TableName("t_sys_permission") +public class SysPermissionDO { + + @TableId(type = IdType.AUTO) + private Long id; + + private String name; + + /** + * 权限类型: 0:菜单,1:按钮 + */ + private Integer type; + + private Long parentId; + + private String permission; +} diff --git a/src/main/java/com/xuxd/kafka/console/beans/dos/SysRoleDO.java b/src/main/java/com/xuxd/kafka/console/beans/dos/SysRoleDO.java new file mode 100644 index 0000000..fc6f65a --- /dev/null +++ b/src/main/java/com/xuxd/kafka/console/beans/dos/SysRoleDO.java @@ -0,0 +1,24 @@ +package com.xuxd.kafka.console.beans.dos; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +/** + * @author: xuxd + * @date: 2023/4/11 21:17 + **/ +@Data +@TableName("t_sys_role") +public class SysRoleDO { + + @TableId(type = IdType.AUTO) + private Long id; + + private String roleName; + + private String description; + + private String permissionIds; +} diff --git a/src/main/java/com/xuxd/kafka/console/beans/dos/SysUserDO.java b/src/main/java/com/xuxd/kafka/console/beans/dos/SysUserDO.java new file mode 100644 index 0000000..df70186 --- /dev/null +++ b/src/main/java/com/xuxd/kafka/console/beans/dos/SysUserDO.java @@ -0,0 +1,26 @@ +package com.xuxd.kafka.console.beans.dos; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +/** + * @author: xuxd + * @date: 2023/4/11 21:17 + **/ +@Data +@TableName("t_sys_user") +public class SysUserDO { + + @TableId(type = IdType.AUTO) + private Long id; + + private String username; + + private String password; + + private String salt; + + private String roleIds; +} diff --git a/src/main/java/com/xuxd/kafka/console/beans/dto/LoginUserDTO.java b/src/main/java/com/xuxd/kafka/console/beans/dto/LoginUserDTO.java new file mode 100644 index 0000000..b4fc2f4 --- /dev/null +++ b/src/main/java/com/xuxd/kafka/console/beans/dto/LoginUserDTO.java @@ -0,0 +1,15 @@ +package com.xuxd.kafka.console.beans.dto; + +import lombok.Data; + +/** + * @author: xuxd + * @date: 2023/5/14 18:59 + **/ +@Data +public class LoginUserDTO { + + private String username; + + private String password; +} diff --git a/src/main/java/com/xuxd/kafka/console/beans/dto/SysPermissionDTO.java b/src/main/java/com/xuxd/kafka/console/beans/dto/SysPermissionDTO.java new file mode 100644 index 0000000..5f34954 --- /dev/null +++ b/src/main/java/com/xuxd/kafka/console/beans/dto/SysPermissionDTO.java @@ -0,0 +1,32 @@ +package com.xuxd.kafka.console.beans.dto; + +import com.xuxd.kafka.console.beans.dos.SysPermissionDO; +import lombok.Data; + +/** + * @author: xuxd + * @date: 2023/4/11 21:17 + **/ +@Data +public class SysPermissionDTO { + + private String name; + + /** + * 权限类型: 0:菜单,1:按钮 + */ + private Integer type; + + private Long parentId; + + private String permission; + + public SysPermissionDO toSysPermissionDO() { + SysPermissionDO permissionDO = new SysPermissionDO(); + permissionDO.setName(this.name); + permissionDO.setType(this.type); + permissionDO.setParentId(this.parentId); + permissionDO.setPermission(this.permission); + return permissionDO; + } +} diff --git a/src/main/java/com/xuxd/kafka/console/beans/dto/SysRoleDTO.java b/src/main/java/com/xuxd/kafka/console/beans/dto/SysRoleDTO.java new file mode 100644 index 0000000..32f89f2 --- /dev/null +++ b/src/main/java/com/xuxd/kafka/console/beans/dto/SysRoleDTO.java @@ -0,0 +1,35 @@ +package com.xuxd.kafka.console.beans.dto; + +import com.xuxd.kafka.console.beans.dos.SysRoleDO; +import lombok.Data; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang3.StringUtils; + +import java.util.List; + +/** + * @author: xuxd + * @date: 2023/4/11 21:17 + **/ +@Data +public class SysRoleDTO { + + private Long id; + + private String roleName; + + private String description; + + private List permissionIds; + + public SysRoleDO toDO() { + SysRoleDO roleDO = new SysRoleDO(); + roleDO.setId(this.id); + roleDO.setRoleName(this.roleName); + roleDO.setDescription(this.description); + if (CollectionUtils.isNotEmpty(permissionIds)) { + roleDO.setPermissionIds(StringUtils.join(this.permissionIds, ",")); + } + return roleDO; + } +} diff --git a/src/main/java/com/xuxd/kafka/console/beans/dto/SysUserDTO.java b/src/main/java/com/xuxd/kafka/console/beans/dto/SysUserDTO.java new file mode 100644 index 0000000..2363bb2 --- /dev/null +++ b/src/main/java/com/xuxd/kafka/console/beans/dto/SysUserDTO.java @@ -0,0 +1,34 @@ +package com.xuxd.kafka.console.beans.dto; + +import com.xuxd.kafka.console.beans.dos.SysUserDO; +import lombok.Data; + +/** + * @author: xuxd + * @date: 2023/4/11 21:17 + **/ +@Data +public class SysUserDTO { + + private Long id; + + private String username; + + private String password; + + private String salt; + + private String roleIds; + + private Boolean resetPassword = false; + + public SysUserDO toDO() { + SysUserDO userDO = new SysUserDO(); + userDO.setId(this.id); + userDO.setUsername(this.username); + userDO.setPassword(this.password); + userDO.setSalt(this.salt); + userDO.setRoleIds(this.roleIds); + return userDO; + } +} diff --git a/src/main/java/com/xuxd/kafka/console/beans/vo/SysPermissionVO.java b/src/main/java/com/xuxd/kafka/console/beans/vo/SysPermissionVO.java new file mode 100644 index 0000000..5655b2e --- /dev/null +++ b/src/main/java/com/xuxd/kafka/console/beans/vo/SysPermissionVO.java @@ -0,0 +1,43 @@ +package com.xuxd.kafka.console.beans.vo; + +import com.xuxd.kafka.console.beans.dos.SysPermissionDO; +import lombok.Data; + +import java.util.List; + +/** + * @author: xuxd + * @date: 2023/4/17 21:18 + **/ +@Data +public class SysPermissionVO { + + private Long id; + + private String name; + + /** + * 权限类型: 0:菜单,1:按钮 + */ + private Integer type; + + private Long parentId; + + private String permission; + + private Long key; + + private List children; + + public static SysPermissionVO from(SysPermissionDO permissionDO) { + SysPermissionVO permissionVO = new SysPermissionVO(); + + permissionVO.setPermission(permissionDO.getPermission()); + permissionVO.setType(permissionDO.getType()); + permissionVO.setName(permissionDO.getName()); + permissionVO.setParentId(permissionDO.getParentId()); + permissionVO.setKey(permissionDO.getId()); + permissionVO.setId(permissionDO.getId()); + return permissionVO; + } +} diff --git a/src/main/java/com/xuxd/kafka/console/beans/vo/SysRoleVO.java b/src/main/java/com/xuxd/kafka/console/beans/vo/SysRoleVO.java new file mode 100644 index 0000000..a56a4de --- /dev/null +++ b/src/main/java/com/xuxd/kafka/console/beans/vo/SysRoleVO.java @@ -0,0 +1,38 @@ +package com.xuxd.kafka.console.beans.vo; + +import com.xuxd.kafka.console.beans.dos.SysRoleDO; +import lombok.Data; +import org.apache.commons.lang3.StringUtils; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +/** + * @author: xuxd + * @date: 2023/4/19 21:12 + **/ +@Data +public class SysRoleVO { + + private Long id; + + private String roleName; + + private String description; + + private List permissionIds; + + public static SysRoleVO from(SysRoleDO roleDO) { + SysRoleVO roleVO = new SysRoleVO(); + roleVO.setId(roleDO.getId()); + roleVO.setRoleName(roleDO.getRoleName()); + roleVO.setDescription(roleDO.getDescription()); + if (StringUtils.isNotEmpty(roleDO.getPermissionIds())) { + List list = Arrays.stream(roleDO.getPermissionIds().split(",")). + filter(StringUtils::isNotEmpty).map(e -> Long.valueOf(e.trim())).collect(Collectors.toList()); + roleVO.setPermissionIds(list); + } + return roleVO; + } +} diff --git a/src/main/java/com/xuxd/kafka/console/beans/vo/SysUserVO.java b/src/main/java/com/xuxd/kafka/console/beans/vo/SysUserVO.java new file mode 100644 index 0000000..199495d --- /dev/null +++ b/src/main/java/com/xuxd/kafka/console/beans/vo/SysUserVO.java @@ -0,0 +1,31 @@ +package com.xuxd.kafka.console.beans.vo; + +import com.xuxd.kafka.console.beans.dos.SysUserDO; +import lombok.Data; + +/** + * @author: xuxd + * @date: 2023/5/6 13:06 + **/ +@Data +public class SysUserVO { + + private Long id; + + private String username; + + private String password; + + private String roleIds; + + private String roleNames; + + public static SysUserVO from(SysUserDO userDO) { + SysUserVO userVO = new SysUserVO(); + userVO.setId(userDO.getId()); + userVO.setUsername(userDO.getUsername()); + userVO.setRoleIds(userDO.getRoleIds()); + userVO.setPassword(userDO.getPassword()); + return userVO; + } +} diff --git a/src/main/java/com/xuxd/kafka/console/cache/RolePermCache.java b/src/main/java/com/xuxd/kafka/console/cache/RolePermCache.java new file mode 100644 index 0000000..2ea8a2c --- /dev/null +++ b/src/main/java/com/xuxd/kafka/console/cache/RolePermCache.java @@ -0,0 +1,91 @@ +package com.xuxd.kafka.console.cache; + +import com.xuxd.kafka.console.beans.RolePermUpdateEvent; +import com.xuxd.kafka.console.beans.dos.SysPermissionDO; +import com.xuxd.kafka.console.beans.dos.SysRoleDO; +import com.xuxd.kafka.console.dao.SysPermissionMapper; +import com.xuxd.kafka.console.dao.SysRoleMapper; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.SmartInitializingSingleton; +import org.springframework.context.ApplicationListener; +import org.springframework.context.annotation.DependsOn; +import org.springframework.stereotype.Component; + +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * @author: xuxd + * @date: 2023/5/18 15:47 + **/ +@DependsOn("dataInit") +@Slf4j +@Component +public class RolePermCache implements ApplicationListener, SmartInitializingSingleton { + + private Map permCache = new HashMap<>(); + + private Map> rolePermCache = new HashMap<>(); + + private final SysPermissionMapper permissionMapper; + + private final SysRoleMapper roleMapper; + + public RolePermCache(SysPermissionMapper permissionMapper, SysRoleMapper roleMapper) { + this.permissionMapper = permissionMapper; + this.roleMapper = roleMapper; + } + + @Override + public void onApplicationEvent(RolePermUpdateEvent event) { + log.info("更新角色权限信息:{}", event); + if (event.isReload()) { + this.loadPermCache(); + } + refresh(); + } + + public Map getPermCache() { + return permCache; + } + + public Map> getRolePermCache() { + return rolePermCache; + } + + private void refresh() { + List roleDOS = roleMapper.selectList(null); + Map> tmp = new HashMap<>(); + for (SysRoleDO roleDO : roleDOS) { + String permissionIds = roleDO.getPermissionIds(); + if (StringUtils.isEmpty(permissionIds)) { + continue; + } + List list = Arrays.stream(permissionIds.split(",")).map(String::trim).filter(StringUtils::isNotEmpty).map(Long::valueOf).collect(Collectors.toList()); + Set permSet = tmp.getOrDefault(roleDO.getId(), new HashSet<>()); + for (Long permId : list) { + SysPermissionDO permissionDO = permCache.get(permId); + if (permissionDO != null) { + permSet.add(permissionDO.getPermission()); + } + } + tmp.put(roleDO.getId(), permSet); + } + rolePermCache = tmp; + } + + private void loadPermCache() { + List roleDOS = permissionMapper.selectList(null); + Map map = roleDOS.stream().collect(Collectors.toMap(SysPermissionDO::getId, Function.identity(), (e1, e2) -> e1)); + permCache = map; + } + + + @Override + public void afterSingletonsInstantiated() { + this.loadPermCache(); + this.refresh(); + } +} diff --git a/src/main/java/com/xuxd/kafka/console/config/AuthConfig.java b/src/main/java/com/xuxd/kafka/console/config/AuthConfig.java new file mode 100644 index 0000000..04b2d42 --- /dev/null +++ b/src/main/java/com/xuxd/kafka/console/config/AuthConfig.java @@ -0,0 +1,21 @@ +package com.xuxd.kafka.console.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +/** + * @author: xuxd + * @date: 2023/5/9 21:08 + **/ +@Data +@Configuration +@ConfigurationProperties(prefix = "auth") +public class AuthConfig { + + private boolean enable; + + private String secret = "kafka-console-ui-default-secret"; + + private long expireHours; +} diff --git a/src/main/java/com/xuxd/kafka/console/controller/AclAuthController.java b/src/main/java/com/xuxd/kafka/console/controller/AclAuthController.java index 0c9ee4f..8bfdbbb 100644 --- a/src/main/java/com/xuxd/kafka/console/controller/AclAuthController.java +++ b/src/main/java/com/xuxd/kafka/console/controller/AclAuthController.java @@ -1,5 +1,6 @@ package com.xuxd.kafka.console.controller; +import com.xuxd.kafka.console.aspect.annotation.Permission; import com.xuxd.kafka.console.beans.AclEntry; import com.xuxd.kafka.console.beans.dto.AddAuthDTO; import com.xuxd.kafka.console.beans.dto.ConsumerAuthDTO; @@ -28,6 +29,7 @@ public class AclAuthController { @Autowired private AclService aclService; + @Permission({"acl:authority:detail", "acl:sasl-scram:detail"}) @PostMapping("/detail") public Object getAclDetailList(@RequestBody QueryAclDTO param) { return aclService.getAclDetailList(param.toEntry()); @@ -38,11 +40,13 @@ public class AclAuthController { return aclService.getOperationList(); } + @Permission("acl:authority") @PostMapping("/list") public Object getAclList(@RequestBody QueryAclDTO param) { return aclService.getAclList(param.toEntry()); } + @Permission({"acl:authority:add-principal", "acl:authority:add", "acl:sasl-scram:add-auth"}) @PostMapping public Object addAcl(@RequestBody AddAuthDTO param) { return aclService.addAcl(param.toAclEntry()); @@ -54,6 +58,7 @@ public class AclAuthController { * @param param entry.topic && entry.username must. * @return */ + @Permission({"acl:authority:producer", "acl:sasl-scram:producer"}) @PostMapping("/producer") public Object addProducerAcl(@RequestBody ProducerAuthDTO param) { @@ -66,6 +71,7 @@ public class AclAuthController { * @param param entry.topic && entry.groupId entry.username must. * @return */ + @Permission({"acl:authority:consumer", "acl:sasl-scram:consumer"}) @PostMapping("/consumer") public Object addConsumerAcl(@RequestBody ConsumerAuthDTO param) { @@ -78,6 +84,7 @@ public class AclAuthController { * @param entry entry * @return */ + @Permission({"acl:authority:clean", "acl:sasl-scram:pure"}) @DeleteMapping public Object deleteAclByUser(@RequestBody AclEntry entry) { return aclService.deleteAcl(entry); @@ -89,6 +96,7 @@ public class AclAuthController { * @param param entry.username * @return */ + @Permission({"acl:authority:clean", "acl:sasl-scram:pure"}) @DeleteMapping("/user") public Object deleteAclByUser(@RequestBody DeleteAclDTO param) { return aclService.deleteUserAcl(param.toUserEntry()); @@ -100,6 +108,7 @@ public class AclAuthController { * @param param entry.topic && entry.username must. * @return */ + @Permission({"acl:authority:clean", "acl:sasl-scram:pure"}) @DeleteMapping("/producer") public Object deleteProducerAcl(@RequestBody ProducerAuthDTO param) { @@ -112,6 +121,7 @@ public class AclAuthController { * @param param entry.topic && entry.groupId entry.username must. * @return */ + @Permission({"acl:authority:clean", "acl:sasl-scram:pure"}) @DeleteMapping("/consumer") public Object deleteConsumerAcl(@RequestBody ConsumerAuthDTO param) { @@ -124,6 +134,7 @@ public class AclAuthController { * @param param acl principal. * @return true or false. */ + @Permission({"acl:authority:clean", "acl:sasl-scram:pure"}) @DeleteMapping("/clear") public Object clearAcl(@RequestBody DeleteAclDTO param) { return aclService.clearAcl(param.toUserEntry()); diff --git a/src/main/java/com/xuxd/kafka/console/controller/AclUserController.java b/src/main/java/com/xuxd/kafka/console/controller/AclUserController.java index 7570287..932d68a 100644 --- a/src/main/java/com/xuxd/kafka/console/controller/AclUserController.java +++ b/src/main/java/com/xuxd/kafka/console/controller/AclUserController.java @@ -1,5 +1,6 @@ package com.xuxd.kafka.console.controller; +import com.xuxd.kafka.console.aspect.annotation.Permission; import com.xuxd.kafka.console.beans.AclEntry; import com.xuxd.kafka.console.beans.AclUser; import com.xuxd.kafka.console.service.AclService; @@ -26,27 +27,32 @@ public class AclUserController { @Autowired private AclService aclService; + @Permission("acl:sasl-scram") @GetMapping public Object getUserList() { return aclService.getUserList(); } + @Permission({"acl:sasl-scram:add-update", "acl:sasl-scram:add-auth"}) @PostMapping public Object addOrUpdateUser(@RequestBody AclUser user) { return aclService.addOrUpdateUser(user.getUsername(), user.getPassword()); } + @Permission({"acl:sasl-scram:del", "acl:sasl-scram:pure"}) @DeleteMapping public Object deleteUser(@RequestBody AclUser user) { return aclService.deleteUser(user.getUsername()); } + @Permission({"acl:sasl-scram:del", "acl:sasl-scram:pure"}) @DeleteMapping("/auth") public Object deleteUserAndAuth(@RequestBody AclUser user) { return aclService.deleteUserAndAuth(user.getUsername()); } + @Permission("acl:sasl-scram:detail") @GetMapping("/detail") public Object getUserDetail(@RequestParam String username) { return aclService.getUserDetail(username); diff --git a/src/main/java/com/xuxd/kafka/console/controller/AuthController.java b/src/main/java/com/xuxd/kafka/console/controller/AuthController.java new file mode 100644 index 0000000..f71e806 --- /dev/null +++ b/src/main/java/com/xuxd/kafka/console/controller/AuthController.java @@ -0,0 +1,36 @@ +package com.xuxd.kafka.console.controller; + +import com.xuxd.kafka.console.beans.ResponseData; +import com.xuxd.kafka.console.beans.dto.LoginUserDTO; +import com.xuxd.kafka.console.config.AuthConfig; +import com.xuxd.kafka.console.service.AuthService; +import org.springframework.web.bind.annotation.*; + +/** + * @author: xuxd + * @date: 2023/5/11 18:54 + **/ +@RestController +@RequestMapping("/auth") +public class AuthController { + + + private final AuthConfig authConfig; + + private final AuthService authService; + + public AuthController(AuthConfig authConfig, AuthService authService) { + this.authConfig = authConfig; + this.authService = authService; + } + + @GetMapping("/enable") + public boolean enable() { + return authConfig.isEnable(); + } + + @PostMapping("/login") + public ResponseData login(@RequestBody LoginUserDTO userDTO) { + return authService.login(userDTO); + } +} diff --git a/src/main/java/com/xuxd/kafka/console/controller/ClientQuotaController.java b/src/main/java/com/xuxd/kafka/console/controller/ClientQuotaController.java index ad6c921..e11da6e 100644 --- a/src/main/java/com/xuxd/kafka/console/controller/ClientQuotaController.java +++ b/src/main/java/com/xuxd/kafka/console/controller/ClientQuotaController.java @@ -1,5 +1,6 @@ package com.xuxd.kafka.console.controller; +import com.xuxd.kafka.console.aspect.annotation.Permission; import com.xuxd.kafka.console.beans.ResponseData; import com.xuxd.kafka.console.beans.dto.AlterClientQuotaDTO; import com.xuxd.kafka.console.beans.dto.QueryClientQuotaDTO; @@ -21,11 +22,13 @@ public class ClientQuotaController { this.clientQuotaService = clientQuotaService; } + @Permission({"quota:user", "quota:client", "quota:user-client"}) @PostMapping("/list") public Object getClientQuotaConfigs(@RequestBody QueryClientQuotaDTO request) { return clientQuotaService.getClientQuotaConfigs(request.getTypes(), request.getNames()); } + @Permission({"quota:user:add", "quota:client:add", "quota:user-client:add", "quota:edit"}) @PostMapping public Object alterClientQuotaConfigs(@RequestBody AlterClientQuotaDTO request) { if (request.getTypes().size() != 2) { @@ -38,6 +41,7 @@ public class ClientQuotaController { return clientQuotaService.alterClientQuotaConfigs(request); } + @Permission("quota:del") @DeleteMapping public Object deleteClientQuotaConfigs(@RequestBody AlterClientQuotaDTO request) { if (request.getTypes().size() != 2) { diff --git a/src/main/java/com/xuxd/kafka/console/controller/ClusterController.java b/src/main/java/com/xuxd/kafka/console/controller/ClusterController.java index d9c67bd..14e0a39 100644 --- a/src/main/java/com/xuxd/kafka/console/controller/ClusterController.java +++ b/src/main/java/com/xuxd/kafka/console/controller/ClusterController.java @@ -1,5 +1,6 @@ package com.xuxd.kafka.console.controller; +import com.xuxd.kafka.console.aspect.annotation.Permission; import com.xuxd.kafka.console.beans.dto.ClusterInfoDTO; import com.xuxd.kafka.console.service.ClusterService; import org.springframework.beans.factory.annotation.Autowired; @@ -34,16 +35,19 @@ public class ClusterController { return clusterService.getClusterInfoList(); } + @Permission("op:cluster-switch:add") @PostMapping("/info") public Object addClusterInfo(@RequestBody ClusterInfoDTO dto) { return clusterService.addClusterInfo(dto.to()); } + @Permission("op:cluster-switch:del") @DeleteMapping("/info") public Object deleteClusterInfo(@RequestBody ClusterInfoDTO dto) { return clusterService.deleteClusterInfo(dto.getId()); } + @Permission("op:cluster-switch:edit") @PutMapping("/info") public Object updateClusterInfo(@RequestBody ClusterInfoDTO dto) { return clusterService.updateClusterInfo(dto.to()); diff --git a/src/main/java/com/xuxd/kafka/console/controller/ConfigController.java b/src/main/java/com/xuxd/kafka/console/controller/ConfigController.java index c7104dd..ffd37b9 100644 --- a/src/main/java/com/xuxd/kafka/console/controller/ConfigController.java +++ b/src/main/java/com/xuxd/kafka/console/controller/ConfigController.java @@ -1,5 +1,6 @@ package com.xuxd.kafka.console.controller; +import com.xuxd.kafka.console.aspect.annotation.Permission; import com.xuxd.kafka.console.beans.ResponseData; import com.xuxd.kafka.console.beans.dto.AlterConfigDTO; import com.xuxd.kafka.console.beans.enums.AlterType; @@ -41,46 +42,55 @@ public class ConfigController { return ResponseData.create().data(configMap).success(); } + @Permission("topic:property-config") @GetMapping("/topic") public Object getTopicConfig(String topic) { return configService.getTopicConfig(topic); } + @Permission("topic:property-config:edit") @PostMapping("/topic") public Object setTopicConfig(@RequestBody AlterConfigDTO dto) { return configService.alterTopicConfig(dto.getEntity(), dto.to(), AlterType.SET); } + @Permission("topic:property-config:del") @DeleteMapping("/topic") public Object deleteTopicConfig(@RequestBody AlterConfigDTO dto) { return configService.alterTopicConfig(dto.getEntity(), dto.to(), AlterType.DELETE); } + @Permission("cluster:property-config") @GetMapping("/broker") public Object getBrokerConfig(String brokerId) { return configService.getBrokerConfig(brokerId); } + @Permission("cluster:edit") @PostMapping("/broker") public Object setBrokerConfig(@RequestBody AlterConfigDTO dto) { return configService.alterBrokerConfig(dto.getEntity(), dto.to(), AlterType.SET); } + @Permission("cluster:edit") @DeleteMapping("/broker") public Object deleteBrokerConfig(@RequestBody AlterConfigDTO dto) { return configService.alterBrokerConfig(dto.getEntity(), dto.to(), AlterType.DELETE); } + @Permission("cluster:log-config") @GetMapping("/broker/logger") public Object getBrokerLoggerConfig(String brokerId) { return configService.getBrokerLoggerConfig(brokerId); } + @Permission("cluster:edit") @PostMapping("/broker/logger") public Object setBrokerLoggerConfig(@RequestBody AlterConfigDTO dto) { return configService.alterBrokerLoggerConfig(dto.getEntity(), dto.to(), AlterType.SET); } + @Permission("cluster:edit") @DeleteMapping("/broker/logger") public Object deleteBrokerLoggerConfig(@RequestBody AlterConfigDTO dto) { return configService.alterBrokerLoggerConfig(dto.getEntity(), dto.to(), AlterType.DELETE); diff --git a/src/main/java/com/xuxd/kafka/console/controller/ConsumerController.java b/src/main/java/com/xuxd/kafka/console/controller/ConsumerController.java index 8418abd..c5a7320 100644 --- a/src/main/java/com/xuxd/kafka/console/controller/ConsumerController.java +++ b/src/main/java/com/xuxd/kafka/console/controller/ConsumerController.java @@ -1,28 +1,20 @@ package com.xuxd.kafka.console.controller; +import com.xuxd.kafka.console.aspect.annotation.Permission; import com.xuxd.kafka.console.beans.ResponseData; import com.xuxd.kafka.console.beans.dto.AddSubscriptionDTO; import com.xuxd.kafka.console.beans.dto.QueryConsumerGroupDTO; import com.xuxd.kafka.console.beans.dto.ResetOffsetDTO; import com.xuxd.kafka.console.service.ConsumerService; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Objects; -import java.util.Set; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.apache.kafka.clients.consumer.OffsetResetStrategy; import org.apache.kafka.common.ConsumerGroupState; import org.apache.kafka.common.TopicPartition; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; + +import java.util.*; /** * kafka-console-ui. @@ -51,26 +43,34 @@ public class ConsumerController { return consumerService.getConsumerGroupList(groupIdList, stateSet); } + @Permission("group:del") @DeleteMapping("/group") public Object deleteConsumerGroup(@RequestParam String groupId) { return consumerService.deleteConsumerGroup(groupId); } + @Permission("group:client") @GetMapping("/member") public Object getConsumerMembers(@RequestParam String groupId) { return consumerService.getConsumerMembers(groupId); } + @Permission("group:consumer-detail") @GetMapping("/detail") public Object getConsumerDetail(@RequestParam String groupId) { return consumerService.getConsumerDetail(groupId); } + @Permission("group:add") @PostMapping("/subscription") public Object addSubscription(@RequestBody AddSubscriptionDTO subscriptionDTO) { return consumerService.addSubscription(subscriptionDTO.getGroupId(), subscriptionDTO.getTopic()); } + @Permission({"group:consumer-detail:min", + "group:consumer-detail:last", + "group:consumer-detail:timestamp", + "group:consumer-detail:any"}) @PostMapping("/reset/offset") public Object restOffset(@RequestBody ResetOffsetDTO offsetDTO) { ResponseData res = ResponseData.create().failed("unknown"); @@ -78,7 +78,7 @@ public class ConsumerController { case ResetOffsetDTO.Level.TOPIC: switch (offsetDTO.getType()) { case ResetOffsetDTO.Type - .EARLIEST: + .EARLIEST: res = consumerService.resetOffsetToEndpoint(offsetDTO.getGroupId(), offsetDTO.getTopic(), OffsetResetStrategy.EARLIEST); break; case ResetOffsetDTO.Type.LATEST: @@ -94,7 +94,7 @@ public class ConsumerController { case ResetOffsetDTO.Level.PARTITION: switch (offsetDTO.getType()) { case ResetOffsetDTO.Type - .SPECIAL: + .SPECIAL: res = consumerService.resetPartitionToTargetOffset(offsetDTO.getGroupId(), new TopicPartition(offsetDTO.getTopic(), offsetDTO.getPartition()), offsetDTO.getOffset()); break; default: @@ -118,11 +118,13 @@ public class ConsumerController { return consumerService.getSubscribeTopicList(groupId); } + @Permission({"topic:consumer-detail"}) @GetMapping("/topic/subscribed") public Object getTopicSubscribedByGroups(@RequestParam String topic) { return consumerService.getTopicSubscribedByGroups(topic); } + @Permission("group:offset-partition") @GetMapping("/offset/partition") public Object getOffsetPartition(@RequestParam String groupId) { return consumerService.getOffsetPartition(groupId); diff --git a/src/main/java/com/xuxd/kafka/console/controller/MessageController.java b/src/main/java/com/xuxd/kafka/console/controller/MessageController.java index 154f4bb..29f3670 100644 --- a/src/main/java/com/xuxd/kafka/console/controller/MessageController.java +++ b/src/main/java/com/xuxd/kafka/console/controller/MessageController.java @@ -1,5 +1,6 @@ package com.xuxd.kafka.console.controller; +import com.xuxd.kafka.console.aspect.annotation.Permission; import com.xuxd.kafka.console.beans.QueryMessage; import com.xuxd.kafka.console.beans.ResponseData; import com.xuxd.kafka.console.beans.SendMessage; @@ -24,16 +25,19 @@ public class MessageController { @Autowired private MessageService messageService; + @Permission("message:search-time") @PostMapping("/search/time") public Object searchByTime(@RequestBody QueryMessageDTO dto) { return messageService.searchByTime(dto.toQueryMessage()); } + @Permission("message:search-offset") @PostMapping("/search/offset") public Object searchByOffset(@RequestBody QueryMessageDTO dto) { return messageService.searchByOffset(dto.toQueryMessage()); } + @Permission("message:detail") @PostMapping("/search/detail") public Object searchDetail(@RequestBody QueryMessageDTO dto) { return messageService.searchDetail(dto.toQueryMessage()); @@ -44,16 +48,19 @@ public class MessageController { return messageService.deserializerList(); } + @Permission("message:send") @PostMapping("/send") public Object send(@RequestBody SendMessage message) { return messageService.send(message); } + @Permission("message:resend") @PostMapping("/resend") public Object resend(@RequestBody SendMessage message) { return messageService.resend(message); } + @Permission("message:del") @DeleteMapping public Object delete(@RequestBody List messages) { if (CollectionUtils.isEmpty(messages)) { diff --git a/src/main/java/com/xuxd/kafka/console/controller/OperationController.java b/src/main/java/com/xuxd/kafka/console/controller/OperationController.java index cf5e509..4daf6bc 100644 --- a/src/main/java/com/xuxd/kafka/console/controller/OperationController.java +++ b/src/main/java/com/xuxd/kafka/console/controller/OperationController.java @@ -1,5 +1,6 @@ package com.xuxd.kafka.console.controller; +import com.xuxd.kafka.console.aspect.annotation.Permission; import com.xuxd.kafka.console.beans.TopicPartition; import com.xuxd.kafka.console.beans.dto.BrokerThrottleDTO; import com.xuxd.kafka.console.beans.dto.ProposedAssignmentDTO; @@ -8,13 +9,7 @@ import com.xuxd.kafka.console.beans.dto.SyncDataDTO; import com.xuxd.kafka.console.service.OperationService; import org.apache.kafka.clients.admin.AdminClientConfig; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; /** * kafka-console-ui. @@ -51,26 +46,31 @@ public class OperationController { return operationService.deleteAlignmentById(id); } + @Permission({"topic:partition-detail:preferred", "op:replication-preferred"}) @PostMapping("/replication/preferred") public Object electPreferredLeader(@RequestBody ReplicationDTO dto) { return operationService.electPreferredLeader(dto.getTopic(), dto.getPartition()); } + @Permission("op:config-throttle") @PostMapping("/broker/throttle") public Object configThrottle(@RequestBody BrokerThrottleDTO dto) { return operationService.configThrottle(dto.getBrokerList(), dto.getUnit().toKb(dto.getThrottle())); } + @Permission("op:remove-throttle") @DeleteMapping("/broker/throttle") public Object removeThrottle(@RequestBody BrokerThrottleDTO dto) { return operationService.removeThrottle(dto.getBrokerList()); } + @Permission("op:replication-update-detail") @GetMapping("/replication/reassignments") public Object currentReassignments() { return operationService.currentReassignments(); } + @Permission("op:replication-update-detail:cancel") @DeleteMapping("/replication/reassignments") public Object cancelReassignment(@RequestBody TopicPartition partition) { return operationService.cancelReassignment(new org.apache.kafka.common.TopicPartition(partition.getTopic(), partition.getPartition())); diff --git a/src/main/java/com/xuxd/kafka/console/controller/TopicController.java b/src/main/java/com/xuxd/kafka/console/controller/TopicController.java index c5b083b..318630a 100644 --- a/src/main/java/com/xuxd/kafka/console/controller/TopicController.java +++ b/src/main/java/com/xuxd/kafka/console/controller/TopicController.java @@ -1,23 +1,19 @@ package com.xuxd.kafka.console.controller; +import com.xuxd.kafka.console.aspect.annotation.Permission; import com.xuxd.kafka.console.beans.ReplicaAssignment; import com.xuxd.kafka.console.beans.dto.AddPartitionDTO; import com.xuxd.kafka.console.beans.dto.NewTopicDTO; import com.xuxd.kafka.console.beans.dto.TopicThrottleDTO; import com.xuxd.kafka.console.beans.enums.TopicType; import com.xuxd.kafka.console.service.TopicService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; /** * kafka-console-ui. @@ -37,26 +33,31 @@ public class TopicController { return topicService.getTopicNameList(false); } + @Permission("topic:load") @GetMapping("/list") public Object getTopicList(@RequestParam(required = false) String topic, @RequestParam String type) { return topicService.getTopicList(topic, TopicType.valueOf(type.toUpperCase())); } + @Permission({"topic:batch-del", "topic:del"}) @DeleteMapping public Object deleteTopic(@RequestBody List topics) { return topicService.deleteTopics(topics); } + @Permission("topic:partition-detail") @GetMapping("/partition") public Object getTopicPartitionInfo(@RequestParam String topic) { return topicService.getTopicPartitionInfo(topic.trim()); } + @Permission("topic:add") @PostMapping("/new") public Object createNewTopic(@RequestBody NewTopicDTO topicDTO) { return topicService.createTopic(topicDTO.toNewTopic()); } + @Permission("topic:partition-add") @PostMapping("/partition/new") public Object addPartition(@RequestBody AddPartitionDTO partitionDTO) { String topic = partitionDTO.getTopic().trim(); @@ -79,16 +80,19 @@ public class TopicController { return topicService.getCurrentReplicaAssignment(topic); } + @Permission({"topic:replication-modify", "op:replication-reassign"}) @PostMapping("/replica/assignment") public Object updateReplicaAssignment(@RequestBody ReplicaAssignment assignment) { return topicService.updateReplicaAssignment(assignment); } + @Permission("topic:replication-sync-throttle") @PostMapping("/replica/throttle") public Object configThrottle(@RequestBody TopicThrottleDTO dto) { return topicService.configThrottle(dto.getTopic(), dto.getPartitions(), dto.getOperation()); } + @Permission("topic:send-count") @GetMapping("/send/stats") public Object sendStats(@RequestParam String topic) { return topicService.sendStats(topic); diff --git a/src/main/java/com/xuxd/kafka/console/controller/UserManageController.java b/src/main/java/com/xuxd/kafka/console/controller/UserManageController.java new file mode 100644 index 0000000..6af2def --- /dev/null +++ b/src/main/java/com/xuxd/kafka/console/controller/UserManageController.java @@ -0,0 +1,97 @@ +package com.xuxd.kafka.console.controller; + +import com.xuxd.kafka.console.aspect.annotation.ControllerLog; +import com.xuxd.kafka.console.aspect.annotation.Permission; +import com.xuxd.kafka.console.beans.Credentials; +import com.xuxd.kafka.console.beans.dto.SysPermissionDTO; +import com.xuxd.kafka.console.beans.dto.SysRoleDTO; +import com.xuxd.kafka.console.beans.dto.SysUserDTO; +import com.xuxd.kafka.console.service.UserManageService; +import org.springframework.web.bind.annotation.*; + +import javax.servlet.http.HttpServletRequest; + +/** + * @author: xuxd + * @date: 2023/4/11 21:34 + **/ +@RestController +@RequestMapping("/sys/user/manage") +public class UserManageController { + + private final UserManageService userManageService; + + public UserManageController(UserManageService userManageService) { + this.userManageService = userManageService; + } + + @Permission({"user-manage:user:add", "user-manage:user:change-role", "user-manage:user:reset-pass"}) + @ControllerLog("新增/更新用户") + @PostMapping("/user") + public Object addOrUpdateUser(@RequestBody SysUserDTO userDTO) { + return userManageService.addOrUpdateUser(userDTO); + } + + @Permission("user-manage:role:save") + @ControllerLog("新增/更新角色") + @PostMapping("/role") + public Object addOrUpdateRole(@RequestBody SysRoleDTO roleDTO) { + return userManageService.addOrUdpateRole(roleDTO); + } + + @ControllerLog("新增权限") + @PostMapping("/permission") + public Object addPermission(@RequestBody SysPermissionDTO permissionDTO) { + return userManageService.addPermission(permissionDTO); + } + + @Permission("user-manage:role:save") + @ControllerLog("更新角色") + @PutMapping("/role") + public Object updateRole(@RequestBody SysRoleDTO roleDTO) { + return userManageService.updateRole(roleDTO); + } + + @Permission({"user-manage:role"}) + @GetMapping("/role") + public Object selectRole() { + return userManageService.selectRole(); + } + + @Permission({"user-manage:permission"}) + @GetMapping("/permission") + public Object selectPermission() { + return userManageService.selectPermission(); + } + + @Permission({"user-manage:user"}) + @GetMapping("/user") + public Object selectUser() { + return userManageService.selectUser(); + } + + @Permission("user-manage:role:del") + @ControllerLog("删除角色") + @DeleteMapping("/role") + public Object deleteRole(@RequestParam Long id) { + return userManageService.deleteRole(id); + } + + @Permission("user-manage:user:del") + @ControllerLog("删除用户") + @DeleteMapping("/user") + public Object deleteUser(@RequestParam Long id) { + return userManageService.deleteUser(id); + } + + @Permission("user-manage:setting") + @ControllerLog("更新密码") + @PostMapping("/user/password") + public Object updatePassword(@RequestBody SysUserDTO userDTO, HttpServletRequest request) { + Credentials credentials = (Credentials) request.getAttribute("credentials"); + if (credentials != null && !credentials.isInvalid()) { + userDTO.setUsername(credentials.getUsername()); + } + return userManageService.updatePassword(userDTO); + } +} diff --git a/src/main/java/com/xuxd/kafka/console/dao/SysPermissionMapper.java b/src/main/java/com/xuxd/kafka/console/dao/SysPermissionMapper.java new file mode 100644 index 0000000..23fd893 --- /dev/null +++ b/src/main/java/com/xuxd/kafka/console/dao/SysPermissionMapper.java @@ -0,0 +1,15 @@ +package com.xuxd.kafka.console.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.xuxd.kafka.console.beans.dos.SysPermissionDO; +import org.apache.ibatis.annotations.Mapper; + +/** + * 系统权限 . + * + * @author: xuxd + * @date: 2023/4/11 21:21 + **/ +@Mapper +public interface SysPermissionMapper extends BaseMapper { +} diff --git a/src/main/java/com/xuxd/kafka/console/dao/SysRoleMapper.java b/src/main/java/com/xuxd/kafka/console/dao/SysRoleMapper.java new file mode 100644 index 0000000..f54d998 --- /dev/null +++ b/src/main/java/com/xuxd/kafka/console/dao/SysRoleMapper.java @@ -0,0 +1,13 @@ +package com.xuxd.kafka.console.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.xuxd.kafka.console.beans.dos.SysRoleDO; +import org.apache.ibatis.annotations.Mapper; + +/** + * @author: xuxd + * @date: 2023/4/11 21:22 + **/ +@Mapper +public interface SysRoleMapper extends BaseMapper { +} diff --git a/src/main/java/com/xuxd/kafka/console/dao/SysUserMapper.java b/src/main/java/com/xuxd/kafka/console/dao/SysUserMapper.java new file mode 100644 index 0000000..73a0060 --- /dev/null +++ b/src/main/java/com/xuxd/kafka/console/dao/SysUserMapper.java @@ -0,0 +1,13 @@ +package com.xuxd.kafka.console.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.xuxd.kafka.console.beans.dos.SysUserDO; +import org.apache.ibatis.annotations.Mapper; + +/** + * @author: xuxd + * @date: 2023/4/11 21:22 + **/ +@Mapper +public interface SysUserMapper extends BaseMapper { +} diff --git a/src/main/java/com/xuxd/kafka/console/dao/init/DataInit.java b/src/main/java/com/xuxd/kafka/console/dao/init/DataInit.java new file mode 100644 index 0000000..71a9e0a --- /dev/null +++ b/src/main/java/com/xuxd/kafka/console/dao/init/DataInit.java @@ -0,0 +1,93 @@ +package com.xuxd.kafka.console.dao.init; + +import com.xuxd.kafka.console.beans.RolePermUpdateEvent; +import com.xuxd.kafka.console.config.AuthConfig; +import com.xuxd.kafka.console.dao.SysPermissionMapper; +import com.xuxd.kafka.console.dao.SysRoleMapper; +import com.xuxd.kafka.console.dao.SysUserMapper; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.SmartInitializingSingleton; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Component; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; + +/** + * @author: xuxd + * @date: 2023/5/17 13:10 + **/ +@Slf4j +@Component +public class DataInit implements SmartInitializingSingleton { + + private final AuthConfig authConfig; + + private final SysUserMapper userMapper; + + private final SysRoleMapper roleMapper; + + private final SysPermissionMapper permissionMapper; + + private final DataSource dataSource; + + private final SqlParse sqlParse; + + private final ApplicationEventPublisher publisher; + + + public DataInit(AuthConfig authConfig, + SysUserMapper userMapper, + SysRoleMapper roleMapper, + SysPermissionMapper permissionMapper, + DataSource dataSource, + ApplicationEventPublisher publisher) { + this.authConfig = authConfig; + this.userMapper = userMapper; + this.roleMapper = roleMapper; + this.permissionMapper = permissionMapper; + this.dataSource = dataSource; + this.publisher = publisher; + this.sqlParse = new SqlParse(); + } + + @Override + public void afterSingletonsInstantiated() { + if (!authConfig.isEnable()) { + log.info("Disable login authentication, no longer try to initialize the data"); + return; + } + try { + Connection connection = dataSource.getConnection(); + Integer userCount = userMapper.selectCount(null); + if (userCount == null || userCount == 0) { + initData(connection, SqlParse.USER_TABLE); + } + + Integer roleCount = roleMapper.selectCount(null); + if (roleCount == null || roleCount == 0) { + initData(connection, SqlParse.ROLE_TABLE); + } + + Integer permCount = permissionMapper.selectCount(null); + if (permCount == null || permCount == 0) { + initData(connection, SqlParse.PERM_TABLE); + } + RolePermUpdateEvent event = new RolePermUpdateEvent(this); + event.setReload(true); + publisher.publishEvent(event); + } catch (SQLException e) { + throw new RuntimeException(e); + } + + } + + private void initData(Connection connection, String table) throws SQLException { + log.info("Init default data for {}", table); + String sql = sqlParse.getMergeSql(table); + PreparedStatement statement = connection.prepareStatement(sql); + statement.execute(); + } +} diff --git a/src/main/java/com/xuxd/kafka/console/dao/init/SqlParse.java b/src/main/java/com/xuxd/kafka/console/dao/init/SqlParse.java new file mode 100644 index 0000000..1ce6975 --- /dev/null +++ b/src/main/java/com/xuxd/kafka/console/dao/init/SqlParse.java @@ -0,0 +1,85 @@ +package com.xuxd.kafka.console.dao.init; + +import com.google.common.io.Files; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.util.ResourceUtils; +import scala.collection.mutable.StringBuilder; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @author: xuxd + * @date: 2023/5/17 21:22 + **/ +@Slf4j +public class SqlParse { + + private final String FILE = "classpath:db/data-h2.sql"; + + private final Map> sqlMap = new HashMap<>(); + + public static final String ROLE_TABLE = "t_sys_role"; + public static final String USER_TABLE = "t_sys_user"; + public static final String PERM_TABLE = "t_sys_permission"; + + public SqlParse() { + sqlMap.put(ROLE_TABLE, new ArrayList<>()); + sqlMap.put(USER_TABLE, new ArrayList<>()); + sqlMap.put(PERM_TABLE, new ArrayList<>()); + + String table = null; + try { + File file = ResourceUtils.getFile(FILE); + List lines = Files.readLines(file, Charset.forName("UTF-8")); + for (String str : lines) { + if (StringUtils.isNotEmpty(str)) { + if (str.indexOf("start--") > 0) { + if (str.indexOf(ROLE_TABLE) > 0) { + table = ROLE_TABLE; + } + if (str.indexOf(USER_TABLE) > 0) { + table = USER_TABLE; + } + if (str.indexOf(PERM_TABLE) > 0) { + table = PERM_TABLE; + } + } + if (isSql(str)) { + if (table == null) { + log.error("Table is null, can not load sql: {}", str); + continue; + } + sqlMap.get(table).add(str); + } + } + } + } catch (FileNotFoundException e) { + throw new RuntimeException(e); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public List getSqlList(String table) { + return sqlMap.get(table); + } + + public String getMergeSql(String table) { + List list = getSqlList(table); + StringBuilder sb = new StringBuilder(); + list.forEach(sql -> sb.append(sql)); + return sb.toString(); + } + + private boolean isSql(String str) { + return StringUtils.isNotEmpty(str) && str.startsWith("insert"); + } +} diff --git a/src/main/java/com/xuxd/kafka/console/interceptor/GlobalExceptionHandler.java b/src/main/java/com/xuxd/kafka/console/exception/GlobalExceptionHandler.java similarity index 60% rename from src/main/java/com/xuxd/kafka/console/interceptor/GlobalExceptionHandler.java rename to src/main/java/com/xuxd/kafka/console/exception/GlobalExceptionHandler.java index 7715f45..9090ce2 100644 --- a/src/main/java/com/xuxd/kafka/console/interceptor/GlobalExceptionHandler.java +++ b/src/main/java/com/xuxd/kafka/console/exception/GlobalExceptionHandler.java @@ -1,11 +1,14 @@ -package com.xuxd.kafka.console.interceptor; +package com.xuxd.kafka.console.exception; import com.xuxd.kafka.console.beans.ResponseData; -import javax.servlet.http.HttpServletRequest; import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.ResponseStatus; + +import javax.servlet.http.HttpServletRequest; /** * kafka-console-ui. @@ -17,6 +20,14 @@ import org.springframework.web.bind.annotation.ResponseBody; @ControllerAdvice(basePackages = "com.xuxd.kafka.console.controller") public class GlobalExceptionHandler { + @ResponseStatus(code = HttpStatus.FORBIDDEN) + @ExceptionHandler(value = UnAuthorizedException.class) + @ResponseBody + public Object unAuthorizedExceptionHandler(HttpServletRequest req, Exception ex) throws Exception { + log.error("unAuthorized: {}", ex.getMessage()); + return ResponseData.create().failed("UnAuthorized: " + ex.getMessage()); + } + @ExceptionHandler(value = Exception.class) @ResponseBody public Object exceptionHandler(HttpServletRequest req, Exception ex) throws Exception { diff --git a/src/main/java/com/xuxd/kafka/console/exception/UnAuthorizedException.java b/src/main/java/com/xuxd/kafka/console/exception/UnAuthorizedException.java new file mode 100644 index 0000000..c29cd6d --- /dev/null +++ b/src/main/java/com/xuxd/kafka/console/exception/UnAuthorizedException.java @@ -0,0 +1,12 @@ +package com.xuxd.kafka.console.exception; + +/** + * @author: xuxd + * @date: 2023/5/17 23:08 + **/ +public class UnAuthorizedException extends RuntimeException{ + + public UnAuthorizedException(String message) { + super(message); + } +} diff --git a/src/main/java/com/xuxd/kafka/console/filter/AuthFilter.java b/src/main/java/com/xuxd/kafka/console/filter/AuthFilter.java new file mode 100644 index 0000000..6137eb3 --- /dev/null +++ b/src/main/java/com/xuxd/kafka/console/filter/AuthFilter.java @@ -0,0 +1,70 @@ +package com.xuxd.kafka.console.filter; + +import com.xuxd.kafka.console.beans.Credentials; +import com.xuxd.kafka.console.config.AuthConfig; +import com.xuxd.kafka.console.utils.AuthUtil; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.core.annotation.Order; +import org.springframework.http.HttpStatus; + +import javax.servlet.*; +import javax.servlet.annotation.WebFilter; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * @author: xuxd + * @date: 2023/5/9 21:20 + **/ +@Order(1) +@WebFilter(filterName = "auth-filter", urlPatterns = {"/*"}) +@Slf4j +public class AuthFilter implements Filter { + + private final AuthConfig authConfig; + + private final String TOKEN_HEADER = "X-Auth-Token"; + + private final String AUTH_URI_PREFIX = "/auth"; + + public AuthFilter(AuthConfig authConfig) { + this.authConfig = authConfig; + } + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { + if (!authConfig.isEnable()) { + filterChain.doFilter(servletRequest, servletResponse); + return; + } + HttpServletRequest request = (HttpServletRequest) servletRequest; + HttpServletResponse response = (HttpServletResponse) servletResponse; + String accessToken = request.getHeader(TOKEN_HEADER); + + String requestURI = request.getRequestURI(); + if (requestURI.startsWith(AUTH_URI_PREFIX)) { + filterChain.doFilter(servletRequest, servletResponse); + return; + } + if (StringUtils.isEmpty(accessToken)) { + response.setStatus(HttpStatus.UNAUTHORIZED.value()); + return; + } + + Credentials credentials = AuthUtil.parseToken(authConfig.getSecret(), accessToken); + if (credentials.isInvalid()) { + response.setStatus(HttpStatus.UNAUTHORIZED.value()); + return; + } + request.setAttribute("credentials", credentials); + + try { + CredentialsContext.set(credentials); + filterChain.doFilter(servletRequest, servletResponse); + }finally { + CredentialsContext.remove(); + } + } +} diff --git a/src/main/java/com/xuxd/kafka/console/interceptor/ContextSetFilter.java b/src/main/java/com/xuxd/kafka/console/filter/ContextSetFilter.java similarity index 97% rename from src/main/java/com/xuxd/kafka/console/interceptor/ContextSetFilter.java rename to src/main/java/com/xuxd/kafka/console/filter/ContextSetFilter.java index cd5acf9..a43fc1d 100644 --- a/src/main/java/com/xuxd/kafka/console/interceptor/ContextSetFilter.java +++ b/src/main/java/com/xuxd/kafka/console/filter/ContextSetFilter.java @@ -1,4 +1,4 @@ -package com.xuxd.kafka.console.interceptor; +package com.xuxd.kafka.console.filter; import com.xuxd.kafka.console.beans.ResponseData; import com.xuxd.kafka.console.beans.dos.ClusterInfoDO; @@ -9,6 +9,7 @@ import com.xuxd.kafka.console.utils.ConvertUtil; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.annotation.Order; import org.springframework.http.MediaType; import javax.servlet.*; @@ -24,6 +25,7 @@ import java.util.Set; * @author xuxd * @date 2022-01-05 19:56:25 **/ +@Order(100) @WebFilter(filterName = "context-set-filter", urlPatterns = {"/acl/*", "/user/*", "/cluster/*", "/config/*", "/consumer/*", "/message/*", "/topic/*", "/op/*", "/client/*"}) @Slf4j public class ContextSetFilter implements Filter { diff --git a/src/main/java/com/xuxd/kafka/console/filter/CredentialsContext.java b/src/main/java/com/xuxd/kafka/console/filter/CredentialsContext.java new file mode 100644 index 0000000..4ec9a6c --- /dev/null +++ b/src/main/java/com/xuxd/kafka/console/filter/CredentialsContext.java @@ -0,0 +1,23 @@ +package com.xuxd.kafka.console.filter; + +import com.xuxd.kafka.console.beans.Credentials; + +/** + * @author: xuxd + * @date: 2023/5/17 23:02 + **/ +public class CredentialsContext { + private static final ThreadLocal CREDENTIALS = new ThreadLocal<>(); + + public static void set(Credentials credentials) { + CREDENTIALS.set(credentials); + } + + public static Credentials get() { + return CREDENTIALS.get(); + } + + public static void remove() { + CREDENTIALS.remove(); + } +} diff --git a/src/main/java/com/xuxd/kafka/console/service/AuthService.java b/src/main/java/com/xuxd/kafka/console/service/AuthService.java new file mode 100644 index 0000000..c437dc1 --- /dev/null +++ b/src/main/java/com/xuxd/kafka/console/service/AuthService.java @@ -0,0 +1,13 @@ +package com.xuxd.kafka.console.service; + +import com.xuxd.kafka.console.beans.ResponseData; +import com.xuxd.kafka.console.beans.dto.LoginUserDTO; + +/** + * @author: xuxd + * @date: 2023/5/14 19:00 + **/ +public interface AuthService { + + ResponseData login(LoginUserDTO userDTO); +} diff --git a/src/main/java/com/xuxd/kafka/console/service/UserManageService.java b/src/main/java/com/xuxd/kafka/console/service/UserManageService.java new file mode 100644 index 0000000..b999662 --- /dev/null +++ b/src/main/java/com/xuxd/kafka/console/service/UserManageService.java @@ -0,0 +1,40 @@ +package com.xuxd.kafka.console.service; + +import com.xuxd.kafka.console.beans.ResponseData; +import com.xuxd.kafka.console.beans.dto.SysPermissionDTO; +import com.xuxd.kafka.console.beans.dto.SysRoleDTO; +import com.xuxd.kafka.console.beans.dto.SysUserDTO; + +/** + * 登录用户权限管理. + * + * @author: xuxd + * @date: 2023/4/11 21:24 + **/ +public interface UserManageService { + + /** + * 增加权限 + */ + ResponseData addPermission(SysPermissionDTO permissionDTO); + + ResponseData addOrUdpateRole(SysRoleDTO roleDTO); + + ResponseData addOrUpdateUser(SysUserDTO userDTO); + + ResponseData selectRole(); + + ResponseData selectPermission(); + + ResponseData selectUser(); + + ResponseData updateUser(SysUserDTO userDTO); + + ResponseData updateRole(SysRoleDTO roleDTO); + + ResponseData deleteRole(Long id); + + ResponseData deleteUser(Long id); + + ResponseData updatePassword(SysUserDTO userDTO); +} diff --git a/src/main/java/com/xuxd/kafka/console/service/impl/AuthServiceImpl.java b/src/main/java/com/xuxd/kafka/console/service/impl/AuthServiceImpl.java new file mode 100644 index 0000000..5baafe4 --- /dev/null +++ b/src/main/java/com/xuxd/kafka/console/service/impl/AuthServiceImpl.java @@ -0,0 +1,96 @@ +package com.xuxd.kafka.console.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.xuxd.kafka.console.beans.Credentials; +import com.xuxd.kafka.console.beans.LoginResult; +import com.xuxd.kafka.console.beans.ResponseData; +import com.xuxd.kafka.console.beans.dos.SysRoleDO; +import com.xuxd.kafka.console.beans.dos.SysUserDO; +import com.xuxd.kafka.console.beans.dto.LoginUserDTO; +import com.xuxd.kafka.console.cache.RolePermCache; +import com.xuxd.kafka.console.config.AuthConfig; +import com.xuxd.kafka.console.dao.SysRoleMapper; +import com.xuxd.kafka.console.dao.SysUserMapper; +import com.xuxd.kafka.console.service.AuthService; +import com.xuxd.kafka.console.utils.AuthUtil; +import com.xuxd.kafka.console.utils.UUIDStrUtil; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +/** + * @author: xuxd + * @date: 2023/5/14 19:01 + **/ +@Slf4j +@Service +public class AuthServiceImpl implements AuthService { + + private final SysUserMapper userMapper; + + private final SysRoleMapper roleMapper; + + private final AuthConfig authConfig; + + private final RolePermCache rolePermCache; + + public AuthServiceImpl(SysUserMapper userMapper, + SysRoleMapper roleMapper, + AuthConfig authConfig, + RolePermCache rolePermCache) { + this.userMapper = userMapper; + this.roleMapper = roleMapper; + this.authConfig = authConfig; + this.rolePermCache = rolePermCache; + } + + @Override + public ResponseData login(LoginUserDTO userDTO) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("username", userDTO.getUsername()); + SysUserDO userDO = userMapper.selectOne(queryWrapper); + if (userDO == null) { + return ResponseData.create().failed("用户名/密码不正确"); + } + String encrypt = UUIDStrUtil.generate(userDTO.getPassword(), userDO.getSalt()); + if (!userDO.getPassword().equals(encrypt)) { + return ResponseData.create().failed("用户名/密码不正确"); + } + Credentials credentials = new Credentials(); + credentials.setUsername(userDO.getUsername()); + credentials.setExpiration(System.currentTimeMillis() + authConfig.getExpireHours() * 3600 * 1000); + String token = AuthUtil.generateToken(authConfig.getSecret(), credentials); + LoginResult loginResult = new LoginResult(); + List permissions = new ArrayList<>(); + String roleIds = userDO.getRoleIds(); + if (StringUtils.isNotEmpty(roleIds)) { + List roleIdList = Arrays.stream(roleIds.split(",")).map(String::trim).filter(StringUtils::isNotEmpty).collect(Collectors.toList()); + roleIdList.forEach(roleId -> { + Long rId = Long.valueOf(roleId); + SysRoleDO roleDO = roleMapper.selectById(rId); + String permissionIds = roleDO.getPermissionIds(); + if (StringUtils.isNotEmpty(permissionIds)) { + List permIds = Arrays.stream(permissionIds.split(",")).map(String::trim). + filter(StringUtils::isNotEmpty).map(Long::valueOf).collect(Collectors.toList()); + permIds.forEach(id -> { + String permission = rolePermCache.getPermCache().get(id).getPermission(); + if (StringUtils.isNotEmpty(permission)) { + permissions.add(permission); + } else { + log.error("角色:{},权限id: {},不存在", roleId, id); + } + }); + } + }); + } + loginResult.setToken(token); + loginResult.setPermissions(permissions); + return ResponseData.create().data(loginResult).success(); + } + +} diff --git a/src/main/java/com/xuxd/kafka/console/service/impl/UserManageServiceImpl.java b/src/main/java/com/xuxd/kafka/console/service/impl/UserManageServiceImpl.java new file mode 100644 index 0000000..8c6f483 --- /dev/null +++ b/src/main/java/com/xuxd/kafka/console/service/impl/UserManageServiceImpl.java @@ -0,0 +1,230 @@ +package com.xuxd.kafka.console.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.xuxd.kafka.console.beans.ResponseData; +import com.xuxd.kafka.console.beans.RolePermUpdateEvent; +import com.xuxd.kafka.console.beans.dos.SysPermissionDO; +import com.xuxd.kafka.console.beans.dos.SysRoleDO; +import com.xuxd.kafka.console.beans.dos.SysUserDO; +import com.xuxd.kafka.console.beans.dto.SysPermissionDTO; +import com.xuxd.kafka.console.beans.dto.SysRoleDTO; +import com.xuxd.kafka.console.beans.dto.SysUserDTO; +import com.xuxd.kafka.console.beans.vo.SysPermissionVO; +import com.xuxd.kafka.console.beans.vo.SysRoleVO; +import com.xuxd.kafka.console.beans.vo.SysUserVO; +import com.xuxd.kafka.console.dao.SysPermissionMapper; +import com.xuxd.kafka.console.dao.SysRoleMapper; +import com.xuxd.kafka.console.dao.SysUserMapper; +import com.xuxd.kafka.console.service.UserManageService; +import com.xuxd.kafka.console.utils.RandomStringUtil; +import com.xuxd.kafka.console.utils.UUIDStrUtil; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Service; + +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * @author: xuxd + * @date: 2023/4/11 21:24 + **/ +@Slf4j +@Service +public class UserManageServiceImpl implements UserManageService { + + private final SysUserMapper userMapper; + + private final SysRoleMapper roleMapper; + + private final SysPermissionMapper permissionMapper; + + private final ApplicationEventPublisher publisher; + + public UserManageServiceImpl(ObjectProvider userMapper, + ObjectProvider roleMapper, + ObjectProvider permissionMapper, + ApplicationEventPublisher publisher) { + this.userMapper = userMapper.getIfAvailable(); + this.roleMapper = roleMapper.getIfAvailable(); + this.permissionMapper = permissionMapper.getIfAvailable(); + this.publisher = publisher; + } + + @Override + public ResponseData addPermission(SysPermissionDTO permissionDTO) { + permissionMapper.insert(permissionDTO.toSysPermissionDO()); + return ResponseData.create().success(); + } + + @Override + public ResponseData addOrUdpateRole(SysRoleDTO roleDTO) { + SysRoleDO roleDO = roleDTO.toDO(); + if (roleDO.getId() == null) { + roleMapper.insert(roleDO); + } else { + roleMapper.updateById(roleDO); + } + publisher.publishEvent(new RolePermUpdateEvent(this)); + return ResponseData.create().success(); + } + + @Override + public ResponseData addOrUpdateUser(SysUserDTO userDTO) { + + if (userDTO.getId() == null) { + if (StringUtils.isEmpty(userDTO.getPassword())) { + userDTO.setPassword(RandomStringUtil.random6Str()); + } + SysUserDO userDO = userDTO.toDO(); + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq(true, "username", userDO.getUsername()); + SysUserDO exist = userMapper.selectOne(queryWrapper); + if (exist != null) { + return ResponseData.create().failed("用户已存在:" + userDO.getUsername()); + } + userDO.setSalt(UUIDStrUtil.random()); + userDO.setPassword(UUIDStrUtil.generate(userDTO.getPassword(), userDO.getSalt())); + userMapper.insert(userDO); + } else { + SysUserDO userDO = userMapper.selectById(userDTO.getId()); + if (userDO == null) { + log.error("查不到用户: {}", userDTO.getId()); + return ResponseData.create().failed("Unknown User."); + } + // 判断是否更新密码 + if (userDTO.getResetPassword()) { + userDTO.setPassword(RandomStringUtil.random6Str()); + userDO.setSalt(UUIDStrUtil.random()); + userDO.setPassword(UUIDStrUtil.generate(userDTO.getPassword(), userDO.getSalt())); + } + userDO.setRoleIds(userDTO.getRoleIds()); + userDO.setUsername(userDTO.getUsername()); + userMapper.updateById(userDO); + } + return ResponseData.create().data(userDTO.getPassword()).success(); + } + + @Override + public ResponseData selectRole() { + List dos = roleMapper.selectList(new QueryWrapper<>()); + return ResponseData.create().data(dos.stream().map(SysRoleVO::from).collect(Collectors.toList())).success(); + } + + @Override + public ResponseData selectPermission() { + QueryWrapper queryWrapper = new QueryWrapper<>(); + + List permissionDOS = permissionMapper.selectList(queryWrapper); + List vos = new ArrayList<>(); + Map posMap = new HashMap<>(); + Map voMap = new HashMap<>(); + + Iterator iterator = permissionDOS.iterator(); + while (iterator.hasNext()) { + SysPermissionDO permissionDO = iterator.next(); + if (permissionDO.getParentId() == null) { + // 菜单 + SysPermissionVO vo = SysPermissionVO.from(permissionDO); + vos.add(vo); + int index = vos.size() - 1; + // 记录位置 + posMap.put(permissionDO.getId(), index); + iterator.remove(); + } + } + // 上面把菜单都处理过了 + while (!permissionDOS.isEmpty()) { + iterator = permissionDOS.iterator(); + while (iterator.hasNext()) { + SysPermissionDO permissionDO = iterator.next(); + Long parentId = permissionDO.getParentId(); + if (posMap.containsKey(parentId)) { + // 菜单下的按扭 + SysPermissionVO vo = SysPermissionVO.from(permissionDO); + Integer index = posMap.get(parentId); + SysPermissionVO menuVO = vos.get(index); + if (menuVO.getChildren() == null) { + menuVO.setChildren(new ArrayList<>()); + } + menuVO.getChildren().add(vo); + voMap.put(permissionDO.getId(), vo); + iterator.remove(); + } else if (voMap.containsKey(parentId)) { + // 按钮下的按扭 + SysPermissionVO vo = SysPermissionVO.from(permissionDO); + SysPermissionVO buttonVO = voMap.get(parentId); + if (buttonVO.getChildren() == null) { + buttonVO.setChildren(new ArrayList<>()); + } + buttonVO.getChildren().add(vo); + voMap.put(permissionDO.getId(), vo); + iterator.remove(); + } + } + } + return ResponseData.create().data(vos).success(); + } + + @Override + public ResponseData selectUser() { + QueryWrapper queryWrapper = new QueryWrapper<>(); + List userDOS = userMapper.selectList(queryWrapper); + List roleDOS = roleMapper.selectList(null); + Map roleDOMap = roleDOS.stream().collect(Collectors.toMap(SysRoleDO::getId, Function.identity(), (e1, e2) -> e1)); + List voList = userDOS.stream().map(SysUserVO::from).collect(Collectors.toList()); + voList.forEach(vo -> { + if (vo.getRoleIds() != null) { + Long roleId = Long.valueOf(vo.getRoleIds()); + vo.setRoleNames(roleDOMap.containsKey(roleId) ? roleDOMap.get(roleId).getRoleName() : null); + } + }); + return ResponseData.create().data(voList).success(); + } + + @Override + public ResponseData updateUser(SysUserDTO userDTO) { + userMapper.updateById(userDTO.toDO()); + return ResponseData.create().success(); + } + + @Override + public ResponseData updateRole(SysRoleDTO roleDTO) { + roleMapper.updateById(roleDTO.toDO()); + publisher.publishEvent(new RolePermUpdateEvent(this)); + return ResponseData.create().success(); + } + + @Override + public ResponseData deleteRole(Long id) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq(true, "role_ids", id); + Integer count = userMapper.selectCount(queryWrapper); + if (count > 0) { + return ResponseData.create().failed("存在用户被分配为当前角色,不允许删除"); + } + roleMapper.deleteById(id); + publisher.publishEvent(new RolePermUpdateEvent(this)); + return ResponseData.create().success(); + } + + @Override + public ResponseData deleteUser(Long id) { + userMapper.deleteById(id); + return ResponseData.create().success(); + } + + @Override + public ResponseData updatePassword(SysUserDTO userDTO) { + SysUserDO userDO = userDTO.toDO(); + userDO.setSalt(UUIDStrUtil.random()); + userDO.setPassword(UUIDStrUtil.generate(userDTO.getPassword(), userDO.getSalt())); + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.eq("username", userDTO.getUsername()); + userMapper.update(userDO, wrapper); + return ResponseData.create().success(); + } +} diff --git a/src/main/java/com/xuxd/kafka/console/utils/AuthUtil.java b/src/main/java/com/xuxd/kafka/console/utils/AuthUtil.java new file mode 100644 index 0000000..dd5d745 --- /dev/null +++ b/src/main/java/com/xuxd/kafka/console/utils/AuthUtil.java @@ -0,0 +1,54 @@ +package com.xuxd.kafka.console.utils; + +import com.google.gson.Gson; +import com.xuxd.kafka.console.beans.Credentials; +import lombok.extern.slf4j.Slf4j; +import org.springframework.util.Base64Utils; + +import java.nio.charset.StandardCharsets; + +/** + * @author: xuxd + * @date: 2023/5/14 19:34 + **/ +@Slf4j +public class AuthUtil { + + private static Gson gson = GsonUtil.INSTANCE.get(); + + public static String generateToken(String secret, Credentials info) { + String json = gson.toJson(info); + String str = json + secret; + String signature = MD5Util.md5(str); + return Base64Utils.encodeToString(json.getBytes(StandardCharsets.UTF_8)) + "." + + Base64Utils.encodeToString(signature.getBytes(StandardCharsets.UTF_8)); + } + + public static boolean isToken(String token) { + return token.split("\\.").length == 2; + } + + public static Credentials parseToken(String secret, String token) { + if (!isToken(token)) { + return Credentials.INVALID; + } + String[] arr = token.split("\\."); + String infoStr = new String(Base64Utils.decodeFromString(arr[0]), StandardCharsets.UTF_8); + String signature = new String(Base64Utils.decodeFromString(arr[1]), StandardCharsets.UTF_8); + + String encrypt = MD5Util.md5(infoStr + secret); + if (!encrypt.equals(signature)) { + return Credentials.INVALID; + } + try { + Credentials credentials = gson.fromJson(infoStr, Credentials.class); + if (credentials.getExpiration() < System.currentTimeMillis()) { + return Credentials.INVALID; + } + return credentials; + } catch (Exception e) { + log.error("解析token失败: {}", token, e); + return Credentials.INVALID; + } + } +} diff --git a/src/main/java/com/xuxd/kafka/console/utils/MD5Util.java b/src/main/java/com/xuxd/kafka/console/utils/MD5Util.java new file mode 100644 index 0000000..801699f --- /dev/null +++ b/src/main/java/com/xuxd/kafka/console/utils/MD5Util.java @@ -0,0 +1,32 @@ +package com.xuxd.kafka.console.utils; + +import lombok.extern.slf4j.Slf4j; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +/** + * @author: xuxd + * @date: 2023/5/14 20:25 + **/ +@Slf4j +public class MD5Util { + + public static MessageDigest getInstance() { + try { + MessageDigest md5 = MessageDigest.getInstance("MD5"); + return md5; + } catch (NoSuchAlgorithmException e) { + return null; + } + } + + public static String md5(String str) { + MessageDigest digest = getInstance(); + if (digest == null) { + return null; + } + return new String(digest.digest(str.getBytes(StandardCharsets.UTF_8)), StandardCharsets.UTF_8); + } +} diff --git a/src/main/java/com/xuxd/kafka/console/utils/RandomStringUtil.java b/src/main/java/com/xuxd/kafka/console/utils/RandomStringUtil.java new file mode 100644 index 0000000..4280f8c --- /dev/null +++ b/src/main/java/com/xuxd/kafka/console/utils/RandomStringUtil.java @@ -0,0 +1,26 @@ +package com.xuxd.kafka.console.utils; + +import java.util.Random; + +/** + * @author: xuxd + * @date: 2023/5/8 9:19 + **/ +public class RandomStringUtil { + + private final static String ALLOWED_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + + public static String random6Str() { + return generateRandomString(6); + } + + public static String generateRandomString(int length) { + Random random = new Random(); + StringBuilder sb = new StringBuilder(length); + for (int i = 0; i < length; i++) { + int index = random.nextInt(ALLOWED_CHARS.length()); + sb.append(ALLOWED_CHARS.charAt(index)); + } + return sb.toString(); + } +} diff --git a/src/main/java/com/xuxd/kafka/console/utils/UUIDStrUtil.java b/src/main/java/com/xuxd/kafka/console/utils/UUIDStrUtil.java new file mode 100644 index 0000000..d07cdbb --- /dev/null +++ b/src/main/java/com/xuxd/kafka/console/utils/UUIDStrUtil.java @@ -0,0 +1,22 @@ +package com.xuxd.kafka.console.utils; + +import java.util.UUID; + +/** + * @author: xuxd + * @date: 2023/5/6 13:30 + **/ +public class UUIDStrUtil { + + public static String random() { + return UUID.randomUUID().toString(); + } + + public static String generate(String ... strs) { + StringBuilder sb = new StringBuilder(); + for (String str : strs) { + sb.append(str); + } + return UUID.nameUUIDFromBytes(sb.toString().getBytes()).toString(); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 8400628..304feee 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -47,3 +47,9 @@ logging: cron: # clear-dirty-user: 0 * * * * ? clear-dirty-user: 0 0 1 * * ? + +# 权限认证设置,设置为true,需要先登录才能访问 +auth: + enable: false + # 登录用户token的过期时间,单位:小时 + expire-hours: 24 \ No newline at end of file diff --git a/src/main/resources/db/data-h2.sql b/src/main/resources/db/data-h2.sql index a9a1360..0cb8c1f 100644 --- a/src/main/resources/db/data-h2.sql +++ b/src/main/resources/db/data-h2.sql @@ -1,5 +1,109 @@ --- DELETE FROM t_kafka_user; --- --- INSERT INTO t_kafka_user (id, username, password) VALUES --- (1, 'Jone', 'p1'), --- (2, 'Jack', 'p2'); \ No newline at end of file +-- 不要随便修改下面的注释,要根据这个注释初始化加载数据 +-- t_sys_permission start-- +insert into t_sys_permission(id, name,type,parent_id,permission) values(0,'主页',0,null,'home'); + +insert into t_sys_permission(id, name,type,parent_id,permission) values(11,'集群',0,null,'cluster'); +insert into t_sys_permission(id, name,type,parent_id,permission) values(12,'属性配置',1,11,'cluster:property-config'); +insert into t_sys_permission(id, name,type,parent_id,permission) values(13,'日志配置',1,11,'cluster:log-config'); +insert into t_sys_permission(id, name,type,parent_id,permission) values(14,'编辑配置',1,11,'cluster:edit'); + +insert into t_sys_permission(id, name,type,parent_id,permission) values(21,'Topic',0,null,'topic'); +insert into t_sys_permission(id, name,type,parent_id,permission) values(22,'刷新',1,21,'topic:load'); +insert into t_sys_permission(id, name,type,parent_id,permission) values(23,'新增',1,21,'topic:add'); +insert into t_sys_permission(id, name,type,parent_id,permission) values(24,'批量删除',1,21,'topic:batch-del'); +insert into t_sys_permission(id, name,type,parent_id,permission) values(25,'删除',1,21,'topic:del'); +insert into t_sys_permission(id, name,type,parent_id,permission) values(26,'分区详情',1,21,'topic:partition-detail'); +insert into t_sys_permission(id, name,type,parent_id,permission) values(27,'首选副本作leader',1,26,'topic:partition-detail:preferred'); +insert into t_sys_permission(id, name,type,parent_id,permission) values(28,'增加分区',1,21,'topic:partition-add'); +insert into t_sys_permission(id, name,type,parent_id,permission) values(29,'消费详情',1,21,'topic:consumer-detail'); +insert into t_sys_permission(id, name,type,parent_id,permission) values(30,'属性配置',1,21,'topic:property-config'); +insert into t_sys_permission(id, name,type,parent_id,permission) values(31,'变更副本',1,21,'topic:replication-modify'); +insert into t_sys_permission(id, name,type,parent_id,permission) values(32,'发送统计',1,21,'topic:send-count'); +insert into t_sys_permission(id, name,type,parent_id,permission) values(33,'限流',1,21,'topic:replication-sync-throttle'); +insert into t_sys_permission(id, name,type,parent_id,permission) values(34,'编辑属性配置',1,30,'topic:property-config:edit'); +insert into t_sys_permission(id, name,type,parent_id,permission) values(35,'删除属性配置',1,30,'topic:property-config:del'); + +insert into t_sys_permission(id, name,type,parent_id,permission) values(41,'消费组',0,null,'group'); +insert into t_sys_permission(id, name,type,parent_id,permission) values(42,'新增订阅',1,41,'group:add'); +insert into t_sys_permission(id, name,type,parent_id,permission) values(43,'删除',1,41,'group:del'); +insert into t_sys_permission(id, name,type,parent_id,permission) values(44,'消费端',1,41,'group:client'); +insert into t_sys_permission(id, name,type,parent_id,permission) values(45,'消费详情',1,41,'group:consumer-detail'); +insert into t_sys_permission(id, name,type,parent_id,permission) values(46,'最小位点',1,45,'group:consumer-detail:min'); +insert into t_sys_permission(id, name,type,parent_id,permission) values(47,'最新位点',1,45,'group:consumer-detail:last'); +insert into t_sys_permission(id, name,type,parent_id,permission) values(48,'时间戳',1,45,'group:consumer-detail:timestamp'); +insert into t_sys_permission(id, name,type,parent_id,permission) values(49,'重置位点',1,45,'group:consumer-detail:any'); +insert into t_sys_permission(id, name,type,parent_id,permission) values(50,'位移分区',1,41,'group:offset-partition'); + + +insert into t_sys_permission(id, name,type,parent_id,permission) values(61,'消息',0,null,'message'); +insert into t_sys_permission(id, name,type,parent_id,permission) values(62,'根据时间查询',1,61,'message:search-time'); +insert into t_sys_permission(id, name,type,parent_id,permission) values(63,'根据偏移查询',1,61,'message:search-offset'); +insert into t_sys_permission(id, name,type,parent_id,permission) values(64,'在线发送',1,61,'message:send'); +insert into t_sys_permission(id, name,type,parent_id,permission) values(65,'在线删除',1,61,'message:del'); +insert into t_sys_permission(id, name,type,parent_id,permission) values(66,'消息详情',1,61,'message:detail'); +insert into t_sys_permission(id, name,type,parent_id,permission) values(67,'重新发送',1,61,'message:resend'); + +insert into t_sys_permission(id, name,type,parent_id,permission) values(80,'限流',0,null,'quota'); +insert into t_sys_permission(id, name,type,parent_id,permission) values(81,'用户',1,80,'quota:user'); +insert into t_sys_permission(id, name,type,parent_id,permission) values(82,'新增配置',1,81,'quota:user:add'); +insert into t_sys_permission(id, name,type,parent_id,permission) values(83,'客户端ID',1,80,'quota:client'); +insert into t_sys_permission(id, name,type,parent_id,permission) values(84,'新增配置',1,83,'quota:client:add'); +insert into t_sys_permission(id, name,type,parent_id,permission) values(85,'用户和客户端ID',1,80,'quota:user-client'); +insert into t_sys_permission(id, name,type,parent_id,permission) values(86,'新增配置',1,85,'quota:user-client:add'); +insert into t_sys_permission(id, name,type,parent_id,permission) values(87,'删除',1,80,'quota:del'); +insert into t_sys_permission(id, name,type,parent_id,permission) values(88,'修改',1,80,'quota:edit'); + + +insert into t_sys_permission(id, name,type,parent_id,permission) values(100,'Acl',0,null,'acl'); +insert into t_sys_permission(id, name,type,parent_id,permission) values(101,'资源授权',1,100,'acl:authority'); +insert into t_sys_permission(id, name,type,parent_id,permission) values(102,'新增主体权限',1,101,'acl:authority:add-principal'); +insert into t_sys_permission(id, name,type,parent_id,permission) values(103,'权限详情',1,101,'acl:authority:detail'); +insert into t_sys_permission(id, name,type,parent_id,permission) values(104,'管理生产权限',1,101,'acl:authority:producer'); +insert into t_sys_permission(id, name,type,parent_id,permission) values(105,'管理消费权限',1,101,'acl:authority:consumer'); +insert into t_sys_permission(id, name,type,parent_id,permission) values(106,'增加权限',1,101,'acl:authority:add'); +insert into t_sys_permission(id, name,type,parent_id,permission) values(107,'清除权限',1,101,'acl:authority:clean'); +insert into t_sys_permission(id, name,type,parent_id,permission) values(108,'SaslScram用户管理',1,100,'acl:sasl-scram'); +insert into t_sys_permission(id, name,type,parent_id,permission) values(109,'新增/更新用户',1,108,'acl:sasl-scram:add-update'); +insert into t_sys_permission(id, name,type,parent_id,permission) values(110,'详情',1,108,'acl:sasl-scram:detail'); +insert into t_sys_permission(id, name,type,parent_id,permission) values(111,'删除',1,108,'acl:sasl-scram:del'); +insert into t_sys_permission(id, name,type,parent_id,permission) values(112,'管理生产权限',1,108,'acl:sasl-scram:producer'); +insert into t_sys_permission(id, name,type,parent_id,permission) values(113,'管理消费权限',1,108,'acl:sasl-scram:consumer'); +insert into t_sys_permission(id, name,type,parent_id,permission) values(114,'增加权限',1,108,'acl:sasl-scram:add-auth'); +insert into t_sys_permission(id, name,type,parent_id,permission) values(115,'彻底删除',1,108,'acl:sasl-scram:pure'); + +insert into t_sys_permission(id, name,type,parent_id,permission) values(140,'用户',0,null,'user-manage'); +insert into t_sys_permission(id, name,type,parent_id,permission) values(141,'用户列表',1,140,'user-manage:user'); +insert into t_sys_permission(id, name,type,parent_id,permission) values(142,'新增',1,141,'user-manage:user:add'); +insert into t_sys_permission(id, name,type,parent_id,permission) values(143,'删除',1,141,'user-manage:user:del'); +insert into t_sys_permission(id, name,type,parent_id,permission) values(144,'重置密码',1,141,'user-manage:user:reset-pass'); +insert into t_sys_permission(id, name,type,parent_id,permission) values(145,'分配角色',1,141,'user-manage:user:change-role'); +insert into t_sys_permission(id, name,type,parent_id,permission) values(146,'角色列表',1,140,'user-manage:role'); +insert into t_sys_permission(id, name,type,parent_id,permission) values(147,'保存',1,146,'user-manage:role:save'); +insert into t_sys_permission(id, name,type,parent_id,permission) values(148,'删除',1,146,'user-manage:role:del'); +insert into t_sys_permission(id, name,type,parent_id,permission) values(149,'权限列表',1,140,'user-manage:permission'); +insert into t_sys_permission(id, name,type,parent_id,permission) values(150,'个人设置',1,140,'user-manage:setting'); + +insert into t_sys_permission(id, name,type,parent_id,permission) values(160,'运维',0,null,'op'); +insert into t_sys_permission(id, name,type,parent_id,permission) values(161,'集群切换',1,160,'op:cluster-switch'); +insert into t_sys_permission(id, name,type,parent_id,permission) values(162,'新增集群',1,161,'op:cluster-switch:add'); +insert into t_sys_permission(id, name,type,parent_id,permission) values(163,'切换',1,161,'op:cluster-switch:switch'); +insert into t_sys_permission(id, name,type,parent_id,permission) values(164,'编辑',1,161,'op:cluster-switch:edit'); +insert into t_sys_permission(id, name,type,parent_id,permission) values(165,'删除',1,161,'op:cluster-switch:del'); +insert into t_sys_permission(id, name,type,parent_id,permission) values(166,'配置限流',1,160,'op:config-throttle'); +insert into t_sys_permission(id, name,type,parent_id,permission) values(167,'解除限流',1,160,'op:remove-throttle'); +insert into t_sys_permission(id, name,type,parent_id,permission) values(168,'首选副本作leader',1,160,'op:replication-preferred'); +insert into t_sys_permission(id, name,type,parent_id,permission) values(169,'副本变更详情',1,160,'op:replication-update-detail'); +insert into t_sys_permission(id, name,type,parent_id,permission) values(170,'副本重分配',1,160,'op:replication-reassign'); +insert into t_sys_permission(id, name,type,parent_id,permission) values(171,'取消副本重分配',1,169,'op:replication-update-detail:cancel'); +-- t_sys_permission end-- + +-- t_sys_role start-- +insert into t_sys_role(id, role_name, description, permission_ids) VALUES (1,'超级管理员','超级管理员','12,13,14,22,23,24,25,26,27,28,29,30,34,35,31,32,33,42,43,44,45,46,47,48,49,50,62,63,64,65,66,67,81,82,83,84,85,86,87,88,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,141,142,143,144,145,146,147,148,149,150,161,162,163,164,165,166,167,168,169,171,170'); +insert into t_sys_role(id, role_name, description, permission_ids) VALUES (2,'普通管理员','普通管理员,不能更改用户信息','12,13,14,22,23,24,25,26,27,28,29,30,34,35,31,32,33,42,43,44,45,46,47,48,49,50,62,63,64,65,66,67,81,82,83,84,85,86,87,88,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,141,146,149,150,161,162,163,164,165,166,167,168,169,171,170'); +-- insert into t_sys_role(id, role_name, description, permission_ids) VALUES (2,'访客','访客','12,13,22,26,29,32,44,45,50,62,63,81,83,85,141,146,149,150,161,163'); +-- t_sys_role end-- + +-- t_sys_user start-- +insert into t_sys_user(id, username, password, salt, role_ids) VALUES (1,'super-admin','3a3e4d32-5247-321b-9efb-9cbf60b2bf6c','e6973cfc-7583-4baa-8802-65ded1268ab6','1' ); +insert into t_sys_user(id, username, password, salt, role_ids) VALUES (2,'admin','3a3e4d32-5247-321b-9efb-9cbf60b2bf6c','e6973cfc-7583-4baa-8802-65ded1268ab6','2' ); +-- t_sys_user end-- \ No newline at end of file diff --git a/src/main/resources/db/schema-h2.sql b/src/main/resources/db/schema-h2.sql index 355ffc1..305ec7d 100644 --- a/src/main/resources/db/schema-h2.sql +++ b/src/main/resources/db/schema-h2.sql @@ -34,4 +34,35 @@ CREATE TABLE IF NOT EXISTS T_CLUSTER_INFO UPDATE_TIME TIMESTAMP NOT NULL DEFAULT NOW() COMMENT '更新时间', PRIMARY KEY (ID), UNIQUE (CLUSTER_NAME) +); + +-- 登录用户的角色权限配置 +CREATE TABLE IF NOT EXISTS t_sys_permission +( + ID IDENTITY NOT NULL COMMENT '主键ID', + name varchar(100) DEFAULT NULL COMMENT '权限名称', + type tinyint(1) NOT NULL DEFAULT 0 COMMENT '权限类型: 0:菜单,1:按钮', + parent_id bigint(20) DEFAULT NULL COMMENT '所属父权限ID', + permission varchar(100) DEFAULT NULL COMMENT '权限字符串', + PRIMARY KEY (id) +); + +CREATE TABLE IF NOT EXISTS t_sys_role +( + ID IDENTITY NOT NULL COMMENT '主键ID', + role_name varchar(100) NOT NULL COMMENT '角色名称', + description varchar(100) DEFAULT NULL COMMENT '角色描述', + permission_ids varchar(500) DEFAULT NULL COMMENT '分配的权限ID', + PRIMARY KEY (id) +); + +CREATE TABLE IF NOT EXISTS t_sys_user +( + ID IDENTITY NOT NULL COMMENT '主键ID', + username varchar(100) DEFAULT NULL COMMENT '用户名', + password varchar(100) DEFAULT NULL COMMENT '用户密码', + salt varchar(100) DEFAULT NULL COMMENT '加密的盐值', + role_ids varchar(100) DEFAULT NULL COMMENT '分配角色的ID', + PRIMARY KEY (id), + UNIQUE (username) ); \ No newline at end of file diff --git a/src/main/resources/logback-test.xml b/src/main/resources/logback-test.xml index cf8f74a..7783f53 100644 --- a/src/main/resources/logback-test.xml +++ b/src/main/resources/logback-test.xml @@ -3,9 +3,9 @@ - + - + diff --git a/ui/src/App.vue b/ui/src/App.vue index db6fde5..8253c21 100644 --- a/ui/src/App.vue +++ b/ui/src/App.vue @@ -15,20 +15,46 @@ >限流 |Acl + |用户 |运维 - 集群:{{ clusterName }} +
+ 集群:{{ clusterName }} + + + | + {{ username }} + + + + + + 退出 + + + + +
diff --git a/ui/src/components/HelloWorld.vue b/ui/src/components/HelloWorld.vue deleted file mode 100644 index 0a5f13a..0000000 --- a/ui/src/components/HelloWorld.vue +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - diff --git a/ui/src/components/MessageBox.vue b/ui/src/components/MessageBox.vue new file mode 100644 index 0000000..540ed99 --- /dev/null +++ b/ui/src/components/MessageBox.vue @@ -0,0 +1,40 @@ + + + + + diff --git a/ui/src/directives/action.js b/ui/src/directives/action.js new file mode 100644 index 0000000..291ee78 --- /dev/null +++ b/ui/src/directives/action.js @@ -0,0 +1,16 @@ +import Vue from "vue"; +import Store from "@/store"; + +const action = Vue.directive("action", { + inserted: function (el, binding) { + const actionName = binding.arg; + const enableAuth = Store.state.auth.enable; + const permissions = Store.state.auth.permissions; + if (enableAuth && (!permissions || permissions.indexOf(actionName) < 0)) { + (el.parentNode && el.parentNode.removeChild(el)) || + (el.style.display = "none"); + } + }, +}); + +export default action; diff --git a/ui/src/main.js b/ui/src/main.js index 3f9f7ef..ed24114 100644 --- a/ui/src/main.js +++ b/ui/src/main.js @@ -6,6 +6,7 @@ import store from "./store"; import Antd from "ant-design-vue"; import "ant-design-vue/dist/antd.css"; import { VueAxios } from "./utils/request"; +import "@/directives/action"; Vue.config.productionTip = false; Vue.use(Antd); diff --git a/ui/src/router/index.js b/ui/src/router/index.js index d14b862..789b782 100644 --- a/ui/src/router/index.js +++ b/ui/src/router/index.js @@ -1,6 +1,7 @@ import Vue from "vue"; import VueRouter from "vue-router"; import Home from "../views/Home.vue"; +import Store from "@/store"; Vue.use(VueRouter); @@ -53,7 +54,21 @@ const routes = [ path: "/client-quota-page", name: "ClientQuota", component: () => - import(/* webpackChunkName: "cluster" */ "../views/quota/ClientQuota.vue"), + import( + /* webpackChunkName: "cluster" */ "../views/quota/ClientQuota.vue" + ), + }, + { + path: "/user-page", + name: "UserManage", + component: () => + import(/* webpackChunkName: "cluster" */ "../views/user/UserManage.vue"), + }, + { + path: "/login-page", + name: "Login", + component: () => + import(/* webpackChunkName: "cluster" */ "../views/login/Login.vue"), }, ]; @@ -64,4 +79,49 @@ const router = new VueRouter({ routes, }); +router.beforeEach((to, from, next) => { + const enableAuth = Store.state.auth.enable; + if (!enableAuth) { + next(); + } else { + if (to.path === "/login-page") { + next(); + } else { + let token = localStorage.getItem("access_token"); + if (token === null || token === "") { + next("/login-page"); + } else { + next(); + } + } + } +}); + +let originPush = VueRouter.prototype.push; +let originReplace = VueRouter.prototype.replace; +VueRouter.prototype.push = function (location, resolve, reject) { + if (resolve && reject) { + originPush.call(this, location, resolve, reject); + } else { + originPush.call( + this, + location, + () => {}, + () => {} + ); + } +}; +VueRouter.prototype.replace = function (location, resolve, reject) { + if (resolve && reject) { + originReplace.call(this, location, resolve, reject); + } else { + originReplace.call( + this, + location, + () => {}, + () => {} + ); + } +}; + export default router; diff --git a/ui/src/store/index.js b/ui/src/store/index.js index 78d8c4a..1441759 100644 --- a/ui/src/store/index.js +++ b/ui/src/store/index.js @@ -1,7 +1,12 @@ import Vue from "vue"; import Vuex from "vuex"; -import { CLUSTER } from "@/store/mutation-types"; -import { setClusterInfo } from "@/utils/local-cache"; +import { CLUSTER, AUTH } from "@/store/mutation-types"; +import { + setClusterInfo, + setPermissions, + setToken, + setUsername, +} from "@/utils/local-cache"; Vue.use(Vuex); @@ -12,6 +17,11 @@ export default new Vuex.Store({ clusterName: undefined, enableSasl: false, }, + auth: { + enable: false, + username: "", + permissions: [], + }, }, mutations: { [CLUSTER.SWITCH](state, clusterInfo) { @@ -28,6 +38,20 @@ export default new Vuex.Store({ state.clusterInfo.enableSasl = enableSasl; setClusterInfo(clusterInfo); }, + [AUTH.ENABLE](state, enable) { + state.auth.enable = enable; + }, + [AUTH.SET_TOKEN](state, info) { + setToken(info); + }, + [AUTH.SET_USERNAME](state, username) { + setUsername(username); + state.auth.username = username; + }, + [AUTH.SET_PERMISSIONS](state, permissions) { + setPermissions(permissions); + state.auth.permissions = permissions; + }, }, actions: {}, modules: {}, diff --git a/ui/src/store/mutation-types.js b/ui/src/store/mutation-types.js index f5ec573..f219f18 100644 --- a/ui/src/store/mutation-types.js +++ b/ui/src/store/mutation-types.js @@ -1,3 +1,10 @@ export const CLUSTER = { SWITCH: "switchCluster", }; + +export const AUTH = { + ENABLE: "enable", + SET_TOKEN: "setToken", + SET_USERNAME: "setUsername", + SET_PERMISSIONS: "setPermissions", +}; diff --git a/ui/src/utils/api.js b/ui/src/utils/api.js index 31b747f..86a618b 100644 --- a/ui/src/utils/api.js +++ b/ui/src/utils/api.js @@ -307,4 +307,54 @@ export const KafkaClientQuotaApi = { url: "/client/quota", method: "delete", }, -}; \ No newline at end of file +}; + +export const UserManageApi = { + getPermissions: { + url: "/sys/user/manage/permission", + method: "get", + }, + addPermission: { + url: "/sys/user/manage/permission", + method: "post", + }, + getRole: { + url: "/sys/user/manage/role", + method: "get", + }, + addOrUpdateRole: { + url: "/sys/user/manage/role", + method: "post", + }, + deleteRole: { + url: "/sys/user/manage/role", + method: "delete", + }, + getUsers: { + url: "/sys/user/manage/user", + method: "get", + }, + addOrUpdateUser: { + url: "/sys/user/manage/user", + method: "post", + }, + deleteUser: { + url: "/sys/user/manage/user", + method: "delete", + }, + updatePassword: { + url: "/sys/user/manage/user/password", + method: "post", + }, +}; + +export const AuthApi = { + enable: { + url: "/auth/enable", + method: "get", + }, + login: { + url: "/auth/login", + method: "post", + }, +}; diff --git a/ui/src/utils/auth.js b/ui/src/utils/auth.js new file mode 100644 index 0000000..7ec19f0 --- /dev/null +++ b/ui/src/utils/auth.js @@ -0,0 +1,16 @@ +import Store from "@/store"; + +export function isUnauthorized(permission) { + const enableAuth = Store.state.auth.enable; + const permissions = Store.state.auth.permissions; + return enableAuth && (!permissions || permissions.indexOf(permission) < 0); +} + +export function isAuthorized(permission) { + const enableAuth = Store.state.auth.enable; + if (!enableAuth) { + return true; + } + const permissions = Store.state.auth.permissions; + return permissions && permissions.indexOf(permission) >= 0; +} diff --git a/ui/src/utils/constants.js b/ui/src/utils/constants.js index bd15f7a..0fcb8dd 100644 --- a/ui/src/utils/constants.js +++ b/ui/src/utils/constants.js @@ -4,4 +4,9 @@ export const ConstantEvent = { export const Cache = { clusterInfo: "clusterInfo", + auth: "auth", + token: "access_token", + username: "login_user", + enableAuth: "enable_auth", + permissions: "permissions", }; diff --git a/ui/src/utils/local-cache.js b/ui/src/utils/local-cache.js index 23ebeb9..80ae0f1 100644 --- a/ui/src/utils/local-cache.js +++ b/ui/src/utils/local-cache.js @@ -8,3 +8,43 @@ export function getClusterInfo() { const str = localStorage.getItem(Cache.clusterInfo); return str ? JSON.parse(str) : undefined; } + +// export function setAuth(auth) { +// localStorage.setItem(Cache.auth, JSON.stringify(auth)); +// } + +export function setToken(token) { + localStorage.setItem(Cache.token, token); +} + +export function getToken() { + return localStorage.getItem(Cache.token); +} + +export function deleteToken() { + localStorage.removeItem(Cache.token); +} + +export function deleteUsername() { + localStorage.removeItem(Cache.username); +} + +export function setUsername(username) { + localStorage.setItem(Cache.username, username); +} + +export function getUsername() { + return localStorage.getItem(Cache.username); +} + +export function setPermissions(permissions) { + localStorage.setItem(Cache.permissions, permissions); +} + +export function getPermissions() { + return localStorage.getItem(Cache.permissions); +} + +// export function setEnableAuth(enable) { +// localStorage.setItem() +// } diff --git a/ui/src/utils/request.js b/ui/src/utils/request.js index 119e8d8..859fecb 100644 --- a/ui/src/utils/request.js +++ b/ui/src/utils/request.js @@ -2,7 +2,7 @@ import axios from "axios"; import notification from "ant-design-vue/es/notification"; import { VueAxios } from "./axios"; import { getClusterInfo } from "@/utils/local-cache"; - +import Router from "@/router"; // 创建 axios 实例 const request = axios.create({ // API 请求的默认前缀 @@ -10,14 +10,30 @@ const request = axios.create({ timeout: 120000, // 请求超时时间 }); +// axios.defaults.headers.common['X-Auth-Token'] = localStorage.getItem('access_token'); + // 异常拦截处理器 const errorHandler = (error) => { if (error.response) { - const data = error.response.data; - notification.error({ - message: error.response.status, - description: JSON.stringify(data), - }); + if (error.response.status == 401) { + notification.error({ + message: error.response.status, + description: "请登录", + }); + Router.push({ path: "/login-page" }); + } else if (error.response.status == 403) { + // const data = error.response.data; + // notification.error({ + // message: error.response.status, + // description: data.msg, + // }); + } else { + const data = error.response.data; + notification.error({ + message: error.response.status, + description: JSON.stringify(data), + }); + } } return Promise.reject(error); }; @@ -29,6 +45,10 @@ request.interceptors.request.use((config) => { config.headers["X-Cluster-Info-Id"] = clusterInfo.id; // config.headers["X-Cluster-Info-Name"] = encodeURIComponent(clusterInfo.clusterName); } + const token = localStorage.getItem("access_token"); + if (token) { + config.headers["X-Auth-Token"] = token; + } return config; }, errorHandler); diff --git a/ui/src/views/acl/Acl.vue b/ui/src/views/acl/Acl.vue index 02cd1ce..19c8a30 100644 --- a/ui/src/views/acl/Acl.vue +++ b/ui/src/views/acl/Acl.vue @@ -1,10 +1,14 @@