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代码来实现这两种算法,并可视化它们的梯度下降过程。 反向传播算法 反向传播算法是深度学习和神经网络训…...

【PDF文件】默认被某种软件打开,如何进行修改?
当有时下载某种软件后,电脑中的PDF文件就默认由该种软件打开,每次需要右键选择打开方式才能选择需要的其他软件打开。如下图所示。 修改方法: (1)点击电脑的“设置”,选择应用 (2)…...

Kaggle Python练习:字符串和字典(Exercise: Strings and Dictionaries)
文章目录 问题:搜索特定单词并定位思路代码实现官方代码代码解析 更进一步 问题:搜索特定单词并定位 一位研究人员收集了数千篇新闻文章。但她想将注意力集中在包含特定单词的文章上。完成以下功能以帮助她过滤文章列表。 您的函数应满足以下条件&…...

React(四) 事件总线,setState的原理,PureComponent优化React性能,ref获取类组件与函数组件
文章目录 一、全局事件总线二、setState的原理1. 为什么要使用setState修改数据2. setState的三种用法(1) 基本使用(2) 传入回调函数(3) setState是一个异步调用 3. setState为什么要设置成异步 二、PureComponent优化性能1. React的diff算法以及Key的优化(扩展)(1) diff算法(2…...

Java学习-JVM
目录 1. 基本常识 1.1 JVM是什么 1.2 JVM架构图 1.3 Java技术体系 1.4 Java与JVM的关系 2. 类加载系统 2.1 类加载器种类 2.2 执行顺序 2.3 类加载四个时机 2.4 生命周期 2.5 类加载途径 2.6 双亲委派模型 3. 运行时数据区 3.1 运行时数据区构成 3.2 堆 3.3 栈…...

leed认证分几个级别
LEED(Leadership in Energy and Environmental Design)认证是一个评估建筑项目可持续性的严格框架,其级别主要分为以下四个: LEED认证(Certified):这是最低级别的认证,要求建筑项目…...

3.C++经典实例-计算一个数的阶乘
阶乘(factorial)是基斯顿卡曼于1808年发明的运算符号,用于表示一个正整数n的所有小于及等于该数的正整数的积。自然数n的阶乘写作n!。例如,5的阶乘表示为5! 1 2 3 4 5 120。 阶乘在数学和计算机科学中有广泛的应用。例如…...

深入理解Qt中的QTableView、Model与Delegate机制
文章目录 显示效果QTableViewModel(模型)Delegate(委托)ITEM控件主函数调用项目下载在Qt中,视图(View)、模型(Model)和委托(Delegate)机制是一种非常强大的架构,它们实现了MVC(模型-视图-控制器)设计模式。这种架构分离了数据存储(模型)、数据展示(视图)和数据操作(委托),使…...

解读《ARM Cortex-M3 与Cortex-M4 权威指南》——第1章 ARM Cortex-M处理器简介
1. 三级流水线设计 解释:三级流水线设计意味着处理器在执行指令时可以同时处理多个步骤。这些步骤通常包括取指(Fetch)、译码(Decode)和执行(Execute)。好处:这种设计提高了指令的执行效率,使得处理器能够在每个时钟周期内完成更多的工作,从而提升整体性能。2. 哈佛总…...

java集合类的框架体系
1.集合的好处 相比数组,他可以存储多种类型的元素,并且可以动态新增; 2. 集合分类 3.Collection接口 3.1常用方法 3.2迭代器-遍历 collection接口继承了Interable接口,collection的子类可以使用迭代器; 注意事项…...

基于SpringBoot+Vue+Uniapp家具购物小程序的设计与实现
详细视频演示 请联系我获取更详细的演示视频 项目运行截图 技术框架 后端采用SpringBoot框架 Spring Boot 是一个用于快速开发基于 Spring 框架的应用程序的开源框架。它采用约定大于配置的理念,提供了一套默认的配置,让开发者可以更专注于业务逻辑而…...