Golang对接Ldap(保姆级教程:概念搭建实战)

Golang对接Ldap(保姆级教程:概念&搭建&实战)

最近项目需要对接客户的LDAP服务,于是趁机好好了解了一下。LDAP实际是一个协议,对应的实现,大家可以理解为一个轻量级数据库。用户查询。比如:我要查询某个用户有没有对应的访问权限。

  • Windows的AD域就是LDAP的一个具体实现,当然AD域除了实现LDAP还实现了其他协议。

🚄本文教程所用代码地址:https://github.com/ziyifast/ziyifast-code_instruction/tree/main/ldap_demo

0 Ldap(Light Directory Access Portocol)

我们日常的办公系统是不是有多个?每个系统之间是不是都有独立的账号密码?密码多了,有时候半天想不起来哪个密码对应哪个系统?

  • 如今大家再也不用为上面的的问题头疼了,因为“LDAP统一认证服务”已经帮助大家解决这些问题了

1 LDAP(轻量级目录访问协议,查询快,特殊的数据库)

LDAP(Light Directory Access Portocol),它是基于X.500标准的轻量级目录访问协议。

  • 是一个为查询、浏览和搜索而优化的数据库,它成树状结构组织数据,类似文件目录一样。
  • 目录数据库和关系数据库不同,它有优异的读性能,但写性能差,并且没有事务处理、回滚等复杂功能,不适于存储修改频繁的数据。所以目录天生是用来查询的,就好象它的名字一样。

LDAP目录服务是由目录数据库和一套访问协议组成的系统。

2 LDAP主流厂商

细心的朋友应该会主要到,LDAP的中文全称是:轻量级目录访问协议,说到底LDAP仅仅是一个访问协议,那么我们的数据究竟存储在哪里呢?
在这里插入图片描述

3 核心概念&术语

核心概念:

  1. 目录树:在一个目录服务系统中,整个目录信息集可以表示为一个目录信息树,树中的每个节点是一个条目。
  2. 条目:每个条目就是一条记录,每个条目有自己的唯一可区别的名称(DN)。
  3. 对象类:与某个实体类型对应的一组属性,对象类是可以继承的,这样父类的必须属性也会被继承下来。
  4. 属性:描述条目的某个方面的信息,一个属性由一个属性类型和一个或多个属性值组成,属性有必须属性和非必须属性。
①dc(Domain Component):域名部分,dc=com

域名的部分,其格式是将完整的域名分成几部分,如域名为example.com变成dc=example,dc=com(一条记录的所属位置)

②uid(User Id):用户Id,uid=ziyi.zhou

用户ID ziyi.zhou(一条记录的ID)

③ou(Organization Unit):组织,ou=develop

组织单位,组织单位可以包含其他各种对象(包括其他组织单元),如“oa组”(一条记录的所属组织)

④cn(Common Name):公共名称,cn=jack
⑤sn(Surname):姓,sn=周
⑥dn(Distinguished Name):类比URL。一条记录的唯一标识。uid=ziyi.zhou,ou=oa组,dc=example,dc=com。

“uid=ziyi.zhou,ou=oa组,dc=example,dc=com”,一条记录的位置(唯一)

  • 类比URL:唯一定位Ldap服务中的一条记录。
⑦rdn(Relative dn):类比文件系统相对路径

相对辨别名,类似于文件系统中的相对路径,它是与目录树结构无关的部分,如“uid=tom”或“cn= Thomas Johansson”

汇总表

使用LDAP流程:

  1. 连接到LDAP服务器;
  2. 绑定到LDAP服务器;
  3. 在LDAP服务器上执行所需的任何操作;
  4. 释放LDAP服务器的连接;
    在这里插入图片描述

参考:https://www.cnblogs.com/wilburxu/p/9174353.html

1 Linux搭建Ldap

以Centos为例搭建openldap。

1.1 搭建Ldap服务(Server端)

1. 安装openldap
# 安装openldap
yum -y install openldap-servers openldap-clients 
# 对管理员密码进行加密
slappasswd -s 123456
#加密后的密码(后面需要用到): {SSHA}VHNPrmccIO/QRS1IOBdwp++K/FkIkFac
2. 新建Ldap配置文件
# 新建Ldap配置文件
vim /etc/openldap/schema/changes.ldif

changes.ldif:

# 修改域名
dn: olcDatabase={2}hdb,cn=config
changetype: modify
replace: olcSuffix
# 注意修改
olcSuffix: dc=yi,dc=com# 修改管理员用户
dn: olcDatabase={2}hdb,cn=config
changetype: modify
replace: olcRootDN
# 注意修改修改管理员用户 (olcRootDN):将管理员账户从原来的cn=admin,dc=ldap,dc=com改为cn=admin,dc=yi,dc=com。
olcRootDN: cn=admin,dc=yi,dc=com# 修改管理员密码
dn: olcDatabase={2}hdb,cn=config
changetype: modify
replace: olcRootPW
# 替换为 slappasswd 生成后的结果
olcRootPW: {SSHA}VHNPrmccIO/QRS1IOBdwp++K/FkIkFac# 修改访问权限
dn: olcDatabase={1}monitor,cn=config
changetype: modify
replace: olcAccess
olcAccess: {0}to *by dn.base="gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth" readby dn.base="cn=admin,dc=yi,dc=com" readby * none
3. 创建域和组织
# 创建配置文件
vim /etc/openldap/schema/basedomain.ldif

basedomain.ldif:

# 配置文件内容,dn、dc都改成自己的配置
# 下列目录:DC=redmond,DC=wa,DC=microsoft,DC=com      
# 如果我们类比文件系统的话,可被看作如下文件路径:   
# Com/Microsoft/Wa/Redmond  
# 例如:CN=test,OU=developer,DC=domainname,DC=com
# 在上面的代码中 cn=test 可能代表一个用户名,ou=developer 代表一个 active directory 中的组织单位。这句话的含义可能就是说明 test 这个对象处在domainname.com 域的 developer 组织单元中dn: dc=yi,dc=com
dc: yi
objectClass: top
objectClass: domaindn: ou=People,dc=yi,dc=com
ou: People
objectClass: top
objectClass: organizationalUnitdn: ou=Group,dc=yi,dc=com
ou: Group
objectClass: top
objectClass: organizationalUnit

应用配置:

# 应用域和配置,回车之后输入未加密前的密码:123456
ldapadd -x -D cn=admin,dc=yi,dc=com -W -f  /etc/openldap/schema/basedomain.ldif# 查看用户列表,观察是否创建成功
ldapsearch -x -b "ou=People,dc=yi,dc=com" | grep dn

通过配置文件新建一个ou

# 新建配置文件
vim testGroup.ldif# 配置文件内容ou:指明为Test Group
dn: ou=Test,dc=yi,dc=com
ou: Test
objectClass: top
objectClass: organizationalUnit# 应用配置(cn=admin,dc=yi,dc=com:admin账户)
# 回车后输入加密前的密码:123456
ldapadd -x -D "cn=admin,dc=yi,dc=com" -W -f testGroup.ldif

在这里插入图片描述

  • 执行命令前:
    在这里插入图片描述
  • 执行命令,应用配置文件新增一个ou
    在这里插入图片描述

1.2 docker搭建可视化工具

1. 安装docker环境
yum install -y yum-utils
yum-config-manager \--add-repo \https://download.docker.com/linux/centos/docker-ce.repo
yum install docker
systemctl start docker
2. 开放ldap端口(399)
# 开放ldap server389端口或关闭防火墙
firewall-cmd --zone=public --add-port=389/tcp --permanent
systemctl restart firewalld
3. docker搭建可视化Ldap管理工具
# 配置主机地址&不开启HTTPS(默认是开启)
docker run -d --privileged -p 10004:80 --name myphpldapadmin \--env PHPLDAPADMIN_HTTPS=false --env PHPLDAPADMIN_LDAP_HOSTS=10.16.64.147  \--detach osixia/phpldapadmin

在这里插入图片描述

4. 浏览器访问

浏览器访问http://ip:10004

账户:

  • dn:cn=admin,dc=yi,dc=com
  • 密码:123456(我们生成密钥之前的明文,slappasswd -s 123456)

登录:
在这里插入图片描述
在这里插入图片描述

1.3 其他客户端工具推荐

1. windows:ldapadmin

官网地址:http://www.ldapadmin.org/

页面效果:
在这里插入图片描述

2. mac:LDAP Browser For MAC

官网地址:https://ldapbrowsermac.com/

在这里插入图片描述

使用效果:
在这里插入图片描述

2 Go操作Ldap

  1. 连接到LDAP服务器并绑定到LDAP服务器;(一般以管理员用户绑定,权限更大)
  2. 在LDAP服务器上执行所需的任何操作;
  3. 释放LDAP服务器的连接;

2.1 连接并以相应角色绑定LDAP服务器

安装依赖:

// 安装go操作ldap库
go get "github.com/go-ldap/ldap/v3"
func loginBind(config *LdapConfig) (*ldap.Conn, error) {l, err := ldap.DialURL(ldapURL, ldap.DialWithTLSConfig(&tls.Config{InsecureSkipVerify: true}))if err != nil {panic(err)return nil, err}_, err = l.SimpleBind(&ldap.SimpleBindRequest{Username: config.BindUserDn, //"cn=admin,dc=yi,dc=com"Password: config.BindUserPassword, //"123456"})if err != nil {fmt.Println("ldap password is error: ", ldap.LDAPResultInvalidCredentials)return nil, err}fmt.Println("bind success...")return l, nil
}

2.2 执行对应操作(add、select、del等)

1. add

以添加用户为例

func addUser(conn *ldap.Conn, user User) error {//添加用户addRequest := ldap.NewAddRequest(fmt.Sprintf("cn=%s,ou=QA,dc=yi,dc=com", user.username), nil)addRequest.Attribute("objectClass", []string{"inetOrgPerson"})addRequest.Attribute("ou", []string{"QA Group"})addRequest.Attribute("cn", []string{"41234123"})addRequest.Attribute("sn", []string{"xx2"})addRequest.Attribute("uid", []string{"10001"})addRequest.Attribute("userPassword", []string{user.password})err := conn.Add(addRequest)if err != nil {fmt.Println("add user error: ", err)return err}return nil
}

执行添加前:
在这里插入图片描述
运行main,在ou=QA下添加一条记录:

package mainimport ("crypto/tls""fmt""github.com/go-ldap/ldap/v3""github.com/ziyifast/log"
)// ldap:未加密
// ldaps:加密
var ldapURL = "ldap://10.100.xx.xxx"type LdapConfig struct {Addr             stringBindUserDn       stringBindUserPassword stringBaseDn           stringLoginName        stringObjectClass      []string
}type User struct {username    stringpassword    stringtelephone   stringemailSuffix stringsnUsername  stringuid         stringgid         string
}func loginBind(config *LdapConfig) (*ldap.Conn, error) {l, err := ldap.DialURL(ldapURL, ldap.DialWithTLSConfig(&tls.Config{InsecureSkipVerify: true}))if err != nil {panic(err)return nil, err}_, err = l.SimpleBind(&ldap.SimpleBindRequest{Username: config.BindUserDn,Password: config.BindUserPassword,})if err != nil {fmt.Println("ldap password is error: ", ldap.LDAPResultInvalidCredentials)return nil, err}fmt.Println("bind success...")return l, nil
}// 创建用户
func addUser(conn *ldap.Conn, user User) error {//添加用户addRequest := ldap.NewAddRequest(fmt.Sprintf("cn=%s,ou=QA,dc=yi,dc=com", user.username), nil)addRequest.Attribute("objectClass", []string{"inetOrgPerson"})addRequest.Attribute("ou", []string{"QA Group"})addRequest.Attribute("cn", []string{"41234123"})addRequest.Attribute("sn", []string{"xx2"})addRequest.Attribute("uid", []string{"10001"})addRequest.Attribute("userPassword", []string{user.password})err := conn.Add(addRequest)if err != nil {fmt.Println("add user error: ", err)return err}return nil
}
func main() {//Ldap Config(用于校验后续的操作,包括查询用户是否存在、添加、删除等)config := new(LdapConfig)config.Addr = "ldap://10.100.xx.xxx"config.BaseDn = "dc=yi,dc=com"config.BindUserDn = "cn=admin,dc=yi,dc=com"config.LoginName = "uid"config.BindUserPassword = "123456"config.ObjectClass = []string{"inetOrgPerson"}//与建立ldap服务建立连接(方便后续查询新增删除项)conn, err := loginBind(config)if err != nil {panic(err)}defer conn.Close()TestAddUser(conn)
}// TestAddUser 测试添加用户
func TestAddUser(conn *ldap.Conn) {//添加用户user := User{username: "wangmazi",password: "123456",}err := addUser(conn, user)if err != nil {panic(err)}fmt.Println("add success...")
}

在这里插入图片描述
效果:

添加成功

在这里插入图片描述

2. select
  • 拼接查询条件
    • 单个条件(cn=jack):查询cn为jack的资源
    • 多个条件(&(cn=wangmazi)(ou=QA)):查询cn为wangmazi并且ou为QA的资源
  • ldap.NewSearchRequest(fmt.Sprintf(“%s”, config.BaseDn)调用查询接口
// 查询用户
func findUser(conn *ldap.Conn, config *LdapConfig, user User) (*ldap.SearchResult, error) {//多个条件:(&(cn=wangmazi)(ou=QA))filter := fmt.Sprintf("(cn=%s)", ldap.EscapeFilter(user.username))request := ldap.NewSearchRequest(fmt.Sprintf("%s", config.BaseDn),ldap.ScopeWholeSubtree,ldap.NeverDerefAliases,0,0,false,filter,[]string{"userPassword"},nil,)searchResult, err := conn.Search(request)if err != nil {fmt.Println("search user error: ", err)return nil, err}return searchResult, nil
}

运行:

package mainimport ("crypto/tls""fmt""github.com/go-ldap/ldap/v3""github.com/ziyifast/log"
)// ldap:未加密
// ldaps:加密
var ldapURL = "ldap://10.100.xx.xxx"type LdapConfig struct {Addr             stringBindUserDn       stringBindUserPassword stringBaseDn           stringLoginName        stringObjectClass      []string
}type User struct {username    stringpassword    stringtelephone   stringemailSuffix stringsnUsername  stringuid         stringgid         string
}func loginBind(config *LdapConfig) (*ldap.Conn, error) {l, err := ldap.DialURL(ldapURL, ldap.DialWithTLSConfig(&tls.Config{InsecureSkipVerify: true}))if err != nil {panic(err)return nil, err}_, err = l.SimpleBind(&ldap.SimpleBindRequest{Username: config.BindUserDn,Password: config.BindUserPassword,})if err != nil {fmt.Println("ldap password is error: ", ldap.LDAPResultInvalidCredentials)return nil, err}fmt.Println("bind success...")return l, nil
}// 查询用户
func findUser(conn *ldap.Conn, config *LdapConfig, user User) (*ldap.SearchResult, error) {//多个条件:(&(cn=wangmazi)(ou=QA))filter := fmt.Sprintf("(cn=%s)", ldap.EscapeFilter(user.username))request := ldap.NewSearchRequest(fmt.Sprintf("%s", config.BaseDn),ldap.ScopeWholeSubtree,ldap.NeverDerefAliases,0,0,false,filter,[]string{"userPassword"},nil,)searchResult, err := conn.Search(request)if err != nil {fmt.Println("search user error: ", err)return nil, err}return searchResult, nil
}
func main() {//Ldap Config(用于校验后续的操作,包括查询用户是否存在、添加、删除等)config := new(LdapConfig)config.Addr = "ldap://10.100.xx.xxx"config.BaseDn = "dc=yi,dc=com"config.BindUserDn = "cn=admin,dc=yi,dc=com"config.LoginName = "uid"config.BindUserPassword = "123456"config.ObjectClass = []string{"inetOrgPerson"}//与建立ldap服务建立连接(方便后续查询新增删除项)conn, err := loginBind(config)if err != nil {panic(err)}defer conn.Close()TestFindUser(conn, config)
}// TestFindUser 测试查询用户
func TestFindUser(conn *ldap.Conn, config *LdapConfig) {user := &User{username: "wangmazi",}searchResult, err := findUser(conn, config, *user)if err != nil {panic(err)}for _, entry := range searchResult.Entries {fmt.Println("find user: ", entry.DN)for _, v := range entry.Attributes {fmt.Println(v.Name, v.Values)}}return
}

效果:
在这里插入图片描述

3. del
  • 拼接要删除的DN(唯一标识,定位一个资源的具体位置)
  • ldap.NewDelRequest(dn, nil)调用删除请求
// 删除用户
func deleteUser(conn *ldap.Conn, config *LdapConfig, user User) error {dn := fmt.Sprintf("cn=%s,ou=QA,%s", user.username, config.BaseDn)log.Infof("del dn %v", dn)delRequest := ldap.NewDelRequest(dn, nil)err := conn.Del(delRequest)if err != nil {fmt.Printf("Failed to delete user %s: %v\n", dn, err)return err}fmt.Printf("User %s successfully deleted.\n", dn)return nil
}

删除前:
在这里插入图片描述
运行TestDeleteUser删除该记录:

package mainimport ("crypto/tls""fmt""github.com/go-ldap/ldap/v3""github.com/ziyifast/log"
)// ldap:未加密
// ldaps:加密
var ldapURL = "ldap://10.100.xx.xxx"type LdapConfig struct {Addr             stringBindUserDn       stringBindUserPassword stringBaseDn           stringLoginName        stringObjectClass      []string
}type User struct {username    stringpassword    stringtelephone   stringemailSuffix stringsnUsername  stringuid         stringgid         string
}func loginBind(config *LdapConfig) (*ldap.Conn, error) {l, err := ldap.DialURL(ldapURL, ldap.DialWithTLSConfig(&tls.Config{InsecureSkipVerify: true}))if err != nil {panic(err)return nil, err}_, err = l.SimpleBind(&ldap.SimpleBindRequest{Username: config.BindUserDn,Password: config.BindUserPassword,})if err != nil {fmt.Println("ldap password is error: ", ldap.LDAPResultInvalidCredentials)return nil, err}fmt.Println("bind success...")return l, nil
}// 查询用户
func findUser(conn *ldap.Conn, config *LdapConfig, user User) (*ldap.SearchResult, error) {//多个条件:(&(cn=wangmazi)(ou=QA))filter := fmt.Sprintf("(cn=%s)", ldap.EscapeFilter(user.username))request := ldap.NewSearchRequest(fmt.Sprintf("%s", config.BaseDn),ldap.ScopeWholeSubtree,ldap.NeverDerefAliases,0,0,false,filter,[]string{"userPassword"},nil,)searchResult, err := conn.Search(request)if err != nil {fmt.Println("search user error: ", err)return nil, err}return searchResult, nil
}func main() {//Ldap Config(用于校验后续的操作,包括查询用户是否存在、添加、删除等)config := new(LdapConfig)config.Addr = "ldap://10.100.xx.xxx"config.BaseDn = "dc=yi,dc=com"config.BindUserDn = "cn=admin,dc=yi,dc=com"config.LoginName = "uid"config.BindUserPassword = "123456"config.ObjectClass = []string{"inetOrgPerson"}//与建立ldap服务建立连接(方便后续查询新增删除项)conn, err := loginBind(config)if err != nil {panic(err)}defer conn.Close()TestDeleteUser(conn, config)
}

运行后效果:
在这里插入图片描述
在这里插入图片描述

2.3 释放连接

func main() {//与建立ldap服务建立连接(方便后续查询新增删除项)conn, err := loginBind(config)if err != nil {panic(err)}err = conn.Close()if err != nil {panic(err)}
}

全部代码

代码地址:https://github.com/ziyifast/ziyifast-code_instruction/tree/main/ldap_demo

package mainimport ("crypto/tls""fmt""github.com/go-ldap/ldap/v3""github.com/ziyifast/log"
)// ldap:未加密
// ldaps:加密
var ldapURL = "ldap://10.16.xx.xx"type LdapConfig struct {Addr             stringBindUserDn       stringBindUserPassword stringBaseDn           stringLoginName        stringObjectClass      []string
}type User struct {username    stringpassword    stringtelephone   stringemailSuffix stringsnUsername  stringuid         stringgid         string
}func loginBind(config *LdapConfig) (*ldap.Conn, error) {l, err := ldap.DialURL(ldapURL, ldap.DialWithTLSConfig(&tls.Config{InsecureSkipVerify: true}))if err != nil {panic(err)return nil, err}_, err = l.SimpleBind(&ldap.SimpleBindRequest{Username: config.BindUserDn,Password: config.BindUserPassword,})if err != nil {fmt.Println("ldap password is error: ", ldap.LDAPResultInvalidCredentials)return nil, err}fmt.Println("bind success...")return l, nil
}// 创建用户
func addUser(conn *ldap.Conn, user User) error {//添加用户addRequest := ldap.NewAddRequest(fmt.Sprintf("cn=%s,ou=QA,dc=yi,dc=com", user.username), nil)addRequest.Attribute("objectClass", []string{"inetOrgPerson"})addRequest.Attribute("ou", []string{"QA Group"})addRequest.Attribute("cn", []string{"41234123"})addRequest.Attribute("sn", []string{"xx2"})addRequest.Attribute("uid", []string{"10001"})addRequest.Attribute("userPassword", []string{user.password})err := conn.Add(addRequest)if err != nil {fmt.Println("add user error: ", err)return err}return nil
}// 查询用户
func findUser(conn *ldap.Conn, config *LdapConfig, user User) (*ldap.SearchResult, error) {//多个条件:(&(cn=wangmazi)(ou=QA))filter := fmt.Sprintf("(cn=%s)", ldap.EscapeFilter(user.username))request := ldap.NewSearchRequest(fmt.Sprintf("%s", config.BaseDn),ldap.ScopeWholeSubtree,ldap.NeverDerefAliases,0,0,false,filter,[]string{"userPassword"},nil,)searchResult, err := conn.Search(request)if err != nil {fmt.Println("search user error: ", err)return nil, err}return searchResult, nil
}// 删除用户
func deleteUser(conn *ldap.Conn, config *LdapConfig, user User) error {dn := fmt.Sprintf("cn=%s,ou=QA,%s", user.username, config.BaseDn)log.Infof("del dn %v", dn)delRequest := ldap.NewDelRequest(dn, nil)err := conn.Del(delRequest)if err != nil {fmt.Printf("Failed to delete user %s: %v\n", dn, err)return err}fmt.Printf("User %s successfully deleted.\n", dn)return nil
}func main() {//Ldap Config(用于校验后续的操作,包括查询用户是否存在、添加、删除等)config := new(LdapConfig)config.Addr = "ldap://10.16.xx.xx"config.BaseDn = "dc=yi,dc=com"config.BindUserDn = "cn=admin,dc=yi,dc=com"config.LoginName = "uid"config.BindUserPassword = "123456"//客户不配置username,我们需要根据配置的ObjectClass查询出对应的用户。//因为如果用户配置的是cn,那么可能会查询出一些组织、其他设备等,所以为了将Ldap第三方用户纳管过来,我们需要添加ObjectClassconfig.ObjectClass = []string{"inetOrgPerson"}//与建立ldap服务建立连接(方便后续查询新增删除项)conn, err := loginBind(config)if err != nil {panic(err)}defer conn.Close()TestDeleteUser(conn, config)
}// TestAddUser 测试添加用户
func TestAddUser(conn *ldap.Conn) {//添加用户user := User{username: "wangmazi",password: "123456",}err := addUser(conn, user)if err != nil {panic(err)}fmt.Println("add success...")
}// TestFindUser 测试查询用户
func TestFindUser(conn *ldap.Conn, config *LdapConfig) {user := &User{username: "wangmazi",}searchResult, err := findUser(conn, config, *user)if err != nil {panic(err)}for _, entry := range searchResult.Entries {fmt.Println("find user: ", entry.DN)for _, v := range entry.Attributes {fmt.Println(v.Name, v.Values)}}return
}func TestDeleteUser(conn *ldap.Conn, config *LdapConfig) {user := User{username: "wangmazi",}err := deleteUser(conn, config, user)if err != nil {panic(err)}}

3 项目对接思路

项目登录对接:支持LDAP登录,用户可直接输入LDAP服务端存在的用户,直接登录系统

  1. 页面提供入口配置LDAP服务
    • Addr:LDAP服务端地址
    • BindUserDn:LDAP管理员用户dn
    • BindUserPassword:LDAP管理员密码
    • BaseDn:操作范围(dc=yi,dc=com表明操作这个范围下的数据)
    • LoginName:配置以哪个参数登录
  2. 页面输入LDAP对应账号
  3. 根据LDAP配置连接LDAP服务端,查询用户输入的账号是否存在,密码是否正确
  4. 可以直接纳管LDAP用户到我方系统,建立对应关系。比如:用户审计…
type LdapConfig struct {Addr             stringBindUserDn       stringBindUserPassword stringBaseDn           stringLoginName        stringObjectClass      []string
}

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/3282.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

力扣HOT100 - 114. 二叉树展开为链表

解题思路&#xff1a; class Solution {List<TreeNode> list new ArrayList<>();public void flatten(TreeNode root) {recur(root);for (int i 1; i < list.size(); i) {TreeNode pre list.get(i - 1);TreeNode cur list.get(i);pre.left null;pre.right…

使用Shell终端访问Linux

一、实验目的 1、熟悉Linux文件系统访问命令&#xff1b; 2、熟悉常用 Linux Shell的命令&#xff1b; 3、熟悉在Linux文件系统中vi编辑器的使用&#xff1b; 4、进一步熟悉虚拟机网络连接模式与参数配置&#xff01; 二、实验内容 1、使用root帐号登陆到Linux的X-windows…

【Qt 学习笔记】Qt常用控件 | 输入类控件 | Combo Box的使用及说明

博客主页&#xff1a;Duck Bro 博客主页系列专栏&#xff1a;Qt 专栏关注博主&#xff0c;后期持续更新系列文章如果有错误感谢请大家批评指出&#xff0c;及时修改感谢大家点赞&#x1f44d;收藏⭐评论✍ Qt常用控件 | 输入类控件 | Combo Box的使用及说明 文章编号&#xff…

【Qt 学习笔记】Qt常用控件 | 显示类控件 | LCD Number的使用及说明

博客主页&#xff1a;Duck Bro 博客主页系列专栏&#xff1a;Qt 专栏关注博主&#xff0c;后期持续更新系列文章如果有错误感谢请大家批评指出&#xff0c;及时修改感谢大家点赞&#x1f44d;收藏⭐评论✍ Qt常用控件 | 显示类控件 | LCD Number的使用及说明 文章编号&#xf…

wps屏幕录制怎么用?分享使用方法!

数字化时代&#xff0c;屏幕录制已成为我们学习、工作和娱乐中不可或缺的一部分。无论是制作教学视频、分享游戏过程&#xff0c;还是录制网络会议&#xff0c;屏幕录制都能帮助我们轻松实现。WPS作为一款功能强大的办公软件&#xff0c;其屏幕录制功能也备受用户青睐。本文将详…

代码随想录:二叉树15-17

目录 404.左叶子之和 题目 代码&#xff08;后序递归&#xff09; 代码&#xff08;前序迭代&#xff09; 513.找树左下角的值 题目 代码&#xff08;层序迭代&#xff09; 112.路径总和 题目 代码&#xff08;前序迭代&#xff09; 112.路径总和II 题目 代码&…

Linux读写文件

前言 学习了文件系统&#xff0c;就能理解为什么说Linux下一切皆文件。 语言层面的操作 在c语言的学习中我们可以使用fopen()函数对文件进行操作。 int main() {//FILE * fp fopen("./log.txt", "w");//FILE * fp fopen("./log.txt", "…

TablePlus for Mac/Win:开启高效数据开发新纪元

在当今数字化时代&#xff0c;数据的重要性日益凸显。无论是企业还是个人&#xff0c;都需要一款强大而实用的本地原生数据开发软件来提升工作效率。而 TablePlus for Mac/Win 正是这样一款卓越的工具&#xff0c;它为用户带来了全新的体验&#xff0c;让数据开发变得更加轻松、…

第1次作业

目录 重点内容提要一、误差度量二、浮点数系统三、误差传播四、数值稳定性 作业解析 重点内容提要 一、误差度量 二、浮点数系统 三、误差传播 四、数值稳定性 作业解析

快速部署 Garnet

快速部署 Garnet Garnet 是 Microsoft Research 推出的一种新型远程缓存存储&#xff0c;其设计速度极快、可扩展且延迟低。 Garnet 在单个节点内是线程可扩展的。它还支持分片集群执行、复制、检查点、故障转移和事务。它可以在主内存以及分层存储&#xff08;例如 SSD 和 Az…

GRASSHOPPER电池Expression

Grasshopper中如果要实现简单的条件if语句的效果&#xff0c;可以使用电池Expression。 举例&#xff1a;获取两个数的差值&#xff0c;永远用大数减去小数

OpenUI在windows下部署使用

OpenUI OpenUI是一个基于Python的AI对话平台&#xff0c;支持接入多种AI模型。 通过聊天的方式来进行UI设计&#xff0c;你可以通过文字来描述你想要的UI界面&#xff0c;OpenUI可以帮你实时进行渲染出效果 安装OpenUI 这里预设你的电脑上已安装git、Python和pip&#xff0…

OSI网络七层协议<随手笔记>

1.OSI OSI&#xff08;Open System Interconnect&#xff09;&#xff0c;即开放式系统互连。 一般都叫OSI参考模型&#xff0c;是ISO组织在1985年研究的网络互连模型。该体系结构标准定义了网络互连的七层框架&#xff08;物理层、数据链路层、网络层、传输层、会话层、表示层…

账号安全及应用

一、账号安全控制 1.1系统账号清理 将用户设置为无法登陆 锁定账户 删除账户 设定账户密码&#xff0c;本质锁定 锁定配置文件-chattr&#xff1a; -a 让文件或目录仅供附加用途。只能追加 -i 不得任意更动文件或目录。 1.2密码安全控制 chage 1.3历史命令 history&am…

Clickhouse离线安装教程

https://blog.51cto.com/u_15060531/4174350 1. 前置 1.1 检查服务器架构 服务器&#xff1a;Centos7.X 需要确保是否x86_64处理器构架、Linux并且支持SSE 4.2指令集 grep -q sse4_2 /proc/cpuinfo && echo "SSE 4.2 supported" || echo "SSE 4.2 …

怡宝母公司冲刺上市:产能未满仍要募资扩产,突击分红25亿元

又一家瓶装水企业冲刺上市。 近日&#xff0c;怡宝母公司华润饮料&#xff08;控股&#xff09;有限公司&#xff08;下称“华润饮料”&#xff09;递交招股书&#xff0c;准备在港交所主板上市&#xff0c;BofA securities&#xff08;美银证券&#xff09;、中银国际、中信证…

C++初阶学习第三弹——类与对象(上)——初始类与对象

前言&#xff1a; 在前面&#xff0c;我们已经初步学习了C的一些基本语法&#xff0c;比如内敛函数、函数重载、缺省参数、引用等等&#xff0c;接下来我们就将正式步入C的神圣殿堂&#xff0c;首先&#xff0c;先给你找个对象 目录 一、类与对象是什么&#xff1f; 二、类的各…

Git 工作原理

Git 工作原理 | CoderMast编程桅杆https://www.codermast.com/dev-tools/git/git-workspace-index-repo.html Workspace&#xff1a;工作区Index / Stage&#xff1a;暂存区Repository&#xff1a;仓库区&#xff08;或本地仓库&#xff09;Remote&#xff1a;远程仓库 Git 一…

T1级,生产环境事故—Shell脚本一键备份K8s的YAML文件

大家好&#xff0c;我叫秋意零。 最近对公司进行日常运维工作时&#xff0c;出现了一个 T1 级别事故。导致公司的“酒云网”APP的无法使用。我和我领导一起搞了一个多小时&#xff0c;业务也停了一个多小时。 起因是&#xff1a;我的部门直系领导&#xff0c;叫我**删除一个 …

数据结构练习-线性表的顺序存储

----------------------------------------------------------------------------------------------------------------------------- 1. 具有n个元素的线性表采用顺序存储结构&#xff0c;在其第i个位置插入一个新元素的算法间复杂度为 ( )(1≤i≤n1) 。 …