第 8 章 用户与组 (Users and Groups)
核心结论
-
UID 与 GID:每个用户有唯一数字 UID 和用户名;每个组有 GID;UID 0 是超级用户(root);Linux 2.4+ 用 32 位 UID/GID(旧系统 16 位)。
-
/etc/passwd:7 字段(用户名:x:UID:GID:comment:home:shell);现代系统 x 表示密码在 /etc/shadow。
-
/etc/shadow:仅 root 可读;存加密密码、过期策略等安全字段;解决 /etc/passwd 可被任意用户读带来的密码破解风险。
-
/etc/group:4 字段(组名:x:GID:成员列表);用户的初始组在 /etc/passwd 的 GID 字段,其他组在 /etc/group 的成员列表。
-
信息查询函数:
getpwnam/getpwuid(按名/UID 查用户)、getgrnam/getgrgid(按名/GID 查组)、getpwent/getgrent(遍历所有记录);_r后缀版本可重入。 -
crypt():密码加密函数;基于 DES(13 字符)或 MD5(34 字符以
$1$开头);盐值(salt)使同一密码产生不同密文。 -
密码影子化 (shadow passwords):把加密密码从 /etc/passwd 移到 /etc/shadow;后者只对 root 可读——降低密码被字典攻击的风险。
|
本章主旨
本章介绍 Linux 系统的用户与组管理。核心:(1) /etc/passwd、/etc/shadow、/etc/group 三个关键文件的结构;(2) 用 getpwnam/getpwuid/getgrnam/getgrgid 查询这些文件;(3) crypt() 加密密码的机制。本章不展开进程凭证(real/effective UID 等),那是第 9 章;不展开 ACL,那是第 17 章。 |
一、核心概念
本章围绕 6 个核心概念展开:从「系统如何标识用户」到「密码文件结构」再到「查询与加密」。
| 概念 | 定义 + 重要性 | 实现提示 |
|---|---|---|
UID 与 GID |
数字标识用户与组;UID 0 是 root;32 位(Linux 2.4+);系统中每个进程/文件都关联 UID/GID。 |
§8.0;用户名/组名只是方便人阅读;内核只用 UID/GID。 |
/etc/passwd 结构 |
7 字段:login:x:UID:GID:gecos:home:shell;现代密码字段是 x(密码在 shadow)。 |
§8.1;任何用户可读;提供用户名→UID 的映射。 |
/etc/shadow 结构 |
仅 root 可读;存加密密码、最后修改日期、最小/最大修改间隔、过期警告、账号过期日等。 |
§8.2;解决 /etc/passwd 公开可读带来的密码破解风险。 |
/etc/group 结构 |
4 字段:groupname:x:GID:user1,user2,…;用户的初始组在 passwd GID 字段,其他组在此列出。 |
§8.3;用户可同时属于多个组(POSIX.1-1990 起)。 |
getpwnam / getpwuid / getgrnam / getgrgid |
按名字/UID 查用户;按名字/GID 查组;返回静态分配的结构(不可重入); |
§8.4;返回 NULL 时检查 errno 区分「找不到」vs「错误」。 |
crypt() 与盐值 |
密码加密函数;DES 输出 13 字符;MD5 输出 34 字符(以 |
§8.5; |
二、详细笔记
8.1 /etc/passwd 文件结构
What:/etc/passwd 每行一个用户,7 个冒号分隔字段;现代系统密码字段是 x(密码在 shadow 文件)。
Why:理解 /etc/passwd 是理解用户身份、登录、home 目录、shell 的基础;几乎所有用户相关程序都解析这个文件。
How:字段顺序(§8.1):
mtk:x:1000:100:Michael Kerrisk:/home/mtk:/bin/bash
(1) (2)(3) (4) (5) (6) (7)
(1) 登录名(唯一)
(2) 密码字段(x 表示 shadow)
(3) UID(1000 表示普通用户)
(4) 初始组 GID
(5) GECOS(注释,通常是用户全名)
(6) home 目录
(7) 登录 shell
特殊值:
-
UID 0 → root(超级用户)。
-
UID 1-99 → 系统用户/伪用户(bin、daemon、nobody 等)。
-
UID 100-999 → 一些发行版的系统用户。
-
UID 1000+ → 普通用户。
-
GID 0 → root 组。
-
密码字段为空 → 无需密码登录。
-
密码字段为
*或!→ 账号禁用。 -
shell 字段为空 → 默认
/bin/sh。
When:
-
想看当前用户信息——
cat /etc/passwd | grep ^$(whoami)。 -
程序需要用户名/UID 转换——用
getpwnam/getpwuid(不要直接解析文件)。 -
NIS/LDAP——底层替换 passwd 文件,但 API 不变。
Example:
// 摘自《The Linux Programming Interface》第 8 章
#include <pwd.h>
struct passwd *pwd = getpwnam("mtk");
if (pwd != NULL) {
printf("Login: %s\n", pwd->pw_name);
printf("UID: %ld\n", (long) pwd->pw_uid);
printf("Home: %s\n", pwd->pw_dir);
printf("Shell: %s\n", pwd->pw_shell);
}
8.2 /etc/shadow 文件结构
What:/etc/shadow 存加密密码与安全策略字段;仅 root 可读;解决 /etc/passwd 公开可读带来的密码破解风险。
Why:历史 /etc/passwd 含加密密码且任意用户可读——攻击者可用字典攻击;shadow 文件限制访问。
How:字段(§8.2):
mtk:$6$salt$hash:17000:0:99999:7:::
(1) (2) (3) (4) (5) (6)(7)(8)
(1) 登录名
(2) 加密密码($6$ 表示 SHA-512)
(3) 最后修改日期(17000 天自 Epoch)
(4) 密码最小修改间隔(0)
(5) 密码最大有效天数(99999)
(6) 过期前警告天数(7)
(7) 账号过期日(未设置)
(8) 保留字段
加密密码格式:
-
$id$salt$hash: -
$1$→ MD5(34 字符总长)。 -
$5$→ SHA-256。 -
$6$→ SHA-512。 -
旧 DES(13 字符)无
$id$前缀。
When:
-
验证用户密码——读取 shadow 文件(需要 root),取密文,用
crypt(plain, salt)比较。 -
修改密码——需要 root,调用
passwd(1)或编程用crypt+ 修改 shadow。 -
大多数程序不直接读 shadow——用
getspnam()(特权)。
Example:
// 摘自《The Linux Programming Interface》第 8 章
// 验证密码(需要 root 读 shadow)
#include <shadow.h>
struct spwd *sp = getspnam("mtk");
if (sp != NULL && sp->sp_pwdp != NULL) {
char *encrypted = crypt(input_password, sp->sp_pwdp);
if (encrypted != NULL && strcmp(encrypted, sp->sp_pwdp) == 0)
printf("password OK\n");
}
8.3 /etc/group 文件结构
What:/etc/group 每行一个组,4 个冒号分隔字段;用户可属于多个组(POSIX.1-1990 起)。
Why:组用于文件权限(chmod g+rwx)、协作(团队共享文件)、权限管理。
How:字段(§8.3):
users:x:100:
jambit:x:106:claus,felli,frank,harti,markus,martin,mtk,paul
(1) (2)(3) (4)
(1) 组名
(2) 加密组密码(x 表示 shadow gshadow)
(3) GID
(4) 成员列表(逗号分隔的用户名)
组成员关系来源:
-
用户在
/etc/passwd的 GID 字段 → 初始组(必属于)。 -
用户在
/etc/group的某条目的「成员列表」→ 额外组。
When:
-
看用户属于哪些组——
groups mtk命令。 -
临时改变主组——
newgrp groupname。 -
程序查询——
getgrnam/getgrgid或getgroups()(当前进程所属组)。
Example:
// 摘自《The Linux Programming Interface》第 8 章
#include <grp.h>
struct group *grp = getgrnam("users");
if (grp != NULL) {
printf("Group: %s\n", grp->gr_name);
printf("GID: %ld\n", (long) grp->gr_gid);
printf("Members: ");
for (char **m = grp->gr_mem; *m != NULL; m++)
printf("%s ", *m);
printf("\n");
}
8.4 查询用户与组信息
What:getpwnam/getpwuid/getgrnam/getgrgid 查询单个记录;getpwent/getgrent 顺序遍历所有记录;_r 后缀版本可重入。
Why:写程序时不应直接解析 /etc/passwd——应该用这些 API,让 NIS/LDAP 透明工作。
How:
// 摘自《The Linux Programming Interface》第 8 章
#include <pwd.h>
#include <grp.h>
struct passwd *getpwnam(const char *name); // 按名字查用户
struct passwd *getpwuid(uid_t uid); // 按 UID 查用户
struct group *getgrnam(const char *name); // 按名字查组
struct group *getgrgid(gid_t gid); // 按 GID 查组
// 顺序遍历
struct passwd *getpwent(void); // 下一个 passwd 记录
void setpwent(void); // 重新从文件头开始
void endpwent(void); // 关闭文件
可重入版本(多线程安全):
// 摘自《The Linux Programming Interface》第 8 章
int getpwnam_r(const char *name, struct passwd *pwd,
char *buf, size_t buflen, struct passwd **result);
// 缓冲区大小通过 sysconf(_SC_GETPW_R_SIZE_MAX) 查询
「未找到」vs「错误」的区分(§8.4):
-
SUSv3:未找到时
getpwnam返回 NULL 且errno不变。 -
旧实现:未找到时设置
errno为ENOENT或ESRCH。 -
正确做法:
errno = 0; pwd = getpwnam(name); if (pwd == NULL) { if (errno == 0) not_found; else error; }
When:
-
写 UID ↔ 用户名转换函数——用
getpwnam_r/getpwuid_r。 -
列出所有用户——
getpwent+ 循环 +endpent。 -
多线程程序——必须用
_r版本。
Example:
// 摘自《The Linux Programming Interface》第 8 章 users_groups/ugid_functions.c
uid_t userIdFromName(const char *name) {
if (name == NULL || *name == '\0') return -1;
char *endptr;
uid_t u = strtol(name, &endptr, 10);
if (*endptr == '\0') return u; // 接受纯数字字符串
struct passwd *pwd = getpwnam(name);
return (pwd != NULL) ? pwd->pw_uid : -1;
}
8.5 密码加密:crypt()
What:crypt(plain, salt) 加密字符串;DES 输出 13 字符;MD5/SHA-256/SHA-512 输出更长;同一 salt 下加密结果确定。
Why:登录验证需要「比较密码」——但不应存明文;用 crypt 加密后存密文;登录时再次 crypt 并比较。
How:
// 摘自《The Linux Programming Interface》第 8 章
#include <unistd.h>
char *crypt(const char *key, const char *salt);
// key: 明文密码
// salt: 2 字符(DES)或 `$id$salt$`(MD5/SHA)
// 返回 13 字符(DES)或更长(MD5/SHA)
// 返回静态分配字符串的指针
盐值(salt)的作用:
-
防止字典攻击——攻击者无法预计算常见密码的密文。
-
同一密码不同 salt → 不同密文——避免泄露「两个用户密码相同」。
示例:
$ python3 -c "import crypt; print(crypt.crypt('hello', '\$6\$salt\$'))"
$6$salt$IxDD3Ea...
$ python3 -c "import crypt; print(crypt.crypt('hello', '\$6\$other\$'))"
$6$other$D4VaG9...
When:
-
实现登录验证——
crypt(input, stored_hash)比较。 -
修改密码——
crypt(new_plain, new_salt)存新密文。 -
现代建议用 SHA-512(
$6$),不用 DES(已不安全)。
Example:
// 摘自《The Linux Programming Interface》第 8 章
// 验证密码
char *stored_hash = "$6$salt$IxDD3Ea..."; // 从 /etc/shadow 读
char *input = getpass("Password: "); // 不回显读入
char *computed = crypt(input, stored_hash);
if (computed != NULL && strcmp(computed, stored_hash) == 0)
printf("OK\n");
else
printf("FAIL\n");
8.6 进程初始凭证
What:登录 shell 启动时,从 /etc/passwd 的 UID/GID 字段设置进程凭证;之后 fork/exec 继承;详见第 9 章。
Why:理解凭证从哪来才能理解登录流程;理解为何 UID 在登录时确定。
How:登录流程简述(§8.0):
-
用户输入用户名密码。
-
login(1)程序读 /etc/shadow,crypt 比较。 -
如果通过——从 /etc/passwd 读 UID、GID、home、shell。
-
setuid(UID),setgid(GID),chdir(home),exec(shell)。 -
shell 启动;从 /etc/group 读 supplementary groups;
setgroups()设置。 -
shell 读 /.bashrc、/.profile 等初始化环境。
When:
-
写 set-UID 程序——临时改变 effective UID(第 9 章)。
-
守护进程——以特定 UID 运行(
setuid后 fork)。 -
多用户系统——凭证决定文件访问权限。
三、关键图表
|
非可视化条目(关键文件与 API 速查)
|
四、思维导图
mindmap
root((第 8 章 用户与组))
UID GID
32 位数字
UID 0 root
数字标识
passwd 文件
7 字段
login x UID GID
gecos home shell
任何用户可读
shadow 文件
仅 root 可读
加密密码
过期策略
解决字典攻击
group 文件
4 字段
groupname x GID members
多组成员
查询 API
getpwnam getpwuid
getgrnam getgrgid
getpwent 遍历
_r 可重入
密码加密
crypt 函数
DES 13 字符
MD5 SHA-512
盐值 salt
登录流程
login 程序
setuid setgid
chdir exec shell
初始化环境
五、重点与易错点
-
UID 0 = root:超级用户可绕过所有权限检查;任何 UID 0 进程都是特权进程。
-
现代 UID 是 32 位:Linux 2.4+ 突破旧的 16 位限制(最大 65535)。
-
/etc/passwd 中密码字段是 x:实际密码在 /etc/shadow;密码字段为空表示无密码。
-
/etc/shadow 仅 root 可读:用
getspnam()需要特权;普通程序不要尝试读。 -
getpwnam 返回静态结构:多线程必须用
_r版本;否则第二次调用覆盖第一次结果。 -
getpwnam 「找不到」 vs 「错误」:必须先
errno = 0,再根据返回值和 errno 区分。 -
crypt 的 salt 来自已存的密文:验证密码时 salt 是密文的前 2 字符(或
$id$salt$);不要生成新 salt。 -
不要自己写密码验证:用 PAM(Pluggable Authentication Modules);第 8 章只讨论底层机制。
-
组信息的多源:用户的初始组在 /etc/passwd GID 字段;其他组在 /etc/group 成员列表。
-
跨章衔接:第 9 章展开 real/effective UID、set-UID 程序、密码数据库
/etc/passwd、/etc/shadow的权限;本章是基础的身份信息查询;ACL 在第 17 章。