【APUE】文件系统 — 系统数据和文件信息

目录

一、/etc/passwd

1.1 getpwnam

1.2 getpwuid

二、/etc/group

2.1 getgrnam

2.2 getgrgid

三、/etc/shadow

补充:何为良好的加密? 

3.1 getspnam

3.2 密码校验示例

3.2.1 crypt

3.2.2 getpass

3.2.3 代码实现

四、时间戳相关

4.1 time

4.2 gmtime

4.3 localtime

4.4 mktime

4.5 strftime 

五、补充说明


UNIX 系统的正常运作需要使用大量的与系统有关的数据文件。 例如用户登录 UNIX 系统时,或者执行 ls -l 等命令时,都需要从这些数据文件获取必要的数据

在不同系统环境下,这些数据文件所储存的内容与格式,甚至数据文件名都可能不太一样。标准提供了读取不同系统下数据文件的接口

后续将简单介绍一下相关数据文件,并介绍获取数据文件内容的接口。注意:所介绍的数据文件依赖于当前系统环境(毕竟如果所有系统环境的数据文件都一样,还需要统一的接口干嘛?直接从文件里读取数据不就得了) 


一、/etc/passwd

第一个介绍的数据文件为 /etc/passwd,首先查看该文件:

vim /etc/passwd

这是在 Ubuntu 某 LTS 版下的文件内容:

针对其中某两条(行)进行分析:

这里用户密码部分以 x 显示,原因后面再介绍 

下面介绍读取数据文件的接口(接口是为了统一与标准化) 


1.1 getpwnam

man 3 getpwnam 

#include <sys/types.h>
#include <pwd.h>

struct passwd * getpwnam(const char *name);

功能:用于获取 /etc/passwd 文件中内容的接口(通过用户名获取)

  • name — 在 /etc/passwd 文件中查询用户名为 name 的那一行,并将那一行的内容填充至 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 */
};

1.2 getpwuid

man 3 getpwuid

#include <sys/types.h>
#include <pwd.h>

struct passwd *getpwuid(uid_t uid);

功能:用于获取 /etc/passwd 文件中内容的接口(通过用户 ID 获取)

  • uid — 在 /etc/passwd 文件中查询用户 ID 为 uid 的那一行,并将那一行的内容填充至 passwd 结构体并返回

代码示例 

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <pwd.h>
#include <string.h>

int main(int argc, char * argv[]) {

        if (argc < 3) {
                // 检验命令行参数
                fprintf(stderr, "Usage: %s <-uid/-n> <uid/name>\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

第二个介绍的数据文件为 /etc/group,首先查看该文件:

vim /etc/group

这是在 Ubuntu 某 LTS 版下的文件内容:

针对其中某条进行分析:

这里组密码部分以 x 显示,原因后面再介绍 

下面介绍读取数据文件的接口(接口是为了统一与标准化) 


2.1 getgrnam

man 3 getgrnam

#include <sys/types.h>
#include <grp.h>

struct group *getgrnam(const char *name);

功能:用于获取 /etc/group 文件中内容的接口(通过组名获取) 

  • name — 在 /etc/group 文件中查询组名为 name 的那一行,并将那一行的内容填充至 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 */
};

2.2 getgrgid

man 3 getgrgid

#include <sys/types.h>
#include <grp.h>

struct group *getgrgid(gid_t gid);

功能:用于获取 /etc/group 文件中内容的接口(通过组 ID 获取)  

  • gid — 在 /etc/group 文件中查询组 ID 为 gid 的那一行,并将那一行的内容填充至 group 结构体并返回 

三、/etc/shadow

取出一行出来分析,这里主要分析第二个字段的内容:这个字段的内容就是加密密码 

由于 /etc/passwd 文件对所有用户都可读,所以将用户密码存放于 /etc/passwd 是一个安全隐患。因此,现在许多 Linux 系统都使用了 shadow 技术,把真正的加密后的用户密码(也称口令)字段存放到 /etc/shadow 文件中,而在 /etc/passwd 文件的口令字段中只存放一个特殊的字符,例如 “x” 或者 “*”


补充:何为良好的加密? 

首先,加密一定要能解密!

问:hash 是加密吗?

答:不是,hash 最多算混淆,因为多个不同的原值可能通过 hash 映射到相同的 hash 值,那么从 hash 值恢复出唯一原值是无法实现的,即加密之后无法解密了

其次,即使相同的原串,经过加密,也应该得到不同的加密后串 

为什么要这样?如果相同的原串加密后得到相同的加密后串会怎么样?下面讲一个故事

因此,相同原串能够加密得到不同加密后串,主要目的是为了防止系统管理员监守自盗 


接下来继续介绍 /etc/shadow 第二个字段的内容,看看 UNIX 系统如何实现加密的

一个条加密密码由下面三部分组成(缺一不可):

  1. 加密方式
  2. 杂字串
  3. 原串与杂字串进行或运算后,再通过加密方式处理后所得到的串 

根据加密方式与杂字串,能够反推并运算得到加密前的串,故满足能解密的条件

即使是相同的原串,由于系统针对不同的用户提供不同杂字串,故即使原串,与不同的杂字串或运算后,再处理得到的串也不同,所得到的加密后串不同,故满足相同原串可得到不同加密后串 

下面介绍读取数据文件的接口(接口是为了统一与标准化)  


3.1 getspnam

man 3 getspnam

#include <shadow.h>

struct spwd *getspnam(const char *name);

功能:用于获取 /etc/shadow 文件中内容的接口(通过登录名获取) 

  • name — 在 /etc/shadow 文件中查询登录名为 name 的那一行,并将那一行的内容填充至 spwd 结构体并返回 

其中,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 的权限! 


3.2 密码校验示例

在此之前需要先补充几个函数

3.2.1 crypt

#define _XOPEN_SOURCE
#include <unistd.h>

char *crypt(const char *key, const char *salt);

Link with -lcrypt

功能:用于给串进行加密

  • key — 待加密的原串
  • salt — 指定加密方式与杂字串,salt 应该具有 "$id$salt$..." 的形式,id 表示特定加密方式,salt 表示特定杂字串,第三个 $ 符后的部分将被忽略
  • 返回加密后的串(注意包括了三个部分......)
  • 编译的时候要加 -lcrypt;定义宏要在包含头文件之前

3.2.2 getpass

 #include <unistd.h>

char *getpass(const char *prompt);

功能:获取输入密码(想一下登录 root 用户时候的 LINUX 命令行输入密码时候的效果,这个函数就能达到这个效果,本质上是在函数内部暂时关闭了终端的回显)

  • prompt:打印的提示字符
  • 返回获取到的字符串,不包括 '\n'

3.2.3 代码实现

#define _XOPEN_SOURCE    // 要在所有头文件包含前宏定义
#include <shadow.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

int main(int argc, char * argv[]) {

        if (argc < 2) {
                fprintf(stderr, "Usage: %s <username>\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 结构体,结构体中的字段记录了年月日等详细信息

这几种表示方式之间可以互相转化,相互之间关系如下:

下面介绍上图中的几个比较常用的函数用法


4.1 time

man 2 time

#include <time.h>

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 点以来的秒数(即获取时间戳)

  • tloc — 若 tloc 不是 NULL,则获取到的结果将存至 tloc 所指向的位置
  • 函数返回获取到的结果

因为函数这样设计,那么我们就有两种获取时间戳的方式

#include <time.h>
#include <stdio.h>
#include <stdlib.h>

int main() {

        time_t timestamp;
        time(&timestamp);    // 或者:timestamp = time(NULL);
        printf("%ld\n", timestamp);

        exit(0);
}

4.2 gmtime

man 3 gmtime

#include <time.h>

struct tm *gmtime(const time_t *timep);

功能:将时间戳转化为 UTC 时间,并填充至结构体的各个字段

  • timep — 待转换的时间戳
  • struct tm — 待被填充的结构体 
  • 函数返回被填充完毕的结构体 

被填充的结构体中的字段含义如下:

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 时间信息


4.3 localtime

man 3 localtime

#include <time.h>

struct tm *localtime(const time_t *timep);

功能:将时间戳转化为本地时间,并填充至结构体的各个字段

  • timep — 待转换的时间戳
  • struct tm — 待被填充的结构体
  • 函数返回被填充完毕的结构体

被填充的结构体与 gmtime 中介绍的结构体相同 

gmtime 得到的是 0 时区,把 UTC 时间转换成北京时间的话,需要在年数上加 1900,月份上加 1,小时数加上 8

localtime 得到的是本地时间,该函数同 gmtime 唯一区别是,在转换小时数不需要加上 8 了


4.4 mktime

man 3 mktime

#include <time.h>

time_t mktime(struct tm *tm);

功能:将 struct tm 结构体所代表的时间转化为时间戳

  • 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 <time.h>
#include <stdio.h>
#include <stdlib.h>

int main() {

        time_t timestamp = time(NULL);

        struct tm * tm = localtime(&timestamp);

        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);
}

4.5 strftime 

man 3 strftime

#include <time.h>

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 文件中获取数据,这能够说明不同系统下数据文件是不同的

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

林沐华

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值