`
cgs1999
  • 浏览: 541424 次
  • 性别: Icon_minigender_1
  • 来自: 上海
社区版块
存档分类
最新评论

使用Java操作LDAP案例

阅读更多
1 案例描述
公司平台使用LDAP来储存企业或用户的信息,在系统的运行过程中需要对LDAP存储的信息进行相关的访问和操作,那么在Java中是如何操作LDAP的呢?

2 案例分析
LDAP是一个得到关于人或者资源的集中、静态数据的快速方式,是一种存储模式和访问协议。
UnboundID LDAP SDK for Java是一个快速、综合易用的 LDAP 目录服务的 Java 客户端API,它提供了一套快速、强大、用户友好并且开源的Java API来与LDAP目录服务器交互,可读写 LDIF、使用BASE64 和 ASN.1 BER 进行编码解码,支持安全通信等特性,要求 Java 1.5 或者更新版本支持,同时也支持 Android 平台。与其它基于Java的LDAP APIs相比,它具有更好的性能、更易于使用,功能更多,而且还是唯一一个不断有活跃开发和增强的SDK。
在软件开发中,对数据的操作无非就是增加、删除、修改、查询等4种操作,对LDAP的操作也一样。下面我们一起探讨在Java中如何使用UnboundID LDAP SDK操作LDAP。

3 解决过程
3.1 准备
1、Java客户端API(UnboundID LDAP SDK for Java)
下载地址 http://sourceforge.net/projects/ldap-sdk/files/

2、LDAP客户端(Apache Directory Studio)
下载地址 http://directory.apache.org/studio/

3.2 定义参数
// 当前配置信息
private static String ldapHost = "172.16.160.196";
private static int ldapPort = 389;
private static String ldapBindDN = "cn=manager,dc=com";;
private static String ldapPassword = "******";
private static LDAPConnection connection = null;

3.3 建立连接
/** 连接LDAP */
public static void openConnection() {
	if (connection == null) {
		try {
			connection = new LDAPConnection(ldapHost, ldapPort, ldapBindDN, ldapPassword);
		} catch (Exception e) {
			System.out.println("连接LDAP出现错误:\n" + e.getMessage());
		}
	}
}

3.4 创建数据
1、创建DC对象
/** 创建DC */
public static void createDC(String baseDN, String dc) {
	String entryDN = "dc=" + dc + "," + baseDN;
	try {
		// 连接LDAP
		openConnection();
		
		SearchResultEntry entry = connection.getEntry(entryDN);
		if (entry == null) {
			// 不存在则创建
			ArrayList<Attribute> attributes = new ArrayList<Attribute>();
			attributes.add(new Attribute("objectClass", "top", "organization", "dcObject"));
			attributes.add(new Attribute("dc", dc));
			attributes.add(new Attribute("o", dc));
			connection.add(entryDN, attributes);
			System.out.println("创建DC" + entryDN + "成功!");
		} else {
			System.out.println("DC " + entryDN + "已存在!");
		}
	} catch (Exception e) {
		System.out.println("创建DC出现错误:\n" + e.getMessage());
	}
}

2、创建组织
/** 创建组织 */
public static void createO(String baseDN, String o) {
	String entryDN = "o=" + o + "," + baseDN;
	try {
		// 连接LDAP
		openConnection();
		
		SearchResultEntry entry = connection.getEntry(entryDN);
		if (entry == null) {
			// 不存在则创建
			ArrayList<Attribute> attributes = new ArrayList<Attribute>();
			attributes.add(new Attribute("objectClass", "top", "organization"));
			attributes.add(new Attribute("o", o));
			connection.add(entryDN, attributes);
			System.out.println("创建组织" + entryDN + "成功!");
		} else {
			System.out.println("组织" + entryDN + "已存在!");
		}
	} catch (Exception e) {
		System.out.println("创建组织出现错误:\n" + e.getMessage());
	}
}

3、创建组织单元
/** 创建组织单元 */
public static void createOU(String baseDN, String ou) {
	String entryDN = "ou=" + ou + "," + baseDN;
	try {
		// 连接LDAP
		openConnection();
		
		SearchResultEntry entry = connection.getEntry(entryDN);
		if (entry == null) {
			// 不存在则创建
			ArrayList<Attribute> attributes = new ArrayList<Attribute>();
			attributes.add(new Attribute("objectClass", "top", "organizationalUnit"));
			attributes.add(new Attribute("ou", ou));
			connection.add(entryDN, attributes);
			System.out.println("创建组织单元" + entryDN + "成功!");
		} else {
			System.out.println("组织单元" + entryDN + "已存在!");
		}
	} catch (Exception e) {
			System.out.println("创建组织单元出现错误:\n" + e.getMessage());
	}
}

4、创建用户
/** 创建用户 */
public static void createEntry(String baseDN, String uid) {
	String entryDN = "uid=" + uid + "," + baseDN;
	try {
		// 连接LDAP
		openConnection();
		
		SearchResultEntry entry = connection.getEntry(entryDN);
		if (entry == null) {
			// 不存在则创建
			ArrayList<Attribute> attributes = new ArrayList<Attribute>();
			attributes.add(new Attribute("objectClass", "top", "account"));
			attributes.add(new Attribute("uid", uid));
			connection.add(entryDN, attributes);
			System.out.println("创建用户" + entryDN + "成功!");
		} else {
			System.out.println("用户" + entryDN + "已存在!");
		}
	} catch (Exception e) {
		System.out.println("创建用户出现错误:\n" + e.getMessage());
	}
}

3.5 修改数据
/** 修改用户信息 */
public static void modifyEntry(String requestDN, Map<String,String> data) {
	try {
		// 连接LDAP
		openConnection();
		
		SearchResultEntry entry = connection.getEntry(requestDN);
		if (entry == null) {
			System.out.println(requestDN + " user:" + requestDN + " 不存在);
			return;
		}
		// 修改信息
		ArrayList<Modification> md = new ArrayList<Modification>();
		for(String key : data.keySet()) {
			md.add(new Modification(ModificationType.REPLACE, key, data.get(key)));
		}
		connection.modify(requestDN, md);

		System.out.println("修改用户信息成!");
	} catch (Exception e) {
		System.out.println("修改用户信息出现错误:\n" + e.getMessage());
	}
}

3.6 删除数据
/** 删除用户信息 */
public static void deleteEntry(String requestDN) {
	try {
		// 连接LDAP
		openConnection();
		
		SearchResultEntry entry = connection.getEntry(requestDN);
		if (entry == null) {
			System.out.println(requestDN + " user:" + requestDN + "不存在");
			return;
		}
		// 删除
		connection.delete(requestDN);
		System.out.println("删除用户信息成!");
	} catch (Exception e) {
		System.out.println("删除用户信息出现错误:\n" + e.getMessage());
	}
}

3.7 查询数据
/** 查询 */
public static void queryLdap(String searchDN, String filter) {
	try {
		// 连接LDAP
		openConnection();
		
		// 查询企业所有用户
		SearchRequest searchRequest = new SearchRequest(searchDN, SearchScope.SUB, "(" + filter + ")");
		searchRequest.addControl(new SubentriesRequestControl());
		SearchResult searchResult = connection.search(searchRequest);
		System.out.println(">>>共查询到" + searchResult.getSearchEntries().size() + "条记录");
		int index = 1;
		for (SearchResultEntry entry : searchResult.getSearchEntries()) {
			System.out.println((index++) + "\t" + entry.getDN());
		}
	} catch (Exception e) {
		System.out.println("查询错误,错误信息如下:\n" + e.getMessage());
	}
}

3.8 测试代码
public static void main(String[] args) {
	String root = "com";
	String dc = "truesens";
	String o = "kedacom";
	String ou = "people";
	String uid = "admin";
	String filter = "objectClass=account";

	createDC("dc=" + root, dc);
	createO("dc=" + dc + ",dc=" + root, o);
	createOU("o=" + o + ",dc=" + dc + ",dc=" + root, ou);
	createEntry("ou=" + ou + ",o=" + o + ",dc=" + dc + ",dc=" + root, uid);
	queryLdap("ou=" + ou + ",o=" + o + ",dc=" + dc + ",dc=" + root, filter);
	
	HashMap<String,String> data = new HashMap<String,String>(0);
	data.put("userid", uid);
modifyEntry("uid="+uid+",ou="+ou+",o="+o+",dc="+dc+",dc="+root, data);
	
	deleteEntry("uid="+uid+",ou="+ou + ",o="+o+",dc=" + dc + ",dc=" + root);
	queryLdap("ou=" + ou + ",o=" + o + ",dc=" + dc + ",dc=" + root, filter);
}


4 解决结果
1、输出结果
创建代理商dc=truesens,dc=com成功!
创建组织o=kedacom,dc=truesens,dc=com成功!
创建组织单元ou=people,o=kedacom,dc=truesens,dc=com成功!
创建用户uid=admin,ou=people,o=kedacom,dc=truesens,dc=com成功!
>>>共查询到1条记录
1	uid=admin,ou=people,o=kedacom,dc=truesens,dc=com
修改用户信息成功!
删除用户信息成功!
>>>共查询到0条记录


2、使用LDAP客户端Apache Directory Studio连接LDAP服务查看LDAP数据,如下图所示


5 总结
从解决过程和解决结果,我们可知在Java中使用UnboundID LDAP SDK for Java所提供的API来操作LDAP,实现对LDAP数据进行增加、修改、删除、查询都很简单和易用。

6 源代码
完整测试代码请查看下面附件
完整代码点击这里下载
  • 大小: 25.8 KB
分享到:
评论
31 楼 cgs1999 2018-04-23  
845896876 写道
老师你好,我发现// 自定义属性 
            attributes.add(new Attribute("enable", enable));
其中的enable这个属性是没有定义的,

给我报错也是属性未定义,请问这个属性是在哪定义的,我的Q是845896876,如果不忙的话可以麻烦您一下么?


由于可以参考一下11楼,自定义属性都是新建的时候加上去的
30 楼 845896876 2018-03-01  
老师你好,我发现// 自定义属性 
            attributes.add(new Attribute("enable", enable));
其中的enable这个属性是没有定义的,

给我报错也是属性未定义,请问这个属性是在哪定义的,我的Q是845896876,如果不忙的话可以麻烦您一下么?
29 楼 cgs1999 2017-09-28  
有网友来信问说unboundid-ldapsdk使用的是什么版本?

本文是2012年写的,当时使用的是unboundid-ldapsdk-2.2.0.jar,最新版本已是4.0.1了,可到http://sourceforge.net/projects/ldap-sdk/files/ 去下载,不过由于版本的问题,可能最新的API已有调整,本文的代码可能在新版本中存在问题,若要使用新版本的网友,请自行查阅相关文档进行调整
28 楼 cgs1999 2014-10-31  
cey009008 写道
请教一下:查询的时候,怎么可以查到一个用户的字段并将其取出来,比如我想查询userPassword字段,希望有知道的游客也可以帮忙回答一下

补充下:就是怎么查到一个用户的所有属性值并获取到


实际上查一下API文档就可以解决你的问题了,参考API文档
http://grepcode.com/file/repo1.maven.org/maven2/com.unboundid/unboundid-ldapsdk/2.3.0/com/unboundid/ldap/sdk/SearchResultEntry.java
http://grepcode.com/file/repo1.maven.org/maven2/com.unboundid/unboundid-ldapsdk/2.3.0/com/unboundid/ldap/sdk/Attribute.java


改造3.7中的查询代码(注:以下代码未经测试)
-------------------------------------------
/** 查询 */
public static void queryLdap(String searchDN, String filter) {
try {
...(略)
for (SearchResultEntry entry : searchResult.getSearchEntries()) {
System.out.println((index++) + "\t" + entry.getDN());

// 获取各字段的值
Collection<Attribute> attributes = entry.getAttributes();
for (Attribute attribute : attributes) {
System.out.println(attribute.getName() + "=" + attribute.getValue());
}

// 获取指定字段的值
if (entry.hasAttribute("userName")) {
System.out.println("userName=" + entry.getAttributeValue("userName"));
}
}
} catch (Exception e) {
System.out.println("查询错误,错误信息如下:\n" + e.getMessage());
}
}
27 楼 cey009008 2014-10-29  
补充下:就是怎么查到一个用户的所有属性值并获取到
26 楼 cey009008 2014-10-29  
请教一下:查询的时候,怎么可以查到一个用户的字段并将其取出来,比如我想查询userPassword字段,希望有知道的游客也可以帮忙回答一下
25 楼 cgs1999 2014-05-28  
路人甲wxf 写道
这个修改LDAP中的密码的话不需要导入证书吗?


在openldap中修改LDAP的连接密码,或修改数据中的密码都不需要导入证书
24 楼 路人甲wxf 2014-05-28  
这个修改LDAP中的密码的话不需要导入证书吗?
23 楼 cgs1999 2014-05-22  
路人甲wxf 写道
我指的是修改LDAP中用户的密码


修改的是LDAP数据,可以直接使用“3.5 修改数据”的方法直接处理,只要将要修改的相关的参数传入即可,参考代码如下:

String requestDN = "uid=admin,ou=people,o=kedacom,dc=truesens,dc=com";
Map<String,String> data = new HashMap<String,String> (0);
data.put("password", "12345678");

modifyEntry(requestDN, data);
22 楼 路人甲wxf 2014-05-22  
我指的是修改LDAP中用户的密码
21 楼 cgs1999 2014-05-21  
路人甲wxf 写道
能说一下怎么修改密码吗??


你是指修改LDAP的密码吗?

我们使用的是openldap,ldap的密码是在安装的时候通过openldap的配置文件slapd.conf设定的,有个配置项rootpw,修改该值就可以了
20 楼 cgs1999 2014-05-21  
Just1n 写道
官网上找到的示例代码,跟楼主这里的不一样。
请问楼主这个是在哪看到的?


不好意思,之前没及时回复,后来给忘了

这个代码是我们之前参考官网的代码,并根据自己的业务开发的代码,同上述类似的代码已在公司的系统中运行
19 楼 路人甲wxf 2014-05-21  
能说一下怎么修改密码吗??
18 楼 Just1n 2014-01-22  
官网上找到的示例代码,跟楼主这里的不一样。
请问楼主这个是在哪看到的?
17 楼 cgs1999 2013-05-01  
Spring_g 写道
楼主 在服务端排序和分页是怎样实现的


排序和分页的处理都是在服务端搜索出所有的数据然后再进行排序或分页的,效率比较低。

现项目由于种种原因(包含由于效率的问题),已不再使用LDAP而改用mysql+memcache方式来实现原有的功能


对于LDAP分页和排序可参考
http://www.doc88.com/p-952216768436.html
16 楼 Spring_g 2013-04-28  
楼主 在服务端排序和分页是怎样实现的
15 楼 cgs1999 2013-04-22  
Spring_g 写道
cgs1999 写道
如前面所提供的范例代码,自定义属性直接加相关的代码即可。

若是自定义objectClass类型,目前我们使用的是创建schema文件的方式在启动时加载,至于动态生成没有使用过~~
我在这边测试的创建自定义的属性 是不能直接添加的,必须添加指定objectclass里配置好的属性,不知道是代码问题 还是版本问题


这个问题没有遇到过,你的问题估计是环境的问题,需结合环境进行排查。

我理解的LDAP中Entry就是一组key-value形成的数据,可以根据需要扩展不同的属性,且不同的Entry的属性也可以不尽相同。
14 楼 Spring_g 2013-04-22  
cgs1999 写道
如前面所提供的范例代码,自定义属性直接加相关的代码即可。

若是自定义objectClass类型,目前我们使用的是创建schema文件的方式在启动时加载,至于动态生成没有使用过~~
我在这边测试的创建自定义的属性 是不能直接添加的,必须添加指定objectclass里配置好的属性,不知道是代码问题 还是版本问题
13 楼 cgs1999 2013-04-22  
Spring_g 写道
要是自定义属性 是不是要写个schema文件在服务启动时加载进去  还是可以动态的生成那?


如前面所提供的范例代码,自定义属性直接加相关的代码即可。

若是自定义objectClass类型,目前我们使用的是创建schema文件的方式在启动时加载,至于动态生成没有使用过~~
12 楼 Spring_g 2013-04-18  
要是自定义属性 是不是要写个schema文件在服务启动时加载进去  还是可以动态的生成那?

相关推荐

    ### 2024年第一季度青岛房地产市场季度简报总结、市场综述

    2024年第一季度,青岛房地产市场经历了显著变化,总体呈现供需双降的趋势。一季度全市商品房新增10,721套,面积约152.04万平方米,同比下降29%;销量为14,936套,面积约200.85万平方米,同比下降38%,成交均价为14,204元/平方米,同比下降2%。土地市场方面,供应总量为39万平方米,同比减少7%,但成交面积为27万平方米,同比增长31%,楼面地价为6,625元/平方米,同比增长253%,土地出让金为17.61亿元,同比增长354%。二手房市场新增挂牌2.9万套,成交13,405套,132.21万平方米,累计挂牌51.70万套,挂牌均价17,800元/平方米。此外,青岛市出台多项政策支持房地产市场平稳健康发展,包括降低房贷利率、优化开发用地土地规划政策、支持房企融资等。这些政策旨在促进市场供需平衡,防止市场大起大落。

    Linux常用命令大全.markdown

    linux常用命令大全

    MATLAB代码,用于模拟具有无限半空间体积导体的电机单元电势(MUP),星号.rar

    1.版本:matlab2014/2019a/2024a 2.附赠案例数据可直接运行matlab程序。 3.代码特点:参数化编程、参数可方便更改、代码编程思路清晰、注释明细。 4.适用对象:计算机,电子信息工程、数学等专业的大学生课程设计、期末大作业和毕业设计。

    空调销售网站策划案例.doc

    空调销售网站策划案例.doc

    全球6G技术大会2024年以用户为中心的6G接入网技术研究白皮书31页.pdf

    全球6G技术大会2024年以用户为中心的6G接入网技术研究白皮书31页.pdf

    简约专业风格毕业答辩模板47个

    简约专业风格毕业答辩模板是一系列专为追求简洁与高效表达的大学生设计的答辩文档模板,共47个。这些模板融合了经典的设计元素与现代审美,强调信息的清晰传递与视觉的整洁,旨在帮助学生在答辩中以最专业的面貌展示自己的研究成果。 每个模板都具备结构合理的布局,适用于各个学科和研究领域,从人文社科到自然科学,均能满足不同需求。简约风格的设计使得学生能够专注于内容本身,避免冗余信息的干扰,提升答辩的专业性和可信度。此外,模板中合理运用的色彩、字体和图表设计,不仅增强了视觉吸引力,也使信息更易于理解。 通过使用这些简约专业风格的毕业答辩模板,毕业生能够自信地呈现自己的学术成果,提升答辩的整体效果,为成功的学术交流打下坚实基础。这些模板是展示个人研究与风格的理想选择。

    【数据集和模型】ChatGPT文本二分类

    由 Epsilon Luoo 在 HC3-Chinese 的基础上进行了一些细微的修改和清洗

    数字人动作捕捉:MATLAB-Kinect骨骼数据实时插值算法.pdf

    文档支持目录章节跳转同时还支持阅读器左侧大纲显示和章节快速定位,文档内容完整、条理清晰。文档内所有文字、图表、函数、目录等元素均显示正常,无任何异常情况,敬请您放心查阅与使用。文档仅供学习参考,请勿用作商业用途。 你是否渴望高效解决复杂的数学计算、数据分析难题?MATLAB 就是你的得力助手!作为一款强大的技术计算软件,MATLAB 集数值分析、矩阵运算、信号处理等多功能于一身,广泛应用于工程、科学研究等众多领域。 其简洁直观的编程环境,让代码编写如同行云流水。丰富的函数库和工具箱,为你节省大量时间和精力。无论是新手入门,还是资深专家,都能借助 MATLAB 挖掘数据背后的价值,创新科技成果。别再犹豫,拥抱 MATLAB,开启你的科技探索之旅!

    HI3519DV500 配置无线网依赖库以及编译脚本

    HI3519DV500 配置无线网依赖库以及编译脚本

    定制小米8-lineage22.1安卓15-fast功能项目线刷双版root 解锁bl后fast线刷

    资源说明; 1-----刷写前提是手机必须解锁bl先。而且会在fast模式刷写固件 2-----刷写方法与官方刷写步骤一样 3-----此固件为定制初始固件。可以在fast模式刷写 4-----属于适配固件。也许有个别bug。不接受请勿下载 5-----需要一定的刷机常识与动手能力的友友刷写。 6-----资源有可复制性。下载后不支持退。请知悉 7-----定制其他需求可以在csdn私信博主 博文参阅:https://csdn9.blog.csdn.net/article/details/143058308

    【机械臂路径规划】基于matlab快速探索随机树RRT和概率路网PRM串联机械臂路径规划【含Matlab源码 13167期】.zip

    Matlab领域上传的视频是由对应的完整代码运行得来的,完整代码皆可运行,亲测可用,适合小白; 1、从视频里可见完整代码的内容 主函数:main.m; 调用函数:其他m文件;无需运行 运行结果效果图; 2、代码运行版本 Matlab 2019b;若运行有误,根据提示修改;若不会,私信博主; 3、运行操作步骤 步骤一:将所有文件放到Matlab的当前文件夹中; 步骤二:双击打开main.m文件; 步骤三:点击运行,等程序运行完得到结果; 4、仿真咨询 如需其他服务,可私信博主; 4.1 博客或资源的完整代码提供 4.2 期刊或参考文献复现 4.3 Matlab程序定制 4.4 科研合作

    世邦魏理仕:2021年西安房地产市场回顾与2022年展望.pdf

    世邦魏理仕:2021年西安房地产市场回顾与2022年展望

    Android Studio 2022.1.1和java编程语言yinyuebofangqi.zip

    Android Studio 2022.1.1和java编程语言yinyuebofangqi

    C知道对话分享图片下载

    C知道对话分享图片

    png-jpg-gif-webp-tiff等图片压缩工具基于nodejs的实现

    png-jpg-gif-webp-tiff等图片压缩工具基于nodejs的实现,绿色本地免安装,解压后运行exe文件,将图片文件或者包含图片的文件夹拖拽到软件界面即可压缩

    派对屋A1效果器电脑调音软件

    我们要了解什么是DSP(Digital Signal Processing)。DSP即数字信号处理,是一种利用数字计算方法对信号进行分析、变换和操作的技术。在汽车音响领域,DSP被广泛应用于改善音质,通过调整频率响应、延时、相位和增益等参数,使声音更加均衡、立体。 惠威是一款数字信号处理器,适用于那些希望升级原车音响系统但预算有限的用户。它通常拥有多个输入和输出接口,可以连接到汽车的音频源和扬声器,通过软件进行调音,使得声音能够适应不同的驾驶环境和听音偏好。 ,集成了先进的噪声抑制技术和强大的功率放大器,旨在为发烧友级别的车载音响系统提供卓越的性能。用户可以通过软件对整个系统的每一个细节进行优化,包括主动分频、时间校正等,以达到Hi-Fi级别的音乐享受。

    通信工程分包合同.docx

    通信工程分包合同.docx

    demo1(1).py

    demo1(1).py

    金融量化交易:MATLAB_构建多因子选股模型的完整开发指南.pdf

    文档支持目录章节跳转同时还支持阅读器左侧大纲显示和章节快速定位,文档内容完整、条理清晰。文档内所有文字、图表、函数、目录等元素均显示正常,无任何异常情况,敬请您放心查阅与使用。文档仅供学习参考,请勿用作商业用途。 你是否渴望高效解决复杂的数学计算、数据分析难题?MATLAB 就是你的得力助手!作为一款强大的技术计算软件,MATLAB 集数值分析、矩阵运算、信号处理等多功能于一身,广泛应用于工程、科学研究等众多领域。 其简洁直观的编程环境,让代码编写如同行云流水。丰富的函数库和工具箱,为你节省大量时间和精力。无论是新手入门,还是资深专家,都能借助 MATLAB 挖掘数据背后的价值,创新科技成果。别再犹豫,拥抱 MATLAB,开启你的科技探索之旅!

    深度学习注意力机制:MATLAB_实现多模态医学影像融合诊断.pdf

    文档支持目录章节跳转同时还支持阅读器左侧大纲显示和章节快速定位,文档内容完整、条理清晰。文档内所有文字、图表、函数、目录等元素均显示正常,无任何异常情况,敬请您放心查阅与使用。文档仅供学习参考,请勿用作商业用途。 你是否渴望高效解决复杂的数学计算、数据分析难题?MATLAB 就是你的得力助手!作为一款强大的技术计算软件,MATLAB 集数值分析、矩阵运算、信号处理等多功能于一身,广泛应用于工程、科学研究等众多领域。 其简洁直观的编程环境,让代码编写如同行云流水。丰富的函数库和工具箱,为你节省大量时间和精力。无论是新手入门,还是资深专家,都能借助 MATLAB 挖掘数据背后的价值,创新科技成果。别再犹豫,拥抱 MATLAB,开启你的科技探索之旅!

Global site tag (gtag.js) - Google Analytics