2024年1月

Qt 是一个跨平台C++图形界面开发库,利用Qt可以快速开发跨平台窗体应用程序,在Qt中我们可以通过拖拽的方式将不同组件放到指定的位置,实现图形化开发极大的方便了开发效率,本章将重点介绍
Charts
组件与
QSql
数据库组件的常用方法及灵活运用。

在之前的文章中详细介绍了关于
QCharts
绘图组件的使用方式,本章将继续延续这个知识点,通过使用
QSql
数据库模块动态的读取某一个时间节点上的数据,当用户点击查询数据时则动态的输出该事件节点的所有数据,并将数据绘制到图形组件内,实现动态查询图形的功能。

首先我们需要生成一些测试数据,在文章课件中有一个
InitDatabase
案例,该案例中通过
QSql
组件动态创建一个
Times
表,该表中有三个字段分别记录了主机IP地址、时间、以及数据,并动态的想表中插入一些随机测试数据,读者可运行这段程序并等待十分钟以上,此时数据库
database.sqlite3
中将会出现如下所示的数据集;

再来看下主窗体是如何设计的,左侧使用一个
ComboBox
下拉选择框,右侧使用两个可自由调节的
Date/TimeEdit
组件,最底部则是一个
graphicsView
绘图组件,如下图;

由于涉及到IP地址的选择,所以在
MainWindow
主构造函数中我们需要对
ComboBox
组件进行初始化,在初始化时我们需要打开数据库并将数据库中的
Times
表,并查询到
address
字段,这里在查询语句中使用
DISTINCT
语句,该语句是用于在SQL查询中选择唯一值的关键字,它能够确保查询的结果集中每个列的值都是唯一的。

SELECT DISTINCT address FROM Times;

在代码中,上述查询的目的是从 "Times" 表中选择唯一的 "address" 列的值。如果 "Times" 表中有多个行具有相同的 "address" 值,
DISTINCT
会确保在结果中只返回一个该值,以避免重复。

当具备了这条语句那么查询唯一值将变得非常容易,当查询到对应值只有只需要通过
comboBox->addItem
即可将唯一的IP地址追加到组件中,如下代码所示;

MainWindow::MainWindow(QWidget *parent): QMainWindow(parent), ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    // 初始化绘图
    InitLineChart();

    // 初始化时间组件
    QDateTime curDateTime = QDateTime::currentDateTime();

    // 设置当前时间
    ui->dateTimeEdit_Start->setDateTime(curDateTime);
    ui->dateTimeEdit_End->setDateTime(curDateTime);

    // 设置时间格式
    ui->dateTimeEdit_Start->setDisplayFormat("yyyy-MM-dd hh:mm:ss");
    ui->dateTimeEdit_End->setDisplayFormat("yyyy-MM-dd hh:mm:ss");

    // 初始化数据库
    db = QSqlDatabase::addDatabase("QSQLITE");
    db.setDatabaseName("database.sqlite3");

    if (!db.open())
    {
        std::cout << db.lastError().text().toStdString() << std::endl;
        return;
    }

    // 查询数据库中的IP地址信息
    QSqlQuery query;
    if (query.exec("SELECT DISTINCT address FROM Times;"))
    {
        QSet<QString> uniqueAddresses;

        while (query.next())
        {
            // Assuming 'address' is the name of the column
            QString data_name = query.value(0).toString();
            uniqueAddresses.insert(data_name);
        }

        // 清空现有的项
        ui->comboBox->clear();

        // 将唯一地址添加到 QComboBox 中
        foreach (const QString &uniqueAddress, uniqueAddresses)
        {
            ui->comboBox->addItem(uniqueAddress);
        }
    }
    else
    {
        std::cout << query.lastError().text().toStdString() << std::endl;
    }
}

接着来看下如何实现
InitLineChart()
绘图函数,绘图部分由于我们不需要直接绘制所以这里可以先初始化折线图表,等待后期添加数据绘制即可,这段代码的实现如下所示;

首先,创建一个
QChart
对象,代表整个图表,并将其添加到
QGraphicsView
中。随后,通过隐藏图例提高图表的美观度。接着,创建一个
QLineSeries
对象,表示折线图中的数据序列,并将其添加到图表中。为确保正确显示,创建了X轴和Y轴的坐标轴对象,并设置了范围、格式和刻度。最后,将X轴和Y轴与折线序列关联,以便在图表中显示数据。这段代码实现了一个简单的折线图的初始化,为进一步添加和展示数据提供了基础。

// 初始化Chart图表
void MainWindow::InitLineChart()
{
    // 创建图表的各个部件
    QChart *chart = new QChart();

    // 将Chart添加到ChartView
    ui->graphicsView_line->setChart(chart);
    ui->graphicsView_line->setRenderHint(QPainter::Antialiasing);

    // 隐藏图例
    chart->legend()->hide();

    // 创建曲线序列
    QLineSeries *series0 = new QLineSeries();

    // 序列添加到图表
    chart->addSeries(series0);

    // 创建坐标轴
    QValueAxis *axisX = new QValueAxis;    // X轴
    axisX->setRange(1, 100);               // 设置坐标轴范围
    axisX->setLabelFormat("%d %");         // 设置X轴格式
    axisX->setMinorTickCount(5);           // 设置X轴刻度

    QValueAxis *axisY = new QValueAxis;    // Y轴
    axisY->setRange(0, 100);               // Y轴范围
    axisY->setMinorTickCount(4);           // s设置Y轴刻度

    // 设置X于Y轴数据集
    chart->setAxisX(axisX, series0);       // 为序列设置坐标轴
    chart->setAxisY(axisY, series0);
}

当界面中的按钮被点击后,事件触发时执行,其主要功能是从数据库中查询记录并根据用户在界面上选择的设备地址、起始时间和结束时间条件,筛选符合条件的数据,并将其显示在折线图中。

首先,获取折线图对象和数据库查询结果的指针,然后清空折线序列准备接收新的数据。通过遍历数据库查询结果,获取每条记录的字段值,同时获取用户输入的查询条件。计算时间差并限制查询范围在3600秒内,然后判断记录是否在指定的时间范围内,并将符合条件的数据点添加到折线序列中。如果查询范围超出定义,输出错误消息。

void MainWindow::on_pushButton_clicked()
{
    // 获取指针
    QLineSeries *series0=(QLineSeries *)ui->graphicsView_line->chart()->series().at(0);

    // 清空图例
    series0->clear();

    // 查询数据
    QSqlQuery query("SELECT * FROM Times;",db);
    QSqlRecord rec = query.record();

    // 赋予数据
    qreal t=0,intv=1;

    // 循环所有记录
    while(query.next())
    {
        // 判断当前记录是否有效
        if(query.isValid())
        {
            QString address_value = query.value(rec.indexOf("address")).toString();
            QString date_time = query.value(rec.indexOf("datetime")).toString();
            int this_value = query.value(rec.indexOf("value")).toInt();

            // 获取组件字符串
            QString address_user = ui->comboBox->currentText();
            QString start_user_time = ui->dateTimeEdit_Start->text();
            QString end_user_time = ui->dateTimeEdit_End->text();

            // 将时间字符串转为秒,并计算差值 (秒为单位)
            QDateTime start_timet = QDateTime::fromString(start_user_time, "yyyy-MM-dd hh:mm:ss");
            QDateTime end_timet = QDateTime::fromString(end_user_time, "yyyy-MM-dd hh:mm:ss");

            uint stime = start_timet.toTime_t();
            uint etime = end_timet.toTime_t();

            // 只允许查询小于3600秒的记录
            uint sub_time = etime - stime;
            if(sub_time <= 3600)
            {
                // 查询指定区间内的数据
                if(date_time.toStdString() >= start_user_time.toStdString() &&
                        date_time.toStdString() <= end_user_time.toStdString() &&
                        address_value == address_user
                        )
                {
                    // std::cout << "区间内的数据: " << this_value << std::endl;
                    series0->append(t,this_value);
                    t+=intv;
                }
            }
            else
            {
                std::cout << "查询范围超出定义." << std::endl;
                return;
            }
        }
    }
}

这段代码实现了通过用户输入条件查询数据库,并动态更新折线图的功能,用于在界面上显示符合条件的数据趋势。

至此数据库与绘图组件的联动效果就实现了,其实很容易理解,因为是一个案例并没有包含任何复杂的功能这也是为了方便功能的展示,读者可自行运行并查询一个区间内的折线图,如下所示;

​这是全网最强的Java设计模式实战教程。此教程用
实际项目场景
,结合SpringBoot,让你
真正掌握
设计模式。

网址是:
Java设计模式实战专栏介绍 - 自学精灵
(也可以百度搜索“自学精灵”)。

本设计模式专栏的威力


  1. Java实战
    来介绍常用的设计模式,让你
    真正掌握
    设计模式。

  2. 项目实际场景
    进行设计模式实战,与
    SpringBoot
    结合,让你
    学完就会在项目中应用
    ,就会进行
    项目架构
    !!
  3. 介绍常用设计模式在项目中的典型应用,让你面试时
    收割offer

    吊打面试官

资料截图

入口页面

内容页面

本专栏与其他资料的对比

其他资料的特点

  1. 对设计模式含义的描述只有生活中的,没有项目中的。
  2. 一个实际项目实例都没有,都是描述生活的代码,这种例子很难让人联想到项目。
  3. 代码是裸Java写的,没结合SpringBoot,很繁琐。
  4. 每种模式只用一种写法,没有各种写法的对比。

以上最终导致:学完后无法将设计模式应用于项目,面试官问也答不上来,
学了等于白学

这套资料的特点


  1. 生活例子帮助理解
    模式的思维,用
    实际项目案例
    让你理解如何应用。
  2. 代码实例都是
    实际项目场景
    ,让你学会实际项目如何使用。
  3. 代码有裸Java的繁琐写法,也有
    结合SpringBoot
    的简洁写法。
  4. 每种模式有从繁琐到简洁的多种写法,
    有各种写法的对比

学完这套设计模式实战,你将能直接
应用于项目
,能
设计复杂的项目
,也能直接
吊打面试官

为什么要学习设计模式?

  1. 设计模式是
    中高级Java
    开发(包括开发组长和架构师)
    必须掌握的技能

    • 如果没掌握设计模式,就无法设计和架构项目的核心功能,就只能做个初级Java开发。
  2. 设计模式可以
    提高开发效率
    、提高代码
    复用性

    扩展性

    维护性
  3. 设计模式是Java
    后端面试必问的内容

学习设计模式的方法?

有效的学习方法

  1. 掌握常用的设计模式(会实战),了解不常用的设计模式(知道名字就行)。
  2. 先了解大体概念,然后用
    项目的实际场景
    去实际写代码。

跟着本专栏进行学习,就能快速、彻底地掌握设计模式及其应用。

无效的学习方法

  1. 企图掌握所有的设计模式
    • 不常用的那些设计模式,根本没必要掌握,了解即可。
  2. 看PDF或者是书籍
    • 我看过很多设计模式书籍和PDF
    • 看完后发现,书里那些内容,根本无法落地到Java实际开发,面试时问到也说不出来!
      学了等于白学
  3. 看网上其他人设计模式文章
    • 网上的设计模式实战的文章都是以生活中的例子写代码。
    • 看完后发现,他们那些文章,根本
      无法落地到Java实际开发
      ,也没有与SpringBoot结合,面试时问到也说不出来!
      学了等于白学

前言

OpenCV是一个基于Apache2.0许可(开源)发行的跨平台计算机视觉和机器学习软件库,它具有C++,Python,Java和MATLAB接口,并支持Windows,Linux,Android和Mac OS。OpenCvSharp是一个OpenCV的 .Net wrapper,应用最新的OpenCV库开发,使用习惯比EmguCV更接近原始的OpenCV,该库采用LGPL发行,对商业应用友好。

1. 项目环境

  • 编码环境:Visual Studio Code
  • 程序框架:.NET 6.0

目前在Mac OS上使用C#语言官方提供了编译
Visual Studio for Mac
,但是根据官方发布的通知后续将不再支持该软件更新,后续将全部转移到
Visual Studio Code
平台,所以在此处我们演示使用
Visual Studio Code
进行演示。而代码的运行与配置使用
dotnet
指令实现。

关于
Visual Studio Code
以及
.NET
的安装方式可以参考一下官方教程:
在 macOS 上安装 .NET

Visual Studio Code on macOS

2. 创建控制台项目

此处使用
dotnet
指令创建新项目,在
Visual Studio Code
的终端中输入一下指令:

dotnet new console --framework net6.0 --use-program-main -o test_opencvsharp

如下图所示,在终端中输入以下指令后,会自动创建新的项目以及项目文件夹。
image

在创建好项目后,我们进行一下项目测试,依次输入以下指令,最后会得到输出:"Hello, World!":

test_opencvsharp
dotnet run

3. 添加 Nuget Package 程序包

OpenCvSharp4是一个可以跨平台使用的程序包,并且官方也提供了编译好的程序包,用户可以根据自己的平台进行安装。在Mac OS上,主要需要安装一下两个包,分别是OpenCvSharp4的官方程序包以及OpenCvSharp4的运行依赖包。

dotnet add package OpenCvSharp4
dotnet add package OpenCvSharp4.runtime.osx_arm64 --prerelease

安装完上面两个安装包后,项目的配置的文件中会增加下面两个配置。

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net6.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="OpenCvSharp4" Version="4.8.0.20230708" />
    <PackageReference Include="OpenCvSharp4.runtime.osx_arm64" Version="4.8.1-rc" />
  </ItemGroup>

</Project>

emsp; 接下来运行
dotnet run
,检验项目中是否包含所需要的配置文件:
OpenCvSharp.dll

runtimes/osx-arm64/native/
。打开项目运行生成的文件夹
bin/{build_config}/{dotnet_version}/
,在本项目中是
bin/Debug/net6.0/
文件夹,如下图所示:

image

可以看出,在程序运行后,安装的程序包中所有项目都已经加载到当前项目中,如果出现缺失,就需要找到程序包位置,将该文件复制到指定路径。

3. 测试应用

最后我们编写项目代码进行测试,如下面代码所示:

using System;
using OpenCvSharp;
namespace test_opencvsharp 
{
    internal class Program
    {
        static void Main(string[] args)
        {
            Mat image = Cv2.ImRead("image.jpg");
            Mat image2=new Mat();
            if (image!=null)
            {
                Console.WriteLine("srcImg is OK!");
            }
            Console.WriteLine("图像的宽度是:{0}",image.Rows);
            Console.WriteLine("图像的高度是:{0}", image.Cols);
            Console.WriteLine("图像的通道数是:{0}", image.Channels());
            Cv2.ImShow("src", image);
            Cv2.CvtColor(image, image2, ColorConversionCodes.RGB2GRAY);//转为灰度图像
            Cv2.ImShow("src1", image2);
            Cv2.WaitKey(0);
            Cv2.DestroyAllWindows();//销毁所有窗口
        }
    }
}

项目代码运行后,最后呈现效果如下图所示:

image

4. 总结

在本次项目中,我们成功实现了在Mac OS上使用OpenCvSharp,并成功配置了OpenCvSharp依赖库,实现了在.NET 6.0环境下使用C#语言调用OpenCvSharp库,实现的图片数据的读取以及图像色彩转换,并进行了图像展示。

前言

最近知识星球中有位小伙伴问了我一个问题:如何保证接口的安全性?

根据我多年的工作经验,这篇文章从11个方面给大家介绍一下保证接口安全的一些小技巧,希望对你会有所帮助

1 参数校验

保证接口安全的第一步,也是最重要的一步,需要对接口的请求参数做校验。

如果我们把接口请求参数的校验做好了,真的可以拦截大部分的无效请求。

我们可以按如下步骤做校验:

  1. 校验参数是否为空
    ,有些接口中可能会包含多个参数,有些参数允许为空,有些参数不允许为空,我们需要对这些参数做校验,防止接口底层出现异常。
  2. 校验参数类型
    ,比如:age是int类型的,用户传入了一个字符串:"123abc",这种情况参数不合法,需要被拦截。
  3. 校验参数的长度
    ,特别是对于新增或者修改数据接口,必须要做参数长度的校验,否则超长了数据库会报异常。比如:数据库username字段长度是30,新用户注册时,输入了超过30个字符的名称,需要提示用户名称超长了。虽说前端会校验字段长度,但接口对参数长度的校验也必不可少。
  4. 校验枚举值
    ,有些接口参数是枚举,比如:status,数据库中设计的该字段只有1、2、3三个值。如果用户传入了4,则需要提示用户参数错误。
  5. 校验数据范围
    ,对于有些金额参数,需要校验数据范围,比如:单笔交易的money必须大于0,小于10000。

我们可以自己写代码,对每个接口的请求参数一一做校验。

也可以使用一些第三方的校验框架。

比如:hiberate的Validator框架,它里面包含了@Null、@NotEmpty、@Size、@Max、@Min等注解。

用它们校验数据非常方便。

当然有些日期字段和枚举字段,可能需要通过自定义注解的方式实现参数校验。

2 统一封装返回值

可能有些小伙伴认为,对接口返回值统一封装是为了让代码更规范。

其实也是处于安全方面的考虑。

假如有这样一种场景:你写的某个接口底层的sql,在某种条件下有语法问题。某个用户请求接口之后,在访问数据库时,直接报了sql语法错误,将数据库名、表名、字段名、相关sql语句都打印出来了。

此时,如果你的接口将这些异常信息直接返回给外网的用户,有些黑客拿着这些信息,将参数做一些调整,拼接一些注入sql,可以对你的数据库发起攻击。

因此,非常有必要对接口的返回值做统一的封装。

例如下面这样:

{
    "code":0,
    "message":null,
    "data":[{"id":123,"name":"abc"}]
}

该json返回值中定义了三个字段:

  • code
    :表示响应码,0-成功,1-参数为空,2-参数错误,3-签名错误 4-请求超时 5-服务器内部错误等。
  • message
    :表示提示信息,如果请求成功,则返回空。如果请求失败,则返回我们专门在代码中处理过,让用户能看懂的错误信息。
  • data
    :表示具体的数据,返回的是一个json字段。

对返回值这样封装之后,即使在接口的底层出现了数据库的异常,也不会直接提示用户,给用户提示的是
服务器内部错误

对返回值统一封装的工作,没有必要在业务代码中做,完全可以在放到API网关。

业务系统在出现异常时,抛出业务异常的RuntimeException,其中有个message字段定义异常信息。

所有的API接口都必须经过API网关,API网关捕获该业务异常,然后转换成统一的异常结构返回,这样能统一返回值结构。

3 做转义

在用户自定义输入框,用户可以输入任意内容。

有些地方需要用html的格式显示用户输入的内容,比如文章详情页或者合同详情页,用户可以自定义文案和样式。

这些地方如果我们不做处理,可能会遭受XSS(Cross Site Scripting)攻击,也就是跨站脚本攻击。

攻击者可以在输入的内容中,增加脚本,比如:
<script>alert("反射型 XSS 攻击")</script>
,这样在访问合同详情页时,会弹出一个不需要的窗口,攻击者甚至可以引导用户访问一些恶意的链接。

由此,我们需要对用户输入内容中的一些特殊标签做
转义

下面这张图中列出了需要转义的常见字符和转义后的字符:

我们可以自定义一个转义注解,打上该注解的字段,表示需要转义。

有个专门的AOP拦截器,将用户的原始内容,转换成转义后的内容。

保存到数据库中是转义之后的内容。

除此之外,为了防止SQL注入的情况,也需要将用户输入的参数做SQL语句方面的转义。

关于SQL注入问题,可以参考我之前写的这篇文章:
卧槽,sql注入竟然把我们的系统搞挂了

4 做权限控制

我们需要对接口做权限控制。

主要包含了下面3种情况。

4.1 校验是否登录

对于用些公共数据,比如:外部分类,所有人都能够看到,不用登录也能看到。

对于这种接口,则不用校验登录。

而对于有些查看内部分类的接口,需要用户登录之后,才能访问。

这种情况就需要
校验登录
了。

可以从当前用户上下文中获取用户信息,校验用户是否登录。

如果用户登录了,当前用户上下文中该用户的信息不为空。

否则,如果用户没登录,则当前用户上下文中该用户的信息为空。

4.2 接口功能权限控制

对于有些重要的接口,比如订单审核接口,只有拥有订单审核权限的运营账号,才有权限访问该接口。

我们需要对该接口做
功能权限控制

可以自定义一个权限注解,在注解上可以
添加权限点

在网关层有个拦截器,会根据当前请求的用户的权限,去跟请求的接口的权限做匹配,只有匹配上次允许访问该接口。

4.3 接口数据权限控制

对于有些订单查询接口,普通运营只能查看普通用户的数据。

而运营经理可以查看普通用户和vip用户的数据。

这种情况我们需要对该订单查询接口做
数据权限控制

不同的角色,能够查看的数据范围不同。

可以在查询数据时,在sql语句中动态拼接过滤数据权限的sql。

5 加验证码

对于一些非常重要的接口,在做接口设计的时候,要考虑恶意用户刷接口的情况。

最早的用户注册接口,是需要用图形验证码校验的,比如下面这样的:

用户只需要输入:账号名称、密码和验证码即可,完成注册。

其中账号名称作为用户的唯一标识。

但有些图形验证码比较简单,很容易被一些暴力破解工具破解。

由此,要给图形验证码增加难道,增加一些干扰项,增加暴力破解工具的难道。

但有个问题是:如果图形验证码太复杂了,会对正常用户使用造成一点的困扰,增加了用户注册的成本,让用户注册功能的效果会大打折扣。

因此,仅靠图形验证码,防止用户注册接口被刷,难道太大了。

后来,又出现了一种移动滑块形式的图形验证方式,安全性更高。

此外,使用验证码比较多的地方是发手机短信的功能。

发手机短信的功能,一般是购买的云服务厂商的短信服务,按次收费,比如:发一条短信0.1元。

如果发送短信的接口,不做限制,被用户恶意调用,可能会产生非常昂贵的费用。

6 限流

上一节中提到的发送短信接口,只校验
验证码
还不够,还需要对用户请求做
限流

从页面上的验证码,只能限制当前页面的不能重复发短信,但如果用户刷新了页面,也可以重新发短信。

因此非常有必要在服务端,即:发送短信接口做限制。

我们可以增加一张
短信发送表

该表包含:id、短信类型、短信内容、手机号、发送时间等字段。

有用户发送短信请求过来时:

  1. 先查询该手机号最近一次发送短信的记录
  2. 如果没有发送过,则发送短信。
  3. 如果该手机号已经发送过短信,但发送时间跟当前时间比超过了60秒,则重新发送一条新的短信。
  4. 如果发送时间跟当前时间比没超过60秒,则直接提示用户操作太频繁,请稍后重试。

这样就能非常有效的防止恶意用户刷短信的行为。

但还是有漏洞。

比如:用户知道在60秒以内,是没法重复发短信的。他有个程序,刚好每隔60秒发一条短信。

这样1个手机号在一天内可以发:60*24 = 1440 条短信。

如果他有100个手机号,那么一天也可以刷你很多条短信。

由此,还需要限制每天同一个手机号可以发的短信次数。

其实可以用redis来做。

用户发短信之后,在redis中保存一条记录,key是手机号,value是发短信的次数,过期时间是24小时。

这样在发送短信之前,要先查询一下,当天发送短信的次数是否超过10次(假设同一个手机号一天最多允许发10条短信)。

如果超过10次,则直接提示用户操作太频繁,请稍后重试。

如果没超过10次,则发送短信,并且把redis中该手机号对应的value值加1。

短信发送接口完整的校验流程如下:

最近我建了新的技术交流群,打算将它打造成高质量的活跃群,欢迎小伙伴们加入。

我以往的技术群里技术氛围非常不错,大佬很多。

加微信:su_san_java,备注:加群,即可加入该群。

7 加ip白名单

对于有些非常重要的基础性的接口,比如:会员系统的开通会员接口,业务系统可能会调用该接口开通会员。

会员系统为了安全性考虑,在设计开通会员接口的时候,可能会加一个
ip白名单
,对非法的服务器请求进行拦截。

这个ip白名单前期可以做成一个
Apollo
配置,可以动态生效。

如果后期ip数量多了的话,可以直接保存到数据库。

只有ip在白名单中的那些服务器,才允许调用开通会员接口。

这样即使开通会员接口地址和请求参数被泄露了,调用者的ip不在白名单上,请求开通会员接口会直接失败。

除非调用者登录到了某一个白名单ip的对应的服务器,这种情况极少,因为一般运维会设置对访问器访问的防火墙。

当然如果用了
Fegin
这种走内部域名的方式访问接口,可以不用设置ip白名单,内部域名只有在公司的内部服务器之间访问,外面的用户根本访问不了。

但对于一些第三方平台的接口,他们更多的是通过设置
ip白名单
的方式保证接口的安全性。

8 校验敏感词

对于某些用户可以自定义内容的接口,还需要对用户输入的内容做
敏感词校验

比如:在创建商品页面,用户输入了:傻逼商品,结果直接显示到了商城的商品列表页面,这种情况肯定是不被允许的。

当然你也可以做一个审核功能,对用户创建的商品信息做人工审核,如果商品数量太多,这样会浪费很多人力。

有个比较好的做法是:对用户自定义的内容,做敏感词校验。

可以调用第三方平台的接口,也可以自己实现一个敏感词校验接口。

可以在
GitHub
上下载一个开源的
敏感词库
,将那些敏感词导入到数据库中。

然后使用
hanlp
对用户输入的内容,进行分词。对分好的词,去匹配敏感词库中的那些敏感词。

如果匹配上了,则说明是敏感词,则验证不通过。

如果没有匹配上,则说明非敏感词,则验证通过。

不可能在每个业务接口中都调用敏感词校验接口,我们可以
自定义注解
,在
AOP拦截器
中调用敏感词校验接口。

在调用业务接口之前,先触发拦截器,校验打了敏感词校验注解的那些字段,将他们里面包含的内容,作为入参传入敏感词校验接口做校验。

当然有时候hanlp分词器会把句子分错词,还需要添加一个敏感词的白名单,白名单中的词不是敏感词。

9 使用https协议

以前很多接口使用的是HTTP(HyperText Transport Protocol,即超文本传输协议)协议,它用于传输客户端和服务器端的数据。

虽说HTTP使用很简单也很方便,但却存在以下3个致命问题:

  1. 使用明文通讯,内容容易被窃听。
  2. 不验证通讯方的真实身份,容易遭到伪装。
  3. 无法证明报文的完整性,报文很容易被篡改。

为了解决
HTTP
协议的这些问题,出现了
HTTPS
协议。

HTTPS协议是在HTTP协议的基础上,添加了加密机制:

  • SSL
    :它是Secure Socket Layer的缩写, 表示安全套接层。
  • TLS
    :它是Transport Layer Security的缩写,表示传输层安全。

HTTPS = HTTP + 加密 + 认证 + 完整性保护。

为了安全性考虑,我们的接口如果能使用HTTPS协议,尽量少使用HTTP协议。

如果你访问过一些大厂的网站,会发现他们提供的接口,都是使用的HTTPS协议。

不过需要注意的地方是:HTTPS协议需要申请证书,有些额外的费用。

10 数据加密

有些信息是用户的核心信息,比如:手机号、邮箱、密码、身份证、银行卡号等,不能别泄露出去。

在保存到数据库时,我们要将这些字段,做加密处理。

后面即使这些数据被泄露了,获得数据的人,由于没有密钥,没办法解密。

这种情况可以使用AES
对称加密
的方式,因为后面系统的有些业务场景,需要把加密的数据解密出来。

为了安全性考虑,我们需要设置一个用于加密的密钥,这个密钥可以稍微复杂一点,包含一些数字、字母和特殊字符。

我们同样可以通过自定义注解的方式,给需要加密的字段添加该注解,在
Mybatis拦截器
中实现加解密的功能。

对于查询操作,需要将加了该注解的字段的数据做解密处理。

对于写入操作,要将加了该注解的字段的数据做加密处理。

有些页面显示的地方,手机号一般不会显示完整的手机号,中间有一部分用
*
代替,比如:
182***3457

这种情况需要做特殊处理。

11 做风险控制

有些特殊的接口,比如用户登录接口,我们需要对该接口做风险控制,尽可能减小
被盗号
的风险。

用户登录失败之后,需要有地方,比如:Redis,记录用户登录失败的次数。

如果用户第一次输入账号密码登录时,出现的是一个稍微简单的验证码。

如果用户把账号或密码连续输错3次之后,出现了更复杂的验证码。

或者改成使用手机短信验证。

如果用户在一天之内,把账号或密码连续输错10次,则直接锁定该账号。

这样处理是为了防止有人用一些软件,暴力破解账号和密码。

在用户登录成功之后,需要有一张表记录用户的ip、所在城市和登录的设备id。

如果你的账号被盗了。

在盗号者在页面输入账号密码登录,会调用登录接口,此时登录接口中可以根据用户的ip和设备id,做一些风险控制。

接口判断如果用户当前登录的ip、所在城市和设备ip,跟上一次登录成功时记录的相差非常大。

比如:1小时之前,用的ip是100.101.101.101,城市是北京,设备id是1001,而1小时之后,用的ip是200.202.202.101,城市是广州,设备id是2002。

这种情况用户的账号极有可能被盗了。

登录接口做安全性升级,需要校验用户手机验证码才能登录成功。

由于盗号者只有你的账号和密码,没有手机验证码,所以即使被盗号了,也没办法登录成功。

最后说一句(求关注,别白嫖我)

如果这篇文章对您有所帮助,或者有所启发的话,帮忙扫描下发二维码关注一下,您的支持是我坚持写作最大的动力。
求一键三连:点赞、转发、在看。
关注公众号:【苏三说技术】,在公众号中回复:面试、代码神器、开发手册、时间管理有超赞的粉丝福利,另外回复:加群,可以跟很多BAT大厂的前辈交流和学习。
此外,如果想要加入我的技术交流群,可以添加微信:li_su123

大家好,我是 frank,「Golang 语言开发栈」公众号作者。

01 介绍

在 Go 语言中,数组固定长度,切片可变长度;数组和切片都是值传递,因为切片传递的是指针,所以切片也被称为“引用传递”。

读者朋友们在使用 Go 语言开发项目时,或者在阅读 Go 开源项目源码时,发现很少使用到数组,经常使用到切片。

本文通过讲解 Golang 切片的一些特性,介绍 Go 语言为什么建议多使用切片,少使用数组。

02 切片

切片的底层是数组,它是可变长度,可以在容量不足时自动扩容。

type SliceHeader struct {
	Data uintptr
	Len  int
	Cap  int
}

阅读上面这段代码,
SliceHeader
结构体是切片在运行时的表现,由 3 部分组成,分别是指向底层数组的指针
Data
,长度
Len
和容量
Cap

声明方式

切片的声明方式有多种,分别是:

var s1 []int
var s2 []int{1, 2, 3}
var s3 []int = make([]int, 3)
var s4 []int = make([]int, 3, 5)

阅读上面这段代码,
s1
只声明未初始化,值为
nil

s2
字面量初始化,编译时会自动推断切片的长度,容量与长度相同;

s3
声明切片,并使用内置函数
make
初始化切片的长度为
3
,因为未指定容量,所以容量与长度相同;
s4
声明切片,并使用内置函数
make
初始化切片的长度为
3
,切片的容量为
5
,容量必须大于等于长度。

字面量初始化与使用内置函数
make
初始化的区别是,字面量初始化,编译时在数据区创建一个数组,并在堆区创建一个切片,程序启动时将数据区的数据复制到堆区;

使用内置函数
make
初始化,编译时根据切片大小判断分配到栈区,还是分配到堆区,小于 64KB 则分配到栈区,大于等于 64KB 则分配到堆区。

数组则是根据数组长度判定是否在栈区初始化,数组长度小于 4 时,编译时在栈区初始化数组。

“引用传递”

数组和切片在作为函数参数传递时,属于值传递,如果使用数组,特别是大数组时,我们需要特别小心,可以考虑使用数组指针;如果使用切片,本身就是拷贝的内存地址,所以切片也被称为“引用传递”。

自动扩容

切片可以使用内置函数
append
追加元素到切片,如果原切片容量不足时,切片可以自动扩容;数组是固定长度,如果数组长度不足时,编译时则报错,或者只能声明一个新数组,并将旧数组中的数据拷贝到新数组。

需要注意的是,虽然使用内置函数
append
追加元素,当切片容量不足时可以自动扩容切片,但是会涉及到内存分配,原切片容量小于 1024,新切片容量是原切片容量的 2 倍;

如果原切片容量大于等于 1024,新切片容量按照原切片容量的 1/4 步长循环扩容,直到新切片的容量大于等于新切片的长度为止。

03 总结

本文我们介绍 Go 语言为什么建议多使用切片,少使用数组。

主要是因为切片值传递的成本更低,更加适合作为函数参数,并且使用内置函数
append
追加切片元素时,当切片容量不足时可以自动扩容。

需要注意的是,虽然切片可以自动扩容,但在扩容时会涉及内存分配,造成系统开销,尽量在创建切片时,预估出切片的最终容量。