一、幾個基本概念
登錄用戶(login user):通過登錄方式進入系統的用戶,強調登錄身份。
當前用戶(current user):執行一個進程或者命令時所使用的用戶身份,強調執行身份。
舉個例子,比如通過用戶名/密碼的方式,以用戶A的身份ssh到服務器上,那麼此時登陸用戶是A,當前用戶也是A;然後以su的方式從A切換到用戶B,此時登陸用戶是A,當前用戶是B。
那麼“登錄用戶”和“當前用戶”有哪些本質意義上的不同呢?我認爲,大多數情況下(有例外)你可以認爲,其本質不同在於是否掌握用戶密碼,也就是是否真正的可驗證爲用戶本人。一般來說,“登錄用戶”需要通過賬號密碼驗證,是真正的“我”,而“當前用戶”可以通過su等手段切換過來,可能是僞造的“我”。
二、獲取用戶名
假設場景:
以root身份登錄ic-monitor01這臺機器(登錄用戶爲root),然後用su的方式切換爲liyanqing.1987這個賬號(當前用戶爲liyanqing.1987)。
[root@ic-monitor01 ~]# su - liyanqing.1987
Last login: Sat Apr 27 09:34:21 CST 2024 on pts/2
Welcome liyanqing.1987 ~
[liyanqing.1987@ic-monitor01 ~]$
Linux shell中有幾種方法獲取當前terminal的登錄用戶名和當前用戶名。
2.1 whoami:獲取當前用戶名
[liyanqing.1987@ic-monitor01 ~]$ whoami
liyanqing.1987
2.2 id -un:獲取當前用戶名
[liyanqing.1987@ic-monitor01 ~]$ id -un
liyanqing.1987
2.3 who am i:獲取登錄用戶名
[liyanqing.1987@ic-monitor01 ~]$ who am i
root pts/0 2024-05-18 09:35 (10.232.135.12)
同樣,通過python也可以獲取當前termianl中的登錄用戶名和當前用戶名。
2.4 getpass.getuser():python獲取當前用戶名
[liyanqing.1987@ic-monitor01 ~]$ python3
Python 3.8.8 (default, Dec 7 2022, 15:48:27)
[GCC 4.8.5 20150623 (Red Hat 4.8.5-44)] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import getpass
>>> print(getpass.getuser())
liyanqing.1987
2.5 os.getlogin():python獲取登錄用戶名
[liyanqing.1987@ic-monitor01 ~]$ python3
Python 3.8.8 (default, Dec 7 2022, 15:48:27)
[GCC 4.8.5 20150623 (Red Hat 4.8.5-44)] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import os
>>> print(os.getlogin())
root
由上可見,通過shell command和python function抓到的當前用戶名及登陸用戶名是一致的,因爲它們的信息來源及信息檢索方式是一致的。
三、用戶名信息來源
當前用戶名信息來源於系統記錄,比如系統環境變量USER就記錄了當前用戶名信息。
[liyanqing.1987@ic-monitor01 ~]$ echo $USER
liyanqing.1987
而登錄用戶名的信息來源,則來自於系統登錄記錄。
首先來了解一下Linux系統的用戶登錄/會話記錄三劍客 utmp/wtmp/btmp,這三個文件記錄和監控了用戶登錄、註銷以及失敗登陸嘗試等信息。
/var/run/utmp:記錄了當前正在登陸系統的用戶。
/var/log/wtmp:記錄了系統上所有的登陸和註銷事件。
/var/log/btmp:記錄了系統上的失敗登錄嘗試。
這三個文件都是二進制的,並不易讀,但是可以通過utmpdump這個工具將其轉換爲更加易讀的文本文件。
[liyanqing.1987@ic-monitor01 ~]$ utmpdump /var/run/utmp > utmp
Utmp dump of /var/run/utmp
[liyanqing.1987@ic-monitor01 ~]$ head -n 3 utmp
[2] [00000] [~~ ] [reboot ] [~ ] [3.10.0-1160.49.1.el7.x86_64] [0.0.0.0 ] [Sun Dec 24 10:41:13 2023 CST]
[6] [03200] [tty1] [LOGIN ] [tty1 ] [ ] [0.0.0.0 ] [Sun Dec 24 10:41:31 2023 CST]
[7] [62407] [ts/0] [root ] [pts/0 ] [10.232.135.12 ] [10.232.135.12 ] [Sat May 18 09:35:05 2024 CST]
/var/run/utemp是最常被用到的一個文件,用於獲取登錄狀態的登錄用戶信息,可以通過w/who/users等系統命令獲取。
[liyanqing.1987@ic-monitor01 ~]$ w
10:20:49 up 145 days, 23:39, 1 user, load average: 0.41, 0.32, 0.33
USER TTY FROM LOGIN@ IDLE JCPU PCPU WHAT
root pts/0 10.232.135.12 09:35 1.00s 0.29s 0.03s w
注意,此處打印的是“登錄用戶”而非“當前用戶”,所以可以看到登錄用戶root的信息,但是看不到當前用戶liyanqing.1987的信息。
[liyanqing.1987@ic-monitor01 ~]$ who
root pts/0 2024-05-18 09:35 (10.232.135.12)
who命令仍然是隻打印登陸用戶信息,而且是隻打印登陸信息,所以更簡潔。
who am i命令是who命令的分支用法,其信息檢索方式詳見第四章。
[liyanqing.1987@ic-monitor01 ~]$ users
root
users比who的命令輸出更簡潔。
四、who am i命令如何獲取登錄用戶信息
已知who am i命令的信息來自於who命令,那麼它的信息檢索原理是什麼呢?(一些tty/pts相關的基礎知識可以參考https://my.oschina.net/liyanqing/blog/11154146)
首先,每個登陸用戶都會有個虛擬終端號與其綁定,這個信息可以通過tty命令獲取。
[liyanqing.1987@n232-135-012 ~]$ tty
/dev/pts/1172
這個綁定信息會在用戶登錄時寫入/var/run/utmp中,也就可以被who命令所查詢到。
[liyanqing.1987@n232-135-012 ~]$ who
user_a pts/0 2024-05-14 19:20 (:59.0)
user_b pts/1 2023-12-27 10:07 (:55.0)
user_c pts/3 2024-02-20 19:30 (:42.0)
user_d pts/4 2024-03-07 16:36 (:42.0)
...
liyanqing.1987 pts/1172 2024-05-18 10:35 (:1.0)
...
執行who am i的時候,它會自動獲取tty信息,然後依據這個pts編號去/var/run/utmp中檢索到匹配的項,然後把相應的信息返回回來。
[liyanqing.1987@n232-135-012 ~]$ who am i
liyanqing.1987 pts/1172 2024-05-18 10:35 (:1.0)
其基本流程如下。
五、避坑:who am i失效分析
那麼存在執行who am i命令沒有輸出的情況嗎?實際上這種情況是存在的。
測試條件:遠程連接工具ETX;操作系統centos 7.9;終端gnome-terminal
[liyanqing.1987@n232-135-013 ~]$ whoami
liyanqing.1987
[liyanqing.1987@n232-135-013 ~]$ who am i
[liyanqing.1987@n232-135-013 ~]$ id -un liyanqing.1987
如上所示,whoami和id -un命令都可以正常獲取當前用戶名,但是who am i無法獲取登陸用戶名。(這是因爲信息來源不同)
更可怕的是,這種情況下,如果用python的os.getlogin()來嘗試獲取登錄用戶信息,會直接導致python coredump。
[liyanqing.1987@n232-135-013 ~]$ python3
Python 3.8.8 (default, Dec 7 2022, 15:48:27)
[GCC 4.8.5 20150623 (Red Hat 4.8.5-44)] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import os
>>> print(os.getlogin())
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
OSError: [Errno 6] No such device or address
從上一章可以知道,who am i的信息可以從who命令輸出中檢索出來,來檢驗一下。
[liyanqing.1987@n232-135-013 ~]$ tty
/dev/pts/1115
[liyanqing.1987@n232-135-013 ~]$ who | grep pts/1115
[liyanqing.1987@n232-135-013 ~]$
果然,who的輸出就不能獲取pts/1115,who am i必然沒有輸出。那麼who的輸出來自於/var/run/utmp,直接人工檢索一下這個文件呢?
/var/run/utmp中也沒有!
但是我們在/var/log/wtmp這個記錄了所有的用戶登錄/註銷信息的文件中是能檢索到pts/1115的。
同時也可以用last命令來二次驗證一下,證明pts/1115確實存活,且爲“still logged in”的狀態。
[liyanqing.1987@n232-135-013 ~]$ last | grep pts/1115
liyanqin pts/1115 :65.0 Thu May 9 15:30 still logged in
那麼爲什麼在當前終端pts/1115存在的情況下,/var/run/utmp中卻沒有記錄呢?
實際上這個問題很早就有人反饋給了redhat官方,但是官方反饋中並沒有給出明確的說明。https://bugzilla.redhat.com/show_bug.cgi?id=91442
如下回答則是一個可信度較高的答案。https://sorami-chi.hateblo.jp/entry/2018/02/18/002530
The root cause behind this problem is that /var/run/utmp is not magically maintained by the OS and it has to be updated by each process, while recent versions of libvte does not do this. This is not a bug of libvte, but an intentional choice discussed like here. Although in the same thread a concern about who am i was raised, it was just removed to improve the code cleanness (it seems like the function to update /var/run/utmp was in a very nasty file that is almost never used). Because both GNOME-terminal and MATE-terminal use libvte, this problem exist in both of them.
根源是因爲操作系統並沒有有效地維護var/run/utmp,每個進程必須獨自更新這個文件,而最新版本的libvte並不能做到這一點。這不是libvte的bug,而是個刻意的選擇。儘管存在影響who am i命令的擔心,但是它仍然被刪了以提升代碼清潔度(更新/var/run/utmp的函數似乎位於一個從未被使用過的討厭的文件中)。因爲gonme-terminal和mete-terminal都使用libvte,所以這兩個終端都存在這個問題。
六、如何健壯地獲取登陸用戶名
Shell中的who am i命令,或者python中的os.getlogin()函數,都是非常常用的功能而很難被繞過,但是第五章提到的故障會在用戶程序(shell/python程序)引起不可預知或者致命的問題,如何繞過這個問題呢?
6.1 shell中獲取登陸用戶名
可以採用下面的寫法,在shell中健壯地獲取登陸用戶信息。
[liyanqing.1987@n232-135-013 ~]$ cat get_login_user.sh
#!/bin/bash
login_user=`who am i | awk '{print $1}'`
if [ "0${login_user}" = "0" ]; then
pts=`tty | cut -c 6-`
login_user=`last -w | grep -w ${pts} | awk '{print $1}'`
fi
echo "login user is : ${login_user}"
在正常的shell中可以獲取login user信息。
[liyanqing.1987@n232-135-013 ~]$ who am i
liyanqing.1987 pts/1183 2024-05-18 11:29 (:65.0)
[liyanqing.1987@n232-135-013 ~]$ ./get_login_user.sh
login user is : liyanqing.1987
在不正常的shell中仍然可以獲取login user信息。
[liyanqing.1987@n232-135-013 ~]$ who am i
[liyanqing.1987@n232-135-013 ~]$ ./get_login_user.sh
login user is : liyanqing.1987
6.2 python中獲取登錄用戶名
可以採用下面的寫法,在python中健壯地獲取登陸用戶信息。
[liyanqing.1987@n232-135-013 ~]$ cat get_login_user.py
#!/bin/env python3
import os
import subprocess
def get_login_user():
try:
login_user = os.getlogin()
except:
pts = subprocess.check_output(['tty']).decode('utf-8')[5:]
login_user = subprocess.check_output('last -w | grep -w ' + str(pts), shell=True).decode('utf-8').split()[0]
return login_user
login_user = get_login_user()
print('login user is : ' + str(login_user))
在正常的shell中可以獲取login user信息。
[liyanqing.1987@n232-135-013 ~]$ who am i
liyanqing.1987 pts/1183 2024-05-18 11:29 (:65.0)
[liyanqing.1987@n232-135-013 ~]$ ./get_login_user.py
login user is : liyanqing.1987
在不正常的shell中仍然可以獲取login user信息,且不會發生coredump。
[liyanqing.1987@n232-135-013 ~]$ who am i
[liyanqing.1987@n232-135-013 ~]$ ./get_login_user.py
login user is : liyanqing.1987