===接第六节=== (五)文件的特殊权限 chmod -R +权限 文件或目录赋予权限 //权限=数值 chmod -R -权限 文件或目录减少权限 //权限=数值 chown -R 所有者:所属组 文件或目录 //指定文件或目录的所有者及所属组 参数:-R表示对目录执行递归操作。 1、SUID SUID是一种对Linux系统内置或开发的二进制程序或命令添加所有者位具有特殊权限s的命令,让二进制程序或命令的执行者(普通用户)临时拥有属主(所有者)的权限(仅对拥有执行权限的二进制程序或命令有效)。 命令格式:chmod u+s 命令或程序 说明:命令执行后二进制程序或命令所有者的权限由rwx中的x变成了s,原先权限位上没有x执行权限的被赋予特殊权限后将变成大写的S。 举例1: //查看passwd程序权限为-rwxr-xr-x,passwd没有s权限。 [root@linuxprobe home]# ll -l /usr/bin/passwd -rwxr-xr-x. 1 root root 27832 Jan 30 2014 /usr/bin/passwd //切换到普通用户执行修改密码的命令。 [root@linuxprobe home]# su – linuxprobe Last login: Sat Feb 29 09:13:01 CST 2020 on pts/1 //执行passwd修改密码。 [linuxprobe@linuxprobe ~]$ passwd Changing password for user linuxprobe. Changing password for linuxprobe. (current) UNIX password: passwd: Authentication token manipulation error //普通用户执行passwd修改密码失败。 //给passwd程序赋予s权限,使普通用户也可以执行该命令。 [root@linuxprobe home]# chmod u+s /usr/bin/passwd //查看passwd程序已经具有s权限。 [root@linuxprobe home]# ll /usr/bin/passwd -rwsr-xr-x. 1 root root 27832 Jan 30 2014 /usr/bin/passwd //再次切换到普通用户执行修改密码的命令。 [root@linuxprobe home]# su - linuxprobe Last login: Sat Feb 29 09:15:30 CST 2020 on pts/1 //执行passwd修改密码。 [linuxprobe@linuxprobe ~]$ passwd Changing password for user linuxprobe. Changing password for linuxprobe. (current) UNIX password: New password: Retype new password: passwd: all authentication tokens updated successfully. //普通用户执行passwd修改密码成功。 [linuxprobe@linuxprobe ~]$ 举例2: [root@linuxprobe bin]# ll /usr/sbin/fdisk //查看fdisk命令所有者位没有s权限。 -rwxr-xr-x. 1 root root 182424 Mar 28 2014 /usr/sbin/fdisk [root@linuxprobe bin]# su - test1 //切换普通用户执行fdisk命令查看磁盘。 Last login: Sat Feb 29 10:10:46 CST 2020 on pts/1 [test1@linuxprobe ~]$ fdisk -l //执行fdisk命令后没有任何输出。 [root@linuxprobe ~]# chmod u+s /usr/sbin/fdisk //给fdisk命令赋予所有者位s权限。 [root@linuxprobe ~]# ll /usr/sbin/fdisk //查看fdisk命令所有者位有s权限。 -rwsr-xr-x. 1 root root 182424 Mar 28 2014 /usr/sbin/fdisk [test1@linuxprobe ~]$ fdisk -l //再次执行fdisk命令查看磁盘后有输出信息。 Disk /dev/sda: 21.5 GB, 21474836480 bytes, 41943040 sectors Units = sectors of 1 * 512 = 512 bytes Sector size (logical/physical): 512 bytes / 512 bytes I/O size (minimum/optimal): 512 bytes / 512 bytes Disk label type: dos Disk identifier: 0x0008b3e2 Device Boot Start End Blocks Id System /dev/sda1 * 2048 1026047 512000 83 Linux /dev/sda2 1026048 41943039 20458496 8e Linux LVM Disk /dev/mapper/rhel-root: 18.8 GB, 18798870528 bytes, 36716544 sectors Units = sectors of 1 * 512 = 512 bytes Sector size (logical/physical): 512 bytes / 512 bytes I/O size (minimum/optimal): 512 bytes / 512 bytes Disk /dev/mapper/rhel-swap: 2147 MB, 2147483648 bytes, 4194304 sectors Units = sectors of 1 * 512 = 512 bytes Sector size (logical/physical): 512 bytes / 512 bytes I/O size (minimum/optimal): 512 bytes / 512 bytes 2、GUID GUID是一种对目录添加所属组位具有特殊权限s的命令,让程序的执行者(普通用户)创建的文件拥有目录所属组的权限,即在某个目录中创建的文件自动继承该目录的用户组权限(只可对目录进行设置)。 命令格式:chmod -R g+s 目录 //参数-R表示对目录执行递归操作。 说明:命令执行后目录或命令所属组的权限由rwx中的x变成了s,原先权限位上没有x执行权限的被赋予特殊权限后将变成大写的S。 举例1: [root@linuxprobe home]# mkdir 1234 [root@linuxprobe home]# chmod 777 1234 //需设置目录的其他用户位权限为7,否则普通用户无法访问该目录。 [root@linuxprobe home]# ll drwxrwxrwx. 2 root root 6 Feb 29 10:46 1234 [root@linuxprobe home]# [root@linuxprobe ~]# su - linuxprobe Last login: Sat Feb 29 10:01:09 CST 2020 on pts/1 [linuxprobe@linuxprobe ~]$ cd /home/1234/ [linuxprobe@linuxprobe 1234]$ touch q.txt [linuxprobe@linuxprobe 1234]$ ll -rw-rw-r--. 1 linuxprobe linuxprobe 0 Feb 29 10:48 q.txt //创建好的文件所属组为普通用户的所属组。 [root@linuxprobe home]# chmod g+s 1234 //root用户下修改目录所属组位具有s特殊权限。 [root@linuxprobe home]# ll //确认目录所属组位s权限修改成功。 drwxrwsrwx. 2 root root 18 Feb 29 10:48 1234 [linuxprobe@linuxprobe 1234]$ touch e.txt //再次创建新的文件 [linuxprobe@linuxprobe 1234]$ ll -rw-rw-r--. 1 linuxprobe root 0 Feb 29 10:49 e.txt //新创建的文件所属组位继承目录所属组的权限。 -rw-rw-r--. 1 linuxprobe linuxprobe 0 Feb 29 10:48 q.txt //未修改目录所属组位具有s特殊权限前创建的文件所属组为普通用户的所属组。 3、SBIT SBIT特殊权限位可确保用户只能删除自己的文件,而不能删除其他用户的文件。当对某个目录设置了SBIT粘滞位权限后,那么该目录中的文件就只能被其所有者执行删除操作,其他用户无权操作。 命令格式:chmod -R o+t目录 //参数-R表示对目录执行递归操作。 说明:命令执行后目录其他人的权限由rwx中的x变成了t,原先权限位上没有x执行权限的被赋予特殊权限后将变成大写的T。 举例1: [root@linuxprobe home]# mkdir linuxdir [root@linuxprobe home]# chmod 777 linuxdir/ //赋予任何人都可以读\写\执行操作的权限 [root@linuxprobe home]# ll drwxrwxrwx. 2 root root 20 Feb 29 11:42 linuxdir [root@linuxprobe ~]# su – linuxprobe //切换linuxprobe用户创建一个文件 [linuxprobe@linuxprobe ~]$ cd /home/linuxdir/ [linuxprobe@linuxprobe linuxdir]$ touch xxx.txt [linuxprobe@linuxprobe linuxdir]$ ll -rw-rw-r--. 1 linuxprobe linuxprobe 0 Feb 29 11:44 xxx.txt [root@linuxprobe ~]# su - test1 //切换test1用户验证是否可以删除linuxprobe用户创建的文件 Last login: Sat Feb 29 11:35:19 CST 2020 on pts/0 [test1@linuxprobe ~]$ cd /home/linuxdir/ [test1@linuxprobe linuxdir]$ ll -rw-rw-r--. 1 linuxprobe linuxprobe 0 Feb 29 11:44 xxx.txt [test1@linuxprobe linuxdir]$ rm -rf xxx.txt //验证结果,上级目录其他人位未配置t权限时任何人都可以删除别人创建的文件,因为目录其他人位的权限是rwx,即非本用户创建的文件本用户也可以删除。 [test1@linuxprobe linuxdir]$ ll total 0 [root@linuxprobe home]# chmod -R o+t linuxdir/ //给目录其他人位赋予t的权限验证该目录下非本用户创建的文件是否可以被删除。 [root@linuxprobe home]# ll drwxrwxrwt. 2 root root 6 Feb 29 11:45 linuxdir //权限赋予成功 [root@linuxprobe ~]# su - linuxprobe Last login: Sat Feb 29 11:39:37 CST 2020 on pts/0 [linuxprobe@linuxprobe ~]$ cd /home/linuxdir/ [linuxprobe@linuxprobe linuxdir]$ touch uuu.txt [linuxprobe@linuxprobe linuxdir]$ ll total 0 -rw-rw-r--. 1 linuxprobe linuxprobe 0 Feb 29 11:46 uuu.txt //切换linuxprobe用户成功创建一个文件 [root@linuxprobe ~]# su - test1 //切换test1用户验证是否可删除linuxprobe用户创建的文件。 Last login: Sat Feb 29 11:45:14 CST 2020 on pts/0 [test1@linuxprobe ~]$ cd /home/linuxdir/ [test1@linuxprobe linuxdir]$ ll total 0 -rw-rw-r--. 1 linuxprobe linuxprobe 0 Feb 29 11:46 uuu.txt [test1@linuxprobe linuxdir]$ rm -rf uuu.txt rm: cannot remove ‘uuu.txt’: Operation not permitted //禁止test1用户删除linuxprobe用户创建的文件。 4、特殊权限举例: 特殊权限值:所有者位SUID=4,所属组位GUID=2,其他人位SBIT=1。 权限数值计算时先计算普通权限位(所有者位+所属组位+其他人位)对应的rwx数值,再根据所有者位、所属组位、其他人位的特殊权限标记计算特殊权限值(所有者位+所属组位+其他人位),最后把特殊权限值写在前,普通权限值写在后形成完整的数值权限表示。 举例1:计算7543的字符表示。 先计算普通权限位543对应为r-x r-- -wx,前面的7表示所有者位+所属组位+其他人位都有特殊权限,即所有者位的r-x加上特殊权限后为r-s,所属组位的r--加上特殊权限后为r-S(最后一位为“-”用大S表示),其他人位的-wx加上特殊权限后为-wt,最终7543对应的字符表示为r-s r-S -wt。 举例2:可以通过完整的数值给文件或目录赋予权限。 [root@linuxprobe home]# chmod 6543 linuxdir/ [root@linuxprobe home]# ll dr-sr-S-wx. 2 root root 20 Feb 29 11:46 linuxdir (六)设置、查看文件的隐藏权限 Linux系统中的文件除了具备一般权限和特殊权限之外,还有一种隐藏权限,即被隐藏起来的权限,默认情况下不能直接被用户发觉。 1、chattr命令设置隐藏权限 chattr命令用于设置文件的隐藏权限。增加权限:chattr +权限 文件,删除权限:chattr -权限 文件。 命令格式:chattr [参数] 文件或目录(为目录时需要带-R参数) 说明:如果想要把某个隐藏功能添加到文件上,则需要在命令后面追加“+参数”,如果想要把某个隐藏功能移出文件,则需要追加“-参数”。 常用参数:i参数无法对文件进行修改;若对目录设置了该参数,则仅能修改其中的子文件内容而不能新建或删除文件。a参数仅允许补充(追加)内容,无法覆盖/删除内容(Append Only)。 举例1:文件增加a属性权限 [root@linuxprobe linuxdir]# touch ooo.txt //创建一个普通文件。 [root@linuxprobe linuxdir]# echo "121333" > ooo.txt //给创建的文件写入数据121333。 [root@linuxprobe linuxdir]# chattr +a ooo.txt //给文件增加a的属性权限。 [root@linuxprobe linuxdir]# echo "4567" > ooo.txt //给文件覆盖写入内容,提示不允许覆盖。 -bash: ooo.txt: Operation not permitted [root@linuxprobe linuxdir]# echo "4567" >> ooo.txt //给文件追加写入内容,写入成功。 [root@linuxprobe linuxdir]# cat ooo.txt 121333 4567 [root@linuxprobe linuxdir]# rm ooo.txt //无法删除文件。 rm: remove regular file ‘ooo.txt’? y rm: cannot remove ‘ooo.txt’: Operation not permitted 举例2:文件增加i属性权限 [root@linuxprobe linuxdir]# chattr +i ooo.txt //给文件增加i的属性权限。 [root@linuxprobe linuxdir]# echo "4567" > ooo.txt //给文件覆盖写入内容,提示不允许覆盖。 -bash: ooo.txt: Permission denied [root@linuxprobe linuxdir]# echo "4567" >> ooo.txt //给文件追加写入内容,提示不允许追加。 -bash: ooo.txt: Permission denied 举例3:目录增加i属性权限 [root@linuxprobe home]# ll drwsrwsrwx. 2 root root 6 Feb 29 12:50 linuxdir [root@linuxprobe home]# cd linuxdir/ [root@linuxprobe linuxdir]# echo 111111111111111 >> 123.txt //文件可以写入内容。 [root@linuxprobe linuxdir]# ll total 4 -rw-r--r--. 1 root root 16 Feb 29 12:54 123.txt [root@linuxprobe home]# chattr -R +i linuxdir/ //目录增加i属性权限。 [root@linuxprobe home]# cd linuxdir/ [root@linuxprobe linuxdir]# echo 222222222 >> 123.txt //文件依然可以追加和覆盖写入内容。 [root@linuxprobe linuxdir]# echo 333333333 > 123.txt [root@linuxprobe linuxdir]# touch 456.txt //新加文件提示禁止操作。 touch: cannot touch ‘456.txt’: Permission denied [root@linuxprobe linuxdir]# rm 123.txt //删除文件提示禁止操作。 rm: remove regular file ‘123.txt’? y rm: cannot remove ‘123.txt’: Permission denied 2、lsattr命令查看隐藏权限 lsattr命令用于显示文件的隐藏权限。 命令格式:lsattr [参数] 文件或目录 在Linux系统中,文件的隐藏权限必须使用lsattr命令来查看,平时使用的ls之类的命令则看不出端倪。 举例1: [root@linuxprobe linuxdir]# ll -al 123.txt -rw-r--r--. 1 root root 10 Feb 29 12:55 123.txt [root@linuxprobe linuxdir]# lsattr 123.txt //显示文件或目录具有i的权限。 ----i----------- 123.txt [root@linuxprobe home]# lsattr linuxdir/ ----i----------- linuxdir/123.txt [root@linuxprobe home]# chattr -R -i linuxdir/ //取消目录的i权限并查看结果。 [root@linuxprobe home]# lsattr linuxdir ---------------- linuxdir/123.txt [root@linuxprobe linuxdir]# chattr +a 123.txt //增加a的权限后查看文件具有a的权限。 [root@linuxprobe linuxdir]# lsattr 123.txt ----ia---------- 123.txt (七)文件访问控制列表 一般权限、特殊权限、隐藏权限其实有一个共性----权限是针对某一类用户设置的。文件的访问控制列表(ACL)可对某个指定的用户进行单独的权限控制。基于普通文件或目录设置ACL其实就是针对指定的用户或用户组设置文件或目录的操作权限。如果针对某个目录设置了ACL,则目录中的文件会继承其ACL;若针对文件设置了ACL,则文件不再继承其所在目录的ACL。 1、setfacl命令 setfacl命令用于管理文件的ACL规则。 命令格式:setfacl [参数] 文件或目录名称(为目录时需要带-R参数) 文件的ACL提供的是在所有者、所属组、其他人的读/写/执行权限之外的特殊权限控制,使用setfacl命令可以针对单一用户u或用户组g、单一文件或目录来进行读r/写w/执行x权限的控制。其中,针对目录文件需要使用-R递归参数,针对普通文件则使用-m参数,如果想要删除某个文件的ACL,则可以使用-b参数。 举例1:设置显示目录的ACL [root@linuxprobe home]# ll drwsrwsrwx. 2 root root 20 Feb 29 12:54 linuxdir [root@linuxprobe home]# setfacl -Rm u:linuxprobe:rwx linuxdir/ //设置用户linuxprobe对目录的权限rwx。 [root@linuxprobe home]# ll //设置成功后,文件的权限最后一个点(.)变成了加号(+)。 drwsrwsrwx+ 2 root root 20 Feb 29 12:54 linuxdir [root@linuxprobe home]# cd linuxdir/ [root@linuxprobe linuxdir]# ll -rw-rwxr--+ 1 root root 10 Feb 29 12:55 123.txt 说明:通过ll命令可以看到文件的权限最后一个点(.)变成了加号(+),这就意味着该文件已经设置了ACL了。 [root@linuxprobe linuxdir]# setfacl -Rm g:linuxprobe:rw- /home/linuxdir //设置用户组linuxprobe对目录的权限rw-。 [root@linuxprobe home]# getfacl linuxdir/ //显示目录或文件的详细ACL信息。 # file: linuxdir/ # owner: root # group: root # flags: ss- user::rwx user:linuxprobe:rwx group::rwx group:linuxprobe:rw- mask::rwx other::rwx 举例2:删除目录的ACL [root@linuxprobe home]# setfacl -b linuxdir/ //删除目录或文件的详细ACL信息。 [root@linuxprobe home]# getfacl linuxdir/ # file: linuxdir/ # owner: root # group: root # flags: ss- user::rwx group::rwx other::rwx 2、getfacl命令 getfacl命令用于显示文件上设置的ACL信息。 命令格式:getfacl 文件或目录名称 (八)su命令和sudo服务 1、su命令 su命令可以解决当前用户在不退出登录的情况下顺畅地切换到其他用户的问题,比如从root管理员切换至普通用户linuxprobe。 举例1: [root@linuxprobe home]# su - linuxprobe Last login: Sat Feb 29 11:46:43 CST 2020 on pts/0 [linuxprobe@linuxprobe ~]$ 说明:su命令与用户名之间有一个减号(-),作用是完全切换到新的用户,即把环境变量信息也变更为新用户的相应信息,而不是保留原始的信息。强烈建议在切换用户身份时添加这个减号(-)。当从root管理员切换到普通用户时是不需要密码验证的,而从普通用户切换成root管理员就需要进行密码验证了,这也是一个必要的安全检查。 举例2: [linuxprobe@linuxprobe ~]$ su - root Password: Last login: Sat Feb 29 10:17:16 CST 2020 from 192.168.10.14 on pts/2 [root@linuxprobe ~]# 2、sudo服务 sudo命令用于给普通用户提供额外的权限来完成原本root管理员才能完成的任务。sudo服务把特定命令的执行权限赋予给指定用户,既可保证普通用户能够完成特定的工作,也可以避免泄露root管理员密码。sudo服务的配置原则:在保证普通用户完成相应工作的前提下,尽可能少地赋予额外的权限。 命令格式:sudo [参数] 命令名称 参数:sudo命令在普通用户下执行,后跟以下参数:-l列出当前用户可执行的命令;-k清空密码的有效时间,下次执行sudo时需要再次输入当前用户的密码进行密码验证;“-u 用户名或UID值”以指定的用户身份执行命令。 sudo命令具有如下功能:
-
限制用户执行指定的命令;
-
记录用户执行的每一条命令;
-
文件(/etc/sudoers)提供集中的用户管理、权限与主机等参数配置;
-
验证密码的后5分钟内(默认值)无须再让用户再次验证密码。
举例1:-l参数的使用。 [test1@linuxprobe ~]$ sudo -l //查看当前用户可执行的命令 Matching Defaults entries for test1 on this host: requiretty, !visiblepw, always_set_home, env_reset, env_keep="COLORS DISPLAY HOSTNAME HISTSIZE INPUTRC KDEDIR LS_COLORS", env_keep+="MAIL PS1 PS2 QTDIR USERNAME LANG LC_ADDRESS LC_CTYPE", env_keep+="LC_COLLATE LC_IDENTIFICATION LC_MEASUREMENT LC_MESSAGES", env_keep+="LC_MONETARY LC_NAME LC_NUMERIC LC_PAPER LC_TELEPHONE", env_keep+="LC_TIME LC_ALL LANGUAGE LINGUAS _XKB_CHARSET XAUTHORITY", secure_path=/sbin\:/bin\:/usr/sbin\:/usr/bin User test1 may run the following commands on this host: (ALL) ALL 举例2:-k参数的使用。 [test1@linuxprobe ~]$ sudo -k //执行-k参数后再次执行sudo命令需要再次输入密码。 [test1@linuxprobe ~]$ sudo -l [sudo] password for test1: //提示输入当前用户的密码。 Matching Defaults entries for test1 on this host: requiretty, !visiblepw, always_set_home, env_reset, env_keep="COLORS DISPLAY HOSTNAME HISTSIZE INPUTRC KDEDIR LS_COLORS", env_keep+="MAIL PS1 PS2 QTDIR USERNAME LANG LC_ADDRESS LC_CTYPE", env_keep+="LC_COLLATE LC_IDENTIFICATION LC_MEASUREMENT LC_MESSAGES", env_keep+="LC_MONETARY LC_NAME LC_NUMERIC LC_PAPER LC_TELEPHONE", env_keep+="LC_TIME LC_ALL LANGUAGE LINGUAS _XKB_CHARSET XAUTHORITY", secure_path=/sbin\:/bin\:/usr/sbin\:/usr/bin User test1 may run the following commands on this host: (ALL) ALL 举例3:“-u 用户名或UID值”参数的使用。 [test1@linuxprobe ~]$ df -h df: ‘/run/user/0/gvfs’: Permission denied //以自身权限执行df命令提示权限禁止信息。 Filesystem Size Used Avail Use% Mounted on /dev/mapper/rhel-root 18G 3.2G 15G 19% / devtmpfs 985M 0 985M 0% /dev tmpfs 994M 84K 994M 1% /dev/shm tmpfs 994M 9.0M 986M 1% /run tmpfs 994M 0 994M 0% /sys/fs/cgroup /dev/sr0 3.5G 3.5G 0 100% /mnt/cdrom /dev/sda1 497M 119M 379M 24% /boot [test1@linuxprobe ~]$ sudo -u root df -h //以root用户权限执行df命令正常显示信息。 Filesystem Size Used Avail Use% Mounted on /dev/mapper/rhel-root 18G 3.2G 15G 19% / devtmpfs 985M 0 985M 0% /dev tmpfs 994M 84K 994M 1% /dev/shm tmpfs 994M 9.0M 986M 1% /run tmpfs 994M 0 994M 0% /sys/fs/cgroup /dev/sr0 3.5G 3.5G 0 100% /mnt/cdrom /dev/sda1 497M 119M 379M 24% /boot 可使用sudo命令提供的visudo命令来配置用户权限,这条命令在配置用户权限时将禁止多个用户同时修改(/etc/sudoers)配置文件,还可以对配置文件内的参数进行语法检查,并在发现参数错误时进行报错。 注意:只有root管理员才可以使用visudo命令编辑sudo服务的配置文件(/etc/sudoers)。 命令格式:visudo [参数] 参数:-c启用check-only模式, 检查/etc/sudoers文件的语法错误、所有者和模式。 举例1: [root@linuxprobe ~]# visudo -c /etc/sudoers: parsed OK 编辑配置文件(/etc/sudoers) 99行参数格式: 谁可以使用 允许使用的主机=(以谁的身份) 可执行命令的列表 用户 主机或子网=(以谁的身份) NOPASSWD:或PASSWD: 命令绝对路径列表(多个命令用逗号分隔) 注:感叹号“!命令绝对路径”表示排除的命令;“NOPASSWD:”表示普通用户执行sudo时不需要密码验证,“PASSWD:”表示普通用户执行sudo时需要密码验证(默认)。 举例1: root ALL=(ALL) ALL //默认配置 linuxprobe 192.168.10.0/24=(root) ALL,!/usr/bin/passwd test1 192.168.20.10=(root) /usr/bin/cat 说明:配置一些Alias,不用写大段大段的配置,可以通过以下4种Alias来定义。 User_Alias Host_Alias Runas_Alias Cmnd_Alias 1)配置User_Alias具有sudo权限的用户的列表 User_Alias USER_FLAG = user1, user2, user3 2)配置Host_Alias主机的列表 Host_Alias HOST_FLAG = hostname1, hostname2, hostname3 3)配置Runas_Alias用户以什么身份执行(例如root或者oracle)的列表 Runas_Alias RUNAS_FLAG = operator1, operator2, operator3 4)配置Cmnd_Alias允许执行的命令的列表 Cmnd_Alias COMMAND_FLAG = command1, command2, command3 配置权限的格式如下: USER_FLAG HOST_FLAG=(RUNAS_FLAG) COMMAND_FLAG 如果不需要密码验证的话,则按照这样的格式来配置: USER_FLAG HOST_FLAG=(RUNAS_FLAG) NOPASSWD: COMMAND_FLAG
来源:oschina
链接:https://my.oschina.net/u/3706537/blog/3179492