若依框架 SpringSecurity :动态菜单源码全拆解(附图解)
一、前置信息
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));
}
}
六、图解

七、后续内容
数据权限篇
更多推荐

所有评论(0)