wenmo8 发布的文章

树莓派镜像替换内核

1. 为什么要替换内核

  • 树莓派官方提供的镜像中,自带的内核版本为
    6.6.31

  • 然而
    github
    上提供的内核源码为
    6.6.40
    ,有些微差别

  • 此外,后续很有可能进行内核裁剪定制,替换内核是一个无法绕开的工作

2. 获取内核源码

  • github
    地址:


    https://github.com/raspberrypi/linux

  • 选择使用
    6.6.y
    版本的内核

  • 将其拷贝到
    ubuntu

  • 解压

    $ unzip linux-rpi-6.6.y.zip
    
  • 进入内核源码目录

    $ cd linux-rpi-6.6.y
    

3. 获取交叉编译工具链

4. 内核编译

  • 修改顶层
    Makefile
    ,指定目标平台
    ARCH
    和交叉编译工具链
    CROSS_COMPILE

  • 树莓派CM4使用的时博通BCM2711的方案,因此使用
    bcm2711_defconfig
    生成默认配置文件

    linux-rpi-6.6.y$ make bcm2711_defconfig
    
  • menuconfig
    配置

    linux-rpi-6.6.y$ make menuconfig
    
  • 在这里我需要将
    LAN78XX
    驱动直接编译进内核,不作为外部模块

  • 开始编译,内核镜像/驱动模块/设备树都需要编译,使用
    -j$(nproc)
    全核编译,加快编译速度

    linux-rpi-6.6.y$ make Image modules dtbs -j$(nproc)
    

5. 挂载树莓派镜像到Ubuntu

  • 解压,得到
    *.img
    的镜像文件

  • 挂载镜像到Ubuntu

    # .img镜像
    jun@ubuntu:$ ls
    2024-07-04-raspios-bookworm-arm64-lite.img
    
    # 查看第一个未使用的回环设备
    jun@ubuntu:$ losetup -f
    /dev/loop0
    
    # 将.img镜像关联到回环设备
    jun@ubuntu:$ sudo losetup /dev/loop0 2024-07-04-raspios-bookworm-arm64-lite.img 
    [sudo] password for jun:
    
    # 查看分区,检测到两个区,其中较小的是系统分区,较大的是根文件系统
    jun@ubuntu:$ sudo kpartx -av /dev/loop0
    add map loop0p1 (253:0): 0 1048576 linear 7:0 8192
    add map loop0p2 (253:1): 0 4481024 linear 7:0 1056768
    
    # 创建系统分区挂载目录
    jun@ubuntu:$ mkdir boot
    
    # 创建根文件系统挂载目录
    jun@ubuntu:$ mkdir rootfs
    
    # 挂载系统分区
    jun@ubuntu:$ sudo mount /dev/mapper/loop0p1 ./boot/
    
    # 挂载根文件系统
    jun@ubuntu:$ sudo mount /dev/mapper/loop0p2 ./rootfs/
    
    # 查看系统分区
    jun@ubuntu:$ ls ./boot/
    bcm2710-rpi-2-b.dtb       bcm2710-rpi-zero-2-w.dtb  bcm2711-rpi-cm4s.dtb       bootcode.bin  fixup4db.dat  fixup_x.dat      kernel8.img       start4.elf    start_x.elf
    bcm2710-rpi-3-b.dtb       bcm2711-rpi-400.dtb       bcm2712d0-rpi-5-b.dtb      cmdline.txt   fixup4x.dat   initramfs_2712   LICENCE.broadcom  start4x.elf
    bcm2710-rpi-3-b-plus.dtb  bcm2711-rpi-4-b.dtb       bcm2712-rpi-5-b.dtb        config.txt    fixup_cd.dat  initramfs8       overlays          start_cd.elf
    bcm2710-rpi-cm3.dtb       bcm2711-rpi-cm4.dtb       bcm2712-rpi-cm5-cm4io.dtb  fixup4cd.dat  fixup.dat     issue.txt        start4cd.elf      start_db.elf
    bcm2710-rpi-zero-2.dtb    bcm2711-rpi-cm4-io.dtb    bcm2712-rpi-cm5-cm5io.dtb  fixup4.dat    fixup_db.dat  kernel_2712.img  start4db.elf      start.elf
    
    # 查看根文件系统
    jun@ubuntu:$ ls ./rootfs/
    bin  boot  dev  etc  home  lib  lost+found  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var
    

6. 安装新内核

  • 进入内核源码目录

  • 安装内核模块到根文件系统

    linux-rpi-6.6.y$ sudo env PATH=$PATH make INSTALL_MOD_PATH=../rootfs modules_install
    

  • 安装头文件到根文件系统的
    usr
    目录

    linux-rpi-6.6.y$ sudo make headers_install INSTALL_HDR_PATH=../rootfs/usr/
    
  • 安装
    Image
    到挂载的
    boot
    分区

    linux-rpi-6.6.y$ sudo cp arch/arm64/boot/Image ../boot/kernel8.img
    
  • 安装设备树文件

    linux-rpi-6.6.y$ sudo cp arch/arm64/boot/dts/broadcom/*.dtb ../boot/
    linux-rpi-6.6.y$ sudo cp arch/arm64/boot/dts/overlays/*.dtb* ../boot/overlays/
    
  • 替换
    version.h

    linux-rpi-6.6.y$ sudo cp include/generated/uapi/linux/version.h ../rootfs/usr/include/linux/version.h
    

7. 解决内核模块无法加载的问题

  • 进入
    rootfs
    根文件系统中的驱动存放目录

    linux-rpi-6.6.y$ cd ./rootfs/lib/modules/6.6.40-v8/
    
  • 查找.ko文件,发现没有任何内核模块

    6.6.40-v8$ find . -name *ko
    
  • 检查发现,该目录下有很多
    .ko.xz
    文件,原因是
    make module_install
    命令执行时,自动将.ko文件进行了压缩,但是这样会导致内核启动时无法加载模块,需要将压缩的.ko文件进行解压

  • 编写脚本
    modules_install.sh
    用来在系统第一次开机时生成
    modules.dep
    文件,将该脚本存放在
    rootfs/lib/modules/6.6.40-v8/
    目录下,脚本内容如下

    #!/bin/bash
    
    # modules path
    MODULES_PATH=/lib/modules/$(uname -r)
    
    # 解压*.ko.xz文件
    module_decompress()
    {
            MODULES_XZ_FILES=`find ${MODULES_PATH} -name *ko.xz`
    
            for MODULE in ${MODULES_XZ_FILES}
            do
                    set -x
                    xz -dk ${MODULE}
                    set +x
            done
            
            return 0
    }
    
    RET=$(cat ${MODULES_PATH}/modules.dep)
    if [ -z "${RET}" ]; then
            
        # modules decompress
        module_decompress
        
        # generate modules.dep
        depmod
            
        # make sure this script executed only once
        SCRIPT_NAME=$(basename $0)
        sed -i "/$SCRIPT_NAME/d" /etc/rc.local
    		
        # reboot
        reboot
    else
            echo "modules already installed!"
    fi
    
    exit 0
    
  • 给脚本以可执行权限

    6.6.40-v8$ sudo chmod 777 modules_install.sh
    
  • 在根文件系统下的
    etc/rc.local
    中添加如下内容,使
    module_install.sh
    脚本开机自启动

    # modules_install
    /bin/bash /lib/modules/$(uname -r)/modules_install.sh &
    

8. 选择内核

  • 修改
    boot
    分区下的
    config.txt
    ,选择使用新的内核
    $ sudo vi ./boot/config.txt
    

  • config.txt
    末尾添加如下内容
    kernel=kernel8.img
    

9. 取消镜像挂载

  • 取消
    5. 挂载树莓派镜像到Ubuntu
    的镜像挂载
    jun@ubuntu:$ ls
    2024-07-04-raspios-bookworm-arm64-lite.img  boot  rootfs
    jun@ubuntu:$ sudo umount ./boot 
    jun@ubuntu:$ sudo umount ./rootfs
    jun@ubuntu:$ sudo losetup -d /dev/loop0
    

10. 镜像烧录

11. 查看内核是否替换成功

  • 开机,首次开机时系统会自动重启两次,属正常现象
  • 进入系统后查看内核版本,内核版本
    6.6.40
    ,替换成功
  # uname -a
  Linux IG-210 6.6.40-v8 #2 SMP PREEMPT Tue Aug 27 14:04:24 CST 2024 aarch64 GNU/Linux

背景

公司政策满3年可以换新电脑,前段时间申请了下,到手后发现是Win11系统,配置翻倍,欣然接受,把一些常用的软件都安装上,但是,用了一段时间后,发现右键刷新要点击2次,开始菜单找东西也完全靠搜索,任务栏不可定义了,和以前常用的右下角日历小工具不兼容,如果要和这些用惯好多年的操作say goodbye,是一个难受的事情,于是想办法折腾一下,把这些恢复“原Win10”的效果。

本文解决的痛点

1、开始菜单变化,找内容靠搜索

2、任务栏小工具无法展示

3、任务栏不可设置位置(顶部、底部、左侧、右侧)

4、鼠标右键菜单没有刷新

用到的工具

StartAllBack 3.6.14

下载地址:
https://www.startallback.com/

ps:这里特意说明一下这个版本号很重要,测试了几个版本,发现这个版本对任务栏小工具的支持最好

设置步骤和效果

1、启用经典开始菜单

效果图

2、启用增强型经典任务栏

效果图

3、资源窗口

效果图

总结

StartAllBack 是一款可以让开始菜单/任务栏/右键菜单重回经典模式的工具,如果有一天你也受不了Win11的新模式,可以用这个软件,重回“经典”也不失为一个不错的选择。

通讯对象

PDO

我的观点:一个 CANopen 设备可以拥有最多 512 个 RPDO 和 512 个 TPDO,总共最多 1024 个 PDO。(得到GPT4o的肯定)

CiA协议栈观点:一个只有一个逻辑设备的 CANopen 设备最多有 512 个 PDO。

PDO的两种用法:

  • TPDO:生产者PDO
  • RPDO:消费者PDO
特点

小而快

传输模式
  • 同步传输:通过(SYNC对象)来获取同步信号

    ​ 同步TPDO的传输倍率:n表示经过n个同步信号后发送下一个消息。(为0时,收到触发信号后的第一个同步信号后进行发送)

  • 事件驱动传输

触发模式

消息的触发模式

触发方式 解释 应用场景
事件驱动 特定的事件进行驱动 超限报警
定时器驱动 固定的时间间隔进行发送 周期性更新数据
远程请求 其它节点通过发送 RTR 帧来请求该设备发送相应的 PDO 允许节点灵活的请求数据
同步触发 由一定数量的Sync和内部事件触发 协调多个设备的同步操作
PDO读写
  • 写协议(推模式):生产者向0 ~ n个消费者写PDO数据(参考spi写数据)
  • 读协议(拉模式):消费者发送RTR请求向生产者请求数据(参考spi读数据)

PDO写

参数 请求/执行 响应/确认
PDO编号 强制
数据 强制

image

PDO读

参数 请求/执行 响应/确认
PDO编号 强制
数据 强制

image

对象描述
  • 通讯参数:定义了PDO的基本特性,如COB-ID、传输类型、禁止时间和事件定时器等,它决定了PDO是以何种方式发送和接收数据。


    • COB-ID:通过COB-ID可以唯一识别一个PDO消息


      • TPDO和RPDO的COB-ID是不同的
      • COB-ID的最高位设置为1时,表示该PDO被禁用
    • 传输类型(Transmission Type):PDO消息的传输方式


      • 同步传输
      • 异步传输

image

  • 抑制时间(Inhibit Time):两个PDO之间的最小时间间隔(以ms为单位)

  • 事件计时器(Event Timer):定了事件触发的周期时间,PDO在到期时发送(表示没有事件发生)

  • 同步计数器(Sync Counter):在同步传输类型下使用,规定了在接收多少次同步信号后发送

  • 映射参数:定义了哪些对象字典条目(变量)被包含在 PDO 消息中,以及这些变量在消息中的位置。


    • 映射条目数:定义了一个PDO在对象字典中的条目数量,每个条目代表一个数据段
    • 映射对象:每个映射对象由32位组成
      • 索引(Index,16位):对象字典中的索引
      • 子索引(Sub-Index, 8位):对象字典项的子索引
      • 位长度(Bit Length,8位):表示映射对象的数据位数(常为字节的整数倍)
PDO 传输实例

点我查看

Multiplex PDO(不懂且没看完,因资料少)

有两种MPDO的使用方法。第一是
目的地址模式(DAM)PDO
,第二是
源地址模式(SAM)PDO

支持接收MPDO的ANope的设备为
MPDO消费者
,支持发送MPDO的C为
MPDO生产者

发送MPDO由事件驱动,不支持定时、远程请求和同步触发模式

寻址模式
  • 目的地址模式(DAM):一个DAM-MPDO可以被所有该MPDO的消费者同时接收(
    类似于广播
    ),且无 应答。如果对象不存在,则生成EMCY帧。
  • 源地址模式(SAM):
MPDO读写

MPDO写

参数 请求/执行 响应/确认
PDO编号 强制
地址类型 强制
Node-ID 强制
Multiplexer 强制
数据 强制

image

SDO

特点

大而慢

传输模式
传输模式 概述 应答模式
快速传输 因数据小于等于4个字节,一帧足够发送完,无需分段 一帧一答
段传输 把数据分割成若干个段,每一段通过单独的帧发送 每帧每答
块传输 把多个段组合成一个子块,每一子块发送完才接受应答信号,所有子块组成块 每子块每答

快速传输模式:
只需要一次数据的发送和应答就可以完成数据传输,效率高,但是传输的数据量少。

段传输:
数据被分成多个段(segment)来传输,每个段最多可以包含7个字节的数据。这种模式的实现较为简单,适合中等长度的数据传输。

块传输:
相比段传输模式,块传输模式能够处理更大的数据量并且提高了传输效率。数据被分为多个块(block),每个块可以包含多个段。块传输还具有校验机制,能够确保数据的完整性和可靠性。

协议详解见
Or CiA301中文手册43页

SYNC

  • 同步生产者定期广播
    同步对象
    (SYNC)。
  • SYNC提供基本的网络同步机制,遵循生产消费模型,该服务无应答。
  • 传输1个字节的计数器(可选)

TIME

  • TIME生产者定期广播
    时间戳对象
    (TIME)。
  • TIME提供了简单的网络时钟。CANopen设备通过时间戳对象来校准本地时间。
  • 传输6个字节的时间戳

EMCY

  • 当设备内部出现致命错误将触发应急(EMCY)报文
  • 由应用设备以最高优先级发送到其他设备。任何具备紧急事件监控与处理能力的从站会接收并处理紧急报文。

前言

在《
[apue] 进程控制那些事儿
》一文的"进程创建-> exec -> 解释器文件"一节中,曾提到脚本文件的识别是由内核作为 exec 系统调用处理的一部分来完成的,并且有以下特性:

  • 指定解释器的以 #!  (shebang) 开头的第一行长度不得超过 128
  • shebang 最多只能指定一个参数
  • shebang 指定的命令与参数会成为新进程的前 2 个参数,用户提供的其它参数依次往后排

这些特性是如何实现的?带着这个疑问,找出系统对应的内核源码看个究竟。

源码定位

和《
[kernel] 带着问题看源码 —— 进程 ID 是如何分配的
》一样,这里使用
bootlin
查看内核 3.10.0 版本源码,脚本文件是在 execve 时解析的,所以先搜索 sys_ execve:

整个调用链如下:

sys_execve -> do_execve -> do_execve_common -> search_binary_handler-> load_binary -> load_script (binfmt_script.c)

为了快速进入主题,前面咱们就不一一细看了,主要解释一下 search_binary_handler。

Linux 中加载不同文件格式的方式是可扩展的,这主要是通过内核模块来实现的,每个模块实现一个格式,新的格式可通过编写内核模块实现快速支持而无需修改内核源码。刚才浏览代码的时候已经初窥门径:

这是目前内核内置的几个模块

  • binfmt_elf:最常用的 Linux 二进制可执行文件
  • binfmt_elf_fdpic:缺失 MMU 架构的二进制可执行文件
  • binfmt_em86:在 Aplha 机器上运行 Intel 的 Linux 二进制文件
  • binfmt_aout:Linux 老的可执行文件
  • binfmt_script:脚本文件
  • binfmt_misc:一种新机制,支持运行期文件格式与应用对应关系的绑定

基本可以归纳为三大类:

  • 可执行文件
  • 脚本文件 (script)
  • 机制拓展 (misc)

其中 misc 机制类似 Windows 上文件通过后缀与应用进行绑定的机制,它除了通过后缀,还可以通过检测文件中的 Magic 字段,作为判断文件类型的依据。目前的主要应用方向是跨架构运行,例如在 x86 机器上运行 arm64、甚至 Windows 程序 (wine),相比编写内核模块,便利性又降低了一个等级。

本文主要关注脚本文件的处理过程。

binfmt

内核模块本身并不难实现,以 script 为例:

static struct linux_binfmt script_format = {
	.module		= THIS_MODULE,
	.load_binary	= load_script,
};

static int __init init_script_binfmt(void)
{
	register_binfmt(&script_format);
	return 0;
}

static void __exit exit_script_binfmt(void)
{
	unregister_binfmt(&script_format);
}

core_initcall(init_script_binfmt);
module_exit(exit_script_binfmt);

主要是通过 register_binfmt / unregister_binfmt 来插入、删除 linux_binfmt 信息节点。

/*
 * This structure defines the functions that are used to load the binary formats that
 * linux accepts.
 */
struct linux_binfmt {
	struct list_head lh;
	struct module *module;
	int (*load_binary)(struct linux_binprm *);
	int (*load_shlib)(struct file *);
	int (*core_dump)(struct coredump_params *cprm);
	unsigned long min_coredump;	/* minimal dump size */
};

linux_binfmt 的内容不多,而且回调函数不用全部实现,没有用到的留空就完事了。下面看下插入节点过程:

static LIST_HEAD(formats);
static DEFINE_RWLOCK(binfmt_lock);

void __register_binfmt(struct linux_binfmt * fmt, int insert)
{
	BUG_ON(!fmt);
	write_lock(&binfmt_lock);
	insert ? list_add(&fmt->lh, &formats) :
		 list_add_tail(&fmt->lh, &formats);
	write_unlock(&binfmt_lock);
}

/* Registration of default binfmt handlers */
static inline void register_binfmt(struct linux_binfmt *fmt)
{
	__register_binfmt(fmt, 0);
}

利用 linux_binfmt.lh 字段 (list_head) 实现链表插入,链表头为全局变量 formats。

search_binary_handler

再看 search_binary_handler 利用 formats 遍历链表的过程:

retval = -ENOENT;
for (try=0; try<2; try++) {

最多尝试 2 次

    read_lock(&binfmt_lock);
	list_for_each_entry(fmt, &formats, lh) {

加锁;通过 formats 遍历整个链表

        int (*fn)(struct linux_binprm *) = fmt->load_binary;
		if (!fn)
			continue;
		if (!try_module_get(fmt->module))
			continue;
		read_unlock(&binfmt_lock);
		bprm->recursion_depth = depth + 1;

检查内核模块是否 alive;执行 load_binary 前解锁 formats 链表以便嵌套;更新嵌套深度

        retval = fn(bprm);
		bprm->recursion_depth = depth;
		if (retval >= 0) {
			if (depth == 0) {
				trace_sched_process_exec(current, old_pid, bprm);
				ptrace_event(PTRACE_EVENT_EXEC, old_vpid);
			}
			put_binfmt(fmt);
			allow_write_access(bprm->file);
			if (bprm->file)
				fput(bprm->file);
			bprm->file = NULL;
			current->did_exec = 1;
			proc_exec_connector(current);
			return retval;
		}

恢复嵌套尝试;执行成功,提前退出

        read_lock(&binfmt_lock);
		put_binfmt(fmt);
		if (retval != -ENOEXEC || bprm->mm == NULL)
			break;
		if (!bprm->file) {
			read_unlock(&binfmt_lock);
			return retval;
		}
	}

执行失败,重新加锁;如果非 ENOEXEC 错误,继续尝试下个 fmt

    read_unlock(&binfmt_lock);
	break;
}

遍历完毕,退出

其中 list_for_each_entry 就是 Linux 对 list 遍历的封装宏:

/**
 * list_for_each_entry	-	iterate over list of given type
 * @pos:	the type * to use as a loop cursor.
 * @head:	the head for your list.
 * @member:	the name of the list_struct within the struct.
 */
#define list_for_each_entry(pos, head, member)				\
	for (pos = list_entry((head)->next, typeof(*pos), member);	\
	     &pos->member != (head); 	\
	     pos = list_entry(pos->member.next, typeof(*pos), member))

本质是个 for 循环。另外,之前的 for (try < 2) 其实并不生效,因为总会被末尾的 break 打断。

不过这里提示了一点 load_binary 的写法:当该接口返回 -ENOEXEC 时,表示这个文件“不合胃口”,请继续遍历 formats 列表尝试,下面在解读 load_script 时可以多加留意。

另外 binfmt 是可以嵌套的,假设在调用一个脚本,它使用 awk 作为解释器,那么整个执行过程看起来像下面这样:

execve (xxx.awk) -> load_script (binfmt_script) -> load_elf_binary (binfmt_elf)

这是因为 awk 作为可执行文件,本身也需要 binfmt 的处理,稍等就可以在 load_script 中看到这一点。

目前 Linux 没有对嵌套深度施加限制。

源码分析

经过一番背景知识铺垫,终于可以进入 binfmt_script 好好看看啦:

static int load_script(struct linux_binprm *bprm)
{
	const char *i_arg, *i_name;
	char *cp;
	struct file *file;
	char interp[BINPRM_BUF_SIZE];
	int retval;

	if ((bprm->buf[0] != '#') || (bprm->buf[1] != '!'))
		return -ENOEXEC;

脚本不以 #! 开头的,忽略;注意 interp 数组的长度:#define BINPRM_BUF_SIZE 128,这是 shebang 不能超过 128 的来源

    /*
	 * This section does the #! interpretation.
	 * Sorta complicated, but hopefully it will work.  -TYT
	 */

	allow_write_access(bprm->file);
	fput(bprm->file);
	bprm->file = NULL;

系统已读取文件头部的一部分字节到内存,脚本文件用完了,释放

    bprm->buf[BINPRM_BUF_SIZE - 1] = '\0';
	if ((cp = strchr(bprm->buf, '\n')) == NULL)
		cp = bprm->buf+BINPRM_BUF_SIZE-1;
	*cp = '\0';

最多截取前 127 个字符,并向前搜索 shebang 结尾 (\n),若有,则设置新的结尾到那里

    while (cp > bprm->buf) {
		cp--;
		if ((*cp == ' ') || (*cp == '\t'))
			*cp = '\0';
		else
			break;
	}
	for (cp = bprm->buf+2; (*cp == ' ') || (*cp == '\t'); cp++);
	if (*cp == '\0') 
		return -ENOEXEC; /* No interpreter name found */

前后 trim 空白字符,如果没有任何内容,忽略;注意初始时 cp 指向字符串尾部,结束时,cp 指向有效信息头部

    i_name = cp;
	i_arg = NULL;
	for ( ; *cp && (*cp != ' ') && (*cp != '\t'); cp++)
		/* nothing */ ;
	while ((*cp == ' ') || (*cp == '\t'))
		*cp++ = '\0';
	if (*cp)
		i_arg = cp;
	strcpy (interp, i_name);

跳过命令名;忽略空白字符;剩下的若有内容全部作为一个参数;命令名复制到 interp 数组中备用

    /*
	 * OK, we've parsed out the interpreter name and
	 * (optional) argument.
	 * Splice in (1) the interpreter's name for argv[0]
	 *           (2) (optional) argument to interpreter
	 *           (3) filename of shell script (replace argv[0])
	 *
	 * This is done in reverse order, because of how the
	 * user environment and arguments are stored.
	 */
	retval = remove_arg_zero(bprm);
	if (retval)
		return retval;
	retval = copy_strings_kernel(1, &bprm->interp, bprm);
	if (retval < 0) return retval; 
	bprm->argc++;
	if (i_arg) {
		retval = copy_strings_kernel(1, &i_arg, bprm);
		if (retval < 0) return retval; 
		bprm->argc++;
	}
	retval = copy_strings_kernel(1, &i_name, bprm);
	if (retval) return retval; 
	bprm->argc++;
	retval = bprm_change_interp(interp, bprm);
	if (retval < 0)
		return retval;

删除 argv 的第一个参数,分别将命令名 (i_name)、参数 (i_arg 如果有的话)、脚本文件名 (bprm->interp) 放置到 argv 前三位。

注意这里调用的顺序恰好相反:bprm->interp、i_arg、i_name,这是由于 argv 在进程中特殊的存放方式导致,参考后面的解说;
最后更新 bprm 中的命令名

    /*
	 * OK, now restart the process with the interpreter's dentry.
	 */
	file = open_exec(interp);
	if (IS_ERR(file))
		return PTR_ERR(file);

	bprm->file = file;
	retval = prepare_binprm(bprm);
	if (retval < 0)
		return retval;

通过命令名指定的路径打开文件,并设置到当前进程,准备加载前的各种信息,包括预读文件的头部的一些内容

    return search_binary_handler(bprm);
}

使用新命令的信息继续搜索 binfmt 模块并加载之

这里主要补充一点,对于 shebang 中的命令名字段,中间不能包含空格,否则会被提前截断,即使使用引号包围也不行 (解析代码根本未对引号做处理),下面是个例子:

> pwd
/ext/code/apue/07.chapter/test black
> ls -lh
total 52K
-rwxr-xr-x 1 yunhai01 DOORGOD 48K Aug 23 19:17 echo
-rwxr--r-- 1 yunhai01 DOORGOD  47 Aug 23 19:17 echo.sh
> cat echo.sh
#! /ext/code/apue/07.chapter/test black/demo

> ./echo a b c
argv[0] = ./echo
argv[1] = a
argv[2] = b
argv[3] = c
> ./echo.sh a b c
bash: ./echo.sh: /ext/code/apue/07.chapter/test: bad interpreter: No such file or directory

文件头预读

这里主要解释两点,一是 prepare_binprm 会预读文件头部的一些数据,供后面 binfmt 判断使用:

/* 
 * Fill the binprm structure from the inode. 
 * Check permissions, then read the first 128 (BINPRM_BUF_SIZE) bytes
 *
 * This may be called multiple times for binary chains (scripts for example).
 */
int prepare_binprm(struct linux_binprm *bprm)
{
	umode_t mode;
	struct inode * inode = file_inode(bprm->file);
	int retval;

	mode = inode->i_mode;
	if (bprm->file->f_op == NULL)
		return -EACCES;

    ...

	/* fill in binprm security blob */
	retval = security_bprm_set_creds(bprm);
	if (retval)
		return retval;
	bprm->cred_prepared = 1;

	memset(bprm->buf, 0, BINPRM_BUF_SIZE);
	return kernel_read(bprm->file, 0, bprm->buf, BINPRM_BUF_SIZE);
}

目前这个 BINPRM_BUF_SIZE 的长度也是 128:

#define BINPRM_BUF_SIZE 128

在 do_execve_common 中也会调用这个接口来为第一次 binfmt 识别做准备:

/*
 * sys_execve() executes a new program.
 */
static int do_execve_common(const char *filename,
				struct user_arg_ptr argv,
				struct user_arg_ptr envp)
{
	struct linux_binprm *bprm;
	struct file *file;
	struct files_struct *displaced;
	bool clear_in_exec;
	int retval;
	const struct cred *cred = current_cred();

    ...

	file = open_exec(filename);
	retval = PTR_ERR(file);
	if (IS_ERR(file))
		goto out_unmark;

	sched_exec();

	bprm->file = file;
	bprm->filename = filename;
	bprm->interp = filename;

	retval = bprm_mm_init(bprm);
	if (retval)
		goto out_file;

	bprm->argc = count(argv, MAX_ARG_STRINGS);
	if ((retval = bprm->argc) < 0)
		goto out;

	bprm->envc = count(envp, MAX_ARG_STRINGS);
	if ((retval = bprm->envc) < 0)
		goto out;

	retval = prepare_binprm(bprm);
	if (retval < 0)
		goto out;

没错,就是这里了

    retval = copy_strings_kernel(1, &bprm->filename, bprm);
	if (retval < 0)
		goto out;

	bprm->exec = bprm->p;
	retval = copy_strings(bprm->envc, envp, bprm);
	if (retval < 0)
		goto out;

	retval = copy_strings(bprm->argc, argv, bprm);
	if (retval < 0)
		goto out;

	retval = search_binary_handler(bprm);
	if (retval < 0)
		goto out;

    ...
}

argv 调整

另外一点是 argv 在内存中的布局,参考之前写的《
[apue] 进程环境那些事儿
》,这里直接贴图:

命令行参数与环境变量是放在进程高地址空间的末尾,以 \0 为间隔的字符串。由于有高地址“天花板”在存在,这里必需先根据字符串长度定位到起始位置,再复制整个字符串,此外为了保证 argv[0] 地址小于 argv[1],整个数组也需要从后向前遍历。这里借用之前写的一个例子证明这一点:

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

int data1 = 2;
int data2 = 3;
int data3;
int data4;

int main (int argc, char *argv[])
{
  char buf1[1024] = { 0 };
  char buf2[1024] = { 0 };
  char *buf3 = malloc(1024);
  char *buf4 = malloc(1024);
  printf ("onstack %p, %p\n",
    buf1,
    buf2);

  extern char ** environ;
  printf ("env %p\n", environ);
  for (int i=0; environ[i] != 0; ++ i)
    printf ("env[%d] %p\n", i, environ[i]);

  printf ("arg %p\n", argv);
  for (int i=0; i < argc; ++ i)
    printf ("arg[%d] %p\n", i, argv[i]);

  printf ("onheap %p, %p\n",
    buf3,
    buf4);

  free (buf3);
  free (buf4);

  printf ("on bss %p, %p\n",
    &data3,
    &data4);

  printf ("on init %p, %p\n",
    &data1,
    &data2);

  printf ("on code %p\n", main);
  return 0;
}

随便给一些参数让它跑个输出:

> ./layout a b c d
onstack 0x7fff2757a970, 0x7fff2757a570
env 0x7fff2757aea8
env[0] 0x7fff2757b4fb
env[1] 0x7fff2757b511
env[2] 0x7fff2757b534
env[3] 0x7fff2757b544
env[4] 0x7fff2757b558
env[5] 0x7fff2757b566
env[6] 0x7fff2757b587
env[7] 0x7fff2757b5af
env[8] 0x7fff2757b5c7
env[9] 0x7fff2757b5e7
env[10] 0x7fff2757b5fa
env[11] 0x7fff2757b608
env[12] 0x7fff2757bcc0
env[13] 0x7fff2757bcc8
env[14] 0x7fff2757be1d
env[15] 0x7fff2757be3b
env[16] 0x7fff2757be59
env[17] 0x7fff2757be6a
env[18] 0x7fff2757be81
env[19] 0x7fff2757be9b
env[20] 0x7fff2757bea3
env[21] 0x7fff2757beb3
env[22] 0x7fff2757bec4
env[23] 0x7fff2757bee0
env[24] 0x7fff2757bf13
env[25] 0x7fff2757bf36
env[26] 0x7fff2757bf62
env[27] 0x7fff2757bf83
env[28] 0x7fff2757bfa1
env[29] 0x7fff2757bfc3
env[30] 0x7fff2757bfce
arg 0x7fff2757ae78
arg[0] 0x7fff2757b4ea
arg[1] 0x7fff2757b4f3
arg[2] 0x7fff2757b4f5
arg[3] 0x7fff2757b4f7
arg[4] 0x7fff2757b4f9
onheap 0x1056010, 0x1056420
on bss 0x6066b8, 0x6066bc
on init 0x606224, 0x606228
on code 0x40179d

重点看下 argv 与 envp 的地址,envp 高于 argv;再看各个数组内部的情况,索引低的地址也低。结合之前的内存布局图,就需要这样排布各个参数:

  • 先排布 envp,envp 内部从后向前遍历
  • 后排布 argv,argv 内部从后向前遍历

代码也确实是这样写的:

    retval = copy_strings(bprm->envc, envp, bprm);
	if (retval < 0)
		goto out;

	retval = copy_strings(bprm->argc, argv, bprm);
	if (retval < 0)
		goto out;

上面这段之前在 do_execve_common 中展示过,先排布 envp 后排布 argv,再看数组内部的处理:

/*
 * 'copy_strings()' copies argument/environment strings from the old
 * processes's memory to the new process's stack.  The call to get_user_pages()
 * ensures the destination page is created and not swapped out.
 */
static int copy_strings(int argc, struct user_arg_ptr argv,
			struct linux_binprm *bprm)
{
	struct page *kmapped_page = NULL;
	char *kaddr = NULL;
	unsigned long kpos = 0;
	int ret;

	while (argc-- > 0) {

倒序遍历数组

        const char __user *str;
		int len;
		unsigned long pos;
        ret = -EFAULT;

		str = get_user_arg_ptr(argv, argc);
		if (IS_ERR(str))
			goto out;

		len = strnlen_user(str, MAX_ARG_STRLEN);
		if (!len)
			goto out;

		ret = -E2BIG;
		if (!valid_arg_len(bprm, len))
			goto out;

		/* We're going to work our way backwords. */
		pos = bprm->p;
		str += len;
		bprm->p -= len;

计算当前字符串长度并预留位置,注意复制时可能存在跨页情况,字符串也是从尾向头分割为一块块复制的

        while (len > 0) {
			int offset, bytes_to_copy;

			if (fatal_signal_pending(current)) {
				ret = -ERESTARTNOHAND;
				goto out;
			}
			cond_resched();

			offset = pos % PAGE_SIZE;
			if (offset == 0)
				offset = PAGE_SIZE;

			bytes_to_copy = offset;
			if (bytes_to_copy > len)
				bytes_to_copy = len;

			offset -= bytes_to_copy;
			pos -= bytes_to_copy;
			str -= bytes_to_copy;
			len -= bytes_to_copy;

			if (!kmapped_page || kpos != (pos & PAGE_MASK)) {
				struct page *page;

				page = get_arg_page(bprm, pos, 1);
				if (!page) {
					ret = -E2BIG;
					goto out;
				}

				if (kmapped_page) {
					flush_kernel_dcache_page(kmapped_page);
					kunmap(kmapped_page);
					put_arg_page(kmapped_page);
				}
				kmapped_page = page;
				kaddr = kmap(kmapped_page);
				kpos = pos & PAGE_MASK;
				flush_arg_page(bprm, kpos, kmapped_page);
			}
			if (copy_from_user(kaddr+offset, str, bytes_to_copy)) {
				ret = -EFAULT;
				goto out;
			}
		}
	}
	ret = 0;

复制单个字符串,字符串可能非常大,一个就好几页,干活的主要是 copy_from_user

out:
	if (kmapped_page) {
		flush_kernel_dcache_page(kmapped_page);
		kunmap(kmapped_page);
		put_arg_page(kmapped_page);
	}
	return ret;
}

出错处理

了解了 argv 与 envp 的布局后,突然发现在数组前插入元素反而简单了,不过需要先将第一个元素删除,这里 Linux 使用了一个 trick:直接移动 argv 指针 (bprm->p) 略过第一个参数:

/*
 * Arguments are '\0' separated strings found at the location bprm->p
 * points to; chop off the first by relocating brpm->p to right after
 * the first '\0' encountered.
 */
int remove_arg_zero(struct linux_binprm *bprm)
{
	int ret = 0;
	unsigned long offset;
	char *kaddr;
	struct page *page;

	if (!bprm->argc)
		return 0;

	do {
		offset = bprm->p & ~PAGE_MASK;
		page = get_arg_page(bprm, bprm->p, 0);
		if (!page) {
			ret = -EFAULT;
			goto out;
		}
		kaddr = kmap_atomic(page);

		for (; offset < PAGE_SIZE && kaddr[offset];
				offset++, bprm->p++)
			;

		kunmap_atomic(kaddr);
		put_arg_page(page);

		if (offset == PAGE_SIZE)
			free_arg_page(bprm, (bprm->p >> PAGE_SHIFT) - 1);
	} while (offset == PAGE_SIZE);

	bprm->p++;
	bprm->argc--;
	ret = 0;

out:
	return ret;
}

经过更新后,bprm->p 指向了第二个参数,argc 减少了 1,后面新参数插入时,会自动覆盖它:

    retval = copy_strings_kernel(1, &bprm->interp, bprm);
	if (retval < 0) return retval; 
	bprm->argc++;
	if (i_arg) {
		retval = copy_strings_kernel(1, &i_arg, bprm);
		if (retval < 0) return retval; 
		bprm->argc++;
	}
	retval = copy_strings_kernel(1, &i_name, bprm);
	if (retval) return retval; 
	bprm->argc++;

copy_string_kernel 将基于 kernel 获取的源字符串调用 copy_strings,因此一切又回到了前面的逻辑,这里只要保持参数倒序处理即可,这段代码之前在 load_script 时展示过,这回大家看明白了吗?

总结

开头提出的三个问题:

  • 指定解释器的以 shebang 开头的第一行长度不得超过 128
  • shebang 最多只能指定一个参数
  • shebang 指定的命令与参数会成为新进程的前 2 个参数,用户提供参数依次后排

都一一得到了解答,其中 shebang 长度 128 这个限制,和整个 execve 预读长度 ()BINPRM_BUF_SIZE) 息息相关,也与 binfmt_misc 规定的格式相关,看起来不好随便突破。

另外通过通读源码,得到了以下额外的知识:

  • 解释器文件可嵌套,且没有深度限制
  • 解释器第一行中的命令名不能包含空白字符
  • 命令行参数在内存空间是倒排的,这一点貌似主要是为了在数组头部插入元素更便利,如果有需求要在数组尾部插入元素,可能就得改为正排了

最后对于 shebang 支持多个 arguments 这一点,目前看只要修改 binfmt_scrpts,应该是可以实现的,这个课题就留给感兴趣的读者作为作业吧,哈哈~

参考

[1].
linux下使用binfmt_misc设定不同二进制的打开程序

[2].
Linux中的binfmt-misc原理分析

[3].
binfmt.d 中文手册

[4].
Linux 的 binfmt_misc (binfmt) module 介紹

[5].
Linux系统的可执行文件格式详细解析

[6].
Kernel Support for miscellaneous Binary Formats (binfmt_misc)

PyJWT

python-jose
是两个用于处理 JSON Web Tokens (JWT) 的 Python 库。它们都有助于生成、解码、验证和管理 JWT,但它们在功能范围和设计哲学上有一些重要的区别。本篇介绍它们之间的一些差异,以及在项目中使用FastAPI+
python-jose
来处理访问令牌的生成以及一些例子代码供参考。

1、
PyJWT

python-jose的差异

PyJWT

PyJWT
是一个专门处理 JWT 的 Python 库,它旨在简化 JWT 的创建和验证。

特点:

  • 专注于 JWT
    : PyJWT 专门用于 JWT 的处理,不提供其他类型的加密或签名功能。
  • 简单易用
    : PyJWT 提供了简单的 API,用于创建和验证 JWT。
  • 支持常见的签名算法
    : 包括 HMAC (HS256, HS384, HS512) 和 RSA (RS256, RS384, RS512)。
  • 轻量级
    : 由于 PyJWT 专注于 JWT,依赖少,安装和使用都很简便。

主要用法示例:

importjwtimportdatetime#创建一个JWT
payload ={"user_id": 123,"exp": datetime.datetime.utcnow() + datetime.timedelta(hours=1)
}
secret
= 'your-secret-key'token= jwt.encode(payload, secret, algorithm='HS256')#解码JWT decoded = jwt.decode(token, secret, algorithms=['HS256'])print(decoded)

python-jose

python-jose
是一个更广泛的加密库,它不仅支持 JWT,还支持多种 JOSE (JSON Object Signing and Encryption) 标准,包括 JWS (JSON Web Signature)、JWE (JSON Web Encryption)、JWK (JSON Web Key)、JWA (JSON Web Algorithms) 等。

特点:

  • 全面的 JOSE 支持
    : 除了 JWT,
    python-jose
    还支持其他 JOSE 标准,因此功能更强大、更灵活。
  • 多种加密与签名算法
    : 支持比 PyJWT 更多的算法,如直接加密 (dir)、对称密钥加密 (A256KW, A192KW, A128KW),以及 RSA 和 ECDSA 的多种模式。
  • 丰富的功能
    : 提供更细粒度的控制,适合需要复杂加密和签名操作的应用场景。
  • 相对复杂
    : 由于功能更广泛,
    python-jose
    的使用复杂度比 PyJWT 更高。

主要用法示例:

from jose importjwtfrom jose.exceptions importJWTError#创建一个JWT
payload ={"user_id": 123,"exp": datetime.datetime.utcnow() + datetime.timedelta(hours=1)
}
secret
= 'your-secret-key'token= jwt.encode(payload, secret, algorithm='HS256')#解码JWT try:
decoded
= jwt.decode(token, secret, algorithms=['HS256'])print(decoded)exceptJWTError as e:print(f"Token is invalid: {e}")

差异总结

  • 功能范围
    :
    PyJWT
    专注于 JWT,适合需要简单 JWT 处理的项目;
    python-jose
    则支持整个 JOSE 标准,适合需要更复杂加密和签名操作的项目。
  • 易用性
    :
    PyJWT
    API 简单,易于上手;
    python-jose
    更强大,但同时也更复杂。
  • 算法支持
    :
    python-jose
    支持的算法更广泛,尤其是在需要高级加密或签名场景时更具优势。
  • 使用场景
    : 如果你的项目只需要生成和验证 JWT,
    PyJWT
    是一个不错的选择;如果你需要全面的 JOSE 支持,包括 JWS、JWE 等,或者需要复杂的加密和签名,
    python-jose
    是更好的选择。

2、使用
python-jose
处理 JWT

在使用
python-jose
处理 JWT 时,捕获和处理异常是一个重要的环节。

1)
安装
python-jose

首先,确保你已经安装了
python-jose

pip install python-jose

2)
使用
python-jose
的 JWT 模块

以下是一个使用
python-jose
的 JWT 处理的示例,包括如何捕获异常:

from jose importjwt, JWTErrorfrom jose.exceptions importExpiredSignatureError#定义密钥和有效负载
secret = 'your-secret-key'payload={"user_id": 123,"exp": datetime.datetime.utcnow() + datetime.timedelta(hours=1)
}
#生成 JWT token = jwt.encode(payload, secret, algorithm='HS256')#解码 JWT 并处理可能的异常 try:
decoded
= jwt.decode(token, secret, algorithms=['HS256'])print(decoded)exceptExpiredSignatureError:print("Token has expired")exceptJWTError:print("Token is invalid")

在处理
python-jose
的 JWT 时,正确地捕获和处理异常是关键。确保你的环境和工具能够正确识别异常类型,将有助于你更好地管理 JWT 错误。

如果我们需要再JWT的playload里面承载更多的信息,可以再claim中声明键值即可,你可以使用
python-jose

FastAPI
来实现 JWT 令牌生成操作。如下所示代码。

使用
FastAPI

python-jose
生成 JWT 令牌:

from fastapi importFastAPI, Depends, HTTPException, status, Requestfrom fastapi.responses importJSONResponsefrom jose importJWTError, jwtfrom datetime importdatetime, timedelta

app
=FastAPI()#配置项,通常从配置文件中加载 JWT_SECRET_KEY = 'your_jwt_secret_key'JWT_ISSUER= 'your_issuer'JWT_AUDIENCE= 'your_audience'JWT_EXPIRED_DAYS= 7ALGORITHM= 'HS256' defgenerate_token(user_info: dict, role_type: str):#获取IP地址 ip = user_info.get('ip', '')#定义声明 claims ={'id': user_info['id'],'email': user_info['email'],'name': user_info['name'],'nickname': user_info.get('nickname', ''),'phone_number': user_info.get('mobile_phone', ''),'gender': user_info.get('gender', ''),'full_name': user_info.get('full_name', ''),'company_id': user_info.get('company_id', ''),'company_name': user_info.get('company_name', ''),'dept_id': user_info.get('dept_id', ''),'dept_name': user_info.get('dept_name', ''),'role_type': role_type,'ip': ip,'mac_addr': '', #无法获得Mac地址 'channel': ''}#定义token过期时间 expiration = datetime.utcnow() + timedelta(days=JWT_EXPIRED_DAYS)#创建JWT token to_encode ={**claims,'iss': JWT_ISSUER,'aud': JWT_AUDIENCE,'exp': expiration
}

token
= jwt.encode(to_encode, JWT_SECRET_KEY, algorithm=ALGORITHM)return token
@app.post('/token')
async
defget_token(request: Request):#模拟的用户信息 user_info ={'id': 123,'email': 'user@example.com','name': 'John Doe','nickname': 'Johnny','mobile_phone': '123-456-7890','gender': 'Male','full_name': 'Johnathan Doe','company_id': 'ABC123','company_name': 'ABC Corp','dept_id': 'Dept001','dept_name': 'IT','ip': request.client.host
}
role_type
= 'Admin'token=generate_token(user_info, role_type)

headers
={'access-token': token,'Authorization': f'Bearer {token}'}return JSONResponse(content={'token': token}, headers=headers)if __name__ == "__main__":importuvicorn
uvicorn.run(app, host
="127.0.0.1", port=8000)

解释

  1. FastAPI
    : 用于构建快速、现代的 Web API。
  2. Request
    : 从
    FastAPI
    中导入,用于获取客户端的 IP 地址。
  3. JWT 配置
    : 使用常量配置 JWT 密钥、发行者、受众、加密算法和过期时间。
  4. generate_token
    函数

    :
    • 构建 JWT 的
      claims
      ,包含用户信息和额外的字段,如 IP 地址、角色类型等。
    • 设置
      exp
      字段定义 JWT 的过期时间。
    • 使用
      jwt.encode
      创建并签名 JWT。
  5. get_token
    路由

    :
    • 模拟从请求中获取用户信息(包括 IP 地址)。
    • 调用
      generate_token
      生成 JWT。
    • 将 JWT 放入响应头中,返回给客户端。

3、在api中如何实现AllowAnonymous和验证授权

在 Python 的
FastAPI
框架中,你可以通过以下方式实现类似于 ASP.NET 中的
AllowAnonymous
和授权验证功能。

1)
实现 JWT 授权验证中间件

首先,你需要一个依赖项来检查请求中是否包含有效的 JWT 令牌。如果令牌无效或缺失,依赖项将拒绝请求。通过这种方式,只有标记为“允许匿名”的路由才会跳过验证。

2)
安装依赖

确保安装了
python-jose
用于处理 JWT,以及
fastapi

3)
创建授权依赖

你可以创建一个名为
get_current_user
的依赖项,用于验证 JWT 令牌并提取用户信息。如果没有提供或验证失败,抛出
HTTPException

from fastapi importDepends, HTTPException, statusfrom jose importJWTError, jwtfrom typing importOptional

JWT_SECRET_KEY
= 'your_jwt_secret_key'ALGORITHM= 'HS256' def get_current_user(token: str =Depends(oauth2_scheme)):try:
payload
= jwt.decode(token, JWT_SECRET_KEY, algorithms=[ALGORITHM])
user_id: str
= payload.get("id")if user_id isNone:raiseHTTPException(
status_code
=status.HTTP_401_UNAUTHORIZED,
detail
="Could not validate credentials",
headers
={"WWW-Authenticate": "Bearer"},
)
returnpayloadexceptJWTError:raiseHTTPException(
status_code
=status.HTTP_401_UNAUTHORIZED,
detail
="Could not validate credentials",
headers
={"WWW-Authenticate": "Bearer"},
)

get_current_user
:用于解析和验证 JWT。如果令牌无效或缺失,会抛出
HTTPException
,返回 401 状态码。

具体使用的时候,我们可能把用户信息缓存在Redis里面提高处理效率。

4)
实现
AllowAnonymous
功能


FastAPI
中,你可以通过
Depends
来实现条件授权。对于需要授权的路由,只需将
get_current_user
作为依赖传递给路由函数。而无需授权的路由,可以直接定义。

from fastapi importFastAPI, Depends, HTTPException, statusfrom fastapi.security importOAuth2PasswordBearer

app
=FastAPI()

oauth2_scheme
= OAuth2PasswordBearer(tokenUrl="token")#允许匿名访问的路由 @app.get("/public")defread_public_data():return {"message": "This is a public endpoint"}#需要授权的路由 @app.get("/protected")def read_protected_data(current_user: dict =Depends(get_current_user)):return {"message": f"Hello, {current_user['name']}"}#模拟登录生成 JWT 令牌的路由 @app.post("/token")deflogin():#这里省略了身份验证过程,只是直接生成一个 JWT 令牌 token = jwt.encode({"id": 1, "name": "John Doe"}, JWT_SECRET_KEY, algorithm=ALGORITHM)return {"access_token": token, "token_type": "bearer"}
  • 公共路由 (
    /public
    )

    :可以直接访问,不需要任何身份验证。
  • 受保护路由 (
    /protected
    )

    :必须提供有效的 JWT 令牌才能访问。
  • 登录路由 (
    /token
    )

    :生成一个 JWT 令牌,可以用于受保护的路

Depends(get_current_user)
:在需要保护的路由中,通过依赖项注入
get_current_user
,确保只有通过身份验证的用户才能访问。


FastAPI
中,建议通过依赖项注入(
Depends
)来获取当前用户的信息,而不是直接访问
request.user
。这种方式更加灵活并且与
FastAPI
的设计哲学更一致。

在具体项目中,我们为了方便,往往通过中间件的方式进行定义和处理授权的过程。

通过authentiate函数处理验证用户令牌的有效性。

我们一般再main.py入口中加入中间件的处理即可。

    #JWT auth, required
app.add_middleware(
AuthenticationMiddleware,
backend
=JwtAuthMiddleware(),
on_error
=JwtAuthMiddleware.auth_exception_handler,
)

4、一些错误处理

当你在解码 JWT 时遇到 "Invalid audience" 错误,通常意味着在生成或解码 JWT 时,
aud
(audience) 声明没有正确设置或验证。以下是解决这个问题的步骤和说明:

1)
理解
aud
(Audience) 声明

  • aud
    是 JWT 中的一个可选声明,通常用于指定 JWT 的接收者(受众)。在解码 JWT 时,
    jwt.decode
    会检查这个声明是否与预期的值匹配。

设置和检查
aud
声明,

生成 Token 时设置
aud

如果你希望 JWT 包含
aud
声明,可以在生成 token 时传递它。例如:

to_encode ={"sub": "user_id","aud": "your_audience",  #设置aud声明
    "exp": datetime.utcnow() + timedelta(minutes=30)
}

token
= jwt.encode(to_encode, settings.TOKEN_SECRET_KEY, algorithm=settings.TOKEN_ALGORITHM)

解码 Token 时验证
aud

在解码 JWT 时,指定
audience
参数以匹配生成时的
aud

try:
payload
=jwt.decode(
token,
settings.TOKEN_SECRET_KEY,
algorithms
=[settings.TOKEN_ALGORITHM],
audience
="your_audience" #验证aud声明 )print(f"Decoded payload: {payload}")exceptJWTError as e:print(f"Token decode failed: {e}")

2)Token decode failed: Subject must be a string.

"Token decode failed: Subject must be a string" 错误通常是由于
sub
(subject) 声明的值不是字符串引起的。
sub
是 JWT 中常用的一个声明,用来标识 token 的主体,比如用户 ID 或用户名。JWT 标准要求
sub
的值必须是字符串。

检查
sub
声明的值

首先,确保在生成 token 时,
sub
声明的值是一个字符串:

to_encode ={"sub": str(user_id),  #确保 user_id 是字符串
    "aud": "your_audience",  #其他字段
    "exp": datetime.utcnow() + timedelta(minutes=30)
}

token
= jwt.encode(to_encode, settings.TOKEN_SECRET_KEY, algorithm=settings.TOKEN_ALGORITHM)

如果
sub
的值是一个非字符串类型(如整数或其他对象),请将其转换为字符串:

user_id = 123  #假设 user_id 是一个整数
to_encode ={"sub": str(user_id),  #将 user_id 转换为字符串
    "aud": "your_audience","exp": datetime.utcnow() + timedelta(minutes=30)
}

token
= jwt.encode(to_encode, settings.TOKEN_SECRET_KEY, algorithm=settings.TOKEN_ALGORITHM)

3)schema如何移除敏感字段

在处理数据模型和模式(schema)时,特别是当涉及到敏感信息(如密码、身份信息等)时,有时需要从输出或序列化结果中移除这些敏感字段。根据你使用的库或框架,处理敏感字段的方法会有所不同。以下是一些常见的方法来移除敏感字段:

使用 Pydantic 的
exclude
参数

如果你在使用 Pydantic(例如在 FastAPI 中),你可以使用模型的
dict
方法的
exclude
参数来排除敏感字段。

from pydantic importBaseModelclassUser(BaseModel):
username: str
email: str
password: str
#敏感字段 user= User(username="user1", email="user1@example.com", password="secret")#创建一个不包含敏感字段的字典 user_dict = user.dict(exclude={"password"})print(user_dict)

使用 SQLAlchemy 的
__mapper_args__

如果你使用 SQLAlchemy 并且希望从序列化结果中排除敏感字段,可以使用模型的
__mapper_args__
进行配置。例如:

from sqlalchemy importColumn, Stringfrom sqlalchemy.ext.declarative importdeclarative_base

Base
=declarative_base()classUser(Base):__tablename__ = 'users'id= Column(String, primary_key=True)
username
=Column(String)
email
=Column(String)
password
= Column(String) #敏感字段 defto_dict(self):#移除敏感字段 return {c.name: getattr(self, c.name) for c in self.__table__.columns if c.name != 'password'}

user
= User(id="1", username="user1", email="user1@example.com", password="secret")print(user.to_dict())

自定义序列化方法

如果你使用自定义模型或类,并且没有使用特定的库,你可以实现自定义序列化方法来排除敏感字段。

classUser:def __init__(self, username, email, password):
self.username
=username
self.email
=email
self.password
= password #敏感字段 defto_dict(self):#移除敏感字段 return{"username": self.username,"email": self.email
}

user
= User(username="user1", email="user1@example.com", password="secret")print(user.to_dict())