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);
	                                else
		                                auth_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_SSL
		status = CheckCertAuth(port);
#else
		Assert(false);
#endif
	}

	if (ClientAuthentication_hook)
		(*ClientAuthentication_hook) (port, status);

	if (status == STATUS_OK)
		sendAuthRequest(port, AUTH_REQ_OK, NULL, 0);
	else
		auth_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;
	else
		pwtype = 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);
	else
		auth_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)));
			}
			else
				return 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;
			else
				input = 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);
			else
				sendAuthRequest(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.mfbz.cn/a/727355.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

OCC介绍及框架分析

1.OCC介绍 Open CASCADE (简称OCC)是一开源的几何造型引擎,OCCT库是由Open CASCADE公司开发和市场运作的。它是为开源社区比较成熟的基于BREP结构的建模引擎,能够满足二维三维实体造型和曲面造型,国内研究和使用它的单…

JetBrains IDEA 新旧UI切换

JetBrains IDE 新旧UI切换 IntelliJ IDEA 的老 UI 以其经典的布局和稳定的性能,成为了许多开发者的首选。而新 UI 则在此基础上进行了全面的改进,带来了更加现代化、响应式和高效的用户体验。无论是新用户还是老用户,都可以通过了解和适应这…

SolidWorks上海官方代理商亿达四方:赋能智能制造,创设计新高度

在上海这片充满活力的热土上,亿达四方作为SolidWorks的正版授权代理商,正以其专业的技术力量和周到的服务体系,为当地制造业的转型升级注入强大动力。我们专注于提供原装正版的SolidWorks系列软件,以及全方位的技术支持与解决方案…

redis-基础篇(2)

黑马redis-基础篇笔记 3. redis的java客户端-Jedis 在Redis官网中提供了各种语言的客户端,地址:https://redis.io/docs/clients/ 标记为❤的就是推荐使用的java客户端,包括: Jedis和Lettuce:这两个主要是提供了Redi…

好用的linux一键换源脚本

最近发现一个好用的linux一键换源脚本&#xff0c;记录一下 官方链接 大陆使用 bash <(curl -sSL https://linuxmirrors.cn/main.sh)# github地址 bash <(curl -sSL https://raw.githubusercontent.com/SuperManito/LinuxMirrors/main/ChangeMirrors.sh) # gitee地址 …

C++第二学期期末考试选择题题库(qlu题库,自用)

又到了期末周&#xff0c;突击一下c吧— 第一次实验 1、已知学生记录的定义为&#xff1a; struct student { int no; char name[20]; char sex; struct 注意年月日都是结构体&#xff0c;不是student里面的 { int year; int month; …

Bureau of Contacts延迟高、卡顿、无法联机怎么办?

Bureau of Contacts是一款最多支持四个人联机玩的恐怖游戏&#xff0c;由MIROWIN开发并发行&#xff0c;6月20日在steam推出抢先体验版&#xff0c;相信喜欢恐怖游戏的玩家已经等不及了。玩家会扮演一名特工&#xff0c;接触并调查超自然现象&#xff0c;游戏分为调查和驱魔两个…

深入理解和实现Windows进程间通信(消息队列)

常见的进程间通信方法 常见的进程间通信方法有&#xff1a; 管道&#xff08;Pipe&#xff09;消息队列共享内存信号量套接字 下面&#xff0c;我们将详细介绍消息队列的原理以及具体实现。 什么是消息队列&#xff1f; Windows操作系统使用消息机制来促进应用程序与操作系…

大模型什么时候应该进行微调

经常会遇到一个问题——LinkedIn 上的人们问我如何微调 LLaMA 等开源模型&#xff0c;试图找出销售 LLM 托管和部署解决方案的业务案例的公司&#xff0c;以及试图利用人工智能和大模型应用于他们的产品。但当我问他们为什么不想使用像 ChatGPT 这样的闭源模型时&#xff0c;他…

示例:WPF中如何绑定ContextMenu和Menu

一、目的&#xff1a;开发过程中&#xff0c;有些模块的右键ContextMenu菜单是需要动态显示的&#xff0c;既是根据不同条件显示不同的菜单&#xff0c;很多是通过代码去生成ContextMenu的MenuItem&#xff0c;本文介绍通过绑定的方式去加载ContextMenu&#xff0c;Menu菜单栏的…

工厂ESOP系统促进工厂快速响应和工艺改进

在当今追求可持续发展和创新的时代&#xff0c;新能源产业正以惊人的速度崛起。新能源工厂作为这一领域的核心生产环节&#xff0c;面临着不断提高效率、优化工艺和快速应用新技术的巨大挑战。为了应对这些挑战&#xff0c;越来越多的新能源工厂开始引入 ESOP 系统&#xff08;…

数据结构与算法3---栈与队

一、栈 1、顺序栈 #define _CRT_SECURE_NO_WARNINGS 1 #include <stdio.h> #include <stdlib.h> //开辟空间#define MAXSIZE 50//顺序栈的基本算法 typedef struct {int stack[MAXSIZE];int top; }SqStack;//初始化 void InitStack(SqStack* S) {S->top …

OCC异常处理机制理解

1.目的 异常处理提供了一种将控制权从正在执行的程序中的给定点转移到与先前执行的另一点关联的异常处理程序的方法。在各种错误条件下引发异常&#xff0c;该异常会中断其正常执行并将控制权传递给捕获此异常的处理程序&#xff0c;以保护软件质量。OCC作为开源的几何算法库&…

LabVIEW程序退出后线程仍在运行问题

LabVIEW程序退出后&#xff0c;线程仍在运行的问题可能源于资源管理不当、未正确终止循环、事件结构未处理、并发编程错误以及外部库调用未结束等方面。本文将从这些角度详细分析&#xff0c;探讨可能的原因和解决方案&#xff0c;并提供预防措施&#xff0c;帮助开发者避免类似…

昇思25天学习打卡营第2天|张量Tensor

一、张量的定义&#xff1a; 张量是一种特殊的数据结构&#xff0c;与数组和矩阵非常相似。张量&#xff08;Tensor&#xff09;是MindSpore网络运算中的基本数据结构&#xff08;也是所有深度学习模型的基础数据结构&#xff09;&#xff0c;下面将主要介绍张量和稀疏张量的属…

重学java 79.JDK新特性 ⑤ JDK8之后的新特性

别怕失败&#xff0c;大不了重头再来 —— 24.6.20 一、接口的私有方法 Java8版本接口增加了两类成员: 公共的默认方法 公共的静态方法 Java9版本接口又新增了一类成员: 私有的方法 为什么IDK1.9要允许接口定义私有方法呢? 因为我们说接口是规范&#xff0c;规范是…

NetSuite Inventory Transfer Export Saved Search

用户之前有提出一个实际的需求&#xff0c;大致意思是想要导出Inventory Transfer的相关明细行信息&#xff0c;且要包含From Location&#xff0c;To Location&#xff0c;Quantity等信息。 我们知道From Location和To Location在IT Form中应该是在Main的部分&#xff0c;在D…

办公技能——如何写好会议纪要,提升职业素养

一、什么是会议纪要 会议纪要是一种记载、反映会议情况和议定事项的纪实性公文&#xff0c;是贯彻落实会议精神、指导工作、解决问题、交流经验的重要工具。 会议纪要可以多向行文&#xff1a;向上级机关汇报会议情况&#xff0c;以便得到上级机关对工作的指导&#xff1b;向同…

Element-UI实现el-dialog弹框拖拽功能

在实际开发中&#xff0c;会发现有些系统&#xff0c;弹框是可以在浏览器的可见区域自由拖拽的&#xff0c;这极大方便用户的操作。但在查看Element-UI中弹框&#xff08;el-dialog&#xff09;组件的文档时&#xff0c;发现并未实现这一功能。不过也无须担心&#xff0c;vue中…

【Linux从入门到放弃】进程地址空间

&#x1f9d1;‍&#x1f4bb;作者&#xff1a; 情话0.0 &#x1f4dd;专栏&#xff1a;《Linux从入门到放弃》 &#x1f466;个人简介&#xff1a;一名双非编程菜鸟&#xff0c;在这里分享自己的编程学习笔记&#xff0c;欢迎大家的指正与点赞&#xff0c;谢谢&#xff01; 进…