一、前置信息

1.1基础配置篇

若依框架 SpringSecurity :基础配置源码全拆解(附图解)-CSDN博客

1.2 用户登录篇

若依框架 SpringSecurity :用户登录源码全拆解(附图解)-CSDN博客

二、接口总体分析

2.1 获取用户接口分析

    /**
     * 获取用户信息
     * 
     * @return 用户信息
     */
    @GetMapping("getInfo")
    public AjaxResult getInfo()
    {
        LoginUser loginUser = SecurityUtils.getLoginUser();
        SysUser user = loginUser.getUser();
        // 角色集合
        Set<String> roles = permissionService.getRolePermission(user);
        // 权限集合
        Set<String> permissions = permissionService.getMenuPermission(user);
        if (!loginUser.getPermissions().equals(permissions))
        {
            loginUser.setPermissions(permissions);
            tokenService.refreshToken(loginUser);
        }
        AjaxResult ajax = AjaxResult.success();
        ajax.put("user", user);
        ajax.put("roles", roles);
        ajax.put("permissions", permissions);
        ajax.put("isDefaultModifyPwd", initPasswordIsModify(user.getPwdUpdateDate()));
        ajax.put("isPasswordExpired", passwordIsExpiration(user.getPwdUpdateDate()));
        return ajax;
    }
  • 通过 SecurityUtils.getLoginUser() 从安全上下文里获取当前登录的用户信息。

  • 获取角色与权限集合调用 permissionService 分别获取该用户的角色权限集合roles)和菜单权限集合permissions)。

  • 权限更新与 Token 刷新对比当前登录用户缓存中的权限与刚查询到的权限。如果两者不一致,就更新缓存中的权限,并调用 tokenService.refreshToken() 刷新用户的 Token,确保权限变更实时生效。

  • 封装并返回结果创建一个成功的 AjaxResult 响应对象。向对象中存入用户信息、角色、权限。

2.2 获取菜单接口

    /**
     * 获取路由信息
     * 
     * @return 路由信息
     */
    @GetMapping("getRouters")
    public AjaxResult getRouters()
    {
        Long userId = SecurityUtils.getUserId();
        List<SysMenu> menus = menuService.selectMenuTreeByUserId(userId);
        return AjaxResult.success(menuService.buildMenus(menus));
    }
  • 获取当前用户 ID通过 SecurityUtils.getUserId() 从安全上下文中获取当前登录用户的唯一标识 userId

  • 查询用户专属菜单树调用 menuService.selectMenuTreeByUserId(userId),根据用户 ID 查询其有权限访问的菜单列表,返回一个树形结构的 List<SysMenu>

三、获取用户接口详细分析

3.1 从安全上下文中获取用户

打开SecurityUtils

    /**
     * 获取用户
     **/
    public static LoginUser getLoginUser()
    {
        try
        {
            return (LoginUser) getAuthentication().getPrincipal();
        }
        catch (Exception e)
        {
            throw new ServiceException("获取用户信息异常", HttpStatus.UNAUTHORIZED);
        }
    }

    /**
     * 获取Authentication
     */
    public static Authentication getAuthentication()
    {
        return SecurityContextHolder.getContext().getAuthentication();
    }

 在配置篇我们提到过jwt过滤,那个时候会把认证对象从redis中取出放入SecurityContextHolder

/**
 * token过滤器 验证token有效性
 * 
 * @author ruoyi
 */
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter
{
    @Autowired
    private TokenService tokenService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException
    {
        LoginUser loginUser = tokenService.getLoginUser(request);
        if (StringUtils.isNotNull(loginUser) && StringUtils.isNull(SecurityUtils.getAuthentication()))
        {
            tokenService.verifyToken(loginUser);
            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
            authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
            SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        }
        chain.doFilter(request, response);
    }
}

而认证对象会把loginUser放在principal属性中

    public UsernamePasswordAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = principal;
        this.credentials = credentials;
        super.setAuthenticated(true);
    }

因为我们的请求是要经过jwt过滤的,这个时候就会把loginUser存入安全上下文,后续就可以直接SecurityContextHolder.getContext().getAuthentication().getPrincipal()获取。

3.2 用户权限的获取

我们在用户登录的时候已经getMenuPermission一次了,为什么获取用户信息时还要再获取一次?直接用user里面的数据不好吗?

我们知道再redis中存入UserLogin后UserLogin的信息不会变了,就算刷新令牌用的还是原来的UserLogin信息,我们从redis中拿出来的信息可能不是最新最准确的,所以我们要实时再获取一次,并且与redis中的UserLogin信息进行对比,如果不一样就刷新,保证我们获取的是最新的。

四、获取菜单接口详细分析

打开ISysMenuService

    /**
     * 根据用户ID查询菜单
     * 
     * @param userId 用户名称
     * @return 菜单列表
     */
    @Override
    public List<SysMenu> selectMenuTreeByUserId(Long userId)
    {
        List<SysMenu> menus = null;
        if (SecurityUtils.isAdmin(userId))
        {
            menus = menuMapper.selectMenuTreeAll();
        }
        else
        {
            menus = menuMapper.selectMenuTreeByUserId(userId);
        }
        return getChildPerms(menus, MENU_ROOT_ID);
    }

    /**
     * 根据父节点的ID获取所有子节点
     * 
     * @param list 分类表
     * @param parentId 传入的父节点ID
     * @return String
     */
    public List<SysMenu> getChildPerms(List<SysMenu> list, long parentId)
    {
        List<SysMenu> returnList = new ArrayList<SysMenu>();
        for (Iterator<SysMenu> iterator = list.iterator(); iterator.hasNext();)
        {
            SysMenu t = (SysMenu) iterator.next();
            // 一、根据传入的某个父节点ID,遍历该父节点的所有子节点
            if (t.getParentId() == parentId)
            {
                recursionFn(list, t);
                returnList.add(t);
            }
        }
        return returnList;
    }

4.1 分析表结构和实体类

我们的菜单是树性的,在表对应的实体类中还有嵌套结构

4.2 递归封装过程

    /**
     * 递归列表
     * 
     * @param list 分类表
     * @param t 子节点
     */
    private void recursionFn(List<SysMenu> list, SysMenu t)
    {
        // 得到子节点列表
        List<SysMenu> childList = getChildList(list, t);
        t.setChildren(childList);
        for (SysMenu tChild : childList)
        {
            if (hasChild(list, tChild))
            {
                recursionFn(list, tChild);
            }
        }
    }

    /**
     * 判断是否有子节点
     */
    private boolean hasChild(List<SysMenu> list, SysMenu t)
    {
        return getChildList(list, t).size() > 0;
    }
    • 先判断用户是否为管理员:
      • 是管理员 → 获取所有菜单
      • 非管理员 → 获取该用户有权限的菜单
    • 调用 getChildPerms 方法,传入菜单列表和根节点 ID(如 0),开始构建树形结构。
    • 遍历所有菜单,筛选出父节点 ID 等于根节点 ID 的菜单,这些就是树的一级菜单。对每个一级菜单,调用 recursionFn 为它加载子节点。
    • 递归挂载子节点:recursionFn。调用 getChildList,找到当前菜单的所有直接子菜单,将这些子菜单设置到当前菜单的 children 属性中。遍历每个子菜单,调用 hasChild 判断它是否还有子节点。如果有子节点,就递归调用 recursionFn,继续为子菜单挂载它的子节点,直到所有层级处理完毕。

    4.3 构建Vo权限树

    return AjaxResult.success(menuService.buildMenus(menus));
        /**
         * 构建前端路由所需要的菜单
         * 
         * @param menus 菜单列表
         * @return 路由列表
         */
        @Override
        public List<RouterVo> buildMenus(List<SysMenu> menus)
        {
            List<RouterVo> routers = new LinkedList<RouterVo>();
            for (SysMenu menu : menus)
            {
                RouterVo router = new RouterVo();
                router.setHidden("1".equals(menu.getVisible()));
                router.setName(getRouteName(menu));
                router.setPath(getRouterPath(menu));
                router.setComponent(getComponent(menu));
                router.setQuery(menu.getQuery());
                router.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon(), StringUtils.equals("1", menu.getIsCache()), menu.getPath()));
                List<SysMenu> cMenus = menu.getChildren();
                if (StringUtils.isNotEmpty(cMenus) && UserConstants.TYPE_DIR.equals(menu.getMenuType()))
                {
                    router.setAlwaysShow(true);
                    router.setRedirect("noRedirect");
                    router.setChildren(buildMenus(cMenus));
                }
                else if (isMenuFrame(menu))
                {
                    router.setMeta(null);
                    List<RouterVo> childrenList = new ArrayList<RouterVo>();
                    RouterVo children = new RouterVo();
                    children.setPath(menu.getPath());
                    children.setComponent(menu.getComponent());
                    children.setName(getRouteName(menu.getRouteName(), menu.getPath()));
                    children.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon(), StringUtils.equals("1", menu.getIsCache()), menu.getPath()));
                    children.setQuery(menu.getQuery());
                    childrenList.add(children);
                    router.setChildren(childrenList);
                }
                else if (menu.getParentId().intValue() == MENU_ROOT_ID && isInnerLink(menu))
                {
                    router.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon()));
                    router.setPath("/");
                    List<RouterVo> childrenList = new ArrayList<RouterVo>();
                    RouterVo children = new RouterVo();
                    String routerPath = innerLinkReplaceEach(menu.getPath());
                    children.setPath(routerPath);
                    children.setComponent(UserConstants.INNER_LINK);
                    children.setName(getRouteName(menu.getRouteName(), routerPath));
                    children.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon(), menu.getPath()));
                    childrenList.add(children);
                    router.setChildren(childrenList);
                }
                routers.add(router);
            }
            return routers;
        }
    • 遍历处理每个菜单循环输入的 menus 列表,为每个 SysMenu 生成一个对应的 RouterVo 对象。

    • 基础路由属性赋值

      • 设置路由是否隐藏(hidden):通过菜单的 visible 字段判断,"1" 代表隐藏。
      • 设置路由名称(name)、路径(path)、对应组件(component)、查询参数(query)。
      • 设置路由元信息(meta):包含菜单名、图标、是否缓存、原路径等。
    • 递归处理子菜单

      • 如果当前菜单是目录类型(TYPE_DIR)且存在子菜单,就递归调用 buildMenus 处理子菜单列表,生成子路由并挂载到当前路由上,同时设置 alwaysShow: true 确保父菜单始终显示。
      • 如果当前菜单是外链 / 嵌套框架类型(isMenuFrame),则创建一个子路由来承载该页面。
      • 如果当前菜单是根目录下的内部链接(isInnerLink),则生成特殊的内部链接路由结构,路径替换为 / 并指向内部链接组件。
    • 收集路由处理完每个菜单的路由属性后,将生成的 RouterVo 对象添加到最终的路由列表中。

    • 返回结果整个方法执行完毕后,返回完整的路由配置列表,供前端框架直接使用。

    五、用户权限认证过程

    我们几乎在每个controller方法前面都有一个注解,这些注解我们已经在配置篇讲解过。

    下面随便举一个例子。

        /**
         * 获取部门列表
         */
        @PreAuthorize("@ss.hasPermi('system:dept:list')")
        @GetMapping("/list")
        public AjaxResult list(SysDept dept)
        {
            List<SysDept> depts = deptService.selectDeptList(dept);
            return success(depts);
        }

    4.1注解的意义

     此处的@ss指的是PermissionService

    /**
     * RuoYi首创 自定义权限实现,ss取自SpringSecurity首字母
     * 
     * @author ruoyi
     */
    @Service("ss")
    public class PermissionService
    • 直接用 @ss.hasPermi('system:dept:list') 就能完成权限校验,不需要在 Controller 里注入 PermissionService 再调用方法。

    4.2 方法的解读

        /**
         * 验证用户是否具备某权限
         * 
         * @param permission 权限字符串
         * @return 用户是否具备某权限
         */
        public boolean hasPermi(String permission)
        {
            if (StringUtils.isEmpty(permission))
            {
                return false;
            }
            LoginUser loginUser = SecurityUtils.getLoginUser();
            if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getPermissions()))
            {
                return false;
            }
            PermissionContextHolder.setContext(permission);
            return hasPermissions(loginUser.getPermissions(), permission);
        }
    
        /**
         * 判断是否包含权限
         * 
         * @param permissions 权限列表
         * @param permission 权限字符串
         * @return 用户是否具备某权限
         */
        private boolean hasPermissions(Set<String> permissions, String permission)
        {
            return permissions.contains(Constants.ALL_PERMISSION) || permissions.contains(StringUtils.trim(permission));
        }
    
    • 入口校验:先检查权限是否为空、用户是否登录、是否有权限集合。
    • 上下文传递:把要校验的权限存入上下文,便于后续扩展。
    • 核心判断:检查用户是否拥有 “全量权限”,或者直接包含目标权限。
    • 结果返回:返回最终的权限校验结果,供 @PreAuthorize 注解做接口拦截。
    PermissionContextHolder是若依自定义的
    /**
     * 权限信息
     * 
     * @author ruoyi
     */
    public class PermissionContextHolder
    {
        private static final String PERMISSION_CONTEXT_ATTRIBUTES = "PERMISSION_CONTEXT";
    
        public static void setContext(String permission)
        {
            RequestContextHolder.currentRequestAttributes().setAttribute(PERMISSION_CONTEXT_ATTRIBUTES, permission,
                    RequestAttributes.SCOPE_REQUEST);
        }
    
        public static String getContext()
        {
            return Convert.toStr(RequestContextHolder.currentRequestAttributes().getAttribute(PERMISSION_CONTEXT_ATTRIBUTES,
                    RequestAttributes.SCOPE_REQUEST));
        }
    }
    

    六、图解

    七、后续内容

    数据权限篇

    若依框架 SpringSecurity :数据权限源码全拆解(附图解)-CSDN博客

    Logo

    汇聚全球AI编程工具,助力开发者即刻编程。

    更多推荐