2023年3月

IO 多路复用

一、什么是内核空间和用户空间

1.1 内核空间和用户空间

操作系统的核心是内核(kernel),它独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。为了保证内核的安全,现在的操作系统一般都强制用户进程不能直接操作内核。

由于我们用户所有的应用都是运行在操作系统之上的,所以一旦操作系统不能稳定运行,那就完了。因此为了保证操作系统的稳定性,Linux 区分了内核空间和用户空间。

可以这样理解,内核空间运行操作系统程序和驱动程序,而用户空间则运行应用程序。Linux 以这种方式隔离了操作系统程序和应用程序,避免了应用程序影响到操作系统自身的稳定性。

1.2 内核态和用户态

当进程运行在内核空间时就处于内核态,而进程运行在用户空间时则处于用户态。

对于以前的 DOS 操作系统来说,是没有内核空间、用户空间以及内核态、用户态这些概念的。可以认为所有的代码都是运行在内核态的,因而用户编写的应用程序代码可以很容易的让操作系统崩溃掉。

1.3 如何从用户空间进入内核空间

其实所有的系统资源管理都是在内核空间中完成的,比如读写磁盘文件、分配回收内存、从网络接口读写数据等等。我们的应用程序是无法直接进行这样的操作的。但是我们可以通过内核提供的接口来完成这样的任务。

比如应用程序要读取磁盘上的一个文件,它可以向内核发起一个「系统调用」,以此来告诉内核「我要读取磁盘上的某某文件」。

系统调用就是操作系统向用户提供服务的接口。

二、什么是 IO

2.1 IO 基本概念

IO 是输入(Input)和输出(Output)的首字母缩写,直观意思是计算机输入和输出,它描述的是计算机的数据流动的过程,因此 IO 的第一大特征是有数据的流动。另外,对于一次 IO 操作,它究竟是输入还是输出,是针对不同的主体而言的,不同的主体有不同的描述。

例如,甲乙两人交谈,甲将大脑中的想法通过声带震动,继而通过声波传入乙的耳朵,乙通过耳膜的震动再由神经将信号解析到大脑,就这个数据流动的过程,对甲而言是输出,对乙而言则是输入。

因此,理解 IO 一定要弄清楚所要研究的
本体
。下面,我们从三个层面来理解IO。

2.2 从直观层面去理解 IO

此时,IO 是计算机和外设之间的数据流动过程,本体是一个有使用意义的可运行的电脑,它是计算机运行的完全必要部分。姑且认为这个完全必要部分是台式电脑的主机,里面有 CPU、内存、主板、电源等设备,因为有了这些,一台有使用意义的电脑即可运行。有了主机,并不能方便的为人所服务,因此得有外设。

外设是电脑的外围设备,如显示器、键盘、鼠标等,它们是完成人机交互的辅助工具。

外设包含两种重要设备(但不限于此):输入设备和输出设备。

  • 像鼠标、键盘属于输入设备,将人的指令转成「鼠键行为」这种数据传给主机
  • 显示器是输出设备,主机通过运算,把「运算结果」这种数据传给显示器

2.3 从计算机架构的角度去理解 IO

从计算机架构上来讲,任何涉及到
计算机核心(CPU 和内存)与其他设备间的数据流动的过程
就是 IO。本体就是计算机核心(CPU 和 内存)。

例如从硬盘上读取数据到内存,是一次输入;将内存中的数据写入到硬盘就产生了输出。在计算机的世界里,这就是 IO 的本质。

2.4 从编程的角度去理解 IO

此时,IO 的主体是其应用程序的运行态,即进程。特别强调的是我们的应用程序其实并不存在实质的 IO 过程,真正的 IO 过程是操作系统的事情,这里把应用程序的 IO 操作分为两种动作:
IO 调用和 IO 执行

  • IO 调用是由进程发起
  • IO 执行是操作系统的工作

因此,更准确些来说,此时所说的 IO 是应用程序对操作系统 IO 功能的一次触发,即 IO 调用。IO 调用的目的是:

  1. 将进程的内部数据迁移到外部,即输出
  2. 或将外部数据迁移到进程内部,即输入

这里,外部数据指非进程空间数据,如从文件中读取的数据。

以一个进程的输入类型的 IO 调用为例,它将完成或引起如下工作内容:

  1. 进程向操作系统请求外部数据
  2. 操作系统将外部数据加载到内核缓冲区
  3. 操作系统将数据从内核缓冲区拷贝到进程缓冲区
  4. 进程读取数据继续后面的工作

2.5 缓存 IO

缓存 IO 又被称作标准 IO,大多数文件系统的默认 IO 操作都是缓存 IO。

在 Linux 的缓存 IO 机制中:

  • 读操作:数据先从磁盘复制到内核空间的缓冲区,然后从内核空间缓冲区复制到用户空间
  • 写操作:数据先从用户空间复制到内核空间缓冲区,然后从内核空间缓冲区复制到磁盘

IO多路复用.drawio

2.6 IO 设备

从一个设备中读数据到内存或者从内存写数据到这个设备,而这个设备就叫 IO 设备:

IO多路复用.drawio

根据 IO 设备不同,IO 分为「磁盘 IO」和「网络 IO」:

  • 磁盘 IO:对存储介质的读写,如硬盘
  • 网络 IO:在网络通信过程中数据的传输,即对网卡的读写

IO多路复用.drawio

2.7 阻塞和非阻塞 IO

阻塞和非阻塞强调的是进程对于操作系统 IO 是否处于就绪状态的处理方式。上面已经说过,应用程序的 IO 实际是分为两个步骤:IO 调用和 IO 执行。

  • IO 调用是由进程发起的
  • IO 执行则是操作系统的工作

操作系统的 IO 情况决定了进程 IO 调用是否能够得到立即响应。如进程发起了读取数据的 IO 调用,操作系统需要将外部数据拷贝到进程缓冲区,在有数据拷贝到进程缓冲区前,进程缓冲区处于不可读状态,我们称之为操作系统 IO 未就绪。

进程的 IO 调用是否能得到立即执行是需要操作系统 IO 处于就绪状态的,对于读取数据的操作:

  • 如果操作系统 IO 处于未就绪状态,当前进程或线程如果一直等待直到其就绪,该种IO方式为阻塞IO
  • 如果进程或线程并不一直等待其就绪,而是可以做其他事情,这种方式为非阻塞 IO

2.7.1 阻塞 IO

我们以 Socket 为例,在 Linux 中,默认情况下所有 Socket 都是阻塞模式的。当用户进程或线程调用系统函数
read()
,内核开始准备数据(从网络接收数据),内核准备数据完成后,数据从内核拷贝到用户空间的应用程序缓冲区,数据拷贝完成后,请求才返回。从发起 Read 请求到最终完成内核到应用程序的拷贝,整个过程都是阻塞的:

image-20230318231811322

如果当前进程或线程一直等待直到其就绪,该种 IO 方式就称为阻塞 IO

2.7.2 非阻塞 IO

如果用户进程或线程线程在发起 Read 请求后立即返回,不用等待内核准备数据的过程,而是可以做其他事情,这种方式就称为非阻塞 IO:

image-20230318231840614

对于非阻塞 IO,我们编程时需要经常去轮询就绪状态。即如果 Read 请求没读取到数据,用户进程或线程会不断轮询发起 Read 请求,直到数据到达(内核准备好数据)后才停止轮询

2.8 同步和异步 IO

同步和异步描述的是针对当前执行进程或线程而言,发起 IO 调用后,当前进程或线程是否挂起等待操作系统的 IO 执行完成。

我们说一个 IO 执行是同步执行的,意思是程序发起 IO 调用后,当前线程或进程需要等待操作系统完成 IO 执行工作并告知进程或线程已经完成,进程或线程才能继续往下执行其他既定指令。

如果说一个 IO 执行是异步的,意思是该动作是由当前进程或线程请求发起,且当前进程或线程不必等待操作系统 IO 的执行完毕,可直接继续往下执行其他既定指令。操作系统完成 IO 后,当前进程或线程会得到操作系统的
通知

以一个读取数据的 IO 操作而言,在操作系统将外部数据写入进程缓冲区这个期间,进程或线程挂起等待操作系统 IO 执行完成的话,这种 IO 执行策略就为同步,如果进程或线程并不挂起而是继续工作,这种 IO 执行策略便为异步。

同步和异步这个概念是针对于程序进程,而阻塞与非阻塞是针对系统处理 IO 操作的过程。

三、多路复用

3.1 最基本的 Socket 模型

服务端:

  1. 首先调用
    socket()
    函数,创建网络协议为 IPv4、以及传输协议为 TCP 的 Socket
  2. 接着调用
    bind()
    函数,给这个 Socket 绑定一个 IP 地址和端口
  3. 绑定完 IP 地址和端口后,就可以调用
    listen()
    函数进行监听
  4. 服务端进入了监听状态后,通过调用
    accept()
    函数,来从内核获取客户端的连接,如果没有客户端连接,则会阻塞等待客户端连接的到来

客户端:

  1. 同样,首先调用
    socket()
    函数,创建网络协议为 IPv4,以及传输协议为 TCP 的 Socket
  2. 接着调用
    connect()
    函数发起连接,然后万众期待的 TCP 三次握手就开始了

连接建立后,客户端和服务端就开始相互传输数据了,双方都可以通过
read()

write()
函数来读写数据。至此, TCP 协议的 Socket 程序的调用过程就结束了,整个过程如下图:

IO多路复用.drawio

TCP Socket 调用流程是最简单、最基本的,它基本只能一对一通信,因为使用的是同步阻塞的方式,当服务端在还没处理完一个客户端的网络 I/O 时,或者读写操作发生阻塞时,其他客户端是无法与服务端连接的。

可如果我们服务器只能服务一个客户,那这样就太浪费资源了,于是我们要改进这个网络 I/O 模型,以支持更多的客户端。

3.2 多进程模型

基于最原始的阻塞网络 I/O, 如果服务器要支持多个客户端,其中比较传统的方式,就是使用多进程模型,也就是为每个客户端分配一个进程来处理请求。

服务器的主进程负责监听客户的连接,一旦与客户端连接完成,
accept()
函数就会返回一个「已连接 Socket」,这时就通过
fork()
函数创建一个子进程,实际上就把父进程所有相关的东西都复制一份,包括文件描述符、内存地址空间、程序计数器、执行的代码等。这两个进程刚复制完的时候,几乎一摸一样,不过,会根据返回值来区分是父进程还是子进程,如果返回值是 0,则是子进程;如果返回值是其他的整数,就是父进程。

正因为子进程会复制父进程的文件描述符,于是就可以直接使用「已连接 Socket 」和客户端通信了,可以发现:

  • 子进程不需要关心「监听 Socket」,只需要关心「已连接 Socket」
  • 父进程则相反,将客户服务交给子进程来处理,因此父进程不需要关心「已连接 Socket」,只需要关心「监听 Socket」

下面这张图描述了从连接请求到连接建立,父进程创建子进程为客户服务的过程:

img

这种用多个进程来应付多个客户端的方式,在应对 100 个客户端还是可行的,但是当客户端数量高达一万时,肯定扛不住的,因为每产生一个进程,必会占据一定的系统资源,而且进程间上下文切换的「包袱」是很重的,性能会大打折扣。

3.3 多线程模型

既然进程间上下文切换的「包袱」很重,那我们就搞个比较轻量级的模型来应对多用户的请求——多线程模型。

线程是运行在进程中的一个「逻辑流」,单进程中可以运行多个线程,同进程里的线程可以共享进程的部分资源的,比如文件描述符列表、进程空间、代码、全局数据、堆、共享库等,这些共享资源在上下文切换时是不需要切换的,而只需要切换线程的私有数据、寄存器等不共享的数据,因此同一个进程下的线程上下文切换的开销要比进程小得多。

当服务器与客户端 TCP Socket 完成连接后,通过
pthread_create()
函数创建线程,然后将「已连接 Socket」的文件描述符传递给线程函数,接着在线程里和客户端进行通信,从而达到并发处理的目的。

如果每来一个连接就创建一个线程,线程运行完后,操作系统还得销毁线程,虽说线程切换的上写文开销不大,但是如果频繁创建和销毁线程,系统开销也是不小的。那么,我们可以使用线程池的方式来避免线程的频繁创建和销毁,所谓的线程池,就是提前创建若干个线程,这样当由新连接建立时,将这个「已连接的 Socket」放入到一个工作队列中,然后线程池里的线程负责从工作队列中取出「已连接 Socket」进程处理。

v2-d67eb5cc4b947eed8b19846d4ed85cb5_720w

3.4 IO 多路复用

上面基于多进程或者多线程的模型,其实还是有问题的。新到来一个 TCP 连接,就需要分配一个进程或者线程,那么如果要达到 C10K,意味着要一台机器维护 1 万个连接,相当于要维护 1 万个进程 / 线程,操作系统就算死扛也是扛不住的。

既然为每个请求分配一个进程 / 线程的方式不合适,那有没有可能只使用一个进程来维护多个 Socket 呢?答案是有的,那就是 I/O 多路复用技术。

img

一个进程虽然任一时刻只能处理一个请求,但是处理每个请求的事件时,耗时控制在 1 毫秒以内,这样 1 秒内就可以处理上千个请求,把时间拉长来看,
多个请求复用了一个进程
,这就是多路复用。这种思想很类似一个 CPU 并发多个进程,所以也叫做时分多路复用。

我们所熟知的 select、poll、epoll,就是内核提供给用户的多路复用系统调用的接口,当用户调用这些接口时,进程就可以通过
一个系统调用函数
从内核中
获取多个事件
,从而实现了 IO 多路复用。

下面对这三个多路复用接口做了简单介绍。

3.4.1 select

select 实现多路复用的方式是,将已连接的 Socket 都放到一个文件描述符集合,然后调用 select 函数将文件描述符集合拷贝到内核中,让内核来检查是否有网络事件产生。检查的方式很粗暴,就是通过遍历文件描述符集合的方式,当检查到有事件产生后,将此 Socket 标记为可读或可写, 接着再把整个文件描述符集合拷贝回用户态里,然后用户态还需要再通过遍历的方法找到可读或可写的 Socket,然后再对其处理。

所以,对于 select 这种方式,需要进行两次「遍历」文件描述符集合的操作,一次是在内核态里,一个次是在用户态里 ,而且还会发生两次「拷贝」文件描述符集合,先从用户空间传入到内核空间,由内核修改后,再传出到用户空间中。

且 select 使用固定长度的 BitsMap,表示文件描述符集合,而且所支持的文件描述符的个数是有限制的,在 Linux 系统中,由内核中的 FD_SETSIZE 限制, 默认最大值为 1024,只能监听 0~1023 的文件描述符。

3.4.2 poll

poll 不再用 BitsMap 来存储所关注的文件描述符,取而代之用动态数组,以链表形式来组织,突破了 select 的文件描述符个数限制,当然还会受到系统文件描述符限制。

但是 poll 和 select 并没有太大的本质区别,都是使用「线性结构」存储进程关注的 Socket 集合,因此都需要遍历文件描述符集合来找到可读或可写的 Socket,时间复杂度为 O(n),而且也需要在用户态与内核态之间拷贝文件描述符集合,这种方式随着并发数上来,性能的损耗会呈指数级增长。

3.4.3 epoll

epoll 通过两个方面,很好解决了 select/poll 的问题。

  1. epoll 在内核里使用
    红黑树
    来跟踪进程所有待检测的文件描述字,把需要监控的 socket 通过
    epoll_ctl()
    函数加入内核中的红黑树里,红黑树是个高效的数据结构,增删查一般时间复杂度是 O(logn),通过对这棵黑红树进行操作,这样就不需要像 select/poll 每次操作时都传入整个 socket 集合,只需要传入一个待检测的 socket,减少了内核和用户空间大量的数据拷贝和内存分配。
  2. epoll 使用事件驱动的机制,内核里维护了一个链表来记录就绪事件,当某个 socket 有事件发生时,通过回调函数内核会将其加入到这个就绪事件列表中,当用户调用
    epoll_wait()
    函数时,只会返回有事件发生的文件描述符的个数,不需要像 select/poll 那样轮询扫描整个 socket 集合,大大提高了检测的效率。

从下图你可以看到 epoll 相关的接口作用:

img

epoll 的方式使得即使监听的 Socket 数量变得很多的时候,效率也不会大幅度降低,而监听的文件描述符的上限就为系统定义的进程打开的最大文件描述符个数。

声明

参考资料

本篇笔记目录索引如下:

  1. Django 连接mysql,执行数据库表结构迁移步骤介绍
  2. 操作数据库,对数据进行简单操作

接下来几篇笔记都会介绍和数据库相关,包括数据库的连接、操作(包括增删改查)、对应的字段类型、model 里Meta 相关参数和 QueryAPI 的详解等。

这一篇先介绍数据库的连接和简单的增删改查操作。

首先介绍一些 Django 的操作表的逻辑,Django 提供了一套非常方便的 orm 方法,可用于Django 能直接对表和数据进行增删改查,对应的数据库里每一张表都有一个对应的 model 的class,class 下每一个字段都对应 mysql 中表的字段,我们在 Django 中定义好这些内容,然后通过命令迁移到 数据库中来完成修改。

1、Django 连接mysql

前一篇笔记我们创建了 一个 名为 polls 的application,接下来我们要在 polls 下面建立和数据库的连接。

连接上 mysql 分为以下几步:

  • 安装依赖
  • 创建数据库
  • 修改settings.py 填写数据库信息
  • 迁移默认表
  • 定义 model 信息
  • 生成 migrations 文件
  • 执行表的迁移

安装依赖

Django 和 mysql 连接需要安装一个 driver,我们这里选择 mysqlclient,以下是安装命令:

pip3 install mysqlclient -i https://mirrors.aliyun.com/pypi/simple/

还是通过 -i 指定源,加速安装。

创建数据库

可以自己在服务器或者本地安装一个可以运行的 mysql,我这里安装的是 mysql 5.7 版本。

创建数据库的时候记得指定编码为 utf-8:

create database func_test default character set='utf8'

在这里,数据库名称为 func_test,库编码为 utf-8。

修改settings.py 填写数据库信息

在 hunter/settings.py 文件下,有一个 DATABASES 的变量,将其修改为以下内容:

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'func_test',
        "USER": "user",
        "PASSWORD": "password",
        "HOST": "xx.xx.xx.xx",
        "PORT": 3306,
    }
}

其中,default 表示系统默认连接的数据库,一个 Django 系统可以连接多个数据库,可以通过这个地方定义变量名称来指定。
如果需要连接多个数据库,后续的操作可以再介绍。在使用的时候通过 using() 来区分
ENGINE:在这里值为 django.db.backends.mysql ,表示连接的数据库类型是 mysql
NAME:表示连接的数据库名称,在这里我们指定的是 func_test
USER:连接数据库使用的用户名
PASSWORD:连接数据库用户名对应的密码
HOST:你的数据库的地址,本地的话是 localhost 或者 127.0.0.1,服务器上填写相应的 ip 即可
PORT:端口,mysql 默认是 3306

以上的那些变量,记得更换成本地的变量。

好了,关于数据库的基本连接配置就都设置完成了。

迁移默认表

Django 系统里是有一些默认的配置表的,
比如用户表,如果使用Django的默认用户系统的话可以用上
django_session表,用于记录用户的登录的 session相关记录
django_migrations表,记录每一次表及字段的创建和修改操作等等

这些表,在我们执行以下操作命令的时候,会自动写入数据库:

python3 manage.py migrate

定义 model 信息

在上一篇笔记里我们创建了一个名为 polls 的 application,里面有一个 models.py 的文件,然后我们编辑该文件,内容如下:

from django.db import models

class Question(models.Model):
    question_text = models.CharField(max_length=200)
    pub_date = models.DateTimeField('date published')


class Choice(models.Model):
    question = models.ForeignKey(Question, on_delete=models.CASCADE)
    choice_text = models.CharField(max_length=200)
    votes = models.IntegerField(default=0)

在上述文件里,每一个class 代表着一张表,每一行都代表着 表里的一个字段,字段的类型在上述的文件里对应数据库的类型为:
CharField:varchar
DateTimeField:datetime
IntegerField:int
ForeignKey:外键
这个类型我们后续会再介绍,这里只做一个简单的介绍。

然后我们还需要将我们这个 model 注册到我们的 settings.py 里的 INSTALLED_APPS:

INSTALLED_APPS = [
    'polls.apps.PollsConfig',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

生成 migrations 文件

我们定义好 model 文件之后,在生成表结构之前,我们还需要生成一个 记录表结构变化的文件,也就是我们的migrations 文件,每一次修改都可以生成一个新的 migration 文件,然后写入数据库,该文件存放在 每一个 application 的 migrations 文件夹下面。
生成 migrations 文件命令:

python3 manage.py makemigrations polls

然后我们可以看到在 polls/migrations/ 文件夹下多了一个 0001_initial.py

执行表的迁移

然后执行下面的命令,Django会根据系统里上一次在 django_migrations 表里记录的上一次执行的地方,检测我们系统里新增的 migrations 文件,自动将数据结构的更改适配到数据库里:

python3 manage.py migrate polls

如果对 model.py 再进行一次更改,然后执行 makemigrations 命令,migrations 文件夹下会自动新增新的迁移命令。
每一个新增的前面都会有序号标识,比如我们最开始的是 0001,之后每一次都会按照序号往后新增。
有兴趣的可以看看 migrations 里的文件内容,那是 python 语言里对应 SQL 的更改内容,这里不做展开,有兴趣的可以看看。

如果我们想在每一次执行 migrate 前,查看会对数据库进行怎么样的更改,可以根据 我们上面说的 序号,来指定 migration 文件查看:

python3 manage.py sqlmigrate polls 0001

python3 manage.py sqlmigrate 是固定语法,
polls 是对应的 application 的名称,
0001 是每一次migration的前面的序号,这样就可以指定到 application 下的每一次更改。

比如上面那条命令,输出的结果如下:
在这里插入图片描述
会将转义执行到 数据库的语句打印出来。

2、操作数据库,对数据进行简单操作

现在我们根据上述的操作,创建了两张表和对应的 model,接下来介绍怎么对数据进行操作:

比如我们要创建一个 Question 数据,使用Django的语法来创建,可以通过 以下命令进入 Django 的交互界面:

python3 manage.py shell

然后挨个输入:

from polls.models import Question
q = Question(question_text="quesiton_text_1", pub_date="2022-01-01 00:00:00")
q.save()

然后查看数据库,可以看到我们通过 save() 操作已经创建了一条数据了。

获取单个数据呢:

q = Question.objects.get(id=1)
print(q.question_text)

以上就是我们这一篇笔记的全部内容,下一篇笔记将详细介绍如果通过 Django 的 model 对数据进行增删改查的操作。

本文首发于本人微信公众号:Django笔记。

原文链接:
Django笔记二之连接数据库、执行migrate数据结构更改操作

如果想获取更多相关文章,可扫码关注阅读:
在这里插入图片描述

统一鉴权认证是一个基础服务。它几乎在所有企业内部都需要,企业内部只要有两个以上系统存在,就有必要实现一套统一的授权系统,否则用户使用非常地麻烦,需要在不同系统之间来回登录切换。特别是在微服务大行其道的今天,这个统一授权认证服务更是一个基础和关键入口。实现的方案有很多种,但都大同小异。

本文主要介绍授权认证服务架构方案设计及实现,这个实践也是本人在企业内部成功实现的经验总结。从旧有系统Cas认证方式到升级Oauth2.0认证,怎么保持两套认证体(Cas和Oauth2)互认整个过程遇到不少问题,以及针对问题解决。将从如下几个方面进行描述。
文章比较长,各位看官花点耐心呀!

目录结构

1、概念介绍

1.1、什么是认证

1.2、什么是授权

1.3、什么是鉴权

1.4、什么是权限控制

1.5、三者关系

2、Http认证方案

2.1 认证流程图

2.2 认证步骤解析

2.3 优缺点对比

2.4 使用场景

3、Session-Cookie认证方案

3.1 认证流程图

3.2 认证步骤解析

3.3 优缺点对比

3.4 使用场景

3.5 代码实现

4、Token认证方案

4.1 Token认证原理

4.2 刷新Token

4.3 Token与Session-Cookie区别

5、OAuth2认证方案

5.1 OAuth2定义

5.2 OAuth2角色

5.3 OAuth2认证流程

6、JWT认证方案

6.1 JWT定义

6.2 JWT组成

6.3 JWT使用

6.4 JWT认证流程

6.5 JWT优缺点

7、集团统一授权认证架构方案

5.1 方案设计

5.2 关键问题

5.3 企业内部实践

5.4 关键代码

8、总结

1、概念介绍

1.1、什么是认证

认证(Identification)是指根据声明者所特有的识别信息,确认声明者的身份。

白话文的意思就是:你需要用身份证证明你自己是你自己。

比如我们常见的认证技术:

  • 身份证
  • 用户名和密码
  • 用户手机:手机短信、手机二维码扫描、手势密码
  • 用户的电子邮箱
  • 用户的生物学特征:指纹、语音、眼睛虹膜
  • 用户的大数据识别
  • 等等

1.2、
什么是授权

授权(Authorization):在信息安全领域是指资源所有者委派执行者,赋予执行者指定范围的资源操作权限,以便对资源的相关操作。

在现实生活领域例如:
银行卡(由银行派发)、门禁卡(由物业管理处派发)、钥匙(由房东派发),这些都是现实生活中授权的实现方式。

在互联网领域例如:
web 服务器的 session 机制、web 浏览器的 cookie 机制、颁发授权令牌(token)等都是一个授权的机制。

1.3、什么是鉴权

鉴权(Authentication)在信息安全领域是指
对于一个声明者所声明的身份权利,对其所声明的真实性进行鉴别确认的过程

若从授权出发,则会更加容易理解鉴权。授权和鉴权是两个上下游相匹配的关系,
先授权,后鉴权

在现实生活领域:
门禁卡需要通过门禁卡识别器,银行卡需要通过银行卡识别器;

在互联网领域:
校验 session/cookie/token 的合法性和有效性

鉴权是一个承上启下的一个环节,上游它接受授权的输出,校验其真实性后,然后获取权限(permission),这个将会为下一步的权限控制做好准备。

1.4、什么是权限控制

权限控制(Access/Permission Control)将可执行的操作定义为权限列表,然后判断操作是否允许/禁止

对于权限控制,可以分为两部分进行理解:一个是权限,另一个是控制。权限是抽象的逻辑概念,而控制是具体的实现方式。

在现实生活领域中:
以门禁卡的权限实现为例,一个门禁卡,拥有开公司所有的门的权限;一个门禁卡,拥有管理员角色的权限,因而可以开公司所有的门。

在互联网领域:
通过 web 后端服务,来控制接口访问,允许或拒绝访问请求。

1.5 认证、授权、鉴权和权限控制的关系

看到这里,我们应该明白了认证、授权、鉴权和权限控制这四个环节是一个前后依次发生、上下游的关系,如下图所示:

需要说明的是,这四个环节在有些时候会同时发生。例如在下面的几个场景:

  • 使用门禁卡开门:
    认证、授权、鉴权、权限控制四个环节一气呵成,在瞬间同时发生
  • 用户的网站登录:
    用户在使用用户名和密码进行登录时,认证和授权两个环节一同完成,而鉴权和权限控制则发生在后续的请求访问中,比如在选购物品或支付时。

2、Http认证方案

在 HTTP 中,基本认证方案(Basic Access Authentication)是允许客户端(通常指的就是网页浏览器)在请求时,通过用户提供用户名和密码的方式,实现对用户身份的验证。

因为几乎所有的线上网站都不会走该认证方案,所以该方案大家了解即可

2.1
认证流程图

2.2
认证步骤解析

(1)客户端(如浏览器):
向服务器请求一个受限的列表数据或资源,例如字段如下

GET /list/ HTTP/1.1
Host: www.baidu.com
Authorization: Basic aHR0cHdhdGNoOmY=

(2)服务器
:客户端你好,这个资源在安全区 baidu.com里,是受限资源,需要基本认证;

并且向客户端返回 401 状态码(Unauthorized 未被授权的)以及附带提供了一个认证域www-Authenticate: Basic realm=”baidu.com”要求进行身份验证;

其中Basic就是验证的模式,而realm="baidu.com"说明客户端需要输入这个安全域的用户名和密码,而不是其他域的

HTTP/1.1 401 Unauthorized
www-Authenticate: Basic realm= "baidu.com"

(3)客户端:
服务器,我已经携带了用户名和密码给你了,你看一下;(注:如客户端是浏览器,那么此时会自动弹出一个弹窗,让用户输入用户名和密码);

输入完用户名和密码后,则客户端将用户名及密码以 Base64 加密方式发送给服务器

传送的格式如下 (其中 Basic 内容为:
用户名:密码 的 ase64 形式
):

GET /list/ HTTP/1.1
Authorization: Basic Ksid2FuZzp3YW5n==

(4)服务器:
客户端你好,我已经校验了Authorization字段你的用户名和密码,是正确的,这是你要的资源。

成功:HTTP/1.1 200 OK
失败:HTTP/1.1 403 Forbidden

2.3 优缺点对比

2.3.1
优点

实现简单,基本所有流行的浏览器都支持

2.3.2
缺点

(1)不安全:

  • 由于是基于 HTTP 传输,所以它在网络上几乎是裸奔的,虽然它使用了 Base64 来编码,但这个编码很容易就可以解码出来。
  • 即使认证内容无法被解码为原始的用户名和密码也是不安全的,恶意用户可以再获取了认证内容后使用其不断的享服务器发起请求,这就是所谓的重放攻击

(2)无法主动注销:

由于 HTTP 协议没有提供机制清除浏览器中的 Basic 认证信息,除非标签页或浏览器关闭、或用户清除历史记录。

2.4 使用场景

内部网络,或者对安全要求不是很高的网络。

3、Session-Cookie认证方案

Session-Cookie认证是利用服务端的
Session(会话
)和
浏览器(客户端)
的 Cookie 来实现的前后端通信认证模式。

在理解这句话之前我们先简单了解下什么是 Cookie以及什么是 Session?

3.1
什么是 Cookie

众所周知,HTTP 是无状态的协议(对于事务处理没有记忆能力,每次客户端和服务端会话完成时,服务端不会保存任何会话信息);

所以为了让服务器区分不同的客户端,就必须主动的去维护一个状态,这个状态用于告知服务端前后两个请求是否来自同一浏览器。而这个状态可以通过Cookie去实现。

特点:

  • Cookie 存储在客户端,可随意篡改,不安全
  • 有大小限制,最大为 4kb
  • 有数量限制,一般一个浏览器对于一个网站只能存不超过 20 个 Cookie,浏览器一般只允许存放 300个 Cookie
  • Android 和 IOS 对 Cookie 支持性不好
  • Cookie 是不可跨域的,但是一级域名和二级域名是允许共享使用的(靠的是 domain)

3.2
什么是 Session

Session 的抽象概念是会话,是无状态协议通信过程中,为了实现中断/继续操作,将用户和服务器之间的交互进行的一种抽象;

具体来说,是服务器生成的一种 Session 结构,可以通过多种方式保存,如内存、数据库、文件等,大型网站一般有专门的 Session 服务器集群来保存用户会话;

原理流程:

  1. 客户端:
    用户向服务器首次发送请求;
  2. 服务器:
    接收到数据并自动为该用户创建特定的 Session / Session ID,来标识用户并跟踪用户当前的会话过程;
  3. 客户端:
    浏览器收到响应获取会话信息,并且会在下一次请求时带上 Session / Session ID;
  4. 服务器:
    服务器提取后会与本地保存的 Session ID进行对比找到该特定用户的会话,进而获取会话状态;
  5. 至此客户端与服务器的通信变成有状态的通信;

特点:

  • Session 保存在服务器上;
  • 通过服务器自带的加密协议进行;

与 Cookie 的差异:

  • 安全性:
    Cookie 由于保存在客户端,可随意篡改,Session 则不同存储在服务器端,无法伪造,所以 Session 的安全性更高;
  • 存取值的类型不同:
    Cookie 只支持字符串数据,Session 可以存任意数据类型;
  • 有效期不同:
    Cookie 可设置为长时间保持,Session 一般失效时间较短;
  • 存储大小不同:
    Cookie 保存的数据不能超过 4K;

看到这里可能就有人想到了,Session-Cookie是不是就是把Session存储在了客户端的Cookie中呢?是的,的确是这样的,我们接着往下看

3.3 Session-Cookie
的认证流程图

3.4 Session-Cookie
认证步骤解析

  1. 客户端:
    向服务器发送登录信息用户名/密码来请求登录校验;
  2. 服务器:
    验证登录的信息,验证通过后自动创建 Session(将 Session 保存在内存中,也可以保存在 Redis 中),然后给这个 Session 生成一个唯一的标识字符串会话身份凭证session_id(通常称为sid),并在响应头Set-Cookie中设置这个唯一标识符;

注:可以使用签名对sid进行加密处理,服务端会根据对应的secret密钥进行解密 (非必须步骤)

  1. 客户端:
    收到服务器的响应后会解析响应头,并自动将sid保存在本地 Cookie 中,浏览器在下次 HTTP 请求时请求头会自动附带上该域名下的 Cookie 信息;
  2. 服务器:
    接收客户端请求时会去解析请求头 Cookie 中的sid,然后根据这个sid去找服务端保存的该客户端的sid,然后判断该请求是否合法;

3.5 Session-Cookie
优缺点对比

优点

  • Cookie 简单易用
  • Session 数据存储在服务端,相较于 JWT 方便进行管理,也就是当用户登录和主动注销,只需要添加删除对应的 Session 就可以了,方便管理
  • 只需要后端操作即可,前端可以无感等进行操作;

缺点

  • 依赖 Cookie,一旦用户在浏览器端禁用 Cookie,这就完蛋,在google浏览器由于考虑用户安全模式下,经常会禁用cookie,所以这个方案局限性还是比较大的;
  • 非常不安全,Cookie 将数据暴露在浏览器中,增加了数据被盗的风险(容易被 CSRF 等攻击);
  • Session 存储在服务端,增大了服务端的开销,用户量大的时候会大大降低服务器性能;
  • 对移动端的支持性不友好;
  • 还有一个非常重要的问题是:如果没一个独立于应用

    中间session服务器

    ,每个应用服务器必须要开启会话保持才能确保业务达到正确目的,即每个
    服务器必须得保持有状态
    ,这在分布式环境高并发条件,服务器实现按需求横向扩容的目标是相违背的,也是让人无法接受的。

3.6
使用场景

  • 一般中大型的网站都适用(除了 APP 移动端);
  • 由于一般的 Session 需集中存储在内存服务器上(如 Redis);

4、Token认证方案

现在我们已经得知,Session-Cookie的一些缺点,以及 Session 的维护给服务端造成很大困扰,我们必须找地方存放它,又要考虑分布式的问题,甚至要单独为了它启用一套 Redis 集群。那有没有更好的办法?

那Token就应运而生了

4.1
Token认证原理

Token是一个令牌,客户端访问服务器时,验证通过后服务端会为其签发一张令牌,之后客户端就可以携带令牌访问服务器,服务端只需要验证令牌的有效性即可。

一句话概括;
访问资源接口(API)时所需要的资源凭证

一般 Token 的组成:

uid
(用户唯一的身份标识) +
time
(当前时间的时间戳) +
sign
(签名,Token的前几位以哈希算法压缩成的一定长度的十六进制字符串)

Token
的认证流程图:

Token
认证步骤解析:

  1. 客户端:
    输入用户名和密码请求登录校验;
  2. 服务器:
    收到请求,去验证用户名与密码;验证成功后,服务端会签发一个 Token 并把这个 Token 发送给客户端;
  3. 客户端:
    收到 Token 以后需要把它存储起来,web 端一般会放在 localStorage 或 Cookie 中,移动端原生 APP 一般存储在本地缓存中;
  4. 客户端发送请求:
    向服务端请求 API 资源的时候,将 Token 通过 HTTP 请求头 Authorization 字段或者其它方式发送给服务端;
  5. 服务器:
    收到请求,然后去验证客户端请求里面带着的 Token ,如果验证成功,就向客户端返回请求的数据,否则拒绝返还(401);

Token
的优点:

  • 服务端无状态化、可扩展性好:
    Token 机制在服务端不需要存储会话(Session)信息,因为 Token 自身包含了其所标识用户的相关信息,这有利于在多个服务间共享用户状态
  • 支持 APP 移动端设备;
  • 安全性好:
    有效避免 CSRF 攻击(因为不需要 Cookie)
  • 支持跨程序调用:
    因为 Cookie 是不允许跨域访问的,而 Token 则不存在这个问题

Token
的缺点:

  • 配合:
    需要前后端配合处理;
  • 占带宽:
    正常情况下比sid更大,消耗更多流量,挤占更多宽带
  • 性能问题:
    虽说验证 Token 时不用再去访问数据库或远程服务进行权限校验,但是需要对 Token 加解密等操作,所以会更耗性能;
  • 有效期短:
    为了避免 Token 被盗用,一般 Token 的有效期会设置的较短,所以就有了Refresh Token;

4.2
刷新 Token

业务接口用来鉴权的 Token,我们称之为Access Token。

为了安全,我们的Access Token有效期一般设置较短,以避免被盗用。但过短的有效期会造成Access Token经常过期,过期后怎么办呢?

一种办法是:刷新 Access Token,让用户重新登录获取新 Token,会很麻烦;

另外一种办法是:再来一个 Token,一个专门生成 Access Token 的 Token,我们称为Refresh Token;

  • Access Token

    用来访问业务接口,由于有效期足够短,盗用风险小,也可以使请求方式更宽松灵活;
  • Refresh Token

    用来获取 Access Token,有效期可以长一些,通过独立服务和严格的请求方式增加安全性;由于不常验证,也可以如前面的 Session 一样处理;

Refresh Token
的认证流程图:

Refresh Token
认证步骤解析:

  1. 客户端:
    输入用户名和密码请求登录校验;
  2. 服务端:
    收到请求,验证用户名与密码;验证成功后,服务端会签发一个Access Token和Refresh Token并返回给客户端;
  3. 客户端:
    把Access Token和Refresh Token存储在本地;
  4. 客户端发送请求:
    请求数据时,携带Access Token传输给服务端;
  5. 服务端
  • 验证 Access Token 有效:正常返回数据
  • 验证 Access Token 过期:拒绝请求
  • 客户端
    ( Access Token 已过期)

    则重新传输 Refresh Token 给服务端;
  • 服务端
    ( Access Token 已过期)

    验证 Refresh Token ,验证成功后返回新的 Access Token 给客户端;
  • 客户端:
    重新携带新的 Access Token 请求接口;
  • 4.3 Token
    和 Session-Cookie 的区别

    Session-Cookie和Token有很多类似的地方,但是Token更像是Session-Cookie的升级改良版。

    • 存储地不同:
      Session 一般是存储在服务端;Token 是无状态的,一般由前端存储;
    • 安全性不同:
      Session 和 Token 并不矛盾,作为身份认证 Token 安全性比 Session 好,因为每一个请求都有签名还能防止监听以及重放攻击;
    • 支持性不同:
      Session-Cookie 认证需要靠浏览器的 Cookie 机制实现,如果遇到原生 NativeAPP 时这种机制就不起作用了,或是浏览器的 Cookie 存储功能被禁用,也是无法使用该认证机制实现鉴权的;而 Token 验证机制丰富了客户端类型。

    如果你的用户数据可能需要和第三方共享,或者允许第三方调用API接口,用Token 。如果永远只是自己的网站,自己的App,用什么就无所谓了。

    5、OAuth2.0认证方案

    5.1 OAuth2.0定义

    OAuth 2.0 是一个开放授权标准,它允许用户让第三方应用基于令牌Token的授权,在无需暴露用户密码的情况下使第三应用能获取对用户数据的有限访问权限 。

    OAuth 2.0定义了四种授权许可类型:

    1. Authorization Code:授权码
    2. Implicit:隐式许可
    3. Resource Owner Password Credentials:密码凭证
    4. Client Credentials :客户端凭证。

    5.2 OAuth2.0角色

    (A)资源拥有者(RO)
    (B)客户端(Client)
    (C)资源服务器(RS)
    (D)授证服务器(AS)。

    5.3 OAuth2.0认证流程

    5.3.1、OAuth 2.0流程图

    关键步骤:

    (A)用户打开客户端以后,客户端要求用户给予授权。
    (B)用户同意给予客户端授权。
    (C)客户端使用上一步获得的授权,向认证服务器申请令牌。
    (D)授权认证服务器对客户端进行认证以后,确认无误,同意发放令牌。
    (E)客户端使用令牌,向资源服务器申请获取资源。
    (F)资源服务器确认令牌无误,同意向客户端开放资源。

    5.3.2、授权码模式

    授权码模式(authorization code)是功能最完整、流程最严密的授权模式。
    它的特点就是通过客户端的后台服务器,与“服务提供商”的授权认证中心进行互动

    关键步骤:

    (A)用户访问客户端,后者将前者导向认证服务器。
    (B)用户选择是否给予客户端授权。
    (C)假设用户给予授权,认证服务器将用户导向客户端事先指定的"重定向URI"(redirection URI),同时附上一个授权码。
    (D)客户端收到授权码,附上早先的"重定向URI",向认证服务器申请令牌。这一步是在客户端的后台的服务器上完成的,对用户不可见。
    (E)认证服务器核对了授权码和重定向URI,确认无误后,向客户端发送访问令牌(access token)和更新令牌(refresh token)。

    说明备注:

    第1步骤中,客户端申请认证的URI,包含以下参数:
    response_type:表示授权类型,必选项,此处的值固定为"code"
    client_id:表示客户端的ID,必选项
    redirect_uri:表示重定向URI,可选项
    scope:表示申请的权限范围,可选项
    state:表示客户端的当前状态,可以指定任意值,认证服务器会原封不动地返回这个值。

    5.3.2、隐式许可模式

    隐式许可模式(implicit grant type)不通过第三方应用程序的服务器,直接在浏览器中向认证服务器申请令牌,跳过了"授权码"这个步骤,因此得名。

    所有步骤在浏览器中完成,令牌对访问者是可见的,且客户端不需要认证。

    关键步骤:

    (A)客户端将用户导向认证服务器。
    (B)用户决定是否给于客户端授权。
    (C)假设用户给予授权,认证服务器将用户导向客户端指定的"重定向URI",并在URI的Hash部分包含了访问令牌。
    (D)浏览器向资源服务器发出请求,其中不包括上一步收到的Hash值。
    (E)资源服务器返回一个网页,其中包含的代码可以获取Hash值中的令牌。
    (F)浏览器执行上一步获得的脚本,提取出令牌。
    (G)浏览器将令牌发给客户端。

    说明备注:

    A步骤中,客户端发出的HTTP请求,包含以下参数:
    response_type:表示授权类型,此处的值固定为"token",必选项。
    client_id:表示客户端的ID,必选项。
    redirect_uri:表示重定向的URI,可选项。
    scope:表示权限范围,可选项。
    state:表示客户端的当前状态,可以指定任意值,认证服务器会原封不动地返回这个值。

    5.3.3、密码凭证模式

    密码凭证模式(Resource Owner Password Credentials Grant)中,用户向客户端提供自己的用户名和密码。

    客户端使用这些信息,向"服务提供商"索要授权。
    在这种模式中,用户必须把自己的密码给客户端,但是客户端不得储存密码。
    这通常用在用户对客户端高度信任的情况下,并且只有授权认证中心在其他授权模式无法执行的情况下,才能考虑使用这种模式。

    关键步骤:

    (A)客户端将用户导向认证服务器。
    (B)用户决定是否给于客户端授权。
    (C)假设用户给予授权,认证服务器将用户导向客户端指定的"重定向URI",并在URI的Hash部分包含了访问令牌。
    (D)浏览器向资源服务器发出请求,其中不包括上一步收到的Hash值。
    (E)资源服务器返回一个网页,其中包含的代码可以获取Hash值中的令牌。
    (F)浏览器执行上一步获得的脚本,提取出令牌。
    (G)浏览器将令牌发给客户端。

    说明备注:

    B步骤中,客户端发出的HTTP请求,包含以下参数:
    grant_type:表示授权类型,此处的值固定为"password",必选项。
    username:表示用户名,必选项。
    password:表示用户的密码,必选项。
    scope:表示权限范围,可选项。

    5.3.4、密码凭证模式

    客户端凭证模式(Client Credentials Grant)指客户端以自己的名义,而不是以用户的名义,向授权认证中心进行认证。严格地说,客户端凭证模式并不属于OAuth框架所要解决的问题。在这种模式中,用户直接向客户端注册,客户端以自己的名义要求"服务提供商"提供服务,其实不存在授权问题。

    关键步骤:

    (A)客户端向认证服务器进行身份认证,并要求一个访问令牌。
    (B)认证服务器确认无误后,向客户端提供访问令牌。

    备注说明:

    A步骤中,客户端发出的HTTP请求,包含以下参数:
    granttype:表示授权类型,此处的值固定为"clientcredentials",必选项。
    scope:表示权限范围,可选项。

    6、JWT Token验证

    我们知道了Token的使用方式以及组成,我们不难发现,服务端验证客户端发送过来的 Token 时,还需要查询数据库获取用户基本信息,然后验证 Token 是否有效;

    这样每次请求验证都要查询数据库,增加了查库带来的延迟等性能消耗;

    那么这时候业界常用的
    JWT
    就应运而生了!!!

    6.1
    JWT定义

    JWT是Auth0提出的通过对 JSON 进行加密签名来实现授权验证的方案;

    就是登录成功后将相关用户信息组成 JSON 对象,然后对这个对象进行某种方式的加密,返回给客户端;客户端在下次请求时带上这个 Token;服务端再收到请求时校验 token 合法性,其实也就是在校验请求的合法性。

    6.2 JWT
    的组成

    JWT 由三部分组成:Header 头部、Payload 负载和Signature 签名

    它是一个很长的字符串,中间用点(.)分隔成三个部分。列如 :

    eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

    Header
    头部:

    在 Header 中通常包含了两部分:

    • typ:代表 Token 的类型,这里使用的是 JWT 类型;
    • alg:使用的 Hash 算法,例如 HMAC SHA256 或 RSA.

    {
    "alg": "HS256",
    "typ": "JWT"
    }

    Payload
    负载:

    它包含一些声明 Claim (实体的描述,通常是一个 User 信息,还包括一些其他的元数据) ,用来存放实际需要传递的数据,JWT 规定了7个官方字段:

    • iss (issuer):签发人
    • exp (expiration time):过期时间
    • sub (subject):主题
    • aud (audience):受众
    • nbf (Not Before):生效时间
    • iat (Issued At):签发时间
    • jti (JWT ID):编号

    除了官方字段,你还可以在这个部分定义私有字段,下面就是一个例子。

    {
    "sub": "1234567890",
    "name": "John Doe",
    "admin": true
    }

    Signature
    签名

    Signature 部分是对前两部分的签名,防止数据篡改。

    首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。

    HMACSHA256(
    base64UrlEncode(header) + "." +
    base64UrlEncode(payload),
    secret)

    JWT 加密、解密标例


    6.3 JWT
    使用

    客户端收到服务器返回的 JWT,可以储存在 Cookie 里面,也可以储存在 localStorage。

    此后,客户端每次与服务器通信,都要带上这个 JWT。你可以把它放在 Cookie 里面自动发送,但是这样不能跨域,所以更好的做法是放在 HTTP 请求的头信息Authorization字段里面。

    Authorization: Bearer <token>

    6.4 JWT
    认证流程

    其实 JWT 的认证流程与 Token 的认证流程差不多,只是不需要再单独去查询数据库查找用户用户;简要概括如下:

    6.5 JWT
    优缺点

    优点:

    • 不需要在服务端保存会话信息(RESTful API 的原则之一就是无状态),所以易于应用的扩展,即信息不保存在服务端,不会存在 Session 扩展不方便的情况;
    • JWT 中的 Payload 负载可以存储常用信息,用于信息交换,有效地使用 JWT,可以降低服务端查询数据库的次数

    缺点:

    • 加密问题:
      JWT 默认是不加密,但也是可以加密的。生成原始 Token 以后,可以用密钥再加密一次。
    • 到期问题:
      由于服务器不保存 Session 状态,因此无法在使用过程中废止某个 Token,或者更改 Token 的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑。

    7、集团统一授权认证架构方案

    首先感谢各位看官!文章很长,为了普及认证基本原理和知识不得花重手笔详细描述清楚,能一路看到这里,恭喜!应该对认证体系有了系统性的了解和认识。

    通过上面各种方案分析整理对比,我们已经有了一个清晰结构认知。下面我们来设计一套适用于集团内部的统一的授权认证方案。

    我们集团内部有很多个业务系统包括:OA系统、商旅系统、财务系统、培训系统、BPM、ERP、MES、MLP、MOM、MSCS、WCC、MTCS、TIMS、SRM、WMS、PLM、......

    营销系统(这里面又包含60多个子系统,如:设计软件、客服400系统、客情调查问卷、订单系统,账号系统、活动、促销、会员、CRM、电商引流、营销补贴、学习培训、传单系统等以及各种独立的业务中台和数据中台),这些系统都要打通统一授权体系,实现单点登录。所以整体实施方案是比较复杂的。

    当时在设计授权方案是遇到不小挑战,基于已有业务存在两个重要困难点:

    1、集团内部已经有非常多个子系统,算下来大大小小有130多个子系统;

    2、集团职能体系和制造体系已经有一套相对早期的基于CAS单点登录系统,而且很难改得动;主要难办的是只要涉及到财务和生产制造系统,保持系统指标稳定压倒其他一切;营销体系的各个系统相对比较新,采用Oauth2协议认证,实现营销体系内部统一认证。但还是不能满足业务的要求,两套认证体系各自为政,这是认人不可接受的。最终的目标是:
    集团内部所有系统一次登录必须互通互联。

    7.1 方案设计

    OAuth2内部整体概要

    Oauth2认证方案

    两套体系互认

    7.2 关键问题

    授权认证中心主要提供四个端点:认证端点、令牌颁发端点、令牌校验端点和注销端点。

    集团内部Cas单点与营销系统Oauth2.0认证内部打通隧道建立互信机制。

    1、用户登录职能或制造系统在CAS认证通过,发起一次营销系统的Oauth2认证服务消息通知,并完成一次授信;

    2、用户登录营销系统在oauth2认证服务鉴权通过,发起一次CAS通讯通过用户关键信息进行TGT交换,并做一次token和tgt绑定动作。

    3、用户在任意一个体系注销,两边系统都会互发一次消息通知。

    通过上次机制设计实现了两套独立认证体系互通互信。不用修改原有旧系统登录逻辑,也是最少代价方案实现全集团系统单点登录 。

    7.3 核心代码实现

    对外提供访问入口API
    
    @RestController
    @RequestMapping("/oauth")
    @Module("令牌授权")
    public class AccessTokenController implements AccessTokenRemoteService {
        @Resource
        private AccessTokenService accessTokenService;
    
    
        @RestApi(name = "授权码模式授权",no = "Auth02",idx = 1)
        @PostMapping(value = "/token/authCode")
        public Response<AccessToken> authByAuthCode(@RequestBody AuthCodeAuthentication authentication){
            return accessTokenService.authByAuthCode(authentication);
        }
    
        @RestApi(name = "密码模式授权",no = "Auth03",idx = 2)
        @PostMapping(value = "/token/password")
        public Response<AccessToken> authByPassword(@RequestBody PasswordAuthentication authentication) {
            return accessTokenService.authByPassword(authentication);
        }
    
        @RestApi(name = "客户端凭证模式授权",no = "Auth04",idx = 3)
        @PostMapping(value = "/token/clientCredentials")
        public Response<AccessToken> authByRefreshToken(@RequestBody ClientCredentialsAuthentication authentication) {
            return accessTokenService.authByClientCredentials(authentication);
        }
    
    
        @RestApi(name = "刷新令牌授权",no = "Auth05",idx = 4)
        @PostMapping(value = "/token/refreshToken")
        public Response<AccessToken> authByRefreshToken(@RequestBody RefreshTokenAuthentication authentication) {
            return accessTokenService.authByRefreshToken(authentication);
        }

    服务层代码
     @Service
    public class AccessTokenService {
        @Resource
        private RedisTokenStore redisTokenStore;
        @Resource
        private ClientDetailsRepository clientDetailsRepository;
        @Resource
        private LoginUserRepository loginUserRepository;
    
        @Value("${miop.auth.defaultClientId:}")
        public String defaultClientId;
        @Resource
        private RedisTemplate redisTemplate;
    
        /**
         * 獲取登錄用戶
         * @param accessToken
         * @return
         */
        public Response<ShiroUser> getUserByToken(String accessToken) {
            OAuth2Authentication auth2Authentication = redisTokenStore.readAuthentication(accessToken);
            if(auth2Authentication == null){
                return Response.failure("令牌已失效");
            }
            UserCO user =  (UserCO)auth2Authentication.getUserAuthentication().getPrincipal();
            if(user == null){
                return Response.failure("令牌已失效");
            }
            SpringContextUtil.getBean(AccessTokenService.class).renewalAccessToken(accessToken);
            ShiroUser shiroUser = BeanToolkit.instance().copy(user,ShiroUser.class);
            shiroUser.setAccessToken(accessToken);
            return Response.of(shiroUser);
        }
    
        /**
         * 一分钟内只更新一次token的过期时间
         * @param accessToken
         * @return
         */
        @Cacheable(value = "renewalAccessToken",key = "#accessToken")
        public char renewalAccessToken(String accessToken){
            redisTokenStore.renewalAccessToken(accessToken);
            return '1';
        }
    
    
        /**
         * 註銷用戶
         * @param accessToken
         * @return
         */
        public Response logout(String accessToken) {
            OAuth2Authentication auth2Authentication = redisTokenStore.readAuthentication(accessToken);
            if(auth2Authentication != null){
                OAuth2AccessToken oAuth2AccessToken = redisTokenStore.getAccessToken(auth2Authentication);
                if(oAuth2AccessToken != null){
                    redisTokenStore.removeRefreshToken(oAuth2AccessToken.getRefreshToken().getValue());
                    redisTokenStore.removeAccessToken(accessToken);
                }
            }
            loginUserRepository.logout(accessToken);
            return Response.success();
        }
    
        public Response<String> createJwt(String accessToken) {
            Response<ShiroUser> shiroUser = getUserByToken(accessToken);
            Map claims = (Map) JSON.toJSON(shiroUser);
            String jwt = JwtUtils.createJwt(claims,20);
            return Response.of(jwt);
        }
    
    
        /**
         * 用户密码授权
         * @param authentication
         * @return
         */
        public Response<AccessToken> authByPassword(PasswordAuthentication authentication){
            AuthLog authLog = createAuthLog(authentication);
            try {
                AccessToken accessToken = auth(authentication,GrantType.PASSWORD);
                authLog.setStatus(Status.NORMAL.getValue());
                ShiroUser user = accessToken.getUser();
                if(user != null){
                    authLog.setUserName(user.getName());
                }
                return Response.of(accessToken);
            }catch (Exception e){
                authLog.setStatus(Status.UN_NORMAL.getValue());
                authLog.setMsg(e.getMessage());
                throw e;
            }finally {
                redisTemplate.opsForList().rightPush(LogConstants.LOGIN_LOG_REDIS_QUEUE, authLog);
            }
        }
    
    
        public Response<AccessToken> authByAuthCode(AuthCodeAuthentication authentication){
            AccessToken accessToken = auth(authentication,GrantType.AUTHORIZATION_CODE);
            return Response.of(accessToken);
        }
    
        public Response<AccessToken> authByRefreshToken(RefreshTokenAuthentication authentication){
            AccessToken accessToken = auth(authentication,GrantType.REFRESH_TOKEN);
            return Response.of(accessToken);
        }
    
        public Response<AccessToken> authByClientCredentials(ClientCredentialsAuthentication authentication){
            AccessToken accessToken = auth(authentication,GrantType.CLIENT_CREDENTIALS);
            return Response.of(accessToken);
        }
    
        private AccessToken auth(BaseAuthentication authentication,GrantType grantType){
            ClientDetails clientDetails = clientDetailsRepository.selectByIdWithCache(authentication.getClientId());
            if(!StringUtil.equals(clientDetails.getClientSecret(),authentication.getClientSecret())){
                throw new AuthException("无效的 client credentials:"+authentication.getClientSecret());
            }
            if(!clientDetails.getAuthorizedGrantTypes().contains(grantType.getValue())){
                throw new AuthException("该clientId不允许"+grantType.getValue()+"授权方式");
            }
            for (String scope : authentication.getScope().split(",")) {
                if (!clientDetails.getScope().contains(scope)) {
                    throw new AuthException("不合法的scope:"+scope);
                }
            }
            TokenRequest tokenRequest = new TokenRequest((Map)JSON.toJSON(authentication), authentication.getClientId(),
                    Arrays.asList(authentication.getScope().split(",")), grantType.getValue());
            OAuth2AccessToken oAuth2AccessToken = AuthorizationServer.endpoints.getTokenGranter().grant(grantType.getValue(),tokenRequest);
            AccessToken accessToken = getAccessToken(oAuth2AccessToken);
            return accessToken;
        }
    
    
        /**
         * 转成自定义的令牌对象
         * @param oAuth2AccessToken
         * @return
         */
        private AccessToken getAccessToken(OAuth2AccessToken oAuth2AccessToken) {
            DefaultOAuth2AccessToken defaultOAuth2AccessToken = (DefaultOAuth2AccessToken)oAuth2AccessToken;
            DefaultExpiringOAuth2RefreshToken refreshToken = (DefaultExpiringOAuth2RefreshToken)defaultOAuth2AccessToken.getRefreshToken();
            AccessToken accessToken = new AccessToken();
            accessToken.setTokenType(defaultOAuth2AccessToken.getTokenType());
            accessToken.setAccessToken(oAuth2AccessToken.getValue());
            accessToken.setAccessTokenExpiresIn(oAuth2AccessToken.getExpiresIn());
            if(oAuth2AccessToken.getRefreshToken() != null){
                accessToken.setRefreshToken(defaultOAuth2AccessToken.getRefreshToken().getValue());
                accessToken.setRefreshTokenExpiresIn((int)((refreshToken.getExpiration().getTime() - System.currentTimeMillis())/1000));
            }
            OAuth2Authentication auth2Authentication = redisTokenStore.readAuthentication(oAuth2AccessToken.getValue());
            if(auth2Authentication != null){
                Object principal = auth2Authentication.getUserAuthentication().getPrincipal();
                ShiroUser shiroUser = BeanToolkit.instance().copy(principal,ShiroUser.class);
                shiroUser.setAccessToken(oAuth2AccessToken.getValue());
                accessToken.setUser(shiroUser);
                Map claims = (Map) JSON.toJSON(shiroUser);
                String jwt = JwtUtils.createJwt(claims,20);
                accessToken.setJwt(jwt);
            }
            return accessToken;
        }
    
        private AuthLog createAuthLog(PasswordAuthentication authentication) {
            AuthLog authLog = new AuthLog();
            authLog.setClientId(authentication.getClientId());
            authLog.setGrantType(GrantType.PASSWORD.getValue());
            authLog.setAccessTime(LocalDateTime.now());
            authLog.setIp(RequestUtil.getIpAddress());
            authLog.setTraceId(Trace.traceId.get());
            authLog.setAccount(authentication.getAccount());
            authLog.setOrgNo(authentication.getOrgNo());
            return authLog;
        }

    8、总结

    1、通过本文,我们可以全现系统学习认证体系的原理和设计方案;

    2、所有公司只有两个以上系统存在就有必要实现授证登录体系,单独从认证服务本身来看是比较简单的,但是跟多个系统集成的时候还是遇到的问题坑,特别是大企业里系统错综复杂,动则上个百个系统集团单点登录,要成功实施上也不是那么简单的事,希望通过本文我们能学到一点启发。

    3、轻巧方案,用最小的代价,最快方式去实现。认证授权还是有一些细节的坑,比如跨域问题、安全攻防的问题本文还没有提到,等有空再补了!有需要的同学持续关注吧,我会持续完善和修改,暂时先偷一下懒。

    4、本文主要是工作中总结的资料方案,也有引用到一些网上的资料(时间太久也不知道原作者是谁,若有涉及侵权问题请联系本人)。

    参考资料

    [1] OAuth:https://en.wikipedia.org/wiki/OAuth
    [2] RFC 6749:http://www.rfcreader.com/#rfc6749

    前面几篇文章梳理了obs的录屏和推流流程,几条纵线整理下来,算是基本理清了obs的工作流程。

    现在回到第一个目标:
    捕捉桌面的帧数据,用rendertarget显示并输出到UE5材质。

    那么,帧数据到底存放在哪里?如何读取?

    现在录屏效率最高的方法,是直接调用gpu方法去从显存拿数据,dx下的方法是AcquireNextFrame函数。

    在整个工程搜索这个函数,果然obs在windows下是用这个方法实现的录屏

    //obs 录屏核心代码
    //用dx截取当前屏幕帧
    EXPORT bool gs_duplicator_update_frame(gs_duplicator_t *d)
    {
    	DXGI_OUTDUPL_FRAME_INFO info;
    	ComPtr<ID3D11Texture2D> tex;
    	ComPtr<IDXGIResource> res;
    	HRESULT hr;
    
    	if (!d->duplicator) {
    		return false;
    	}
    	if (d->updated) {
    		return true;
    	}
    
    	hr = d->duplicator->AcquireNextFrame(0, &info, res.Assign());
    	if (hr == DXGI_ERROR_ACCESS_LOST) {
    		return false;
    
    	} else if (hr == DXGI_ERROR_WAIT_TIMEOUT) {
    		return true;
    
    	} else if (FAILED(hr)) {
    		blog(LOG_ERROR,
    		     "gs_duplicator_update_frame: Failed to update "
    		     "frame (%08lX)",
    		     hr);
    		return true;
    	}
        //关键步骤,这一步调用dx查询接口,将屏幕帧写入tex
    	hr = res->QueryInterface(__uuidof(ID3D11Texture2D),
    				 (void **)tex.Assign());
    	if (FAILED(hr)) {
    		blog(LOG_ERROR,
    		     "gs_duplicator_update_frame: Failed to query "
    		     "ID3D11Texture2D (%08lX)",
    		     hr);
    		d->duplicator->ReleaseFrame();
    		return true;
    	}
        //copy材质到d->duplicator->texture
    	copy_texture(d, tex);
    	d->duplicator->ReleaseFrame();
    	d->updated = true;
    	return true;
    }

    看到这里,明白了怪不得之前所有结构都找不到图像帧的二进制data数据。

    因为obs用的都是directX或openGL 的texture来存储data数据,这样做的好处是copy和渲染都直接在显存操作,避免了内存和显存交换数据进行的效率损耗。

    obs是优雅了,但太浑然一体了。导致我想开个口子从obs拿二进制数据到Unrea5进行渲染就不容易做了,最简单的办法就是在obs中增加一个内存数据desktopdata,直接挂在obs下面,并增加相应的访问接口。

    对应的获取可以用这个接口,从显存map地址可供cpu访问

    bool gs_texture_map(gs_texture_t *tex, uint8_t **ptr, uint32_t *linesize)
    {
    	HRESULT hr;
    
    	if (tex->type != GS_TEXTURE_2D)
    		return false;
    
    	gs_texture_2d *tex2d = static_cast<gs_texture_2d *>(tex);
    
    	D3D11_MAPPED_SUBRESOURCE map;
    	hr = tex2d->device->context->Map(tex2d->texture, 0,
    					 D3D11_MAP_WRITE_DISCARD, 0, &map);
    	if (FAILED(hr))
    		return false;
    
    	*ptr = (uint8_t *)map.pData;
    	*linesize = map.RowPitch;
    	return true;
    }

    但这样效率肯定不会高,因为obs调用显存接口录屏之后, 还需要从显存往内存desktopdata copy一次数据。

    然后我用desktopdata数据再从内存copy到Unreal5的显存,这会中断unreal5本身的渲染,去等待我这次copy完成。

    有貌似完美的解决方法,如果我把desktopdata创建到显存,去暂存录屏数据,让unreal5直接访问显存的desktopdata,去copy材质,理论上是完美的,但有一个最大的雷,obs用的是dx11,unreal5.1用的是dx12,这样copy感觉会遇到一些不可测的风险。

    剩下的方法有:

    1 不用obs的获取桌面方法,直接在unreal5里用dx12重写获取桌面接口,但也意味着无法使用obs的rtmp推流相关流程和接口,工作量很大。而且obs最厉害的是音频和视频多通道混合,这些都是我想用的。

    2 帮obs升级dx12,这个可以干,但工作量同样很大,但比1还是简单一些。

    十三、实现登出

    至此关于Blazor的内容,先写到这里, 我们基本上完成了登入、增加、删除、查询、修改等功能,应对一般的应用,已经足够。今天实现登录功能。有登入,必然要有登出,本文我们来介绍一下如何登出。

    1. 在Visual Studio 2022的解决方案资源管理器中,鼠标左键选中“Pages”文件夹,右键单击,在弹出菜单中选择“添加—>Razor组件…”,并将组件命名为“Logout.razor”。登出组件的功能是用于退出登入,返回首面。其代码如下:

    @page "/Logout"
    @using BlazorAppDemo.Auth;
    @inject IAuthService authService
    @inject NavigationManager navigation

    @code {
    protected override async Task OnInitializedAsync()
    {

    await authService.LogoutAsync();
    navigation.NavigateTo("/");
    }

    }
    2. 在Visual Studio 2022的解决方案管理器中,使用鼠标左键,双击TokenManager.cs文件,对代码进行修改。具体代码如下:

    usingBlazorAppDemo.Models;usingSystem.Collections.Concurrent;namespaceBlazorAppDemo.Utils
    {
    public classTokenManager
    {
    private const string TOKEN = "authToken";private static readonly ConcurrentDictionary<string, UserToken>tokenManager;staticTokenManager()
    {
    tokenManager
    =new ConcurrentDictionary<string, UserToken>();
    }
    public static ConcurrentDictionary<string, UserToken> Instance { get { returntokenManager; } }public static string Token { get { returnTOKEN; } }public static bool RemoveToken(stringtoken)
    {
    if (tokenManager.TryRemove(token,outUserToken delUserToken))
    {
    Console.WriteLine($
    "delete token {delUserToken.Token}");return true;
    }
    else{

    Console.WriteLine($
    "unable delete token {delUserToken.Token}");return false;
    }
    }
    }
    }

    3.在Visual Studio 2022的解决方案资源管理器中,鼠标左键双击“Api”文件夹中的 “AuthController.cs”文件,将此文件中的Logout方法的代码补全。代码如下:

    usingBlazorAppDemo.Models;usingBlazorAppDemo.Utils;usingMicrosoft.AspNetCore.Http;usingMicrosoft.AspNetCore.Identity;usingMicrosoft.AspNetCore.Mvc;usingMicrosoft.Extensions.Configuration;usingMicrosoft.IdentityModel.Tokens;usingNewtonsoft.Json.Linq;usingSystem.IdentityModel.Tokens.Jwt;usingSystem.Security.Claims;usingSystem.Text;namespaceBlazorAppDemo.Api
    {
    [Route(
    "api/[controller]")]
    [ApiController]
    public classAuthController : ControllerBase
    {
    private readonlyIJWTHelper jwtHelper;publicAuthController(IJWTHelper _IJWTHelper)
    {
    this.jwtHelper =_IJWTHelper;

    }

    [HttpPost(
    "Login")]public async Task<ActionResult<UserToken>>Login(UserInfo userInfo)
    {
    //Demo用
    if (userInfo.UserName == "admin" && userInfo.Password == "666666666666")
    {
    returnBuildToken(userInfo);
    }
    else{
    UserToken userToken
    = newUserToken()
    {
    StatusCode
    =System.Net.HttpStatusCode.Unauthorized,
    IsSuccess
    = false};returnuserToken;
    }
    }
    /// <summary>
    ///建立Token/// </summary>
    /// <param name="userInfo"></param>
    /// <returns></returns>
    privateUserToken BuildToken(UserInfo userInfo)
    {
    string jwtToken = jwtHelper.CreateJwtToken<UserInfo>(userInfo);//建立UserToken,回传客户端
    UserToken userToken = newUserToken()
    {
    StatusCode
    =System.Net.HttpStatusCode.OK,
    Token
    =jwtToken,
    ExpireTime
    = DateTime.Now.AddMinutes(30),
    IsSuccess
    = true};returnuserToken;
    }

    [HttpGet(
    "Logout")]public async Task<ActionResult<UserToken>>Logout()
    {
    bool flag=TokenManager.RemoveToken(TokenManager.Token);var response = newUserToken();
    response.IsSuccess
    = !flag;returnresponse;
    }
    }
    }

    4.在Visual Studio 2022的解决方案资源管理器中,鼠标左键选中“Auth”文件夹中的 “AuthService.cs”文件,将此文件中的LogoutAsync方法中添加如下代码:

    usingBlazorAppDemo.Models;usingBlazorAppDemo.Utils;usingMicrosoft.AspNetCore.Components.Authorization;usingMicrosoft.AspNetCore.Identity;usingNewtonsoft.Json;usingNewtonsoft.Json.Linq;usingSystem.Collections.Concurrent;usingSystem.Net.Http;usingSystem.Text;namespaceBlazorAppDemo.Auth
    {
    public classAuthService : IAuthService
    {
    private readonlyHttpClient httpClient;private readonlyAuthenticationStateProvider authenticationStateProvider;private readonlyIConfiguration configuration;private readonlyApi.AuthController authController;private readonly stringcurrentUserUrl, loginUrl, logoutUrl;publicAuthService( HttpClient httpClient, AuthenticationStateProvider authenticationStateProvider,
    IConfiguration configuration,Api.AuthController authController)
    {
    this.authController =authController;this.httpClient =httpClient;this.authenticationStateProvider =authenticationStateProvider;this.configuration =configuration;
    currentUserUrl
    = configuration["AuthUrl:Current"] ?? "Auth/Current/";

    loginUrl
    = configuration["AuthUrl:Login"] ?? "api/Auth/Login";
    logoutUrl
    = configuration["AuthUrl:Logout"] ?? "/api/Auth/Logout/";
    }
    public async Task<UserToken>LoginAsync(UserInfo userInfo)
    {
    response.Content.ReadFromJsonAsync
    <UserToken>();var result =authController.Login(userInfo);var loginResponse =result.Result.Value;if (loginResponse != null &&loginResponse.IsSuccess)
    {
    TokenManager.Instance.TryAdd(TokenManager.Token, loginResponse);
    ((ImitateAuthStateProvider)authenticationStateProvider).NotifyUserAuthentication(loginResponse.Token);

    httpClient.DefaultRequestHeaders.Authorization
    = new System.Net.Http.Headers.AuthenticationHeaderValue("bearer",
    loginResponse.Token);
    returnloginResponse;
    }
    return new UserToken() { IsSuccess = false};
    }
    public async Task<UserToken>LogoutAsync()
    {
    var result =authController.Logout();var logoutResponse =result.Result.Value;
    ((ImitateAuthStateProvider)authenticationStateProvider).NotifyUserLogOut();
    httpClient.DefaultRequestHeaders.Authorization
    = null;returnlogoutResponse;
    }
    }

    }

    LogoutAsync方法:

    • 将token从TokenManger实例中移除
    • 通知前面页面更新登录状态
    • 将request中的header参数bearer token移除。

    5. 在Visual Studio 2022的解决方案管理器中,使用鼠标左键,双击ImitateAuthStateProvider.cs文件,对代码进行修改。具体代码如下:

    usingBlazorAppDemo.Models;usingBlazorAppDemo.Utils;usingMicrosoft.AspNetCore.Components.Authorization;usingSystem.Net.Http;usingSystem.Security.Claims;namespaceBlazorAppDemo.Auth
    {
    public classImitateAuthStateProvider : AuthenticationStateProvider
    {
    private readonlyIJWTHelper jwt;privateAuthenticationState anonymous;private readonlyHttpClient httpClient;publicImitateAuthStateProvider(IJWTHelper _jwt, HttpClient httpClient)
    {
    anonymous
    = new AuthenticationState(new ClaimsPrincipal(newClaimsIdentity()));
    jwt
    =_jwt;this.httpClient =httpClient;
    }
    bool isLogin = false;string token = string.Empty;public override Task<AuthenticationState>GetAuthenticationStateAsync()
    {
    //确认是否已经登录
    UserToken userToken;
    TokenManager.Instance.TryGetValue(TokenManager.Token,
    outuserToken);string tokenInLocalStorage=string.Empty;if (userToken != null)
    {
    tokenInLocalStorage
    =userToken.Token;
    }
    if (string.IsNullOrEmpty(tokenInLocalStorage))
    {
    //沒有登录,则返回匿名登录者
    returnTask.FromResult(anonymous);
    }
    //將token取出转换为claim
    var claims =jwt.ParseToken(tokenInLocalStorage);//在每次request的header中都将加入bearer token
    httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("bearer",
    tokenInLocalStorage);
    //回传带有user claim的AuthenticationState

    return Task.FromResult(new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity(claims, "jwt"))));

    }
    public voidLogin(UserInfo request)
    {
    //1.验证用户账号密码是否正确
    if (request == null)
    {
    isLogin
    =false;
    }
    if (request.UserName == "user" && request.Password == "666666666666")

    {
    isLogin
    = true;
    token
    = jwt.CreateJwtToken<UserInfo>(request);
    Console.WriteLine($
    "JWT Token={token}");
    }

    NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
    }
    public void NotifyUserAuthentication(stringtoken)
    {
    var claims =jwt.ParseToken(token);var authenticatedUser = new ClaimsPrincipal(new ClaimsIdentity(claims, "jwt"));var authState = Task.FromResult(newAuthenticationState(authenticatedUser));
    NotifyAuthenticationStateChanged(authState);
    }
    public voidNotifyUserLogOut()
    {
    var authState =Task.FromResult(anonymous);
    NotifyAuthenticationStateChanged(authState);
    }

    }
    }

    6. 在Visual Studio 2022的解决方案管理器中,使用鼠标左键,双击MainLayout.razor文件,对代码进行修改。具体代码如下:

    @using BlazorAppDemo.Pages
    @inherits LayoutComponentBase
    <PageTitle>BlazorAppDemo</PageTitle>

    <divclass="page">
    <divclass="sidebar">
    <NavMenu/>
    </div>

    <main>
    <AuthorizeView>
    <Authorized>
    <divclass="top-row px-4">


    <ahref="https://docs.microsoft.com/aspnet/"target="_blank">About</a>
    <divclass="col-3 oi-align-right">你好, @context.User.Identity.Name!<ahref="/Logout">Logout</a>
    </div>
    </div>

    <articleclass="content px-4">@Body</article>

    </Authorized>
    <NotAuthorized>
    <divstyle="margin: 120px 0; width:100%; text-align: center; color: red;">

    <spanstyle="font-size:20px">检测到登录超时,请重新<ahref="/login"style="text-decoration:underline">登录</a>


    </span>
    </div>
    <RedirectToLogin></RedirectToLogin>
    </NotAuthorized>
    </AuthorizeView>

    </main>
    </div>

    7. 在Visual Studio 2022的菜单栏上,找到“调试-->开始调试”或是按F5键,Visual Studio 2022会生成BlazorAppDemo应用程序。浏览器会打开登录页面。我们在登录页面的用户名输入框中输入用户名,在密码输入框中输入密码,点击“登录”按钮,进行登录。我们进入了系统,在页面的右上角处,会出现登录用户的用户名,与一个“Logout”按钮。如下图。