PostgreSQL源码分析——口令认证

认证机制

对于数据库系统来说,其作为服务端,接受来自客户端的请求。对此,必须有对客户端的认证机制,只有通过身份认证的客户端才可以访问数据库资源,防止非法用户连接数据库。PostgreSQL支持认证方法有很多:

typedef enum UserAuth
{uaReject,uaImplicitReject,			/* Not a user-visible option */uaTrust,uaIdent,uaPassword,uaMD5,uaSCRAM,uaGSS,uaSSPI,uaPAM,uaBSD,uaLDAP,uaCert,uaRADIUS,uaPeer
#define USER_AUTH_LAST uaPeer	/* Must be last value of this enum */
} UserAuth;

对此,我们仅分析常用的口令认证方式。目前PostgreSQL支持MD5、SCRAM-SHA-256对密码哈希和身份验证支持,建议使用SCRAM-SHA-256,相较MD5安全性更高。不同的基于口令的认证方法的可用性取决于用户的口令在服务器上是如何被加密(或者更准确地说是哈希)的。这由设置口令时的配置参数password_encryption控制。可用值为scham-sha-256md5

host    all             all             0.0.0.0/0               scham-sha-256

口令认证并不是简单的客户端告诉服务端密码,服务端比对成功就可以了。而是要设计一个客户端和服务器协商身份验证机制,避免密码明文存储等等问题。对此,PostgreSQL引入SCRAM身份验证机制(RFC5802、RFC7677)

image.png

也就是说,后面的口令认证方法中,遵循的是如上的口令认证协议。

参考文档:53.3.1. SCRAM-SHA-256认证

源码分析

这里只分析口令认证的情况。 口令认证分为明文口令认证和加密口令认证。明文口令认证要求客户端提供一个未加密的口令进行认证,安全性较差,已经被禁止使用。加密口令认证要求客户端提供一个经过SCRAM-SHA-256加密的口令进行认证,该口令在传送过程中使用了结合salt的单向哈希加密,增强了安全性。口令都存储在pg_authid系统表中,可通过CREATE USER test WITH PASSWORD '******';等命令进行设置。

客户端

以psql客户端为例,当客户端发出连接请求后,分析口令认证的过程。

main
--> parse_psql_options(argc, argv, &options);// 连接数据库,中间会有认证这块,有不同的认证方法,这里列的是口令认证方式的流程
--> do {// 输入host、port、user、password、dbname等信息PQconnectdbParams(keywords, values, true);--> PQconnectStartParams(keywords, values, expand_dbname);--> makeEmptyPGconn();--> conninfo_array_parse(keywords, values, &conn->errorMessage, true, expand_dbname);--> fillPGconn(conn, connOptions)--> connectDBStart(conn)--> PQconnectPoll(conn)    // 尝试建立TCP连接--> socket(addr_cur->ai_family, SOCK_STREAM, 0);--> connect(conn->sock, addr_cur->ai_addr, addr_cur->ai_addrlen)--> connectDBComplete(conn);// 认证这块与数据库服务端交互以及状态很多,这里没有全部列出。--> PQconnectPoll(conn);--> pqReadData(conn);--> pqsecure_read(conn, conn->inBuffer + conn->inEnd, conn->inBufSize - conn->inEnd);--> pqsecure_raw_read(conn, ptr, len);--> recv(conn->sock, ptr, len, 0);--> pg_fe_sendauth(areq, msgLength, conn);--> pg_SASL_init(conn, payloadlen)--> conn->sasl->init(conn,password,selected_mechanism);--> pg_saslprep(password, &prep_password);--> conn->sasl->exchange(conn->sasl_state, NULL, -1, &initialresponse, &initialresponselen, &done, &success);--> build_client_first_message(state);--> pg_strong_random(raw_nonce, SCRAM_RAW_NONCE_LEN)PQconnectionNeedsPassword(pset.db)     // 是否需要密码,如果需要,等待用户数据密码}// 进入主循环,等待用户输入命令,执行命令
--> MainLoop(stdin);  --> psql_scan_create(&psqlscan_callbacks);--> while (successResult == EXIT_SUCCESS){// 等待用户输入命令gets_interactive(get_prompt(prompt_status, cond_stack), query_buf);SendQuery(query_buf->data); // 把用户输入的SQL命令发送到数据库服务--> ExecQueryAndProcessResults(query, &elapsed_msec, &svpt_gone, false, NULL, NULL) > 0);--> PQsendQuery(pset.db, query);    // 通过libpq与数据库交互--> PQgetResult(pset.db);}
服务端

服务端相关流程:

main()
--> PostmasterMain(argc, argv);--> InitProcessGlobals();   // set MyProcPid, MyStartTime[stamp], random seeds--> pg_prng_strong_seed(&pg_global_prng_state)--> srandom(pg_prng_uint32(&pg_global_prng_state));--> InitializeGUCOptions();--> ProcessConfigFile(PGC_POSTMASTER);--> ProcessConfigFileInternal(context, true, elevel);--> ParseConfigFp(fp, abs_path, depth, elevel, head_p, tail_p);--> load_hba()  // 读pg_hba.conf--> parse_hba_line(tok_line, LOG)   // 解析pg_hba.conf中的每条记录,解析到HbaLine的链表结构中--> load_ident()--> ServerLoop();--> BackendStartup(port);--> InitPostmasterChild();--> BackendInitialize(port);--> pq_init();--> RegisterTimeout(STARTUP_PACKET_TIMEOUT, StartupPacketTimeoutHandler);--> enable_timeout_after(STARTUP_PACKET_TIMEOUT, AuthenticationTimeout * 1000);--> ProcessStartupPacket(port, false, false);   // Read a client's startup packet--> pq_startmsgread();--> pq_getbytes((char *) &len, 1)--> InitProcess();--> BackendRun(port);--> PostgresMain(port->database_name, port->user_name);--> BaseInit();// 在接收用户SQL请求前进行认证--> InitPostgres(dbname, InvalidOid, username, InvalidOid,!am_walsender, false, NULL);--> PerformAuthentication(MyProcPort);--> enable_timeout_after(STATEMENT_TIMEOUT, AuthenticationTimeout * 1000);--> ClientAuthentication(port);--> hba_getauthmethod(port);--> check_hba(port);// 根据不同的认证方法进行认证--> switch (port->hba->auth_method){		case uaTrust:status = STATUS_OK;break;case uaMD5:case uaSCRAM:// 口令认证status = CheckPWChallengeAuth(port, &logdetail);--> break;// ...}--> if (status == STATUS_OK)sendAuthRequest(port, AUTH_REQ_OK, NULL, 0);elseauth_failed(port, status, logdetail);--> disable_timeout(STATEMENT_TIMEOUT, false);--> initialize_acl();// 认证通过后才能接受用户的SQL请求--> for (;;){// ...exec_simple_query(query_string);}

每次客户端发起连接时,postgres主进程都会fork一个子进程:

static int BackendStartup(Port *port)
{// ...pid = fork_process();if (pid == 0)				/* child */{free(bn);/* Detangle from postmaster */InitPostmasterChild();/* Close the postmaster's sockets */ClosePostmasterPorts(false);/* Perform additional initialization and collect startup packet */BackendInitialize(port);/** Create a per-backend PGPROC struct in shared memory. We must do* this before we can use LWLocks. In the !EXEC_BACKEND case (here)* this could be delayed a bit further, but EXEC_BACKEND needs to do* stuff with LWLocks before PostgresMain(), so we do it here as well* for symmetry.*/InitProcess();  /* And run the backend */BackendRun(port);  // }
}

每个子进程在接收用户的SQL请求前,需要先进行认证。主要实现在ClientAuthentication函数中,通过认证才能继续进行下一步。

/** Client authentication starts here.  If there is an error, this* function does not return and the backend process is terminated. */
void ClientAuthentication(Port *port)
{int			status = STATUS_ERROR;const char *logdetail = NULL;hba_getauthmethod(port);/** This is the first point where we have access to the hba record for the* current connection, so perform any verifications based on the hba* options field that should be done *before* the authentication here. */if (port->hba->clientcert != clientCertOff){/* If we haven't loaded a root certificate store, fail */if (!secure_loaded_verify_locations())ereport(FATAL,(errcode(ERRCODE_CONFIG_FILE_ERROR),errmsg("client certificates can only be checked if a root certificate store is available")));/* If we loaded a root certificate store, and if a certificate is* present on the client, then it has been verified against our root* certificate store, and the connection would have been aborted* already if it didn't verify ok. */if (!port->peer_cert_valid)ereport(FATAL,(errcode(ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION), errmsg("connection requires a valid client certificate")));}// 根据不同的认证方法,进行不同的处理switch (port->hba->auth_method){// ...case uaMD5:case uaSCRAM:status = CheckPWChallengeAuth(port, &logdetail);break;case uaPassword:status = CheckPasswordAuth(port, &logdetail);break;case uaTrust:status = STATUS_OK;break;}if ((status == STATUS_OK && port->hba->clientcert == clientCertFull)|| port->hba->auth_method == uaCert){/** Make sure we only check the certificate if we use the cert method* or verify-full option.*/
#ifdef USE_SSLstatus = CheckCertAuth(port);
#elseAssert(false);
#endif}if (ClientAuthentication_hook)(*ClientAuthentication_hook) (port, status);if (status == STATUS_OK)sendAuthRequest(port, AUTH_REQ_OK, NULL, 0);elseauth_failed(port, status, logdetail);
}

对于口令认证,具体实现为CheckPWChallengeAuth函数。

/* MD5 and SCRAM authentication. */
static int CheckPWChallengeAuth(Port *port, const char **logdetail)
{int			auth_result;char	   *shadow_pass;PasswordType pwtype;/* First look up the user's password. */ // 查找pg_authid系统表中的用户密码shadow_pass = get_role_password(port->user_name, logdetail);if (!shadow_pass)pwtype = Password_encryption;elsepwtype = get_password_type(shadow_pass);/* If 'md5' authentication is allowed, decide whether to perform 'md5' or* 'scram-sha-256' authentication based on the type of password the user* has.  If it's an MD5 hash, we must do MD5 authentication, and if it's a* SCRAM secret, we must do SCRAM authentication.** If MD5 authentication is not allowed, always use SCRAM.  If the user* had an MD5 password, CheckSASLAuth() with the SCRAM mechanism will fail. */if (port->hba->auth_method == uaMD5 && pwtype == PASSWORD_TYPE_MD5)auth_result = CheckMD5Auth(port, shadow_pass, logdetail);elseauth_result = CheckSASLAuth(&pg_be_scram_mech, port, shadow_pass,logdetail);if (shadow_pass)pfree(shadow_pass);/* If get_role_password() returned error, return error, even if the authentication succeeded. */if (!shadow_pass){Assert(auth_result != STATUS_OK);return STATUS_ERROR;}if (auth_result == STATUS_OK)set_authn_id(port, port->user_name);return auth_result;
}int CheckSASLAuth(const pg_be_sasl_mech *mech, Port *port, char *shadow_pass, const char **logdetail)
{StringInfoData sasl_mechs;int			mtype;StringInfoData buf;void	   *opaq = NULL;char	   *output = NULL;int			outputlen = 0;const char *input;int			inputlen;int			result;bool		initial;/** Send the SASL authentication request to user.  It includes the list of* authentication mechanisms that are supported.*/initStringInfo(&sasl_mechs);mech->get_mechanisms(port, &sasl_mechs);/* Put another '\0' to mark that list is finished. */appendStringInfoChar(&sasl_mechs, '\0');sendAuthRequest(port, AUTH_REQ_SASL, sasl_mechs.data, sasl_mechs.len);pfree(sasl_mechs.data);/** Loop through SASL message exchange.  This exchange can consist of* multiple messages sent in both directions.  First message is always* from the client.  All messages from client to server are password* packets (type 'p').*/initial = true;do{pq_startmsgread();mtype = pq_getbyte();if (mtype != 'p'){/* Only log error if client didn't disconnect. */if (mtype != EOF){ereport(ERROR,(errcode(ERRCODE_PROTOCOL_VIOLATION), errmsg("expected SASL response, got message type %d",mtype)));}elsereturn STATUS_EOF;}/* Get the actual SASL message */initStringInfo(&buf);if (pq_getmessage(&buf, PG_MAX_SASL_MESSAGE_LENGTH)){/* EOF - pq_getmessage already logged error */pfree(buf.data);return STATUS_ERROR;}elog(DEBUG4, "processing received SASL response of length %d", buf.len);/* The first SASLInitialResponse message is different from the others.* It indicates which SASL mechanism the client selected, and contains* an optional Initial Client Response payload.  The subsequent* SASLResponse messages contain just the SASL payload. */if (initial){const char *selected_mech;selected_mech = pq_getmsgrawstring(&buf);/** Initialize the status tracker for message exchanges.** If the user doesn't exist, or doesn't have a valid password, or* it's expired, we still go through the motions of SASL* authentication, but tell the authentication method that the* authentication is "doomed". That is, it's going to fail, no matter what.** This is because we don't want to reveal to an attacker what* usernames are valid, nor which users have a valid password. */opaq = mech->init(port, selected_mech, shadow_pass);inputlen = pq_getmsgint(&buf, 4);if (inputlen == -1)input = NULL;elseinput = pq_getmsgbytes(&buf, inputlen);initial = false;}else{inputlen = buf.len;input = pq_getmsgbytes(&buf, buf.len);}pq_getmsgend(&buf);/* Hand the incoming message to the mechanism implementation. */result = mech->exchange(opaq, input, inputlen,&output, &outputlen,logdetail);/* input buffer no longer used */pfree(buf.data);if (output){/*PG_SASL_EXCHANGE_FAILURE with some output is forbidden by SASL.Make sure here that the mechanism used got that right. */if (result == PG_SASL_EXCHANGE_FAILURE)elog(ERROR, "output message found after SASL exchange failure");/* Negotiation generated data to be sent to the client. */elog(DEBUG4, "sending SASL challenge of length %d", outputlen);if (result == PG_SASL_EXCHANGE_SUCCESS)sendAuthRequest(port, AUTH_REQ_SASL_FIN, output, outputlen);elsesendAuthRequest(port, AUTH_REQ_SASL_CONT, output, outputlen);pfree(output);}} while (result == PG_SASL_EXCHANGE_CONTINUE);/* Oops, Something bad happened */if (result != PG_SASL_EXCHANGE_SUCCESS){return STATUS_ERROR;}return STATUS_OK;
}

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

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

相关文章

Stable Diffusion 3 Medium 正式开源

Stable Diffusion 3 Medium 正式开源 Stability AI宣布Stable Diffusion 3 Medium现已开源,这是最新的文本生成图像AI模型,被官方声称为“迄今为止最先进的开源模型”,其性能超过了Midjourney 6。 这款Stable Diffusion 3 Medium模型拥有2…

【SpringBoot项目常见细化错误】(保姆级教程)Result Maps collection already contains value for

SpringBoot项目常见错误 1.当Mybatis报错 Result Maps collection already contains value for一、重复点击Mybatis-Generator导致配置文件重复生成XML二、正确配置Yml仔细检查有没有多了或者少了一个空格三、spring boot mybatis四、应该用resultMap来接收返回值,…

Blazor的SSR服务端渲染是不是交互式的

从.NET8开始,Blazor引入了SSR服务端渲染,归功于MVC和RazePage的沉淀,虽然来得晚,但一经发布,就将Blazor推向了新的高度。从今年开始,Youtube上关于Blazor的优质教学视频,以肉眼可见的速度在增加…

[保姆级教程]uniapp实现接口请求和请求方法二次封装

文章目录 新建文件配置方法发送请求使用 新建文件 现在src中新建一个api目录 新建index.js和request.js文件 配置方法发送请求 request.js中输入以下内容 const http {baseUrl: http://127.0.0.1:8000,request(config) {config beforeRequest(config)config.url this…

前端工具篇

在线工具 https://tool.lu/ 程序员工具箱 http://tool.pfan.cn/apitest 配色 https://webkul.github.io/coolhue/ 在线字符串和16进制互转 https://kw360.net/ox2str/ 代码美化截图 https://carbon.now.sh/?bgrgba 菜鸟工具 https://www.jyshare.com/ 文件格式转换 htt…

C++智能指针auto_ptr(有缺陷已废弃)

一、auto_ptr不能共享所有权 auto_ptr类没有拷贝构造函数。可以看出将Right对象的资源释放掉了。 在下面的操作中,ptr1的值给ptr2,那么此时ptr1的值将会为nullptr。在构造和赋值都发生了所有权的转移。调用函数的时候,接收参数的时候同样也会…

Rapidfuzz,一个高效的 Python 模糊匹配神器

目录 01初识 Rapidfuzz 什么是 Rapidfuzz? 为什么选择 Rapidfuzz? 安装 Rapidfuzz 配置 Rapidfuzz 02基本操作 简单比率计算 03高级功能 查找单个最佳匹配 查找多个最佳匹配 使用阈值优化性能 04实战案例…

IPython大师课:提升数据科学工作效率的终极工具

IPython是一个增强的Python交互式shell,它提供了丰富的功能和易用性改进,特别适合进行数据分析、科学计算和一般的Python开发。本文将全面介绍IPython的基本概念、使用方法、主要作用以及注意事项。 一、IPython简介 1. IPython的起源 IPython最初由Fe…

【Android】使用Binder(AIDL)实现利用自定义Bean进行的进程间通信(二)

项目前置 这是我之前写的关于Binder的一些知识点和使用基本数据类型在通信的文章,感兴趣的可以看一下: Binder(一)Binder的介绍和AIDL使用Binder的实例 项目目标 在两个APP之间进行数据传递,使用Android推荐的Binder通讯&#…

使用Flink接受kafka中的数据并对数据进行ETL

做这个开发是因为:在实际开发操作中,你的kafka主题中会有大量的数据但是需求并不需要所有数据,所有我们要对数据进行清洗,把需要的数据保存在flink流中,为下流的开发做好数据保障! 首先创建工具类 再写一…

Golang | Leetcode Golang题解之第151题反转字符串中的单词

题目&#xff1a; 题解&#xff1a; import ("fmt" )func reverseWords(s string) string {//1.使用双指针删除冗余的空格slowIndex, fastIndex : 0, 0b : []byte(s)//删除头部冗余空格for len(b) > 0 && fastIndex < len(b) && b[fastIndex]…

视频与音频的交响:探索达摩院VideoLLaMA 2的技术创新

一、简介 文章&#xff1a;https://arxiv.org/abs/2406.07476 代码&#xff1a;https://github.com/DAMO-NLP-SG/VideoLLaMA2 VideoLLaMA 2是由阿里巴巴集团的DAMO Academy团队开发的视频大型语言模型&#xff08;Video-LLM&#xff09;&#xff0c;旨在通过增强空间-时间建模…

高考分数线一分一段统计汇总(熟练SQL窗口函数)

高考分数线一分一段统计汇总(使用SQL窗口函数) select 总分数&#xff0c; 一分一段人数&#xff0c; sum(一分一段人数) over( order by 总分数 desc) as 累计排名 from( select 总分数&#xff0c; count(考生号) as 一分一段人数 from &#xff08; select 考生号&…

大咖专栏 | AI 时代下,我们可以拥有怎样的数据库?

Hi&#xff0c;各位朋友们&#xff0c;我是 KaiwuDB 高级架构师赵衎衎。 KaiwuDB 始于万物互联时代下千万条数据洪流中&#xff0c;我们持续打磨构造了更加灵活兼容的分布式多模架构&#xff0c;实现了海量异构数据高性能、低成本的集中管理… …这些底层特性都在为后续提供更…

多线程与高并发- Synchronized锁

简介 synchronized 是 Java 语言的一个关键字&#xff0c;它允许多个线程同时访问共享的资源&#xff0c;以避免多线程编程中的竞争条件和死锁问题。synchronized可以用来给对象或者方法进行加锁&#xff0c;当对某个对象或者代码块加锁时&#xff0c;同时就只能有一个线程去执…

界面组件Kendo UI for Angular 2024 Q2亮点 - 让应用程序界面拥有AI提示

随着最新的2024年第二季度发布&#xff0c;Kendo UI for Angular为应用程序开发设定了标准&#xff0c;包括生成式AI集成、增强的设计系统功能和可访问的数据可视化。新的2024年第二季度版本为应用程序界面提供了人工智能(AI)提示&#xff0c;从设计到代码的生产力增强、可访问…

【分数限制下,专科学子将何去何从】

本文仅讲述专科学子 高考之前&#xff0c;咱就是班里中等偏上水平&#xff0c;有时考试考的好点能冲上历年本科线&#xff0c;有时考的差点就直接名落孙山。 现在高考完了&#xff0c;也就可能在本科线周围晃荡&#xff0c;那我接下来该怎么办呢。 是继续上学&#xff0c;还是去…

奇怪的缓存一致性问题

天猫国际用户Push中心承接了国际用户触达相关的需求&#xff0c;比如短信、端内消息投放等等&#xff0c;并存在较高的并发场景。 该系统此前发现了一个查询投放计划plan为null的异常情况&#xff0c;在初期排查时有些丈二和尚摸不着头脑&#xff0c;后面突然灵光乍现——原来是…

Django从入门到精通:First [Django版本.Python面向对象.Web基础.创建Django项目]

文章目录 Django初学者指南1 Django简介1.1 Django的历史1.2 使用Django的知名网站1.4 Django的主要特点1.5 Django的工作原理 2 Django 版本选择2.1 Django 支持的 Python 版本2.2 Django 版本 3 Django 开发 Web 程序3.1 Python知识点3.1.1 Python 函数3.1.2 Python 面向对象…

智能鼠类监测设备

TH-SH1在人类社会与自然环境共存的过程中&#xff0c;鼠类作为一类常见的害虫&#xff0c;给我们的生活、生产带来了诸多不便。为了更好地控制鼠害&#xff0c;科学家们不断研发出各种高效的监测与防控设备。近期&#xff0c;一款全新的鼠类监测设备崭露头角&#xff0c;其能够…