Преглед на файлове

完成部分权限的认证操作的迁移

YunaiV преди 4 години
родител
ревизия
bbe71ec2c8
променени са 39 файла, в които са добавени 370 реда и са изтрити 394 реда
  1. 1 1
      http-client.env.json
  2. 3 5
      ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/ServerController.java
  3. 24 24
      ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/SwaggerController.java
  4. 25 11
      ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/SysUserOnline.java
  5. 2 3
      ruoyi-common/src/main/java/com/ruoyi/common/annotation/DataScope.java
  6. 4 4
      ruoyi-common/src/main/java/com/ruoyi/common/annotation/DataSource.java
  7. 2 4
      ruoyi-common/src/main/java/com/ruoyi/common/annotation/RepeatSubmit.java
  8. 2 1
      ruoyi-common/src/main/java/com/ruoyi/common/constant/GenConstants.java
  9. 9 16
      ruoyi-common/src/main/java/com/ruoyi/common/filter/RepeatableFilter.java
  10. 11 19
      ruoyi-common/src/main/java/com/ruoyi/common/filter/RepeatedlyRequestWrapper.java
  11. 0 40
      ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/Server.java
  12. 0 12
      ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/Cpu.java
  13. 0 24
      ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/Jvm.java
  14. 1 12
      ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/Mem.java
  15. 0 39
      ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/Sys.java
  16. 0 55
      ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/SysFile.java
  17. 0 82
      ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/PermissionService.java
  18. 3 1
      ruoyi-ui/src/utils/request.js
  19. 8 3
      ruoyi-ui/src/views/system/role/index.vue
  20. 0 5
      src/main/java/cn/iocoder/dashboard/framework/logger/operatelog/core/aop/OperateLogAspect.java
  21. 2 2
      src/main/java/cn/iocoder/dashboard/framework/security/core/handler/LogoutSuccessHandlerImpl.java
  22. 11 9
      src/main/java/cn/iocoder/dashboard/framework/security/core/service/SecurityAuthFrameworkService.java
  23. 26 0
      src/main/java/cn/iocoder/dashboard/framework/security/core/service/SecurityPermissionFrameworkService.java
  24. 18 1
      src/main/java/cn/iocoder/dashboard/framework/web/core/handler/GlobalExceptionHandler.java
  25. 1 3
      src/main/java/cn/iocoder/dashboard/modules/system/controller/auth/vo/SysAuthLoginReqVO.java
  26. 3 0
      src/main/java/cn/iocoder/dashboard/modules/system/controller/user/SysUserController.http
  27. 2 1
      src/main/java/cn/iocoder/dashboard/modules/system/controller/user/SysUserController.java
  28. 8 2
      src/main/java/cn/iocoder/dashboard/modules/system/dal/mysql/dao/permission/SysRoleMenuMapper.java
  29. 29 0
      src/main/java/cn/iocoder/dashboard/modules/system/mq/consumer/permission/SysRoleMenuRefreshConsumer.java
  30. 17 0
      src/main/java/cn/iocoder/dashboard/modules/system/mq/message/permission/SysRoleMenuRefreshMessage.java
  31. 27 0
      src/main/java/cn/iocoder/dashboard/modules/system/mq/producer/permission/SysPermissionProducer.java
  32. 2 2
      src/main/java/cn/iocoder/dashboard/modules/system/service/auth/SysAuthService.java
  33. 8 0
      src/main/java/cn/iocoder/dashboard/modules/system/service/permission/SysMenuService.java
  34. 3 2
      src/main/java/cn/iocoder/dashboard/modules/system/service/permission/SysPermissionService.java
  35. 10 0
      src/main/java/cn/iocoder/dashboard/modules/system/service/permission/SysRoleService.java
  36. 8 3
      src/main/java/cn/iocoder/dashboard/modules/system/service/permission/impl/SysMenuServiceImpl.java
  37. 95 7
      src/main/java/cn/iocoder/dashboard/modules/system/service/permission/impl/SysPermissionServiceImpl.java
  38. 4 0
      src/main/java/cn/iocoder/dashboard/modules/system/service/permission/impl/SysRoleServiceImpl.java
  39. 1 1
      src/main/resources/application.yaml

+ 1 - 1
http-client.env.json

@@ -1,6 +1,6 @@
 {
   "local": {
     "baseUrl": "http://127.0.0.1:8080/api",
-    "token": "yudaoyuanma1"
+    "token": "test1"
   }
 }

+ 3 - 5
ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/ServerController.java

@@ -9,17 +9,15 @@ import com.ruoyi.framework.web.domain.Server;
 
 /**
  * 服务器监控
- * 
+ *
  * @author ruoyi
  */
 @RestController
 @RequestMapping("/monitor/server")
-public class ServerController
-{
+public class ServerController {
     @PreAuthorize("@ss.hasPermi('monitor:server:list')")
     @GetMapping()
-    public AjaxResult getInfo() throws Exception
-    {
+    public AjaxResult getInfo() throws Exception {
         Server server = new Server();
         server.copyTo();
         return AjaxResult.success(server);

+ 24 - 24
ruoyi-admin/src/main/java/com/ruoyi/web/controller/tool/SwaggerController.java → ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/SwaggerController.java

@@ -1,24 +1,24 @@
-package com.ruoyi.web.controller.tool;
-
-import org.springframework.security.access.prepost.PreAuthorize;
-import org.springframework.stereotype.Controller;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.RequestMapping;
-import com.ruoyi.common.core.controller.BaseController;
-
-/**
- * swagger 接口
- * 
- * @author ruoyi
- */
-@Controller
-@RequestMapping("/tool/swagger")
-public class SwaggerController extends BaseController
-{
-    @PreAuthorize("@ss.hasPermi('tool:swagger:view')")
-    @GetMapping()
-    public String index()
-    {
-        return redirect("/swagger-ui.html");
-    }
-}
+package com.ruoyi.web.controller.tool;
+
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import com.ruoyi.common.core.controller.BaseController;
+
+/**
+ * swagger 接口
+ * 
+ * @author ruoyi
+ */
+@Controller
+@RequestMapping("/tool/swagger")
+public class SwaggerController extends BaseController
+{
+    @PreAuthorize("@ss.hasPermi('tool:swagger:view')")
+    @GetMapping()
+    public String index()
+    {
+        return redirect("/swagger-ui.html");
+    }
+}

+ 25 - 11
ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/SysUserOnline.java

@@ -5,31 +5,45 @@ package com.ruoyi.system.domain;
  *
  * @author ruoyi
  */
-public class SysUserOnline
-{
-    /** 会话编号 */
+public class SysUserOnline {
+    /**
+     * 会话编号
+     */
     private String tokenId;
 
-    /** 部门名称 */
+    /**
+     * 部门名称
+     */
     private String deptName;
 
-    /** 用户名称 */
+    /**
+     * 用户名称
+     */
     private String userName;
 
-    /** 登录IP地址 */
+    /**
+     * 登录IP地址
+     */
     private String ipaddr;
 
-    /** 登录地址 */
+    /**
+     * 登录地址
+     */
     private String loginLocation;
 
-    /** 浏览器类型 */
+    /**
+     * 浏览器类型
+     */
     private String browser;
 
-    /** 操作系统 */
+    /**
+     * 操作系统
+     */
     private String os;
 
-    /** 登录时间 */
+    /**
+     * 登录时间
+     */
     private Long loginTime;
 
-
 }

+ 2 - 3
ruoyi-common/src/main/java/com/ruoyi/common/annotation/DataScope.java

@@ -8,14 +8,13 @@ import java.lang.annotation.Target;
 
 /**
  * 数据权限过滤注解
- * 
+ *
  * @author ruoyi
  */
 @Target(ElementType.METHOD)
 @Retention(RetentionPolicy.RUNTIME)
 @Documented
-public @interface DataScope
-{
+public @interface DataScope {
     /**
      * 部门表的别名
      */

+ 4 - 4
ruoyi-common/src/main/java/com/ruoyi/common/annotation/DataSource.java

@@ -6,21 +6,21 @@ import java.lang.annotation.Inherited;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.lang.annotation.Target;
+
 import com.ruoyi.common.enums.DataSourceType;
 
 /**
  * 自定义多数据源切换注解
- *
+ * <p>
  * 优先级:先方法,后类,如果方法覆盖了类上的数据源类型,以方法的为准,否则以类上的为准
  *
  * @author ruoyi
  */
-@Target({ ElementType.METHOD, ElementType.TYPE })
+@Target({ElementType.METHOD, ElementType.TYPE})
 @Retention(RetentionPolicy.RUNTIME)
 @Documented
 @Inherited
-public @interface DataSource
-{
+public @interface DataSource {
     /**
      * 切换数据源名称
      */

+ 2 - 4
ruoyi-common/src/main/java/com/ruoyi/common/annotation/RepeatSubmit.java

@@ -9,15 +9,13 @@ import java.lang.annotation.Target;
 
 /**
  * 自定义注解防止表单重复提交
- * 
- * @author ruoyi
  *
+ * @author ruoyi
  */
 @Inherited
 @Target(ElementType.METHOD)
 @Retention(RetentionPolicy.RUNTIME)
 @Documented
-public @interface RepeatSubmit
-{
+public @interface RepeatSubmit {
 
 }

+ 2 - 1
ruoyi-common/src/main/java/com/ruoyi/common/constant/GenConstants.java

@@ -2,7 +2,7 @@ package com.ruoyi.common.constant;
 
 /**
  * 代码生成通用常量
- * 
+ *
  * @author ruoyi
  */
 public class GenConstants
@@ -13,6 +13,7 @@ public class GenConstants
     /** 树表(增删改查) */
     public static final String TPL_TREE = "tree";
 
+
     /** 树编码字段 */
     public static final String TREE_CODE = "treeCode";
 

+ 9 - 16
ruoyi-common/src/main/java/com/ruoyi/common/filter/RepeatableFilter.java

@@ -8,45 +8,38 @@ import javax.servlet.ServletException;
 import javax.servlet.ServletRequest;
 import javax.servlet.ServletResponse;
 import javax.servlet.http.HttpServletRequest;
+
 import org.springframework.http.MediaType;
 import com.ruoyi.common.utils.StringUtils;
 
 /**
  * Repeatable 过滤器
- * 
+ *
  * @author ruoyi
  */
-public class RepeatableFilter implements Filter
-{
+public class RepeatableFilter implements Filter {
     @Override
-    public void init(FilterConfig filterConfig) throws ServletException
-    {
+    public void init(FilterConfig filterConfig) throws ServletException {
 
     }
 
     @Override
     public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
-            throws IOException, ServletException
-    {
+            throws IOException, ServletException {
         ServletRequest requestWrapper = null;
         if (request instanceof HttpServletRequest
-                && StringUtils.equalsAnyIgnoreCase(request.getContentType(), MediaType.APPLICATION_JSON_VALUE))
-        {
+                && StringUtils.equalsAnyIgnoreCase(request.getContentType(), MediaType.APPLICATION_JSON_VALUE)) {
             requestWrapper = new RepeatedlyRequestWrapper((HttpServletRequest) request, response);
         }
-        if (null == requestWrapper)
-        {
+        if (null == requestWrapper) {
             chain.doFilter(request, response);
-        }
-        else
-        {
+        } else {
             chain.doFilter(requestWrapper, response);
         }
     }
 
     @Override
-    public void destroy()
-    {
+    public void destroy() {
 
     }
 }

+ 11 - 19
ruoyi-common/src/main/java/com/ruoyi/common/filter/RepeatedlyRequestWrapper.java

@@ -9,19 +9,18 @@ import javax.servlet.ServletInputStream;
 import javax.servlet.ServletResponse;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletRequestWrapper;
+
 import com.ruoyi.common.utils.http.HttpHelper;
 
 /**
  * 构建可重复读取inputStream的request
- * 
+ *
  * @author ruoyi
  */
-public class RepeatedlyRequestWrapper extends HttpServletRequestWrapper
-{
+public class RepeatedlyRequestWrapper extends HttpServletRequestWrapper {
     private final byte[] body;
 
-    public RepeatedlyRequestWrapper(HttpServletRequest request, ServletResponse response) throws IOException
-    {
+    public RepeatedlyRequestWrapper(HttpServletRequest request, ServletResponse response) throws IOException {
         super(request);
         request.setCharacterEncoding("UTF-8");
         response.setCharacterEncoding("UTF-8");
@@ -30,41 +29,34 @@ public class RepeatedlyRequestWrapper extends HttpServletRequestWrapper
     }
 
     @Override
-    public BufferedReader getReader() throws IOException
-    {
+    public BufferedReader getReader() throws IOException {
         return new BufferedReader(new InputStreamReader(getInputStream()));
     }
 
     @Override
-    public ServletInputStream getInputStream() throws IOException
-    {
+    public ServletInputStream getInputStream() throws IOException {
 
         final ByteArrayInputStream bais = new ByteArrayInputStream(body);
 
-        return new ServletInputStream()
-        {
+        return new ServletInputStream() {
 
             @Override
-            public int read() throws IOException
-            {
+            public int read() throws IOException {
                 return bais.read();
             }
 
             @Override
-            public boolean isFinished()
-            {
+            public boolean isFinished() {
                 return false;
             }
 
             @Override
-            public boolean isReady()
-            {
+            public boolean isReady() {
                 return false;
             }
 
             @Override
-            public void setReadListener(ReadListener readListener)
-            {
+            public void setReadListener(ReadListener readListener) {
 
             }
         };

+ 0 - 40
ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/Server.java

@@ -55,46 +55,6 @@ public class Server {
      */
     private List<SysFile> sysFiles = new LinkedList<SysFile>();
 
-    public Cpu getCpu() {
-        return cpu;
-    }
-
-    public void setCpu(Cpu cpu) {
-        this.cpu = cpu;
-    }
-
-    public Mem getMem() {
-        return mem;
-    }
-
-    public void setMem(Mem mem) {
-        this.mem = mem;
-    }
-
-    public Jvm getJvm() {
-        return jvm;
-    }
-
-    public void setJvm(Jvm jvm) {
-        this.jvm = jvm;
-    }
-
-    public Sys getSys() {
-        return sys;
-    }
-
-    public void setSys(Sys sys) {
-        this.sys = sys;
-    }
-
-    public List<SysFile> getSysFiles() {
-        return sysFiles;
-    }
-
-    public void setSysFiles(List<SysFile> sysFiles) {
-        this.sysFiles = sysFiles;
-    }
-
     public void copyTo() throws Exception {
         SystemInfo si = new SystemInfo();
         HardwareAbstractionLayer hal = si.getHardware();

+ 0 - 12
ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/Cpu.java

@@ -38,22 +38,10 @@ public class Cpu {
      */
     private double free;
 
-    public int getCpuNum() {
-        return cpuNum;
-    }
-
-    public void setCpuNum(int cpuNum) {
-        this.cpuNum = cpuNum;
-    }
-
     public double getTotal() {
         return Arith.round(Arith.mul(total, 100), 2);
     }
 
-    public void setTotal(double total) {
-        this.total = total;
-    }
-
     public double getSys() {
         return Arith.round(Arith.mul(sys / total, 100), 2);
     }

+ 0 - 24
ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/Jvm.java

@@ -40,18 +40,10 @@ public class Jvm {
         return Arith.div(total, (1024 * 1024), 2);
     }
 
-    public void setTotal(double total) {
-        this.total = total;
-    }
-
     public double getMax() {
         return Arith.div(max, (1024 * 1024), 2);
     }
 
-    public void setMax(double max) {
-        this.max = max;
-    }
-
     public double getFree() {
         return Arith.div(free, (1024 * 1024), 2);
     }
@@ -75,22 +67,6 @@ public class Jvm {
         return ManagementFactory.getRuntimeMXBean().getVmName();
     }
 
-    public String getVersion() {
-        return version;
-    }
-
-    public void setVersion(String version) {
-        this.version = version;
-    }
-
-    public String getHome() {
-        return home;
-    }
-
-    public void setHome(String home) {
-        this.home = home;
-    }
-
     /**
      * JDK启动时间
      */

+ 1 - 12
ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/Mem.java

@@ -27,27 +27,16 @@ public class Mem {
         return Arith.div(total, (1024 * 1024 * 1024), 2);
     }
 
-    public void setTotal(long total) {
-        this.total = total;
-    }
-
     public double getUsed() {
         return Arith.div(used, (1024 * 1024 * 1024), 2);
     }
 
-    public void setUsed(long used) {
-        this.used = used;
-    }
-
     public double getFree() {
         return Arith.div(free, (1024 * 1024 * 1024), 2);
     }
 
-    public void setFree(long free) {
-        this.free = free;
-    }
-
     public double getUsage() {
         return Arith.mul(Arith.div(used, total, 4), 100);
     }
+
 }

+ 0 - 39
ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/Sys.java

@@ -31,43 +31,4 @@ public class Sys {
      */
     private String osArch;
 
-    public String getComputerName() {
-        return computerName;
-    }
-
-    public void setComputerName(String computerName) {
-        this.computerName = computerName;
-    }
-
-    public String getComputerIp() {
-        return computerIp;
-    }
-
-    public void setComputerIp(String computerIp) {
-        this.computerIp = computerIp;
-    }
-
-    public String getUserDir() {
-        return userDir;
-    }
-
-    public void setUserDir(String userDir) {
-        this.userDir = userDir;
-    }
-
-    public String getOsName() {
-        return osName;
-    }
-
-    public void setOsName(String osName) {
-        this.osName = osName;
-    }
-
-    public String getOsArch() {
-        return osArch;
-    }
-
-    public void setOsArch(String osArch) {
-        this.osArch = osArch;
-    }
 }

+ 0 - 55
ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/SysFile.java

@@ -41,59 +41,4 @@ public class SysFile {
      */
     private double usage;
 
-    public String getDirName() {
-        return dirName;
-    }
-
-    public void setDirName(String dirName) {
-        this.dirName = dirName;
-    }
-
-    public String getSysTypeName() {
-        return sysTypeName;
-    }
-
-    public void setSysTypeName(String sysTypeName) {
-        this.sysTypeName = sysTypeName;
-    }
-
-    public String getTypeName() {
-        return typeName;
-    }
-
-    public void setTypeName(String typeName) {
-        this.typeName = typeName;
-    }
-
-    public String getTotal() {
-        return total;
-    }
-
-    public void setTotal(String total) {
-        this.total = total;
-    }
-
-    public String getFree() {
-        return free;
-    }
-
-    public void setFree(String free) {
-        this.free = free;
-    }
-
-    public String getUsed() {
-        return used;
-    }
-
-    public void setUsed(String used) {
-        this.used = used;
-    }
-
-    public double getUsage() {
-        return usage;
-    }
-
-    public void setUsage(double usage) {
-        this.usage = usage;
-    }
 }

+ 0 - 82
ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/PermissionService.java

@@ -17,68 +17,6 @@ import com.ruoyi.common.utils.StringUtils;
  */
 @Service("ss")
 public class PermissionService {
-    /**
-     * 所有权限标识
-     */
-    private static final String ALL_PERMISSION = "*:*:*";
-
-    /**
-     * 管理员角色权限标识
-     */
-    private static final String SUPER_ADMIN = "admin";
-
-    @Autowired
-    private TokenService tokenService;
-
-    /**
-     * 验证用户是否具备某权限
-     *
-     * @param permission 权限字符串
-     * @return 用户是否具备某权限
-     */
-    public boolean hasPermi(String permission) {
-        if (StringUtils.isEmpty(permission)) {
-            return false;
-        }
-        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
-        if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getPermissions())) {
-            return false;
-        }
-        return hasPermissions(loginUser.getPermissions(), permission);
-    }
-
-    /**
-     * 验证用户是否不具备某权限,与 hasPermi逻辑相反
-     *
-     * @param permission 权限字符串
-     * @return 用户是否不具备某权限
-     */
-    public boolean lacksPermi(String permission) {
-        return hasPermi(permission) != true;
-    }
-
-    /**
-     * 验证用户是否具有以下任意一个权限
-     *
-     * @param permissions 以 PERMISSION_NAMES_DELIMETER 为分隔符的权限列表
-     * @return 用户是否具有以下任意一个权限
-     */
-    public boolean hasAnyPermi(String permissions) {
-        if (StringUtils.isEmpty(permissions)) {
-            return false;
-        }
-        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
-        if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getPermissions())) {
-            return false;
-        }
-        Set<String> authorities = loginUser.getPermissions();
-        for (String permission : permissions.split(PERMISSION_DELIMETER)) {
-            if (permission != null && hasPermissions(authorities, permission)) {
-                return true;
-            }
-        }
-        return false;
-    }
 
     /**
      * 判断用户是否拥有某个角色
@@ -103,16 +41,6 @@ public class PermissionService {
         return false;
     }
 
-    /**
-     * 验证用户是否不具备某角色,与 isRole逻辑相反。
-     *
-     * @param role 角色名称
-     * @return 用户是否不具备某角色
-     */
-    public boolean lacksRole(String role) {
-        return hasRole(role) != true;
-    }
-
     /**
      * 验证用户是否具有以下任意一个角色
      *
@@ -135,14 +63,4 @@ public class PermissionService {
         return false;
     }
 
-    /**
-     * 判断是否包含权限
-     *
-     * @param permissions 权限列表
-     * @param permission  权限字符串
-     * @return 用户是否具备某权限
-     */
-    private boolean hasPermissions(Set<String> permissions, String permission) {
-        return permissions.contains(ALL_PERMISSION) || permissions.contains(StringUtils.trim(permission));
-    }
 }

+ 3 - 1
ruoyi-ui/src/utils/request.js

@@ -61,7 +61,9 @@ service.interceptors.response.use(res => {
         }
       ).then(() => {
         store.dispatch('LogOut').then(() => {
-          location.href = '/index';
+          if (location.pathname !== '/login') { // 避免重复跳转
+            location.href = '/index';
+          }
         })
       })
     } else if (code === 500) {

+ 8 - 3
ruoyi-ui/src/views/system/role/index.vue

@@ -392,6 +392,7 @@ export default {
         menuIds: [],
         dataScope: undefined,
         deptCheckStrictly: false,
+        menuCheckStrictly: true,
         remark: undefined
       };
       this.resetForm("form");
@@ -471,8 +472,12 @@ export default {
       });
       // 获得角色拥有的菜单集合
       listRoleMenus(id).then(response => {
+        // 设置为严格,避免设置父节点自动选中子节点,解决半选中问题
+        this.form.menuCheckStrictly = true
         // 设置选中
-        this.$refs.menu.setCheckedKeys(response.data, true, false);
+        this.$refs.menu.setCheckedKeys(response.data);
+        // 设置为非严格,继续使用半选中
+        this.form.menuCheckStrictly = false
       })
     },
     /** 分配数据权限操作 */
@@ -523,7 +528,7 @@ export default {
           roleId: this.form.id,
           dataScope: this.form.dataScope,
           dataScopeDeptIds: this.form.dataScope !== SysDataScopeEnum.DEPT_CUSTOM ? [] :
-              this.$refs.dept.getCheckedKeys(false)
+              this.$refs.dept.getCheckedKeys()
         }).then(response => {
           this.msgSuccess("修改成功");
           this.openDataScope = false;
@@ -536,7 +541,7 @@ export default {
       if (this.form.id !== undefined) {
         assignRoleMenu({
           roleId: this.form.id,
-          menuIds: this.$refs.menu.getCheckedKeys(true)
+          menuIds: [...this.$refs.menu.getCheckedKeys(), ...this.$refs.menu.getHalfCheckedKeys()]
         }).then(response => {
           this.msgSuccess("修改成功");
           this.openMenu = false;

+ 0 - 5
src/main/java/cn/iocoder/dashboard/framework/logger/operatelog/core/aop/OperateLogAspect.java

@@ -233,11 +233,6 @@ public class OperateLogAspect {
         }
     }
 
-    private static void fillContentFields(SysOperateLogCreateReqVO operateLogVO) {
-        operateLogVO.setContent(CONTENT.get());
-        operateLogVO.setExts(EXTS.get());
-    }
-
     private static boolean isLogEnable(ProceedingJoinPoint joinPoint, OperateLog operateLog) {
         // 有 @OperateLog 注解的情况下
         if (operateLog != null) {

+ 2 - 2
src/main/java/cn/iocoder/dashboard/framework/security/core/handler/LogoutSuccessHandlerImpl.java

@@ -2,7 +2,7 @@ package cn.iocoder.dashboard.framework.security.core.handler;
 
 import cn.hutool.core.util.StrUtil;
 import cn.iocoder.dashboard.framework.security.config.SecurityProperties;
-import cn.iocoder.dashboard.framework.security.core.service.SecurityFrameworkService;
+import cn.iocoder.dashboard.framework.security.core.service.SecurityAuthFrameworkService;
 import cn.iocoder.dashboard.framework.security.core.util.SecurityUtils;
 import cn.iocoder.dashboard.util.servlet.ServletUtils;
 import org.springframework.security.core.Authentication;
@@ -26,7 +26,7 @@ public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler {
     private SecurityProperties securityProperties;
 
     @Resource
-    private SecurityFrameworkService securityFrameworkService;
+    private SecurityAuthFrameworkService securityFrameworkService;
 
     @Override
     public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {

+ 11 - 9
src/main/java/cn/iocoder/dashboard/framework/security/core/service/SecurityFrameworkService.java → src/main/java/cn/iocoder/dashboard/framework/security/core/service/SecurityAuthFrameworkService.java

@@ -4,16 +4,11 @@ import cn.iocoder.dashboard.framework.security.core.LoginUser;
 import org.springframework.security.core.userdetails.UserDetailsService;
 
 /**
- * Security 框架 Service 接口,定义 security 组件需要的功能
+ * Security 框架 Auth Service 接口,定义 security 组件需要的功能
+ *
+ * @author 芋道源码
  */
-public interface SecurityFrameworkService extends UserDetailsService {
-
-    /**
-     * 基于 token 退出登录
-     *
-     * @param token token
-     */
-    void logout(String token);
+public interface SecurityAuthFrameworkService extends UserDetailsService {
 
     /**
      * 校验 token 的有效性,并获取用户信息
@@ -32,4 +27,11 @@ public interface SecurityFrameworkService extends UserDetailsService {
      */
     LoginUser mockLogin(Long userId);
 
+    /**
+     * 基于 token 退出登录
+     *
+     * @param token token
+     */
+    void logout(String token);
+
 }

+ 26 - 0
src/main/java/cn/iocoder/dashboard/framework/security/core/service/SecurityPermissionFrameworkService.java

@@ -0,0 +1,26 @@
+package cn.iocoder.dashboard.framework.security.core.service;
+
+/**
+ * Security 框架 Permission Service 接口,定义 security 组件需要的功能
+ *
+ * @author 芋道源码
+ */
+public interface SecurityPermissionFrameworkService {
+
+    /**
+     * 判断是否有权限
+     *
+     * @param permission 权限
+     * @return 是否
+     */
+    boolean hasPermission(String permission);
+
+    /**
+     * 判断是否有权限,任一一个即可
+     *
+     * @param permissions 权限
+     * @return 是否
+     */
+    boolean hasAnyPermissions(String... permissions);
+
+}

+ 18 - 1
src/main/java/cn/iocoder/dashboard/framework/web/core/handler/GlobalExceptionHandler.java

@@ -3,7 +3,9 @@ package cn.iocoder.dashboard.framework.web.core.handler;
 import cn.iocoder.dashboard.common.exception.GlobalException;
 import cn.iocoder.dashboard.common.exception.ServiceException;
 import cn.iocoder.dashboard.common.pojo.CommonResult;
+import cn.iocoder.dashboard.framework.security.core.util.SecurityUtils;
 import lombok.extern.slf4j.Slf4j;
+import org.springframework.security.access.AccessDeniedException;
 import org.springframework.validation.BindException;
 import org.springframework.validation.FieldError;
 import org.springframework.web.HttpRequestMethodNotSupportedException;
@@ -64,6 +66,9 @@ public class GlobalExceptionHandler {
         if (ex instanceof ServiceException) {
             return serviceExceptionHandler((ServiceException) ex);
         }
+        if (ex instanceof AccessDeniedException) {
+            return accessDeniedExceptionHandler(request, (AccessDeniedException) ex);
+        }
         if (ex instanceof GlobalException) {
             return globalExceptionHandler(request, (GlobalException) ex);
         }
@@ -131,7 +136,7 @@ public class GlobalExceptionHandler {
     public CommonResult<?> validationException(ValidationException ex) {
         log.warn("[constraintViolationExceptionHandler]", ex);
         // 无法拼接明细的错误信息,因为 Dubbo Consumer 抛出 ValidationException 异常时,是直接的字符串信息,且人类不可读
-        return CommonResult.error(BAD_REQUEST.getCode(), "请求参数不正确");
+        return CommonResult.error(BAD_REQUEST);
     }
 
     /**
@@ -158,6 +163,18 @@ public class GlobalExceptionHandler {
         return CommonResult.error(METHOD_NOT_ALLOWED.getCode(), String.format("请求方法不正确:%s", ex.getMessage()));
     }
 
+    /**
+     * 处理 Spring Security 权限不足的异常
+     *
+     * 来源是,使用 @PreAuthorize 注解,AOP 进行权限拦截
+     */
+    @ExceptionHandler(value = AccessDeniedException.class)
+    public CommonResult<?> accessDeniedExceptionHandler(HttpServletRequest req, AccessDeniedException ex) {
+        log.warn("[accessDeniedExceptionHandler][userId({}) 无法访问 url({})]", SecurityUtils.getLoginUserId(),
+                req.getRequestURL(), ex);
+        return CommonResult.error(FORBIDDEN);
+    }
+
     /**
      * 处理业务异常 ServiceException
      *

+ 1 - 3
src/main/java/cn/iocoder/dashboard/modules/system/controller/auth/vo/SysAuthLoginReqVO.java

@@ -7,9 +7,7 @@ import lombok.Builder;
 import lombok.Data;
 import lombok.NoArgsConstructor;
 import org.hibernate.validator.constraints.Length;
-import org.springframework.validation.annotation.Validated;
 
-import javax.validation.Valid;
 import javax.validation.constraints.NotEmpty;
 import javax.validation.constraints.Pattern;
 
@@ -22,7 +20,7 @@ public class SysAuthLoginReqVO {
 
     @ApiModelProperty(value = "账号", required = true, example = "yudaoyuanma")
     @NotEmpty(message = "登陆账号不能为空")
-    @Length(min = 5, max = 16, message = "账号长度为 5-16 位")
+    @Length(min = 4, max = 16, message = "账号长度为 4-16 位")
     @Pattern(regexp = "^[A-Za-z0-9]+$", message = "账号格式为数字以及字母")
     private String username;
 

+ 3 - 0
src/main/java/cn/iocoder/dashboard/modules/system/controller/user/SysUserController.http

@@ -0,0 +1,3 @@
+### 请求 /system/user/page 接口 => 没有权限
+GET {{baseUrl}}/system/user/page?pageNo=1&pageSize=10
+Authorization: Bearer test104 # 使用测试账号

+ 2 - 1
src/main/java/cn/iocoder/dashboard/modules/system/controller/user/SysUserController.java

@@ -18,6 +18,7 @@ import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiImplicitParam;
 import io.swagger.annotations.ApiImplicitParams;
 import io.swagger.annotations.ApiOperation;
+import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.validation.annotation.Validated;
 import org.springframework.web.bind.annotation.*;
 import org.springframework.web.multipart.MultipartFile;
@@ -41,7 +42,7 @@ public class SysUserController {
 
     @ApiOperation("获得用户分页列表")
     @GetMapping("/page")
-//    @PreAuthorize("@ss.hasPermi('system:user:list')")
+    @PreAuthorize("@ss.hasPermission('system:user:list')")
     public CommonResult<PageResult<SysUserPageItemRespVO>> pageUsers(@Validated SysUserPageReqVO reqVO) {
         // 获得用户分页列表
         PageResult<SysUserDO> pageResult = userService.pageUsers(reqVO);

+ 8 - 2
src/main/java/cn/iocoder/dashboard/modules/system/dal/mysql/dao/permission/SysRoleMenuMapper.java

@@ -1,16 +1,17 @@
 package cn.iocoder.dashboard.modules.system.dal.mysql.dao.permission;
 
+import cn.iocoder.dashboard.framework.mybatis.core.mapper.BaseMapperX;
 import cn.iocoder.dashboard.modules.system.dal.mysql.dataobject.permission.SysRoleMenuDO;
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
-import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import org.apache.ibatis.annotations.Mapper;
 
 import java.util.Collection;
+import java.util.Date;
 import java.util.List;
 import java.util.stream.Collectors;
 
 @Mapper
-public interface SysRoleMenuMapper extends BaseMapper<SysRoleMenuDO> {
+public interface SysRoleMenuMapper extends BaseMapperX<SysRoleMenuDO> {
 
     default List<SysRoleMenuDO> selectListByRoleId(Long roleId) {
         return selectList(new QueryWrapper<SysRoleMenuDO>().eq("role_id", roleId));
@@ -32,4 +33,9 @@ public interface SysRoleMenuMapper extends BaseMapper<SysRoleMenuDO> {
                 .in("menu_id", menuIds));
     }
 
+    default boolean selectExistsByUpdateTimeAfter(Date maxUpdateTime) {
+        return selectOne(new QueryWrapper<SysRoleMenuDO>().select("id")
+                .gt("update_time", maxUpdateTime).last("LIMIT 1")) != null;
+    }
+
 }

+ 29 - 0
src/main/java/cn/iocoder/dashboard/modules/system/mq/consumer/permission/SysRoleMenuRefreshConsumer.java

@@ -0,0 +1,29 @@
+package cn.iocoder.dashboard.modules.system.mq.consumer.permission;
+
+import cn.iocoder.dashboard.framework.redis.core.pubsub.AbstractChannelMessageListener;
+import cn.iocoder.dashboard.modules.system.mq.message.permission.SysRoleMenuRefreshMessage;
+import cn.iocoder.dashboard.modules.system.service.permission.SysPermissionService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.Resource;
+
+/**
+ * 针对 {@link SysRoleMenuRefreshMessage} 的消费者
+ *
+ * @author 芋道源码
+ */
+@Component
+@Slf4j
+public class SysRoleMenuRefreshConsumer extends AbstractChannelMessageListener<SysRoleMenuRefreshMessage> {
+
+    @Resource
+    private SysPermissionService permissionService;
+
+    @Override
+    public void onMessage(SysRoleMenuRefreshMessage message) {
+        log.info("[onMessage][收到 Role 与 Menu 的关联刷新消息]");
+        permissionService.initLocalCache();
+    }
+
+}

+ 17 - 0
src/main/java/cn/iocoder/dashboard/modules/system/mq/message/permission/SysRoleMenuRefreshMessage.java

@@ -0,0 +1,17 @@
+package cn.iocoder.dashboard.modules.system.mq.message.permission;
+
+import cn.iocoder.dashboard.framework.redis.core.pubsub.ChannelMessage;
+import lombok.Data;
+
+/**
+ * 角色与菜单数据刷新 Message
+ */
+@Data
+public class SysRoleMenuRefreshMessage implements ChannelMessage {
+
+    @Override
+    public String getChannel() {
+        return "system.role-menu.refresh";
+    }
+
+}

+ 27 - 0
src/main/java/cn/iocoder/dashboard/modules/system/mq/producer/permission/SysPermissionProducer.java

@@ -0,0 +1,27 @@
+package cn.iocoder.dashboard.modules.system.mq.producer.permission;
+
+import cn.iocoder.dashboard.framework.redis.core.util.RedisMessageUtils;
+import cn.iocoder.dashboard.modules.system.mq.message.permission.SysRoleMenuRefreshMessage;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.Resource;
+
+/**
+ * Permission 权限相关消息的 Producer
+ */
+@Component
+public class SysPermissionProducer {
+
+    @Resource
+    private StringRedisTemplate stringRedisTemplate;
+
+    /**
+     * 发送 {@link SysRoleMenuRefreshMessage} 消息
+     */
+    public void sendRoleMenuRefreshMessage() {
+        SysRoleMenuRefreshMessage message = new SysRoleMenuRefreshMessage();
+        RedisMessageUtils.sendChannelMessage(stringRedisTemplate, message);
+    }
+
+}

+ 2 - 2
src/main/java/cn/iocoder/dashboard/modules/system/service/auth/SysAuthService.java

@@ -1,6 +1,6 @@
 package cn.iocoder.dashboard.modules.system.service.auth;
 
-import cn.iocoder.dashboard.framework.security.core.service.SecurityFrameworkService;
+import cn.iocoder.dashboard.framework.security.core.service.SecurityAuthFrameworkService;
 
 /**
  * 认证 Service 接口
@@ -9,7 +9,7 @@ import cn.iocoder.dashboard.framework.security.core.service.SecurityFrameworkSer
  *
  * @author 芋道源码
  */
-public interface SysAuthService extends SecurityFrameworkService {
+public interface SysAuthService extends SecurityAuthFrameworkService {
 
     String login(String username, String password, String captchaUUID, String captchaCode);
 

+ 8 - 0
src/main/java/cn/iocoder/dashboard/modules/system/service/permission/SysMenuService.java

@@ -59,6 +59,14 @@ public interface SysMenuService {
     List<SysMenuDO> listMenusFromCache(Collection<Long> menuIds, Collection<Integer> menuTypes,
                                        Collection<Integer> menusStatuses);
 
+    /**
+     * 获得权限对应的菜单数组
+     *
+     * @param permission 权限标识
+     * @return 数组
+     */
+    List<SysMenuDO> getMenuListByPermissionFromCache(String permission);
+
     /*
      * 创建菜单
      *

+ 3 - 2
src/main/java/cn/iocoder/dashboard/modules/system/service/permission/SysPermissionService.java

@@ -1,5 +1,6 @@
 package cn.iocoder.dashboard.modules.system.service.permission;
 
+import cn.iocoder.dashboard.framework.security.core.service.SecurityPermissionFrameworkService;
 import cn.iocoder.dashboard.modules.system.dal.mysql.dataobject.permission.SysMenuDO;
 import org.springframework.lang.Nullable;
 
@@ -14,12 +15,12 @@ import java.util.Set;
  *
  * @author 芋道源码
  */
-public interface SysPermissionService {
+public interface SysPermissionService extends SecurityPermissionFrameworkService {
 
     /**
      * 初始化
      */
-    void init();
+    void initLocalCache();
 
     /**
      * 获得角色们拥有的菜单列表,从缓存中获取

+ 10 - 0
src/main/java/cn/iocoder/dashboard/modules/system/service/permission/SysRoleService.java

@@ -56,6 +56,16 @@ public interface SysRoleService {
      */
     boolean hasAnyAdmin(Collection<SysRoleDO> roleList);
 
+    /**
+     * 判断角色编号数组中,是否有管理员
+     *
+     * @param ids 角色编号数组
+     * @return 是否有管理员
+     */
+    default boolean hasAnyAdmin(Set<Long> ids) {
+        return hasAnyAdmin(listRolesFromCache(ids));
+    }
+
     /**
      * 创建角色
      *

+ 8 - 3
src/main/java/cn/iocoder/dashboard/modules/system/service/permission/impl/SysMenuServiceImpl.java

@@ -61,7 +61,7 @@ public class SysMenuServiceImpl implements SysMenuService {
      *
      * 这里声明 volatile 修饰的原因是,每次刷新时,直接修改指向
      */
-    private volatile Multimap<String, SysMenuDO> permMenuCache;
+    private volatile Multimap<String, SysMenuDO> permissionMenuCache;
     /**
      * 缓存菜单的最大更新时间,用于后续的增量轮询,判断是否有更新
      */
@@ -76,7 +76,7 @@ public class SysMenuServiceImpl implements SysMenuService {
     private SysMenuProducer menuProducer;
 
     /**
-     * 初始化 {@link #menuCache} 和 {@link #permMenuCache} 缓存
+     * 初始化 {@link #menuCache} 和 {@link #permissionMenuCache} 缓存
      */
     @Override
     @PostConstruct
@@ -95,7 +95,7 @@ public class SysMenuServiceImpl implements SysMenuService {
             permMenuCacheBuilder.put(menuDO.getPermission(), menuDO);
         });
         menuCache = menuCacheBuilder.build();
-        permMenuCache = permMenuCacheBuilder.build();
+        permissionMenuCache = permMenuCacheBuilder.build();
         assert menuList.size() > 0; // 断言,避免告警
         maxUpdateTime = menuList.stream().max(Comparator.comparing(BaseDO::getUpdateTime)).get().getUpdateTime();
         log.info("[initLocalCache][缓存菜单,数量为:{}]", menuList.size());
@@ -162,6 +162,11 @@ public class SysMenuServiceImpl implements SysMenuService {
                 .collect(Collectors.toList());
     }
 
+    @Override
+    public List<SysMenuDO> getMenuListByPermissionFromCache(String permission) {
+        return new ArrayList<>(permissionMenuCache.get(permission));
+    }
+
     @Override
     public Long createMenu(SysMenuCreateReqVO reqVO) {
         // 校验父菜单存在

+ 95 - 7
src/main/java/cn/iocoder/dashboard/modules/system/service/permission/impl/SysPermissionServiceImpl.java

@@ -2,12 +2,16 @@ package cn.iocoder.dashboard.modules.system.service.permission.impl;
 
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.collection.CollectionUtil;
+import cn.hutool.core.util.ArrayUtil;
+import cn.iocoder.dashboard.framework.mybatis.core.dataobject.BaseDO;
+import cn.iocoder.dashboard.framework.security.core.util.SecurityUtils;
 import cn.iocoder.dashboard.modules.system.dal.mysql.dao.permission.SysRoleMenuMapper;
 import cn.iocoder.dashboard.modules.system.dal.mysql.dao.permission.SysUserRoleMapper;
 import cn.iocoder.dashboard.modules.system.dal.mysql.dataobject.permission.SysMenuDO;
 import cn.iocoder.dashboard.modules.system.dal.mysql.dataobject.permission.SysRoleDO;
 import cn.iocoder.dashboard.modules.system.dal.mysql.dataobject.permission.SysRoleMenuDO;
 import cn.iocoder.dashboard.modules.system.dal.mysql.dataobject.permission.SysUserRoleDO;
+import cn.iocoder.dashboard.modules.system.mq.producer.permission.SysPermissionProducer;
 import cn.iocoder.dashboard.modules.system.service.permission.SysMenuService;
 import cn.iocoder.dashboard.modules.system.service.permission.SysPermissionService;
 import cn.iocoder.dashboard.modules.system.service.permission.SysRoleService;
@@ -18,23 +22,28 @@ import com.google.common.collect.Multimap;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
+import org.springframework.transaction.support.TransactionSynchronization;
+import org.springframework.transaction.support.TransactionSynchronizationManager;
 
 import javax.annotation.PostConstruct;
 import javax.annotation.Resource;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.List;
-import java.util.Set;
+import java.util.*;
 
 /**
  * 权限 Service 实现类
  *
  * @author 芋道源码
  */
-@Service
+@Service("ss") // 使用 Spring Security 的缩写,方便食用
 @Slf4j
 public class SysPermissionServiceImpl implements SysPermissionService {
 
+    /**
+     * 定时执行 {@link #schedulePeriodicRefresh()} 的周期
+     * 因为已经通过 Redis Pub/Sub 机制,所以频率不需要高
+     */
+    private static final long SCHEDULER_PERIOD = 5 * 60 * 1000L;
+
     /**
      * 角色编号与菜单编号的缓存映射
      * key:角色编号
@@ -51,6 +60,10 @@ public class SysPermissionServiceImpl implements SysPermissionService {
      * 这里声明 volatile 修饰的原因是,每次刷新时,直接修改指向
      */
     private volatile Multimap<Long, Long> menuRoleCache;
+    /**
+     * 缓存菜单的最大更新时间,用于后续的增量轮询,判断是否有更新
+     */
+    private volatile Date maxUpdateTime;
 
     @Resource
     private SysRoleMenuMapper roleMenuMapper;
@@ -62,14 +75,22 @@ public class SysPermissionServiceImpl implements SysPermissionService {
     @Resource
     private SysMenuService menuService;
 
+    @Resource
+    private SysPermissionProducer permissionProducer;
+
     /**
      * 初始化 {@link #roleMenuCache} 和 {@link #menuRoleCache} 缓存
      */
     @Override
     @PostConstruct
-    public void init() {
+    public void initLocalCache() {
+        // 获取角色与菜单的关联列表,如果有更新
+        List<SysRoleMenuDO> roleMenuList = this.loadRoleMenuIfUpdate(maxUpdateTime);
+        if (CollUtil.isEmpty(roleMenuList)) {
+            return;
+        }
+
         // 初始化 roleMenuCache 和 menuRoleCache 缓存
-        List<SysRoleMenuDO> roleMenuList = roleMenuMapper.selectList(null);
         ImmutableMultimap.Builder<Long, Long> roleMenuCacheBuilder = ImmutableMultimap.builder();
         ImmutableMultimap.Builder<Long, Long> menuRoleCacheBuilder = ImmutableMultimap.builder();
         roleMenuList.forEach(roleMenuDO -> {
@@ -78,9 +99,32 @@ public class SysPermissionServiceImpl implements SysPermissionService {
         });
         roleMenuCache = roleMenuCacheBuilder.build();
         menuRoleCache = menuRoleCacheBuilder.build();
+        assert roleMenuList.size() > 0; // 断言,避免告警
+        maxUpdateTime = roleMenuList.stream().max(Comparator.comparing(BaseDO::getUpdateTime)).get().getUpdateTime();
         log.info("[initLocalCache][初始化角色与菜单的关联数量为 {}]", roleMenuList.size());
     }
 
+    /**
+     * 如果角色与菜单的关联发生变化,从数据库中获取最新的全量角色与菜单的关联。
+     * 如果未发生变化,则返回空
+     *
+     * @param maxUpdateTime 当前角色与菜单的关联的最大更新时间
+     * @return 角色与菜单的关联列表
+     */
+    private List<SysRoleMenuDO> loadRoleMenuIfUpdate(Date maxUpdateTime) {
+        // 第一步,判断是否要更新。
+        if (maxUpdateTime == null) { // 如果更新时间为空,说明 DB 一定有新数据
+            log.info("[loadRoleMenuIfUpdate][首次加载全量角色与菜单的关联]");
+        } else { // 判断数据库中是否有更新的角色与菜单的关联
+            if (!roleMenuMapper.selectExistsByUpdateTimeAfter(maxUpdateTime)) {
+                return null;
+            }
+            log.info("[loadRoleMenuIfUpdate][增量加载全量角色与菜单的关联]");
+        }
+        // 第二步,如果有更新,则从数据库加载所有角色与菜单的关联
+        return roleMenuMapper.selectList();
+    }
+
     @Override
     public List<SysMenuDO> listRoleMenusFromCache(Collection<Long> roleIds, Collection<Integer> menuTypes,
                                                   Collection<Integer> menusStatuses) {
@@ -140,6 +184,15 @@ public class SysPermissionServiceImpl implements SysPermissionService {
         if (!CollectionUtil.isEmpty(deleteMenuIds)) {
             roleMenuMapper.deleteListByRoleIdAndMenuIds(roleId, deleteMenuIds);
         }
+        // 发送刷新消息. 注意,需要事务提交后,在进行发送刷新消息。不然 db 还未提交,结果缓存先刷新了
+        TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
+
+            @Override
+            public void afterCommit() {
+                permissionProducer.sendRoleMenuRefreshMessage();
+            }
+
+        });
     }
 
     @Override
@@ -189,4 +242,39 @@ public class SysPermissionServiceImpl implements SysPermissionService {
         // TODO 实现我
     }
 
+    @Override
+    public boolean hasPermission(String permission) {
+        return hasAnyPermissions(permission);
+    }
+
+    @Override
+    public boolean hasAnyPermissions(String... permissions) {
+        // 如果为空,说明已经有权限
+        if (ArrayUtil.isEmpty(permissions)) {
+            return true;
+        }
+
+        // 获得当前登陆的角色。如果为空,说明没有权限
+        Set<Long> roleIds = SecurityUtils.getLoginUserRoleIds();
+        if (CollUtil.isEmpty(roleIds)) {
+            return false;
+        }
+        // 判断是否是超管。如果是,当然符合条件
+        if (roleService.hasAnyAdmin(roleIds)) {
+            return true;
+        }
+
+        // 遍历权限,判断是否有一个满足
+        return Arrays.stream(permissions).anyMatch(permission -> {
+            List<SysMenuDO> menuList = menuService.getMenuListByPermissionFromCache(permission);
+            // 采用严格模式,如果权限找不到对应的 Menu 的话,认为
+            if (CollUtil.isEmpty(menuList)) {
+                return false;
+            }
+            // 获得是否拥有该权限,任一一个
+            return menuList.stream().anyMatch(menu -> CollUtil.containsAny(roleIds,
+                    menuRoleCache.get(menu.getId())));
+        });
+    }
+
 }

+ 4 - 0
src/main/java/cn/iocoder/dashboard/modules/system/service/permission/impl/SysRoleServiceImpl.java

@@ -217,6 +217,8 @@ public class SysRoleServiceImpl implements SysRoleService {
         updateObject.setId(id);
         updateObject.setStatus(status);
         roleMapper.updateById(updateObject);
+        // 发送刷新消息
+        roleProducer.sendRoleRefreshMessage();
     }
 
     @Override
@@ -229,6 +231,8 @@ public class SysRoleServiceImpl implements SysRoleService {
         updateObject.setDataScope(dataScope);
         updateObject.setDataScopeDeptIds(dataScopeDeptIds);
         roleMapper.updateById(updateObject);
+        // 发送刷新消息
+        roleProducer.sendRoleRefreshMessage();
     }
 
     /**

+ 1 - 1
src/main/resources/application.yaml

@@ -34,7 +34,7 @@ yudao:
     token-timeout: 1d
     session-timeout: 30m
     mock-enable: true
-    mock-secret: yudaoyuanma
+    mock-secret: test
   swagger:
     title: 管理后台
     description: 提供管理员管理的所有功能