每个用户都拥有一个唯一的用户名和一个与之相关的数值型用户标识符(UID)。用户可以隶属于一个或多个组。而每个组也都拥有唯一的一个名称和一个组标识符(GID)。用户和组 ID 的主要用途有二:其一,确定各种系统资源的所有权;其二,对赋予进程访问上述资源的权限加以控制。
8.1 密码文件:/etc/passwd
针对系统的每个用户账号,系统密码文件/etc/passwd 会专列一行进行描述。每行都包含 7个字段,之间用冒号分隔,如下所示:
- 登录名:登录系统时,用户所必须输入的唯一名称。
- 经过加密的密码:该字段包含的是经过加密处理的密码,长度为 13 个字符。如果密码字段中包含了任何其他字符串,特别是,当字符串长度超过 13 个字符时,将禁止此账户登录,原因是此类字符串不能代表一个经过加密的有效密码。
- 用户 ID(UID):用户的数值型 ID。如果该字段的值为 0,那么相应账户即具有特权级权限。这种账号一般只有一个,其登录名为 root。
- 组 ID(GID):用户属组中首选属组的数值型 ID。关于用户与属组之间从属关系的进一步信息,会在系统组文件中加以定义。
- 注释:该字段存放关于用户的描述性文字。诸如 finger(1)之类的各种程序会显示此信息。
- 主目录:用户登录后所处的初始路径。会以该字段内容来设置 HOME 环境变量。
- 登录 shell:一旦用户登录,便交由该程序控制。通常,该程序为 shell 的一种(比如,bash),但也可以是其他任何程序。
8.2 shadow 密码文件:/etc/shadow
shadow 密码文件包含有登录名(用来匹配密码文件中的相应记录)、经过加密的密码,以及其他若干与安全性相关的字段。
8.3 组文件:/etc/group
对用户所属各组信息的定义由两部分组成:一,密码文件中相应用户记录的组 ID 字段;二,组文件列出的用户所属各组。
系统中的每个组在组文件/etc/group 中都对应着一条记录。每条记录包含 4 个字段,之间以冒号分隔,如下所示:
- 组名:组的名称。与密码文件中的登录名相似,可以将其视为与数值型组标识符相对应的人类可读(符号)标识符。
- 经过加密处理的密码:组密码属于非强制特性,对应于该字段。如果用户并非某组的成员,那么在使用 newgrp(1)(groups(1)命令会显示当前 shell 进程所属各组的信息,如果将一个或多个用户名作为其命令行参数,那么该命令将显示相应用户所属各组的信息。)启动新 shell 之前(新 shell 的属组包括该组),就需要用户提供此密码。如果启用了shadow 密码,那么系统将不解析该字段(这时,该字段通常只包含字母 x,但也允许其内容为包括空字符串在内的任何字符串),而经过加密的密码实际上则存放于shadow 组文件/etc/gshadow 中,仅供具有特权的用户和程序访问。组密码的加密方式类似于用户密码。
- 组 ID(GID):该组的数值型 ID。正常情况下,对应于组 ID 号 0,只定义一个名为 root的组(与/etc/passwd 中用户 ID 为 0 的记录相近)。
- 用户列表:属于该组的用户名列表,之间以逗号分隔。
8.4 获取用户和组的信息
8.4.1 从密码文件获取记录
函数 getpwnam()和 getpwuid()的作用是从密码文件中获取记录。
struct passwd *getpwnam(const char *name); /* 成功返回一个指向passwd类型的指针,错误返回NULL且不会改变 errno */
struct passwd *getpwuid(uid_t uid); /* 成功返回一个指向passwd类型的指针,错误返回NULL且不会改变 errno */
passwd类型的结构如下:
struct passwd
{
char *pw_name; /* 用户名 */
char *pw_passwd; /* 加密密码,仅当未启用 shadow 密码的情况下,pw_passwd 字段才会包含有效信息。 */
uid_t pw_uid; /* 用户ID */
gid_t pw_gid; /* 组ID */
char *pw_gecos; /* 用户信息 */
char *pw_dir; /* 家目录 */
char *pw_shell; /* 登录shell */
};
函数 getpwuid()的返回结果与 getpwnam()完全一致,但会使用提供给 uid 参数的数值型用户 ID 作为查询条件。getpwnam()和 getpwuid()均会返回一个指针,指向一个静态分配的结构。对此二者的任何一次调用都会改写该数据结构。
【注】由于 getpwnam()和 getpwuid()返回的指针指向由静态分配而成的内存,故而二者都是不可重入的(not reentrant)。实际上,情况甚至要更加复杂,因为返回的 passwd 结构还包含了指向其他信息(比如,pw_name)的指针,而这些信息同样也是由静态分配而成的。
SUSv3 规定,如果在 passwd 文件中未发现匹配记录,那么 getpwnam()和 getpwuid()将返回 NULL,且不会改变 errno。这意味着,可以使用如下代码,对出错和“未发现匹配记录”这两种情况加以区分:
struct passwd *pwd;
errno = 0;
pwd = getpwnam(name);
if(pwd == NULL)
{
if(errno == 0)
/* Not found */;
else
/* Error */;
}
8.4.2 从组文件获取记录
函数 getgrnam()和 getgrgid()的作用是从组文件中获取记录。
struct group *getgrnam(const char *name); /* 成功返回一指向group类型的指针,错误返回NULL */
struct group *getgrgid(gid_t gid); /* 成功返回一指向group类型的指针,错误返回NULL */
group类型的结构如下:
struct group
{
char *gr_name; /* 组名 */
char *gr_passwd; /* 加密密码,仅当未启用 shadow 密码的情况下,pw_passwd 字段才会包含有效信息。 */
gid_t gr_gid; /* 组ID */
char **gr_men; /* 以 NULL 结尾的指针数组,指向 /etc/group 中列出的成员名称 */
};
与前述密码相关函数一样,对这两个函数的任何一次调用都会改写该结构的内容 。如果未能在 group 文件中发现匹配记录,那么这两个函数的行为变化与前述 getpwnam()和 getpwuid()函数相同。
8.4.3 程序示例
/**************************ugid_functions.c**************************/
#include <pwd.h>
#include <grp.h>
#include <ctype.h>
#include "ugid_functions.h" /* Declares functions defined here */
/* 通过UID获取username */
char * /* Return name corresponding to 'uid', or NULL on error */
userNameFromId(uid_t uid)
{
struct passwd *pwd;
pwd = getpwuid(uid);
return (pwd == NULL) ? NULL : pwd->pw_name;
}
/* 通过username获取UID */
uid_t /* Return UID corresponding to 'name', or -1 on error */
userIdFromName(const char *name)
{
struct passwd *pwd;
uid_t u;
char *endptr;
if (name == NULL || *name == '\0') /* NULL或者空字节 */
return -1; /* 错误 */
u = strtol(name, &endptr, 10); /* strtol()函数会尝试将name中的字符串转换为长整型数。 */
if (*endptr == '\0') /* 如果为(纯)数值的字符串形式,将字符串转换为数字返回给调用者 */
return u;
pwd = getpwnam(name);
if (pwd == NULL)
return -1;
return pwd->pw_uid;
}
/* 通过GID获取groupname */
char * /* Return name corresponding to 'gid', or NULL on error */
groupNameFromId(gid_t gid)
{
struct group *grp;
grp = getgrgid(gid);
return (grp == NULL) ? NULL : grp->gr_name;
}
/* 通过groupname获取GID */
gid_t /* Return GID corresponding to 'name', or -1 on error */
groupIdFromName(const char *name)
{
struct group *grp;
gid_t g;
char *endptr;
if (name == NULL || *name == '\0') /* On NULL or empty string */
return -1; /* return an error */
g = strtol(name, &endptr, 10); /* strtol()函数会尝试将name中的字符串转换为长整型数。 */
if (*endptr == '\0') /* 如果为(纯)数值的字符串形式,将字符串转换为数字返回给调用者 */
return g;
grp = getgrnam(name);
if (grp == NULL)
return -1;
return grp->gr_gid;
}
8.4.4 扫描密码文件和组文件中的所有记录
函数 setpwent()、getpwent()和 endpwent()的作用是按顺序扫描密码文件中的记录。
struct passwd *getpwent(void); /* 成功返回指向passwd类型的指针,抵达流末端(或出错)时返回NULL */
void wetpwent(void);
void endpwent(void);
可使用以下代码遍历整个密码文件,并打印出登录名和用户 ID。
struct passwd *pwd;
while((pwd = getpwent()) != NULL)
print("%-8s %5ld\n", pwd->pw_name, (long) pwd->pw_uid);
endpwent();
如果需要让后续的 getpwent()调用(也许是在程序的其他代码中,也许是在所调用的其他库函数中,该函数再次出现)再次打开密码文件并重启扫描过程,此处的 endpwent()调用就必不可少。此外,如果对该文件处理到中途时,还可以调用 setpwent()函数重返文件起始处。
8.4.5 从 shadow 密码文件中获取记录
下列函数的作用包括从 shadow 密码文件中获取个别记录,以及扫描该文件中的所有记录。
struct spwd *getspnam(const char *name); /* 成功返回指向spwd类型的指针,没找到或者错误返回NULL */
struct spwd *getspent(void); /* 成功返回指向spwd类型的指针,抵达流末端(或出错)时返回NULL */
void setspent(void);
void endspent(void);
spwd类型的结构如下:
struct spwd
{
char *sp_namp; /* 用户名 */
char *sp_pwdp; /* 加密密码 */
long sp_lstchg; /* 最后一次更改密码的时间 */
long sp_min; /* 密码更改之间的最小天数 */
long sp_max; /* 密码更改前的最大天数要求——用户在必须更改其密码之前可以保持当前密码不变的最长天数。*/
long sp_warn; /* 在用户账户的密码即将到期之前,系统提前向用户发出警告的天数 */
long sp_inact; /* 在密码过期后账户被视为不活跃并被锁定的天数 */
long sp_expire; /* 帐户到期日期 */
unsigned long sp_flag; /* Reserved for future use */
}