C# 完美操作 Active Directory 详细总结,轻松玩转域管理

前言
嗨,大家好!
在这个数据信息飞速发展的 21 世纪,数据安全成为了每个企业关注的焦点,保护企业数据安全日益成为企业工作中的重中之重。
域服务器,尤其是微软的 Active Directory(AD),因其显著的安全优势,已成为不少企业的首选。
简单来说,域服务器就是一个 “超级管理员”,它可以集中管理网络中的所有用户、计算机和其他资源。
通过域服务器,企业可以轻松地为用户分配和管理权限,确保数据安全;也可以通过组策略,统一管理网络中所有计算机的设置和安全策略,更好地保护敏感信息。
域服务器天然就是一个企业员工信息的数据库,将业务系统的身份鉴权跟域服务器紧密结合,无疑已经成为安全技术发展的趋势。
C# 拥有丰富的类库来与 Active Directory(AD)互动,但使用时很不方便,因此我根据项目的实际业务需求,造了一个轮子,封装了一些常用的操作 AD 的方法,简化了与 AD 的交互,用起来还挺方便的。
今天,我很高兴与大家分享这些便利,希望能让你的开发之旅充满乐趣和效率!
下面,让我们一起来看看具体的实现步骤吧!
Step By Step 代码
1. 创建配置文件
首先,需要创建一个配置文件如 LDAPConfig.config,用于保存域的相关配置信息,内容如下:
<?xml version="1.0" encoding="utf-8" ?>
<LDAPConfiguration><Host><URL>192.168.0.120:389</URL><LoginDN>CN=corp_test,CN=Users,DC=jacky,DC=com</LoginDN><Password>+6nkUhDs5lmcfMYS/qe7Qw==</Password></Host><UserSearch><SearchBase>OU=某某市软件技术有限公司,DC=corp,DC=com</SearchBase><SearchFilter>(&(objectClass=Person)(sAMAccountName={0}))</SearchFilter><UserAttribute>sAMAccountName,memberOf,displayName</UserAttribute></UserSearch><AdminGroup>AndoErp_Administrators</AdminGroup>
</LDAPConfiguration>
2. 创建配置文件实体类
接下来,创建一个配置文件实体类 LDAPConfigModel,读取和解析配置文件中的信息,留意注释
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Serialization;
using Common.Util;namespace Common.Model
{[XmlRoot("LDAPConfiguration")]public class LDAPConfigModel{/// <summary>/// AD/LDAP 服务器和绑定帐号配置/// </summary>[XmlElement("Host")]public LdapHostSetting LdapHost { get; set; }/// <summary>/// 用户搜索配置/// </summary>[XmlElement("UserSearch")]public UserSearchSetting UserSearch { get; set; }/// <summary>/// 管理员组/// </summary>public string AdminGroup { get; set; }}public class LdapHostSetting{/// <summary>/// AD/LDAP 服务器 URL/// </summary>public string URL { get; set; }/// <summary>/// 绑定帐号的 distinguished name/// </summary>public string LoginDN { get; set; }/// <summary>/// 绑定帐号的密码(加密状态)/// </summary>public string Password { get; set; }/// <summary>/// 绑定帐号的密码/// </summary>[XmlIgnore]public string SafePassword{get { return EncryptUtil.AESDecode(Password); }set { Password = EncryptUtil.AESEncode(value); }}}public class UserSearchSetting{/// <summary>/// 搜索路径/// </summary>public string SearchBase { get; set; }/// <summary>/// 搜索过滤器/// </summary>public string SearchFilter { get; set; }/// <summary>/// 搜索属性/// </summary>public string UserAttribute { get; set; }}
}
3. 创建一个域常用操作方法的封装类
然后,创建一个名为 LdapUtil 的静态类,封装所有与域相关的操作方法,重点:留意代码注释
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.DirectoryServices.Protocols;
using Common.Model;
using System.Net;
using Ando.ERP.Logger;
using System.IO;
using System.Reflection;namespace Common.Util
{/// <summary>/// 域 LDAP/AD 常用操作方法封装类/// </summary>public static class LdapUtil{static readonly LDAPConfigModel ldapConfig = null;/// <summary>/// 静态构造方法,读取配置文件,初始化 ldapConfig 对象/// </summary>static LdapUtil(){if (ldapConfig != null) return;string ldapConfigPath;if (AppDomain.CurrentDomain.SetupInformation.PrivateBinPath != null)ldapConfigPath = Path.Combine(AppDomain.CurrentDomain.SetupInformation.PrivateBinPath, "ConfigFile", "LDAPConfig.config");elseldapConfigPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "ConfigFile", "LDAPConfig.config");if (!File.Exists(ldapConfigPath)){Assembly myAssembly = Assembly.GetExecutingAssembly();FileInfo dllFile = new FileInfo(myAssembly.Location);string path = dllFile.Directory.FullName;ldapConfigPath = Path.Combine(path, "ConfigFile", "LDAPConfig.config");}ldapConfig = ConfigUtil.Deserialize<LDAPConfigModel>(ldapConfigPath);}#region common private method/// <summary>/// 连接域服务器/// </summary>/// <param name="loginDN">域用户</param>/// <param name="loginPassword">域用户密码</param>/// <param name="ldapConnection">LdapConnection 对象</param>private static void Connect(string loginDN, string loginPassword, ref LdapConnection ldapConnection){var networkCredential = new NetworkCredential(loginDN, loginPassword);ldapConnection.SessionOptions.SecureSocketLayer = false;ldapConnection.SessionOptions.ProtocolVersion = 3;ldapConnection.AuthType = AuthType.Basic;ldapConnection.Credential = networkCredential;ldapConnection.Bind();}/// <summary>/// <para>通过 LDAP 配置信息连接域服务器</para>/// <para>如果参数 cLoginDN 不为空,使用 cLoginDN 作为 loginDN</para>/// <para>如果参数 cLoginPassword 不为空,使用 cLoginPassword 作为 loginPassword</para>/// </summary>/// <param name="ldapConfig"></param>/// <param name="ldapConnection"></param>/// <param name="cLoginDN"></param>/// <param name="cLoginPassword"></param>private static void Connect(ref LdapConnection ldapConnection, string cLoginDN = "", string cLoginPassword = ""){string loginDN = string.IsNullOrEmpty(cLoginDN) ? ldapConfig.LdapHost.LoginDN : cLoginDN;string loginPassword = string.IsNullOrEmpty(cLoginPassword) ? ldapConfig.LdapHost.SafePassword : cLoginPassword;AndoErpLogger.DEFAULT.DebugFormat("Connecting to LDAP/AD server [{0}] with account [{1}]", ldapConfig.LdapHost.URL, loginDN);Connect(loginDN, loginPassword, ref ldapConnection);}/// <summary>/// 通过 Action 执行一些自定义操作,LDAP Util 核心方法/// </summary>/// <param name="ldapConfig"></param>/// <param name="cLoginDN"></param>/// <param name="cLoginPassword"></param>/// <param name="func"></param>private static void LDAPCore(string cLoginDN, string cLoginPassword, Action<LdapConnection> action){LdapConnection ldapConnection = null;try{ldapConnection = new LdapConnection(ldapConfig.LdapHost.URL);Connect(ref ldapConnection, cLoginDN, cLoginPassword);action(ldapConnection);}catch{throw;}finally{if (ldapConnection != null) ldapConnection.Dispose();}}/// <summary>/// 通过用户搜索配置搜索域数据/// </summary>/// <param name="ldapConnection"></param>/// <param name="sizeLimit">指定返回的实体数</param>/// <param name="searchPath">搜索路径</param>/// <param name="filter">过滤字符串</param>/// <param name="isSubtree">是否深度搜索</param>/// <param name="attrs">搜索属性</param>private static SearchResultEntryCollection Search(LdapConnection ldapConnection, int sizeLimit, string searchPath, string filter, params string[] attrs){try{SearchRequest request = new SearchRequest(searchPath, filter, SearchScope.Subtree, attrs);if (sizeLimit > 0) request.SizeLimit = sizeLimit;SearchResponse response = (SearchResponse)ldapConnection.SendRequest(request);return response.Entries;}catch (DirectoryOperationException e){// 返回此异常中已处理的所有数据// 因为 LDAP/AD 搜索默认在 1000 以内,超过就会报这个错SearchResponse response = (SearchResponse)e.Response;return response.Entries;}catch (Exception ex){throw;}}#endregion#region business method/// <summary>/// 获取指定域用户的 distinguished name 值/// </summary>/// <param name="ldapConfig"></param>/// <param name="userName"></param>/// <returns></returns>private static LdpaUserInfo GetLDAPUserDN(string userName){var ldpaUser = new LdpaUserInfo();LDAPCore(null, null, (ldapConnection) =>{var userEntries = GetUserEntries(ldapConnection, userName, 50);var userEntry = userEntries[0];ldpaUser.UserDN = userEntry.DistinguishedName;ldpaUser.UserGroupList = GetAttributeValues(userEntry, "memberOf");ldpaUser.UserDisplayName = GetAttributeValues(userEntry, "displayName")[0];});return ldpaUser;}/// <summary>/// 获取用户搜索配置中的所有域帐户实体/// </summary>/// <param name="ldapConfig"></param>/// <param name="ldapConnection"></param>/// <param name="userName"></param>/// <param name="sizeLimit"></param>/// <returns></returns>private static SearchResultEntryCollection GetUserEntries(LdapConnection ldapConnection, string userName, int sizeLimit){string userSearchBasePath = ldapConfig.UserSearch.SearchBase;string userSearchFilter = ldapConfig.UserSearch.SearchFilter;string[] userAttribute = ldapConfig.UserSearch.UserAttribute.Split(',');if (!string.IsNullOrEmpty(userName)){userSearchFilter = string.Format(userSearchFilter, userName);}var userEntries = Search(ldapConnection, sizeLimit, userSearchBasePath, userSearchFilter, userAttribute);if (userEntries == null || userEntries.Count == 0){string exceptionMsg = string.Format("没有找到符合条件的域用户,请检查用户搜索配置。搜索路径: [{0}], 过滤条件: [{1}]", userSearchBasePath, userSearchFilter);throw new LdapException(exceptionMsg);}return userEntries;}/// <summary>/// 获取域帐户实体配置中的属性的值/// </summary>/// <param name="entry"></param>/// <param name="attributeName"></param>/// <returns></returns>private static List<string> GetAttributeValues(SearchResultEntry entry, string attributeName){var attributes = entry.Attributes;var attributeObj = attributes[attributeName];if (attributeObj == null){return null;}List<string> valueList = new List<string>(); var attributeValues = attributes[attributeName].GetValues(typeof(string));foreach (var attributeValue in attributeValues){valueList.Add(CommonUtil.TranNull<string>(attributeValue));}return valueList;}/// <summary>/// 检查登录帐户是否存在域中/// </summary>/// <param name="userName"></param>/// <param name="password"></param>/// <param name="ldapConfig"></param>/// <returns></returns>public static bool IsExistLDAPUser(string userName, string password, out bool isAdministrator, out string userDisplayName){bool result = false;bool userIsAdmin = false;string userShowName = string.Empty;try{var ldpaUser = GetLDAPUserDN(userName);string userDN = ldpaUser.UserDN;userShowName = ldpaUser.UserDisplayName;LDAPCore(userDN, password, (ldapConnection) =>{if (ldpaUser.UserGroupList == null || ldpaUser.UserGroupList.Count == 0)userIsAdmin = false;else{var findResult = ldpaUser.UserGroupList.First(x => x.IndexOf(ldapConfig.AdminGroup, StringComparison.OrdinalIgnoreCase) > 0);if (string.IsNullOrEmpty(findResult))userIsAdmin = false;elseuserIsAdmin = true;}result = true;});}catch (Exception ex){// TODO 可将错误信息写到日志中,方便排查原因result = false;}isAdministrator = userIsAdmin;userDisplayName = userShowName;return result;}/// <summary>/// 获取用户搜索配置中的所有域帐户的 displayName 的值/// </summary>/// <param name="ldapConfig"></param>/// <param name="userName"></param>/// <returns></returns>public static List<string> GetAllUsers(){var list = new List<string>();LDAPCore(null, null, (ldapConnection) =>{var userEntries = GetUserEntries(ldapConnection, "*", 350);foreach (var userEntry in userEntries){var userDisplayName = GetAttributeValues((SearchResultEntry)userEntry, "displayName")[0];list.Add(userDisplayName);}});return list;}#endregion#region Inner class/// <summary>/// 域用户基本信息/// </summary>sealed class LdpaUserInfo{public string UserDN { get; set; }public List<string> UserGroupList { get; set; }public string UserDisplayName { get; set; }}#endregion}
}
4. 使用示例
最后,我们来看一下如何使用这个封装类来执行一些基本操作,比如登录
/// <summary>
/// 登录
/// </summary>
/// <param name="userName"></param>
/// <param name="password"></param>
/// <returns></returns>
public void Login(string userName, string password)
{var hasUser = LdapUtil.IsExistLDAPUser(userName, password, out bool isAdministrator, out string userDisplayName);if (hasUser){// 域用户存在,登录成功,继续处理后续业务}else{// 域用户不存在,登录失败}
}
总结
好了,今天的分享就到这里啦!
通过以上的封装,我们可以更高效地与 Active Directory 进行交互,无论是用户的身份验证、信息查询,还是其他操作,这些方法都能帮助简化代码,提高开发效率,你可以把它用在自己的项目里,根据自己的实际业务需求,继续添加新的业务处理方法或删减其中一些方法!
随着数字化进程的加速,越来越多的企业转向域服务器来高效管理网络环境,C# 的灵活性和强大功能使其与 Active Directory 的结合成为了一种自然的选择,希望这篇教程能够给你提供一些实用的操作方法和思路。
最后,如果你有更好的想法或建议,欢迎留言讨论!
往期精彩
- 闲话 .NET(7):.NET Core 能淘汰 .NET FrameWork 吗?
- 常用的 4 种 ORM 框架(EF Core,SqlSugar,FreeSql,Dapper)对比总结
我是老杨,一个执着于编程乐趣、至今奋斗在一线的 10年+ 资深研发老鸟,是软件项目管理师,也是快乐的程序猿,持续免费分享全栈实用编程技巧、项目管理经验和职场成长心得!欢迎关注老杨的公众号,更多干货等着你!
相关文章:
C# 完美操作 Active Directory 详细总结,轻松玩转域管理
前言 嗨,大家好! 在这个数据信息飞速发展的 21 世纪,数据安全成为了每个企业关注的焦点,保护企业数据安全日益成为企业工作中的重中之重。 域服务器,尤其是微软的 Active Directory(AD)&…...
PCL 点云配准 KD-ICP算法(精配准)
目录 一、概述 1.1原理 1.2实现步骤 1.3应用场景 二、代码实现 2.1关键函数 2.1.1 加载点云函数 2.1.2 构建KD树函数 2.1.3 KD-ICP配准函数 2.1.4 点云可视化函数 2.2完整代码 三、实现效果 PCL点云算法汇总及实战案例汇总的目录地址链接: PCL点云算法…...
uniapp打包安卓apk步骤
然后安装在手机上就可以啦...
Springboot 整合 Java DL4J 实现安防监控系统
🧑 博主简介:历代文学网(PC端可以访问:https://literature.sinhy.com/#/literature?__c1000,移动端可微信小程序搜索“历代文学”)总架构师,15年工作经验,精通Java编程,…...
【数据结构与算法】第1课—算法复杂度
文章目录 1. 数据结构2. 算法3. 算法效率4. 算法复杂度5. 算法时间复杂度5.1 大O的渐进表示法5.2 时间复杂度示例 6. 空间复杂度6.1 练习16.2 练习26.3 练习3 1. 数据结构 数据结构是计算机存储、组织数据的方式,指相互之间存在一种和多种特定关系的数据元素的集合&…...
利用高德API获取整个城市的公交路线并可视化(五)
如果说我比别人看得更远些,那是因为我站在了巨人的肩上。——牛顿 参考:使用高德API获取公交线路数据,无需代码_实时公交api-CSDN博客 记录于2024年10月,因数据获取受网站更新策略等影响可能会失效,故记录写作时间,同时拾人牙慧,优化了后半部分数据直接导出为csv和shp…...
DNS:互联网域名系统的核心
什么是 DNS? DNS(Domain Name System,域名系统)是互联网的一项基础服务,它负责将人类容易记忆的域名(如 www.example.com)转换成计算机可以识别的 IP 地址(如 192.0.2.1)…...
小猿口算炸鱼脚本
目录 写在前面: 一、关于小猿口算: 二、代码逻辑 1.数字识别 2.答题部分 三、代码分享: 补充:软件包下载 写在前面: 最近小猿口算已经被不少大学生攻占,小学生直呼有挂。原本是以为大学生都打着本…...
浅谈云原生--微服务、CICD、Serverless、服务网格
往期推荐 浅学React和JSX-CSDN博客 一文搞懂大数据流式计算引擎Flink【万字详解,史上最全】-CSDN博客 一文入门大数据准流式计算引擎Spark【万字详解,全网最新】_大数据 spark-CSDN博客 目录 1. 云原生概念和特点 2. 常见云模式 3. 云对外提供服务的…...
android app执行shell命令视频课程补充android 10/11适配-千里马android
(https://blog.csdn.net/learnframework/article/details/120103471) https://blog.csdn.net/learnframework/article/details/120103471 hi,有学员在学习跨进程通信专题课程时候,在实战app执行一个shell命令的项目时候,对课程本身的android …...
C++笔记-UTF8和UTF8-dom的区别
在文件格式上,UTF-8 和 UTF-8-BOM 是两种不同的编码方式,其中 UTF-8-BOM 包含字节顺序标记(BOM),而 UTF-8 则不包含。 UTF-8: UTF-8 是一种以字节为单位的可变长度字符编码,常用于以字节为单位…...
“探索Adobe Photoshop 2024:订阅方案、成本效益分析及在线替代品“
设计师们对Adobe Photoshop这款业界领先的图像编辑软件肯定不会陌生。如果你正考虑加入Photoshop的用户行列,可能会对其价格感到好奇。Photoshop的价值在于其强大的功能,而它的价格也反映了这一点。下面,我们就来详细了解一下Adobe Photoshop…...
网页复制粘贴助手,Chrome网页复制插件(谷歌浏览器复制插件)
一款解决网页限制复制问题的插件,当你遇到限制复制粘贴和右键的网页是不是很头痛?安装这个插件后,点下插件按钮就能解决了 碰到这种情况 也是非常头疼 chrome拓展-chrome插件-强制复制 当我们浏览网页的时候,看到感兴趣的内容就…...
【C++刷题】力扣-#118-杨辉三角
题目描述 给定一个非负整数 numRows,生成杨辉三角的前 numRows 行。在杨辉三角中,每个数是它正上方两个数的和。 示例 示例 1: 输入: numRows 5 输出: [[1],[1,1],[1,2,1],[1,3,3,1],[1,4,6,4,1]]示例 2: 输入: numRows 1 输出: [[1]]题解 这个问题…...
Linux下的环境变量
目录 1.引言 1.1bash的部分工作 1.2main函数也有参数 1.3我们可以通过给main函数传入不同的参数,让同一份代码实现不同的功能 1.4先认识一个环境变量PATH,帮助Linux找到指令程序的地址 2.环境变量 2.1环境变量的概念 2.2见见其他的环境变量 2…...
Edge论文的创新点
创新点及其来源 1. 从灰度边缘重建RGB图像的方法(EdgRec) 基于的方法:传统的重建方法,如使用自动编码器或生成模型来重建正常样本的图像,并通过对原始图像和重建图像的比较来检测异常。 重建过程: 训练阶…...
ComfyUI 高级实战:实现华为手机的AI消除功能
大家好,我是每天分享AI应用的萤火君! 不知道大家是否还记得华为 Pura 70的「AI消除」事件,当时使用 华为Pura 70 系列手机的智能消除功能时,该功能可以被用来消除照片中女性胸口处的衣物,这一功能曾引发广泛的关注和伦…...
我记得我曾喜欢过冬天
写在前面 1316 字 | 感触 | 世界 | 情感 | 体验 | 经历 | 想法 | 认知 正文 晚上出门,起电单车,很冷。冻得有些发抖。下车,我第一时间和珍发了消息。 我说,居然在四川感受到了哈尔滨的温度。 哈尔滨的夏天很热,但哈尔…...
最新夜间数据集发布LoLI-Street: 33000帧数据,涵盖19000个目标
最新夜间数据集发布LoLI-Street: 33000帧数据,涵盖19000个目标 Abstract 低光照图像增强(LLIE)对于许多计算机视觉任务至关重要,包括目标检测、跟踪、分割和场景理解。尽管已有大量研究致力于提高在低光照条件下捕捉的低质量图像…...
反向传播算法与随机搜索算法的比较
反向传播算法与随机搜索算法的比较 在这篇文章中,我们将通过一个简单的线性回归问题来比较反向传播算法和随机搜索算法的性能。我们将使用Python代码来实现这两种算法,并可视化它们的梯度下降过程。 反向传播算法 反向传播算法是深度学习和神经网络训…...
未来机器人的大脑:如何用神经网络模拟器实现更智能的决策?
编辑:陈萍萍的公主一点人工一点智能 未来机器人的大脑:如何用神经网络模拟器实现更智能的决策?RWM通过双自回归机制有效解决了复合误差、部分可观测性和随机动力学等关键挑战,在不依赖领域特定归纳偏见的条件下实现了卓越的预测准…...
日语AI面试高效通关秘籍:专业解读与青柚面试智能助攻
在如今就业市场竞争日益激烈的背景下,越来越多的求职者将目光投向了日本及中日双语岗位。但是,一场日语面试往往让许多人感到步履维艰。你是否也曾因为面试官抛出的“刁钻问题”而心生畏惧?面对生疏的日语交流环境,即便提前恶补了…...
三维GIS开发cesium智慧地铁教程(5)Cesium相机控制
一、环境搭建 <script src"../cesium1.99/Build/Cesium/Cesium.js"></script> <link rel"stylesheet" href"../cesium1.99/Build/Cesium/Widgets/widgets.css"> 关键配置点: 路径验证:确保相对路径.…...
ssc377d修改flash分区大小
1、flash的分区默认分配16M、 / # df -h Filesystem Size Used Available Use% Mounted on /dev/root 1.9M 1.9M 0 100% / /dev/mtdblock4 3.0M...
Golang dig框架与GraphQL的完美结合
将 Go 的 Dig 依赖注入框架与 GraphQL 结合使用,可以显著提升应用程序的可维护性、可测试性以及灵活性。 Dig 是一个强大的依赖注入容器,能够帮助开发者更好地管理复杂的依赖关系,而 GraphQL 则是一种用于 API 的查询语言,能够提…...
高等数学(下)题型笔记(八)空间解析几何与向量代数
目录 0 前言 1 向量的点乘 1.1 基本公式 1.2 例题 2 向量的叉乘 2.1 基础知识 2.2 例题 3 空间平面方程 3.1 基础知识 3.2 例题 4 空间直线方程 4.1 基础知识 4.2 例题 5 旋转曲面及其方程 5.1 基础知识 5.2 例题 6 空间曲面的法线与切平面 6.1 基础知识 6.2…...
BCS 2025|百度副总裁陈洋:智能体在安全领域的应用实践
6月5日,2025全球数字经济大会数字安全主论坛暨北京网络安全大会在国家会议中心隆重开幕。百度副总裁陈洋受邀出席,并作《智能体在安全领域的应用实践》主题演讲,分享了在智能体在安全领域的突破性实践。他指出,百度通过将安全能力…...
今日科技热点速览
🔥 今日科技热点速览 🎮 任天堂Switch 2 正式发售 任天堂新一代游戏主机 Switch 2 今日正式上线发售,主打更强图形性能与沉浸式体验,支持多模态交互,受到全球玩家热捧 。 🤖 人工智能持续突破 DeepSeek-R1&…...
DeepSeek 技术赋能无人农场协同作业:用 AI 重构农田管理 “神经网”
目录 一、引言二、DeepSeek 技术大揭秘2.1 核心架构解析2.2 关键技术剖析 三、智能农业无人农场协同作业现状3.1 发展现状概述3.2 协同作业模式介绍 四、DeepSeek 的 “农场奇妙游”4.1 数据处理与分析4.2 作物生长监测与预测4.3 病虫害防治4.4 农机协同作业调度 五、实际案例大…...
Mysql中select查询语句的执行过程
目录 1、介绍 1.1、组件介绍 1.2、Sql执行顺序 2、执行流程 2.1. 连接与认证 2.2. 查询缓存 2.3. 语法解析(Parser) 2.4、执行sql 1. 预处理(Preprocessor) 2. 查询优化器(Optimizer) 3. 执行器…...
