整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

对微前端的11个错误认识

对微前端的11个错误认识

文最初发布于 Bits and Pieces 博客,经原作者授权由 InfoQ 中文站翻译并分享。

微前端是一个可以追溯到多年前的新趋势。随着新方法的出现以及各种挑战被克服,它们正在慢慢地进入主流。但遗憾的是,许多非常明显的认识误区,让许多人很难理解微前端到底是什么。

简而言之,微前端就是将微服务的一些好处引入前端。除此之外,我们不应该忘记,微服务也不是什么“银弹”。

提示:要在微前端或任何其他项目之间共享 React/Angular/Vue 组件,可以使用像 Bit 这样的工具。它允许你从任何代码库中“harvest”组件,并将它们共享到 bit.dev 的一个集合中。它让团队可以在任何存储库中使用你的组件。使用它可以优化协作、加速开发和保持 UI 一致性。

示例:在 bit.dev 中查找共享 React 组件

认识误区

我想列一下在过去几个月中,我最常听到的关于微前端的误解。先从从一个明显的例子开始。

微前端需要 JavaScript

目前,许多微前端解决方案都是 JavaScript 框架。这怎么可能错呢?JavaScript 不再是可选的。每个人都想要高度交互的体验,而 JS 在提供这些体验中发挥着至关重要的作用。

除了加载速度快、可访问 Web 应用的优点外,还有其他因素应该考虑。因此,许多 JavaScript 框架都提供了 isomorphic 渲染能力。最终,这让它们不仅能够在客户端进行拼接(stitch),还能在服务器上准备好一切。如果有性能要求(如第一次有意义渲染的初始时间),这个选项听起来很不错。

请记住,isomorphic 渲染有其自身的挑战。

然而,即使一个 JavaScript 解决方案没有提供 isomorphic 呈现,这也没什么问题。如果不想在构建微前端时使用 JavaScript,我们当然可以这样做。有许多模式,其中很多根本不需要 JavaScript。

考虑一种“比较旧的”模式:使用<frameset>。我听见你笑了?好吧,有一些现如今人们试图做的分割,它以前就支持了(下文有更详细的讨论)。一个页面(可能由另一个服务渲染)负责菜单,而另一个页面负责标题。

复制代码

<frameset cols="25%,*,25%">  <frame src="menu.html">  <frame src="content.html">  <frame src="sidebar.html"></frameset>

如今,我们使用更灵活(且仍然受到活跃支持)的<iframe>元素。它们提供了一些很好的特性——最重要的是使得不同的微前端相互隔离,但仍然可以通过postMessage进行通信。

微前端只在客户端有效

在 JavaScript 认识误区之后,这是下一个层次。当然,在客户端有多种技术可以实现微前端,实际上,我们甚至不需要<iframe>或任何类似的技术。

微前端可以像服务器端“includes”一样简单。有了诸如 Edge Side Includes 之类的高级技术,这将变得更加强大。如果我们排除了在微前端功能中实现的微前端场景,那么即使是简单的链接也可以很好的工作。最终,微前端解决方案也能像小而独立的服务器端渲染器一样简单。每个渲染器可能只有一个页面那么小。

下图展示了在反向代理中发生的更高级的拼接:

通过反向代理实现服务器端拼接

当然,可能 JavaScript 有许多优点,但它仍然高度依赖于你试图通过微前端解决的问题。根据你的需要,服务器端解决方案可能仍然是最好的(或者至少是更好的)选择。

你应该使用多个框架

在几乎每一个关于微前端的教程中,不同的部分不仅由不同的团队开发,而且使用了不同的技术。这是假的。

适当的微前端方法可能使用不同的技术,但是,这不应该是目标。我们做微服务也不只是为了在后端拼凑技术。如果我们使用多种技术,那只是因为我们获得了一个特定的好处。

我们的目标应该始终是某种统一性。最好的方法是考虑一个新项目:我们会怎么做?如果答案是“使用单一框架”,那么我们就走上正轨了。

长远来看,有很多原因可以解释为什么应用程序中会出现多个框架。可能是遗留的,可能为了方便,也可能是一个概念验证。无论是什么原因:能够处理这种场景还是不错的,但它绝不应该是开始就希望达到的状态。

不管你的微前端框架多高效——使用多个框架总是要付出不可忽视的代价。不仅初始渲染会花费更长的时间,而且内存消耗也会朝着错误的方向发展。不能使用方便模型(例如,针对某个框架的模式库)。需要更多的重复。最终,程序的 Bug 数量、不一致行为和可感知的响应性都会受到影响。

按技术组件划分

一般来说,这没有多大意义。我还没见过微服务后端的数据处理在一个服务中而 API 在另一个服务中。通常,服务由多个层组成。虽然某些技术内容(如日志记录)肯定会引入到公共服务中,但有时也会使用诸如 Sidecar 之类的技术。此外,还需要通用服务编程技术。

对于微前端,情况也是如此。为什么一个微前端只能做菜单?每个微前端都有相应的菜单吗?拆分应该根据业务需求来做,而不是技术决策。如果你读过一些关于领域驱动设计的书,你就会知道它是关于定义这些领域的——而这个定义与任何技术要求无关。

考虑以下划分:

按布局分解成微前端

这些都是技术组件,和微前端无关。在一个真实的微前端应用程序中,屏幕可能看起来是这样的。

按领域分解成微前端

的确,这里的拼接要复杂得多,但这是一个可靠的微前端应用程序应该为你提供的!

不应该共享任何东西

不。你应该共享那些值得共享的东西。你绝对不应该共享所有东西(见下一条)。但要做到始终如一,你至少需要共享一套原则。至于是通过共享库、共享 URL,或者只是在构建或设计应用程序时使用的文档,那就不重要了。

对于微服务,“无共享”架构如下图所示:

微服务的“无共享”架构

在浏览器中,这将导致使用<iframe>,因为目前没有其他方法可以防止资源泄漏。使用影子 DOM,则 CSS 可能会被隔离,但脚本层面仍然能访问所有内容。

即使想遵循无共享的架构,我们也会遇到麻烦。

当然,共享的程度越深(例如,使用一个通过应用 shell 追加到 DOM 的共享库),就越会出问题。另一方面,共享程度越浅(例如,只是一个指定基本设计元素的文档),就会出现更多的不一致性。

应该共享一切

绝对不是。如果这样想,那么单体更有意义。就性能而言,这可能已经是一个问题了。什么可以延迟加载?我们能去掉一些东西吗?但真正的问题是依赖管理。什么都不能更新,因为它可能会破坏某个东西。

共享部件的好处是一致性保证。

现在,如果我们共享所有的东西,我们就是用复杂性来换一致性。这种一致性是不可维护的,因为复杂性会在每个角落引入 Bug。

问题的根源在于“依赖地狱”。下图很好地说明了这一点。

简而言之,如果一切都相互依赖,那么我们就会有依赖问题。仅仅更新一个方框就会影响整个系统。一致吗?确实。简单的?绝不。

微前端只是 Web 端的

为什么只是 Web?诚然,到目前为止,我们接触到的主要是 Web,但其概念和想法可以应用于任何类型的应用程序(移动应用、客户端应用……甚至是 CLI 工具)。在我看来,微前端只是“插件架构”的一个花哨叫法。不过,插件接口如何设计,以及运行使用插件的应用程序需要具备什么条件,这就是另外一回事了。

下图显示了一个非常通用的插件架构,来自 Omar Elgabry :

通用插件架构

该架构并没有在哪里运行的概念。它既可以在手机上运行,也能在 Windows 上运行,甚至还能在服务器上运行。

微前端需要大型团队

为什么?如果解决方案超级复杂,那么我肯定会找一个简单的。有些问题需要复杂的解决方案,但好的解决方案通常是简单的。

根据场景的不同,它甚至可能不需要一个分布式团队。拥有分布式团队是采用微前端的首要原因之一,但这不是唯一原因。另一个很好的理由是特性的粒度

如果从业务的角度来看微前端,那么你就会发现,拥有启用和关闭特定特性的能力是很有意义的。针对不同的市场,使用不同的微前端。回到一个简单的权限模式,这是有意义的。不需要编写代码来根据特定条件打开或关闭某些东西。所有这些都留给公共层,可以根据(可能是动态的)条件激活或停用。

这样,不能(或不应该)使用的代码也不会被交付。虽然这不应该是一个保护层,但它肯定是一个便捷(和性能)层。用户不会感到困惑,因为他们看到的是他们能做的。他们看不到没有交付的功能,所以没有字节浪费在不可用的代码上。

微前端无法调试

对于任何类型的实现(或供讨论的底层架构),开发经验都可能遭到削弱。应对这种情况的唯一方法是开发人员优先。实现中的第一原则应该是:使调试和开发成为可能。采用标准的工具。

有些微前端框架根本不接受这一点。有些需要在线连接、专用环境、多重服务……这不应该是标准,也绝不是常态。

微服务需要微前端(或反过来)

解耦的模块化后端可能为解耦前端打下了一个很好的基础,但通常情况下,情况并非如此。后端单体,前端模块化,也是完全可行的,例如,为简化个性化可能就要结合授权、权限和市场。

实际上,同样,微服务后端并不能证明适合将类似的模式应用于前端。许多微服务后端都是由单用途的应用程序操作的,它们的功能没有增加,只是外观发生了改变。

微前端需要单存储库

我已经读到过好几次,要创建一个微前端解决方案,就需要利用单存储库,最好使用像 Lerna 这样的工具。我不认可这一点。当然,单存储库有一些优点,但也有明显的缺点。

虽然有一些微前端框架需要联合 CI/CD 构建,但大多数都不需要。联合 CI/CD 构建通常会导致单存储库,因为其设置要简单得多。但对我来说,这是单体重新打包。如果你在单存储库上进行联合构建,那么你就失去了让微前端富有吸引力的两个非常重要的优点:

  1. 独立部署
  2. 独立开发

不管怎样,如果你看到微前端解决方案需要单存储库:那样做就行。一个精心设计的单体系统可能会更好,它不会有分布式系统的所有问题。

结论

微前端技术并不适合所有人。我不认为微前端是未来的发展趋势,但我也相信它们在未来会发挥重要作用。


关注我并转发此篇文章,私信我“领取资料”,即可免费获得InfoQ价值4999元迷你书,点击文末「了解更多」,即可移步InfoQ官网,获取最新资讯~

一文章中整合了spring cloud 用Sentinel来实现降级熔断,并用Nacos来作为配置存储。需要看的朋友可以查看我的主页,或者直接点击:面对大流量如何优雅熔断降级Sentinel实践 。在上文我们实现了Sentine控制台将配置推送到应用服务,但是还没有实现Sentine控制台将配置实时推拉到Nacos,本文将通过修改官方的源码来实现此步骤。

上篇文章中的推拉数据如下:


Sentinel控制台并没有与远程配置中心Nacos相连接。本文将实现连接到配置中心。


一、下载官方源码

可以下载最新的版本的:

https://github.com/alibaba/Sentinel

,也可以下载稳定版本的地方下载源码:

https://github.com/alibaba/Sentinel/releases

二、修改pom.xml中的sentinel-datasource-nacos的依赖,将<scope>test</scope>注释掉,这样才能在主程序中使用

具体如下:

       <!-- for Nacos rule publisher sample -->
        <dependency>
            <groupId>com.alibaba.csp</groupId>
            <artifactId>sentinel-datasource-nacos</artifactId>
<!--            使用Sentinel Dashboard动态推拉数据同步到Nacos 注掉test-->
<!--            <scope>test</scope>-->
        </dependency>


三、修改sidebar.html的dashboard.flowV1为dashboard.flow

找到resources/app/scripts/directives/sidebar/sidebar.html中的这段代码

把原来:

<li ui-sref-active="active">
  <a ui-sref="dashboard.flowV1({app: entry.app})">
    <i class="glyphicon glyphicon-filter"></i>  流控规则
  </a>
</li>

修改为:

<li ui-sref-active="active">
  <a ui-sref="dashboard.flow({app: entry.app})">
    <i class="glyphicon glyphicon-filter"></i>  流控规则
  </a>
</li>

如下:

四、修改flow_v2.html里的dashboard.flowV1为dashboard.flow

原来:

<a class="btn btn-default-inverse" style="float: right; margin-right: 10px;" ui-sref="dashboard.flowV1({app: app})">
  回到单机页面
</a>

修改为:

<a class="btn btn-default-inverse" style="float: right; margin-right: 10px;" ui-sref="dashboard.flow({app: app})">
  回到单机页面
</a>

修改效果如下:

五、修改identity.js文件的 FlowServiceV1 为FlowServiceV2

如下图:

六、开始修改dashboard的java源码,读取为通过Nacos

包的结构图如下:

1、添加FlowRuleNacosProvider

import com.alibaba.csp.sentinel.dashboard.datasource.entity.rule.FlowRuleEntity;
import com.alibaba.csp.sentinel.dashboard.rule.DynamicRuleProvider;
import com.alibaba.csp.sentinel.datasource.Converter;
import com.alibaba.csp.sentinel.util.StringUtil;
import com.alibaba.nacos.api.config.ConfigService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.List;

/**
 * @author Eric Zhao,yaokj
 * @since 1.4.0
 */
@Component("flowRuleNacosProvider")
public class FlowRuleNacosProvider implements DynamicRuleProvider<List<FlowRuleEntity>> {

    @Autowired
    private NacosConfigProperties nacosConfigProperties;

    @Autowired
    private ConfigService configService;
    @Autowired
    private Converter<String, List<FlowRuleEntity>> converter;

    @Override
    public List<FlowRuleEntity> getRules(String appName) throws Exception {
        String dataidPostfix=NacosConfigUtil.FLOW_DATA_ID_POSTFIX;
        if(StringUtil.isNotBlank(nacosConfigProperties.getDataidPostfix())){
            dataidPostfix=nacosConfigProperties.getDataidPostfix();
        }

        String rules=configService.getConfig(appName + dataidPostfix, nacosConfigProperties.getGroupId(), 3000);
        if (StringUtil.isEmpty(rules)) {
            return new ArrayList<>();
        }
        return converter.convert(rules);

    }
}

2、添加FlowRuleNacosPublisher


import com.alibaba.csp.sentinel.dashboard.datasource.entity.rule.FlowRuleEntity;
import com.alibaba.csp.sentinel.dashboard.rule.DynamicRulePublisher;
import com.alibaba.csp.sentinel.datasource.Converter;
import com.alibaba.csp.sentinel.util.AssertUtil;
import com.alibaba.csp.sentinel.util.StringUtil;
import com.alibaba.nacos.api.config.ConfigService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.List;

/**
 * @author Eric Zhao,yaokj
 * @since 1.4.0
 */
@Component("flowRuleNacosPublisher")
public class FlowRuleNacosPublisher implements DynamicRulePublisher<List<FlowRuleEntity>> {

    @Autowired
    private NacosConfigProperties nacosConfigProperties;

    @Autowired
    private ConfigService configService;
    @Autowired
    private Converter<List<FlowRuleEntity>, String> converter;

    @Override
    public void publish(String app, List<FlowRuleEntity> rules) throws Exception {
        AssertUtil.notEmpty(app, "app name cannot be empty");
        if (rules==null) {
            return;
        }

        String dataidPostfix=NacosConfigUtil.FLOW_DATA_ID_POSTFIX;
        if(StringUtil.isNotBlank(nacosConfigProperties.getDataidPostfix())){
            dataidPostfix=nacosConfigProperties.getDataidPostfix();
        }

        configService.publishConfig(app + dataidPostfix, nacosConfigProperties.getGroupId(), converter.convert(rules));
    }
}

3、添加NacosConfig


import com.alibaba.csp.sentinel.dashboard.datasource.entity.rule.FlowRuleEntity;
import com.alibaba.csp.sentinel.datasource.Converter;
import com.alibaba.fastjson.JSON;
import com.alibaba.nacos.api.PropertyKeyConst;
import com.alibaba.nacos.api.config.ConfigFactory;
import com.alibaba.nacos.api.config.ConfigService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.List;
import java.util.Properties;

/**
 * @author Eric Zhao,yaokj
 * @since 1.4.0
 */
@Configuration
public class NacosConfig {
    @Autowired
    private NacosConfigProperties nacosConfigProperties;

    @Bean
    public Converter<List<FlowRuleEntity>, String> flowRuleEntityEncoder() {
        return JSON::toJSONString;
    }

    @Bean
    public Converter<String, List<FlowRuleEntity>> flowRuleEntityDecoder() {
        return s -> JSON.parseArray(s, FlowRuleEntity.class);
    }

    @Bean
    public ConfigService nacosConfigService() throws Exception {
        Properties properties=new Properties();
        properties.put(PropertyKeyConst.SERVER_ADDR, nacosConfigProperties.getServerAddr());
        properties.put(PropertyKeyConst.NAMESPACE, nacosConfigProperties.getNamespace());
        return ConfigFactory.createConfigService(properties);
        
    }
}

4、添加NacosConfigProperties


import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

/**
 * yaokj
 */
@Component
@ConfigurationProperties(prefix="nacos.server")
public class NacosConfigProperties {

    private String ip;

    private String port;

    private String namespace;

    private String groupId;

    private String dataidPostfix;

    public String getIp() {
        return ip;
    }

    public void setIp(String ip) {
        this.ip=ip;
    }

    public String getPort() {
        return port;
    }

    public void setPort(String port) {
        this.port=port;
    }

    public String getNamespace() {
        return namespace;
    }

    public void setNamespace(String namespace) {
        this.namespace=namespace;
    }

    public String getGroupId() {
        return groupId;
    }

    public void setGroupId(String groupId) {
        this.groupId=groupId;
    }

    public String getDataidPostfix() {
        return dataidPostfix;
    }

    public void setDataidPostfix(String dataidPostfix) {
        this.dataidPostfix=dataidPostfix;
    }

    public String getServerAddr() {
        return this.getIp()+":"+this.getPort();
    }

    @Override
    public String toString() {
        return "NacosConfigProperties{" +
                "ip='" + ip + '\'' +
                ", port='" + port + '\'' +
                ", namespace='" + namespace + '\'' +
                ", groupId='" + groupId + '\'' +
                ", dataidPostfix='" + dataidPostfix + '\'' +
                '}';
    }
}

5、添加NacosConfigUtil


/**
 * @author yaokj
 * @since 1.4.0
 */
public final class NacosConfigUtil {

    public static final String GROUP_ID="SENTINEL_GROUP";
    
    public static final String FLOW_DATA_ID_POSTFIX="-flow-rules";
    public static final String PARAM_FLOW_DATA_ID_POSTFIX="-param-rules";
    public static final String CLUSTER_MAP_DATA_ID_POSTFIX="-cluster-map";

    /**
     * cc for `cluster-client`
     */
    public static final String CLIENT_CONFIG_DATA_ID_POSTFIX="-cc-config";
    /**
     * cs for `cluster-server`
     */
    public static final String SERVER_TRANSPORT_CONFIG_DATA_ID_POSTFIX="-cs-transport-config";
    public static final String SERVER_FLOW_CONFIG_DATA_ID_POSTFIX="-cs-flow-config";
    public static final String SERVER_NAMESPACE_SET_DATA_ID_POSTFIX="-cs-namespace-set";

    private NacosConfigUtil() {}
}

6、在application.properties添加Nacos配置

#nacos
nacos.server.ip=nacos-web.test.xx.net
nacos.server.port=80
nacos.server.namespace=sentinel-config
nacos.server.group-id=SENTINEL_GROUP
nacos.server.dataid-postfix=-flow-rules

如下:


七、然后修改FlowControllerV2中的默认DynamicRuleProvider和DynamicRulePublisher

将FlowControllerV2类的:

@Autowired
@Qualifier("flowRuleDefaultProvider")
private DynamicRuleProvider<List<FlowRuleEntity>> ruleProvider;
@Autowired
@Qualifier("flowRuleDefaultPublisher")
private DynamicRulePublisher<List<FlowRuleEntity>> rulePublisher;

修改为:

@Autowired
@Qualifier("flowRuleNacosProvider")
private DynamicRuleProvider<List<FlowRuleEntity>> ruleProvider;
@Autowired
@Qualifier("flowRuleNacosPublisher")
private DynamicRulePublisher<List<FlowRuleEntity>> rulePublisher;


八、演示

通过上面的步骤已经修改完成源码了。

1、在客户端服务应用zizai-wxwork-api的配置为如下

spring:
  cloud:
    sentinel:
      transport:
        dashboard: localhost:8080
      datasource:
        ds:
          nacos:
            server-addr: nacos-web.test.xx.net:80
            dataId: ${spring.application.name}-flow-rules
            groupId: SENTINEL_GROUP
            namespace: sentinel-config
            data-type: json
            rule-type: flow
    nacos:
      discovery:
        server-addr: nacos-web.test.xx.net:80
        namespace: c8d3b947-7c0b-4cc6-9958-xx

2、Nacos的配置为如下

3、在Sentinel控制台上配置便可以同步过去


4、打开Nacos查看,可看到同步成功



实现了限流。


喜欢的朋友评论、点赞、转发、收藏本文。有疑问的在评论区留言。谢谢!

可能会认为钓鱼网站很难检测和跟踪,但实际上,许多钓鱼网站都包含唯一标识它们的HTML片段。

你可能会认为钓鱼网站很难检测和跟踪,但实际上,许多钓鱼网站都包含唯一标识它们的HTML片段。本文就以英国皇家邮政(Royal Mail)钓鱼网站为例来进行说明,它们都包含字符串css_4WjozGK8ccMNs2W9MfwvMVZNPzpmiyysOUq4_0NulQo。

这些长而随机的字符串是追踪钓鱼网站的绝佳指标,几乎可以肯定,任何含有css_4WjozGK8ccMNs2W9MfwvMVZNPzpmiyysOUq4_0NulQo的网页都是皇家邮政钓鱼工具的实例。

但是,像这样的独特字符串最终如何成为检测网络钓鱼工具标识的呢?

不幸的是,我们并不是RFC 3514的模仿者,在RFC 3514中,如果所有的IP数据包是恶意的,那么它们都包含一个标志信号。不,这些识别字符串完全是由钓鱼工具开发者无意中包含的。

→【网络安全更多技术学习资料包】←

钓鱼工具是如何诞生的?

钓鱼网站试图尽可能接近他们真正的目标网站,然而,大多数钓鱼者并不具备复制公司网站的技能。相反,他们采用了快捷方式,只是假冒了原始网站的HTML并对其进行了一些小的调整。

假冒目标网站并将其变成钓鱼工具的过程大致如下:

1.使用诸如HTTrack之类的工具复制目标网站,甚至只需在网络浏览器中点击文件→保存即可。

2.调整HTML以添加一个请求受害者个人信息的表单。

3.将其与PHP后端粘合在一起,以保存收集到的数据。

然后,可以将该工具包轻松部署到便宜的托管服务提供商上,并准备收集受害者的详细信息。

4.通过复制整个网页,钓鱼者几乎不需要什么技巧或精力即可获得一个超级逼真的钓鱼页面。但是,这种假冒模式意味着他们的钓鱼页面充满了他们实际上并不需要的东西。

特别是,原始网站中的任何特殊字符串都有可能意外地出现在最终的钓鱼工具中。这对我们来说很好,因为寻找特殊字符串是一种非常容易和可靠的方法来检测钓鱼网站。

所谓的特殊字符串就是一个足够长或复杂的字符串,该字符串在整个互联网上都是独一无二的,这可能是因为它是随机字符(如64a9e3b8)或只是因为它足够长。

那么,问题来了:为什么在最初的网站中会有这些字符串?事实证明,在现代开发实践中,网站到处都是这些足够长或复杂的字符串。

网页中长或复杂的字符串是怎么来的?

现代网站很少是100%静态的内容,当前的开发实践和网络安全特性意味着,有多种方法可以使冗长的随机字符串最终出现在网站中。以下是我所见过的各种来源的概述:

1.文件名中的哈希

现代网站通常使用诸如Webpack或Parcel之类的“捆绑包”进行处理,这些捆绑包将所有JavaScript和CSS组合成一组文件。例如,网站的sidebar.css和footer.css可能合并为一个styles.css文件。

为了确保浏览器获得这些文件的正确版本,捆绑程序通常在文件名中包含一个哈希。昨天你的网页可能使用的是styles.64a9e3b8.css,但是在更新你的样式表之后,它现在使用的是styles.a4b3a5ee.css。这个文件名的改变迫使浏览器获取新的文件,而不是依赖于它的缓存。

但这些足够长或复杂的文件名正是最近皇家邮政(Royal Mail)的钓鱼工具被发现的原因。

当钓鱼者假冒真正的皇家邮政网站时,HTML看起来是这样的:

不幸的是,不管他们用什么技术来假冒网站,文件名都没有改变。因此,通过urlscan.io查找大量使用CSS文件的钓鱼网站是很容易的:

2. 版本控制参考

网络钓鱼者针对的任何网站很可能都是由一个团队开发的,他们很可能会使用git等版本控制系统(VCS)进行协作。

一个合理的常见的选择是在网站的每一个构建中嵌入一个来自VCS的参考,这有助于完成诸如将漏洞报告与当时正在运行的代码版本相关联之类的任务。

例如,Monzo网站使用一个小的JavaScript代码片段嵌入了git commit哈希:

VCS参考资料对于安防人员来说非常有用,因为它们很容易在版本控制系统中找到。如果你发现一个钓鱼网站无意中包含了VCS参考,你就可以直接查找该网站的编写时间(也就是该网站被假冒的时间)。

3.SaaS的API密钥

网站经常使用各种第三方服务,如对讲机或reCAPTCHA。为了使用这些服务,网站通常需要包含相关的JavaScript库以及一个API密钥。

例如,Tide使用reCAPTCHA,并将这段代码作为其集成的一部分:

因为reCAPTCHA “sitekey” 对每个网站来说都是唯一的,因此任何包含字符串6Lclb0UaAAAAAJJVHqW2L8FXFAgpIlLZF3SPAo3w且不在tide.co上的页面都很可能是假冒的网站。

虽然SaaS API密钥是非常独特的,并且具有很好的指示作用,但它们变化非常少,因此无法区分从同一网站假冒出来的不同钓鱼工具。一个网站可能会使用相同的API密钥达数年之久,因此在那时创建的所有工具包都将包含相同的密钥。出于同样的原因,API密钥对于识别何时创建网络钓鱼工具包也没有任何帮助。

4. 跨站请求伪造(CSRF)令牌

事实证明,许多网络安全最佳实践也使网络钓鱼成为重要的指标。其中最常见的可能是“跨网站请求伪造”(CSRF)令牌。

简单地说,CSRF是一个漏洞,恶意网站可以借此诱骗用户在目标网站上执行经过身份验证的操作。例如,此HTML创建了一个按钮,点击该按钮可将POST请求发送到https://example.com/api/delete-my-account":

如果example.com不能防御CSRF,它将处理此请求并删除毫无戒心的用户帐户。

防御CSRF的最常见方法是使用所谓的CSRF令牌,这是一个嵌入在每个网页中的随机值,服务器希望将其与敏感请求一起发送回去。例如,example.com的“删除我的账户”按钮应该是这样的:

服务器将拒绝任何不包含预期随机值的请求。

CSRF令牌非常适合检测钓鱼网站,因为从设计上看,它们是独一无二的。

5. 内容安全策略随机数

内容安全策略(CSP)是一种较新的安全手段,可帮助防御跨网站脚本(XSS)攻击。它允许开发人员指定策略,比如只允许特定域的< script >标记,或更有趣的是,对于我们的用例,仅允许包含指定“nonce”的< script >标记。

要使用基于随机数的CSP,网站需要包含以下政策:

并且使用具有匹配随机值的脚本标签:

这有助于防止XSS攻击,因为恶意注入的JavaScript不会具有匹配的现时值,因此浏览器将拒绝运行它。

就像CSRF令牌一样,CSP随机数也构成了完美的网络钓鱼工具包检测器:它们的设计不可篡改,因此通常会为每个请求随机生成长且复杂的字符串。

6. 的资源完整性哈希

现代浏览器中可用的另一个安全功能是子资源完整性(SRI),通过允许你指定期望内容的哈希值,可以保护你免受恶意修改的JavaScript / CSS的侵害。当浏览器加载受SRI保护的JavaScript / CSS文件时,它将对内容进行哈希处理并将其与HTML中的预期哈希进行比较。如果不匹配,则会引发漏洞。

例如,以下是研究人员的博客如介绍的如何将子资源完整性用于其CSS:

这个SRI哈希值是根据研究者网站上所有CSS计算得出的,结果,尽管研究者使用的是公共博客模板,但极不可能有另一个网站具有相同的哈希值,他们必须使用完全相同的模板版本,并且必须包含所有相同的插件。

对于自定义网站比研究者更多的公司,实际上可以确保没有其他网站拥有完全相同的CSS。

如何使用这些长且复杂的字符串来防御网络钓鱼

下次当你分析网络钓鱼网站时,请注意其中一些有用的长且复杂的字符串。

文件名中的哈希可能是你遇到的最常见的事例,这些也是最有用的,因为你可以在urlscan.io上搜索文件名以查找同一工具包的其他实例。