文章851
标签121
分类10

掌握 PHP Trait 的概念和用法

请简述一下 PHP Trait 的概念和用法

作为一个老程序员,在面试的时候被面试官用这个问题嘲讽了好一阵... 由于一直使用老的框架,再加上懒惰,导致我根本没法完整的回答这个问题,今天就带领各位一起学习(复习)这个概念,希望各位能有所收获。

1. Trait 是什么

Trait [treɪt] 翻译过来是 "特性"、"特点" 、"特质",是一种在 PHP 中复用代码的形式。

我们看下官方的解释:

Trait 是为类似 PHP 的单继承语言而准备的一种代码复用机制。Trait 为了减少单继承语言的限制,使开发人员能够自由地在不同层次结构内独立的类中复用 methodTraitClass 组合的语义定义了一种减少复杂性的方式,避免传统多继承和 Mixin 类相关典型问题。

上述说明可以提取出几个关键词:代码复用单继承减少复杂性

说到单继承,不得不提到另外一个特性:多态。多态和继承是软件开发中常用的代码复用方式,但是继承的方式虽然也能解决问题,但其思路违背了面向对象的原则,显得很粗暴;多态方式也可行,但不符合软件开发中的 DRYDon't repeat yourself ) 原则,增加了维护成本。

此时此刻,Trait 以一种全新的继承方式出现了,它既解决了前文叙述的两种继承方式的弊端,也相对优雅的实现了代码的复用。

简单说一下 Trait 在底层的运行原理:PHP 解释器在编译代码时会把 Trait 部分代码复制粘贴到类的定义体中,但是不会处理这个操作引入的不兼容问题。(是不是很厉害)

2. 定义和使用 Trait

接下来我还会沿用第二节课中的例子,但这次不是买手机,而是自己做手机。假设我们是一家比较特殊的手机厂商,为什么特殊呢,因为我们什么品牌的手机都做(就当是山寨工厂吧),废话不多说,我来说一下制作的规则:

假设我们掌握了三种手机的制作工艺,他们分别是:小米 Note3、三星 Galaxy S8 和 iPhone X,他们各有各的特点(相对于其他两种品牌独一无二)还有相同的特点(面部识别),为方便大家理解,我画了一张图(看不清图片的同学请点击图片查看大图):

Xnip2024-02-21_18-42-25.png

简单解释一下:众所周知,这三部手机都有面部识别的功能(上图左侧部分),这属于相同点,其实他们的相同点不止这些(比如都可以打电话、发短信等),而了解他们的同学也都知道,其中任何一部手机都有相对于其他手机没有的功能(上图右侧部分)。

下面我将用代码的形式讲解,当把这些手机抽象为 PHP 类的概念时,如何利用 PHP Trait 更好更优雅的实现这个图中的功能。

首先,我需要把『面部识别』这三个手机都有的功能抽象为一个 Trait,请看代码示例:

// 小米 Note3 三星 S8 iPhone X
// 共同拥有的面部识别功能
trait Faceable {
    protected $face_id = 0;
    // 就当我是获取面部信息的功能
    public function getFace()
    {
        //...
        return $this->face_id;
    }
    // 就当我是设置面部信息的功能
    public function setFace(string $face_id)
    {
        //...
        $this->face_id = $face_id;
    }
}

定义一个 Trait 是不是很简单,除特殊关键字以外,内部构造其实和 PHP 普通的类没啥区别。这里需要注意的是,Trait 的命名规范最好是以able 结尾,这样方便我们自己识别和理解。其次,建议每个文件只定义一种性状,这是良好的实践。

接下来就是定义这三部手机的类了,需要注意的是,我在实现这三部手机中特有的功能外,同时引用了我刚才定义的面部识别 Trait

// 小米 Note3
class MiNote3 {
    // 引入面部识别 Trait
    use Faceable;
    // 独有的 MIUI
    protected $miui;
    // 初始化MIUI
    public function __construct($miui)
    {
        $this->miui = $miui;
        $this->bootUI();
    }

    private function bootUI()
    {
        return $this->miui;
    }

    //...
}

注:小米的 MIUI 可以说是安卓阵营的佼佼者,用来当做其独特的特性不为过。

// 三星 GalaxyS8
class SamsangS8 {
    // 引入面部识别 Trait
    use Faceable;
    // 独有的 Bixby
    protected $bixby;
    
    public function __construct()
    {
        $this->sayHello();
    }

    private function sayHello()
    {
        echo "Hi I am Bixby!";
    }

    //...
}

注:Bixby 是三星近期发布的语音助手,虽然还在内测中,我有幸提前体验了一把,感觉各个方面都碾压苹果的 Siri 和其他语音助手。所以把这个 Bixby 的功能作为一个三星S8独有的特性,抽象出来一个类。

// iPhoneX
class iPhoneX {
    // 引入面部识别 Trait
    use Faceable;
    // 独有的 TrueDepth
    protected $true_depth;

    public function __construct()
    {
        $this->openCamera();
    }

    private function openCamera()
    {
        return $this->true_depth;
    }
    //...
}

注:iPhoneX 最好玩的特点莫过于前置的深感摄像头了,可以把表情动作赋给 emoji,这个功能就可以作为这个手机独有的特色抽象出来一个类。

通过以上示例,我们就可以把『面部识别』这三部手机都拥有的特性很轻松的组装到这三部手机中,使他们都具有完整性。用代码来说呢,就是每个类都使用了一个公共的 PHP Trait,在不改变自身的前提下,拥有了额外的属性。

3. Trait 的好处

有的人看完上述示例可能会有这样的疑问:与其像代码里这么写,我不如新建一个名为 Mobile 的类,在这个类里完成面部识别的功能,然后让三个手机的类直接继承 Mobile 类不就行了吗?比如:

// 手机公共类
class Mobile {
    protected $message;
    //...
    public function __construct($message)
    {
        $this->message = $message;
    }

    public function getMessage()
    {
        return $this->message;
    }

    // 实现了开关机 闹钟 音乐播放等功能...
    # code...
}

然后这样引用

// 小米Note3
class MiNote3 extends Mobile {
     // ...
}

// GalaxyS8
class SamsangS8 extends Mobile {
    // ...
}

答案是可以的,但是你有没有想过,除了『面部识别』,是不是还有其他的特性我没有列出来?比如三星和苹果都有的超大分辨率,小米没有;小米和三星都是安卓系统,而苹果不是。这样的关系,你如果都写在一个类里,由于 PHP 只有单继承的原因,会使你的 Mobile 类显得异常臃肿,难以解读。

但是你使用了 Trait 之后,我们只需要再提取出『安卓系统』和『高分辨率』这两个特性,就可以很方便的在这三个类里随意组合,而且还能保证你的代码非常清晰。

// 小米Note3
class MiNote3 {
    use Faceable,Androidable;
    // ...
}

// GalaxyS8
class SamsangS8 {
    use Faceable,Androidable,HDisplayable;
    // ...
}

// iPhoneX
class iPhoneX {
    use Faceable,HDisplayable;
    // ...
}

这样看起来是不是清晰很多呢?他不仅降低了代码的耦合性,还提升了代码的可读性。依我看来,他不光是某种特性的集合,更像是将某个功能细化了的代码块。

但还请大家记住,所有思路的设计没有对错之分,只有好坏之分。不要为了两段相同的能够写在一起而将完整的属性无脑拆拆拆,优雅设计的背后都有深思熟虑的设计过程。

原文地址 : https://zhuanlan.zhihu.com/p/31362082

OA-一次审批流程控制的简单数据库表设计

原文地址 : https://juejin.cn/post/7197025946930462779

前言

  • OA(office automation),简单来说就是做审批流程控制的
  • 写本文原由是实习期间做的是有关教务系统的东西,其核心也就是做审批流程控制,因此想写点东西出来小结小结(目前来说个人水平不咋地,写此文也是站在个人的角度上去写的,所以写的可能会很差,也会有错,勿喷)
  • 此文重点在于数据库层面上的设计,实现逻辑上有牵扯到,但不多,目前不是很懂后端接口的实现逻辑(虽然能跑,但自己总想到其他乱七八糟的问题,后端接口怎么写好点还为想好),=后面有时间再研究再去补

1. 数据库设计

  • 主要是牵扯到三个表(业务表,审核主表,审核明细表)

1.1 业务表


  1. 实际上,对于某个系统来说,其审核的业务类型不仅仅只有一个
  2. 比如说有请假类型的,采购类型的等等
  3. 此业务表就是用来存储某种业务所拥有的信息的
  4. 也就是说你审批业务有多少种类,其业务表就对应有多个
  5. 本文我们会以请假类型的业务作为示例,所以此处的业务表就为请假表
  6. 就算是同一种类型的审批业务,对于不同的单位来说,所拥有的字段信息也是不同的,而本文重点又在于流程控制
  7. 所以此处为例的请假表我们知道有就行了(就不做字段设置了,实际生产环境需要用什么字段加上去即可),此表最重要的是其主键字段Id为我们后面联表查询
    Xnip2024-02-21_10-57-32.png

1.2 审核主表

  1. 审核主表主要是做一个总的流程控制管理的
  2. 该表设计如下
    Xnip2024-02-21_10-58-44.png
  • 讲讲为啥这样搞:

    • 主键id没啥好说的
    • 而审核类型audit_type和审批编号flow_no这是为了方便回找具体的业务记录信息的

      • 通常来说审核类型是具有多种的,为了减少其值在数据库中的占用,通常我们会采用映射方法表示其含义,如下图所示

Xnip2024-02-21_11-00-07.png

    • 以至于通过审核类型audit_type的值,我们可以定位到具体的哪张业务表,然后在通过审批编号flow_no的值,作为对应业务表的主键id,寻找到对应记录,从而获取此业务相关信息。
  • 审核状态audit_status顾名思义表示总的审核状态,通常也是采用映射方法表示其含义,如下图所示

Xnip2024-02-21_11-02-21.png

  • 实际上如果说系统只有一个请假的审核业务,业务表和审核主表是可以合在一起的(为啥?为了后续做各种功能实现时少写点SQL,多点时间摸鱼它不香么-.-)
  • 注意:实际上应该还需增加几个字段,这里到后面引出问题时会自动补充(现在直接就给出来的话感觉不自然)

1.3 审核明细表

  • 审核明细表的记录实际上对应的是某个审核业务记录,每个审核节点的一些审核信息
  • 该表设计如下:
    Xnip2024-02-21_11-03-23.png
  • 讲讲为啥这样搞

    • 审核idaudit_id,是作为外键用来连接审核主表的id,表示当前记录从属于其对应的审核业务的(起名困难症,这个不知道其啥名好,就瞎搞了个)
    • 审核人idaudit_user_id,顾名思义,表明当前审核节点应该让谁来审核
    • 审核状态audit_status,表示当前审核节点的审核人审核结果,通常也是采用映射法来表示其含义

Xnip2024-02-21_11-04-25.png

    • 其他字段也没啥好说的,如果实际需要,自己额外+字段即可

1.4 小结

  • 三个表之前的联系如下
    Xnip2024-02-21_11-05-21.png
    • 这样三个表之间的关系就串联起来了,通过某个审核业务记录id,可以查到对应审核状况,通过某条审核记录id,可以查看对应审核业务相关的信息

2. 所示

  • 在这里我们会通过一个员工请假审批的案例来描述数据库字段的变化(重点在于数据库的变化,后端接口逻辑处理这里不多说)
  • 假设目前来说,此员工的请假审批节点为:小组长-->部门主任-->经理

2.1 员工发出申请

  1. 接口中获取前端传来的请假表单信息,在请假表中新增一条记录(对应字段信息填充上去即可)
  2. 在审核主表中同样新增一条记录表示有新的审核记录过来

    • id(主键):主键自增还是uuid,看个人
    • flow_no(审批编号):就是刚刚在请假表中新增记录的主键id
    • audit_type(审核类型):根据预先定义好的映射规则,凭借具体的审核类型做自动映射填充(在实际开发中可能在-
    • controller层,一个接口对应一个审核类型业务,也可能一个接口对应多种审核类型业务, 前者很好说,可以把映射规则定义成常量的形式--后面如果要改映射规则的话,直接改常量即可,方便后面维护,直接填充就好,而后者的话,实际上也可以让前端传来一个标志号,用来表示此审核业务的类型,后端拿到后再做具体分析填充)--不做映射,直接填充具体审核类型的名称也可以,看个人喜欢
    • audit_status(审核进度):现在是申请人发出申请,这里值毫无疑问是出于"待审",当然,按照个人习惯以及上述审核主表的设计,这里我也采用映射,也即此时值为"1"
  3. 最后则是轮到在审核明细表中新增记录了(这里有个 【纠结点】 ,3个审核节点,一种是直接对应新增三条记录,还有就是一条记录一条记录增,这里为了方便描述,采用第一种方式,第二种方式会在后面说啥情况)

    • id(主键):主键自增还是uuid,看个人
    • audit_id(审核id):就是刚刚在审核主表中新增记录的主键id
    • audit_user_id(审核人id):分别把小组长,部门主任,经理的用户id查询出来填充到对应新增记录的字段值即可
    • audit_time(审核时间)、audit_opinion(审核意见)、audit_remark(审核备注):这些审核人都还没开始审呢,均为null
    • audit_status(审核状态):由于新增审核审批后,小组长作为第一个审核的节点,所以是"待我审核”,而部门主任,经理需等待前审核节点的审核,所以均为"审核中",根据映射规则填充即可
  1. 注意:这是在同一事务中操作的

2.2 小组长审核同意


  1. 根据前端传来的具体业务记录的id,在审核主表中,审核明细表查的对应记录信息
  2. 审核明细表:

    • 根据前端传来的表单信息在小组长的记录中更改为对应字段值(如审核时间,审核意见等等,审核状态也即为"通过"映射的值3)
    • 由于后续还有审核节点,因此此时需要再做多一步,将下一级审核结点(也就是将部门主任那条记录的审核状态改为"待我审核"映射的值2)
  3. 审核主表(这里也有个小分类)

    • 后续还有审核节点:
    1. 也就是当前这种情况,后面还有部门主任,经理这些审核节点
    2. 此时审核主表中的这条记录的audit_status审核进度字段就无需更改
    • 后续无审核节点:
    1. 当后面再无审核节点时
    2. 也就意味着,当前审核节点通过审核后,此审核业务记录总体上也是审核通过
    3. 此时需要把审核主表的这条记录的audit_status审核进度字段更改为"通过"映射的值3
  4. 注意:这里也是要在同一事务下

2.3 部门主任审核驳回

  1. 同理,根据前端传来的具体业务记录的id,在审核主表中,审核明细表查的对应记录信息
  2. 审核明细表:

    • 根前端传来的表单信息,在部门主任的记录中更改为对应字段值(如审核时间,审核意见等等,此时的审核状态即为"驳回"映射的值4)
  3. 审核主表:

    • 直接将对应记录的audit_status审核状态字段值改为“驳回”映射的值3即可
  4. 注意:这也需要再同一事务下

2.- 员工撤销申请

模拟下员工撤销申请的操作
在审核主表对应的记录中,将audit_status审核状态字段改为"撤销"映射的值4就好
而审核明细表中对应的记录其实可以不用动(实际上每次对审核明细表对应记录做操作时,都需要把审核主表对应记录的audit_status字段值查出来,比较一下是否可操作,这是牵扯到后端接口的写法,还没有理清楚,这里重点在于描述数据库的字段值变化)

2.- 谁审核的??

  1. 模拟下环境
  2. 假设员工发出请假申请后,隔了一段时间再登录系统查看审核情况
  3. 这也很好操作,直接把审核主表对应记录的audit_status审核状态显示出来即可
  4. 4个审核状态,通过,撤销状态倒是很好理解
  5. 比如员工说想进一步了解,目前是待哪位领导审核,还是说想知道到底是哪位领导驳回了他的申请
  6. 此时就需要到审核明细表中查询所示了,审核明细表中的审核状态映射如下所示
    Xnip2024-02-21_11-10-38.png
  7. 想查待哪位领导审核,就可以通过审核主表对应记录的主键id以及 "待我审核"映射的值2到审核明细表中查询,即可唯一确定一条记录,从而获取到用户id
  8. 想查哪位领导驳回了申请,同理查询即可

3. 存在问题

3.1 疑问点

  • 也即是在章节【2.1】中提到的,新发出申请在明细表中新增记录时,是直接新增三条还是一条一条新增
  • 新增三条意思很明显,一下子把所有审核结点的基础信息给添加上去
  • 这里特别解释下一条一条新增时啥含义:

    • 在审批流中,通常是只有存在一个流行结点(自己瞎定义的一个节点概念,不知道业界有没有这个专业术语)
    • 说人话就是只有待某个节点的那审核人审核后,才会进入下一节点的审核流程,在其审核的声明周期中,处于活跃状态的是只有一个结点
    • 也就代表前置审核节点未审核时,或者审核驳回时,后续节点的记录是没啥存在意义的
    • 而一条一条新增的含义,也就是待某个结点审核通过后,我才往明细表中添加下一节点的记录
  • 至于我在疑问啥?就是如果是直接采用发出申请时,新增三条记录这种形式的话
  • 那么如果第一结点审核驳回的话,后续两个结点的记录信息也就是多余的
  • 注意,这是我举例请假审核流程的三个节点出来的
  • 实际中,某种审核业务的审核节点可能有多个,也可能存在多种审核业务
  • 此时如果遭遇前置审核节点审核驳回的情况时,这时候审核明细表中这些记录的多余可能说就不能忽视了(虽然说实际上很可能是遇不到这种情况,或者说是不介意一点点硬盘的占用)
  • 而采用一条一条新增这种形式时,就不会采生如此诸多的多余记录,就是写后端接口时要稍微麻烦一丢丢,某个节点审核通过后,得主动找下一个审核节点
  • 在之前做一个HRM系统时,有接触到Actviti工作流引擎,了解到采用的就是这种一条一条新增的形式
  • 正如上述所说,直接新增三条这种形式造成多余的记录,虽然实际上影响是可以忽略不计的
  • 但是此时写出来,就是想提个醒,让自己知道,这种可能会造成的问题,要怎么优化(步是一点点进的)
  • 当然也可以骂我考虑复杂了

3.2 寻找下一节点

  • 特别注意,我们上述举的请假例子(只有3个审核节点)是很简化的,审核节点也是固定,不轻易更改的
  • 当某个审核结点通过后,我们即可知道下一个审核结点是谁,写其代码来也比较简单
  • 但还是老实际,审核业务是有多种类型的,某种审核业务的审核结点也可能是比较长的
  • 如果还是这样做的话,在审核流程的代码中就得添加寻找下一节点业务代码,就有点乱了
  • 比如说一个具有十级审核节点的审核业务,当第五级节点审核通过后,就得去找第六级审核节点是谁,开发的时候分得清没啥问题,就是后面他人维护时就一脸懵逼了
  • 如何改呢?首先得往审核主表和审核明细表中新增几个字段,如下所示

Xnip2024-02-21_11-13-51.png

    • totoal_point:代表此审核业务总的审核结点数
    • now_point:代表当前审核业务处于第几级审核结点处

Xnip2024-02-21_11-14-36.png

  • now_point:代表此记录处于第几级审核节点

  • 还是举请假的例子,当发出申请时(注意此时往审核明细表中添加记录时仍然采用一下子新增3条的形式)
    • 在审核主表中:

      • total_point值为3,代表共有3个审核节点
      • now_point值为1,代表目前审核流程处于第一个审核节点处
    • 在审核明细表中:

      • now_point:根据含义,小组长值为1,部门主任值为2,经理值为3
  • 当小组长审核同意后,根据total_pointnow_point的联系,自然得知下一节点,改其中状态即可,代码中无需额外搞小动作
  • 同时根据经理审核同意后,又根据total_pointnow_point的联系,自然得知此时此审核业务记录已经总体通过
  • 这样一看,寻找下一节点问题似乎得到了解决
  • 看是一切挺好,但是还是有问题
  • 就是审核节点的确定,要知道就算是同一类型的审核业务,也可能会由于你所处的部门、职位不同,得到的审核节点是不一样的
  • 就比如经理请假的申请,可能需要总经理-->总裁的审核,而正常的员工请假申请,就需要比较多的审核节点
  • 当发出申请时,审核的总节点数以及每级的审核员到底如何确定呢?

    • 前端可视化选择:

      • 也就是在系统上发出申请时,给个可视化界面列出领导信息,同时让申请人逐级往上选择各个审核领导
      • 后端接口收到请求时,就把所有审核节点信息给确定下来了
      • 这是比较好的解决方案,所处部门人员总会知道部门领导是谁,至于选到哪个职位的领导为最终节点,看实际情况
    • 在接口处逻辑处理:

      • 这个有点相当于把问题转移了,把之前在具体的审核流程中寻找下一节点的问题转为在发出申请时就确定各个审核节点的问题

3.3 某节点审核是得具有某个职位的人员


  • 为啥有这个呢?这是在实习期间无意中碰到的一个点
  • 到现在这里,我们举的请假例子中,基本每个审核节点就是要特定的人员审核
  • 但是,并不是实际上都是这样的,就比如拿在实习期间碰到的例子为例,一个科研项目的申报,最初由所在院系辅导员审核,然后是学校科研部门管理员审核,最后是到所在院系院长
  • 问题就出在学校科研部门管理员身上,这个实际上是指具有这个职称/角色的人员,是不仅一个的
  • 也就是说具有学校科研部门管理员职称/角色的人,都可以审核
  • 按照我们之前的做法,是明显达到不了这个功能呢
  • 要做的话,其实也很简单,审核明细表中新增字段即可,如下图所示
    Xnip2024-02-21_11-21-42.png
  • 实际上这个字段应该也是玩映射的,举个例子如下所示
    Xnip2024-02-21_11-22-41.png
  • 这样的话根据此值来判断即可(主要是业务接口处的逻辑处理)
  • 至于如果值为2时,也就具有某个职称的人员均可审核时,这个职称的id号该存哪,再新增个字段来存也可以,也可以直接存在audit_user_id字段里,自己分的清楚就好,就是这么离谱

4. 小结

  • 不知道说啥
  • 这个用来做简单的OA应该没啥问题
  • 复杂一点的OA暂时没了解过
  • 之前了解到Activiti,是通过数据库中的数据来存储审核流程的
  • 好像也可以改成配置文件,诸如properties
  • 后面可以试一下能不能改-.-

Go实战项目-Beego的Session、日志文件的使用和redis的选择使用

[踩坑] AES在线加密输出结果和 PHP 加密结果不一致问题修复

Xnip2024-02-21_11-27-35.png

上面的 加密方式 模式为ECB 填充Pkcs5Padding 编码Hex字符集 utf8(unicode编码)

Java 加密后能保证输出和 在线站点加密结果 一致, 但php 不求行.

给出两个demo

package org.example.jdk21demo.itpay;

import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.security.Key;
import org.apache.commons.codec.binary.Hex;

/**
 * @author Stephen
 * @since 2024/2/19 19:39
 */
public class AesUtils {

    private static final String ALGORITHM = "AES/ECB/PKCS5Padding";

    /**
     * Encrypts a string using AES encryption in ECB mode with PKCS5Padding.
     *
     * @param data The string to be encrypted.
     * @param key  The encryption key.
     * @return The encrypted string in hex format.
     * @throws Exception If an encryption error occurs.
     */
    public static String encrypt(String data, String key) throws Exception {

        Key secretKey = new SecretKeySpec(key.getBytes(), "AES");
        Cipher cipher = Cipher.getInstance(ALGORITHM);
        cipher.init(Cipher.ENCRYPT_MODE, secretKey);
        byte[] encryptedBytes = cipher.doFinal(data.getBytes());
        return Hex.encodeHexString(encryptedBytes);
    }

    /**
     * Decrypts a string using AES encryption in ECB mode with PKCS5Padding.
     *
     * @param encryptedData The encrypted string in hex format to be decrypted.
     * @param key           The decryption key.
     * @return The decrypted string.
     * @throws Exception If a decryption error occurs.
     */
    public static String decrypt(String encryptedData, String key) throws Exception {
        Key secretKey = new SecretKeySpec(key.getBytes(), "AES");
        Cipher cipher = Cipher.getInstance(ALGORITHM);
        cipher.init(Cipher.DECRYPT_MODE, secretKey);
        byte[] decryptedBytes = cipher.doFinal(Hex.decodeHex(encryptedData.toCharArray()));
        return new String(decryptedBytes);
    }

    public static void main(String[] args) {
        try {
            String key = "R/xxxxxxxxxxxxxx=="; // Replace this with your actual key
            String originalString = "hello1111111111";

            String encryptedString = AesUtils.encrypt(originalString, key);
            String decryptedString = AesUtils.decrypt(encryptedString, key);

            System.out.println("Original: " + originalString);
            System.out.println("Encrypted (Hex): " + encryptedString);
            System.out.println("Decrypted: " + decryptedString);


        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

上面 是java 的demo 输出没问题 6dce953815c585e4321112d1582b4b4a

<?php
class AesUtils
{
    const ALGORITHM = 'aes-192-ecb'; // AES-192 : 需要提供24位的密钥Key
//    const ALGORITHM = 'aes-256-ecb'; // AES-256 : 需要提供32位的密钥Key
//    const ALGORITHM = 'aes-128-ecb';// AES-128 : 需要提供16位的密钥Key
//    const ALGORITHM = 'AES/ECB/PKCS5Padding';

    /**
     * Encrypts a string using AES encryption in ECB mode with PKCS5Padding.
     * @param string $data The string to be encrypted.
     * @param string $key The encryption key.
     * @return string The encrypted string in hex format.
     * @throws Exception If an encryption error occurs.
     */
    public static function encrypt($data, $key)
    {
        $cipher = openssl_encrypt(
            $data,
            self::ALGORITHM,
            (string)$key,
            OPENSSL_RAW_DATA
        );
        return bin2hex($cipher);
    }

    /**
     * Decrypts a string using AES encryption in ECB mode with PKCS5Padding.
     * @param string $encryptedData The encrypted string in hex format to be decrypted.
     * @param string $key The decryption key.
     * @return string The decrypted string.
     * @throws Exception If a decryption error occurs.
     */
    public static function decrypt($encryptedData, $key)
    {
        $encryptedData = hex2bin($encryptedData);
        $decrypted = openssl_decrypt($encryptedData, self::ALGORITHM, (string)$key, OPENSSL_RAW_DATA);
        return $decrypted;
    }
}

// R/9U2DxTzoN7BGq1Yt1cvw==
$key = "R/xxxxxxxxxxx=="; // 隐藏key
$originalString = "hello1111111111";

try {
    $encryptedString = AesUtils::encrypt($originalString, $key);
    $decryptedString = AesUtils::decrypt($encryptedString, $key);

    echo "Original: " . $originalString . "\n";
    echo "Encrypted (Hex) 1: 6dce953815c585e4321112d1582b4b4a \n";
    echo "Encrypted (Hex) 2: " . $encryptedString . "\n";
    echo "Decrypted: " . $decryptedString . "\n";
} catch (Exception $e) {
    echo $e->getMessage();
}

php demo ,数据结果很难说, 重点问题在于 key 的字节长度需要修改 加密方式

    const ALGORITHM = 'aes-128-ecb'; // AES-128 : 需要提供16位的密钥Key
    const ALGORITHM = 'aes-192-ecb'; // AES-192 : 需要提供24位的密钥Key
    const ALGORITHM = 'aes-256-ecb'; // AES-256 : 需要提供32位的密钥Key

注意呦 , 长度不一样

JSON Web Token (JWT) 入门指南

JSON Web Token (JWT) 是一种开放标准 (RFC 7519),用于在网络应用环境中以安全的方式传递信息。它常用于身份验证和信息交换,尤其是在跨域认证的场景中。本文将介绍与 JWT 相关的几个重要方面,并通过图解帮助理解其原理和使用。

一、认证流程

2024-10-17T08:07:02.png

  • 用户使用账号(手机/邮箱/用户名)密码请求服务器
  • 服务器验证用户账号是否和数据库匹配
  • 服务器通过验证后发送给客户端一个 JWT Token
  • 客户端存储 Token,并在每次请求时携带该 Token(如何携带?)
  • 服务端验证 Token 值,并根据 Token 合法性返回对应资源(如何验证?)

二、跨域认证的问题

在现代 Web 应用中,跨域认证是一个常见问题。随着单页应用(SPA)和微服务架构的普及,前端和后端服务可能部署在不同的域名或端口下。这种情况下,传统的会话认证方式(如 Cookie)可能面临以下挑战:

  1. 同源策略:浏览器的同源策略限制了跨域请求,导致无法直接访问不同域的资源。
  2. 状态管理:使用 Cookie 进行认证时,必须在每次请求中携带 Cookie,增加了状态管理的复杂性。
  3. 安全性:需要确保跨域请求的安全性,防止 CSRF(跨站请求伪造)等攻击。

JWT 作为一种无状态的认证方式,能够有效解决这些问题。它通过在客户端存储令牌,避免了传统会话管理的复杂性。

三、JWT 的原理

JWT 的基本原理是将用户的身份信息和其他相关数据编码为一个签名的字符串。这个字符串包含三个部分:头部(Header)、载荷(Payload)和签名(Signature)。

  1. 头部:通常由两部分组成,类型(即 JWT)和签名算法(如 HMAC SHA256)。
  2. 载荷:包含用户信息和其他数据,如用户 ID、角色、过期时间等。
  3. 签名:通过将编码后的头部和载荷与密钥结合,使用指定的算法生成的签名,用于验证信息的完整性和真实性。

通过这种机制,JWT 能够在不同的域之间安全地传递用户身份信息。

JWT 的结构示意图

JWT = Header.Payload.Signature
+------------------------------------+
|      Header                        |
| {                                  |
|   "alg": "HS256",                  |
|   "typ": "JWT"                     |
| }                                  |
+------------------------------------+
|      Payload                       |
| {                                  |
|   "userId": "123",                 |
|   "name": "John Doe",              |
|   "iat": 1516239022                |
| }                                  |
+------------------------------------+
|      Signature                     |
| HMACSHA256(                        |
|  base64UrlEncode(header) + "." +.  |
|  base64UrlEncode(payload), secret) |
+------------------------------------+

四、JWT 的数据结构

JWT 的数据结构由三个部分组成,使用点(.)分隔:

xxxxx.yyyyy.zzzzz
  • xxxxx:编码后的头部,通常为 JSON 对象,包含 token 的类型和算法信息。
  • yyyyy:编码后的载荷,包含声明(Claims),即用户的身份信息和其他相关数据。
  • zzzzz:签名,由头部、载荷和密钥生成,确保数据未被篡改。

示例

一个简单的 JWT 可能看起来像这样:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

五、JWT 的使用方式

1. 用户登录

用户通过提供用户名和密码进行登录,服务器验证后生成 JWT,并将其返回给客户端。

2. 客户端存储

客户端可以将 JWT 存储在 localStoragesessionStorage 中,作为后续请求的身份凭证。

3. 发送请求

在后续的 API 请求中,客户端将 JWT 作为 Authorization 头的一部分发送给服务器:

Authorization: Bearer <token>

4. 服务器验证

服务器接收到请求后,解析 JWT,验证签名和有效性,从而确定用户的身份。

六、JWT 的几个特点

1. 无状态

JWT 是无状态的,服务器不需要存储会话信息,所有信息都包含在 JWT 中。这使得扩展和负载均衡变得简单。

2. 自包含

JWT 包含所有必要的信息,客户端可以独立地进行身份验证,无需额外的数据库查询。

3. 支持跨域

由于 JWT 是一个字符串,可以轻松地在不同的域之间传递,解决了跨域认证的问题。

4. 安全性

通过签名机制,JWT 确保了信息的完整性和真实性。可以使用对称或非对称加密算法增强安全性。

5. 可扩展性

JWT 可以包含自定义的声明(Claims),使得开发者可以根据需要扩展用户信息。

参考文献

  1. RFC 7519: JSON Web Token (JWT)
  2. JWT.io - JSON Web Tokens
  3. 阮一峰的网络日志 - JSON Web Token 入门教程
  4. Auth0 - Introduction to JSON Web Tokens
">