2019年11月11日 By mikel 分类: 产品, 人工智能

来源: 一束激光冒充人声:110米外黑掉智能音箱,手机电脑平板也中招_IT新闻_博客园

郭一璞 光栗子 发自 凹非寺
量子位 报道 公众号 QbitAI

周末的下午,你正在家里打游戏,不曾注意到身旁的智能音箱上,多出了一个小绿点。

那是一束激光,来自窗外马路对面的另一栋建筑。

突然,房间的灯开了。

空调、空气净化器、扫地机器人启动了,手机收到了电商平台的扣款提示,甚至你外面的车库门也已然洞开……

而你的手机和平板电脑也突然开始发疯,疯狂的下载删除不同的应用,播放奇怪的视频和音乐,给社交软件上的好友发奇怪的信息……

到底发生了什么?是谁,不知不觉侵入了你的生活?

其实,这是来自日本电气通信大学和美国密歇根大学科学家的一项新发现:

当激光打在装有语音助手的设备上,就可以冒充人类的语音,被麦克风转换成电信号,悄无声息的发出指令,控制相连的设备。

因此,那些和 Google Assistant、亚马逊 Alexa、苹果 Siri 相连的机器,不管是智能的灯具、门锁、电器这些硬件设施,还是各种电商、支付、社交 App,都会不知不觉间被控制。

虽然科学家们还没有在其他品牌的语音助手上测试,不过量子位采访到了腾讯安全团队 Tencent Blade Team,他们说:从原理上讲问题大都是相通的。

不需要太强的激光,普通激光笔的强度就可以,就算距离有110 米远,就算在外面的另一栋建筑里,就算要穿过玻璃窗,都可以控制你家里的智能音箱、手机和平板们。

恐怖的演示过程

来看看科学家们的实地演示。

将命令 Google Assistant 打开车库门的语句“OK Google, open the garage door”的嵌入激光中,打在智能音箱的麦克风上。

智能音箱回了一句“OK, opening”,接着车库门就开了。

那么如果把距离设置的非常远呢?

在第二段演示中,激光发射器和智能音箱的距离长达110 米

科学家们将询问时间的语句“OK Google, what time is it?”的嵌入激光中,打在智能音箱的麦克风上。

“It’s 9:43”在没有任何人发话询问的情况下,智能音箱自己突然说了一个时间。

即使在窗外的另一栋建筑里,也不影响激光对智能音箱的控制。

在第三段演示中,科学家们将激光源挪到了远处的一栋高高的建筑上,隔着玻璃窗发射激光,将命令 Google Assistant 打开车库门的语句“OK Google, open the garage door”的嵌入激光中,打在窗口智能音箱的麦克风上。

因为这次发射点又高又远,所以科学家们干脆给激光配了一个长焦镜头。

智能音箱还是顺利的回复“OK, opening”,打开了车库的门。

当然,演示中并没有“鬼故事现场”的感觉,一个原因是激光可见,另一个原因是语音助手的声音你可以听到。

因此,科学家们也尝试了人类肉眼看不见的红外光,在比较近的距离是可以起到作用的;

至于,语音助手回话会被主人听见,先用激光发个指令把音量调零,就真的悄无声息控制一切了。

看到这里你可能疑惑,激光怎么能冒充人声?

让麦克风听成人声

故事是从去年春天开始。

来自日本的菅原健,是个研究网络安全问题的科学家。他专程跑去美国,给密歇根大学的同行傅佳伟 (Kevin Fu) 教授,秀了一波自己刚刚解锁的技能:

把一束高强度的激光,对准iPad的麦克风,然后用每秒震荡大约 1000 次的正弦波,不停地调整激光的强度。

傅佳伟在一旁带着耳机,听麦克风收到了什么。让人惊讶的是,他听到了一种高频音调。

明明是接收声波的设备,却把光波当成声波接收了,这是MEMS 麦克风的一个重要弱点。而大部分手机和智能音箱,都是使用 MEMS 麦克风,因此。

自从有了这个神奇的发现,菅原君就开始和傅佳伟的实验室一起,用激光去欺骗智能音箱,攻击各种接收语音指令的设备。

科学家说,只要用一种特定的频率去调整激光的强度,激光便会用同样的频率去干扰麦克风,让麦克风把光波解调成电信号

就像下面这张图,上为激光发射的信号,下为麦克风接收的信号,频率几乎一致:

不用指定发射位置,只要对准麦克风射出激光,麦克风就把光线转换成电信号了,像日常把声波转换成电信号一样。

当然,如果只是随意的电信号,并不足以让音箱乖乖听你的话。必须让它以为是有人类发出语音才行。

所以,研究人员还要对激光做调幅 (AM) ,让麦克风转出接近人类语音的信号。

就像开头展示的那样,他们选定了一系列指令,包括:“现在几点了”“把音量调零”“买一支激光笔”“打开车库门”等等。

然后,用这些词句的语音波形,来定制激光的强度变化。

这样,智能音箱收到的电信号,就会和听到人声的时候差不多了。

一开始,他们用60 毫瓦的激光,测试了16 台不同的智能音箱。

结果,50 米是成功接收的最远距离。

攻击手机,就稍微困难一些了:iPhone 需要10 米以内,安卓手机需要5 米以内。

后来,科学家们又想测试一下,这项技术的极限在哪里。于是,把激光强度调低到了5 毫瓦,相当于一支廉价激光笔的水平,把距离拉远到110 米

虽然,许多音箱都没有响应,但Google Home和初代Echo Plus依旧中了招,就是开头看到的那样。

进一步加大难度,隔着窗户发射激光,76 米距离。这次没骗到一只 Echo,但 Google Home 依然被骗了,堪称硕果仅存:

至于,麦克风为什么对光波也有反应,哈佛大学电气工程系的退休教授 Paul Horowitz 说,至少有两种物理机制,可以让麦克风把误解成声波

一是激光的脉冲会加热麦克风的振膜,令周围的空气膨胀,产生一种压力。声音也是依靠产生声压,才被麦克风捕捉到的。

二是,如果被攻击的设备,不是完全不透光的话,光线其实可以直接穿过麦克风,直接到达芯片的所在,这样就能把光波的振动,翻译成电信号了。

这可能跟太阳能板里的二极管,还有光纤电缆末端的光电效应,原理一样。如果真是这样,想让激光被当做语音指令,就更容易了。

另外,量子位还采访到了腾讯的安全团队 Tencent Blade Team,他们的理解是:

这项研究和此前业界的 “海豚音攻击”有异曲同工之妙,都是利用了麦克风的一些特殊的硬件特性进行攻击,但这次的“光攻击”可以从更远距离(超过 100 米)发起,在现实生活中的攻击利用难度更低。

攻击的难度降低,防御的难度提升。Blade 团队认为,这项研究的意义十分重大:

从目前公开的信息来看,厂商很难从软件层面对这个漏洞进行彻底修复,之前安全圈内也没有 MEMS 麦克风会将光信号转换为电信号相关的安全研究,这项研究还是具有很高的创新性与实战意义。

怎样才能不被黑?

看到这样的攻击效果,谷歌和亚马逊很快就回应了:

谷歌说,已经在仔细观察这次的研究成果,并且强调一直对保护用户、提升设备的安全性能非常重视。

亚马逊也发表了声明说,正在看论文,后面将会和作者们交流,更深入地了解这项研究。

在厂商们给出补救措施之前,研究人员先为他们提供了一些友善的建议:

比如,可以设置让用户先输入语音密码,解锁后才能发布指令。

比如,可以在麦克风周围加上光屏蔽,抵挡激光的攻击。

再比如,在音箱两侧依靠两个麦克风同时接收指令,然后对比。因为两个麦克风,很难同时被击中。

当然,对产品做出这样的升级,还需要不少时间。

而你现在能做的就是,不要把智能音箱放在黑客能看到的地方。

不然,还是去用那些需要解锁的设备吧,人脸解锁和指纹解锁都能起到保护作用,避免语音助手接收到黑客的指令。

腾讯 Blade Team 还提示,最好关闭声纹识别 (因为声纹也可以用激光冒充) ,也可以在设备外部的麦克风口贴上黑色标签纸,阻挡激光攻击。

作者们


一作菅原健,就是从日本跑到美国炫 (mian) 技 (ji) 的那一位,电子通信大学 (UBE) 的准教授。


傅佳伟 (Kevin Fu) ,密歇根大学的教授,专注攻击各种 AI。量子位之前报道过一种把硬盘改造成窃听器的方法,也是他参与的研究。难怪,菅原君会不远万里去找他。


Daniel Genkin,密歇根大学助理教授。他和傅佳伟都是这项研究的负责人。

另外,还有两位作者,他们是傅佳伟教授实验室的成员,Benjamin Cyr 以及 Sara Rampazzi。

传送门

论文

Light Commands: Laser-Based Audio Injection Attacks on Voice-Controllable Systems

Takeshi Sugawara, Benjamin Cyr, Sara Rampazzi, Daniel Genkin, Kevin Fu

https://lightcommands.com/20191104-Light-Commands.pdf

主页

https://lightcommands.com/

一束激光冒充人声:110米外黑掉智能音箱,手机电脑平板也中招_IT新闻_博客园已关闭评论
2019年11月11日 By mikel 分类: Java源码, 产品, 人工智能

来源: 日本中二少年教你用姿势估计把自己变成3D人物,动作实时同步,iOS上也能实现

大数据文摘出品
作者:刘俊寰
不知道从什么时候开始,3D动画就热起来了,但是很多经典动画3D化后就变味了,人物的肢体动作看上去僵硬了不少。并且,传统3D靠一帧一帧制作,费时费力。
现在,你就拥有一个拯救3D动画的机会!
一位日本中二少年自学了机器学习后,就给自己做了个酷炫的模型,可以把自己的动作实时变成流畅的3D人物动作,而且整个过程非常简单易操作。
话不多说先看效果图:
这个推特名为幸彦青柳(Yukihiko Aoyagi)的日本小哥将3D姿态估计与3D开发平台和一些渲染引擎(比如Unity)相结合,于此更够跟准确地跟踪3D空间中的人体运动。上面的动图就是针对动作的实时估计和生成。
不过可惜的是,这个项目目前还只支持单人动作,不能实现双人对打。
项目已经在GitHub上开源:
https://github.com/yukihiko/ThreeDPoseUnitySample?source=post_page—–e74d7d347c2———————-
趁着它还没刷爆朋友圈,赶紧上手试一试!
用3D姿势估计的Onnx模型移动Unity

青柳君尝试过多种实现方式,包括WindowsML,ML.Net,Onnx Runtime等,但最终选择了OpenCVSharp,也就是OpenCV模型导入功能,在Unity中加载和执行Onnx,因为OpenCVSharp在Unity和.Net环境中可以用相同的方式处理,图像也不会被转换为Mat格式。
尽管看上去处理起来很容易,但目前还缺少相关数据,青柳君特意总结了他的这次尝试,将文章公布在了Qiita上。
相关链接:
https://qiita.com/yukihiko_a/items/386e3a86a5e523757707
有关Onnx的代码部分如下:
// Properties for onnx and estimation    private Net Onnx;    private Mat[] outputs = new Mat[4];
    private const int inputImageSize = 224;    private const int JointNum = 24;    private const int HeatMapCol = 14;    private const int HeatMapCol_Squared = 14 * 14;    private const int HeatMapCol_Cube = 14 * 14 * 14;
    char[] heatMap2Dbuf = new char[JointNum * HeatMapCol_Squared * 4];    float[] heatMap2D = new float[JointNum * HeatMapCol_Squared];    char[] offset2Dbuf = new char[JointNum * HeatMapCol_Squared * 2 * 4];    float[] offset2D = new float[JointNum * HeatMapCol_Squared * 2];
    char[] heatMap3Dbuf = new char[JointNum * HeatMapCol_Cube * 4];    float[] heatMap3D = new float[JointNum * HeatMapCol_Cube];    char[] offset3Dbuf = new char[JointNum * HeatMapCol_Cube * 3 * 4];    float[] offset3D = new float[JointNum * HeatMapCol_Cube * 3];
    public void InitONNX()    {        Onnx = Net.ReadNetFromONNX(Application.dataPath + @"\MobileNet3D2.onnx");        for (var i = 0; i < 4; i++) outputs[i] = new Mat();    }
    /// <summary>    /// Predict    /// </summary>    /// <param name="img"></param>    public void Predict(Mat img)    {        var blob = CvDnn.BlobFromImage(img, 1.0 / 255.0, new OpenCvSharp.Size(inputImageSize, inputImageSize), 0.0, false, false);        Onnx.SetInput(blob);        Onnx.Forward(outputs, new string[] { "369", "373", "361", "365" });
        // copy 2D outputs        Marshal.Copy(outputs[2].Data, heatMap2Dbuf, 0, heatMap2Dbuf.Length);        Buffer.BlockCopy(heatMap2Dbuf, 0, heatMap2D, 0, heatMap2Dbuf.Length);        Marshal.Copy(outputs[3].Data, offset2Dbuf, 0, offset2Dbuf.Length);        Buffer.BlockCopy(offset2Dbuf, 0, offset2D, 0, offset2Dbuf.Length);        for (var j = 0; j < JointNum; j++)        {            var maxXIndex = 0;            var maxYIndex = 0;            jointPoints[j].score2D = 0.0f;            for (var y = 0; y < HeatMapCol; y++)            {                for (var x = 0; x < HeatMapCol; x++)                {                    var l = new List<int>();                    var v = heatMap2D[(HeatMapCol_Squared) * j + HeatMapCol * y + x];
                    if (v > jointPoints[j].score2D)                    {                        jointPoints[j].score2D = v;                        maxXIndex = x;                        maxYIndex = y;                    }                }
            }
            jointPoints[j].Pos2D.x = (offset2D[HeatMapCol_Squared * j + HeatMapCol * maxYIndex + maxXIndex] + maxXIndex / (float)HeatMapCol) * (float)inputImageSize;            jointPoints[j].Pos2D.y = (offset2D[HeatMapCol_Squared * (j + JointNum) + HeatMapCol * maxYIndex + maxXIndex] + maxYIndex / (float)HeatMapCol) * (float)inputImageSize;        }
        // copy 3D outputs        Marshal.Copy(outputs[0].Data, heatMap3Dbuf, 0, heatMap3Dbuf.Length);        Buffer.BlockCopy(heatMap3Dbuf, 0, heatMap3D, 0, heatMap3Dbuf.Length);        Marshal.Copy(outputs[1].Data, offset3Dbuf, 0, offset3Dbuf.Length);        Buffer.BlockCopy(offset3Dbuf, 0, offset3D, 0, offset3Dbuf.Length);        for (var j = 0; j < JointNum; j++)        {            var maxXIndex = 0;            var maxYIndex = 0;            var maxZIndex = 0;            jointPoints[j].score3D = 0.0f;            for (var z = 0; z < HeatMapCol; z++)            {                for (var y = 0; y < HeatMapCol; y++)                {                    for (var x = 0; x < HeatMapCol; x++)                    {                        float v = heatMap3D[HeatMapCol_Cube * j + HeatMapCol_Squared * z + HeatMapCol * y + x];                        if (v > jointPoints[j].score3D)                        {                            jointPoints[j].score3D = v;                            maxXIndex = x;                            maxYIndex = y;                            maxZIndex = z;                        }                    }                }            }
            jointPoints[j].Now3D.x = (offset3D[HeatMapCol_Cube * j + HeatMapCol_Squared * maxZIndex + HeatMapCol * maxYIndex + maxXIndex] + (float)maxXIndex / (float)HeatMapCol) * (float)inputImageSize;            jointPoints[j].Now3D.y = (float)inputImageSize - (offset3D[HeatMapCol_Cube * (j + JointNum) + HeatMapCol_Squared * maxZIndex + HeatMapCol * maxYIndex + maxXIndex] + (float)maxYIndex / (float)HeatMapCol) * (float)inputImageSize;            jointPoints[j].Now3D.z = (offset3D[HeatMapCol_Cube * (j + JointNum * 2) + HeatMapCol_Squared * maxZIndex + HeatMapCol * maxYIndex + maxXIndex] + (float)(maxZIndex - 7) / (float)HeatMapCol) * (float)inputImageSize;        }    }
模型输入224×224的图像,输出的关节数为24个,热图(Heatmap)为14×14。
2D热图格式是24x14x14,3D的是24x14x14x14。将其作为与热图的坐标偏移值,输出的2D(x,y)变为2x24x14x14,3D(x,y,z)变为3x24x14x14x14。
 public void InitONNX()    {        Onnx = Net.ReadNetFromONNX(Application.dataPath + @"\MobileNet3D2.onnx");        for (var i = 0; i < 4; i++) outputs[i] = new Mat();    }

首先,使用InitONNX()读取Onnx文件。
由于OpenCV的输出是通过Mat对象返回的,需要准备四个数组。
 public void Predict(Mat img)    {        var blob = CvDnn.BlobFromImage(img, 1.0 / 255.0, new OpenCvSharp.Size(inputImageSize, inputImageSize), 0.0, false, false);        Onnx.SetInput(blob);        Onnx.Forward(outputs, new string[] { "369", "373", "361", "365" });
        // copy 2D outputs        Marshal.Copy(outputs[2].Data, heatMap2Dbuf, 0, heatMap2Dbuf.Length);        Buffer.BlockCopy(heatMap2Dbuf, 0, heatMap2D, 0, heatMap2Dbuf.Length);        Marshal.Copy(outputs[3].Data, offset2Dbuf, 0, offset2Dbuf.Length);        Buffer.BlockCopy(offset2Dbuf, 0, offset2D, 0, offset2Dbuf.Length);

Predict方法参数的Mat对象是正常的CV_8UC3 Mat图像数据,需要将其转换为Blob Mat才能传递给Onnx,这个过程利用BlobFromImage就能完成。
在Output中,“369”和“373”是3D,“361”和“365”是2D。但如果是Mat对象,处理起来就稍微复杂一些,因为还需要将其转换为float数组。
然后,通过改变关节数和热图大小,找到最大热图。
由于3D是一个相当大的循环,最好再做一些改进,但是由于它现在移动得足够快,保持原样也是可以的。
在iOS上也能实现3D姿势估计

去年的日本黄金周,青柳君第一次接触机器学习,也一直在3D姿势估计这块有所钻研。
今年3月份,他在iOS上实现了3D姿势估计。据本人推特发言称,他用了一天时间学习,然后做出了这个模型。
根据青柳君本人介绍,iOS项目的学习环境是Windows10/PyTorch0.4,执行环境是iPhone XS Max,至于选择iPhone XS Max的原因,青柳君说,iPhone XS Max的A12处理器功能非常强大。
还是先看看效果如何:
  
青柳君准备了2D和3D的数据集,2D数据集是利兹运动姿势数据集,利兹运动姿势扩展训练数据集、MPII人类姿势数据集、Microsoft COCO;而3D数据集是原始数据集。
在此之前他还做了很多准备,包括从AssetStore购买的数据等,当然还有Unity。
然后就可以利用Unity创建3D角色动画了,创建角色图像和坐标,包括肩膀、肘部、手腕、拇指、中指、脚、膝盖、脚踝、脚趾、耳朵、眼睛、鼻子,以输出身体的中心位置,即肚脐。
该数据集由于许可原因结果变得十分复杂,导致发布失败。
由于这是CG,因此可以随意更改角色的纹理和姿势。最初,他希望更改每个时期数据集的内容,以提高泛化性能,但没有效果,为此大约有100,000个副本用于学习。
即使是用3D版本的图像,也可以照原样学习,最后可以获得相似的图像,但是无法获得预期的性能。
将通过PyTorch学习得到的模型导出到Onnx,用coremltools转换为CoreML模型,此时就算是估计到了相同的图像,结果也会有所不同,所以准确度未知。
将模型导入Mac,使用XCode的iPhone版本,通过实时捕获后方摄像机图像执行3D估计。
XS Max能以大约40fps的速度运行,但是,一段时间,手机会变热,速记也会下降至约30fps。如果仅用于学习2D模型,其运行速度会接近100fps。
由于这是个3D项目,显示时无法从摄像机看到的部分,判断热图的阈值已降低到几乎为零。例如,如果手臂正常可见,热图的最大部分为0.5或更高(最大值为1.0);如果看不到手臂,将得到0.2或0.1的值,阈值降低。
但就结果而言,无论身在何处,系统都可以判断为有人。
Adobe发布最新动作追踪软件

上周,Adobe也发布了一款用于视觉效果和动态图形软件After Effects,该软件的AI功能能够自动跟踪人体运动并将其应用于动画。
简单地说,就是能够把现实人物的动作直接转换成为动画。
与青柳君的机器学习项目的效果相差无几!
Adobe研究科学家Jimei Yang在演示中说,这一功能利用了Adobe的人工智能平台Sensei,该平台用超过10000张图像进行了训练,从而能够识别人体的关键点。
据了解,人体跟踪器在源视频中能够检测到人体的运动,胳膊、躯干和腿部的18个关节点将生成相关跟踪点,然后将跟踪点转移到动画角色上,利用该功能,快速创建2D人物动画根本不在话下!
怎么样,有没有觉得打开了新世界的大门?
当然,对于姿势估计的实现还远远不止现在的程度,未来希望不仅是青柳君和Adobe,有更多人都参与到这个领域的研究和学习中来,促进相关领域的发展。
日本中二少年教你用姿势估计把自己变成3D人物,动作实时同步,iOS上也能实现已关闭评论
2019年11月10日 By mikel 分类: 架构设计

来源: 干货来袭-整套完整安全的API接口解决方案 – hubro – 博客园

在各种手机APP泛滥的现在,背后都有同样泛滥的API接口在支撑,其中鱼龙混杂,直接裸奔的WEB API大量存在,安全性令人堪优

在以前WEB API概念没有很普及的时候,都采用自已定义的接口和结构,对于公开访问的接口,专业点的都会做下安全验证,数据签名之类

反而现在,谁都可以用WEB API估接口,安全性早忘一边了,特别是外包小公司的APP项目,80%都有安全漏洞(面试了大半年APP开发得出的结论)

特在过年之前,整理了下在用的解决方案,本方案解决了

  • 数据安全问题
  • 标准消息结构
  • 接口测试程序
  • 接口文档体现

正文

数据结构

对于一个接口,返回的内容除了要返回业务数据外,还得返回处理状态,并且这个状态是在每个接口都得有

所以数据格式都会定义为:

数据头(描述数据信息)

———————————–

数据体(具体数据)

本文定义结构为

复制代码
/// <summary>
    /// 处理结果
    /// </summary>
    public class DealResult
    {
        /// <summary>
        /// 处理结果
        /// </summary>
        public bool Result
        {
            get;
            set;
        }
        /// <summary>
        /// 消息
        /// </summary>
        public string Message
        {
            get;
            set;
        }
        /// <summary>
        /// 关联数据
        /// </summary>
        public object Data
        {
            get;
            set;
        }
    }
复制代码

所有接口都返回此对象,会描述本次请求的状态,和对应的数据,服务端则根据实际情况,返回处理结果和对应的数据

 

数据安全

开方式接口安全性就不用多说了,解决方法为加密,或数据签名验证,本文方案为进行数据签名

同返回的数据一样,提交到服务器的数据格式也统一约定,定义一个数据头基类

复制代码
    /// <summary>
    /// 参数基类
    /// </summary>
    [Serializable]
    public class ParameBase
    {
        string time = DateTime.Now.ToString("yyyy-MM-dd hh:mm:ss");
        /// <summary>
        /// 时间 格式 yyyy-MM-dd hh:mm:ss
        /// </summary>
        public string Time
        {
            get
            {
                return time;
            }
            set
            {
                time = value;
            }
        }
        /// <summary>
        /// 来源网站 = 1, IOS = 2,Android = 3, 微信 = 4
        /// </summary>
        public int SourceFrom
        {
            get;
            set;
        }
        /// <summary>
        /// 签名
        /// </summary>
        public string Token
        {
            get;
            set;
        }
       
    }
复制代码

 

一个登录对象表示为

复制代码
    /// <summary>
    /// 登录
    /// </summary>
    public class Login : ParameBase
    {
        /// <summary>
        /// 用户名
        /// </summary>
        public string Name
        {
            get;
            set;
        }
        /// <summary>
        /// 密码
        /// </summary>
        public string Password
        {
            get;
            set;
        }
    }
复制代码

数据签名表示为(KEY稍后讲到)

Token=MD5(属性值1+值2….+KEY)

按此对象表示为 MD5(Name+PassWord+Source+Time+KEY)

如果是GET参数怎么办,一样,按参数名计算,同时传递的参数要附带上Source,Time,Token

 

密钥机制

有的喜欢把密钥放在客户端,或固定密钥,显然都有安全问题,解决方法是动态获取

这就意味着在设计接口时,有一个接口是首先要调用的,让服务器返回密钥,于是就有了登录的概念

过程表示为

登录>返回用户信息和密钥=>存储用户信息和密钥=>使用密钥调用其它接口

这样只有登录者和服务器才知道自已的密钥了

综上所述,数据结构表示为

客户端提交结构为 ParameBase(附带签名信息)

服务端返回结构为 DealResult

 

登录机制

同网页请求一样,怎么知道多次调用是同一个人呢,这里采用了COOKIE的形式,登录后服务端返回一个COOKIE,客户端再请求时带上这个COOKIE

服务端需要存储这个COOKIE标识,所有的验证处理都会基于此标识来判断用户

 

有了上面基础,进入项目阶段

WEB API项目

其实用什么项目类型都行,只是WEB API方便了对象结构序列化和传参

默认WEB API路由RESUFUL形式,没有控制器方法,只能按METHOD来定义,很不方便,改成控制器的形式,这样就能用方法名来访问了

更改路由配置为

1
2
3
4
5
config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{action}/{id}",//加上路由ACTION参数
                defaults: new { id = RouteParameter.Optional }
            );

在此文,数据分为请求和返回,以登录返回用户信息为例,登录为请求,用户信息为返回,示例对象结构为

用户对象

复制代码
/// <summary>
    /// 登录返回用户
    /// </summary>
    public class User
    {
        /// <summary>
        /// 用户编号
        /// </summary>
        public int Id
        {
            get;
            set;
        }
        /// <summary>
        /// 名称
        /// </summary>
        public string Name
        {
            get;
            set;
        }
        /// <summary>
        /// 本次登录的KEY
        /// </summary>
        public string Key
        {
            get;
            set;
        }
        /// <summary>
        /// 本资登录的凭证
        /// </summary>
        public string Voucher
        {
            get;
            set;
        }

       
    }
复制代码

 

请求方式

这里只采用了GET,POST两种方式,根据实际情况定义,控制器方法一定需要都标明,不然会出现路由BUG

定义登录方法

 View Code

这里可以看到,创建了两个GUID,一个为用户凭证,一个为用户密钥,放入用户信息返回,同时调用LoginStatusContext.SetLoginStatus保存登录信息

同时使用了AnonymousSign标注,此方法使用默认签名Setting.DefaultKey

定义获取用信息方法

复制代码
        /// <summary>
        /// 基本信息
        /// </summary>
        /// <param name="name">参数name</param>
        /// <returns>User</returns>
        [HttpGet]
        public DealResult GetBasicInfo(string name)
        {
            var user = new User() { Name = name, Id = CurrentUserId };
            return DealResult(true, string.Empty, user);
        }
复制代码

 

示例控制器完整定义

复制代码
 /// <summary>
    /// 帐号操作
    /// </summary>
    [SignCheckAttribute]
    public class AccountController : BaseController
    {
        /// <summary>
        /// 登录
        /// </summary>
        /// <param name="parame"></param>
        /// <returns>User</returns>
        [HttpPost]
        [AnonymousSign]
        public DealResult Login([FromBody] Login parame)
        {
            if (parame.Password != "123")
            {
                return DealResult(false, "密码不正确");
            }
            string key2 = System.Guid.NewGuid().ToString();
            string voucher = System.Guid.NewGuid().ToString();
            var user = new User() { Name = parame.Name, Id = 1, Key = key2, Voucher = voucher };
            var timeDiff = (DateTime.Now - Convert.ToDateTime(parame.Time)).TotalSeconds;//保存客户端和服务端时间差
            LoginStatusContext.SetLoginStatus(voucher, user.Id, key2, timeDiff);
            CoreHelper.CookieHelper.AddCookies("user", voucher);//存入COOKIE
            return DealResult(true, "", user);
        }


        /// <summary>
        /// 基本信息
        /// </summary>
        /// <param name="name">参数name</param>
        /// <returns>User</returns>
        [HttpGet]
        public DealResult GetBasicInfo(string name)
        {
            var user = new User() { Name = name, Id = CurrentUserId };
            return DealResult(true, string.Empty, user);
        }

        /// <summary>
        /// 测试异常
        /// </summary>
        /// <returns></returns>
        [HttpGet]
        public DealResult TestException()
        {
            int a = 0;
            var b = 10 / a;
            return DealResult(true);
        }

    }
复制代码

 

此控制器标注了SignCheckAttribute用以进行签名判断

具体实现可看SignCheckAttribute代码

SignCheckAttribute里实现了有

  • 数据签名判断
  • 签名超时判断
  • 用户登录限制
  • 签名重复使用处理(一个签名只能使用一次)
  • 过期登录用户处理(没有主动退出用户清理)

为了统一处理异常,配置了异常处理

1
GlobalConfiguration.Configuration.Filters.Add(new ExceptionAttribute());

对接口进行测试

大杀器来了,配合此方案放出了对应的测试工具,虽然WEB API有个扩展,但没法对此方案测试

使用此工具能方便按方案要求调用接口,为了方便参数拼接,POST和GET都采用URL参数的形式输入

测试登录/api/account/login

测试获取信息/api/account/GetBasicInfo

测试异常处理/api/account/TestException

在未登录情况下调用获取信息

接口文档

接口结构文档一直是很让人头疼的事,手写更改了又得维护,版本不一样还麻烦,自动生成最好了,同样WEB API 带扩展没法表示此结构详细

大杀器2号来了,按代码注释动态生成接口文档,文档格式与控制器保持一致

Home控制器代码实现

复制代码
    public ActionResult Index(SummaryAnalysis.ExportType exportType = SummaryAnalysis.ExportType.NONE)
        {
            if (exportType != SummaryAnalysis.ExportType.NONE)
            {
                var str = SummaryAnalysis.Load(exportType);
                return File(str, "application/octet-stream", "Model_" + exportType + ".zip");
            }
            else
            {
                if (string.IsNullOrEmpty(outPut))
                {
                    outPut = SummaryAnalysis.Load(exportType);
                }
                ViewBag.OutPut = outPut;
                return View();
            }
        }
    }
复制代码

在见过的开发文档,我觉得这是最好的展现形式了,还有锚点,快速定位到对象结构,并且与源代码保持一致

附WEB API 自带文档生成区别

附上项目源码

http://pan.baidu.com/s/1c2rDacK

项目结构:

———-WPF测试程序

———-接口示例

虽然跟CRL快速开发框架无关,但还是加上CRL的名,好文要顶!

干货来袭-整套完整安全的API接口解决方案 – hubro – 博客园已关闭评论
2019年11月10日 By mikel 分类: Debug

来源: 如何设置宝塔面板优化 php 服务器性能-魏艾斯博客

为了提高 php 服务器性能,安装宝塔面板的服务器需要设置一下。整个设置过程很简单,魏艾斯博客把如何设置宝塔面板提高服务器性能的过程写下来,希望能帮到初次使用宝塔面板的朋友们。

群里面太多朋友初次接触 php 环境和宝塔面板,而且很多人用 avada、The 7、newspaper、Enfold、Betheme、 Richer 等耗费系统资源的 wordpress 模板,如果不做额外优化真的打开很慢,比如一个朋友使用阿里云 ECS 国内服务器,2 核 8g 内存 40g 系统盘 100g 数据盘 5m 带宽,newspaper 主题,艺术类网站以图片为主,首页就是一个大 banner 和 N 多图片,全高清高分辨率那种的,联通百兆光纤打开需要近 10 秒钟,他自己也很不满意。要知道国内带宽成本极高,一台云服务器接近一半成本是带宽费用,普通网站 1M 带宽起步也不错了而他买了 5M 还慢,所以不优化一下真的不好意思让客户上来看。

宝塔面板是我们再熟悉不过的国产 Linux 面板了,目前在国内可以说是风声水起,用户量不断上涨。有关宝塔面板的安装使用在魏艾斯博客里面已经写过很多了,不再啰嗦。为了让服务器运行速度更快,我们就使用宝塔面板提供的功能来优化一下服务器配置。本文部分内容参考自@很文博客,老魏根据大家遇到的问题和自己的经验加以补充和丰富。

关于宝塔面板的入门基础知识:

1、定期释放内存

添加计划任务,可以设置每天或一周释放一次,间隔时间根据自己网站情况定,执行时机为半夜,如:1:30。

看下图,在宝塔面板左侧的计划任务中操作即可。

2、添加 Swap

关于 Swap 的重要性在lnmp 中创建、启用 SWAP 分区详细教程中已经写过了,无论服务器内存多大都必须添加 Swap。在宝塔面板的软件管理>>系统工具中找到 Linux 工具箱 1.4,点击最右侧的“设置”打开 Swap 虚拟内存,按照下面的推荐值进行设置。

Swap 推荐值:2G 和 2G 以下内存的服务器,设置成和物理内存相同容量 SWAP;2G 以上的,设置为 2G。如果跑的程序特别耗费内存,2G 内存以上的 Swap 也可以设置与内存相同。

3、安装 PHP 缓存扩展

个人博客的 php 缓存扩展推荐使用 Opcache 脚本缓存和 Memcached 内容缓存。在软件管理>>你使用的 php 版本最右侧点击“设置”,进入安装扩展,右侧点击这两个扩展后面的“安装”,再确认一次,很快搞定。

内容缓存和脚本缓存都只安装一个,不要同时安装多个。

其他的扩展组件,在确认用不到的前提下,都卸载掉,因为开着还不用会浪费系统资源。

4、php 配置调整

同样在 php 管理的配置修改中,memory_limit 脚本内存限制修改成 256M,这样 wordpress 跑起来更顺畅了;upload_max_filesize 允许上传文件的最大尺寸,像 avada 模板可能会超过这个数值无法上传,需要修改为大于上传文件的数值。

在配置文件中 Ctrl+F 搜索 memory_limit,把默认值修改成 256M,保存。下图是老魏改好后的。

5、PHP 并发调整

宝塔面板提供了几个优化过的 php 并发方案,一般 1 核 1G 内存的云服务器,设置 30 并发或 50 并发都可以,其他配置请自己测试一下合适并发方案。不建议并发超过 300 ,如果并发不够用还是优化数据库缓存可能更有效。

6、php 版本和 MySQL 数据库版本

php7 的性能比低版本有很大提升,老魏强烈建议使用 php7 以上版本;1 核 1G 配置的数据库推荐 MySQL5.5,而 5.6 要求最低内存 1G 以上,所以大家应该知道如何选择了。

7、卸载多余扩展组件

如果程序不需要,就卸载 ZendGuardLoader、ioncube、PATH_INFO,安装后不用也一直占用一部分内存。

设置完了要重启服务器,让调整后的参数生效。

8、像本文开头提到的群友网站高清图片较多的,如果达到几千张且还要不停增加的话,建议先点我领取千元代金券,再购买阿里云 ECS 云服务器的基础上搭配阿里云存储 OSS,把图片分流到 OSS 存储空间上去,ECS 只放程序部分,这样网站打开速度会快很多的。

9、WordPress 如何优化提升速度,这是个永恒的任务。想让网站加载速度快,优化的关键是找到平衡点,点我打开在线课程开始学习,全面分析原因并给出相对应的一整套解决方案。

以上优化方案分别在阿里云服务器(点我领取千元代金券 抵用 50 元起)和腾讯云服务器(点我五折优惠购买)上测试通过。

宝塔面板官方网站:点我直达

1、宝塔官方赠送3188元优惠券礼包。

2、新用户可享受0.99元体验1个月专业版。

经过上面的一番折腾整理优化,你的云服务器应该比之前流畅很多了,没有做到的同学赶紧去试试吧,也可以在下面留言交流你认为更合适的优化方案。

如何设置宝塔面板优化 php 服务器性能-魏艾斯博客已关闭评论
2019年11月10日 By mikel 分类: Debug

来源: 宝塔面板安装memcached的误区及正确方法-魏艾斯博客

宝塔面板添加memcached可以有效加速服务器访问速度,提高网站的用户体验。不过在实际使用中,魏艾斯博客发现很多新手有一个误区,就是添加了错误的memcached组件,导致起不到正常加速服务器的效果。本文中魏艾斯博客分享一下误区在哪里以及应该怎么安装是正确的加速组件。

参考资料:宝塔面板安装memcached及详细配置方法

宝塔面板官方网址:点我进入

1、宝塔官方赠送3188元优惠券礼包。

2、新用户可享受0.99元体验1个月专业版

1、错误的memcached组件添加方法

很多人直接安装下图中蓝色M字样的memcached组件。从这里安装后,服务器不会有加速效果,访问网站也不会感觉到比以前快。

2、正确的memcached安装方法

以老魏目前使用的php版本为例,正确安装方法是在“软件管理”的php7.0>设置中,找到“安装扩展”右侧的memcached,安装。这才是正确的memcached组件。安装完成后,再返回看到上图蓝色M字样的memcached已经自动安装上去了,无需人工干预。

也就是说这里有一个顺序问题,老魏测试了一下,如果你先安装上图蓝色M字样的Memcached,那么下图php7.0的memcached不会被安装,同样也起不到加速效果,失败;若先去php7.0中安装memcached,再返回会看到蓝色M字样的Memcached已经被自动安装好了,这时候服务器会加速,网站打开也会成倍提速,这才是正确的安装步骤和效果。请大家一定注意这个顺序不能乱搞,搞错了你会埋怨老魏分享错误、无效的资料,浪费你的时间、精力,却还没效果,其实是你把这个顺序搞颠倒了。

安装memcached组件后,还要安装一个插件,叫 memcached is your friend 这个插件。具体安装过程请移步安装 memcached 和 object-cache.php 中的第二步提到的这个插件,按照要求安装就可以了。宝塔面板如果不安装的话,memcached就不会正常工作了,命中率一直是0。

3、用宝塔面板加速

通过宝塔面板优化服务器速度、性能的参考资料如何设置宝塔面板优化 php 服务器性能,写的很详细了,这里不再啰嗦。

这个memcached是从服务器层面加速,比wordpress插件那种程序层面加速要快上很多,占用资源也小。因为wordpess缓存插件的原理是把数据库内容缓存到硬盘中,而memcached是缓存到内存中。内存的读取速度是几倍于硬盘读取速度的。不过你使用了memcached就必须卸载掉wordpress super cache之类的缓存插件,因为都是缓存插件会互相影响效果。

至于memcached的效果如何检测,有两种方法:一是使用宝塔面板自带的检测功能,不够直观;二是使用第三方php文件,很直观。操作起来很简单,请移步多种方法实时监测 Memcached命中率

宝塔面板安装memcached的误区及正确方法-魏艾斯博客已关闭评论
2019年11月10日 By mikel 分类: Debug

来源: linux上使用redis–宝塔面板 – 雨蝶的博客 – CSDN博客

1.首先下载redis,并运行起来

2.放行对应的端口号

3.要安装redis扩展,进到设置

linux上使用redis–宝塔面板 – 雨蝶的博客 – CSDN博客已关闭评论
2019年11月8日 By mikel 分类: PHP

来源: ECSHOP怎樣可以取消用手机上网时,不自动转到mobile页面 – wpindesign – 博客园

打开 index.php 删除以下代码

$ua = strtolower($_SERVER['HTTP_USER_AGENT']);

$uachar = "/(nokia|sony|ericsson|mot|samsung|sgh|lg|philips|panasonic|alcatel|lenovo|cldc|midp|mobile)/i";

if(($ua == '' || preg_match($uachar, $ua))&& !strpos(strtolower($_SERVER['REQUEST_URI']),'wap'))
{
$Loaction = 'mobile/';

if (!empty($Loaction))
{
ecs_header("Location: $Loaction\n");

exit;
}

ECSHOP怎樣可以取消用手机上网时,不自动转到mobile页面 – wpindesign – 博客园已关闭评论
2019年11月8日 By mikel 分类: Debug

来源: 宝塔面板的ftp无法使用解决 – 代码仔-兴江 – 博客园

宝塔面板的ftp无法使用解决

先检查这些内容

1.注意内网IP和外网IP

2.检查ftp服务是否启动 (面板首页即可看到)

3.检查防火墙20端口 ftp 21端口及被动端口39000 – 40000是否放行 (如是腾讯云/阿里云等还需检查安全组)

注意:最好到配置文件修改端口一致

PassivePortRange 39000 40000

截图:

 

 

放行对应的防火墙端口:

 

 

4.是否主动/被动模式都不能连接

5.新建一个用户看是否能连接

6.更换ftp客户端使用flashfxp 如图勾上 再尝试连接

 

 

但凡解决了就可以登陆了,如果还是不行,那么试试最后一种方法:

登陆宝塔面板管理系统,找到左侧的“软件管理”–“FTP软件”—点击“设置”

 

 

然后点击配置修改:如图所示,查找“ForcePassiveIP”(位置188行左右)

# ForcePassiveIP                192.168.0.1

删除前面的“#”将“192.168.0.1”修改为:服务器的IP地址,是服务器不是你客户端的IP地址!

ForcePassiveIP  服务器公网ip地址

 

 

最后修改完记得重新启动一下FTP软件,打开FTP连接,你会发现奇迹发生了

宝塔面板的ftp无法使用解决 – 代码仔-兴江 – 博客园已关闭评论
2019年11月8日 By mikel 分类: Debug

WIndows10系统中主流浏览器已经变成了Edge,但是有些网站指定需要使用ie打开。有些小伙伴打开IE一会就闪退那要怎么解决呢?别急,下面就给

来源: win10 中ie浏览器闪一下就没有了应该怎么处理 – 系统族

WIndows 10系统中主流浏览器已经变成了Edge,但是有些网站指定需要使用ie打开。有些小伙伴打开IE一会就闪退那要怎么解决呢?别急,下面就给大家简单细说一下。

系统推荐:win10系统下载

1、原因分析:出现这样的情况一般是由于ie中【第三方浏览扩展】导致的

解决方法一:按住windows+r调出运行,在框中输入:inetcpl.cpl  点击确定;

win10 中ie浏览器闪一下就没有了应该怎么处理

2、在【internet  属性】界面点击【高级】选卡,在下面框中将“启用第三方浏览器扩展”和“启用自动崩溃恢复”的勾去掉,点击应用并确定即可。

win10 中ie浏览器闪一下就没有了应该怎么处理

上面就是关于win10打开ie就闪退的解决方法,有遇到这个问题的小伙伴可以按照小编的步骤实地操作一下,想了解更多win10系统的知识,请大家关注系统族!
win10 中ie浏览器闪一下就没有了应该怎么处理 – 系统族已关闭评论
2019年11月7日 By mikel 分类: 架构设计

来源: 可以编写代码的代码:代码生成的利与弊 – 聚变归来 – 博客园

代码生成的当前状态

代码生成的当前状态是无处不在的(2019年春季)。
如今,代码生成发生在软件堆栈的每一层,

  • 包括Java库(如swagger CodeGen),
  • 最新的交叉编译器/编译器(如针对JavaScript应用程序的Babel)
  • 以及全栈生成器(如Starter StackGen(tm))

REST API的数量激增,导致在过去十年中针对各种编程语言和环境开发的API客户端生成器种类繁多。
就像某种巨大的数字Turducken一样,REST API及其生成的客户端似乎能够将任何事物连接到任何事物。
因此,让我们看一下代码生成管理中涉及的火鸡,鸭子和小鸡……

1.代码重写

自动编码的一种广泛形式是代码重写-用于将一种语言版本的语法转换为另一种语言,有时甚至完全转换为另一种语言。
例如,诸如Scala之类的语言实际上是将您的代码重写为与Java兼容的代码。
著名的奇怪的Lombok项目基本上在您键入时重写Java代码,生成幻像方法,因此您可以专注于出色。
就像Babel一样,任何优秀的JavaScript开发人员都可以证明“编译器”是如何将您的高级ES2017重写为已使用了10年的,与浏览器兼容的旧版JavaScript,因此您可以编写现代代码,而不必担心浏览器的行为方式。

让计算机进行肮脏的工作??甜!

2.基于模板的生成

React生态系统在创建基于模板的应用程序框架(包括React的“ create-react-app” CLI命令)方面特别活跃。
在整个软件行业中,其他基于模板的代码生成也很普遍。HTML模板和流行的新静态站点生成器通常利用流行的胡须或车把模板引擎来利用模板解析。
在服务器模板引擎(例如Velocity)以及基于服务器的胡须和车把实现中,吐出了预渲染的源文件(例如HTML和越来越多的JavaScript)。

3.运行时动态代码

模板并不是生成代码的唯一方法。
通过自省,例如Java和Java Reflection API等语言以及Ruby Metaprogramming,运行代码可以是“自我感知”的,一旦您的代码自我感知,构建真正动态应用程序的选项就会真正扩展。
在Starter,我们使用Java构建了StackGen生成器引擎,这意味着我们能够使用便捷的JavaPoet库来动态构建Java类文件,其中包括在运行时动态合成的功能。有趣的是,这些类可以在内存中编译并由类加载器加载,而无需写入磁盘。
他们说Java不是动态语言!
这种类型的动态生成的代码令人兴奋,因为它不仅节省了我们手动编写代码的时间和金钱,而且还提供了机会在运行时在应用程序中创建响应行为,而无需人工干预(可能基于AI决策)使我们的应用程序和数字体验比以往更具吸引力和实用性。

但是生成的代码很烂!

不幸的是,软件行业的历史上充斥着代码生成器,无代码开发工具,所见即所得前端生成器的示例,并被似乎从未消亡的古老平台吐出的普遍的加密源代码恐怖所困扰(我正在寻找在您的DreamWeaver Ultradev上还是应该说Drumbeat 2000 ?!)。
当机器充满了晦涩的技术和混乱的变量名时,谁想要处理机器吐出的代码?生成的昔日代码只是令人沮丧和悲伤,而您想要建立的任何东西都没有。

幸运的是,时代变了

在2019年,工具和软件开发的最佳实践已经发展到人类和机器可以真正在中间相遇的地步。诸如APIcur.io,StackGen和Microsoft PowerApps之类的代码生成器经过发展,可以克服过去的局限性,同时将所有代码生成技术中的最佳功能结合到现代开发工具中。

代码生成的优点

  • 无需手动编码=更少的错误
  • 实现更大,更复杂的模式可能节省大量时间
  • 简单应用和PoC的快速上市周期
  • 稳定的基础架构意味着更少的时间浪费
  • CI / CD友好,可以将代码生成作为步骤添加到任何CI管道或开发工作流程中
  • 端到端生成消除了处理多个源文件和文件类型时的错误
  • 可以将新功能集成到基础模板中,以在单个构建中跨大型代码库实现
  • 但这并不是说代码生成没有缺点。

代码生成的缺点

  • 并非所有应用程序都将从代码生成中受益
  • 与手动选择每个代码库,模式和编码样式相比,代码生成更加不灵活
  • 对基础模板的更改将推广到所有生成的文件,因此更改必须高度兼容并经过全面测试
  • 生成的代码必须在代码库中与开发人员代码仔细隔离- 存在覆盖开发人员代码的风险,并且应该可以在不影响任何现有代码的情况下进行重新生成
  • 代码生成会增加一些复杂性-例如,要真正了解生成器并与之合作,您需要了解生成的代码及其原因-除了代码库的常规问题之外
  • 编写其他代码的编写代码的思维模式可能会变得松散,尤其是在编写生成其他应用程序的应用程序时

LCDP和将创新推向边缘

LCDP(低代码开发平台)作为“无代码开发”的最新形式以及桥接高级用户/ jr的新方法而受到关注。开发者差距。
随着对新功能和新系统需求的增长,开发人员变得捉襟见肘,许多用户被超载的Excel电子表格所束缚,或者由于各个级别的开发人员资源稀缺而根本没有所需的软件功能。
消费者SaaS解决方案可以完成许多一次性任务,但是一堆杂乱无章的Web应用程序和服务并不是满足许多需求的可靠基础。
另一方面,对于许多组织来说,昂贵的企业系统根本不是一个选择。
除了节省开发成本外,授权“公民开发人员”在适当时构建自己的解决方案是LCDP价值主张的主要驱动力。
代码生成器直接位于解决方案域的中间。

使用OpenAPI和StackGen端到端生成

StackGen采用整体方法来生成代码-基于OpenAPI / Swagger模式。
开发从一开始就开始,重点是使用OpenAPI / Swagger设计出色的API 。
然后,该设计准则使您可以生成具有可预测行为的健壮堆栈,并使用此处的任何自定义逻辑和前端设计对其进行精确扩展。
为了达到这种灵活性,StackGen使用了3种代码生成步骤以及3种不同的实现方式。
系统的3个主要组件使用了多种生成技术:Swagger CodeGen,MyBatis生成的DAO和映射,以及即将到来的StackGen PRO,ReactJS前端生成。
Swagger CodeGen和StackGen React代码都使用Mustache从Mustache模板文件生成React内容。

  • Mustache
  • JavaPoet
  • Swagger CodeGen
  • MyBatis Generator

这种方法的优势在于它是可插入的,并允许我们在运行时和编译之前生成。
通过生成对开发人员友好的格式的高质量源代码,现代代码生成器在提供现代高度可扩展且健壮的基准代码库的同时,使全栈开发人员的效率倍增。

关于作者

约翰·麦克马洪(John McMahon)是Starter Inc.的首席执行官,该公司是Extentech Inc的创始人,并且多年来是StackGen,Sheetster,OpenXLS,ExtenXLS和众多软件项目的开发商。

翻译原文

https://medium.com/bigdecimal/code-that-codes-pros-and-cons-of-code-generators-15b2e571281a

可以编写代码的代码:代码生成的利与弊 – 聚变归来 – 博客园已关闭评论
备案信息冀ICP 0007948