请简述一下 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

原文地址 : http://www.35coder.com/convert_encryption_codes_to_php/

写代码的经历中,总少不了与外部的程序对接,一旦有这样的事,往往周期会很长,很麻烦,因为你要考虑的事会多了很多,其中安全性的加密解密就是重要的一项。写代码,可以出Bug,但逼格不能弱。什么是逼格?和别人对接一下,连加密解密都没有,连验证签名都没有,别人一眼就望穿你,这就是眼界的问题了。

这次的故事是对接一个大的支付系统,对方也是第一个对接我们,然后定了接口和加解密算法,给了个Java的Demo,问了声,有没有PHP的,没有,歇菜,自己来吧。
代码说多不多,说少不少,为了先说事,代码放在最后面。

第一个坑:加密算法多多,你到底要闹咋样?

码农兄弟们可以先下去看一眼代码,然后说说它用了啥算法?
接口传输的安全性算法,首先要区分是加签名还是加密?区别是,签名+原文可以验证收到的信息是否被篡改,不过别指望有了签名就可以还原出原文来。加密就是不让别人看到原文是啥,然后给把钥匙,要让接收的人解密看出原文来。两者的算法基本上来说,就是完全不同的。

加密还分对称非对称。对称有:DES3DESTDEABlowfishRC2RC4RC5IDEASKIPJACKAES等,非对称有:RSAElgamal背包算法RabinD-HECC(椭圆曲线加密算法)

还有,你以为拿个公钥就够了?当然不是,还要一对。更坑的是,可能有不同的格式。对方给了我一个keystore格式的,发觉php完全不支持,想办法转成了pem格式的。

常见的密钥格式:jksjcep12pfxbksubr等等
常见的证书文件格式:cer,crtrsap7b,p7r,p7c,pem,p10,csr等等

最后还有一点,这次碰到的算法具体的参数。
我这次遇到的是3DES加密,AlGORITHM = "DESede/ECB/PKCS5Padding";,后面的类型和填充方式都不能差一点。

第二个坑:到底什么是密钥?

你会说这个很简单啊
Java里就是像这样:

PrivateKey privateKey = (PrivateKey) keyStore.getKey(keyAlias, password.toCharArray()); 

php里就是像这样:

$privateKey=openssl_pkey_get_private(file_get_contents($this->privatePemFilePath)); 

你以为你以为的就是你以为的吗?前面说了,即使算法一样,密钥格式不一样,开发语言一样,用法也完全不一样。

上面的只是格式不同,下面的还有编码的不同:
看起来我从代码里读出的密码是这个,但其实送入算法中的可不是,还要做一个Base64转换,如果你送入了错误的,会永远困在迷惘中。

$this->dESCORPKey = C('lakala_encrypt_key');
$key = $this->$dESCORPKey;
$encryptData = self::encrypt($key, $signedReq);
...
public function encrypt($key,$data){
    $decode_key = base64_decode($key);//此处需要BASE64解码(变为2进制)
    $encData = openssl_encrypt($data, 'DES-EDE3', $decode_key, OPENSSL_RAW_DATA);
    $encData = base64_encode($encData);
    return $encData;
}

PS:网上有在线加密解密算法的工具,往往和代码出来的结果不一致,除了各种参数都需要写对以外,特别注意密码(密钥)的输入格式,要不要Base64编码或者解码。

第三个坑:带中文的字符串格式

看一眼下面的代码,你就会知道,php里有很多json_encodejson_decodejava代码里有很多getByte()这样转换成字节的操作,一个经典的问题就来了,你如果传入了中文,中文按什么格式进行编码?编码不同,决定了加密算法操作时二进制的不同,也决定了最终输出的不同。
在写代码的时候查阅了javagetByte()方法,默认值竟然是读取机器的字符格式!!!所以,写代码的时候一定要注意,最好加上具体格式,不要用默认值,要这么写:getByte("UTF-8"),否则换了机器表现不一样,你会死的很难看。

第四个坑:语言的问题

虽然上帝说人人平等,但现实远远复杂的多。
虽然加密解密本质上说就是一种运算变换,什么语言都可以实现的,可你不会从底层开始搞的,那就涉及语言的问题。
最简单的例子:java里具体运算时都会把String转换成byte进行操作。PHP就不行。

加密算法这个玩意虽然复杂,不过前人已经给我们造了很多轮子了,网上随便一搜就可以找到很多资料,问题是没有成系统,特别各个语言间的轮子差异还蛮大的。如何做适配,在网上的资料非常的难找。

PHP里加密方法还有mcryptopenssl两套,互相之间的写法还差异蛮大的,让人头疼。

举个栗子(这里的Java解析使用了fastjson):

1 java中:
2 CommReq req = JSON.parseObject(jsonStr, CommReq.class, Feature.OrderedField);
3 PHP中:
4 $req = json_decode(trim($jsonStr), true);
5 ksort($req);

看起来很像了吧?才不是呢!以下是输入的Json

{
    "head": {
        "serviceSn": "B5D79F38B96040B7B992B6BE329D9975",
        "serviceId": "MPBF001",
        "channelId": "05",
        "inputSource": "I002",
        "opId": "",
        "requestTime": "20180628142105",
        "versionId": "1.0.0",
        "businessChannel": "LKLZFLLF"
    },
    "request": {
        "userId": "40012345678",
        "userName": "AA",
        "userMobile": "18675529912",
        "idNo": "110101198609096078"
    }
}

问题出在具体的类型定义,以及Json解析的深度
JAVA中有定义具体按哪个类型解析

1 public class CommReq implements Serializable {
2     private static final long serialVersionUID = 1L;
3     private CommReqHead head;
4     private String request;
5 }

JAVA这种强类型语言,必须定义类型,将request定义成String型,导致JSON解析出来的结果:

"request":"{\"userId\":\"40012345678\",\"userName\": \"AA\", \"userMobile\": \"18675529912\", \"idNo\": \"110101198609096078\" }";

PHP是弱类型语言,直接嵌套进去也解析出来了

"request": {"userId":"40012345678","userName": "AA", "userMobile": "18675529912", "idNo": "110101198609096078" } }

如果PHP要和JAVA真正保持一致(因为一旦不一致,加密结果就不一样了)

1 $req = json_decode(trim($jsonStr), true);
2 ksort($req);
3 req['request']=json_encode(req['request']);

前面也提到了密钥类型的问题,其实也和语言有关,PHP只支持PEM格式,所以,还用工具对keystore进行了转换,转换之后发现几个密码都已经不需要了。生成公钥PEM私钥PEM加上加密解密Key就可以了。

小结

回顾来看,其实只是解决了很小的一个问题,将一段JAVA代码转换成了PHP代码,甚至中间复杂的算法细节都调用原来就有的模块,更不用怀疑这些模块写的算法的正确性,但调试这样一个东西,却的的确确花费了非常大的精力。技术没有任何中间地带,只有行或者不行,容不得半分作假。开发必须要注重细节,细节到位了才不会出Bug,这点在加密解密这件事上,尤其的明显,输入差一个字符,输出完全不同。开发没有很容易的事,只有我做过,我熟悉的事。

代码在这里哦。钥匙就不提供了,这样直接copy代码跑不起来是正常的。哈哈

/**
 *
 */
package com.chuangmi.foundation.lakala.service.impl;

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Security;
import java.security.Signature;

import javax.annotation.PostConstruct;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import javax.security.cert.X509Certificate;
import javax.servlet.http.HttpServletRequest;

import org.apache.commons.codec.binary.Base64;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONException;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.parser.Feature;
import com.chuangmi.foundation.lakala.service.SignService;
import com.chuangmi.foundation.lakala.service.models.CommReq;
import com.chuangmi.foundation.lakala.service.models.CommRequest;
import com.chuangmi.foundation.lakala.service.models.CommRes;
import com.chuangmi.foundation.lakala.service.models.CommResponse;


@Service
public class SignServiceImpl implements SignService {

    private static final Logger logger = LoggerFactory.getLogger(SignService.class);

    @Value("${cer.filePath}")
    private String cerFilePath;

    @Value("${key.filePath}")
    private String keyFilePath;

    @Value("${key.passWord}")
    private String keyPassWord;

    @Value("${key.alias}")
    private String alias;

    @Value("${encrypt.key}")
    private String dESCORPKey;

    /**
     * 加密算法与填充方式
     */
    public static final String AlGORITHM = "DESede/ECB/PKCS5Padding"; // 定义加密算法,可用

    @PostConstruct
    public void init(){
        if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null){

            System.out.println("security provider BC not found, will add provider");

            Security.addProvider(new BouncyCastleProvider());

        }
    }

    /**
     * 加签并加密需要发送的请求报文
     * @param 接口报文
     * @return 加签后可以发送的json密文
     * @throws JSONException
     */
    @Override
    public String signRequestJsonToSend(String jsonStr) throws JSONException {
        JSONObject resultObj = null;
        if(jsonStr.indexOf("\"head\"") >= 0)
        {
            CommReq req = JSON.parseObject(jsonStr, CommReq.class, Feature.OrderedField);
            // 对报文体签名
            String signData = signData(req.getRequest());

            logger.info("加签成功,原始报文:" + jsonStr + ",报文体签名:" + signData);

            req.getHead().setSignData(signData);
            resultObj = JSONObject.parseObject(JSON.toJSONString(req), Feature.OrderedField);
        }
        else if(jsonStr.indexOf("\"comm\"") >= 0)
        {
            CommRequest req = JSON.parseObject(jsonStr, CommRequest.class, Feature.OrderedField);
            // 对报文体签名
            String signData = signData(req.getData());

            logger.info("加签成功,原始报文:" + jsonStr + ",报文体签名:" + signData);

            req.getComm().setSigntx(signData);
            resultObj = JSONObject.parseObject(JSON.toJSONString(req), Feature.OrderedField);
        }

        String signedReq = String.valueOf(JSONObject.toJSON(resultObj));
        logger.info("加签后的报文:" + signedReq);

        //加密
        byte[] key = Base64.decodeBase64(dESCORPKey);
        byte[] encryptData = encrypt(key, signedReq.getBytes());

        String encryptStr = Base64.encodeBase64String(encryptData);

        logger.info("加密成功,密文:" + encryptStr);

        return encryptStr;
    }

    /**
     * 加签并加密需要发送的响应报文
     * @param 接口报文
     * @return 加签后可以发送的json密文
     * @throws JSONException
     */
    @Override
    public String signResponseJsonToSend(String jsonStr) throws JSONException {
        JSONObject resultObj = null;
        if(jsonStr.indexOf("\"head\"") >= 0)
        {
            CommRes response = JSON.parseObject(jsonStr, CommRes.class, Feature.OrderedField);
            resultObj = JSONObject.parseObject(JSON.toJSONString(response), Feature.OrderedField);
        }
        else if(jsonStr.indexOf("\"comm\"") >= 0)
        {
            CommResponse response = JSON.parseObject(jsonStr, CommResponse.class, Feature.OrderedField);
            // 对报文体签名
            String signData = signData(response.getData());

            logger.info("加签成功,原始报文:" + jsonStr + ",报文体签名:" + signData);

            response.getComm().setSigntx(signData);
            resultObj = JSONObject.parseObject(JSON.toJSONString(response), Feature.OrderedField);
        }

        String signedReq = String.valueOf(JSONObject.toJSON(resultObj));
        logger.info("加签后的响应报文:" + signedReq);

        //加密
        byte[] key = Base64.decodeBase64(dESCORPKey);
        byte[] encryptData = encrypt(key, signedReq.getBytes());

        String encryptStr = Base64.encodeBase64String(encryptData);

        logger.info("加密成功的响应报文,密文:" + encryptStr);

        return encryptStr;
    }

    /**
     * 从request提取json data,并解密,验签, 验签成功则返回data,否则返回null
     * @param request
     * @return
     * @throws IOException
     * @throws JSONException
     */
    @Override
    public String verifyReceivedRequest(HttpServletRequest request) throws IOException, JSONException {
        String json = extractJson(request);
        logger.info("接收报文密文:" + json);

        return verifyRequestJson(json);
    }

    /**
     * 对收到的请求json解密,验签, 验签成功则返回data,否则返回null
     * @param json
     * @return
     * @throws UnsupportedEncodingException
     * @throws JSONException
     */
    @Override
    public String verifyRequestJson(String json) throws UnsupportedEncodingException, JSONException {
        //解密
        byte[] key = Base64.decodeBase64(dESCORPKey);
        byte[] decryptBytes = decrypt(key, Base64.decodeBase64(json));
        String orig = new String(decryptBytes, "UTF-8");
        logger.info("【收到的报文请求】接收报文:" + orig);

        // 验签
        JSONObject obj = JSONObject.parseObject(orig, Feature.OrderedField);
        String reqStr = String.valueOf(JSONObject.toJSON(obj));
        if(reqStr.indexOf("\"comm\"") >= 0)
        {
            CommRequest req = JSON.parseObject(reqStr, CommRequest.class, Feature.OrderedField);
            String signtx = req.getComm().getSigntx();

            logger.info("报文中的签名:" + signtx);
            boolean nRet = verifyData(req.getData(), signtx);
            if(nRet)
            {
                req.getComm().setSigntx("");
                return JSON.toJSONString(req);
            }
            else
            {
                return null;
            }
        }
        else if(reqStr.indexOf("\"head\"") >= 0)
        {
            CommReq req = JSON.parseObject(reqStr, CommReq.class, Feature.OrderedField);
            String signData = req.getHead().getSignData();

            logger.info("报文中的签名:" + signData);
            boolean nRet = verifyData(req.getRequest(), signData);
            if(nRet)
            {
                req.getHead().setSignData("");
                return JSON.toJSONString(req);
            }
            else
            {
                return null;
            }
        }
        else
        {
            return null;
        }
    }

    /**
     * 对响应的报文json解密,验签, 验签成功则返回data,否则返回null
     * @param json
     * @return
     * @throws UnsupportedEncodingException
     * @throws JSONException
     */
    @Override
    public String verifyResponseJson(String json) throws UnsupportedEncodingException, JSONException {
        //解密
        byte[] key = Base64.decodeBase64(dESCORPKey);
        byte[] decryptBytes = decrypt(key, Base64.decodeBase64(json));
        String orig = new String(decryptBytes, "UTF-8");
        logger.info("【收到的响应报文】报文:" + orig);

        // 验签
        JSONObject obj = JSONObject.parseObject(orig, Feature.OrderedField);
        String reqStr = String.valueOf(JSONObject.toJSON(obj));
        if(reqStr.indexOf("\"comm\"") >= 0)
        {
            CommResponse response = JSON.parseObject(reqStr, CommResponse.class, Feature.OrderedField);
            String signtx = response.getComm().getSigntx();

            logger.info("报文中的签名:" + signtx);
            boolean nRet = verifyData(response.getData(), signtx);
            if(nRet)
            {
                response.getComm().setSigntx("");
                return JSON.toJSONString(response);
            }
            else
            {
                return null;
            }
        }
        else if(reqStr.indexOf("\"head\"") >= 0)
        {
            CommRes response = JSON.parseObject(reqStr, CommRes.class, Feature.OrderedField);
            return JSON.toJSONString(response);
        }
        else
        {
            return null;
        }
    }

    public String extractJson(HttpServletRequest request) throws IOException {
        //用于接收对方的jsonString
        StringBuilder jsonString = new StringBuilder();
        BufferedReader reader = request.getReader();
        try {
            String line;
            while ((line = reader.readLine()) != null) {
                jsonString.append(line);
            }
        } finally {
            reader.close();
        }
        String data = jsonString.toString();
        return data;
    }

    /*  使用私钥签名,并返回密文
      * @param param  需要进行签名的数据
      * @return 签名
      */
    private String signData(String param)
    {
        InputStream inputStream = null;
        try {

            String store_password = keyPassWord;// 密钥库密码
            String password = keyPassWord;// 私钥密码
            String keyAlias = alias;// 别名
            // a. 创建针对jks文件的输入流

            inputStream = new FileInputStream(keyFilePath);// CA 文件名 如: D://tmp/encrypt.jks
            // input = getClass().getClassLoader().getResourceAsStream(keyFile);
            // 如果制定classpath下面的证书文件

            // b. 创建KeyStore实例 (store_password密钥库密码)
            KeyStore keyStore = KeyStore.getInstance("JKS");
            keyStore.load(inputStream, store_password.toCharArray());

            // c. 获取私钥 (keyAlias 为私钥别名,password为私钥密码)
            PrivateKey privateKey = (PrivateKey) keyStore.getKey(keyAlias, password.toCharArray());

            // 实例化一个用SHA算法进行散列,用RSA算法进行加密的Signature.
            Signature dsa = Signature.getInstance("SHA1withRSA");
            // 加载加密散列码用的私钥
            dsa.initSign(privateKey);
            // 进行散列,对产生的散列码进行加密并返回
            byte[] p = param.getBytes();
            dsa.update(p);

            return Base64.encodeBase64String(dsa.sign());// 进行签名, 加密后的也是二进制的,但是返回给调用方是字符串,将byte[]转为base64编码

        } catch (Exception gse) {

            gse.printStackTrace();
            return null;

        } finally {

            try {
                if (inputStream != null)
                    inputStream.close();// 判断
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

    }

    /*  用公钥证书对字符串进行签名验证
      * @param urlParam  需要进行签名验证的数据
      * @param signParam 编码后的签名
      * @return 校验签名,true为正确 false为错误
      */
    private boolean verifyData(String urlParam, String signParam)
    {
        boolean verifies = false;

        InputStream in = null;

        try {

            // a. 创建针对cer文件的输入流
            InputStream inputStream = new FileInputStream(cerFilePath);// CA 文件名 如: D://tmp/cerfile.p7b
            // input = getClass().getClassLoader().getResourceAsStream(keyFile);
            // 如果制定classpath下面的证书文件

            // b. 创建KeyStore实例 (store_password密钥库密码)
            X509Certificate cert = X509Certificate.getInstance(inputStream);

            // c. 获取公钥 (keyAlias 为公钥别名)
            PublicKey pubKey = cert.getPublicKey();

            if (pubKey != null) {
                // d. 公钥进行验签
                // 获取Signature实例,指定签名算法(与之前一致)
                Signature dsa = Signature.getInstance("SHA1withRSA");
                // 加载公钥
                dsa.initVerify(pubKey);
                // 更新原数据
                dsa.update(urlParam.getBytes());

                // 公钥验签(true-验签通过;false-验签失败)
                verifies = dsa.verify(Base64.decodeBase64(signParam));// 将签名数据从base64编码字符串转回字节数组
            }

        } catch (Exception gse) {
            gse.printStackTrace();
        } finally {

            try {
                if (in != null)
                    in.close();// 判断
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return verifies;

    }


    // DES,DESede,Blowfish
    /**
     * 使用3des加密明文
     *
     * @param byte[] key: 密钥
     * @param byte[] src: 明文
     *
     */
    private byte[] encrypt(byte[] key, byte[] src) {
        try {

            if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null){

                System.out.println("security provider BC not found, will add provider");

                Security.addProvider(new BouncyCastleProvider());

            }

            // 生成密钥
            SecretKey deskey = new SecretKeySpec(key, AlGORITHM);
            // 加密
            Cipher c1 = Cipher.getInstance(AlGORITHM);
            c1.init(Cipher.ENCRYPT_MODE, deskey);
            return c1.doFinal(src);// 在单一方面的加密或解密
        } catch (java.security.NoSuchAlgorithmException e1) {
            e1.printStackTrace();
        } catch (javax.crypto.NoSuchPaddingException e2) {
            e2.printStackTrace();
        } catch (java.lang.Exception e3) {
            e3.printStackTrace();
        }
        return null;
    }

    /**
     * 使用3des解密密文
     *
     * @param byte[] key: 密钥
     * @param byte[] src: 密文
     *
     */
    private byte[] decrypt(byte[] keybyte, byte[] src) {
        try {

            if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null){

                System.out.println("security provider BC not found, will add provider");

                Security.addProvider(new BouncyCastleProvider());

            }

            // 生成密钥
            SecretKey deskey = new SecretKeySpec(keybyte, AlGORITHM);
            // 解密
            Cipher c1 = Cipher.getInstance(AlGORITHM);
            c1.init(Cipher.DECRYPT_MODE, deskey);
            return c1.doFinal(src);
        } catch (java.security.NoSuchAlgorithmException e1) {
            e1.printStackTrace();
        } catch (javax.crypto.NoSuchPaddingException e2) {
            e2.printStackTrace();
        } catch (java.lang.Exception e3) {
            e3.printStackTrace();
        }

        return null;
    }

    public static void main(String[] args) throws JSONException {

        InputStream inputStream = null;
        try {

            String store_password = "123456";// 密钥库密码
            String password = "123456";// 私钥密码
            String keyAlias = "www.lakala.com";// 别名
            // a. 创建针对jks文件的输入流

            inputStream = new FileInputStream("/Users/rinson/eclipse-workspace/lakala/lakala-server/asdc.keystore");// CA 文件名 如: D://tmp/encrypt.jks
            // input = getClass().getClassLoader().getResourceAsStream(keyFile);
            // 如果制定classpath下面的证书文件

            // b. 创建KeyStore实例 (store_password密钥库密码)
            KeyStore keyStore = KeyStore.getInstance("JKS");
            keyStore.load(inputStream, store_password.toCharArray());

            // c. 获取私钥 (keyAlias 为私钥别名,password为私钥密码)
            PrivateKey privateKey = (PrivateKey) keyStore.getKey(keyAlias, password.toCharArray());

            // 实例化一个用SHA算法进行散列,用RSA算法进行加密的Signature.
            Signature dsa = Signature.getInstance("SHA1withRSA");
            // 加载加密散列码用的私钥
            dsa.initSign(privateKey);
            String param = "XXXXX";
            // 进行散列,对产生的散列码进行加密并返回
            dsa.update(param .getBytes());

            System.out.println(Base64.encodeBase64String(dsa.sign()));// 进行签名, 加密后的也是二进制的,但是返回给调用方是字符串,将byte[]转为base64编码

        } catch (Exception gse) {

            gse.printStackTrace();

        } finally {

            try {
                if (inputStream != null)
                    inputStream.close();// 判断
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

PHP

<?php

namespace Common\Lib\Lakala;

class SignService
{
    private $publicPemFilePath='';
    private $privatePemFilePath='';
    private $dESCORPKey = '';

    //初始化
    public function __construct()
    {
        $this->publicPemFilePath = C('lakala_cer_filePath');
        $this->privatePemFilePath=C('lakala_key_filePath');
        $this->dESCORPKey = C('lakala_encrypt_key');
    }

    /**
     * 加签并加密需要发送的请求报文
     * @param $head 是java类CommReqHead,需在调用时传入
     * @param $data 是具体的请求参数数组
     *
     */
    public function ToLakala($head,$data,$url){
        //CommReq
        $ret = [
            'head'=>$head,
            'request'=>$data,
        ];
        $ret = json_encode($ret);
        //会对整个请求body加签加密,返回字符串body体
        $params = $this->signRequestJsonToSend($ret);

        //http request
        $ch = curl_init($url);
        curl_setopt($ch, CURLOPT_POST, 1);
        curl_setopt($ch, CURLOPT_HEADER, 0);
        curl_setopt($ch, CURLOPT_FRESH_CONNECT, 1);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
        curl_setopt($ch, CURLOPT_FORBID_REUSE, 1);
        curl_setopt($ch, CURLOPT_TIMEOUT, 30);
        curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: application/json; charset=utf-8', 'Content-Length: ' . strlen($data)));
        curl_setopt($ch, CURLOPT_POSTFIELDS, $params);
        $result = curl_exec($ch);
        curl_close($ch);
        $result = $params;
        //验证返回结果
        $response = $this->verifyResponseJson($result);
        //结果返回
        return $response;
    }


    public function FromLakala(){
        $lakalaSign = new SignService();
        $params = I('');
        $params = $this->verifyRequestJson($params);
        return $params;
    }

    public function FromLakalaResponse($head,$response){
        $ret = [
            'head'=>$head,
            'response'=>$response,
        ];
        $res = $this->signResponseJsonToSend($ret);
        return $res;
    }

    /**
     * 加签并加密需要发送的请求报文
     * @param $jsonStr 接口报文(String类型)
     * @return 加签后可以发送的json密文
     */
    public function signRequestJsonToSend($jsonStr)
    {
        if(strpos($jsonStr,"\"head\"")!= false)
        {
            $req = json_decode(trim($jsonStr), true);
            ksort($req);
            // 对报文体签名
            $signData = $this->signData($req['request']);
            $req['head']['signData']= $signData;
            $req['request']=json_encode($req['request'],JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES);
            ksort($req['head']);
            $resultObj = $req;

        }
        else if(strpos($jsonStr,"\"comm\"")!= false)
        {
            $req = json_decode(trim($jsonStr), true);
            ksort($req);
            // 对报文体签名
            $signData = $this->signData($req['data']);
            $req['comm']['signtx']=$signData;
            $req['data']=json_encode($req['data'],JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES);
            ksort($req['head']);
            $resultObj = $req;
        }

        $signedReq = json_encode($resultObj,JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES);

        //logger.info("加签后的报文:" + signedReq);
        //此处直接放入要加密的数据
        $key = $this->dESCORPKey;
        $encryptData = self::encrypt($key,$signedReq);
        //logger.info("加密成功,密文:" + encryptStr);

        return $encryptData;
    }

    /**
     * 加签并加密需要发送的响应报文
     * @param $jsonStr 接口报文(String类型)
     * @return 加签后可以发送的json密文
     */
    public function signResponseJsonToSend($jsonStr) {
        if(strpos($jsonStr,"\"head\"")!= false)
        {
            $response = json_decode(trim($jsonStr), true);
            $resultObj = json_decode(json_encode($response));
        }
        else if(strpos($jsonStr,"\"comm\"")!= false)
        {
            $response = json_decode(trim($jsonStr), true);
            ksort($response);
            // 对报文体签名
            $signData = $this->signData($response['data']);

            //logger.info("加签成功,原始报文:" + jsonStr + ",报文体签名:" + signData);
            $response['comm']['signTx']=$signData;
               $response['data']=json_encode($response['data'],JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES);
            ksort($response['comm']);
            $resultObj = $response;
        }

        $signedReq = json_encode($resultObj,JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES);
        //logger.info("加签后的响应报文:" + signedReq);

        $key = $this->$dESCORPKey;
        $encryptData = self::encrypt($key, $signedReq);

        //logger.info("加密成功的响应报文,密文:" + encryptStr);
        return $encryptData;
    }

    /**
     * 对响应的报文json解密,验签, 验签成功则返回data,否则返回null
     * @param json (String)
     * @return (String)
     */
    public function verifyResponseJson($json) {
        //解密
        $key = $this->dESCORPKey;
        $decryptBytes = self::decrypt($key, $json);

        //logger.info("【收到的响应报文】报文:" + orig);

        // 验签
        $obj = json_decode($decryptBytes);
        $reqStr = json_encode($obj);
        if(strpos($reqStr,"\"comm\"")!= false)
        {
            $response = json_decode($reqStr,true);
            $signtx = $response['comm']['signtx'];

            //logger.info("报文中的签名:" + signtx);
            $nRet = $this->verifyData($response['data'], $signtx);
            if($nRet)
            {
                $response['comm']['signtx']="";
                return json_encode($response);
            }
            else
            {
                return null;
            }
        }
        else if(strpos($reqStr,"\"head\"")!= false)
        {
            return $reqStr;
        }
        else
        {
            return null;
        }
    }

    /**
     * 对收到的请求json解密,验签, 验签成功则返回data,否则返回null
     * @param json (String)
     * @return (String)
     */
    public function verifyRequestJson($json) {
        //解密
        $key = $this->dESCORPKey;
        $decryptBytes = self::decrypt($key, $json);

        // 验签
        $obj = json_decode($decryptBytes);
        $reqStr = json_encode($obj);
        if(strpos($reqStr,"\"comm\"")!= false)
        {
            $req = json_decode($reqStr,true);
            ksort($req);
            $signtx = $req['comm']['signtx'];

            //logger.info("报文中的签名:" + signtx);
            $nRet = $this->verifyData($req['data'], $signtx);
            if($nRet)
            {
                $req['comm']['signtx']="";
                return json_encode($req);
            }
            else
            {
                return null;
            }
        }
        else if(strpos($reqStr,"\"head\"")!= false)
        {
            $req = json_decode($reqStr,true);
            ksort($req);
            $signData = $req['head']['signData'];
            //logger.info("报文中的签名:" + signData);
            $nRet = $this->verifyData($req['request'], $signData);
            return $nRet;
            if($nRet)
            {
                $req['head']['signData']="";
                return json_encode($req);
            }
            else
            {
                return null;
            }
        }
        else
        {
            return null;
        }
    }
        /*  使用私钥签名,并返回密文
        * @param param  需要进行签名的数据(Array)
        * @return 签名(加签字符串String)
        */
    private function signData($param)
    {
        $content = json_encode($param,JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES);
        $privateKey=openssl_pkey_get_private(file_get_contents($this->privatePemFilePath));
        openssl_sign($content, $signature, $privateKey);
        openssl_free_key($privateKey);
        return base64_encode($signature);

    }

    /*  用公钥证书对字符串进行签名验证
      * @param urlParam  需要进行签名验证的数据(直接从解密报文中取出的String)
      * @param signParam 编码后的签名(直接从解密报文中取出的String)
      * @return 校验签名,true为正确 false为错误
      */
    private function verifyData($urlParam,$signParam)
    {
        $signature = base64_decode($signParam);
        $pubkeyid = openssl_pkey_get_public(file_get_contents($publicPemFilePath));
        // state whether signature is okay or not
        $verifies = openssl_verify($urlParam, $signature, $pubkeyid);
        return $verifies;
    }

    /**
     * @param $key获取的密钥字符串(直接从配置文件中取出)
     * @param $data (字符串)
     * @return string
     */
    public function encrypt($key,$data){
        $decode_key = base64_decode($key);//此处需要BASE64解码(变为2进制)
        $encData = openssl_encrypt($data, 'DES-EDE3', $decode_key, OPENSSL_RAW_DATA);
        $encData = base64_encode($encData);
        return $encData;
    }

    /**
     * @param $key获取的密钥字符串(直接从配置文件中取出)
     * @param $data (字符串)
     * @return string
     */
    public function decrypt($key,$data){
        $decode_key = base64_decode($key);//此处需要BASE64解码(变为2进制)
        $data = base64_decode($data);//此处需要BASE64解码(变为2进制)
        $decData = openssl_decrypt($data, 'DES-EDE3', $decode_key, OPENSSL_RAW_DATA);
        return $decData;
    }

}

要查看一个进程已经创建了多少子进程,可以通过使用系统工具和命令来获取相关信息。下面是几种常用的方法:

  1. 使用ps命令:

    ps -eLf | grep <进程ID>
    ```
    用上述命令替换`<进程ID>`为你要查看的进程的ID。这会列出指定进程及其子进程的详细信息,包括进程ID(PID)和父进程ID(PPID)。
  2. 使用pstree命令:

    pstree -p <进程ID>
    ```
    同样,将`<进程ID>`替换为你要查看的进程的ID。`pstree`命令会以树状结构显示进程及其子进程的关系,包括对应的进程ID。
  3. 使用pgrep命令:

    pgrep -P <进程ID> | wc -l
    ```
    这个命令会统计指定进程的子进程数量。将`<进程ID>`替换为你要查看的进程的ID。`pgrep -P <进程ID>`会列出指定进程的所有子进程的ID,通过`wc -l`命令可以计算出子进程的数量。

请注意,以上命令在不同的操作系统和版本中可能会有所差异。你可以根据自己的操作系统选择适用的命令,并根据需要进行进一步的处理和筛选。

场景描述

Supervisor 脚本定时重启脚本任务, 任务中存在多个子进程, 重启服务基本都是能一波带走所有子进程.
但这次发现有个服务重启失败, 查看原因是存在子进程未kill掉, 子进程占用服务端口, 重启服务端口被占用最后服务BACKOFF了, 原因就是子进程没杀掉, 莫得办法. 下面配置也都是有的,因为之前踩过坑.没想到还是有问题.

;使用supervisorctl停止时,子进程也会一起停止
stopasgroup=true
;向进程组发送kill信号,包括子进程
killasgroup=true     

为了监控这种服务异常拉起失败的场景, 百度一下 Supervisor 是有一个 事件监听的 Event Listener.

Event TypesSupervisor 官方定义,覆盖了进程运行生命周期的各种状态。

下面翻译一些常用的类型 :

Event解释
PROCESS_STATE进程状态发生改变
PROCESS_STATE_STARTING进程状态从其他状态转换为正在启动(Supervisord的配置项中有startsecs配置项,是指程序启动时需要程序至少稳定运行x秒才认为程序运行正常,在这x秒中程序状态为正在启动)
PROCESS_STATE_RUNNING进程从正在启动状态转换为正在运行状态
PROCESS_STATE_BACKOFF进程从正在启动状态转换为启动失败状态,Supervisor 正在重启该进程
PROCESS_STATE_STOPPING进程从正在运行状态或正在启动状态转换为正在停止状态
PROCESS_STATE_EXITED进程从正在运行状态转换为退出状态,expected 退出码,如果是 0 表示进程异常退出,1 表示进程正常退出。
PROCESS_STATE_STOPPED进程从正在停止状态转换为已停止状态
PROCESS_STATE_FATAL进程从启动失败状态(BACKOFF)转换为失败状态(FATAL). 意味着 startretries 尝试次数已达上限,Supervisor 已放弃重启该进程。
PROCESS_LOG进程产生日志输出,被管理的进程需配置,stdout_events_enabled=true or stderr_events_enabled=true 这个事件通知才会生效。
PROCESS_LOG_STDOUT进程产生标准输出,被管理的进程需配置,stdout_events_enabled=true
PROCESS_LOG_STDERR进程产生错误输出,被管理的进程需配置,stderr_events_enabled=true

Supervisor 配置如下

[eventlistener:phplistener]
command=/usr/bin/python3 /opt/webserver/listener.py   
events=PROCESS_STATE_EXITED,PROCESS_STATE_FATAL  ; 监控事件

stdout_logfile=/opt/weblogs/phplistener_stdout.log
stderr_logfile=/opt/weblogs/phplistener_stderr.log

需要特别注意的是 这个eventlistener 配置修改后不能通过 supervisorctl update 更新配置. 要吗remove后在update/add ,要不就reread.

/opt/webserver/listener.py 文件

# encoding: utf-8
import sys
import requests
import time  # 步骤1:导入时间模块


def write_stdout(s):
    # Only eventlistener protocol messages may be sent to stdout
    sys.stdout.write(s)
    sys.stdout.flush()

def write_stderr(s):
    sys.stderr.write(s)
    sys.stderr.flush()

def send_message(message):
    url = 'https://xxxx/send_message'
    params = {'content': message}
    response = requests.post(url, params=params)
    return response


def main():
    while True:
        write_stdout('READY\n')
        line = sys.stdin.readline()
        write_stderr(line)
        headers = dict([x.split(':') for x in line.split() ])
        eventname = headers['eventname']

        data = sys.stdin.read(int(headers['len']))
        write_stderr(data+'\n')
        datas = dict([y.split(':') for y in data.split() ])

        processname = datas['processname']
        from_state = datas['from_state']

        # Customize this part to perform specific actions based on the event data
        event_message = "【 Supervisor Listener 服务异常 】 \n\n 服务名称 : "+processname+"\n Event : "+eventname+"\n State : "+from_state+"\n\n"+line+"\n"+data+"\n"

        # 仅报警失败的服务
        if eventname == 'PROCESS_STATE_FATAL':

            response = send_message(event_message)
            if response.status_code == 200:
                write_stderr("Message sent successfully.\n")
            else:
                write_stderr("Failed to send message.\n")

        write_stdout('RESULT 2\nOK')

if __name__ == '__main__':
    main()

从此拒绝再使用 file_get_contents 请求网络地址.

PHP file_get_contents() returns "failed to open stream: HTTP request failed!"

今天你发现通服务下 php_error.log 大量抛出错误

[21-Aug-2023 16:59:42 Asia/Shanghai] PHP Warning:  file_get_contents(http://127.0.0.1?xxx): failed to open stream: HTTP request failed! in /data/1.php on line 36

本来想着是 ulimit 被限制了. ulimit -n 输出 :102400 已经非常大了. 还增加了访问服务器的进程.
but 并解决不了问题. 最后只能替换掉 file_get_contents 方法 使用 curl

替换后 问题得到解决了.

相关内容很早前就有说明, 具体原因不详 应该就是文件打开受限制了. 没权限设置无限大, 那就只能平替

https://stackoverflow.com/questions/697472/php-file-get-contents-returns-failed-to-open-stream-http-request-failed

karp

创建我自己的巨人