You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何通过ADAL JS获取当前用户角色并实现React侧边栏菜单权限控制

实现Azure AD角色获取与侧边栏权限控制

我来帮你一步步搞定这个需求,基于你现有的react-adal集成和Redux配置,咱们分几个核心步骤来实现:

1. 从Azure AD解析用户角色

react-adal的authContext已经缓存了用户的id token,我们可以从中提取角色信息。首先在adalConfig.js里加一个解析token的工具函数:

// adalConfig.js
import { AuthenticationContext, adalFetch, withAdalLogin } from 'react-adal';

// 你的原有配置
export const adalConfig = { 
  tenant: 'abc-af96-4f7c-82db-b6f0bd7ae9b6', 
  clientId: 'abc-969c-49b2-8a58-78eece990daf', 
  endpoints: { 
    api:'abc-083c-4c10-b40f-f1d764319b21',
    apiUrl: 'https://abc.azurewebsites.net/api' 
  },
  cacheLocation: 'localStorage' 
}; 

export const authContext = new AuthenticationContext(adalConfig); 

// 新增:解析id token获取用户角色
export const getUserRoles = () => {
  const user = authContext.getCachedUser();
  if (!user) return [];
  
  // 解析token的payload部分(JWT的第二段)
  const tokenPayload = JSON.parse(atob(user.idToken.split('.')[1]));
  // Azure AD的角色默认在`roles`字段里,若用组映射角色需看`groups`
  return tokenPayload.roles || [];
};

// 你的原有导出
export const adalApiFetch = (fetch, url, options) => adalFetch(authContext, adalConfig.endpoints.api, fetch, adalConfig.apiUrl+url, options); 
export const withAdalLoginApi = withAdalLogin(authContext, adalConfig.endpoints.api); 

2. 把角色存入Redux状态

你的项目已经在用Redux管理登录状态,我们把角色也加入状态管理。首先更新Auth reducer:

// 假设你的reducer在redux/auth/reducer.js
const initialState = {
  idToken: null,
  roles: [] // 新增角色数组
};

export default (state = initialState, action) => {
  switch (action.type) {
    case 'SET_ID_TOKEN':
      return { ...state, idToken: action.payload };
    case 'SET_USER_ROLES': // 新增处理角色的action
      return { ...state, roles: action.payload };
    default:
      return state;
  }
};

然后定义对应的action:

// redux/auth/actions.js
export const setUserRoles = (roles) => ({
  type: 'SET_USER_ROLES',
  payload: roles
});

3. 在认证完成后获取角色

在用户登录成功后,调用工具函数获取角色并更新Redux。比如在App.js(路由中RestrictedRoute对应的组件)的挂载阶段:

// containers/App/App.js
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { setUserRoles } from '../../redux/auth/actions';
import { getUserRoles } from '../../adalConfig';

class App extends Component {
  componentDidMount() {
    // 获取当前用户角色并更新Redux
    const userRoles = getUserRoles();
    this.props.setUserRoles(userRoles);
  }

  render() {
    return (
      // 你的原有App内容,比如包含侧边栏和路由的布局
    );
  }
}

export default connect(null, { setUserRoles })(App);

4. 根据角色过滤侧边栏菜单

接下来修改侧边栏组件,根据Redux里的角色控制菜单项显示。首先给菜单配置options.js添加权限规则:

// sidebar/options.js
export default [
  {
    key: 'dashboard',
    title: <IntlMessages id="sidebar.dashboard" />,
    icon: 'dashboard',
    path: '/dashboard',
    requiredRoles: ['Admin', 'RegularUser'] // 这两个角色都能看到
  },
  {
    key: 'admin-settings',
    title: <IntlMessages id="sidebar.adminSettings" />,
    icon: 'setting',
    path: '/admin',
    requiredRoles: ['Admin'] // 只有Admin能看到
  },
  {
    key: 'profile',
    title: <IntlMessages id="sidebar.profile" />,
    icon: 'user',
    path: '/profile'
    // 无权限要求,所有登录用户可见
  }
];

然后修改Sidebar组件,添加过滤逻辑:

// sidebar/index.js
import React, { Component } from "react";
import { connect } from "react-redux";
import clone from "clone";
import { Link } from "react-router-dom";
import { Layout } from "antd";
import options from "./options";
import Scrollbars from "../../components/utility/customScrollBar.js";
import Menu from "../../components/uielements/menu";
import IntlMessages from "../../components/utility/intlMessages";
import SidebarWrapper from "./sidebar.style";
import appActions from "../../redux/app/actions";
import Logo from "../../components/utility/logo";
import themes from "../../settings/themes";
import { themeConfig } from "../../settings";

const SubMenu = Menu.SubMenu;
const { Sider } = Layout;
const { toggleOpenDrawer, changeOpenKeys, changeCurrent, toggleCollapsed } = appActions;

const stripTrailingSlash = str => {
  if (str.substr(-1) === "/") {
    return str.substr(0, str.length - 1);
  }
  return str;
};

class Sidebar extends Component {
  constructor(props) {
    super(props);
    this.handleClick = this.handleClick.bind(this);
    this.onOpenChange = this.onOpenChange.bind(this);
    this.renderMenuItems = this.renderMenuItems.bind(this);
  }

  handleClick(e) {
    this.props.changeCurrent([e.key]);
    if (this.props.app.view === "MobileView") {
      setTimeout(() => {
        this.props.toggleCollapsed();
        this.props.toggleOpenDrawer();
      }, 100);
    }
  }

  onOpenChange(newOpenKeys) {
    // 你的原有逻辑
    const { app, changeOpenKeys } = this.props;
    const latestOpenKey = newOpenKeys.find(key => !app.openKeys.includes(key));
    const latestCloseKey = app.openKeys.find(key => !newOpenKeys.includes(key));
    let nextOpenKeys = [];
    if (latestOpenKey) {
      nextOpenKeys = this.getAncestorKeys(latestOpenKey).concat(latestOpenKey);
    }
    if (latestCloseKey) {
      nextOpenKeys = this.getAncestorKeys(latestCloseKey);
    }
    changeOpenKeys(nextOpenKeys);
  }

  // 新增:过滤并渲染菜单项
  renderMenuItems() {
    const { roles } = this.props;
    // 克隆原配置避免修改源数据
    const filteredOptions = clone(options).filter(option => {
      // 无权限要求的菜单项直接显示
      if (!option.requiredRoles) return true;
      // 检查用户是否拥有至少一个所需角色
      return option.requiredRoles.some(role => roles.includes(role));
    });

    return filteredOptions.map(option => {
      // 处理带子菜单的项
      if (option.children) {
        // 过滤子菜单
        const filteredChildren = option.children.filter(child => {
          if (!child.requiredRoles) return true;
          return child.requiredRoles.some(role => roles.includes(role));
        });
        // 子项全被过滤则不显示父菜单
        if (filteredChildren.length === 0) return null;

        return (
          <SubMenu 
            key={option.key} 
            title={
              <span>
                {option.icon && <i className={`icon ${option.icon}`} />}
                <span>{option.title}</span>
              </span>
            }
          >
            {filteredChildren.map(child => (
              <Menu.Item key={child.key}>
                <Link to={child.path}>{child.title}</Link>
              </Menu.Item>
            ))}
          </SubMenu>
        );
      }

      // 普通菜单项
      return (
        <Menu.Item key={option.key}>
          <Link to={option.path}>
            {option.icon && <i className={`icon ${option.icon}`} />}
            <span>{option.title}</span>
          </Link>
        </Menu.Item>
      );
    });
  }

  render() {
    return (
      <SidebarWrapper>
        <Sider>
          <Logo />
          <Scrollbars>
            <Menu
              onClick={this.handleClick}
              mode="inline"
              openKeys={this.props.app.openKeys}
              selectedKeys={this.props.app.current}
              theme={themeConfig.theme}
            >
              {/* 调用渲染方法 */}
              {this.renderMenuItems()}
            </Menu>
          </Scrollbars>
        </Sider>
      </SidebarWrapper>
    );
  }
}

// 连接Redux,获取用户角色
const mapStateToProps = state => ({
  app: state.App,
  roles: state.Auth.roles // 对应Auth reducer里的roles字段
});

export default connect(mapStateToProps, { 
  toggleOpenDrawer, 
  changeOpenKeys, 
  changeCurrent, 
  toggleCollapsed 
})(Sidebar);

关键注意点

  • 确认Azure AD应用清单里的角色已正确分配给用户/组,且id token包含roles声明。若未显示,检查应用清单的optionalClaims,确保将roles添加到id token声明中。
  • 若通过Azure AD组实现角色,id token会返回组的对象ID,需在前端维护组ID到角色名称的映射表后再进行过滤。

内容的提问来源于stack exchange,提问作者Luis Valencia

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.29 07:54:11