2023年3月

简介

责任链模式(Chain of Responsibility Pattern)是一种行为型设计模式,也叫职责链模式、命令链模式。这种模式为请求创建了一个接收者对象的链,允许你将请求沿着处理者链进行发送,每个处理者均可对请求进行处理,或将其传递给链上的下个处理者。

当程序需要使用不同方式来处理多种类请求,且请求类型和顺序不可知,或者当必须按顺序执行多个处理时,可以使用责任链模式。或者如果所需处理及其顺序必须在运行时进行改变,也可以使用该模式。

作用

  1. 避免请求发送者与接收者耦合在一起,客户只需要将请求发送到链上,而无须关心请求的处理细节和请求的传递。
  2. 通过改变链内的成员或者调动它们的次序,允许动态地新增或者删除责任。

实现步骤

  1. 创建一个抽象处理器类,用来供处理器继承。
  2. 抽象处理器类可将各子类按任意组织为链式,以便调用。
  3. 创建多个互不干涉的处理器,实现抽象类的next方法,以便不断执行链式检查。

UML

Java代码

抽象事件处理类

//AbstractHandler.java 所有处理变成链式,可以互动干涉,动态组合
public abstract classAbstractHandler {//形成职责链
   privateAbstractHandler next;//创建调用链,传入多个handler,按顺序形成链,返回第一个handler
   public staticAbstractHandler link(AbstractHandler first, AbstractHandler... chain) {
AbstractHandler head
=first;for(AbstractHandler handler : chain) {
head.next
=handler;
head
=handler;
}
returnfirst;
}
//子类需要实现的检查方法 public abstract boolean check(intuid);//继续下一个检查 protected boolean checkNext(intuid) {if (next == null) {return true;
}
returnnext.check(uid);
}
}

不同事件,可以多个,互不关联

```java//AuthHandler.java 权限检查类
public class AuthHandler extendsAbstractHandler {//如果检查不通过则返回失败,否则继续下一个检查
    public boolean check(intuid) {
System.out.println(
this.getClass().getName() + "::check() [uid = " + uid + "]");if (uid % 2 == 0) {return false;
}
returncheckNext(uid);
}
}
```

```java
//RequestHandler.java 请求是否安全合法检查 public class RequestHandler extendsAbstractHandler {//如果检查不通过则返回失败,否则继续下一个检查 public boolean check(intuid) {
System.out.println(
this.getClass().getName() + "::check() [uid = " + uid + "]");if (uid % 1 != 0) {return false;
}
returncheckNext(uid);
}
}
```

```java
//UserHandler.java 用户基本信息检查类 public class UserHandler extendsAbstractHandler {//如果检查不通过则返回失败,否则继续下一个检查 public boolean check(intuid) {
System.out.println(
this.getClass().getName() + "::check() [uid = " + uid + "]");if (uid % 3 == 0) {return false;
}
returncheckNext(uid);
}
}
```

测试调用

    /*** 责任链模式核心是打造一个调用处理链,每个处理链都实现抽象类的next方法,从而可以任意组织各种检查行为。
* 通过改变链内的成员或者调动它们的顺序,允许动态地新增或者删除职责,从而实现按需组织。
*/ //可以任意组织职责链,先后顺序根据需要来 AbstractHandler handler1 =AbstractHandler.link(newRequestHandler(),newUserHandler(),newAuthHandler());

System.out.println(
"handler1.check(1001)开始");
handler1.check(
1001);
System.out.println(
"handler1.check(1002)开始");
handler1.check(
1002);//可以任意组织职责链,先后顺序根据需要来 AbstractHandler handler2 =AbstractHandler.link(newAuthHandler(),newRequestHandler(),newUserHandler());

System.out.println(
"handler2.check(1001)开始");
handler2.check(
1001);
System.out.println(
"handler2.check(1002)开始");
handler2.check(
1002);

C语言代码

func.h 头文件函数

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

//定义通用handler
typedef structHandler
{
char name[50];//handler链指针 struct Handler *next;//结构体内部的check_handler函数,供各handler独立实现 bool (*check_handler)(struct Handler *, int);
} Handler;
//创建handler调用链,逐个创建 Handler *link_handler(Handler *handler, Handler *next);//两种创建链式hander的方式,功能相同,可以传入多个参数 Handler *make_handler_chain_count(intlenght, ...);
Handler
*make_handler_chain(Handler *handler, ...);//检查handler通用函数 bool check_handler_start(Handler *handler, intparam);//定义权限检查handler typedef structAuthHandler
{
char name[50];
Handler
*next;bool (*check_handler)(struct Handler *, int);
} AuthHandler;
//创建AuthHandler AuthHandler *create_auth_handler(char *name);//定义请求检查handler typedef structRequestHandler
{
char name[50];
Handler
*next;bool (*check_handler)(struct Handler *, int);
} RequestHandler;
//创建RequestHandler RequestHandler *create_request_handler(char *name);//定义用户检查handler typedef structUserHandler
{
char name[50];
Handler
*next;bool (*check_handler)(struct Handler *, int);
} UserHandler;
//创建UserHandler UserHandler *create_user_handler(char *name);

统一事件处理

//handler.c 基础事件
#include <stdio.h>#include<stdlib.h>#include<stdarg.h>#include"func.h"

//创建调用链,按顺序形成链,返回第一个handler
Handler *link_handler(Handler *handler, Handler *next)
{
handler
->next =next;returnhandler;
}
//不定参数创建调用链,第一个参数是handler的数量,后面是多个handler Handler *make_handler_chain_count(intlenght, ...)
{
va_list args;
va_start(args, lenght);
//取出第1个handler Handler *first = va_arg(args, Handler *);
Handler
*head =first;//把handler追加到next中,形成链,总长度减去第1个 for (int i = 0; i < lenght - 1; i++)
{
head
->next = va_arg(args, Handler *);
head
= head->next;
}
va_end(args);
returnfirst;
}
//不定参数创建调用链,第一个参数是handler的数量,后面是多个handler,最后一个传NULL Handler *make_handler_chain(Handler *first, ...)
{
va_list args;
va_start(args, first);
Handler
*head =first;//把handler追加到next中,以NULL作为结束符 while (head !=NULL)
{
head
->next = va_arg(args, Handler *);
head
= head->next;
}
va_end(args);
returnfirst;
}
//单独handler检查开始函数 bool check_handler_start(Handler *handler, intparam)
{
return handler->check_handler(handler, param);
}

不同事件,可以多个,互不关联

```c//auth_handler.c 权限检查类
#include <string.h>#include<stdio.h>#include<stdlib.h>#include"func.h"

/*AuthHandler check函数实现*/
bool auth_handler_check(Handler *handler, intparam)
{
printf(
"\r\n auth_handler_check: [handler.name = %s param = %d]", handler->name, param);
AuthHandler
*auth_handler = (AuthHandler *)handler;//这里是判断条件,如果出错则终止调用链,返回false if (param % 2 == 0)
{
printf(
"\r\n auth_handler_check: error[ %d %s 2 ] == 0", param, "%");return false;
}
//通过next调用下一步检查 if (handler->next !=NULL)
{
return auth_handler->next->check_handler(handler->next, param);
}
return true;
}
/*创建具体处理器的函数*/AuthHandler*create_auth_handler(char *name)
{
AuthHandler
*handler = (AuthHandler *)malloc(sizeof(AuthHandler));
strncpy(handler
->name, name, 50);//将handler的check_handler函数赋值为指定函数,便于检查处理 handler->check_handler = &auth_handler_check;
handler
->next =NULL;returnhandler;
}
```

```c
//request_handler.c 请求是否安全合法检查 #include <string.h>#include<stdio.h>#include<stdlib.h>#include"func.h" /*RequestHandler check函数实现*/ bool request_handler_check(Handler *handler, intparam)
{
printf(
"\r\n request_handler_check: [handler.name = %s param = %d]", handler->name, param);
RequestHandler
*request_handler = (RequestHandler *)handler;//这里是判断条件,如果出错则终止调用链,返回false if (param % 5 == 0)
{
printf(
"\r\n request_handler_check: error[ %d %s 5 ] == 0", param, "%");return false;
}
//通过next调用下一步检查 if (handler->next !=NULL)
{
return request_handler->next->check_handler(handler->next, param);
}
return true;
}
/*创建具体处理器的函数*/RequestHandler*create_request_handler(char *name)
{
RequestHandler
*handler = (RequestHandler *)malloc(sizeof(RequestHandler));
strncpy(handler
->name, name, 50);//将handler的check_handler函数赋值为指定函数,便于检查处理 handler->check_handler = &request_handler_check;
handler
->next =NULL;returnhandler;
}
```

```c
//user_handler.c 用户基本信息检查类 #include <string.h>#include<stdio.h>#include<stdlib.h>#include"func.h" /*UserHandler check_handler函数实现*/ bool user_handler_check(Handler *handler, intparam)
{
printf(
"\r\n user_handler_check: [handler.name = %s param = %d]", handler->name, param);
UserHandler
*user_handler = (UserHandler *)handler;//这里是判断条件,如果出错则终止调用链,返回false if (param % 3 == 0)
{
printf(
"\r\n user_handler_check: error[ %d %s 3 ] == 0", param, "%");return false;
}
//通过next调用下一步检查 if (handler->next !=NULL)
{
return user_handler->next->check_handler(handler->next, param);
}
return true;
}
/*创建具体处理器的函数*/UserHandler*create_user_handler(char *name)
{
UserHandler
*handler = (UserHandler *)malloc(sizeof(UserHandler));
strncpy(handler
->name, name, 50);//将handler的check_handler函数赋值为指定函数,便于检查处理 handler->check_handler = &user_handler_check;
handler
->next =NULL;returnhandler;
}
```

测试调用

#include <stdio.h>#include<stdlib.h>#include<stdbool.h>#include"../src/func.h"

int main(void)
{
/**
* 责任链模式核心是打造一个调用处理链,每个处理链都实现抽象类的next方法,从而可以任意组织各种检查行为。
* 通过改变链内的成员或者调动它们的顺序,允许动态地新增或者删除职责,从而实现按需组织。
*/ //创建一组hanler RequestHandler *request_handler = create_request_handler("request_handler_01");
UserHandler
*user_handler = create_user_handler("user_handler_02");
AuthHandler
*auth_handler = create_auth_handler("auth_handler_03");
printf(
"创建handler:\r\n %s %s %s", request_handler->name, user_handler->name, auth_handler->name);//将handler逐个链接成职责链 link_handler((Handler *)request_handler, (Handler *)user_handler);
link_handler((Handler
*)user_handler, (Handler *)auth_handler);

printf(
"\r\n建立职责链:\r\n");
Handler
*handler_cur = (Handler *)request_handler;while (handler_cur !=NULL)
{
printf(
"-> %s", handler_cur->name);
handler_cur
= handler_cur->next;
}
//从任意handler开始检查//printf("\r\ncheck_handler_start检查:");//check_handler_start((Handler *)request_handler, 666);//从执行handler开始 printf("\r\n开始检查:");bool result1 = request_handler->check_handler((Handler *)request_handler, 666);
printf(
"\r\n执行结果: %s \r\n", result1 ? "true" : "false");/*释放内存*/ free(handler_cur);free(request_handler);free(auth_handler);free(user_handler);/*** ========分割线============ ***/printf("\r\n=============\r\n");/*创建一组hanler*/RequestHandler*request_handler2 = create_request_handler("request_handler_101");
UserHandler
*user_handler2 = create_user_handler("user_handler_102");
AuthHandler
*auth_handler2 = create_auth_handler("auth_handler_103");
printf(
"\r\n创建handler:\r\n %s %s %s", request_handler2->name, user_handler2->name, auth_handler2->name);//将handler一次性链接为职责链,传入多个handler,第一个参数是数量 Handler *handler2 = make_handler_chain_count(3, auth_handler2, request_handler2, user_handler2);
printf(
"\r\n建立职责链:\r\n");
Handler
*handler_cur2 = (Handler *)handler2;while (handler_cur2 !=NULL)
{
printf(
"-> %s", handler_cur2->name);
handler_cur2
= handler_cur2->next;
}
//调用通用检查函数开始 printf("\r\n开始检查:");bool result2 = check_handler_start(handler2, 777);
printf(
"\r\n执行结果: %s \r\n", result2 ? "true" : "false");/*释放内存*/ free(handler_cur2);free(request_handler2);free(auth_handler2);free(user_handler2);/*** ========分割线============ ***/printf("\r\n=============\r\n");/*再创建一组hanler*/RequestHandler*request_handler3 = create_request_handler("request_handler_201");
UserHandler
*user_handler3 = create_user_handler("user_handler_202");
AuthHandler
*auth_handler3 = create_auth_handler("auth_handler_203");
printf(
"\r\n创建handler:\r\n %s %s %s", request_handler3->name, user_handler3->name, auth_handler3->name);//将handler一次性链接为职责链,传入多个handler,最后一个参数是NULL Handler *handler3 = make_handler_chain((Handler *)auth_handler3, user_handler3, request_handler3, NULL);
Handler
*handler_cur3 = (Handler *)handler3;
printf(
"\r\n建立职责链:\r\n");while (handler_cur3 !=NULL)
{
printf(
"-> %s", handler_cur3->name);
handler_cur3
= handler_cur3->next;
}
printf(
"\r\n开始检查:");bool result3 = check_handler_start(handler3, 167);
printf(
"\r\n执行结果: %s \r\n", result3 ? "true" : "false");/*释放内存*/ free(handler_cur3);free(request_handler3);free(auth_handler3);free(user_handler3);return 0;
}

更多语言版本

不同语言实现设计模式:
https://github.com/microwind/design-pattern

前言

这是写作《C#与.NET6 开发从入门到实践》时的小故事,作为本书正式上市的宣传,在此分享给大家。

正文

.NET目前有两个比较成熟的GraphQL框架,其中一个是HotChocolate,在使用对比后认为这个使用更方便,因此将其作为本书的介绍内容。结果在研究客户端订阅的时候发现一个bug:如果订阅因为异常(例如网络掉线)而结束,订阅不会有任何反应(只是这个订阅永远不会再收到数据)。我就在想,如果一个订阅长时间没有收到数据,到底是因为出问题了还是只是单纯的没有新数据。

随后我就跑到GitHub上提交问题,结果作者并没有理解我的意思,然后就不了了之。最后在准备实战演练的项目时又用到了客户端,又想起来这个bug,就当场强迫症发作又给作者提问题,结果又是石沉大海。无奈干脆下载代码亲自研究客户端到底是哪里出问题导致这个bug。感兴趣的朋友可以看看
https://github.com/ChilliCream/graphql-platform/issues/5006

这里我就简单介绍一下问题的原因和处理方法。客户端在WebSocket和订阅实例之间使用Pipeline作为中介,然后使用Channel向用户API传递解析后的对象。其中数据读取和写入使用两个Task分别负责,读取WebSocket写入Pipeline的Task在循环读取WebSocket的时候没有向Channel传递表示连接结束的对象,导致客户端实例不知道实情,最后用户API没有任何反。用户API中使用的IObserver也是一个关键点,客户端在订阅异常结束时没有调用Complete方法通知观察者数据流已经结束,导致用户API没有反应。根据微软文档,应该在确定数据流结束后调用Complete方法通知观察者结束观察,无论数据流因何结束。

客户端使用的Pipeline是一个网络数据缓存的优秀中介,手动处理粘包数据是非常麻烦的,Pipeline就专门负责管理数据是否被消耗,查看过但没有被消耗的数据应该如何处理。新的Span和Memory类型则可以实现直接访问缓冲区内存,避免查看但不消耗数据的情况下反复复制内存导致性能下降和内存占用上升。

结果作者在使用Pipeline之后切断了WebSocket和用户API之间的直接联系又没有处理相关情况最后导致了这个bug。目前作者正在考虑改用其他方式实现客户端,这个问题也许会自然消失。我的修复已经合并到主分支,不过被标记到13.0上,但是客户端目前12.0和13.0的包共存,12.0的代码生成器生成的代码和13.0的实现不兼容。不知道12.0版有没有包含修复。

结语

这个问题前前后后历时将近半年,真是把我折磨的够呛。最后请让我再宣传一次我的新书《C#与.NET6 开发从入门到实践》。详情请看
《C#与.NET6 开发从入门到实践》上市,作者亲自来打广告了!

购买方式:
京东自营:
https://item.jd.com/13860498.html
当当自营:
https://product.dangdang.com/29546824.html

本文地址:
https://www.cnblogs.com/coredx/p/17253035.html

Web请求与响应

Web的工作原理可以分为以下几个步骤:

  1. 输入URL:Web客户端使用Web浏览器输入所需访问的URL(统一资源定位符)。

  2. 建立连接:Web浏览器与Web服务器之间建立TCP/IP连接,以便传输数据。

  3. 发送HTTP请求:Web浏览器向Web服务器发送HTTP请求,请求所需的Web资源。

  4. 接收HTTP响应:Web服务器接收HTTP请求,并通过HTTP响应将Web资源发送给Web浏览器。

  5. 渲染Web页面:Web浏览器接收HTTP响应,并根据所收到的数据渲染Web页面,以便呈现给用户。

  6. 关闭连接:Web浏览器和Web服务器之间的TCP/IP连接被关闭。

什么是 HTTP 请求?

HTTP(超文本传输协议)是一种用于 Web 服务器和客户端(浏览器)之间通信以通过 Internet 传输数据的协议。 HTTP 请求是客户端向服务器发送的请求特定资源(如网页、图像或视频)的消息。 HTTP 请求方法决定了请求的类型,例如 GET、POST、PUT、DELETE、HEAD、OPTIONS、CONNECT、TRACE 等。

HTTP请求的结构:

HTTP 请求由请求行、标头和可选的消息正文组成。

请求行包括 HTTP 方法、所请求资源的 URL(统一资源定位符)以及所使用的 HTTP 版本。

标头提供有关请求的其他信息,例如用户代理、请求的内容类型和可接受的编码格式。

消息正文是可选的,包含数据,例如表单数据或 JSON 。

HTTP请求方法:

HTTP 定义了各种请求方法,指示要对 URL 标识的资源执行的操作。最常见的 HTTP 请求方法是:

  1. GET:从服务器获取资源,例如网页或图像。

  2. POST:将数据提交给服务器进行处理,例如表单提交或文件上传。

  3. PUT:用新数据更新服务器上的现有资源。

  4. DELETE:从服务器中删除资源。

  5. HEAD:检索资源的标头,不带消息正文。

  6. OPTIONS:用于获取当前URL所支持的方法。若请求成功,则它会在HTTP头中包含一个名为“Allow”的头,值是所支持的方法,如“GET, POST”。

  7. CONNECT:建立到资源的网络连接,例如代理服务器。

  8. TRACE:回显接收到的请求消息,用于调试目的。
    HTTP 标头:

HTTP 标头用于提供有关 HTTP 请求或响应的附加信息。它们是由冒号分隔的键值对,包含在请求或响应消息中。 Header有多种类型,例如通用Header、请求Header、响应Header和实体Header。一些常见的标头包括:

  1. User-Agent:向访问网站提供你所使用的浏览器类型、操作系统及版本、CPU 类型、浏览器渲染引擎、浏览器语言、浏览器插件等信息的标识

  2. Accept:指定客户端接受的内容类型。

  3. Content-Type:指定请求或响应消息中内容的类型。

  4. Content-Length:指定消息体的长度,以字节为单位。

  5. Cache-Control:指定响应的缓存指令,例如 max-age 和 must-revalidate。

  6. Authorization:指定请求的身份验证凭据。
    HTTP 消息体body:

HTTP 消息正文是 HTTP 请求或响应的可选部分,包含各种格式的数据,例如 HTML、JSON、XML 或二进制数据。在请求中,消息正文包含要发送到服务器的数据,例如表单数据或文件上传。在响应中,消息正文包含所请求资源的实际内容,例如 HTML 页面或图像。

总之,了解 http 请求和响应的基础知识对于 Web 开发以及 Web 服务器和客户端之间的通信至关重要。通过了解如何构建 HTTP 请求、各种请求方法、标头的使用以及可选的消息正文,开发人员可以有效地与服务器通信并创建健壮的 Web 应用程序。

什么是 HTTP 响应?

HTTP响应是由服务器向客户发送的对请求的响应。 HTTP 响应包含以下组件:

  1. HTTP版本

  2. 状态码

  3. 状态消息

  4. 响应头

  5. 响应体

1、HTTP版本

响应中使用的 HTTP 版本在响应的第一行中指定。例如:

HTTP/1.1 200 OK

这指定使用 HTTP 版本 1.1 发送响应。

2、状态码

状态代码表示所请求操作的结果。 HTTP 定义了五类状态码:

  1. 信息提示 (100-199)

  2. 成功 (200-299)

  3. 重定向 (300-399)

  4. 客户端错误 (400-499)

  5. 服务器错误 (500-599)
    每个状态码都是一个三位数字,它包含在HTTP版本之后的响应的第一行中。例如:

HTTP/1.1 200 OK

这表明操作成功,请求的资源包含在响应正文中。

3、状态信息

状态消息是对状态代码的描述。它包含在状态代码之后的响应的第一行中。例如:

HTTP/1.1 404 Not Found

这表示未找到请求的资源,状态消息提供了问题的简短描述。

4、响应头

响应标头包含有关响应的元数据。它们类似于请求标头,但提供有关响应而不是请求的信息。响应标头可以包括有关服务器、缓存策略、cookie 等的信息。

响应标头包含在第一行之后的响应中。每个标头都是一个由冒号分隔的键值对。例如:

HTTP/1.1 200 OK

Content-Type: text/html

Content-Length: 1274

Server: Apache

在此示例中,响应包含三个标头:Content-Type、Content-Length 和 Server。

5、响应体

响应正文包含请求的资源或错误消息,如果找不到请求的资源。响应正文的格式取决于响应中包含的 Content-Type 标头。例如,如果 Content-Type 标头设置为“text/html”,则响应正文应包含 HTML 代码。

响应主体包含在响应标头之后的响应中。如果响应主体很大,它可能会被拆分成多个数据包。

HTTP 请求和响应的技术点

HTTP 请求和响应有几个开发人员需要注意的技术点。这些技术点决定了请求和响应如何在客户端和服务器之间构建和传输。

1、请求和响应标头

HTTP 请求和响应包含提供有关请求或响应的附加信息的标头。标头是在消息标头中发送的键值对,它们用于传达有关请求或响应的元数据。 Headers 有多种类型,包括通用 Headers、请求 Headers、响应 Headers 和实体 Headers。

通用标头适用于请求和响应,并提供有关整个消息的信息,例如消息格式、发送日期和时间以及消息是否可以缓存。

请求标头用于提供有关发出请求的客户端的信息,例如用户代理(用于访问服务器的软件)、接受的内容类型以及用于压缩消息正文的编码。

响应头提供了服务器对客户端请求的响应信息,如响应的内容类型、消息体的长度、响应是否可以缓存等。

实体标头用于提供有关消息正文的信息,例如内容长度和内容编码。

2、HTTP 方法

HTTP 请求使用一组方法来指定要对 URL 中标识的资源执行的所需操作。最常见的 HTTP 方法是 GET、POST、PUT、DELETE 和 HEAD。

  1. GET:用于从服务器检索数据。

  2. POST:用于向服务器发送数据以创建或更新资源。

  3. PUT:用于更新服务器上的现有资源。

  4. DELETE:用于从服务器中删除资源。

  5. HEAD:用于检索资源的标头,而不是消息正文。
    HTTP 状态码
    HTTP 响应包括指示请求状态的状态代码。有五类状态代码,每类都有自己的一组代码:

• 1xx: 信息 - 表示服务器已收到请求并正在继续处理它。
• 2xx: 成功——表示请求被成功接收、理解和接受。
• 3xx: 重定向——表示客户端需要采取进一步的行动来完成请求。
• 4xx: 客户端错误 - 表示请求包含错误的语法或无法实现。
• 5xx: 服务器错误 - 表示服务器未能满足有效请求。
最常见的状态代码是 200 OK(请求成功)、404 Not Found(未找到请求的资源)和 500 Internal Server Error(服务器在处理请求时遇到错误)。

3、Cookies

HTTP 请求和响应还可以包括 cookie,它们是存储在客户端计算机上的小文本文件。 Cookie 用于存储有关客户端偏好或之前与服务器交互的信息。服务器可以使用 cookie 来识别客户端并提供定制的内容。

4、缓存

HTTP 请求和响应还可以被缓存,这意味着客户端或中间服务器可以存储响应的副本以备将来使用。缓存有助于减少网络流量并提高性能,但如果缓存的内容过时或陈旧,也会导致问题。

作者博客:

yhttps://blog.51cto.com/sdwml/6104070

1.基于SpringBoot请求

请求
(HttpServletRequest):获取请求数据

在浏览器地址输入地址,点击回车请求服务器,这个过程就是一个请求过程。

1.1简单参数

1.1.1原始方式

在原始的Web程序当中,需要通过Servlet中提供的API:HttpServletRequest(请求对象),获取请求的相关信息。比如获取请求参数:

Tomcat接受到Http请求时:把请求的相关信息封装到HttpServletRequest对象中。

在Controller中,我们要想获取Request对象,可以直接在方法的形参中声明HttpServletRequest对象。然后就可以通过该对象来获取请求信息:

//根据指定的参数名获取请求参数的数据值
String  request.getParameter("参数名")
@RestController
public class RequestController {
    //原始方式
    @RequestMapping("/simpleParam")
    public String simpleParam(HttpServletRequest request){
        // http://localhost:8080/simpleParam?name=Tom&age=10
        // 请求参数: name=Tom&age=10   (有2个请求参数)
        // 第1个请求参数: name=Tom   参数名:name,参数值:Tom
        // 第2个请求参数: age=10     参数名:age , 参数值:10
​
        String name = request.getParameter("name");//name就是请求参数名
        String ageStr = request.getParameter("age");//age就是请求参数名
​
        int age = Integer.parseInt(ageStr);//需要手动进行类型转换
        System.out.println(name+"  :  "+age);
        return "OK";
    }
}

1.1.2SpringBoot方式

在Springboot的环境中,对原始的API进行了封装,接收参数的形式更加简单。 如果是简单参数,参数名与形参变量名相同,定义同名的形参即可接收参数。

@RestController
public class RequestController {
    // http://localhost:8080/simpleParam?name=Tom&age=10
    // 第1个请求参数: name=Tom   参数名:name,参数值:Tom
    // 第2个请求参数: age=10     参数名:age , 参数值:10
    
    //springboot方式
    @RequestMapping("/simpleParam")
    public String simpleParam(String name , Integer age ){//形参名和请求参数名保持一致
        System.out.println(name+"  :  "+age);
        return "OK";
    }
}

结论:不论是GET请求还是POST请求,对于简单参数来讲,只要保证==请求参数名和Controller方法中的形参名保持一致==,就可以获取到请求参数中的数据值。

1.1.3参数名不一致

如果方法形参名称与请求参数名称不一致,controller方法中的形参还能接收到请求参数值吗?

@RestController
public class RequestController {
    // http://localhost:8080/simpleParam?name=Tom&age=20
    // 请求参数名:name
​
    //springboot方式
    @RequestMapping("/simpleParam")
    public String simpleParam(String username , Integer age ){//请求参数名和形参名不相同
        System.out.println(username+"  :  "+age);
        return "OK";
    }
}

答案:运行没有报错。 controller方法中的username值为:null,age值为20

  • 结论:对于简单参数来讲,请求参数名和controller方法中的形参名不一致时,无法接收到请求数据

那么如果我们开发中,遇到了这种请求参数名和controller方法中的形参名不相同,怎么办?

解决方案
:可以使用Spring提供的@RequestParam注解完成映射

在方法形参前面加上 @RequestParam 然后通过value属性执行请求参数名,从而完成映射。代码如下:

@RestController
public class RequestController {
    // http://localhost:8080/simpleParam?name=Tom&age=20
    // 请求参数名:name
​
    //springboot方式
    @RequestMapping("/simpleParam")
    public String simpleParam(@RequestParam("name") String username , Integer age ){
        System.out.println(username+"  :  "+age);
        return "OK";
    }
}

注意事项:

@RequestParam中的required属性默认为true(默认值也是true),代表该请求参数必须传递,如果不传递将报错

如果该参数是可选的,可以将required属性设置为false

@RequestMapping("/simpleParam")
public String simpleParam(@RequestParam(name = "name", required = false) String username, Integer age){
System.out.println(username+ ":" + age);
return "OK";
}

1.2实体参数

在使用简单参数做为数据传递方式时,前端传递了多少个请求参数,后端controller方法中的形参就要书写多少个。如果请求参数比较多,通过上述的方式一个参数一个参数的接收,会比较繁琐。

此时,我们可以考虑将请求参数封装到一个实体类对象中。 要想完成数据封装,需要遵守如下规则:
请求参数名与实体类的属性名相同

1.2.1简单实体类对象

定义POJO实体类:

public class User {
    private String name;
    private Integer age;
​
    public String getName() {
        return name;
    }
​
    public void setName(String name) {
        this.name = name;
    }
​
    public Integer getAge() {
        return age;
    }
​
    public void setAge(Integer age) {
        this.age = age;
    }
​
    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}
​

Controller方法

@RestController
public class RequestController {
    //实体参数:简单实体对象
    @RequestMapping("/simplePojo")
    public String simplePojo(User user){
        System.out.println(user);
        return "OK";
    }
}

1.2.2复杂实体类对象

上面我们讲的呢是简单的实体对象,下面我们在来学习下复杂的实体对象。

复杂实体对象指的是,在实体类中有一个或多个属性,也是实体对象类型的。如下:

  • User类中有一个Address类型的属性(Address是一个实体类)

复杂实体对象的封装,需要遵守如下规则:

  • 请求参数名与形参对象属性名相同,按照对象层次结构关系即可接收嵌套实体类属性参数。

定义POJO实体类:

  • Address实体类

public class Address {
    private String province;
    private String city;
​
    public String getProvince() {
        return province;
    }
​
    public void setProvince(String province) {
        this.province = province;
    }
​
    public String getCity() {
        return city;
    }
​
    public void setCity(String city) {
        this.city = city;
    }
​
    @Override
    public String toString() {
        return "Address{" +
                "province='" + province + '\'' +
                ", city='" + city + '\'' +
                '}';
    }
}
  • User实体类

public class User {
    private String name;
    private Integer age;
    private Address address; //地址对象
​
    public String getName() {
        return name;
    }
​
    public void setName(String name) {
        this.name = name;
    }
​
    public Integer getAge() {
        return age;
    }
​
    public void setAge(Integer age) {
        this.age = age;
    }
​
    public Address getAddress() {
        return address;
    }
​
    public void setAddress(Address address) {
        this.address = address;
    }
​
    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", address=" + address +
                '}';
    }
}

Controller方法:

@RestController
public class RequestController {
    //实体参数:复杂实体对象
    @RequestMapping("/complexPojo")
    public String complexPojo(User user){
        System.out.println(user);
        return "OK";
    }
}

Postman测试:

1.3数组集合参数

数组集合参数的使用场景:在HTML的表单中,有一个表单项是支持多选的(复选框),可以提交选择的多个值。

后端程序接收上述多个值的方式有两种:

  1. 数组

  2. 集合

1.3.1数组

数组参数:
请求参数名与形参数组名称相同且请求参数为多个,定义数组类型形参即可接收参数

Controller方法:

@RestController
public class RequestController {
    //数组集合参数
    @RequestMapping("/arrayParam")
    public String arrayParam(String[] hobby){
        System.out.println(Arrays.toString(hobby));
        return "OK";
    }
}

Postman测试:

在前端请求时,有两种传递形式:

方式一:
http://localhost:8080/arrayParam?hobby=game&hobby=java

方式二:
http://localhost:8080/arrayParam?hobby=game,java

1.3.2集合

集合参数:
请求参数名与形参集合对象名相同且请求参数为多个,@RequestParam 绑定参数关系

默认情况下,请求中参数名相同的多个值,是封装到数组。如果要封装到集合,要使用@RequestParam绑定参数关系

Controller方法:

@RestController
public class RequestController {
    //数组集合参数
    @RequestMapping("/listParam")
    public String listParam(@RequestParam List<String> hobby){
        System.out.println(hobby);
        return "OK";
    }
}

Postman测试:

方式一:
http://localhost:8080/listParam?hobby=game&hobby=java

方式二:
http://localhost:8080/listParam?hobby=game,java

1.4日期函数

上述演示的都是一些普通的参数,在一些特殊的需求中,可能会涉及到日期类型数据的封装。比如,如下需求:

因为日期的格式多种多样(如:2022-12-12 10:05:45 、2022/12/12 10:05:45),那么对于日期类型的参数在进行封装的时候,需要通过@DateTimeFormat注解,以及其pattern属性来设置日期的格式。

  • @DateTimeFormat注解的pattern属性中指定了哪种日期格式,前端的日期参数就必须按照指定的格式传递。

  • 后端controller方法中,需要使用Date类型或LocalDateTime类型,来封装传递的参数。

Controller方法:

@RestController
public class RequestController {
    //日期时间参数
   @RequestMapping("/dateParam")
    public String dateParam(@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime updateTime){
        System.out.println(updateTime);
        return "OK";
    }
}

1.5JSON参数

如果是比较复杂的参数,前后端通过会使用JSON格式的数据进行传输。 (JSON是开发中最常用的前后端数据交互方式)

我们学习JSON格式参数,主要从以下两个方面着手:

  1. Postman在发送请求时,如何传递json格式的请求参数

  2. 在服务端的controller方法中,如何接收json格式的请求参数

Postman发送JSON格式数据:

服务端Controller方法接收JSON格式数据:

  • 传递json格式的参数,在Controller中会使用实体类进行封装。

  • 封装规则:
    JSON数据键名与形参对象属性名相同,定义POJO类型形参即可接收参数。需要使用 @RequestBody标识。

  • @RequestBody注解:将JSON数据映射到形参的实体类对象中(JSON中的key和实体类中的属性名保持一致)

实体类:Address

public class Address {
    private String province;
    private String city;
    
    //省略GET , SET 方法
}

实体类:User

public class User {
    private String name;
    private Integer age;
    private Address address;
    
    //省略GET , SET 方法
}    

Controller方法:

@RestController
public class RequestController {
    //JSON参数
    @RequestMapping("/jsonParam")
    public String jsonParam(@RequestBody User user){
        System.out.println(user);
        return "OK";
    }
}

Postman测试:

1.6路径参数

传统的开发中请求参数是放在请求体(POST请求)传递或跟在URL后面通过?key=value的形式传递(GET请求)。

在现在的开发中,经常还会直接在请求的URL中传递参数。例如:

http://localhost:8080/user/1        
http://localhost:880/user/1/0

上述的这种传递请求参数的形式呢,我们称之为:路径参数。

学习路径参数呢,主要掌握在后端的controller方法中,如何接收路径参数。

路径参数:

  • 前端:通过请求URL直接传递参数

  • 后端:使用{…}来标识该路径参数,需要使用@PathVariable获取路径参数

Controller方法:

@RestController
public class RequestController {
    //路径参数
    @RequestMapping("/path/{id}")
    public String pathParam(@PathVariable Integer id){
        System.out.println(id);
        return "OK";
    }
}

传递多个路径参数:

Controller方法:

@RestController
public class RequestController {
    //路径参数
    @RequestMapping("/path/{id}/{name}")
    public String pathParam2(@PathVariable Integer id, @PathVariable String name){
        System.out.println(id+ " : " +name);
        return "OK";
    }
}

2.基于SpringBoot响应

响应
(HttpServletReponse):设置响应数据

服务器根据浏览器发送的请求,放回数据到浏览器在网页上进行显示的过程就叫响应。

2.1@ResponseBody

controller方法中的return的结果,怎么就可以响应给浏览器呢?

答案:使用@ResponseBody注解

@ResponseBody注解:

  • 类型:方法注解、类注解

  • 位置:书写在Controller方法上或类上

  • 作用:将方法返回值直接响应给浏览器


    • 如果返回值类型是实体对象/集合,将会转换为JSON格式后在响应给浏览器

但是在我们所书写的Controller中,只在类上添加了@RestController注解、方法添加了@RequestMapping注解,并没有使用@ResponseBody注解,怎么给浏览器响应呢?

@RestController
public class HelloController {
    @RequestMapping("/hello")
    public String hello(){
        System.out.println("Hello World ~");
        return "Hello World ~";
    }
}

原因:在类上添加的@RestController注解,是一个组合注解。

  • @RestController = @Controller + @ResponseBody

@RestController源码:

@Target({ElementType.TYPE})   //元注解(修饰注解的注解)
@Retention(RetentionPolicy.RUNTIME)  //元注解
@Documented    //元注解
@Controller   
@ResponseBody 
public @interface RestController {
    @AliasFor(
        annotation = Controller.class
    )
    String value() default "";
}

结论:在类上添加@RestController就相当于添加了@ResponseBody注解。

  • 类上有@RestController注解或@ResponseBody注解时:表示当前类下所有的方法返回值做为响应数据


    • 方法的返回值,如果是一个POJO对象或集合时,会先转换为JSON格式,在响应给浏览器

下面我们来测试下响应数据:

@RestController
public class ResponseController {
    //响应字符串
    @RequestMapping("/hello")
    public String hello(){
        System.out.println("Hello World ~");
        return "Hello World ~";
    }
    //响应实体对象
    @RequestMapping("/getAddr")
    public Address getAddr(){
        Address addr = new Address();//创建实体类对象
        addr.setProvince("广东");
        addr.setCity("深圳");
        return addr;
    }
    //响应集合数据
    @RequestMapping("/listAddr")
    public List<Address> listAddr(){
        List<Address> list = new ArrayList<>();//集合对象
        
        Address addr = new Address();
        addr.setProvince("广东");
        addr.setCity("深圳");
​
        Address addr2 = new Address();
        addr2.setProvince("陕西");
        addr2.setCity("西安");
​
        list.add(addr);
        list.add(addr2);
        return list;
    }
}

在服务端响应了一个对象或者集合,那私前端获取到的数据是什么样子的呢?我们使用postman发送请求来测试下。测试效果如下:

2.2统一响应结果

大家有没有发现一个问题,我们在前面所编写的这些Controller方法中,返回值各种各样,没有任何的规范。

如果我们开发一个大型项目,项目中controller方法将成千上万,使用上述方式将造成整个项目难以维护。那在真实的项目开发中是什么样子的呢?

在真实的项目开发中,无论是哪种方法,我们都会定义一个统一的返回结果。方案如下:

前端:只需要按照统一格式的返回结果进行解析(仅一种解析方案),就可以拿到数据。

统一的返回结果使用类来描述,在这个结果中包含:

  • 响应状态码:当前请求是成功,还是失败

  • 状态码信息:给页面的提示信息

  • 返回的数据:给前端响应的数据(字符串、对象、集合)

定义在一个实体类Result来包含以上信息。代码如下:

public class Result {
    private Integer code;//响应码,1 代表成功; 0 代表失败
    private String msg;  //响应码 描述字符串
    private Object data; //返回的数据
public Result() { }
public Result(Integer code, String msg, Object data) {
    this.code = code;
    this.msg = msg;
    this.data = data;
}
​
public Integer getCode() {
    return code;
}
​
public void setCode(Integer code) {
    this.code = code;
}
​
public String getMsg() {
    return msg;
}
​
public void setMsg(String msg) {
    this.msg = msg;
}
​
public Object getData() {
    return data;
}
​
public void setData(Object data) {
    this.data = data;
}
​
//增删改 成功响应(不需要给前端返回数据)
public static Result success(){
    return new Result(1,"success",null);
}
//查询 成功响应(把查询结果做为返回数据响应给前端)
public static Result success(Object data){
    return new Result(1,"success",data);
}
//失败响应
public static Result error(String msg){
    return new Result(0,msg,null);
}
}

改造Controller:

@RestController
public class ResponseController { 
    //响应统一格式的结果
    @RequestMapping("/hello")
    public Result hello(){
        System.out.println("Hello World ~");
        //return new Result(1,"success","Hello World ~");
        return Result.success("Hello World ~");
    }
​
    //响应统一格式的结果
    @RequestMapping("/getAddr")
    public Result getAddr(){
        Address addr = new Address();
        addr.setProvince("广东");
        addr.setCity("深圳");
        return Result.success(addr);
    }
​
    //响应统一格式的结果
    @RequestMapping("/listAddr")
    public Result listAddr(){
        List<Address> list = new ArrayList<>();
​
        Address addr = new Address();
        addr.setProvince("广东");
        addr.setCity("深圳");
​
        Address addr2 = new Address();
        addr2.setProvince("陕西");
        addr2.setCity("西安");
​
        list.add(addr);
        list.add(addr2);
        return Result.success(list);
    }
}

使用Postman测试:

本文介绍在
ArcMap
软件中,通过已有的
Python
脚本程序,建立新的
工具箱
并在其中设置
自定义工具
的方法。

通过本文介绍的操作,我们便可以实现将自己的
Python
代码封装,并像其他
ArcGIS
已有工具一样,进行界面化的地理处理操作。

话不多说,我们开始具体操作。首先,在
ArcMap
中打开“
ArcToolbox
”。

接下来,在弹出的窗口中空白处右键,选择“
Add Toolbox
”。

在弹出的窗口中,选择右上角的“
New Toolbox
”,即可在下方列表中看到刚刚新建立的、以
.tbx
为后缀的工具箱。我这里不小心点了两次“
New Toolbox
”,所以下方列表出现了两个新的工具箱。

此时,在“
ArcToolbox
”中也可以看到自己刚刚建立的工具箱。

在“
ArcToolbox
”中,右键刚刚建立的工具箱,选择“
Rename
”,即可对其加以重命名。

重命名完毕后,同样右键该工具箱,选择“
Properties
”,可以对其更多属性信息加以调整。

同时,在这里还可以修改该工具箱的别名与描述文本。

上述描述文本主要填写对这一工具箱的介绍即可。配置完毕描述文本后,我们在工具箱右键,并选择“
Item Description
”。

即可在其中看到我们刚刚配置的描述文本。

接下来,我们开始将已有的
Python
代码脚本导入工具箱,从而将其作为一个自定义工具。

在工具箱上右键,选择“
Add
”→“
Script
”。

在弹出的窗口中配置该脚本成为工具箱里的一个自定义工具后,该工具的名称、标记、描述信息等。

在这里需要注意,脚本所对应的工具名称不能含有空格或下划线;我上图所示的工具名称就带了下划线,需要修改。

修改后,找到对应的
Python
程序脚本文件,并将其导入。

导入完毕后,开始按照脚本文件中变量的数量、要求等进行工具参数的设置。

其中,可以在“
Default
”中配置某一参数的默认数值;在“
Filter
”中设置某一参数在导入时的过滤方法。

例如,假如某个参数是文件类型,那么就可以在“
Filter
”中通过文件后缀名来对这一参数加以限制。我们以
.xls
格式的文件为例,在其中输入该格式的后缀名。

点击确定后,打开刚刚建立的这一自定义工具。

点击这一参数所对应的项。

可以看到,这里就仅支持选择
.xls
格式的文件。

如果相对多种文件格式加以约束,可以用英文分号分隔,在文件格式过滤栏中输入多个文件后缀名,例如
xls;xlsx

可以看到,此时就可以对上述两种文件格式加以选择了。

此外,还可以对不同参数“是否为必填项”这一属性加以修改。比如我们将第一个参数修改为非必填,且设置一个默认选项。

打开自定义工具,可以看到这一参数不带有绿色的“必填”标识,且具有一个默认值。

至此,大功告成。

.net core 自定义授权策略提供程序进行权限验证

在这之前先了解一下鉴权和授权的概念;

鉴权

鉴权可以说是身份验证,身份验证是确定用户身份的过程;

在ASP.NET Core 中身份验证是由身份验证服务IAuthenticationService负责的,它被身份验证中间件使用, 身份验证服务会使用已注册的身份验证处理程序来完成与身份验证相关的操作。身份验证相关的操作包括:对用户身份进行验证,对未经身份验证的用户进行资源访问时做出响应。

身份验证处理程序及其配置选项

身份验证处理程序包括CookieAuthenticationHandler 和 JwtBearerHandler,身份验证处理程序的注册 是在调用AddAuthentication之后扩展方法AddJwtBearer 和 AddCookie 提供的

身份验证处理程序会由实现IAuthenticationService 接口的AuthenticationService 的AuthenticateAsync 方法去调用

授权

授权是确定用户是否有权访问资源的过程,这里先简单带过一下后面接着讲

授权方案

授权方案包括 基于角色的授权,基于声明的授权,基于策略的授权,这里着重说一下策略授权,

基于策略的授权

授权策略包含一个或多个要求

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("AtLeast21", policy =>
        policy.Requirements.Add(new MinimumAgeRequirement(21)));
});

在前面的示例中,创建了“AtLeast21”策略。 该策略有一个最低年龄要求,其作为要求的参数提供

IAuthorizationRequirement

IAuthorizationRequirement用于跟踪授权是否成功的机制

在IAuthorizationHandler 的 HandleAsync方法中 作为参数被调用,由HttpContext 的Requirements 属性提供

IAuthorizationHandler

IAuthorizationHandler 用于检查策略是否满足要求,主要执行的方法是HandleAsync,我们可以继承微软提供的AuthorizationHandler,默认实现了HandlAsync ,在具有多个IAuthorizationRequirement 的情况下默认是循环去执行HandleRequirementAsync方法,在某些情况下我们可以去重写从而去执行特定IAuthorizationRequirement,当然方法多样

        public virtual async Task HandleAsync(AuthorizationHandlerContext context)
        {
            if (context.Resource is TResource)
            {
                foreach (var req in context.Requirements.OfType<TRequirement>())
                {
                    await HandleRequirementAsync(context, req, (TResource)context.Resource);
                }
            }
        }
IAuthorizationPolicyProvider

IAuthorizationPolicyProvider 自定义策略提供程序

继承IAuthorizationPolicyProvider 需要去实现IAuthorizationPolicyProvider 三个方法,三个方法的执行由我们我们的Authorize特性决定,并且Authorize特性的策略名称会传递到 IAuthorizationPolicyProvider 的GetPolicyAsync 作为参数使用

        public Task<AuthorizationPolicy?> GetPolicyAsync(string policyName)
        {
            var policy = new AuthorizationPolicyBuilder();
            //添加鉴权方案
            policy.AddAuthenticationSchemes("Bearer");
            policy.AddAuthenticationSchemes(CookieAuthenticationDefaults.AuthenticationScheme);
            if (policyName is null)
            {
                return Task.FromResult<AuthorizationPolicy>(null);
            }
            var authorizations = policyName.Split(',');
            if (authorizations.Any())
            {
                //权限策略构建器,添加自定义的AuthorizaRequirement
                policy.AddRequirements(new AuthorizeRequirement(authorizations));
            }
            return Task.FromResult(policy.Build());
        }

这里看一下Authorize特性相关的属性

    public class AuthorizeAttribute : Attribute, IAuthorizeData
    {
        /// <summary>
        /// </summary>
        public AuthorizeAttribute() { }

        /// <summary>
        /// 初始化类类实例并且设置可访问资源策略名称
        /// </summary>
        /// <param name="policy">The name of the policy to require for authorization.</param>
        public AuthorizeAttribute(string policy)
        {
            Policy = policy;
        }
        /// <summary>
        /// 设置或获取可以访问资源策略名称
        /// </summary>
        public string? Policy { get; set; }

        /// <summary>
        /// 设置或获取可以访问资源的角色
        /// </summary>
        public string? Roles { get; set; }

        /// <summary>
        /// 设置或获取可以访问资源鉴权方案名称
        /// </summary>
        public string? AuthenticationSchemes { get; set; }
    }
GetDefaultPolicyAsync

默认策略,当我们的Authorize特性上不提供策略时执行,这里的CustomAuthorization特性是继承了Authorize特性

        [CustomAuthorization]
        [HttpGet("SetNotOP")]
        [NoResult]
        public int SetNotOP()
        {
            throw new ArgumentNullException(nameof(TestTask));
            return 1;
        }
GetPolicyAsync

在Authorize添加策略时执行

        [CustomAuthorization("test1", "test1")]
        [HttpGet("TestTask")]
        public async Task<int> TestTask()
        {
            await Task.CompletedTask;
            return 1;
        }
GetFallbackPolicyAsync

后备授权策略,是指在没有为请求指定其他策略时,由授权中间件提供的策略。在这里可以指返回空值,也可以设置指定策略返回

    public Task<AuthorizationPolicy?> GetFallbackPolicyAsync()
    {
        return Task.FromResult<AuthorizationPolicy>(null);
    }
    //或者
    public Task<AuthorizationPolicy?> GetFallbackPolicyAsync()
    {
        var policy = new AuthorizationPolicyBuilder();
        policy.AddAuthenticationSchemes("Bearer");
        policy.AddAuthenticationSchemes(CookieAuthenticationDefaults.AuthenticationScheme);
        return Task.FromResult<AuthorizationPolicy>(policy.Build());
    }
IAuthorizationService

IAuthorizationService是确认授权成功与否的主要服务,兵器 负责去执行我们自定义的AuthorizationHandle

看一段由微软官方简化授权服务的代码,可以看到AuthorizeAsync会去循环执行自定义的AuthorizationHandle

public async Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, 
             object resource, IEnumerable<IAuthorizationRequirement> requirements)
{
    // Create a tracking context from the authorization inputs.
    var authContext = _contextFactory.CreateContext(requirements, user, resource);

    // By default this returns an IEnumerable<IAuthorizationHandlers> from DI.
    var handlers = await _handlers.GetHandlersAsync(authContext);

    // Invoke all handlers.
    foreach (var handler in handlers)
    {
        await handler.HandleAsync(authContext);
    }

    // Check the context, by default success is when all requirements have been met.
    return _evaluator.Evaluate(authContext);
}

到这里应该对整个授权流程有了个大致的了解,在授权前会由鉴权中间件进行一个鉴权,鉴权通过后由IAuthorizationPolicyProvider 来提供一个授权策略(授权策略里可以添加我们需要的IAuthorizationRequirement),最后由IAuthorizationService 的HandleAsync去执行自定义AuthorizeHandle

具体实现

自定义特性
    public class CustomAuthorizationAttribute : AuthorizeAttribute
    {
        public virtual string[] AuthorizeName { get; private set; }

        public CustomAuthorizationAttribute(params string[] authorizeName)
        {
            AuthorizeName = authorizeName;
            Policy = string.Join(",", AuthorizeName);
        }
    }
自定义策略提供程序
    public class AuthorizationProvider : IAuthorizationPolicyProvider
    {
        public Task<AuthorizationPolicy> GetDefaultPolicyAsync()
        {
            var policy = new AuthorizationPolicyBuilder();
            policy.AddAuthenticationSchemes("Bearer");
            policy.AddAuthenticationSchemes(CookieAuthenticationDefaults.AuthenticationScheme);
            //默认授权测率必须添加一个IAuthorizationRequirement的实现
            policy.AddRequirements(new AuthorizeRequirement());
            return Task.FromResult<AuthorizationPolicy>(policy.Build());
        }

        public Task<AuthorizationPolicy?> GetFallbackPolicyAsync()
        {
            return Task.FromResult<AuthorizationPolicy>(null);
        }

        public Task<AuthorizationPolicy?> GetPolicyAsync(string policyName)
        {
            var policy = new AuthorizationPolicyBuilder();
            policy.AddAuthenticationSchemes("Bearer");
            policy.AddAuthenticationSchemes(CookieAuthenticationDefaults.AuthenticationScheme);
            if (policyName is null)
            {
                return Task.FromResult<AuthorizationPolicy>(null);
            }
            var authorizations = policyName.Split(',');
            if (authorizations.Any())
            {
                policy.AddRequirements(new AuthorizeRequirement(authorizations));
            }
            return Task.FromResult(policy.Build());
        }
    }
自定义授权处理程序

IPermissionsCheck 是我注入的权限检测程序,其实对于权限认证,重要的是控制对资源的访问,整篇文章下来无非就是将特性上的值提供到我们所需要进行权限检测的程序中去,当然我们也可以用权限过滤器反射获取Authorize特性上的值来实现

public class AuthorizeHandler : AuthorizationHandler<AuthorizeRequirement>
{
    private readonly IPermissionCheck _permisscheck;
    private readonly IHttpContextAccessor _httpContextAccessor;

    public AuthorizeHandler(IHttpContextAccessor httpContextAccessor
        , IServiceProvider serviceProvider)
    {
        using var scope = serviceProvider.CreateScope();
        _permisscheck = scope.ServiceProvider.GetRequiredService<IPermissionCheck>();
        _httpContextAccessor = httpContextAccessor;
    }

    protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, AuthorizeRequirement requirement)
    {
        var identity = _httpContextAccessor?.HttpContext?.User?.Identity;
        var httpContext = _httpContextAccessor?.HttpContext;
        var isAuthenticated = identity?.IsAuthenticated ?? false;
        var claims = _httpContextAccessor?.HttpContext?.User?.Claims;
        var userId = claims?.FirstOrDefault(p => p.Type == "Id")?.Value;
        //判断是否通过鉴权中间件--是否登录
        if (userId is null || !isAuthenticated)
        {
            context.Fail();
            return;
        }
        var defaultPolicy = requirement.AuthorizeName?.Any() ?? false;
        //默认授权策略
        if (!defaultPolicy)
        {
            context.Succeed(requirement);
            return;
        }
        var roleIds = claims?
            .Where(p => p?.Type?.Equals("RoleIds") ?? false)
            .Select(p => long.Parse(p.Value));
        var roleNames = claims?
            .Where(p => p?.Type?.Equals(ClaimTypes.Role) ?? false)
            .Select(p => p.Value);
        UserTokenModel tokenModel = new UserTokenModel()
        {
            UserId = long.Parse(userId ?? "0"),
            UserName = claims?.FirstOrDefault(p => p.Type == ClaimTypes.Name)?.Value ?? "",
            RoleNames = roleNames?.ToArray(),
            RoleIds = roleIds?.ToArray(),
        };
        if (requirement.AuthorizeName.Any())
        {
            if (!_permisscheck.IsGranted(tokenModel, requirement.AuthorizeName))
            {
                context.Fail();
                return;
            }
        }
        context.Succeed(requirement);
    }
}
自定义IAuthorizationRequirement
public class AuthorizeRequirement : IAuthorizationRequirement
{
    public virtual string[] AuthorizeName { get; private set; }
    public AuthorizeRequirement(params string[] authorizeName)
    {
        AuthorizeName = authorizeName;
    }

    public AuthorizeRequirement() { }
}
自定义授权结果中间件

自定义授权结果中间件的作用,返回自定义响应,增强默认质询或禁止响应

    public class AuthorizeMiddleHandle : IAuthorizationMiddlewareResultHandler
    {
        public async Task HandleAsync(RequestDelegate next, HttpContext context, AuthorizationPolicy                                               policy, PolicyAuthorizationResult authorizeResult)
        {
            if (!authorizeResult.Succeeded || authorizeResult.Challenged)
            {
                var isLogin = context?.User?.Identity?.IsAuthenticated ?? false;
                var path = context?.Request?.Path ?? "";
                context.Response.StatusCode = StatusCodes.Status401Unauthorized;
                var response = new AjaxResponse();
                response.UnAuthorizedRequest = true;
                response.StatusCode = "401";
                var error = new ErrorInfo();
                error.Error = isLogin ? $"你没有权限访问该接口-接口路由{path}" : "请先登录系统";
                response.Error = error;
                await context.Response.WriteAsJsonAsync(response);
                return;
            }
            await next(context);
        }
    }
相关服务的注册
            context.Services.AddScoped<IPermissionCheck, PermissionCheck>();
            context.Services.AddSingleton<IAuthorizationPolicyProvider, AuthorizationProvider>();
            context.Services.AddSingleton<IAuthorizationMiddlewareResultHandler, AuthorizeMiddleHandle>();
            context.Services.AddSingleton<IAuthorizationHandler, AuthorizeHandler>();

到这里对于权限认证有了个大概的了解,至于是通过自定义策略提供程序自定义AuthorizHandle这一系列复杂的操作还是通过权限过滤器取决看官自己。个人认为通过自定义策略提供程序自定义AuthorizHandle这种方式更灵活性,能够应对更多复杂场景。