Posted on 2007-12-24 09:27 伯乐共勉 阅读(54) 评论(0)  编辑  收藏 所属分类: INDY与SOCKET

摘要:本文主要介绍如何在Delphi中使用SSL协议.一共分为七个部分:(1)SSL协议是什么?(2)Delphi中如何使用SSL协议?(3)SSL客户端编程实例.(4)SSL服务端编程实例.(5)SSL证书编程实例.(6)中间人欺骗实例.(7)其它.本文作者同时有一个用SSL协议编写的作品叫SSLPROXY,感兴趣的读者可以从作者主页http://www.138soft.org下载.

一:SSL协议是什么?
  SSL是一种加密传输协议.引用网上一段话:SSL 是Secure socket Layer英文缩写,它的中文意思是安全套接层协议,指使用公钥和私钥技术组合的安全网络通讯协议。SSL协议是网景公司(Netscape)推出的基于 WEB应用的安全协议,SSL协议指定了一种在应用程序协议(如Http、Telenet、NMTP和FTP等)和TCP/IP协议之间提供数据安全性分层的机制,它为TCP/IP连接提供数据加密、服务器认证、消息完整性以及可选的客户机认证,主要用于提高应用程序之间数据的安全性,对传送的数据进行加密和隐藏,确保数据在传送中不被改变,即确保数据的完整性。

二:Delphi中如何使用SSL协议?
  要使用SSL协议,有几种方法,一种是根据SSL协议文档自己实现,另外一种是调用第三方的开发库.现在一般的开发者(无论是Delphi还是VC)都是使用开源的OpenSSL库.本文的介绍都是基于OpenSSL的.
  Delphi中的Indy组件本身就是支持OpenSSL的.例如,对于IdTCPClient,只需要拖一个IdSSLIOHandlerSocket控件到窗口,然后将IdTCPClient的IOHandler指向它即可.不过IdSSLOpenSSLHeaders.pas的声明比较老,是基于OpenSSL 0.9.4的,而0.9.4的DLL文件现在比较少见了.现在一般使用OpenSSL 0.9.7i.(可以打开IdSSLOpenSSLHeaders.pas,搜索版本,老版本是这样的:

OPENSSL_OPENSSL_VERSION_NUMBER = $00904100;
  OPENSSL_OPENSSL_VERSION_TEXT = 'OpenSSL 0.9.4 09 Aug 1999';  {Do not localize}

0.9.4和0.9.7i的区别是0.9.4的部分函数位于ssleay32.dll中,而0.9.7i则抛弃了此DLL.网上有很多修改过的基于0.9.7i版本的IdSSLOpenSSLHeaders.pas,本文的演示代码主要是基于0.9.7i.

三:SSL客户端编程实例

使用OpenSSL之前,得先装载SSL库.直接调用Load函数即可完成:

if not uIdSSLOpenSSLHeaders.Load then //装载ssl库
  begin
    Application.MessageBox('装载ssl动态库失败!', '错误', MB_ICONEXCLAMATION + MB_TOPMOST);
    Halt;
  end;

然后装载错误字符库和加密算法:

IdSslLoadErrorStrings;
  IdSslAddSslAlgorithms; //load所有的SSL算法
 
  然后,初始化SSL使用的协议版本和SSL上下文:

methClient := IdSslMethodClientV23;//SSL协议有多个版本,包括SSLv2,SSLv23,SSLv3和TLSv1我们这里使用v23.
  if methClient = nil then
  begin
    Application.MessageBox('建立SSL Client所用的method失败!', '错误', MB_ICONEXCLAMATION + MB_TOPMOST);
    Halt;
  end;

ctxClient := IdSslCtxNew(methClient); //初始化上下文情景.
  if ctxClient = nil then
  begin
    Application.MessageBox('创建客户端SSL_CTX失败!', '错误', MB_ICONEXCLAMATION + MB_TOPMOST);
    Halt;
  end;

上下文情景初始化完毕后,就可以进行socket通信了.首先创建一个正常的socket,连接到目的地址,然后申请一个SSL会话的环境:
  sslClient := IdSslNew(ctxClient);
  if sslClient = nil then
  begin
    closesocket(ClientSocket);
    Application.MessageBox('申请SSL会话的环境失败!', '错误', MB_ICONEXCLAMATION + MB_TOPMOST);
    Halt;
  end;

然后绑定套接字:
  IdSslSetFd(sslClient, ClientSocket);
  IdSslConnect(sslClient);
  成功后就可以调用IdSslRead和IdSslWrite来读写数据了.
  下面我们给出一个完整的例子,用于连接安全焦点的论坛首页.此程序用到一个Memo控件和一个按钮控件.注意:此代码没有做过多的错误处理,读者可以自行添加:

procedure TForm1.Button1Click(Sender: TObject);
const
  //Url: string = 'https://www.xfocus.net/bbs/index.php?lang=cn';
  Host: string = 'www.xfocus.net';
  Port: Word = 443;
var
  Wsa: TWSAData;
  ctxClient: PSSL_CTX; //SSL上下文
  methClient: PSSL_METHOD;
  sRemoteIP: string;

ClientSocket: TSocket;
  sAddr: TSockAddr;
  sslClient: PSSL;
  pServer_Cert: PX509;
  pStr: Pchar;
  buf: array[0..4095] of Char;
  nRet: integer;
  strSend: string;
begin
  Button1.Enabled := False;

if WSAStartup($101, Wsa) <> 0 then //初始化Wsock32.dll,MakeWord(2,2),
  begin
    Application.MessageBox('初始化Winsock动态库失败!', '错误', MB_ICONEXCLAMATION + MB_TOPMOST);
    Halt;
  end;

if not uIdSSLOpenSSLHeaders.Load then //装载ssl库
  begin
    Application.MessageBox('装载ssl动态库失败!', '错误', MB_ICONEXCLAMATION + MB_TOPMOST);
    Halt;
  end;
  IdSslLoadErrorStrings;
  IdSslAddSslAlgorithms; //load所有的SSL算法

methClient := IdSslMethodClientV23;
  if methClient = nil then
  begin
    Application.MessageBox('建立SSL Client所用的method失败!', '错误', MB_ICONEXCLAMATION + MB_TOPMOST);
    Halt;
  end;

ctxClient := IdSslCtxNew(methClient); //初始化上下文情景
  if ctxClient = nil then
  begin
    Application.MessageBox('创建客户端SSL_CTX失败!', '错误', MB_ICONEXCLAMATION + MB_TOPMOST);
    Halt;
  end;

sRemoteIP := GetIPAddressByHost(Host);
  if sRemoteIP = '' then
  begin
    Application.MessageBox('获取远程IP地址失败!', '错误', MB_ICONEXCLAMATION + MB_TOPMOST);
    Halt;
  end;

ClientSocket := Socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
  if ClientSocket = INVALID_SOCKET then
  begin
    Application.MessageBox('创建socket失败!', '错误', MB_ICONEXCLAMATION + MB_TOPMOST);
    Halt;
  end;

FillChar(sAddr, Sizeof(TSockAddr), #0);
  sAddr.sin_family := AF_INET;
  sAddr.sin_addr.S_addr := inet_addr(Pchar(sRemoteIP));
  sAddr.sin_port := htons(Port);

if connect(ClientSocket, sAddr, sizeof(TSockAddr)) = SOCKET_ERROR then
  begin
    Application.MessageBox('连接远程服务器失败!', '错误', MB_ICONEXCLAMATION + MB_TOPMOST);
    Halt;
  end;

sslClient := IdSslNew(ctxClient); //申请SSL会话的环境,参数就是前面我们申请的 SSL通讯方式。返回当前的SSL 连接环境的指针。
  if sslClient = nil then
  begin
    closesocket(ClientSocket);
    Application.MessageBox('申请SSL会话的环境失败!', '错误', MB_ICONEXCLAMATION + MB_TOPMOST);
    Halt;
  end;

IdSslSetFd(sslClient, ClientSocket); //绑定读写套接字
  if IdSslConnect(sslClient) = -1 then
  begin
    IdSslFree(sslClient);
    closesocket(ClientSocket);
    Application.MessageBox('绑定读写套接字失败!', '错误', MB_ICONEXCLAMATION + MB_TOPMOST);
    Halt;
  end;

Memo1.Lines.Add(Format('SSL连接使用的算法为:%s', [IdSSLCipherGetName(IdSSLGetCurrentCipher(sslClient))]));

pServer_Cert := IdSslGetPeerCertificate(sslClient);
  if (pServer_Cert <> nil) then
  begin
    Memo1.Lines.Add('服务端证书:');
    pStr := IdSslX509NameOneline(IdSslX509GetSubjectName(pServer_Cert), nil, 0);
    if pStr <> nil then Memo1.Lines.Add(Format('主题: %s', [pStr]));
    pStr := IdSslX509NameOneline(IdSslX509GetIssuerName(pServer_Cert), nil, 0);
    if pStr <> nil then Memo1.Lines.Add(Format('发行者: %s', [pStr]));
  end
  else
  begin
    Memo1.Lines.Add('客户端没有证书.');
  end;
  Memo1.Lines.Add('');

strSend := 'GET /bbs/index.php?lang=cn HTTP/1.1'#$D#$A +
    'Accept: */*'#$D#$A +
    'Accept-Language: zh-cn'#$D#$A +
    'User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0)'#$D#$A +
    'Host: ' + Host + #$D#$A +
    'Connection: Keep-Alive'#$D#$A#$D#$A;

if IdSslWrite(sslClient, @strSend[1], Length(strSend)) <= 0 then
  begin
    IdSslFree(sslClient);
    closesocket(ClientSocket);
    Application.MessageBox('发送失败!', '错误', MB_ICONEXCLAMATION + MB_TOPMOST);
    Halt;
  end;

FillChar(buf, sizeof(buf), #0);
  nRet := IdSslRead(sslClient, buf, sizeof(buf));
  while nRet > 0 do
  begin
    Memo1.Lines.Add(StrPas(Buf));
    FillChar(buf, sizeof(buf), #0);
    Application.ProcessMessages;
    nRet := IdSslRead(sslClient, buf, sizeof(buf));
  end;

IdSslFree(sslClient);
  closesocket(ClientSocket);
  if ctxClient <> nil then IdSslCtxFree(ctxClient);
  WSACleanup; //结束对WSocket32.dll调用
  Button1.Enabled := True;
end;

四:SSL服务端编程实例

服务端没有什么好讲的,跟客户端不同的地方,在于初始化时需要加载证书,然后Accept后需要再用IdSslAccept关联.关于如何用Delphi生成证书文件,详见下一讲.下面直接贴代码:

var
  Form1: TForm1;
  g_Wsa: TWSAData;
  g_ServerSocketMain: TSocket = INVALID_SOCKET;
  g_ctxServer: PSSL_CTX = nil; //SSL上下文
  g_methServer: PSSL_METHOD = nil;
  g_DebugCritSec: TRTLCriticalSection;
  g_hAcceptThread: THandle = 0;
  g_Start: BOOL = False;

implementation

{$R *.dfm}

function AcceptThread(lp: Pointer): DWORD; stdcall;
var
  nAddrLen: integer;
  sdClient: TSocket;
  sAddrs: TSockAddr;
  nErrorCode: integer;
  sslServer: PSSL;
  pClient_Cert: PX509;
  pStr: Pchar;
  nRet: integer;
  buf: array[0..4095] of Char;
  strBody, strSend: string;
begin
  Result := 0;

while g_Start do
  begin
    nAddrLen := sizeof(TSockAddr);
    sdClient := accept(g_ServerSocketMain, @sAddrs, @nAddrLen);
    if (sdClient = INVALID_SOCKET) then
    begin
      nErrorCode := WSAGetLastError;
      EnterCriticalSection(g_DebugCritSec);
      if g_Start then Form1.Memo1.Lines.Add(Format('发生错误.错误代码:%d', [nErrorCode]));
      LeaveCriticalSection(g_DebugCritSec);
    end
    else
    begin
      EnterCriticalSection(g_DebugCritSec);
      Form1.Memo1.Lines.Add(Format('新连接.客户端IP:%s', [inet_ntoa(sAddrs.sin_addr)]));
      LeaveCriticalSection(g_DebugCritSec);

sslServer := IdSslNew(g_ctxServer); //申请SSL会话的环境,参数就是前面我们申请的 SSL通讯方式,返回当前的SSL 连接环境的指针.
      if sslServer = nil then
      begin
        closesocket(sdClient);
        Continue;
      end;

IdSslSetFd(sslServer, sdClient); //绑定读写套接字

nRet := IdSslAccept(sslServer);
   &nb"7p0�B  if (nRet = -1) then
        closesocket(sdClient);
        IdSslShutdown(sslServer);
        IdSslFree(sslServer);
        Continue;
      end;

EnterCriticalSection(g_DebugCritSec);
      Form1.Memo1.Lines.Add(Format('SSL连接使用的算法为:%s', [IdSSLCipherGetName(IdSSLGetCurrentCipher(sslServer))]));
      LeaveCriticalSection(g_DebugCritSec);

pClient_Cert := IdSslGetPeerCertificate(sslServer);
      if (pClient_Cert <> nil) then
      begin
        EnterCriticalSection(g_DebugCritSec);
        Form1.Memo1.Lines.Add('客户端证书:');
        LeaveCriticalSection(g_DebugCritSec);
        pStr := IdSslX509NameOneline(IdSslX509GetSubjectName(pClient_Cert), nil, 0);
        if pStr = nil then Exit;
        EnterCriticalSection(g_DebugCritSec);
        Form1.Memo1.Lines.Add(Format('主题: %s', [pStr]));
        LeaveCriticalSection(g_DebugCritSec);
        IdSslFree(pStr);
        pStr := IdSslX509NameOneline(IdSslX509GetIssuerName(pClient_Cert), nil, 0);
        if pStr = nil then Exit;
        IdSslFree(pStr);
        EnterCriticalSection(g_DebugCritSec);
        Form1.Memo1.Lines.Add(Format('发行者: %s', [pStr]));
        LeaveCriticalSection(g_DebugCritSec);
        IdSslFree(pStr);
        IdSslFree(pClient_Cert);
      end
      else
      begin
        EnterCriticalSection(g_DebugCritSec);
        Form1.Memo1.Lines.Add('客户端没有证书.');
        LeaveCriticalSection(g_DebugCritSec);
      end;

FillChar(buf, sizeof(buf), #0);
      nRet := IdSslRead(sslServer, buf, sizeof(buf));
      if nRet <= 0 then
      begin
        closesocket(sdClient);
        sdClient := INVALID_SOCKET;
        IdSslShutdown(sslServer);
        IdSslFree(sslServer);
        sslServer := nil;
      end
      else
      begin
        EnterCriticalSection(g_DebugCritSec);
        Form1.Memo1.Lines.Add('客户端发送请求:'#$D#$A + StrPas(buf));
        LeaveCriticalSection(g_DebugCritSec);
      end;

strBody := 'Your IP is:' + inet_ntoa(sAddrs.sin_addr);

strSend := 'HTTP/1.1 200 OK'#$D#$A +
        'Content-Length: ' + IntToStr(Length(strBody)) + #$D#$A +
        'Content-Type: text/html'#$D#$A#$D#$A + strBody;

IdSslWrite(sslServer, @strSend[1], Length(strSend));

closesocket(sdClient);
      IdSslShutdown(sslServer);
      IdSslFree(sslServer);
    end;
  end;
  g_hAcceptThread := 0;
end;

procedure TForm1.Button1Click(Sender: TObject);
var
  sAddr: TSockAddr;
  dwThreadID: DWORD;
begin
  Button1.Enabled := False;

//Create a socket
  g_ServerSocketMain := Socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
  if g_ServerSocketMain = INVALID_SOCKET then
  begin
    MessageBox(0, '创建Socket失败!', '错误', MB_ICONEXCLAMATION + MB_TOPMOST);
    Application.Terminate;
    Exit;
  end;

//Bind Port
  FillChar(sAddr, Sizeof(TSockAddr), #0);
  sAddr.sin_family := AF_INET;
  sAddr.sin_port := htons(StrToIntDef(Trim(Edit1.Text), 443));
  sAddr.sin_addr.S_addr := INADDR_ANY;

if Bind(g_ServerSocketMain, sAddr, Sizeof(TSockAddr)) = SOCKET_ERROR then
  begin
    MessageBox(0, '绑定端口失败!', '错误', MB_ICONEXCLAMATION + MB_TOPMOST);
    Application.Terminate;
    Exit;
  end;

//listen
  if listen(g_ServerSocketMain, SOMAXCONN) = SOCKET_ERROR then
  begin
    MessageBox(0, '监听端口失败!', '错误', MB_ICONEXCLAMATION + MB_TOPMOST);
    Application.Terminate;
    Exit;
  end;
  g_Start := TRUE;
  g_hAcceptThread := CreateThread(nil, 0, @AcceptThread, nil, 0, dwThreadId);
end;

procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction);
begin
  g_Start := FALSE;
  if g_ServerSocketMain <> INVALID_SOCKET then
  begin
    closesocket(g_ServerSocketMain);
    g_ServerSocketMain := INVALID_SOCKET;
  end;

if (g_hAcceptThread <> 0) then
  begin
    WaitForSingleObject(g_hAcceptThread, INFINITE);
    CloseHandle(g_hAcceptThread);
    g_hAcceptThread := 0;
  end;
end;

initialization

if WSAStartup($101, g_Wsa) <> 0 then //初始化Wsock32.dll,2.2版本可以使用MakeWord(2,2),
  begin
    MessageBox(0, '初始化Winsock动态库失败!', '错误', MB_ICONEXCLAMATION + MB_TOPMOST);
    Halt;
  end;

if not Load then //装载ssl库失败
  begin
    MessageBox(0, '装载ssl动态库失败!', '错误', MB_ICONEXCLAMATION + MB_TOPMOST);
    Halt;
  end;
  IdSslLoadErrorStrings;
  IdSslAddSslAlgorithms; //load所有的SSL算法.

//==========================  初始化SSL Server  ================================
  g_methServer := IdSslMethodV23; //建立SSL所用的method.
  if g_methServer = nil then
  begin
    MessageBox(0, '建立SSL Server所用的method失败!', '错误', MB_ICONEXCLAMATION + MB_TOPMOST);
    Halt;
  end;
  g_ctxServer := IdSslCtxNew(g_methServer); //初始化上下文情景.
  if g_ctxServer = nil then //创建SSL_CTX失败
  begin
    MessageBox(0, '创建服务端SSL_CTX失败!', '错误', MB_ICONEXCLAMATION + MB_TOPMOST);
    Halt;
  end;

if IdSslCtxUseCertificateFile(g_ctxServer, Pchar(ExtractFilePath(GetModuleName(HInstance)) + 'UserCert.pem'), OPENSSL_SSL_FILETYPE_PEM) <= 0 then //加载证书失败
  begin
    MessageBox(0, '加载证书失败!', '错误', MB_ICONEXCLAMATION + MB_TOPMOST);
    Halt;
  end;
  if IdSslCtxUsePrivateKeyFile(g_ctxServer, Pchar(ExtractFilePath(GetModuleName(HInstance)) + 'UserKey.pem'), OPENSSL_SSL_FILETYPE_PEM) <= 0 then //加载私钥失败
  begin
    MessageBox(0, '加载私钥失败!', '错误', MB_ICONEXCLAMATION + MB_TOPMOST);
    Halt;
  end;
  if not IdSslCtxCheckPrivateKeyFile(g_ctxServer) = 0 then //密钥证书不匹配
  begin
    MessageBox(0, '密钥和证书不匹配!', '错误', MB_ICONEXCLAMATION + MB_TOPMOST);
    Halt;
  end;
  InitializeCriticalSection(g_DebugCritSec);
finalization
  if g_ctxServer <> nil then IdSslCtxFree(g_ctxServer);
  DeleteCriticalSection(g_DebugCritSec);
  WSACleanup; //结束对WSocket32.dll调用
end.

完整代码下载地址:http://www.138soft.com/download/ssldemo_server.rar

实际上,稍微用心的人结合第一讲的客户端例子,已经可以写出中间人欺骗程序了.

完整代码下载地址:http://www.138soft.com/download/ssldemo_client.rar

老陈---谈Delphi中SSL协议的应用相关推荐

  1. SSH协议、HTTPS中SSL协议的完整交互过程

    1.(SSH)公私钥认证原理 服务器建立公钥:每一次启动sshd服务时,该服务会主动去找/etc/ssh/ssh_host*的文件 客户端通过ssh工具进行连接,如Xshell,SecureCRT 服 ...

  2. 开发微信小程序中SSL协议的申请、证书绑定、TLS 版本处理等

    在上篇随笔<基于微信小程序的系统开发准备工作>介绍了开发微信小程序的一些前期的架构设计.技术路线 .工具准备等方面内容,本篇随笔继续这个步骤,逐步介绍我们实际开发过程中对SSL协议的申请及 ...

  3. [老码团队]Swift中的协议 - 用协议来实现委托模式

    委托是一种设计模式,它允许类或结构体将一些需要它们负责的功能交由(委托)给其他的类型的实例.在这种设计模式中,会涉及到几个角色: 公共接口(Interface):负责封装起需要被委托的功能 代理者(D ...

  4. HTTPS中SSL协议总结

    SSL全称Secure Socket Layer,安全套接字层.SSL用来保证Web浏览器与Web服务器交换信息的安全性,保证传输过程中消息的加密性. 在SSL中使用到的技术: 散列算法验证数据完整性 ...

  5. delphi中利用Indy的TIdFtp控件实现FTP协议

    2019独角兽企业重金招聘Python工程师标准>>> delphi中利用Indy的TIdFtp控件实现FTP协议版权声明:本文为博主原创文章,未经博主允许不得转载.现在很多应用都需 ...

  6. 浅谈 HTTPS 和 SSL/TLS 协议的背景与基础

    来自:编程随想   >> 相关背景知识 要说清楚 HTTPS 协议的实现原理,至少需要如下几个背景知识. 大致了解几个基本术语(HTTPS.SSL.TLS)的含义 大致了解 HTTP 和 ...

  7. SSL协议安全系列:PKI体系中的证书吊销

    GoSSIP_SJTU · 2016/03/03 10:06 0x00 前言 在前面的章节我们讨论了部分SSL/TLS握手协议.记录协议中存在的安全问题,针对它们的攻击以及相应的加固方案.在SSL/T ...

  8. 在Delphi中关于UDP协议的实现

    首先我把UDP无连接协议的套接字调用时序图表示出来 在我把在Delphi中使用UDP协议实现数据通讯收发的实现方法总结如下: 例子描述:下面例子是我的一个实际设备通讯的例子,使用UDP协议在4660端 ...

  9. python 迭代器协议_浅谈Python中的生成器和迭代器

    迭代器 迭代器协议 对象必须提供一个next方法,执行该方法要么返回迭代中的下一项,要么返回一个异常来终止本次迭代.(只能往前走,不能往后退!) 迭代器对象 遵循了(实现了)迭代器协议的对象.(对象内 ...

最新文章

  1. 关于程序员的那些事——一个五年程序员的总结
  2. 关于xib里面的NSLayoutConstraint的multiplier修改
  3. java socket 字节读取_Java SocketChannel 读取ByteBuffer字节的处理模型
  4. 一个1990年代的小故事
  5. python解析mht文件_php解析mht文件转换成html的方法
  6. 关于Win7 内存变小处理方法
  7. sybase性能优化经验浅谈
  8. 智能电视系列(4)-高通,天才与极限
  9. Java多线程:生命周期,实现与调度
  10. 力扣78. 子集(JavaScript)
  11. python获取app信息的库_Python学习教程:另辟蹊径,appium抓取app应用数据了解一下...
  12. 分割字符串函数strtok
  13. [JNI]开发之旅(5)访问c/c++函数
  14. 史上最全的ocr文字识别体验,让你一次用个够
  15. 无法直接在IE中打开PDF文件的解决方法
  16. 电子科技大学格拉斯哥学院基础实践——共享单车的调查
  17. 报名系统网页导出html,【网页报名表如何导出pdf】_网页的报名表怎样转换为word或者PDF格式...
  18. 验证码的作用,为什么要存在验证码
  19. 一张图理解非对称加密解密过程
  20. #【精华】 PB根据一串字符串生成拼音码

热门文章

  1. 二进制转化为十进制数字的代码
  2. 基于SpringBoot的小说网站
  3. L017-linux系统定时任务crond入门小节
  4. 虚拟机RedHatEenterpriseLinux5安装及Oracle10.2.0安装手记
  5. 编程中「缺省」是什么意思?
  6. Cyclone V SoC FPGA学习之路第二章:硬件篇
  7. 一个平庸的人的自我反思
  8. 【Tanzu 社区版=TCE】 Mac 笔记本快速部署安装体验-(二)
  9. vue js 进来页面自动播放背景音乐
  10. 弹窗Modal实现和有滚动条偏移解决方法。