整合營銷服務(wù)商

          電腦端+手機(jī)端+微信端=數(shù)據(jù)同步管理

          免費(fèi)咨詢熱線:

          文件上傳總結(jié)

          文件上傳總結(jié)

          戶端

          js檢查

          一般都是在網(wǎng)頁上寫一段javascript腳本,校驗上傳文件的后綴名,有白名單形式也有黑名單形式。

          查看源代碼可以看到有如下代碼對上傳文件類型進(jìn)行了限制:

          <script type="text/javascript"> function checkFile() {
                  var file=document.getElementsByName('upload_file')[0].value;
                  if (file==null || file=="") {
                      alert("請選擇要上傳的文件!");
                      return false;
                  }
                  //定義允許上傳的文件類型
                  var allow_ext=".jpg|.png|.gif";
                  //提取上傳文件的類型
                  var ext_name=file.substring(file.lastIndexOf("."));
                  //判斷上傳文件類型是否允許上傳
                  if (allow_ext.indexOf(ext_name)==-1) {
                      var errMsg="該文件不允許上傳,請上傳" + allow_ext + "類型的文件,當(dāng)前文件類型為:" + ext_name;
                      alert(errMsg);
                      return false;
                  }
              } </script>

          我們可以看到對上傳文件類型進(jìn)行了限制。

          繞過方法

          1. 我們直接刪除代碼中onsubmit事件中關(guān)于文件上傳時驗證上傳文件的相關(guān)代碼即可。

          或者可以不加載所有js,還可以將html源碼copy一份到本地,然后對相應(yīng)代碼進(jìn)行修改,本地提交即可。

          1. burp改包,由于是js驗證,我們可以先將文件重命名為js允許的后綴名,在用burp發(fā)送數(shù)據(jù)包時候改成我們想要的后綴。

          即可上傳成功:

          服務(wù)端

          黑名單

          特殊可解析后綴

              if (file_exists(UPLOAD_PATH)) {
                  $deny_ext=array('.asp','.aspx','.php','.jsp');
                  $file_name=trim($_FILES['upload_file']['name']);
                  $file_name=deldot($file_name);//刪除文件名末尾的點(diǎn)
                  $file_ext=strrchr($file_name, '.');
                  $file_ext=strtolower($file_ext); //轉(zhuǎn)換為小寫
                  $file_ext=str_ireplace('::$DATA', '', $file_ext);//去除字符串::$DATA
                  $file_ext=trim($file_ext); //收尾去空
          
                  if(!in_array($file_ext, $deny_ext)) {
                      $temp_file=$_FILES['upload_file']['tmp_name'];
                      $img_path=UPLOAD_PATH.'/'.date("YmdHis").rand(1000,9999).$file_ext;            
                      if (move_uploaded_file($temp_file,$img_path)) {
                           $is_upload=true;
                      } else {
                          $msg='上傳出錯!';
                      }
                  } else {
                      $msg='不允許上傳.asp,.aspx,.php,.jsp后綴文件!';
                  }
              } else {
                  $msg=UPLOAD_PATH . '文件夾不存在,請手工創(chuàng)建!';
              }

          這里做了黑名單處理,我們可以通過特殊可解析后綴進(jìn)行繞過。

          繞過方法

          之前在https://www.jianshu.com/p/1ccbab572974中總結(jié)過,這里不再贅述,可以使用php3,phtml等繞過。

          上傳.htaccess

          if (isset($_POST['submit'])) {
              if (file_exists(UPLOAD_PATH)) {
                  $deny_ext=array(".php",".php5",".php4",".php3",".php2","php1",".html",".htm",".phtml",".pht",".pHp",".pHp5",".pHp4",".pHp3",".pHp2","pHp1",".Html",".Htm",".pHtml",".jsp",".jspa",".jspx",".jsw",".jsv",".jspf",".jtml",".jSp",".jSpx",".jSpa",".jSw",".jSv",".jSpf",".jHtml",".asp",".aspx",".asa",".asax",".ascx",".ashx",".asmx",".cer",".aSp",".aSpx",".aSa",".aSax",".aScx",".aShx",".aSmx",".cEr",".sWf",".swf");
                  $file_name=trim($_FILES['upload_file']['name']);
                  $file_name=deldot($file_name);//刪除文件名末尾的點(diǎn)
                  $file_ext=strrchr($file_name, '.');
                  $file_ext=strtolower($file_ext); //轉(zhuǎn)換為小寫
                  $file_ext=str_ireplace('::$DATA', '', $file_ext);//去除字符串::$DATA
                  $file_ext=trim($file_ext); //收尾去空
          
                  if (!in_array($file_ext, $deny_ext)) {
                      $temp_file=$_FILES['upload_file']['tmp_name'];
                      $img_path=UPLOAD_PATH.'/'.$file_name;
                      if (move_uploaded_file($temp_file, $img_path)) {
                          $is_upload=true;
                      } else {
                          $msg='上傳出錯!';
                      }
                  } else {
                      $msg='此文件不允許上傳!';
                  }
              } else {
                  $msg=UPLOAD_PATH . '文件夾不存在,請手工創(chuàng)建!';
              }
          }
          ?>

          我們發(fā)現(xiàn)黑名單限制了很多后綴名,但是沒有限制.htaccess

          .htaccess文件是Apache服務(wù)器中的一個配置文件,它負(fù)責(zé)相關(guān)目錄下的網(wǎng)頁配置.通過htaccess文件,可以實現(xiàn):網(wǎng)頁301重定向、自定義404頁面、改變文件擴(kuò)展名、允許/阻止特定的用戶或者目錄的訪問、禁止目錄列表、配置默認(rèn)文檔等功能。

          繞過方法

          我們需要上傳一個.htaccess文件,內(nèi)容為:

          SetHandler application/x-httpd-php
          

          這樣所有的文件都會解析為php,接下來上傳圖片馬即可

          后綴大小寫繞過

          if (isset($_POST['submit'])) {
              if (file_exists(UPLOAD_PATH)) {
                  $deny_ext=array(".php",".php5",".php4",".php3",".php2",".html",".htm",".phtml",".pht",".pHp",".pHp5",".pHp4",".pHp3",".pHp2",".Html",".Htm",".pHtml",".jsp",".jspa",".jspx",".jsw",".jsv",".jspf",".jtml",".jSp",".jSpx",".jSpa",".jSw",".jSv",".jSpf",".jHtml",".asp",".aspx",".asa",".asax",".ascx",".ashx",".asmx",".cer",".aSp",".aSpx",".aSa",".aSax",".aScx",".aShx",".aSmx",".cEr",".sWf",".swf",".htaccess");
                  $file_name=trim($_FILES['upload_file']['name']);
                  $file_name=deldot($file_name);//刪除文件名末尾的點(diǎn)
                  $file_ext=strrchr($file_name, '.');
                  $file_ext=str_ireplace('::$DATA', '', $file_ext);//去除字符串::$DATA
                  $file_ext=trim($file_ext); //首尾去空
          
                  if (!in_array($file_ext, $deny_ext)) {
                      $temp_file=$_FILES['upload_file']['tmp_name'];
                      $img_path=UPLOAD_PATH.'/'.date("YmdHis").rand(1000,9999).$file_ext;
                      if (move_uploaded_file($temp_file, $img_path)) {
                          $is_upload=true;
                      } else {
                          $msg='上傳出錯!';
                      }
                  } else {
                      $msg='此文件類型不允許上傳!';
                  }
              } else {
                  $msg=UPLOAD_PATH . '文件夾不存在,請手工創(chuàng)建!';
              }
          }

          我們發(fā)現(xiàn)對.htaccess也進(jìn)行了檢測,但是沒有對大小寫進(jìn)行統(tǒng)一。

          繞過方法

          后綴名改為PHP即可

          空格繞過

          if (isset($_POST['submit'])) {
              if (file_exists(UPLOAD_PATH)) {
                  $deny_ext=array(".php",".php5",".php4",".php3",".php2",".html",".htm",".phtml",".pht",".pHp",".pHp5",".pHp4",".pHp3",".pHp2",".Html",".Htm",".pHtml",".jsp",".jspa",".jspx",".jsw",".jsv",".jspf",".jtml",".jSp",".jSpx",".jSpa",".jSw",".jSv",".jSpf",".jHtml",".asp",".aspx",".asa",".asax",".ascx",".ashx",".asmx",".cer",".aSp",".aSpx",".aSa",".aSax",".aScx",".aShx",".aSmx",".cEr",".sWf",".swf",".htaccess");
                  $file_name=$_FILES['upload_file']['name'];
                  $file_name=deldot($file_name);//刪除文件名末尾的點(diǎn)
                  $file_ext=strrchr($file_name, '.');
                  $file_ext=strtolower($file_ext); //轉(zhuǎn)換為小寫
                  $file_ext=str_ireplace('::$DATA', '', $file_ext);//去除字符串::$DATA
                  
                  if (!in_array($file_ext, $deny_ext)) {
                      $temp_file=$_FILES['upload_file']['tmp_name'];
                      $img_path=UPLOAD_PATH.'/'.date("YmdHis").rand(1000,9999).$file_ext;
                      if (move_uploaded_file($temp_file,$img_path)) {
                          $is_upload=true;
                      } else {
                          $msg='上傳出錯!';
                      }
                  } else {
                      $msg='此文件不允許上傳';
                  }
              } else {
                  $msg=UPLOAD_PATH . '文件夾不存在,請手工創(chuàng)建!';
              }

          黑名單沒有對文件中的空格進(jìn)行處理,可在后綴名中加空格繞過。

          繞過方法

          點(diǎn)繞過

          if (isset($_POST['submit'])) {
              if (file_exists(UPLOAD_PATH)) {
                  $deny_ext=array(".php",".php5",".php4",".php3",".php2",".html",".htm",".phtml",".pht",".pHp",".pHp5",".pHp4",".pHp3",".pHp2",".Html",".Htm",".pHtml",".jsp",".jspa",".jspx",".jsw",".jsv",".jspf",".jtml",".jSp",".jSpx",".jSpa",".jSw",".jSv",".jSpf",".jHtml",".asp",".aspx",".asa",".asax",".ascx",".ashx",".asmx",".cer",".aSp",".aSpx",".aSa",".aSax",".aScx",".aShx",".aSmx",".cEr",".sWf",".swf",".htaccess");
                  $file_name=trim($_FILES['upload_file']['name']);
                  $file_ext=strrchr($file_name, '.');
                  $file_ext=strtolower($file_ext); //轉(zhuǎn)換為小寫
                  $file_ext=str_ireplace('::$DATA', '', $file_ext);//去除字符串::$DATA
                  $file_ext=trim($file_ext); //首尾去空
                  
                  if (!in_array($file_ext, $deny_ext)) {
                      $temp_file=$_FILES['upload_file']['tmp_name'];
                      $img_path=UPLOAD_PATH.'/'.$file_name;
                      if (move_uploaded_file($temp_file, $img_path)) {
                          $is_upload=true;
                      } else {
                          $msg='上傳出錯!';
                      }
                  } else {
                      $msg='此文件類型不允許上傳!';
                  }
              } else {
                  $msg=UPLOAD_PATH . '文件夾不存在,請手工創(chuàng)建!';
              }

          windows會對文件中的點(diǎn)進(jìn)行自動去除,所以可以在文件末尾加點(diǎn)繞過,不再贅述

          ::$DATA繞過

          同windows特性,可在后綴名中加” ::$DATA”繞過,不再贅述

          路徑拼接繞過

          if (isset($_POST['submit'])) {
              if (file_exists(UPLOAD_PATH)) {
                  $deny_ext=array(".php",".php5",".php4",".php3",".php2",".html",".htm",".phtml",".pht",".pHp",".pHp5",".pHp4",".pHp3",".pHp2",".Html",".Htm",".pHtml",".jsp",".jspa",".jspx",".jsw",".jsv",".jspf",".jtml",".jSp",".jSpx",".jSpa",".jSw",".jSv",".jSpf",".jHtml",".asp",".aspx",".asa",".asax",".ascx",".ashx",".asmx",".cer",".aSp",".aSpx",".aSa",".aSax",".aScx",".aShx",".aSmx",".cEr",".sWf",".swf",".htaccess");
                  $file_name=trim($_FILES['upload_file']['name']);
                  $file_name=deldot($file_name);//刪除文件名末尾的點(diǎn)
                  $file_ext=strrchr($file_name, '.');
                  $file_ext=strtolower($file_ext); //轉(zhuǎn)換為小寫
                  $file_ext=str_ireplace('::$DATA', '', $file_ext);//去除字符串::$DATA
                  $file_ext=trim($file_ext); //首尾去空
                  
                  if (!in_array($file_ext, $deny_ext)) {
                      $temp_file=$_FILES['upload_file']['tmp_name'];
                      $img_path=UPLOAD_PATH.'/'.$file_name;
                      if (move_uploaded_file($temp_file, $img_path)) {
                          $is_upload=true;
                      } else {
                          $msg='上傳出錯!';
                      }
                  } else {
                      $msg='此文件類型不允許上傳!';
                  }
              } else {
                  $msg=UPLOAD_PATH . '文件夾不存在,請手工創(chuàng)建!';
              }
          }

          這里對文件名進(jìn)行了處理,刪除了文件名末尾的點(diǎn),并且把處理過的文件名拼接到路徑中。

          繞過方法

          這里我們可以構(gòu)造文件名1.PHP. . (點(diǎn)+空格+點(diǎn)),經(jīng)過處理后,文件名變成1.PHP.,即可繞過。

          雙寫繞過

              if (file_exists(UPLOAD_PATH)) {
                  $deny_ext=array("php","php5","php4","php3","php2","html","htm","phtml","pht","jsp","jspa","jspx","jsw","jsv","jspf","jtml","asp","aspx","asa","asax","ascx","ashx","asmx","cer","swf","htaccess");
          
                  $file_name=trim($_FILES['upload_file']['name']);
                  $file_name=str_ireplace($deny_ext,"", $file_name);
                  $temp_file=$_FILES['upload_file']['tmp_name'];
                  $img_path=UPLOAD_PATH.'/'.$file_name;        
                  if (move_uploaded_file($temp_file, $img_path)) {
                      $is_upload=true;
                  } else {
                      $msg='上傳出錯!';
                  }
              } else {
                  $msg=UPLOAD_PATH . '文件夾不存在,請手工創(chuàng)建!';
              }
          }

          繞過方法

          這里我們可以看到將文件名替換為空,我們可以采用雙寫繞過:1.pphphp

          白名單

          MIME檢查

          if (isset($_POST['submit'])) {
              if (file_exists(UPLOAD_PATH)) {
                  if (($_FILES['upload_file']['type']=='image/jpeg') || ($_FILES['upload_file']['type']=='image/png') || ($_FILES['upload_file']['type']=='image/gif')) {
                      $temp_file=$_FILES['upload_file']['tmp_name'];
                      $img_path=UPLOAD_PATH . '/' . $_FILES['upload_file']['name'];          
                      if (move_uploaded_file($temp_file, $img_path)) {
                          $is_upload=true;
                      } else {
                          $msg='上傳出錯!';
                      }
                  } else {
                      $msg='文件類型不正確,請重新上傳!';
                  }
              } else {
                  $msg=UPLOAD_PATH.'文件夾不存在,請手工創(chuàng)建!';
              }

          繞過方法

          這里檢查Content-type,我們burp抓包修改即可繞過:

          %00 截斷

          if(isset($_POST['submit'])){
              $ext_arr=array('jpg','png','gif');
              $file_ext=substr($_FILES['upload_file']['name'],strrpos($_FILES['upload_file']['name'],".")+1);
              if(in_array($file_ext,$ext_arr)){
                  $temp_file=$_FILES['upload_file']['tmp_name'];
                  $img_path=$_GET['save_path']."/".rand(10, 99).date("YmdHis").".".$file_ext;
          
                  if(move_uploaded_file($temp_file,$img_path)){
                      $is_upload=true;
                  } else {
                      $msg='上傳出錯!';
                  }
              } else{
                  $msg="只允許上傳.jpg|.png|.gif類型文件!";
              }
          }

          $img_path直接拼接,因此可以利用%00截斷繞過

          繞過方法

          然后直接訪問/upload/1.php即可


          00截斷(post)

          if(isset($_POST['submit'])){
              $ext_arr=array('jpg','png','gif');
              $file_ext=substr($_FILES['upload_file']['name'],strrpos($_FILES['upload_file']['name'],".")+1);
              if(in_array($file_ext,$ext_arr)){
                  $temp_file=$_FILES['upload_file']['tmp_name'];
                  $img_path=$_POST['save_path']."/".rand(10, 99).date("YmdHis").".".$file_ext;
          
                  if(move_uploaded_file($temp_file,$img_path)){
                      $is_upload=true;
                  } else {
                      $msg="上傳失敗";
                  }
              } else {
                  $msg="只允許上傳.jpg|.png|.gif類型文件!";
              }
          }
          ?>

          save_path是通過post傳進(jìn)來的,還是利用00截斷,但這次需要在二進(jìn)制中進(jìn)行修改,因為post不會像get對%00進(jìn)行自動解碼。

          繞過方法



          接下來訪問1.php即可

          文件內(nèi)容檢查

          文件幻數(shù)檢測

          主要是檢測文件內(nèi)容開始處的文件幻數(shù),比如圖片類型的文件幻數(shù)如下,
          要繞過jpg 文件幻數(shù)檢測就要在文件開頭寫上下圖的值:


          Value=FF D8 FF E0 00 10 4A 46 49 46

          要繞過gif 文件幻數(shù)檢測就要在文件開頭寫上下圖的值



          Value=47 49 46 38 39 61
          要繞過png 文件幻數(shù)檢測就要在文件開頭寫上下面的值



          Value=89 50 4E 47

          然后在文件幻數(shù)后面加上自己的一句話木馬代碼就行了

          文件相關(guān)信息檢測

          圖像文件相關(guān)信息檢測常用的就是getimagesize()函數(shù)

          只需要把文件頭部分偽造好就ok 了,就是在幻數(shù)的基礎(chǔ)上還加了一些文件信息

          有點(diǎn)像下面的結(jié)構(gòu)

          GIF89a
          (...some binary data for image...)
          <?php phpinfo(); ?>
          (... skipping the rest of binary data ...)

          本次環(huán)境中的文件頭檢測,getimagesize,php_exif都可以用圖片馬繞過:

          copy normal.jpg /b + shell.php /a webshell.jpg
          

          文件加載檢測

          一般是調(diào)用API 或函數(shù)去進(jìn)行文件加載測試,常見的是圖像渲染測試,甚至是進(jìn)行二次渲染(過濾效果幾乎最強(qiáng))。對渲染/加載測試的攻擊方式是代碼注入繞過,對二次渲染的攻擊方式是攻擊文件加載器自身。

          對渲染/加載測試攻擊- 代碼注入繞過

          可以用圖像處理軟件對一張圖片進(jìn)行代碼注入

          用winhex 看數(shù)據(jù)可以分析出這類工具的原理是

          在不破壞文件本身的渲染情況下找一個空白區(qū)進(jìn)行填充代碼,一般會是圖片的注釋區(qū)

          對于渲染測試基本上都能繞過,畢竟本身的文件結(jié)構(gòu)是完整的

          二次渲染

          imagecreatefromjpeg二次渲染它相當(dāng)于是把原本屬于圖像數(shù)據(jù)的部分抓了出來,再用自己的API 或函數(shù)進(jìn)行重新渲染在這個過程中非圖像數(shù)據(jù)的部分直接就隔離開了

          if (isset($_POST['submit'])){
              // 獲得上傳文件的基本信息,文件名,類型,大小,臨時文件路徑
              $filename=$_FILES['upload_file']['name'];
              $filetype=$_FILES['upload_file']['type'];
              $tmpname=$_FILES['upload_file']['tmp_name'];
          
              $target_path=UPLOAD_PATH.basename($filename);
          
              // 獲得上傳文件的擴(kuò)展名
              $fileext=substr(strrchr($filename,"."),1);
          
              //判斷文件后綴與類型,合法才進(jìn)行上傳操作
              if(($fileext=="jpg") && ($filetype=="image/jpeg")){
                  if(move_uploaded_file($tmpname,$target_path))
                  {
                      //使用上傳的圖片生成新的圖片
                      $im=imagecreatefromjpeg($target_path);
          
                      if($im==false){
                          $msg="該文件不是jpg格式的圖片!";
                          @unlink($target_path);
                      }else{
                          //給新圖片指定文件名
                          srand(time());
                          $newfilename=strval(rand()).".jpg";
                          $newimagepath=UPLOAD_PATH.$newfilename;
                          imagejpeg($im,$newimagepath);
                          //顯示二次渲染后的圖片(使用用戶上傳圖片生成的新圖片)
                          $img_path=UPLOAD_PATH.$newfilename;
                          @unlink($target_path);
                          $is_upload=true;
                      }
                  } else {
                      $msg="上傳出錯!";
                  }
          
              }else if(($fileext=="png") && ($filetype=="image/png")){
                  if(move_uploaded_file($tmpname,$target_path))
                  {
                      //使用上傳的圖片生成新的圖片
                      $im=imagecreatefrompng($target_path);
          
                      if($im==false){
                          $msg="該文件不是png格式的圖片!";
                          @unlink($target_path);
                      }else{
                           //給新圖片指定文件名
                          srand(time());
                          $newfilename=strval(rand()).".png";
                          $newimagepath=UPLOAD_PATH.$newfilename;
                          imagepng($im,$newimagepath);
                          //顯示二次渲染后的圖片(使用用戶上傳圖片生成的新圖片)
                          $img_path=UPLOAD_PATH.$newfilename;
                          @unlink($target_path);
                          $is_upload=true;               
                      }
                  } else {
                      $msg="上傳出錯!";
                  }
          
              }else if(($fileext=="gif") && ($filetype=="image/gif")){
                  if(move_uploaded_file($tmpname,$target_path))
                  {
                      //使用上傳的圖片生成新的圖片
                      $im=imagecreatefromgif($target_path);
                      if($im==false){
                          $msg="該文件不是gif格式的圖片!";
                          @unlink($target_path);
                      }else{
                          //給新圖片指定文件名
                          srand(time());
                          $newfilename=strval(rand()).".gif";
                          $newimagepath=UPLOAD_PATH.$newfilename;
                          imagegif($im,$newimagepath);
                          //顯示二次渲染后的圖片(使用用戶上傳圖片生成的新圖片)
                          $img_path=UPLOAD_PATH.$newfilename;
                          @unlink($target_path);
                          $is_upload=true;
                      }
                  } else {
                      $msg="上傳出錯!";
                  }
              }else{
                  $msg="只允許上傳后綴為.jpg|.png|.gif的圖片文件!";
              }
          }

          本關(guān)綜合判斷了后綴名、content-type,以及利用imagecreatefromgif判斷是否為gif圖片,最后再做了一次二次渲染。

          繞過方法

          得去找圖片經(jīng)過GD庫轉(zhuǎn)化后沒有改變的部分,再將未改變的部分修改為相應(yīng)的php代碼。

          條件競爭

          if(isset($_POST['submit'])){
              $ext_arr=array('jpg','png','gif');
              $file_name=$_FILES['upload_file']['name'];
              $temp_file=$_FILES['upload_file']['tmp_name'];
              $file_ext=substr($file_name,strrpos($file_name,".")+1);
              $upload_file=UPLOAD_PATH . '/' . $file_name;
          
              if(move_uploaded_file($temp_file, $upload_file)){
                  if(in_array($file_ext,$ext_arr)){
                       $img_path=UPLOAD_PATH . '/'. rand(10, 99).date("YmdHis").".".$file_ext;
                       rename($upload_file, $img_path);
                       $is_upload=true;
                  }else{
                      $msg="只允許上傳.jpg|.png|.gif類型文件!";
                      unlink($upload_file);
                  }
              }else{
                  $msg='上傳出錯!';
              }
          }

          這里先將文件上傳到服務(wù)器,然后通過rename修改名稱,再通過unlink刪除文件,因此可以通過條件競爭的方式在unlink之前,訪問webshell。

          繞過方法


          然后不斷訪問webshell:

          上傳成功。

          參考鏈接:

          1. https://blog.csdn.net/Kevinhanser/article/details/81613003
          2. https://secgeek.net/bookfresh-vulnerability/
          3. https://xz.aliyun.com/t/2435

          2人點(diǎn)贊

          知識歸納

          introduction

          Swift 是一種適用于iOS/macOS應(yīng)用開發(fā)、服務(wù)器端的編程語言。自2014年蘋果發(fā)布 Swift 語言以來,Swift5 實現(xiàn)了 ABI 穩(wěn)定性、Module 穩(wěn)定性和Library Evolution,與Objective-C(下文簡稱“OC”)相比,Swift 在開發(fā)效率、安全、編譯優(yōu)化、運(yùn)行性能和內(nèi)存管理方面具有顯著優(yōu)勢。(官方博客:https://www.swift.org/about/)

          百度App 已在工程和環(huán)境上支持 Swift 開發(fā),百度搜索大前端團(tuán)隊負(fù)責(zé)搜索服務(wù)的穩(wěn)定落地,我們積極探索 Swift的應(yīng)用,希望能大幅提升開發(fā)效率和靈活性、提升端用戶的搜索體驗。然而,在實施過程中可能會遇到各種問題,例如代碼陳舊且不支持Swift,人員對Swift掌握不夠熟練、意識不足,協(xié)作方對Swift的支持不足等。

          對于其他語言來說,Swift相對年輕,我們在實踐過程中整理一些常見問題及其解決方法,希望能幫助讀者更順利地使用Swift進(jìn)行編程,提高研發(fā)效率。


          全文6947字,預(yù)計閱讀時間18分鐘。

          GEEK TALK

          01

          Swift 適用場景

          在決定是否引入Swift前,我們需要判斷場景是否適合。通常情況下,可以用OC的場景均適合使用Swift,但也有一些不太適合直接替換的場景,需要慎重,比如:

          1、涉及OC動態(tài)性,頻繁在runtime時操作屬性和方法;

          2、核心基礎(chǔ)功能,出現(xiàn)問題影響面較大的邏輯;

          3、調(diào)用C++(目前Swift不能直接調(diào)用C++);

          4、繼承不支持Swift組件的類。

          此外,對使用OC比較久遠(yuǎn)的工程,使用Swift前也應(yīng)注意:

          1、能在工程環(huán)境和單獨(dú)模塊上支持Swift;

          2、模塊較多的工程,可以內(nèi)外OC和Swift混編;

          3、為了避免Swift Waring帶來的潛在問題,可以把SWIFT_TREAT_WARNINGS_AS_ERRORS設(shè)置為YES,這樣警告會作為錯誤,輔助程序員更好的規(guī)范代碼;

          4、模塊Module化后,要注意維護(hù) umbrella header 中的公開頭文件。

          △注:本文中的“組件”均指代工程中的“Target”。


          GEEK TALK

          02

          Swift的基本用法

          2.1 Swift 的字符串為什么這么難用?

          如:字符串不能通過索引取字符

          • 原因:Swift認(rèn)為字符串是由一個個字形群集 (grapheme clusters)組成的,字形群集的大小不固定所以不能用整數(shù)去索引 (字形群集其實就是Swift中的Character(字符)類)。
          • 解決方案:如要通過下標(biāo)取字符可以為String添加擴(kuò)展在下標(biāo)subscript實現(xiàn)通過傳入Int索引,在subscript轉(zhuǎn)為String.index獲取對應(yīng)字符的方式。

          2.2 try try? try! 的區(qū)別

          當(dāng)你進(jìn)行文件操作時,可能會遇到需要使用try、try?和try!的情況。它們在異常處理方面有所不同。

          1、使用try時,如果出現(xiàn)異常,程序會進(jìn)入異常處理流程,你可以在catch語句塊中處理這個異常。

          2、使用try?時,如果發(fā)生異常,它不會進(jìn)入異常處理流程,而是返回一個可選值類型。也就是說,如果出現(xiàn)異常,它將返回nil。

          3、使用try!時,它不允許異常繼續(xù)傳播。一旦出現(xiàn)異常,程序會立即停止執(zhí)行。

          因此,在文件操作中,你可以根據(jù)需要選擇合適的異常處理方式。在百度App中一般推薦使用try?。


          2.3 public 和 open 的區(qū)別

          在Swift語言中,public和open都是用于在模塊中聲明需要對外界暴露的函數(shù)的關(guān)鍵字,但它們在繼承和公開程度上有所不同。

          1、public關(guān)鍵字修飾的類在模塊外部無法被繼承。這意味著,如果其他模塊試圖繼承這個類,編譯器會報錯。這樣的限制可以保護(hù)類的完整性,但也可能限制了其在其他模塊中的可重用性。

          2、open關(guān)鍵字則允許任意繼承。如果一個類被open關(guān)鍵字修飾,那么其他模塊中的類可以自由地繼承這個類,不受任何限制。這樣的公開程度使得open關(guān)鍵字修飾的類在模塊間的重用性和擴(kuò)展性更加靈活。

          從公開程度上來說,public的限制比open更嚴(yán)格,所以可以說public < open,即public的公開程度比open要低。

          2.4 解析JSON情況

          在Swift中解析JSON的情況,如果自行將JSON轉(zhuǎn)換為字典,需要涉及到類型判斷、轉(zhuǎn)換等操作,代碼比較復(fù)雜。這時可以使用第三方庫SwiftyJSON、ObjectMapper或者系統(tǒng)庫JSONEncoder來簡化操作,提高開發(fā)效率。

          2.5 UIView子類必須添加init?(coder decoder: NSCoder)的原因

          1、這是NSCoding protocol定義的,遵守了NSCoding protocol的所有類必須繼承。只是有的情況會隱式繼承,而有的情況下需要顯示實現(xiàn)。

          2、當(dāng)我們在子類定義了指定初始化器(包括自定義和重寫父類指定初始化器),那么必須顯示實現(xiàn)required init?(coder aDecoder: NSCoder),而其他情況下則會隱式繼承,我們可以不用理會。

          3、當(dāng)我們使用storyboard實現(xiàn)界面的時候,程序會調(diào)用這個初始化器。

          4、注意要去掉fatalError,fatalError的意思是無條件停止執(zhí)行并打印。

          2.6 Swift類和子類的初始化

          Swift的類和子類初始化涉及到兩個關(guān)鍵階段。首先,確保所有的存儲屬性被賦予初始值,然后,在實例準(zhǔn)備使用之前,可以自定義存儲屬性的值。為了確保這兩個階段成功,實施了四步安全檢查,詳細(xì)如下:

          1、在完成本類所有存儲屬性賦值之后,指定構(gòu)造器才能向上代理到父類的構(gòu)造器。

          2、在為繼承的屬性設(shè)置新值之前,指定構(gòu)造器必須向上代理調(diào)用父類構(gòu)造器。

          3、便利構(gòu)造器必須先調(diào)用其他構(gòu)造器,再為任意屬性(包括所有同類中定義的)賦新值。

          4、在第一階段構(gòu)造完成之前,構(gòu)造器不能調(diào)用任何實例方法,不能讀取任何實例屬性的值,不能引用self作為一個值。

          總之,類初始化必須完成的一個任務(wù)就是讓所有的存儲屬性都有初始值(optional 除外)。如果父類有指定初始化,子類必須也有指定初始化,并且必須調(diào)用父類的其中一個指定初始化(如果是必須初始化,就是重載),并遵循兩段式初始化的規(guī)則。一個便利初始化必須調(diào)用同一類中的初始化方法(可以是另一個便利初始化,也可以是指定初始化),但最終一定會調(diào)用到一個指定初始化。便利初始化不遵循兩段式初始化的規(guī)則,不能被子類調(diào)用或者重載。


          GEEK TALK

          03

          OC與Swift的互相調(diào)用及跳轉(zhuǎn)

          3.1 組件內(nèi)Swift文件調(diào)用公開OC頭文件

          • 將公開OC頭文件(如:xyz.h)添加到組件(如:ABC)umbrella header中(如:#import);
          • Swift文件中直接調(diào)用公開OC頭文件內(nèi)容。

          3.2 組件內(nèi)Swift文件調(diào)用非公開(私有)的OC文件

          組件應(yīng)該盡可能少的公開暴露頭文件,但Swift和OC混編不可避免使用OC非公開頭文件,因此我們可以采取以下措施:將Framework 中將私有頭文件聲明為一個私有 module(modulemap內(nèi)聲明),由組件內(nèi)的 Swift 源碼 import 該私有 module 即可。

          1、創(chuàng)建Private.modulemap文件,以NewModule做為組件名為例,可以命名為NewModule.private.modulemap,內(nèi)容為下,module后面加_Private

          • 羅列頭文件的形式
          framework module NewModule_Private {
            header "xxxxx.h"
          }
          • 使用根頭文件的形式,添加頭文件NewModule_Private.h
          framework module NewModule_Private {
            umbrella header "NewModule_Private.h"
          
          
            export *
            module * { export * }
          }

          2、在組件build settings中配置MODULEMAP_PRIVATE_FILE路徑,MODULEMAP_PRIVATE_FILE='NewModule.private.modulemap';百度App中在NewModule.boxspec中如下代碼設(shè)置路徑;

          s.xcconfig={
              'MODULEMAP_PRIVATE_FILE'=> '${BOX_ROOT}/NewModule.private.modulemap'
          }

          3、將NewModule.private.modulemap添加到工程目錄;百度App中在NewModule.boxspec中如下代碼設(shè)置路徑;

          s.refer_files=[
                "NewModule.private.modulemap",
          ]

          4、將xxxxx.h設(shè)置為Private header,百度App中在NewModule.boxspec中如下代碼設(shè)置xxxxx.h到Private header

          s.private_headers=[
              "Sources/xxxxx.h"
            ]

          5、調(diào)用方式

          import NewModule_Private
          let objectX=xxxxx()
          print(objectX)

          注意:

          • 添加的Private頭文件可能存在傳遞頭文件的情況,即import其他頭文件,也需要將傳遞的頭文件添加到NewModule_Private中,同時import需要使用尖括號;
          • Private Header也會暴露在framework中,所以可以約定外部組件使用Public Header,而避免使用Private Header,因為隨著業(yè)務(wù)發(fā)展和Swift&OC混編,Private Header是不穩(wěn)定的。

          3.3 組件內(nèi)OC文件如何調(diào)用Swift文件?

          • Swift 類需要繼承 NSObject,方法前面加上@objc 標(biāo)識,并且是 public 或者 open 的;
          • 引入方式 #import"

          3.4 OC中的向前聲明,被Swift文件引用該組件會報錯

          如error: cannot find protocol definition for 'xxxProtocol'

          • 原因:此報錯在OC中是代碼警告,百度App中默認(rèn)情況Swift中SWIFT_TREAT_WARNINGS_AS_ERRORS 設(shè)置為 YES,導(dǎo)致OC中的Warning視為Error;
          • 解決方案:三選一

          1、暫時設(shè)置 SWIFT_TREAT_WARNINGS_AS_ERRORS 為 NO

          2、import xxxProtocol 不要向前聲明

          3、使用 pragma 忽略警告

          3.8 Swift怎么用OC定義的宏?

          • 在Swift中,能直接使用定義為常量的宏,不能使用帶有方法調(diào)用的宏,也不能使用靜態(tài)常量。
          下面這種定義為常量的宏可以使用
          #define APP_LANGUAGE_EN @"en" 
          #define kNavigationBarHeight 44.0
          
          
          下面帶有方法調(diào)用的宏不可以使用
          #define kScreenHeight [[UIScreen mainScreen] bounds].size.height
          #define kScreenWidth [[UIScreen mainScreen] bounds].size.width
          
          
          下面帶有靜態(tài)常量swift不能使用,可以改成宏
          static NSString *const StopTabRefreshNotifyNameHtml=@"TabRefreshNotifyNameHtml";

          3.6 Swift與OC泛型的混編

          • 在我分們基礎(chǔ)框架中,有一個使用了OC泛型的類,如:
          @interface BBAXYZ<T> : NSObject <BBAXYZEventProtocol>  
          @property (nonatomic, weak) T page;  
          @end

          這個泛型的使用導(dǎo)致無法使用Swift來繼承和開發(fā)BBAXYZ的子類。然而,這個基礎(chǔ)框架是業(yè)務(wù)的核心部分,因此,我們需要在未來支持Swift的開發(fā)。

          • 經(jīng)過仔細(xì)觀察和分析,我們發(fā)現(xiàn)泛型主要被用于指定page屬性的類型。因此,我們可以考慮去掉泛型,改為提供一個返回適當(dāng)類型的方法。這樣,我們就可以在Swift中順利地繼承和使用這個基礎(chǔ)框架。修改后的代碼如下:
          @interface BBAXYZ : NSObject <BBAXYZEventProtocol>  
          - (id<BBAXYZEventProtocol>)page;  
          @end

          然后,我們可以創(chuàng)建一個OC類來實現(xiàn)這個基礎(chǔ)框架,并讓所有的子類繼承這個OC類并實現(xiàn) page 方法,以返回適當(dāng)類型的對象。這樣,我們就可以在Swift中順利地繼承和使用這個基礎(chǔ)框架。

          例如:

          @interface BBAABC : BBAXYZ  
          - (UIViewController<BBAXYZEventProtocol> *)page; 
          @end

          需要注意的是,雖然這樣的修改增加了輕量級的中間OC類,但它仍然實現(xiàn)了Swift與OC的混編,并允許我們在Swift中開發(fā)新的子類。這種方式既保證了代碼的兼容性,又使得我們可以繼續(xù)利用OC的優(yōu)點(diǎn)。

          • 使用方式
          class BBAEFG: BBAABC {
              
          }

          3.7 Swift調(diào)用OC接口,OC的nullability標(biāo)注使用時的注意事項

          問題場景:

          1、OC 接口定義為 nonnull,swfit 調(diào)用時正常是當(dāng)做不可選類型使用,這時如果 OC 接口不規(guī)范返回 nil,則出現(xiàn)運(yùn)行時崩潰。

          2、OC 接口未定義 nonnull 或 nullable,在這種情況下,編譯器會將 OC 的指針類型當(dāng)成是隱式解析可選類型(例如 String!)導(dǎo)入到 Swift 中。swift 調(diào)用時,OC接口如果返回 nil,將會因為隱式解析一個為 nil 的可選值導(dǎo)致運(yùn)行時崩潰。

          解決方式:

          1、Swift 調(diào)用 OC 接口時,如果 OC 的接口聲明為 nonnull 或未指定 nullability 時,只有明確 OC 接口不為空的情況下才可調(diào)用

          2、在 OC 環(huán)境下,將 nil 賦值給 nonnull 指針也沒有關(guān)系,編譯器只會產(chǎn)生警告。這就需要程序員按規(guī)范編寫 OC 代碼,正確使用 nullability 標(biāo)注,并增加運(yùn)行時判空的斷言,以支持向后兼容。


          GEEK TALK

          04

          其他常見問題

          4.1 Xcode編譯只提示編譯錯誤,提示信息非常少

          • 原因:使用Swift語言開發(fā)的組件,依賴了不支持Module化的組件,導(dǎo)致組件都能編譯成功,但整個工程卻編譯失敗了;
          • 解決方案:二選一

          1、檢查并保障所有依賴的組件都已經(jīng)Module化了,如配置build settings;

          2、在組件中新增Swift文件(空文件也行)。

          4.2 由于組件開啟了Library Evolution 導(dǎo)致的編譯報錯

          錯誤顯示:@objc' instance method in extension of subclass of 'xxxxx' requires iOS 13.0.0

          這是由于組件開啟了Library Evolution導(dǎo)致,開關(guān)BUILD_LIBRARY_FOR_DISTRIBUTION 控制的。

          一個庫開啟了Library Evolution,在依賴鏈下游的庫中將:

          1、對它的類實現(xiàn) @objc 子類。

          2、對它的類使用 extension 實現(xiàn) @objc 的方法(這在 UIKit 的 protocol 中經(jīng)常會遇到)。

          3、對它的類實現(xiàn)子類,并添加 @objc 方法,且方法中使用父類的類型作為參數(shù)。

          這些功能是實現(xiàn)的局限。估計是需要在 Swift 運(yùn)行時有一些對應(yīng)的更改,所以只在 swift 5.1 (iOS 13)運(yùn)行時里才可以運(yùn)行。

          除此之外,還會有一些編譯時沒有報錯,但運(yùn)行時 crash 或結(jié)果不正確的情況。

          百度App中默認(rèn)開啟Library Evolution,一個組件關(guān)閉Library Evolution會導(dǎo)致二進(jìn)制存在不兼容的情況,暫時無解決方案。

          4.3 暴露的Private頭文件如果使用雙引號import,會報警告,需要修改為尖括號

          如果使用import <xxxx.h>,Project下其他Target就引用不到了,如百度App中Debug模塊引用此Private頭文件時,會報錯 not found with <angled> include, use "quotes" instead。

          • 原因:這是由于其他Target對主模塊引用時默認(rèn)是從當(dāng)前項目下引用頭文件,而尖括號方式從系統(tǒng)庫或用戶庫中引用;
          • 解決方案:其他Target將Private Header 配置到其 HEADER_SEARCH_PATHS,使用雙引號和尖括號均可。


          GEEK TALK

          05

          總結(jié)

          以上是我們在Swift開發(fā)過程中所遇到的一些常見問題及其相應(yīng)的解決方案。然而,隨著我們不斷深入Swift開發(fā)這片浩渺的海洋,更多獨(dú)特的問題將會逐漸浮現(xiàn)。我們會持續(xù)將這些新問題以及其對應(yīng)的解決方案整理并發(fā)布出來,為廣大的開發(fā)者們提供有價值的參考。歡迎大家留言探討。

          作者:路濤、艷紅

          來源:微信公眾號:百度Geek說

          出處:https://mp.weixin.qq.com/s/hHH4a8tOukqhd9A5SxbzYw

          家都說這世上最美的就是玫瑰和女人,當(dāng)這兩者“碰撞”在一起的時候,一切就變得美不可言了。想必大家也知道很多名人的美都與玫瑰結(jié)下了不解之緣,比如超模娜塔莉亞·沃迪亞諾娃。

          可能是現(xiàn)實版的灰姑娘變成公主的故事,娜塔莉亞小時候靠擺水果攤維生,14歲那年在自家水果攤忙活時,被模特星探發(fā)掘,飛往法國巴黎,開啟傳奇超模生涯。

          之后的她也經(jīng)歷過離婚,執(zhí)意一個人帶著三個孩子生活。但最后和貝爾納·阿爾諾(頂級奢侈品公司LVMH )的兒子安托萬.阿爾諾結(jié)婚,目前兩人育有兩個孩子,帶著五個孩子一起生活。當(dāng)然,于我們而言,她是幸運(yùn)的。但如果沒有足夠的勤奮和自律,她可能還是一個水果攤的老板娘。

          還記得當(dāng)時剛生孩子的她,產(chǎn)后第三個星期她就回T臺上了。反正從她出道到現(xiàn)在,肌膚狀態(tài)一直都非常的好。看到她為某時尚雜志拍攝的getready with me 視頻之后就發(fā)現(xiàn)原來皮膚好,很多原因是在于選對了產(chǎn)品呀。

          可以說她的“本命“就是Fresh了吧,視頻里所有的推薦幾乎都是Fresh的單品呢~而今天小編就要來推薦一下Fresh家的東西了,感覺最近超多仙女急需一瓶好用的水哦,畢竟換季像清倉一樣,是時候買瓶新的水給自己啦。

          推薦產(chǎn)品:Fresh玫瑰花瓣水

          今天小編就是要推薦這款超美的Fresh馥蕾詩玫瑰花瓣水,真真實實的玫瑰花哦,搶先用過的小編表示看到這么多玫瑰花心情都變好了!所以Fresh玫瑰花瓣水怎么樣?

          答案當(dāng)然是肯定的,光不含酒精這一點(diǎn)就能讓小仙女們愛上了好嘛!而且小編還想說連續(xù)使用Fresh馥蕾詩玫瑰花瓣水兩周,肌膚的水潤度真的得到了極大提升哦。連續(xù)早晚使用,涂抹瞬間,都能馬上感受到水潤修復(fù)的舒適感。

          這瓶玫瑰水中含有超多真實的玫瑰花瓣,也正是因為如此,每天涂的時候都有一股天然的玫瑰花的香味,可以說是每天都在期待著這股玫瑰花的味道,恨不得一天涂N次,讓肌膚和自己都一直在真正的花海中一樣。

          想必有盆友就要問了,Fresh玫瑰花瓣水功效是什么呢?別急,小編這就告訴你們哦。它不僅可以幫助肌膚補(bǔ)充水分,還能細(xì)致毛孔,對紅腫的痘痘有消炎的效果哦。同時還能舒緩肌膚,改善膚質(zhì)。讓肌膚如花瓣一般,再現(xiàn)水潤舒緩、細(xì)致柔嫩的肌膚,并為后續(xù)的護(hù)膚步驟做好準(zhǔn)備。

          使用方法也很簡單哦!每日早晚潔凈肌膚后,用化妝棉或指尖沾取適量玫瑰花瓣水,輕柔掃過面部與頸部就可以啦!所以經(jīng)過小編的一番“講解“,是不是覺得Fresh玫瑰花瓣水好用了呢?在這里小編還想說搭配同系列的Fresh產(chǎn)品效果會更好哦。

          比如,玫瑰潤澤密集保濕面霜可以為肌膚帶來持續(xù)潤養(yǎng),深徹補(bǔ)水。當(dāng)然再搭配玫瑰潤澤保濕舒緩面膜,用完之后能讓大家的肌膚看上去更加光彩柔嫩哦。大家可以根據(jù)自己的肌膚狀況選擇購買哦~這里小編就給你帶來點(diǎn)福利啦,F(xiàn)resh玫瑰花瓣水體驗裝免費(fèi)試用申領(lǐng) ~趕緊點(diǎn)擊鏈接領(lǐng)取吧~

          http://freshrose.comeyes.cn/index.html?utm_source=Onlylady&utm_medium=Onlyladyeditorial&utm_campaign=fresh2018


          主站蜘蛛池模板: 中文字幕亚洲一区二区三区| 国模无码视频一区二区三区| 国产av一区最新精品| 国产suv精品一区二区33| 国产在线无码一区二区三区视频| 一区二区三区亚洲视频| 黑人大战亚洲人精品一区| 久久se精品一区二区国产| 亚洲色无码专区一区| 色一情一乱一伦一区二区三区日本| 一区二区三区视频在线播放| 久久精品无码一区二区app| 无码乱人伦一区二区亚洲一 | www.亚洲一区| 国模吧一区二区三区| 亚洲av乱码一区二区三区| 国产中的精品一区的| 亚洲福利视频一区二区三区| 精品国产一区二区三区色欲| 无码AⅤ精品一区二区三区| 无码aⅴ精品一区二区三区| 丝袜美腿一区二区三区| 蜜桃AV抽搐高潮一区二区| 在线播放一区二区| 国产成人精品久久一区二区三区| 亚洲一区综合在线播放| 亚洲av无码一区二区三区天堂古代| 人妻少妇久久中文字幕一区二区 | 无码国产精品一区二区免费vr| 综合人妻久久一区二区精品| 亚洲A∨无码一区二区三区| 正在播放国产一区| 日韩精品一区二区三区中文 | 国产精品成人一区二区| 高清精品一区二区三区一区| 中文字幕日韩人妻不卡一区| 少妇人妻精品一区二区| 国产成人无码精品一区二区三区| 国产福利一区二区| 无码人妻精品一区二区三 | V一区无码内射国产|