目录
UNIX 系统的正常运作需要使用大量的与系统有关的数据文件。 例如用户登录 UNIX 系统时,或者执行 ls -l 等命令时,都需要从这些数据文件获取必要的数据
在不同系统环境下,这些数据文件所储存的内容与格式,甚至数据文件名都可能不太一样。标准提供了读取不同系统下数据文件的接口
后续将简单介绍一下相关数据文件,并介绍获取数据文件内容的接口。注意:所介绍的数据文件依赖于当前系统环境(毕竟如果所有系统环境的数据文件都一样,还需要统一的接口干嘛?直接从文件里读取数据不就得了)
第一个介绍的数据文件为 /etc/passwd,首先查看该文件:
vim /etc/passwd
这是在 Ubuntu 某 LTS 版下的文件内容:
针对其中某两条(行)进行分析:
这里用户密码部分以 x 显示,原因后面再介绍
下面介绍读取数据文件的接口(接口是为了统一与标准化)
man 3 getpwnam
- #include
- #include
-
- struct passwd * getpwnam(const char *name);
功能:用于获取 /etc/passwd 文件中内容的接口(通过用户名获取)
其中,struct passwd 内容如下:可以看出一个结构体中的各个成员就对应了 /etc/passwd 文件中的一行的不同字段
- struct passwd {
- char *pw_name; /* username */
- char *pw_passwd; /* user password */
- uid_t pw_uid; /* user ID */
- gid_t pw_gid; /* group ID */
- char *pw_gecos; /* user information */
- char *pw_dir; /* home directory */
- char *pw_shell; /* shell program */
- };
man 3 getpwuid
- #include
- #include
-
- struct passwd *getpwuid(uid_t uid);
功能:用于获取 /etc/passwd 文件中内容的接口(通过用户 ID 获取)
代码示例
- #include
- #include
- #include
- #include
- #include
-
- int main(int argc, char * argv[]) {
-
- if (argc < 3) {
- // 检验命令行参数
- fprintf(stderr, "Usage: %s <-uid/-n>
\n" , argv[0]); - exit(-1);
- }
- struct passwd * pwline;
- if (strcmp(argv[1],"-uid") == 0) { // 注意字符串的比较方式
- pwline = getpwuid(atoi(argv[2])); // 需要将char*进行转换
- if (pwline == NULL) { // 错误检查
- printf("Not found or error!\n");
- exit(-1);
- }
- } else if (strcmp(argv[1], "-n") == 0) {
- pwline = getpwnam(argv[2]);
- if (pwline == NULL) {
- printf("Not found or error!\n");
- exit(-1);
- }
- } else {
- exit(-1);
- }
- printf("username: %s, user ID: %d, group ID: %d, user password: %s\n", pwline->pw_name, pwline->pw_uid, pwline->pw_gid, pwline->pw_passwd);
- exit(0);
-
- }
可以看出,能够通过用户名或者用户 ID,并依靠接口获取 /etc/passwd 数据文件中的信息
第二个介绍的数据文件为 /etc/group,首先查看该文件:
vim /etc/group
这是在 Ubuntu 某 LTS 版下的文件内容:
针对其中某条进行分析:
这里组密码部分以 x 显示,原因后面再介绍
下面介绍读取数据文件的接口(接口是为了统一与标准化)
man 3 getgrnam
- #include
- #include
-
- struct group *getgrnam(const char *name);
功能:用于获取 /etc/group 文件中内容的接口(通过组名获取)
其中,struct group 内容如下:可以看出一个结构体中的各个成员就对应了 /etc/group 文件中的一行的不同字段
- struct group {
- char *gr_name; /* group name */
- char *gr_passwd; /* group password */
- gid_t gr_gid; /* group ID */
- char **gr_mem; /* NULL-terminated array of pointers to names of group members */
- };
man 3 getgrgid
- #include
- #include
-
- struct group *getgrgid(gid_t gid);
功能:用于获取 /etc/group 文件中内容的接口(通过组 ID 获取)
取出一行出来分析,这里主要分析第二个字段的内容:这个字段的内容就是加密密码
由于 /etc/passwd 文件对所有用户都可读,所以将用户密码存放于 /etc/passwd 是一个安全隐患。因此,现在许多 Linux 系统都使用了 shadow 技术,把真正的加密后的用户密码(也称口令)字段存放到 /etc/shadow 文件中,而在 /etc/passwd 文件的口令字段中只存放一个特殊的字符,例如 “x” 或者 “*”
首先,加密一定要能解密!
问:hash 是加密吗?
答:不是,hash 最多算混淆,因为多个不同的原值可能通过 hash 映射到相同的 hash 值,那么从 hash 值恢复出唯一原值是无法实现的,即加密之后无法解密了
其次,即使相同的原串,经过加密,也应该得到不同的加密后串
为什么要这样?如果相同的原串加密后得到相同的加密后串会怎么样?下面讲一个故事
因此,相同原串能够加密得到不同加密后串,主要目的是为了防止系统管理员监守自盗
接下来继续介绍 /etc/shadow 第二个字段的内容,看看 UNIX 系统如何实现加密的
一个条加密密码由下面三部分组成(缺一不可):
根据加密方式与杂字串,能够反推并运算得到加密前的串,故满足能解密的条件
即使是相同的原串,由于系统针对不同的用户提供不同杂字串,故即使原串,与不同的杂字串或运算后,再处理得到的串也不同,所得到的加密后串不同,故满足相同原串可得到不同加密后串
下面介绍读取数据文件的接口(接口是为了统一与标准化)
man 3 getspnam
- #include
-
- struct spwd *getspnam(const char *name);
功能:用于获取 /etc/shadow 文件中内容的接口(通过登录名获取)
其中,struct spwd 内容如下:一个结构体中的各个成员对应了 /etc/shadow 文件中的一行的不同字段(以 : 分隔一行的不同字段值)
- struct spwd {
- char *sp_namp; /* Login name */
- char *sp_pwdp; /* Encrypted password */
- long sp_lstchg; /* Date of last change (measured in days since 1970-01-01 00:00:00 +0000 (UTC)) */
- long sp_min; /* Min # of days between changes */
- long sp_max; /* Max # of days between changes */
- long sp_warn; /* # of days before password expires to warn user to change it */
- long sp_inact; /* # of days after password expires until account is disabled */
- long sp_expire; /* Date when account expires(measured in days since 1970-01-01 00:00:00 +0000 (UTC)) */
- unsigned long sp_flag; /* Reserved */
- };
- struct spwd {
- char *sp_namp; /* 登录名 */
- char *sp_pwdp; /* 加密密码 */
- long sp_lstchg; /* 上次更改日期(以 1970-01-01 00:00:00 +0000 (UTC) 后的天数为单位) */ */
- long sp_min; /* 两次更改之间的最短间隔天数 */
- long sp_max; /* 最大更改间隔天数 */
- long sp_warn; /* 密码过期前警告用户更改密码的天数 */
- long sp_inact; /* 密码过期后直到账户被禁用的天数 */
- long sp_expire; /* 帐户过期日期(以 1970-01-01 00:00:00 +0000 (UTC) 后的天数为单位) */
- unsigned long sp_flag; /* 保留 */
- };
需要注意一点:即使是通过这个接口获取 /etc/shadow 中的内容,也需要调用这个接口的用户有能够访问 /etc/shadow 的权限!
在此之前需要先补充几个函数
- #define _XOPEN_SOURCE
- #include
-
- char *crypt(const char *key, const char *salt);
-
- Link with -lcrypt
功能:用于给串进行加密
- #include
-
- char *getpass(const char *prompt);
功能:获取输入密码(想一下登录 root 用户时候的 LINUX 命令行输入密码时候的效果,这个函数就能达到这个效果,本质上是在函数内部暂时关闭了终端的回显)
- #define _XOPEN_SOURCE // 要在所有头文件包含前宏定义
- #include
- #include
- #include
- #include
- #include
-
- int main(int argc, char * argv[]) {
-
- if (argc < 2) {
- fprintf(stderr, "Usage: %s
\n" , argv[0]); - exit(-1);
- }
-
- struct spwd * shadowline = getspnam(argv[1]); // 根据登录名获取/etc/shadow的一行
-
- char * input_pass = getpass("PassWord:"); // 获取输入原串
-
- char * crypted_pass = crypt(input_pass, shadowline->sp_pwdp); // 获取加密后串,按照指定加密方式和杂字串加密
- // 用 与 从/etc/shadow得到的那行的加密串 相同的加密方式和杂字串,对输入原串进行加密
-
- if (strcmp(shadowline->sp_pwdp, crypted_pass) == 0)
- puts("ok!");
- else
- puts("false!");
-
- exit(0);
-
- }
首先介绍一下一个时间的表示方式
UNIX 系统内部对时间的表示方式:time_t 类型的符号整数,表示自 1970 年 1 月 1 日早晨 0 点以来的秒数,又称时间戳
用户喜欢看到的时间表示方式:char * 类型的字符串,直观明了
程序员最容易操作的表示方式:struct tm 结构体,结构体中的字段记录了年月日等详细信息
这几种表示方式之间可以互相转化,相互之间关系如下:
下面介绍上图中的几个比较常用的函数用法
man 2 time
- #include
-
- time_t time(time_t *tloc);
-
- // time() returns the time as the number of seconds since the Epoch, 1970-01-01 00:00:00 +0000 (UTC).
功能:获取当前时间距离 1970 年 1 月 1 日早晨 0 点以来的秒数(即获取时间戳)
因为函数这样设计,那么我们就有两种获取时间戳的方式
- #include
- #include
- #include
-
- int main() {
-
- time_t timestamp;
- time(×tamp); // 或者:timestamp = time(NULL);
- printf("%ld\n", timestamp);
-
- exit(0);
- }
man 3 gmtime
- #include
-
- struct tm *gmtime(const time_t *timep);
功能:将时间戳转化为 UTC 时间,并填充至结构体的各个字段
被填充的结构体中的字段含义如下:
- struct tm {
- int tm_sec; /* Seconds (0-60) */
- int tm_min; /* Minutes (0-59) */
- int tm_hour; /* Hours (0-23) */
- int tm_mday; /* Day of the month (1-31) */
- int tm_mon; /* Month (0-11) */
- int tm_year; /* Year - 1900 */
- int tm_wday; /* Day of the week (0-6, Sunday = 0) */
- int tm_yday; /* Day in the year (0-365, 1 Jan = 0) */
- int tm_isdst; /* Daylight saving time */
- };
因此,我们可以先获取时间戳,再通过时间戳获取这样的一个结构体,从而在结构体中得到我们想要的 UTC 时间信息
man 3 localtime
- #include
-
- struct tm *localtime(const time_t *timep);
功能:将时间戳转化为本地时间,并填充至结构体的各个字段
被填充的结构体与 gmtime 中介绍的结构体相同
gmtime 得到的是 0 时区,把 UTC 时间转换成北京时间的话,需要在年数上加 1900,月份上加 1,小时数加上 8
localtime 得到的是本地时间,该函数同 gmtime 唯一区别是,在转换小时数不需要加上 8 了
man 3 mktime
- #include
-
- time_t mktime(struct tm *tm);
功能:将 struct tm 结构体所代表的时间转化为时间戳
注意:该函数的形参不是 const,说明可能会对实参内容进行改变:怎么改变呢?
if structure members are outside their valid interval, they will be normalized (so that, for example, 40 October is changed into 9 November)
可以用这个特性,计算:从当前时间开始的第1000天是哪一年哪一月哪一日
- #include
- #include
- #include
-
- int main() {
-
- time_t timestamp = time(NULL);
-
- struct tm * tm = localtime(×tamp);
-
- tm->tm_mday += 1000;
-
- mktime(tm); // 自动对tm中的内容进行了标准化处理
-
- printf("%d-%d-%d %d:%d:%d\n", tm->tm_year + 1900, tm->tm_mon, tm->tm_mday, tm->tm_hour, tm->tm_min, tm->tm_sec);
-
- exit(0);
- }
man 3 strftime
- #include
-
- size_t strftime(char *s, size_t max, const char *format, const struct tm *tm);
功能:格式化 struct tm 结构体为字符串
函数根据指定 format 格式化 struct tm,并将结果放入容量为 max 的字符数组中。具体如何通过 format 指定格式详见 man 手册
之前说过,在不同系统环境下,数据文件所储存的内容与格式,甚至数据文件名都可能不太一样。标准提供了读取不同系统下数据文件的接口
为了说明这一点,我们看看在 macOS 系统下,接口 getpwnam 的内容:
仔细看,在macOS 下,getpwnam 是从 /etc/master.passwd 文件中获取数据的,而并不像 ubuntu 下从 /etc/passwd 文件中获取数据,这能够说明不同系统下数据文件是不同的