☰
Shell编程必知:用户身份与文件权限深度解析
2026/10/9 3:26:49 网站建设 项目流程

前七篇我们把Shell编程的基础语法、变量、条件、循环、函数和文本处理都过了一遍,这套系列走到这里,其实已经能写出不少能跑的脚本了。但很多朋友在实际写自动化脚本的时候会碰到一个特别尴尬的场景:脚本在你自己机器上跑得好好的,一放到服务器上就报Permission denied;或者你用crontab定时跑一个任务,明明手动执行没问题,定时任务里却各种没权限。这类问题十有八九就是卡在用户身份和文件权限这两件事上。

所以这次我们专门把OpenEuler(以及其他Linux发行版)底层的用户身份体系和文件权限机制拿出来单独讲透。这篇说完之后,你在写脚本时对"当前脚本是以什么身份在跑""这个文件到底能不能被脚本读写""为什么mkdir没问题但是touch文件却报错"这类问题会有非常清晰的认识,排查思路也会直线清晰。

1. 用户身份:Linux一切权限的起点

1.1 三类用户与UID/GID的底层逻辑

Linux系统里的用户,不是像Windows那样把"账户名"当成权限判断的唯一依据。内核真正识别的,是一个数字,叫UID(User ID)。这个设计从Unix时代就定下来了,到现在OpenEuler依然完全遵循这套逻辑。

你先执行一下id命令,输出大概是:

[root@localhost ~]# id uid=0(root) gid=0(root) groups=0(root)

里面有两个关键字段:uid和gid。gid是主组ID,也就是用户所属的主要组的标识。你再看一眼普通用户:

[dev@localhost ~]$ id uid=1000(dev) gid=1000(dev) groups=1000(dev),10(wheel)

这里有个非常核心的底层规则:内核判断权限时只看uid和gid,不看用户名。用户名只是给人类看的显示层,所有用户信息都写在/etc/passwd和/etc/shadow这两个文件里。/etc/passwd每行对应一个用户,保存用户名、UID、主组、家目录、登录Shell这些信息;而密码hash存在/etc/shadow。

OpenEuler里用户主要有三种类型:

用户类型UID范围典型用途
root超级用户0系统管理,不受一般权限约束
系统用户1-999给服务进程使用,如sshd、nginx、mysql
普通用户1000+人类用户登录、跑日常任务

系统用户是很容易被忽略的一类。你搭Nginx、搭MySQL的时候,安装包会自动创建对应的系统用户,比如nginx用户和mysql用户。为什么要专门搞个系统用户来跑服务?因为这是安全设计的基本功:服务进程用低权限用户运行,即使被攻破,攻击者拿到的也只是一个低权限壳,而不是root。记住这个思路,后面写脚本创建服务时也用得上。

1.2 每次登录背后的身份切换链路

你平时在终端敲命令的时候,其实已经被Shell带着走了一遍完整的身份链路。以ssh登录为例:sshd服务收到连接请求后,会验证用户密码或密钥,然后通过PAM模块完成认证,接着读取/etc/passwd拿到用户的家目录和登录Shell,最后以这个用户的UID、GID调用Shell。如果你登录的是root,Shell进程就是uid=0;如果你登录的是dev用户,Shell就是uid=1000。

这中间的"切换"过程,又是Shell编程绕不开的内容。最常用的就是su和sudo。

su的用法是直接切换用户,比如su - dev,加-表示完全模拟该用户的登录环境,包括环境变量、当前目录全部切换。su的问题在于它默认需要目标用户的密码,而且切过去之后你就完全变成了那个用户,后续所有命令都以那个身份执行。这在自动化脚本里其实不好控制。

sudo就不一样了。sudo不是"切换身份",而是"以某个身份执行单条命令"。这个设计在脚本里非常实用:

sudo -u nginx ls /var/log/nginx/ sudo ls /root/ # 不指定-u,默认以root身份执行

sudo的授权规则写在/etc/sudoers里,系统管理员可以通过visudo命令编辑。最简单的授权是把用户加入wheel组,OpenEuler默认配置下,wheel组里的成员拥有sudo权限。

需要特别注意的是,sudo执行时身份和执行环境是两回事。你在脚本里写了sudo echo $HOME,看到的结果还是你自己用户的家目录,而不是root的家目录。因为Shell是先展开$HOME,再执行sudo命令的。要拿到root的环境,得用sudo env | grep HOME或者sudo -i这种方式。

我自己在写脚本时,最常用的判断语句是:

if [ "$(id -u)" -eq 0 ]; then echo "当前是root身份" else echo "当前不是root,需要检查sudo情况" fi

这个写法比whoami更可靠,因为id -u返回的是纯数字,做数值比较不会受到用户名格式不确定的影响。

2. 文件权限:rwx在文件和目录上的完全不同含义

2.1 读、写、执行对普通文件和目录的差异化影响

文件权限是Linux新手最容易混淆的部分,尤其是目录。总有人问:"我明明对这个目录有r权限,为什么进不去?"答案就在于,目录上的r和文件上的r含义不同,而进入目录靠的是另一个权限。

先用一个表格把r、w、x在文件和目录上的区别列清楚:

权限普通文件上的含义目录上的含义
r可以读取文件内容可以列出目录里的文件名(ls)
w可以修改/截断文件内容可以在目录里创建、删除、重命名文件
x可以执行该文件可以进入目录(cd),可以访问目录内文件的元数据

这里会产生一个特别经典的坑:如果目录只有r没有x,你执行ls dir能看到一堆文件名,但如果你对这些文件名执行stat、cat,会报Permission denied。因为读取文件的元数据、打开文件本身,都需要目录上的x权限。简单说,目录的x权限是"通行证",r权限只是"看门牌"。

再看数字权限的计算方式。Linux用三位八进制数来表示权限,每一位对应一个权限集合,数字1代表执行(x),2代表写(w),4代表读(r)。三位分别代表属主、属组、其他用户。例如:

  • chmod 755→ 属主rwx,属组r-x,其他r-x。这是最常见的一类文件权限,比如/usr/bin下的可执行程序。
  • chmod 644→ 属主rw-,属组r--,其他r--。这是普通文本配置文件的标配,能读不能改。
  • chmod 600→ 属主rw-,其他人完全不可访问。这是密钥文件、密码文件的推荐权限。

我自己常用的chmod命令有两种风格。一种就是数字:chmod 755 script.sh;另一种是符号模式:chmod u+x script.sh,chmod g-w file.conf。符号模式在修改单个权限位时非常好用,不用重新算整个数字。

2.2 chmod、chown、chgrp的实际用法与易错点

chown用来改文件属主和属组,chgrp只改属组。但Linux里chown已经能同时改两者,所以chgrp我平时用得不多了。语法是:

chown 用户名:组名 文件 chown -R 用户名:组名 目录

-R是递归,作用于目录下所有子文件和子目录。这是最容易踩坑的地方:如果你只改了目录本身的属主,没有加-R,那么目录内的文件仍然归原来的属主,你照样没有权限操作里面的内容。

还有一个隐蔽的坑:chown要求目标文件的所有者是你,或者你有权限修改该文件的所有权。如果是root,当然想怎么改就怎么改。但普通用户想把一个文件改成另一个用户的,基本是不可能的,这是系统故意设计的——防止你通过"把文件送给别人"这种方式来绕过权限限制。

再提一个权限数字计算容易出错的点。很多人觉得chmod 777是万能解药,出了问题就777,但这恰恰是安全灾难的开始。777意味着任何用户都能读写执行这个文件。在公网服务器上,一个777的脚本就意味着任何能登录到这台机器的人都能修改它——如果攻击者拿到了这个脚本的控制权,就能在里面塞任何恶意代码,然后以运行脚本的人的身份去执行。

正确的思路是最小权限。举个实际例子,如果我要让Nginx能读取网站的静态文件,我应该给Nginx用户所在组赋予r-x权限,而不是直接777:

chown -R nginx:nginx /var/www/html chmod 750 /var/www/html

这样属主nginx能进目录,属组成员也能读,其他人一律拒绝。

3. Shell编程中的身份与权限实战

3.1 脚本开头如何判断当前用户是不是root

我写任何需要在系统级操作的脚本,第一行要干的活儿就是校验身份。这不是小题大做,而是防止"脚本被普通用户执行后留下一堆隐患"的常规防御动作。最常用的写法有两种:

if [ "$EUID" -ne 0 ]; then echo "请用root或sudo执行此脚本" exit 1 fi

EUID是bash内置的环境变量,保存当前Shell的有效UID。另一种是id -u,它更通用,适合在sh和bash下都能跑的场景:

if [ "$(id -u)" -ne 0 ]; then echo "必须root权限才能执行" exit 1 fi

这里要解释一个概念:有效UID。脚本里经常出现"执行身份"和"文件属主"不一致的情况。如果脚本有SUID权限(后面会讲),脚本进程的有效UID可能是别人。所以在判断权限时要关注EUID,而不是单纯看UID。

还有一种常见需求:脚本既可以root执行,也可以普通用户执行,但需要根据身份调整行为。这时候可以写:

if [ "$(id -u)" -eq 0 ]; then LOG_DIR="/var/log/myapp" else LOG_DIR="$HOME/.myapp/logs" fi

根据不同身份选择不同的配置路径,这比让每个人手动改路径要稳得多。

3.2 用sudo和su在脚本中切换身份的正确姿势

脚本里切换身份,最常见的做法是用sudo执行单条命令,而不是用su切整个Shell。原因前面说过,su会把接下来的所有命令都换成那个用户的身份,容易超出预期作用范围。

比如你想在脚本里以postgres用户身份执行一个数据库命令:

sudo -u postgres psql -c "SELECT version();"

sudo -u是指定执行身份。如果不加-u,默认就是root。

但遇到一个实际问题:脚本在非交互模式下执行时,sudo可能会卡住等密码输入。比如你把这个脚本放在crontab里,流程完全无人值守,sudo等密码会直接导致任务挂死。解决办法是用sudo -n:

if sudo -n true 2>/dev/null; then # 用户已经配置了免密sudo,可以继续 sudo -u postgres psql -c "SELECT 1;" else echo "当前用户没有免密sudo权限,无法继续" exit 1 fi

sudo -n表示非交互执行,如果sudo需要密码就直接报错返回,而不是等待输入。这样我们可以提前判断sudo是否可用。

另一个容易被忽视的是环境变量。sudo默认会重置环境变量,只保留少量安全相关的变量。所以你在脚本里设置的JAVA_HOME,在sudo后可能直接变成空。如果必须保留某个环境变量,用sudo VAR=value这种方式传递,或者改/etc/sudoers里的env_keep配置。

3.3 为脚本和服务创建专用运行用户

做自动化部署时,不要直接用root运行一切服务。规范做法是创建专用用户。比如我要给某个Java应用脚本创建一个运行用户:

useradd -r -s /sbin/nologin -d /opt/myapp myapp

这里的参数含义:

  • -r:创建系统用户(UID小于1000),并且不会在/etc/login.defs里创建家目录目录模板。
  • -s /sbin/nologin:设置登录Shell为nologin,禁止该用户交互式登录。
  • -d /opt/myapp:指定家目录位置。

为什么用/sbin/nologin?因为服务用户根本不需要登录,给不给登录权限只会增加被攻破后的横向移动风险。

创建完之后,把应用目录的所有权交给这个用户:

chown -R myapp:myapp /opt/myapp

然后在systemd的service文件里用User=myapp指定运行身份。这样即使这个服务被打穿,攻击者能做的也只是在myapp身份下搞事情,危害范围被牢牢限制住。

如果脚本需要临时以这个用户身份执行某条命令,可以这样:

sudo -u myapp /opt/myapp/bin/start.sh

3.4 特殊权限位的检测与防护

除r/w/x之外,Linux还有三个特殊权限位:SUID、SGID和Sticky Bit。这三个位在Shell编程中经常被忽略,但一旦出问题就是安全事件。

SUID(Set User ID):当一个可执行文件设置了SUID位后,无论谁执行它,进程的有效用户ID都会变成文件的所有者。最典型的例子是/usr/bin/passwd:这个文件有SUID位,属主是root,所以普通用户执行passwd命令时,能临时以root身份修改/etc/shadow文件。这个设计是合理的,因为修改密码必须写/etc/shadow,但普通用户没有这个权限。

可以用ls查看SUID位:

[root@localhost ~]# ls -l /usr/bin/passwd -rwsr-xr-x. 1 root root 36352 Apr 1 12:34 /usr/bin/passwd

注意属主权限位的s,它和x加在一起显示为s;如果文件本身没有x,则显示为S。

SGID和SUID类似,作用于所属组;Sticky Bit则作用于目录,最著名的是/tmp:它的权限是drwxrwxrwt,最后的t就是Sticky Bit。在设置了Sticky Bit的目录里,只有文件所有者(或root)才能删除或重命名自己的文件,其他用户即使有该目录的写权限,也不能删别人的文件。

在安全加固时,定期找出所有SUID文件是个好习惯:

find / -perm -4000 -type f 2>/dev/null

这个命令会列出全系统所有设置了SUID位的文件。发现不在预期列表里的新SUID文件,基本可以断定是入侵痕迹或被植入的后门。SGID的查找方式是-perm -2000。

Shell脚本本身不能设置SUID位。Linux内核在加载脚本解释器时,会强制以解释器的身份运行脚本,也就是说脚本的SUID位会被忽略。这是因为脚本解释器(bash、python这类)在运行时非常容易受到路径、环境变量的影响,开SUID等于把root权限送给任何人。如果你确实需要普通用户执行某个特权操作,规范做法是用sudo提权,而不是给脚本SUID。

4. 权限问题排查:从"Permission denied"开始

4.1 权限不足时的系统排查思路

不管是"用户拒绝访问内存文件权限怎么办"还是"脚本读取配置文件报权限错误",这类问题在OpenEuler上基本可以归纳成几个固定的排查步骤。

第一步,确认当前身份:

id

这一步能明确当前是root还是普通用户、属于哪些组。很多时候你以为自己还在root身份,其实su到别的用户后忘了切回来,后面全在普通权限下操作。

第二步,确认文件的权限和属主:

ls -ld /path/to/file

注意-d参数只显示目录本身的权限。很多人排查目录权限时忘记加-d,看到的是目录里面的文件列表,信息完全错位。

第三步,查看文件系统挂载情况:

mount | grep /path

可能你的文件在/tmp这类有特殊挂载选项的目录上。系统如果以noexec方式挂载了分区,哪怕你有x权限也无法执行里面的程序。这在安全加固的OpenEuler上非常常见。

第四步,检查ACL(如果有):

getfacl /path/to/file

有些权限问题用ls -l看不出来,但ACL里面藏着额外的授权规则。OpenEuler默认开启了ACL支持,getfacl能看到比传统用户/组/其他更细粒度的权限配置。

4.2 umask对新建文件权限的影响与调整

每次你用touch、mkdir或者重定向创建新文件,它的初始权限并不是你想象的777或666,而是由当前Shell的umask值决定的。umask相当于是"默认权限掩码",用来抹掉不想要的权限位。

看一下当前值:

[root@localhost ~]# umask 0022

umask是0022时,新建目录的权限是777减去022得到755,新建文件是666减去022得到644。也就是说,新配置文件默认不让组和其他用户修改,这个设计很合理。

但如果你在脚本里创建一个临时文件,不希望其他用户能读,比如密钥文件,可以临时改umask:

umask 077 touch secret.key umask 022

共享模式下另一个极端是umask被设成000,这意味着任何新建文件都是777,组合其他用户都能写。如果你发现一台服务器上所有人创建的文件都是可写的,大概率是有人改了全局umask配置。排查位置是/etc/profile、/etc/bashrc和用户的~/.bashrc。

4.3 目录权限导致的"能看不能进"问题

"能看不能进"是最经典的目录权限问题。比如你执行ls /data/project能列出文件名,但cd /data/project却报Permission denied,或者cat /data/project/config也报权限不足。

原因我们前面讲过:目录的r权限只负责"列名字",x权限才负责"访问这些名字指向的文件元数据和内容"。一个用户拥有r但缺少x时,ls会输出一堆文件名,但每个文件的具体信息(大小、权限、修改时间)都读不到,更别说打开文件了。

排查办法:

namei -l /data/project/config

namei会逐级遍历路径上的每一层目录,并显示每层的权限。它可以直接告诉我们:是/data没权限,还是/data/project没权限,还是最后那个config文件本身有问题。这是一个非常高效的排查工具,强烈建议放进你的工具箱。

修复方式很简单,把路径上所有缺失x权限的目录补上x:

chmod +x /data /data/project

但注意,不要用chmod 777这种粗暴手段去修权限,应该精确地只加x,或者是按逻辑分组加权限。

4.4 Windows与OpenEuler权限机制的一点对照

有朋友拿来Windows上的问题问:"Windows提示setnamedsecurityinfo failed (win32),到底怎么处理?"这正好引出跨平台权限机制的差异。

Windows的ACL机制和Linux完全不是一回事。Windows的权限是基于ACE(访问控制项)列表的,每个文件或目录都有一串ACE,可以精确控制“某个组对某个文件能不能读”这种细粒度权限;而Linux传统权限只有三组类别、三个位,粒度粗一些,补救方案才引入ACL来提供类似能力。

Windows创建文件时默认会从父目录继承ACL,没有任何"umask"这种全局掩码;而Linux没有继承传统权限,新建文件只受umask影响,组权限需要靠目录setgid位继承。这就导致两个系统上排查权限问题的方法截然不同。

在实际跨平台开发里,如果从Windows复制文件到OpenEuler,最常碰到的坑就是Windows上的文件默认带了继承权限,复制过来之后在Linux侧看到的权限往往是奇怪的754或600,而不是你以为的644或755。解决办法是复制后用chmod重新设置。

有些开发工具(比如某些AI应用框架)在Windows上读取文件报权限错误,本质也是ACL冲突:当前进程令牌没有足够的权限去打开那个文件。这和Linux上的Permission denied逻辑是一样的,只是表现形式不同。

5. 脚本权限管理的几个再啰嗦两句的细节

这个系列写到这里,我越来越觉得用户身份和文件权限才是Shell脚本能稳定运行的最大前提。做了几年运维和自动化,遇到的大部分故障不是代码逻辑错,而是脚本跑在了错误的身份下,或者目标文件根本不可访问。

有几个细节我再啰嗦一遍。

第一,用绝对路径执行脚本。如果你不在脚本目录里运行脚本,./script.sh这种相对路径很容易因为PATH不同而找错解释器或找不到依赖文件。脚本开头加上:

#!/bin/bash cd "$(dirname "$0")"

dirname "$0"能拿到脚本所在目录,cd进去之后,后续所有相对路径都基于脚本目录了。这个习惯能规避掉一大类"脚本明明写对了却找不到文件"的权限问题。

第二,脚本里输出日志时,先确认日志目录可写。crontab跑脚本失败,很多是因为脚本往/var/log下写日志,但普通用户的crontab任务没有写权限。我的做法是统一把脚本日志放到/var/log/myapp/并chown给运行用户,或者由脚本自动用mkdir -p "$LOG_DIR"并检查写权限。

第三,避免在脚本里使用裸的rm -rf配合变量路径。比如rm -rf $DIR/*这种写法,一旦$DIR是空值,命令就变成rm -rf /*,后果想想都害怕。至少加一层判断:

if [ -n "$DIR" ] && [ -d "$DIR" ]; then rm -rf "${DIR:?}"/* fi

${DIR:?}会在变量为空时抛错,从源头防止灾难。

最后再分享一个我自己的习惯:每次部署新脚本或新服务,我都会用find / -perm -4000 -type f扫一遍系统,查完才放心上线。权限管理不是一次性工作,而是要形成肌肉记忆:新建文件时想一下umask要不要改,给脚本提权时想一下是不是该用sudo而不是开SUID,创建服务用户时想一下nologin加没加。这些点都做到位,你的OpenEuler服务器会安稳很多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询