省钱的开源项目「GitHub 热点速览」 - 削微寒 - 博客园

mikel阅读(165)

来源: 省钱的开源项目「GitHub 热点速览」 – 削微寒 – 博客园

本期,我从上周的热门开源项目中挑选了 5 个既省钱又省事,还好玩的开源项目。

首先,推荐的是省钱的电动汽车智能充电管理平台 evcc,它可以根据分时电价智能安排电动车充电时间,从而降低电费,如果你家还有太阳能充电和储能设备,evcc 更能最大限度地利用电能,让你的充电成本降到最低。说到省事,开源的 PaaS 平台 dokku,它可以让你轻松搭建一个类似 Heroku 的平台,又能帮你省下一笔可观的费用。再来看看在线 AI 证件照制作工具 HivisionIDPhotos,这款工具让你无需再花钱拍摄证件“大头照”,随时在线生成专业证件照,方便又实惠。

除此之外,还有两款特别好玩的开源项目。《暗黑破坏神》网页版 diabloweb,让你无需安装游戏就能在浏览器里重温经典;以及 SQLpage,通过 SQL 命令就能创建简单的网页,让你无需编写前端代码,同样省时省力。

  • 本文目录
    • 1. 开源热搜项目
      • 1.1 电动汽车智能充电管理平台:evcc
      • 1.2 基于 Docker 的开源 PaaS 平台:dokku
      • 1.3 一条 SQL 生成网页的工具:SQLpage
      • 1.4 在浏览器里玩《暗黑破坏神》:diabloweb
      • 1.5 轻量级的 AI 证件照制作工具:HivisionIDPhotos
    • 2. HelloGitHub 热评
      • 2.1 强大的终端日志文件查看工具:lnav
      • 2.2 游戏修改器管理工具:Game-Cheats-Manager
    • 3. 结尾

1. 开源热搜项目

1.1 电动汽车智能充电管理平台:evcc

主语言:GoStar:3.1k周增长:200

这是一个开源的 EV(电动汽车)充电器控制平台,为电动汽车车主提供灵活且易于安装的充电解决方案。它提供了可视化且适配移动端的 Web 平台,用户可以通过该平台远程启动、停止和监控车辆的充电状态。智能充电功能还可以根据电价、太阳储能和日程安排,智能安排充电时间,从而节约电费。平台支持多种充电设备和车辆型号,为家庭充电桩提供更加智能的控制。

GitHub 地址→github.com/evcc-io/evcc

1.2 基于 Docker 的开源 PaaS 平台:dokku

主语言:GoStar:28k周增长:1600

该项目是一款开源的 PaaS(平台即服务)平台,利用 Docker 容器进行应用程序的部署和运行。它提供了类似 Heroku 的命令行工具,让部署和管理应用程序的变得更加方便,支持从 Git 仓库自动部署应用、同时管理多个应用以及丰富的插件系统,可作为 Heroku 的开源替代品。

GitHub 地址→github.com/dokku/dokku

1.3 一条 SQL 生成网页的工具:SQLpage

主语言:RustStar:1.4k周增长:100

该项目是用 Rust 编写的基于 SQL 的 Web 应用构建工具。用户只需编写 SQL,就能自动生成可交互的 Web 界面,无需使用其他编程语言,让数据科学家和分析师能够方便地展示 SQL 的查询结果,是一种快速、简单的数据可视化方案,支持多种主流数据库。

GitHub 地址→github.com/lovasoa/SQLpage

1.4 在浏览器里玩《暗黑破坏神》:diabloweb

主语言:JavaScriptStar:2.6k周增长:300

该项目是将经典的动作角色扮演游戏《Diablo 1》(暗黑破坏神)移植到浏览器的开源项目。它通过 WebAssembly 技术,让用户无需安装即可以在浏览器重温这款经典游戏。

GitHub 地址→github.com/d07RiV/diabloweb

1.5 轻量级的 AI 证件照制作工具:HivisionIDPhotos

主语言:PythonStar:1.9k周增长:1.5k

这是一款简单易用的 AI 证件照制作工具,能够生成标准证件照和六寸排版照。它提供了简洁的 Web 界面和 API 服务,即使在没有 GPU 的电脑上也能够运行,支持抠图、尺寸调整和自定义底色等功能。

GitHub 地址→github.com/Zeyi-Lin/HivisionIDPhotos

2. HelloGitHub 热评

在本章节中,我们将为大家介绍本周 HelloGitHub 网站上的热门开源项目。同时,期待您与我们分享使用这些开源项目的心得与体验。

2.1 强大的终端日志文件查看工具:lnav

主语言:C++

这是一款用于查看和分析日志文件的轻量级工具。它无需配置、开箱即用,可自动识别日志格式并解压文件,支持同时处理多个文件和目录、实时更新、文本高亮、正则与 SQL 过滤日志等功能,特别适合在服务器和开发环境中使用。

项目详情→hellogithub.com/repository/f771d96afed44371b9c805d3cc8954c6

2.2 游戏修改器管理工具:Game-Cheats-Manager

主语言:Python

这是一款强大的游戏修改器管理工具,支持搜索、下载、启动、导入和更新游戏修改器等功能。

项目详情→hellogithub.com/repository/3ca6e8e23401477282ba72d2d8932311

3. 结尾

以上就是本期「GitHub 热点速览」的全部内容,希望你能够在这里找到自己感兴趣的开源项目,如果你有其他好玩、有趣的 GitHub 开源项目想要分享,欢迎来 HelloGitHub 与我们交流和讨论。

员工考勤打卡时,如何避免非本人代替打卡? - 华为云开发者联盟 - 博客园

mikel阅读(171)

来源: 员工考勤打卡时,如何避免非本人代替打卡? – 华为云开发者联盟 – 博客园

本文分享自华为云社区《员工考勤打卡时,如何避免非本人代替打卡?》,作者: HuaweiCloudDeveloper 。

1、背景

使用APP进行打卡时,为避免非本人及非真人现场打卡的情况出现,想结合华为云的人脸识别能力,通过调用API,达成可检测是否本人且真人现场打卡的效果。

2、云服务介绍

华为云FRS:人脸识别服务(Face Recognition Service),能够在图像中快速检测人脸、分析人脸关键点信息、获取人脸属性、实现人脸的精确比对和检索。该服务可应用于身份验证、电子考勤、客流分析等场景。

华为云FunctionGraph:函数工作流(FunctionGraph)是一项基于事件驱动的函数托管计算服务。通过函数工作流,只需编写业务函数代码并设置运行的条件,无需配置和管理服务器等基础设施,函数以弹性、免运维、高可靠的方式运行。

华为云APIG:API网关(API Gateway)是为企业开发者及合作伙伴提供的高性能、高可用、高安全的API托管服务, 帮助企业轻松构建、管理和部署不同规模的API。简单、快速、低成本、低风险的实现内部系统集成、成熟业务能力开放及业务能力变现。

华为云OBS: 对象存储服务(Object Storage Service,OBS)是一个基于对象的海量存储服务,为客户提供海量、安全、高可靠、低成本的数据存储能力,使用时无需考虑容量限制,并且提供多种存储类型供选择,满足客户各类业务场景诉求。

华为云DNS:云解析服务(Domain Name Service)提供高可用,高扩展的权威DNS服务和DNS管理服务,把人们常用的域名或应用资源转换成用于计算机连接的IP地址,从而将最终用户路由到相应的应用资源上。此服务默认开通,免费使用。

3 、方案设计

3.1 方案简述

通过APIG调用functiongraph函数,在functiongraph上完成人脸识别-活体检测、人脸识别-人脸比对等API的调用,并将响应结果通过API返回给APP。实现APP调用一次API即可完成人脸识别的功能。

  1. 人脸识别服务的人脸比对功能,可实现检测是否其本人打卡。
  2. 人脸识别服务的活体检测功能,可实现检测是否活人打卡。
  3. 使用Functiongraph的函数,APP端只需考虑调用一个API,且只需考虑人脸识别的总体输入和返回结果。
  4. Functiongraph由APIG来调用,利用APPkey、APPsecret及HTTPS,解决了APP端调用的安全认证等问题。
  5. OBS桶用来存储人脸照片,通过约定的用户标识做文件存储路径,易于管理和使用。

3.2 方案架构图

0、初始化:前置准备工作,新建一个OBS桶做人脸库,将员工的人脸照片存放到人脸库,并把以user-id 或自定义字段作为路径,标识员工。

1、员工登录APP后,进行人脸识别时,将通过APIG来调用functionGraph,上传关键信息:摄像头捕获的照片或视频、员工的人脸库标识(user-id)。

2、functionGraph调用活体检测API,传入照片/视频(根据需求选择动作活体检测/静默活体检测,推荐静默活体检测)。

注:本方案采用静默活体检测方式,APP端上传的照片提前转换为base64格式

3、活体检测API返回响应:有confidence、picture(base64)。

4、在functionGraph中调用OBS接口,通过user-id从OBS人脸库获取库中的员工照片。

5、通过代码将从OBS获取的照片文件,转换为base64格式。

6、将两个base64格式的照片作为输入参数调用人脸比对API 。(备注:此处可根据需求,是否需要多次调用API,使用多个照片进行验证)

7、人脸比对API返回包含了similarity的响应。

8、functionGraph将similarity、confidence传回给APP/后端。(备注:也可直接在functionGraph完成判定,返回人脸识别结果)

3.3 Functiongraph实现代码

代码附件:(附件请见文章最后)

代码时序图:

4、方案部署

4.1 部署流程图

4.2 前置准备

  • 拥有已实名认证的华为云账号,开通云服务functiongraph、人脸比对、活体检测、OBS
  • 注册公网域名,完成ICP备案

4.3、创建OBS人脸库

4.3.1 创建OBS桶

参考帮助文档:https://support.huaweicloud.com/qs-obs/obs_qs_0007.html创建私有桶

4.3.2 上传对象

参考帮助文档: https://support.huaweicloud.com/qs-obs/obs_qs_0008.html,上传对象,建立OBS人脸库。

要求:文件的路径使用用户标识(如userid)命名

4.4 Functiongraph搭建

4.4.1 创建委托

登录IAM控制台(https://console.huaweicloud.com/iam/?region=cn-north-4#/iam/agencies

1)创建委托

委托名称:自定义

委托类型:云服务

云服务:函数工作流functiongraph

持续时间:永久

2)选择策略

OBS:获取对象等基本操作权限

FRS:fullaccess

APIG:fullaccess

3)设置最小授权范围,此处选择所有,实际可根据项目情况分配。

4)完成委托创建

4.4.2 上传FRS依赖包

因functiongraph公共的依赖包中,FRS-SDK不是最新的(无静默活体检测API),故我们需上传最新的FRS-SDK,作为依赖包。

1)从官网获取FRS-SDK下载路径

https://sdkcenter.developer.huaweicloud.com/?language=python

2)下载整个Python-v3的SDK

https://github.com/huaweicloud/huaweicloud-sdk-python-v3

下载后解压,进入内部,找到frs后缀的SDK

进入SDK目录,在setup所在的目录,全部选择进行压缩。压缩成功后,需要确保setup文件在压缩包的根目录下

将压缩好的文件,上传到functiongraph的依赖包管理。

依赖包名称:自定义

运行时语言:2.7

描述:自定义

上传方式:上传ZIP文件

4.4.3 创建函数

1)进入functiongraph控制台创建函数。

Functiongraph版本:functiongraph v2

函数类型:事件函数

函数名称:自定义

所属应用:默认

委托名称:选择创建的委托(如无,请点击右边的 创建委托 前往创建,创建步骤参考4.4.1)

企业项目:自行选择

自定义函数:关闭

运行时语言:Python2.7

函数执行入口:Index.handler

代码上传方式:静默代码

4.4.4 编辑函数代码

将示例代码复制进来,编辑相关默认变量的值

1)粘贴3.3节的代码至index.py中

2)根据自己项目情况,设置默认Region、endpoint、buketname的值,若4.4.6节不设置环境变量的值,将默认取此处的默认值。如下默认是北京四

3)编辑完成后,点击保存

4.4.5 添加依赖包

1)在函数菜单-代码页,点击添加-依赖代码包

2)在公共依赖包,搜索obs,勾选OBS-sdk

3)在私有依赖包,勾选前面步骤上传的frs-sdk,然后确定保存

4.4.6 编辑环境变量

在函数菜单-配置页,添加环境变量:region、bucketname(OBS桶名)、endpoint

若此处不设置环境变量,则函数会使用4.4.4节代码设置的默认值。

4.4.7 调试函数

1)点击配置测试事件

2)选择apig的事件模板,添加body的内容和queryStringParameters的userid,进行保存。

3)点击测试,运行完毕可查看执行结果。

4.5 添加APIG

4.5.1 添加APIG触发器

1)在函数菜单-触发器页,点击创建触发器

触发器类型:API网关服务(APIG)

API名称:自定义

分组:选择API分组(如无点击右边 创建分组 进行创建)

发布环境:RELEASE(如无点击右边 创建发布环境 进行创建)

安全认证:测试环境可选择None(后面可编辑进行更改)

请求协议:测试环境可选择HTTP(后面可编辑进行更改)

后端超时(毫秒):5000

2)创建完成后,在触发器页面会添加一个APIG触发器,提供访问URL

4.5.2 编辑APIG

1)点击APIG触发器名称,前往APIG控制台,点击编辑

2)编辑基本信息

此处可更改安全认证,为方便调试,此处保持无认证

3)定义API请求

此处需添加API的请求参数-用户标识,用于functiongraph中,取在OBS人脸照片库中的员工照片。

4)定义后端服务

添加后端服务参数,跟前面的入参做一个映射。

因为是在functiongraph创建的APIG,故此处已自动绑定functiongraph的函数为后端服务,故基础定义保持默认即可。

5)返回结果基础定义

返回结果的响应示例,暂设置为空即可,点击完成。

4.5.3 调试API

1)API详情页,点击调试,跳转到API调试页面

2)输入相关请求参数,发起请求,进行调试。

4.5.4 发布API

编辑完成后的API,需要进行发布,公网才可访问

4.6 绑定独立域名

子域名仅供开发测试使用,每天最多访问1000次。如需开发服务,则需为API所在分组绑定独立域名。

4.6.1 添加记录集

1、登录云解析控制台(也可使用其他平台,已完成ICP备案的域名),选择域名解析》公网域名,点击需要创建记录集的域名名称。

2、添加记录集

填写以下信息

主机记录:域名前缀,如face-test

类型:选择CNAME – 将域名指向另外一个域名

别名:默认即可

线路类型:默认即可

TTL(秒):默认即可

值:填写要指向的别名(此处为APIG上的子域名)

添加成功

4.6.2 添加自定义域名

1、在API详情页-总览,点击添加增加自定义域名。

2、跳转到API所在分组的域名管理控制台,点击绑定独立域名

3、输入前面创建的记录集,点击确定。(如果是刚添加的记录集需刷新,约等5分钟)

添加完后,即可在公网通过自定义域名,访问APIG。

4.7 问题记录

1、并发测试API时,发现偶现以下错误

错误1:人脸比对传入的base64字符串无法识别

错误2:数据传输被提前终止了

问题定位:因函数中的存储到本地的文件用的是同一个路径,并发操作时出现异步的同时占用一个路径,从而导致文件有丢失或文件转码有误

解决方案:给函数中的文件路径配置上时间戳,避免并发操作时,交叉操作同一个文件。

2、APIG错误码请参考:https://support.huaweicloud.com/usermanual-apig/apig-ug-180530090.html

3、使用APIG触发functiongraph时,发现第一个api请求响应时间较长(2s多),后面的请求就较短了(约500ms)。

问题定位:超过一分钟无调用函数时,函数会销毁。再次进行函数调用时,需要重新启动实例,所以第一次调用时间会比较长。

解决方案:设置预留实例,来消除冷启动效果。预留实例是为指定函数版本单独预留的函数运行实例,不同于普通的函数实例,预留实例长期存活,可以达到消除函数冷启动的效果。

预留实例需要提交工单开通,详情请参考:https://support.huaweicloud.com/usermanual-functiongraph/functiongraph_01_0306.html

5、后期思考

本方案的人脸比对,只比对一次。若人脸库中,用户的库照片有多个,是否需要遍历对比,取总体对比的结果。如对比多次,需要考虑从OBS获取照片、人脸比对的API要多次调用,性能下降、费用提升等。

附件:index.zip2.62KB

NET Core 多身份校验与策略模式 - 无昵称老炮儿 - 博客园

mikel阅读(160)

来源: NET Core 多身份校验与策略模式 – 无昵称老炮儿 – 博客园

背景需求:

系统需要对接到XXX官方的API,但因此官方对接以及管理都十分严格。而本人部门的系统中包含诸多子系统,系统间为了稳定,程序间多数固定Token+特殊验证进行调用,且后期还要提供给其他兄弟部门系统共同调用。

原则上:每套系统都必须单独接入到官方,但官方的接入复杂,还要官方指定机构认证的证书等各种条件,此做法成本较大。

so:

为了解决对接的XXX官方API问题,我们搭建了一套中继系统,顾名思义:就是一套用于请求中转的中继系统。在系统搭建的时,Leader提出要做多套鉴权方案,必须做到 动静结合 身份鉴权。

动静结合:就是动态Token 和 静态固定Token。

动态Token:用于兄弟部门系统或对外访问到此中继系统申请的Token,供后期调用对应API。

固定Token:用于当前部门中的诸多子系统,提供一个超级Token,此Token长期有效,且不会随意更换。

入坑:

  因为刚来第一周我就接手了这个项目。项目处于申请账号阶段,即将进入开发。对接的是全英文文档(申请/对接流程/开发API….),文档复杂。当时我的感觉:OMG,这不得跑路?整个项目可谓难度之大。然后因为对内部业务也不熟悉,上手就看了微服务等相关系统代码,注:每套系统之间文档少的可怜,可以说系统无文档状态

项目移交的时候,Leader之说让我熟悉并逐渐进入开发,让我请教同事。好嘛,请教了同事。同事也是接了前任离职的文档而已,大家都不是很熟悉。于是同事让我启新的项目也是直接对接微服务形式开发,一顿操作猛如虎。

项目开发第二周,已经打出框架模型并对接了部分API。此时,Leader开会问进度,结果来一句:此项目使用独立API方式运行,部署到Docker,不接入公司的微服务架构。好嘛,几天功夫白费了,真是取其糟粕去其精华~,恢复成WebAPI。

技术实现:

  因为之前对身份认证鉴权这一块没有做太多的深入了解,Leader工期也在屁股追,就一句话:怎么快怎么来,先上后迭代。好嘛,为了项目方便,同时为了符合动静结合的身份认证鉴权 。于是,我用了 JWT+自定义身份认证 实现了需求。

方案一:多身份认证+中间件模式实现

添加服务:Services.AddAuthentication 默认使用JWT

 //多重身份认证
//默认使用JWT,如果Controller使用 AuthenticationSchemes 则采用指定的身份认证
Services.AddAuthentication(options =>
{
    options.AddScheme<CustomAuthenticationHandler>(CustomAuthenticationHandler.AuthenticationSchemeName, CustomAuthenticationHandler.AuthenticationSchemeName);
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
    options.RequireHttpsMetadata = false;//设置元数据地址或权限是否需要HTTPs
    options.SaveToken = true;
    options.TokenValidationParameters = new TokenValidationParameters
    {
        ValidateIssuer = true,
        ValidateAudience = true,
        ValidateLifetime = true,
        ValidateIssuerSigningKey = true,
        ValidIssuer = builder.Configuration["Jwt:Issuer"],
        ValidAudience = builder.Configuration["Jwt:Audience"],
        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["Jwt:SecretKey"]!))
    };
    options.Events = new CustomJwtBearerEvents();
});

自定义身份认证 CustomAuthenticationHandler.cs代码

    public class CustomAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
    {
        public const string AuthenticationSchemeName = "CustomAuthenticationHandler";
        private readonly IConfiguration _configuration;
        public CustomAuthenticationHandler(
            IOptionsMonitor<AuthenticationSchemeOptions> options,
            ILoggerFactory logger,
            UrlEncoder encoder,
            ISystemClock clock,
            IConfiguration configuration)
            : base(options, logger, encoder, clock)
        {
            _configuration = configuration;
        }
        /// <summary>
        /// 固定Token认证
        /// </summary>
        /// <returns></returns>
        protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
        {
            string isAnonymous = Request.Headers["IsAnonymous"].ToString();
            if (!string.IsNullOrEmpty(isAnonymous))
            {
                bool isAuthenticated = Convert.ToBoolean(isAnonymous);
                if (isAuthenticated)
                    return AuthenticateResult.NoResult();
            }

            string authorization = Request.Headers["Authorization"].ToString();
            // "Bearer " --> Bearer后面跟一个空格
            string token = authorization.StartsWith("Bearer ") ? authorization.Remove(0, "Bearer ".Length) : authorization;
            if (string.IsNullOrEmpty(token))
                return AuthenticateResult.Fail("请求头Authorization不允许为空。");

            //通过密钥,进行加密、解密对比认证
            if (!VerifyAuthorization(token))
                return AuthenticateResult.Fail("传入的Authorization身份验证失败。");


            return AuthenticateResult.Success(GetTicket());
        }
        private AuthenticationTicket GetTicket()
        {
            // 验证成功,创建身份验证票据
            var claims = new[]
            {
                new Claim(ClaimTypes.Role, "Admin"),
                new Claim(ClaimTypes.Role, "Public"),
            };
            var identity = new ClaimsIdentity(claims, Scheme.Name);
            var principal = new ClaimsPrincipal(identity);
            var ticket = new AuthenticationTicket(principal, new AuthenticationProperties(), this.Scheme.Name);
            return ticket;
        }
        private bool VerifyAuthorization(string token)
        {
            //token: [0]随机生成64位字符串,[1]载荷数据,[2]采用Hash对[0]+[1]的签名
            var tokenArr = token.Split('.');
            if (tokenArr.Length != 3)
            {
                return false;
            }
            try
            {
                //1、先比对签名串是否一致
                string signature = tokenArr[1].Hmacsha256HashEncrypt().ToLower();
                if (!signature.Equals(tokenArr[2].ToLower()))
                {
                    return false;
                }

                //解密
                var aecStr = tokenArr[1].Base64ToString();
                var clientId = aecStr.DecryptAES();
                //2、再验证载荷数据的有效性
                var clientList = _configuration.GetSection("FixedClient").Get<List<FixedClientSet>>();
                var clientData = clientList.SingleOrDefault(it => it.ClientID.Equals(clientId));
                if (clientData == null)
                {
                    return false;
                }
            }
            catch (Exception)
            {
                throw;
            }

            return true;
        }
    }

使用中间件:UseMiddleware

app.UseAuthentication();
//中间件模式:自定义认证中间件:双重认证选其一
//如果使用 策略,需要注释掉 中间件
app.UseMiddleware<FallbackAuthenticationMiddleware>(); //使用中间件实现
app.UseAuthorization();

中间件FallbackAuthenticationMiddleware.cs代码实现

   public class FallbackAuthenticationMiddleware
  {
      private readonly RequestDelegate _next;
      private readonly IAuthenticationSchemeProvider _schemeProvider;

      public FallbackAuthenticationMiddleware(RequestDelegate next, IAuthenticationSchemeProvider schemeProvider)
      {
          _next = next;
          _schemeProvider = schemeProvider;
      }
      /// <summary>
      /// 身份认证方案
      /// 默认JWT。JWT失败,执行自定义认证
      /// </summary>
      /// <param name="context"></param>
      /// <returns></returns>
      public async Task InvokeAsync(HttpContext context)
      {
          var endpoints = context.GetEndpoint();
          if (endpoints == null || !endpoints.Metadata.OfType<IAuthorizeData>().Any() || endpoints.Metadata.OfType<IAllowAnonymous>().Any())
          {
              await _next(context);
              return;
          }

          //默认JWT。JWT失败,执行自定义认证
          var result = await Authenticate_JwtAsync(context);
          if (!result.Succeeded)
              result = await Authenticate_CustomTokenAsync(context);

          // 设置认证票据到HttpContext中 
          if (result.Succeeded)
              context.User = result.Principal;

          await _next(context);
      }
      /// <summary>
      /// JWT的认证
      /// </summary>
      /// <param name="context"></param>
      /// <returns></returns>
      private async Task<dynamic> Authenticate_JwtAsync(HttpContext context)
      {
          var verify = context.User?.Identity?.IsAuthenticated ?? false;
          string authenticationType = context.User.Identity.AuthenticationType;
          if (verify && authenticationType != null)
          {
              return new { Succeeded = verify, Principal = context.User, Message = "" };
          }

          await Task.CompletedTask;

          // 找不到JWT身份验证方案,或者无法获取处理程序。
          return new { Succeeded = false, Principal = new ClaimsPrincipal { }, Message = "JWT authentication scheme not found or handler could not be obtained." };
      }

      /// <summary>
      /// 自定义认证
      /// </summary>
      /// <param name="context"></param>
      /// <returns></returns>
      private async Task<dynamic> Authenticate_CustomTokenAsync(HttpContext context)
      {
          // 自定义认证方案的名称
          var customScheme = "CustomAuthenticationHandler";

          var fixedTokenHandler = await context.RequestServices.GetRequiredService<IAuthenticationHandlerProvider>().GetHandlerAsync(context, customScheme);
          if (fixedTokenHandler != null)
          {
              var Res = await fixedTokenHandler.AuthenticateAsync();
              return new { Res.Succeeded, Res.Principal, Res.Failure?.Message };
          }

          //找不到CustomAuthenticationHandler身份验证方案,或者无法获取处理程序。
          return new { Succeeded = false, Principal = new ClaimsPrincipal { }, Message = "CustomAuthenticationHandler authentication scheme not found or handler could not be obtained." };

      }
  }

方案二:通过[Authorize]标签的AuthenticationSchemes
因为中间件还要多维护一段中间件的代码,显得略微复杂,于是通过[Authorize(AuthenticationSchemes = “”)]方式。

     //使用特定身份认证    
    //[Authorize(AuthenticationSchemes = CustomAuthenticationHandler.AuthenticationSchemeName)]
    //任一身份认证
    [Authorize(AuthenticationSchemes = $"{CustomAuthenticationHandler.AuthenticationSchemeName},{JwtBearerDefaults.AuthenticationScheme}")]
    public class DataProcessingController : ControllerBase
    {
    }

方案二:通过[Authorize]标签的policy

如果还有其他身份认证,那不断增加AuthenticationSchemes拼接在Controller的头顶,显得不太好看,且要是多个Controller使用,也会导致维护麻烦,于是改用策略方式。

在Program.cs添加服务AddAuthorization。使用策略的好处是增加易维护性。

 //授权策略
//Controller使用 policy 则采用指定的策略配置进行身份认证
builder.Services.AddAuthorization(option =>
{
    option.AddPolicy(CustomPolicy.Policy_A, policy => policy
            .RequireAuthenticatedUser()
            .AddAuthenticationSchemes(CustomAuthenticationHandler.AuthenticationSchemeName, JwtBearerDefaults.AuthenticationScheme)
            );

    option.AddPolicy(CustomPolicy.Policy_B, policy => policy
            .RequireAuthenticatedUser()
            .AddAuthenticationSchemes(CustomAuthenticationHandler.AuthenticationSchemeName)
            );

    option.AddPolicy(CustomPolicy.Policy_C, policy => policy
            .RequireAuthenticatedUser()
            .AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)
            );
});
     //使用特定策略身份认证
    [Authorize(policy:CustomPolicy.Policy_B)]
    public class DataProcessingController : ControllerBase
    {
    }
     /// <summary>
    /// 策略类
    /// </summary>
    public static class CustomPolicy
    {
        public const string Policy_A= "Policy_A";

        public const string Policy_B = "Policy_B";

        public const string Policy_C = "Policy_C";
    }

最后附上截图:

添加服务:

使用中间件:

控制器:

这样,整套中继系统就能完美的满足Leader的需求,且达到预期效果。

源码Demo:https://gitee.com/LaoPaoE/project-demo.git
最后附上:

AuthorizeAttribute 同时使用 Policy 和 AuthenticationSchemes 和 Roles 时是怎么鉴权的流程:

  1. AuthenticationSchemes鉴权:
    • AuthenticationSchemes 属性指定了用于验证用户身份的认证方案(如Cookies、Bearer Tokens等)。
    • ASP.NET Core会根据这些认证方案对用户进行身份验证。如果用户未通过身份验证(即未登录或未提供有效的认证信息),则请求会被拒绝,并可能重定向到登录页面。
  2. Roles鉴权(如果指定了Roles):
    • 如果AuthorizeAttribute中还指定了 Roles 属性,那么除了通过身份验证外,用户还必须属于这些角色之一。
    • ASP.NET Core会检查用户的角色信息,以确定用户是否属于 Roles  属性中指定的一个或多个角色。
  3. Policy鉴权(如果指定了Policy):
    • Policy 属性指定了一个或多个授权策略,这些策略定义了用户必须满足的额外条件才能访问资源。
    • ASP.NET Core会调用相应的 IAuthorizationHandler 来评估用户是否满足该策略中的所有要求。这些要求可以基于角色、声明(Claims)、资源等定义。
    • 如果用户不满足策略中的任何要求,则授权失败,并返回一个HTTP 403 Forbidden响应。

鉴权顺序和组合

  • 通常,AuthenticationSchemes的验证会首先进行,因为这是访问任何受保护资源的前提。
  • 如果AuthenticationSchemes验证通过,接下来会根据是否指定了Roles和Policy来进一步进行鉴权。
  • Roles和Policy的鉴权顺序可能因ASP.NET Core的具体版本和配置而异,但一般来说,它们会作为独立的条件进行评估。
  • 用户必须同时满足AuthenticationSchemes、Roles(如果指定)和Policy(如果指定)中的所有条件,才能成功访问受保护的资源。

注意事项

  • 在某些情况下,即使AuthenticationSchemes和Roles验证都通过,但如果Policy中的要求未得到满足,用户仍然无法访问资源。
  • 可以通过自定义 IAuthorizationRequirement 和 IAuthorizationHandler 来实现复杂的授权逻辑,以满足特定的业务需求。
  • 确保在应用程序的身份验证和授权配置中正确设置了AuthenticationSchemes、Roles和Policy,以便它们能够协同工作,提供有效的访问控制。

推荐一款开源一站式SQL审核查询平台!功能强大、安全可靠! - 狂师 - 博客园

mikel阅读(212)

来源: 推荐一款开源一站式SQL审核查询平台!功能强大、安全可靠! – 狂师 – 博客园

1、前言

在当今这个数据驱动的时代,数据库作为企业核心信息资产的载体,其重要性不言而喻。随着企业业务规模的不断扩大,数据库的数量和种类也日益增多,这对数据库的管理与运维工作提出了前所未有的挑战。在这样的背景下,一款高效、易用的数据库管理工具显得尤为重要。Archery,作为一款开源的数据库管理平台,以其独特的魅力成为了众多开发者和运维工程师的得力助手,如同古代战场上的利箭,精准而有力地击中了数据库管理的痛点。

2、Archery介绍

Archery是一个基于Python Flask开发的数据库管理平台,它支持多种数据库(如MySQL、PostgreSQL、MariaDB、Redis等),提供了包括数据库查询、管理、权限控制、SQL审核、数据备份与恢复等功能在内的全方位数据库管理解决方案。其设计初衷是为了简化数据库管理流程,提高运维效率,同时保证数据的安全性和可靠性。

项目地址

https://github.com/hhyo/Archery
https://gitee.com/rtttte/Archery

3、核心功能解析

  1. 数据库管理
    Archery通过直观的Web界面,允许用户轻松管理多个数据库实例。用户可以添加、删除、修改数据库连接信息,实时查看数据库状态,以及执行诸如数据库备份、恢复、优化等高级操作。这一功能极大地降低了数据库管理的门槛,使得即便是非专业的运维人员也能快速上手。
  2. SQL查询与审核
    Archery内置了强大的SQL编辑器,支持语法高亮、自动补全等功能,极大提升了SQL编写和调试的效率。同时,它还提供了SQL审核功能,通过预设的规则库对SQL语句进行自动化审查,帮助用户及时发现并纠正潜在的风险,保障数据库操作的安全性和合规性。
  3. 权限控制
    Archery支持细粒度的权限控制策略,可以针对不同的用户或用户组设置不同的操作权限。这一功能有效防止了数据泄露和误操作的风险,保障了数据的安全性和隐私性。
  4. 数据备份与恢复
    数据备份是数据库运维中不可或缺的一环。Archery提供了自动化的数据备份和恢复功能,用户可以自定义备份策略,实现定时备份、增量备份等需求。同时,当数据发生丢失或损坏时,用户可以迅速通过备份文件恢复数据,保障业务的连续性。
  5. 可视化监控
    Archery还集成了数据库性能监控功能,通过图表和报告的形式展示数据库的运行状态、性能指标等关键信息。这使得运维人员能够实时监控数据库的健康状况,及时发现并解决潜在的问题。

4、优势与应用场景

优势

  • 开源免费:Archery作为一款开源项目,其源代码完全公开,用户可以自由下载、使用并根据实际需求进行修改和扩展。
  • 易于部署:Archery支持Docker等容器化部署方式,简化了部署流程,降低了部署门槛。
  • 功能全面:涵盖了数据库管理的各个方面,满足了从日常运维到高级管理的各种需求。
  • 安全性高:通过权限控制、SQL审核等功能保障了数据的安全性和合规性。

应用场景

  • 中小型企业:对于资源有限的中小型企业而言,Archery提供了一种低成本、高效率的数据库管理解决方案。
  • 互联网企业:面对海量数据和复杂的数据库环境,Archery的自动化管理和监控功能能够显著提升运维效率。
  • 金融、医疗等敏感行业:这些行业对数据的安全性和合规性要求极高,Archery的权限控制和SQL审核功能能够有效保障数据的安全性。

5、安装与使用

Archery提供了容器化部署和非容器化部署两种模式,部署较为简单,在此不过多介绍,可参照如下部署文档即可

1、容器化部署:

https://github.com/hhyo/archery/wiki/docker

2、非容器化部署:

https://github.com/hhyo/archery/wiki/manual

3、在线体验地址 : https://demo.archerydms.com/
在这里插入图片描述
账号和密码为:archer/archer

6、小结

Archery作为一款开源的数据库管理平台,以其丰富的功能、易于部署的特点和强大的性能赢得了广泛的关注和好评。它如同一把精准的利箭,穿透了数据库管理的重重迷雾,为数据库的管理与运维工作带来了前所未有的便利和效率。未来,随着技术的不断进步和应用的不断深化,Archery有望成为更多企业和个人首选的数据库管理工具。

使用C#爬取快手作者主页,并下载视频/图集(附源码及软件下载链接) - 猫叔Vincent - 博客园

mikel阅读(395)

来源: 使用C#爬取快手作者主页,并下载视频/图集(附源码及软件下载链接) – 猫叔Vincent – 博客园

最近发现一些快手的作者,作品还不错,出于学习研究的目的,决定看一下怎么爬取数据。现在网上有一些爬虫工具,不过大部分都失效了,或者不开源。于是自己就写了一个小工具。先看一下成果:
image
image
软件只需要填写作者uid以及网页版的请求Cookie,即可实现自动下载,下载目录在程序根目录下的Download文件夹。
由于快手的风控比较厉害,软件也做了应对措施。不过需要用户点击软件中的提示文字,复制粘贴到浏览器,把请求的json保存到本地文件。使用软件提供的解析本地json按钮解析下载即可。如果返回的json文件很短或者没有数据,需要在快手的任意一个页面刷新一下,也就是告诉快手风控,现在是正常浏览,没有机器人的行为。

下面说一下构建整个App的思路。

1. 快手网页端准备

  1. 打开https://live.kuaishou.com/ ,在顶部搜索你要爬取的作者昵称,进入作者主页。也可以从App端分享作者的主页链接,粘贴进来。作者主页加载完成后,地址栏的地址一定要是类似:https://live.kuaishou.com/profile/xxxxxx。 后面的xxxxxx就是作者的user id。这个记住,复制出来,后面会用到。
  2. 按F12打开浏览器的开发者工具(我之前就说过开发者工具是好东西,研究爬虫必备,一定要好好学习)。
  3. 选择开发者工具顶部的“网络”,“全部”,如图所示。在请求列表中找到user id,点击它,右面就会出来请求的标头。里面有个Cookie,需要记住,复制出来。如果没有的话,记得刷新页面。
    image
  4. 在列表里面可以看到很多请求,我们需要从中找到网页端展示作品列表的那条请求,即public开头的,或者直接在左上角搜索public,即可过滤绝大部分无关请求。这个请求的响应数据里面有作者作品的完整json响应。
    image

你可以右击它,在新标签页面打开,打开后地址栏会显示完成的浏览器请求地址。这个网址需要记住,后续会用到。那个count默认是12或者20,我们用到时候,直接拉满,9999即可。
image
image

2. Postman拦截请求,模拟请求,并生成C#请求代码

  1. 安装postman interceptor拦截器,安装地址https://chromewebstore.google.com/detail/postman-interceptor/aicmkgpgakddgnaphhhpliifpcfhicfo 不得不说,这又是一个神器,搭配开发者工具,理论上可以搞定几乎所有的爬虫需求了。
  2. 打开Postman,点击右下角的Start Proxy,
    image
    开启拦截后,重新回到网页版作者主页,刷新一下页面,等页面加载完成后,点击停止拦截。否则列表会一直增多,因为他会拦截电脑的所有网络请求。这时Postman拦截器就会拦截到一大堆请求,同理,找到public请求,或者在左上角输入public,即可过滤出来我们需要的。
    image
    点击这个请求链接
    image
    这是Postman会打开一个新的窗口,包含了请求这个链接的所有参数以及标头信息。
    image
    点击Postman最右面的代码工具即可生成我们需要的代码。你可以选择C#、python、js、curl等等。
    image

3. 使用WPF写界面以及下载逻辑

  1. 新建WPF工程,为了界面好看,这次我用了开源的WPF UI,之前用过HandyControl、MicaWPF,这些都是不错的UI控件库。
    下载使用了开源的Downloader,请求使用了RestSharp,解析Json使用NewtonsoftJson,另外推荐一个免费的图标库FlatIcon。
    界面如下:
点击查看代码
  1. 后台逻辑没有使用MVVM,就是图方便。
点击查看代码
  1. 下载类,下载完文件后,将文件的日志修改为发表日志,方便排序以及数据分析。
点击查看代码
  1. 源码分享
    完整版代码已上传到Github https://github.com/hupo376787/KuaishouDownloader ,喜欢的点一下Star谢谢。

4. 下载使用

打开https://github.com/hupo376787/KuaishouDownloader/releases/tag/1.0,点击下载zip文件,解压缩后,就可以像开头那样使用了。
image
image

ThinkPHP5中find()和select()区别 - 范仁义 - 博客园

mikel阅读(164)

来源: ThinkPHP5中find()和select()区别 – 范仁义 – 博客园

读取数据是指读取数据表中的一行数据(或者关联数据),主要通过find方法完成,例如:

1
2
3
4
$User = M("User"); // 实例化User对象
// 查找status值为1name值为think的用户数据 
$data $User->where('status=1 AND name="thinkphp"')->find();
dump($data);

find方法查询数据的时候可以配合相关的连贯操作方法,其中最关键的则是where方法,如何使用where方法我们会在查询语言章节中详细描述。

如果查询出错,find方法返回false,如果查询结果为空返回NULL,查询成功则返回一个关联数组(键值是字段名或者别名)。 如果上面的查询成功的话,会输出:

1
2
3
4
array (size=3)
  'name' => string 'thinkphp' (length=8)
  'email' => string 'thinkphp@gmail.com' (length=18)
  'status'=> int 1

即使满足条件的数据不止一个,find方法也只会返回第一条记录(可以通过order方法排序后查询)。

还可以用data方法获取查询后的数据对象(查询成功后)

1
2
3
4
$User = M("User"); // 实例化User对象
// 查找status值为1name值为think的用户数据 
$User->where('status=1 AND name="thinkphp"')->find();
dump($User->data());

读取数据集

读取数据集其实就是获取数据表中的多行记录(以及关联数据),使用select方法,使用示例:

1
2
3
$User = M("User"); // 实例化User对象
// 查找status值为1的用户数据 以创建时间排序 返回10条数据
$list $User->where('status=1')->order('create_time')->limit(10)->select();

如果查询出错,select的返回值是false,如果查询结果为空,则返回NULL,否则返回二维数组。

复制代码
$about=M('document');
$abouts=$about->where('id=2')->select();
$abouts2=$about->where('id=2')->find();
var_dump($abouts);
var_dump($abouts2);
复制代码

输出结果:

复制代码
array (size=1)
  0 => 
    array (size=24)
      'id' => string '2' (length=1)
      'uid' => string '1' (length=1)
      'name' => string '' (length=0)
      'title' => string '公司简介' (length=12)
      'category_id' => string '39' (length=2)
      'group_id' => string '0' (length=1)
      'description' => string '公司简介公司简介公司简介公司简介公司简介公司简介公司简介公司简介公司简介公司简介公司简介公司简介公司简介' (length=156)
      'root' => string '0' (length=1)
      'pid' => string '0' (length=1)
array (size=24)
  'id' => string '2' (length=1)
  'uid' => string '1' (length=1)
  'name' => string '' (length=0)
  'title' => string '公司简介' (length=12)
  'category_id' => string '39' (length=2)
  'group_id' => string '0' (length=1)
  'description' => string '公司简介公司简介公司简介公司简介公司简介公司简介公司简介公司简介公司简介公司简介公司简介公司简介公司简介' (length=156)
  'root' => string '0' (length=1)
  'pid' => string '0' (length=1)
复制代码

从上面的代码可以看出,find()返回一个一维数组select()返回一个二维数组,所以在取值时有所不同,一维数组取值用data[id]data[“id”],二维数组取值用data[0][“id”],由于一开始没了解这个用法,调试一天也取不值,最后有var_dump()方法才看到两个方法的不同所在!

复制代码
        $about=M('document');
        $abouts=$about->where('id=2')->select();
        $abouts2=$about->where('id=2')->find();
//        var_dump($abouts);
//        var_dump($abouts2);
         if($abouts){
            $article = M('document_article');
            //$info = $article->find($abouts['id']);
            $info=$article->where('id='.$abouts2['id'])->find();
        }
        $this->assign('wzjj',$info);
复制代码

 

这是DDD建模最难的部分(其实很简单) - 老肖想当外语大佬 - 博客园

mikel阅读(181)

来源: 这是DDD建模最难的部分(其实很简单) – 老肖想当外语大佬 – 博客园

本文书接上回《为了落地DDD,我是这样“PUA”大家的》 ,欢迎关注公众号(老肖想当外语大佬),加群、获取最新文章更新和DDD框架源码,视频和直播在B站。
https://mp.weixin.qq.com/s/DjC0FSWY1bgJyLPIND5evA
 

什么是最重要的事

如果你认真读过前面的文章,那么一定知道我们的核心逻辑:领域驱动是一种价值观,这个价值观是:“领域(边界)”的明确是软件设计掌控复杂度最重要的事。
那么整个软件交付过程中,架构师的职责就是持续保持“需求”、“模型”、“代码”三者的边界范围明确且一致。再回过头看整个软件交付存在的意义,就是为了满足需求,因此本质上来说,“需求”的边界就决定了一切,那么结论就不言而喻了:
最重要的事,就是需求的边界(范围)。
 

什么叫边界明确

假如说我有两个需求A和B,那么我们建模的时候就有如下几种结果,大家感受一下,通常情况下,自己给出的结果是哪个选项?哪个选项又是最理想的?
 

我相信大部分人都会认同,其中B和D是符合边界明确、需求与模型边界一致的原则的,而现实的情况大部分结果是A和C,各种各样的“join”充斥着系统的各个角落,一个典型的例子,就是用户-角色系统的设计。
用户-角色系统,通常会有这样几个关键需求:
  1. 创建用户
  2. 创建角色
  3. 为用户设置角色
  4. 查看用户有哪些角色
  5. 查看一个角色包含多少个用户
我们用最传统的设计方法来做,模型大体是这样的:
 

这样的结果是不是对应到了前面选项A和C比较类似?因为“用户聚合”与“角色聚合”连线的存在,导致需求与模型的边界不一致。
而我们要做的,就是在满足所有需求的同时,消除打破边界的连线。

如何操作

首先我们分析上图,假如我们把下面几个需求先去掉:
  1. 为用户设置角色
  2. 查看用户有哪些角色
  3. 查看一个角色包含多少个用户
那么我们会得到一个符合边界明确原则的设计:
 

然后我们再思考,下面两个需求,应该哪个聚合负责:
  1. 为用户设置角色
  2. 查看用户有哪些角色
答案很显然是“用户聚合”,那么我们可以得到下面的设计:
 

这时你会疑问,如果没有“用户聚合”和“角色聚合”的连线,怎么设置用户有哪些角色呢?
问题的关键,就在这里,通常我们总是会把“关系表”在图中用一条线来表示,那如果我说,“用户聚合”有一个集合属性,叫做“用户角色”,你会认同吗?如果我们知道用户对象有一个集合属性叫“用户角色”,那么是不是上图就很合理?
如果顺着这个思路,我们再来看需求“查看一个角色包含多少个用户”,它应该由哪个模型来解决?我想你已经知道答案了,就是“用户聚合”,最终我们得到如下设计:
 

到此,所有的需求可以满足,需求被划分为两个范围,分别对应两个模型。

为什么说它很难

如果你一直跟着我的思路,完成了上面的过程,那么你会发现,需求的边界不是客观存在的,而是我们主观的划分,这个划分的目的是为了在一个确定的范围内,能够解决这个问题。因为它是主观的,就不可衡量和判断,每个人都可以有自己的划分思路。另外它又是简单的,因为你可以像上图一样,这样划分边界,给出对应的模型解决它,就像在给自己家的袜子分配收纳盒一样简单。
所以,我常常叹息,关于领域驱动设计:
说它难,难的是做出取舍。
说它简单,是因为能明确知道取什么舍什么。

最新AI生成视频工具!效果不输快手可灵,CogVideoX下载介绍 - 老艾的AI世界 - 博客园

mikel阅读(361)

来源: 最新AI生成视频工具!效果不输快手可灵,CogVideoX下载介绍 – 老艾的AI世界 – 博客园

要说AI生成视频最火的项目,当属国产的快手可灵了,甚至比OpenAI的Sora还要火,前者还是个ppt,可灵已经在落地公测了,博主在前段时间申请试用通道的时候,竟然排到几十万人开外的位置,好在最后还是拿到了使用资格,还没用上的外国友人只能干着急,在社交媒体发「求求了!」

 

但就在最近,可灵开始收费了,而且年费要大几千,着实有点儿压力~

就在几天前,对标快手可灵的CogVideoX横空出世了,还可以部署到本地电脑使用,从下图可以看到其效果甚至不输快手可灵

 

CogVideoX最新中文版:

百度网盘:https://pan.baidu.com/s/1b2dS7Wj6-yta7xo7NrjihQ?pwd=7hmo

 

CogVideoX的提示词上限为226个token,视频长度为6秒,帧率为8帧/秒,视频分辨率为720*480。用户只需输入一段文字,就能快速生成充满想象力的视频,还可以自由选择生成的视频风格,包括卡通、3D、黑白、油画、电影等,并配上软件自带的音乐

CogVideoX通过融合文本、时间和空间三个维度,实现了高效、连贯的视频生成,并采用DiT架构和优化算法,显著提升了推理速度和生成质量

工作流程

· 数据预处理:将输入的图像或视频数据转换为模型可以处理的格式,将图像切分成固定大小的patches(小块),然后将这些patches转换为特征向量

· 噪声引入:在数据预处理后的特征向量上逐步引入噪声,形成一个噪声增加的扩散过程,这个过程可以视为从原始数据到噪声数据的转换

· 模型训练:使用引入了噪声的特征向量作为输入,训练Transformer模型,模型的目标是学习如何逆转噪声增加的过程,即从噪声数据恢复出原始数据

· 视频生成:在模型训练完成后,通过输入随机生成的噪声和原始数据的映射关系到模型中,经过模型的处理后生成新的图像或视频

使用方法

1.点击软件界面右下角的load按钮,选择项目自带的workflow.json文件并加载

2.在新打开的界面中,输入待生成视频的提示词

3.设置参数(一般保持默认)

4.点击Queue Prompt按钮

稍微等待一下,视频就生成好了,甄嬛传中皇上啃鸡腿这段我怎么没见过?

 

注意事项

①项目安装路径不要包含中文

②推荐使用GTX1070以上显卡运行此项目

③使用过程中若不慎关闭软件后台,请重新打开,并刷新网页

使li标签横向排列的三种方式_li横向排列-CSDN博客

mikel阅读(176)

来源: 使li标签横向排列的三种方式_li横向排列-CSDN博客

一、display:inline

通过把li转换为行内元素实现,行内元素不独占一行,根据内容撑开大小

 

 

 

 

二、float:left

通过让li浮动实现横向排列 ,浮动会使li脱离文本流,且不占位置

 

 

三。通过flex布局实现

首先给父级定义flex,li{ flex:1}的意思为独占父级宽度的n分之1,这里的n取决于li的个数,也就是4分之1

 

 

————————————————

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

原文链接:https://blog.csdn.net/m0_61685348/article/details/120694044

深入理解 PHP 高性能框架 Workerman 守护进程原理 - Yxh_blogs - 博客园

mikel阅读(167)

来源: 深入理解 PHP 高性能框架 Workerman 守护进程原理 – Yxh_blogs – 博客园

大家好,我是码农先森。

守护进程顾名思义就是能够在后台一直运行的进程,不会霸占用户的会话终端,脱离了终端的控制。相信朋友们对这东西都不陌生了吧?如果连这个概念都还不能理解的话,建议回炉重造多看看 Linux 进程管理相关的基础知识。在我们日常的编程中常见有类似 php think ...php artisan ...php yii ... 等命令启动需要一直执行的任务,都会通过 nohup 挂载到后台保持长期运行的状态。同样在 Workerman 中也是使用类似 php index.php start 的命令来启动进程,但不同的是它不需要利用 nohup 便可以挂载到后台运行。那有些朋友就会好奇它是怎么实现的呢?为了解决朋友们的疑惑,我们今天就重点深入分析一下 Workerman 守护进程的实现原理。

我们先了解一些进程相关的知识:

  • 父进程:父进程是生成其他进程的进程。当一个进程创建了另一个进程时,创建者被称为父进程,而被创建的进程则成为子进程。父进程可以通过进程标识符(PID)来识别它所创建的子进程。
  • 子进程:子进程是由父进程创建的新进程。子进程继承了父进程的一些属性,例如环境变量、文件描述符等。子进程独立于父进程运行,它可以执行自己的代码,并且具有自己的资源和内存空间。
  • 进程组:进程组是一组相关联的进程的集合。每个进程组都有一个唯一的进程组ID(PGID),用于标识该进程组。进程组通常由一个父进程创建,并且包含了与父进程具有相同会话ID(SID)的所有子进程。
  • 会话:会话是一组关联进程的集合,通常由用户登录到系统开始,直至用户注销或关闭终端会话结束,一个会话中的进程共享相同的控制终端。每个会话都有一个唯一的会话ID(SID),用于标识该会话。会话通常包含一个或多个进程组,其中第一个进程组成为会话的主进程组。

这些概念俗称八股文,向来都不怎么好理解,那我们来看个例子。执行了命令 php index.php 便产生了进程 61052「该进程的父进程是 Bash 进程 8243,这里不用管它」,然后通过 Fork 创建了子进程 61053 且其父进程就是 61052,这两个进程拥有共同的进程组 61052 和会话 8243。调用 posix_setsid 函数,将会为子进程 61053 开启新的进程组 61053 和新的会话 61053,这里的会话可以理解为一个新的命令窗口终端。最后子进程 61053 通过 Fork 创建了子进程 61054,进程 61053 升级成了父进程,这里再次 Fork 的原因是要避免被终端控制进程所关联,这个进程 61052 是在终端的模式下创建的,自此进程 61054 就形成了守护进程。

Copy
[manongsen@root phpwork]$ php index.php
[parent] 进程ID: 61052, 父进程ID: 8243, 进程组ID: 61052, 会话ID: 8243 
[parent1] 进程ID: 61052, 父进程ID: 8243, 进程组ID: 61052, 会话ID: 8243 退出了该进程
[child1] 进程ID: 61053, 父进程ID: 61052, 进程组ID: 61052, 会话ID: 8243 
[child1] 进程ID: 61053, 父进程ID: 61052, 进程组ID: 61053, 会话ID: 61053 
[parent2] 进程ID: 61053, 父进程ID: 61052, 进程组ID: 61053, 会话ID: 61053 退出了该进程
[child2] 进程ID: 61054, 父进程ID: 61053, 进程组ID: 61053, 会话ID: 61053 保留了该进程

[manongsen@root phpwork]$ ps aux | grep index.php
root             66064   0.0  0.0 408105040   1472 s080  S+   10:00下午   0:00.00 grep index.php
root             61054   0.0  0.0 438073488    280   ??  S    10:00下午   0:00.00 php index.php

上面举例的进程信息,正是这段代码运行所产生的。如果看了这段代码且细心的朋友,会发现为什么 posix_setsid 这个函数不放在第一次 Fork 前调用,而在第二次 Fork 前调用呢,这样的话就不用 Fork 两次了?原因是组长进程是不能创建会话的,进程组ID 61052 和进程ID 61052 相同「即当前进程则为组长进程」,所以需要子进程来创建新的会话,这一点需要特别注意一下。

Copy
<?php

function echoMsg($prefix, $suffix="") {
    // 进程ID
    $pid = getmypid(); 
    // 进程组ID
    $pgid = posix_getpgid($pid);
    // 会话ID
    $sid = posix_getsid($pid); 
    // 父进程ID
    $ppid = posix_getppid();

    echo "[{$prefix}] 进程ID: {$pid}, 父进程ID: {$ppid}, 进程组ID: {$pgid}, 会话ID: {$sid} {$suffix}" . PHP_EOL;
}

// [parent] 进程ID: 61052, 父进程ID: 8243, 进程组ID: 61052, 会话ID: 8243
echoMsg("parent");

// 第一次 Fork 进程  
$pid = pcntl_fork();
if ( $pid < 0 ) {
    exit('fork error');
} else if( $pid > 0 ) {
    // [parent1] 进程ID: 61052, 父进程ID: 8243, 进程组ID: 61052, 会话ID: 8243 退出了该进程
    echoMsg("parent1", "退出了该进程");
    exit;
}

// 创建的 子进程ID 为 61053 但 进程组、会话 还是和父进程是同一个
// [child1] 进程ID: 61053, 父进程ID: 61052, 进程组ID: 61052, 会话ID: 8243 
echoMsg("child1");

// 调用 posix_setsid 函数,会创建一个新的会话和进程组,并设置 进程组ID 和 会话ID 为该 进程ID
if (-1 === \posix_setsid()) {
    throw new Exception("Setsid fail");
}

// 现在会发现 进程组ID 和 会话ID 都变成了 61053 在这里相当于启动了一个类似 Linux 终端下的会话窗口
// [child1] 进程ID: 61053, 父进程ID: 61052, 进程组ID: 61053, 会话ID: 61053 
echoMsg("child1");

// 第二次 Fork 进程
// 这里需要二次 Fork 进程的原因是避免被终端控制进程所关联,这个进程 61052 是在终端的模式下创建的
// 需要脱离这个进程 61052 以确保守护进程的稳定
$pid = pcntl_fork();
if ( $pid  < 0 ){
    exit('fork error');
} else if( $pid > 0 ) {
    // [parent2] 进程ID: 61053, 父进程ID: 61052, 进程组ID: 61053, 会话ID: 61053 退出了该进程
    echoMsg("parent2", "退出了该进程");
    exit;
}

// 到这里该进程已经脱离了终端进程的控制,形成了守护进程
// [child2] 进程ID: 61054, 父进程ID: 61053, 进程组ID: 61053, 会话ID: 61053 保留了该进程
echoMsg("child2", "保留了该进程");

sleep(100);

有时间的朋友最好自行执行代码并分析一遍,会有不一样的收获。这里假装你已经实践过了,这下我们来看 Workerman 的 Worker.php 文件中 554 行的 runAll 方法中的 static::daemonize() 这个函数,实现的流程逻辑和上面的例子几乎一样。不过这里还使用了 umask 这个函数,其主要的作用是为该进程所创建的文件或目录赋予相应的权限,保证有权限操作文件或目录。

Copy
// workerman/Worker.php:554
/**
 * Run all worker instances.
 * 运行进程
 * @return void
 */
public static function runAll()
{
    static::checkSapiEnv();
    static::init();
    static::parseCommand();
    static::lock();
    // 创建进程并形成守护进程
    static::daemonize();
    static::initWorkers();
    static::installSignal();
    static::saveMasterPid();
    static::lock(\LOCK_UN);
    static::displayUI();
    static::forkWorkers();
    static::resetStd();
    static::monitorWorkers();
}

// workerman/Worker.php:1262
/**
 * Run as daemon mode.
 * 使用守护进程模式运行
 * @throws Exception
 */
protected static function daemonize()
{
	// 判断是否已经是守护状态、以及当前系统是否是 Linux 环境
    if (!static::$daemonize || static::$_OS !== \OS_TYPE_LINUX) {
        return;
    }
    
    // 设置 umask 为 0 则当前进程创建的文件权限都为 777 拥有最高权限
    \umask(0);
    
    // 第一次创建进程
    $pid = \pcntl_fork();
    if (-1 === $pid) {
    	// 创建进程失败
        throw new Exception('Fork fail');
    } elseif ($pid > 0) {
    	// 主进程退出
        exit(0);
    }

	// 子进程继续执行...
    // 调用 posix_setsid 函数,可以让进程脱离父进程,转变为守护进程
    if (-1 === \posix_setsid()) {
        throw new Exception("Setsid fail");
    }

	// 第二次创建进程,在基于 System V 的系统中,通过再次 Fork 父进程退出
	// 保证形成的守护进程,不会成为会话首进程,不会拥有控制终端
    $pid = \pcntl_fork();
    if (-1 === $pid) {
    	// 创建进程失败
        throw new Exception("Fork fail");
    } elseif (0 !== $pid) {
    	// 主进程退出
        exit(0);
    }

    // 子进程继续执行...
}

守护进程也是 Workerman 中重要的一部分,它保障了 Workerman 进程的稳定性。不像我们通过 nohup 启动的命令,挂起到后台之后,有时还神不知鬼不觉的就挂了,朋友们或许都有这样的经历吧。当然在市面上也有一些开源的守护进程管理软件,比如 supervisor 等,其次还有人利用会话终端 screen、tmux 等工具来实现。其实守护进程的实现方式有多种多样,我们这里只是为了分析 Workerman 中守护进程的实现原理,而引出了在 PHP 中实现守护进程模式的例子,希望本次的内容能对你有所帮助。

感谢大家阅读,个人观点仅供参考,欢迎在评论区发表不同观点。