最近業務須要出一份Java Web應用源碼安全審查報告, 對比了市面上數種工具及其分析結果, 基於結果總結了一份規則庫. 本文目錄結構以下: html
基於class文件分析, 他是大名鼎鼎的findbugs的插件, 安裝比較簡單. 在findbugs官網下載安裝包, 插件jar, 把jar放到findbugs-3.0.1\plugin目錄.前端
打開bin路徑下的findbugs.bat啓動軟件. 在菜單欄 - 編輯 - 選項能夠查看插件是否啓用成功.java
新建項目, 輸入名稱, 選擇須要分析的class路徑, 引用的第三方包地址, 源碼路徑, 點擊Analyze便可. git
最終生成的結果能夠轉爲html報告, 也能夠導出xml文件, 在findbugs分析查看. 本文主要關注Security一欄.程序員
360出品, 名聲彷佛不太好, 誤報比較多, 不過結果也有必定的參考價值. 若是代碼在碼雲的話, 點服務一欄, 能夠在線掃描碼雲庫裏的源碼, 本地代碼要去官網申請試用.github
掃描結果web
HP出品的老牌掃描工具, 網上有破解的. 安裝過程一路next便可. 最後啓動AuditWorkBench, 選擇scan java project, 耐心等待.正則表達式
結果跟findbugs相似.算法
啄木鳥源碼分析, 國內的一款也是基於字節碼分析的工具, 提供了一個收費的在線審計平臺. 知乎有很多軟文, 沒找到免費試用的地方, 放棄了.sql
命令注入
命令注入是指應用程序執行命令的字符串或字符串的一部分來源於不可信賴的數據源,程序沒有對這些不可信賴的數據進行驗證、過濾,致使程序執行惡意命令的一種攻擊方式。
String dir = request.getParameter("dir");
Process proc = Runtime.getRuntime().exec("cmd.exe /c dir" + dir);
若是攻擊者傳遞了一個dir形式爲"dummy && del c:\\dbms\\*.*"的字符串,那麼該段代碼將會在執行其餘指定命令的同時執行這條刪除命令。
修復方式
(1)程序對非受信的用戶輸入數據進行淨化,刪除不安全的字符。
(2)限定輸入類型, 建立一份安全字符串列表,限制用戶只能輸入該列表中的數據。
1 // 方式1 2 if (!Pattern.matches("[0-9A-Za-z@.]+", dir)) { 3 // Handle error 4 } 5 6 // 方式2 7 int number = Integer.parseInt(request.getParameter("dir")); 8 switch (number) { 9 case 1: 10 btype = "tables" 11 break; // Option 1 12 case 2: 13 btype = "users" 14 break; // Option 2 15 ......
HTTP響應截斷
程序從一個不可信賴的數據源獲取數據,未進行驗證就置於HTTP頭文件中發給用戶,可能會致使HTTP響應截斷攻擊。
String author = request.getParameter(AUTHOR_PARAM); ... Cookie cookie = new Cookie("author", author); cookie.setMaxAge(cookieExpiration); response.addCookie(cookie);
那麼若是攻擊者提交的是一個惡意字符串,好比「Wiley Hacker\r\nHTTP/1.1 200 OK\r\n...」,那麼HTTP響應就會被分割成如下形式的兩個響應:
HTTP/1.1 200 OK
...
Set-Cookie: author=Wiley Hacker
HTTP/1.1 200 OK
...
這樣第二個響應已徹底由攻擊者控制,攻擊者能夠用所需的頭文件和正文內容構建該響應實施攻擊。
修復方式
防止HTTP響應截斷攻擊的最安全的方法是建立一份安全字符白名單,只接受徹底由這些受承認的字符組成的輸入出如今HTTP響應頭文件中。
String author = request.getParameter(AUTHOR_PARAM); if (Pattern.matches("[0-9A-Za-z]+", author)) { ... Cookie cookie = new Cookie("author", author); cookie.setMaxAge(cookieExpiration); response.addCookie(cookie); }
SQL注入
SQL注入是一種數據庫攻擊手段。攻擊者經過嚮應用程序提交惡意代碼來改變原SQL語句的含義,進而執行任意SQL命令,達到入侵數據庫乃至操做系統的目的。
String sqlString = "SELECT * FROM db_user WHERE username = '" + username + "' AND password = '" + pwd + "'"; Statement stmt = connection.createStatement(); ResultSet rs = stmt.executeQuery(sqlString);
攻擊者可以替代username和password中的任意字符串,它們可使用下面的關於password的字符串進行SQL注入。
SELECT * FROM db_user WHERE username='' AND password='' OR '1'='1'
修復方式
形成SQL注入攻擊的根本緣由在於攻擊者能夠改變SQL查詢的上下文,使程序員本來要做爲數據解析的數值,被篡改成命令了。防止SQL注入的方法以下:
(1)正確使用參數化API進行SQL查詢。
(2)若是構造SQL指令時須要動態加入約束條件,能夠經過建立一份合法字符串列表,使其對應於可能要加入到SQL指令中的不一樣元素,來避免SQL注入攻擊。
String sqlString = "select * from db_user where username=? and password=?"; PreparedStatement stmt = connection.prepareStatement(sqlString); stmt.setString(1, username); stmt.setString(2, pwd); ResultSet rs = stmt.executeQuery();
正則表達式注入
數據被傳遞至應用程序並做爲正則表達式使用。可能致使線程過分使用 CPU 資源,從而致使拒絕服務攻擊。
下述代碼java中字符串的split, replaceAll均支持正則的方式, 致使CPU掛起.
1 final String input = "0000000000000000000000000000000000000000000000"; 2 long startTime = System.currentTimeMillis(); 3 System.out.println(input.split("(0*)*A")); 4 System.out.println("耗時:" + (System.currentTimeMillis() - startTime) + "ms");
該正則的意思是說匹配器在輸入的末尾並無檢測到」A」。如今外側的限定符後退一次,內存的則前進一次,如此重複,沒法獲得結果。
所以,匹配器逐步回退,並嘗試全部的組合以找出匹配符號。它最終將返回(沒有匹配的結果),可是該過程的複雜性是指數型的(輸入中添加一個字符加倍了運行時間)
修復方式
使用線程池 + Future, 限定執行時間, 並捕獲異常.
1 ExecutorService service = Executors.newFixedThreadPool(1); 2 Future result = service.submit(new Callable<Object>() { 3 @Override 4 public Object call() { 5 final String input = "0000000000000000000000000000000000000000000000"; 6 return input.split("(0*)*A"); 7 } 8 }); 9 service.shutdown(); 10 System.out.println(result.get(5, TimeUnit.SECONDS));
LDAP注入
LDAP注入是指客戶端發送查詢請求時,輸入的字符串中含有一些特殊字符,致使修改了LDAP原本的查詢結構,從而使得能夠訪問更多的未受權數據的一種攻擊方式。
如下代碼動態構造一個 LDAP 查詢,並對其加以執行,該查詢能夠檢索全部報告給指定經理的僱員記錄。該經理的名字是從 HTTP 請求中讀取的,所以不可信任。
1 DirContext ctx = new InitialDirContext(env); 2 String managerName = request.getParameter("managerName"); 3 //retrieve all of the employees who report to a manager 4 String filter = "(manager=" + managerName + ")"; 5 NamingEnumeration employees = ctx.search("ou=People,dc=example,dc=com",filter);
若是攻擊者爲 managerName 輸入字符串 Hacker, Wiley)(|(objectclass=*),則該查詢會變成:
(manager=Hacker, Wiley)(|(objectclass=*))
根據執行查詢的權限,增長 |(objectclass=*) 條件會致使篩選器與目錄中的全部輸入都匹配,並且會使攻擊者檢索到有關用戶輸入池的信息。
若是攻擊者可以控制查詢的命令結構,那麼這樣的攻擊至少會影響執行 LDAP 查詢的用戶能夠訪問的全部記錄。
修復方式
用白名單的方法,確保LDAP查詢中由用戶控制的數值徹底來自於預約的字符集合,應不包含任何LDAP元字符。
好比使用Spring框架中EqualsFilter類來構造一個編碼得當的篩選器字符串.
DirContext ctx = new InitialDirContext(env); String managerName = request.getParameter("managerName"); //retrieve all of the employees who report to a manager EqualsFilter filter = new EqualsFilter("manager", managerName); NamingEnumeration employees = ctx.search("ou=People,dc=example,dc=com",filter.toString());
拒絕服務
拒絕服務是攻擊者經過極度消耗應用資源,以至程序崩潰或其餘合法用戶沒法進行使用的一種攻擊方式。
例如解壓文件前,未檢查文件大小,攻擊者能夠經過提供一個超大文件,實施DOS攻擊。
1 FileOutputStream fos = new FileOutputStream(entry.getName()); 2 dest = new BufferedOutputStream(fos, BUFFER); 3 while ((count = zis.read(data, 0, BUFFER)) != -1) { 4 dest.write(data, 0, count); 5 }
修復方式
對涉及到系統資源的外部數據應該進行嚴格校驗,防止無限制的輸入。對於用戶上傳的文件, 要在後臺進行大小校驗.
好比對解壓文件進行驗證,超過100M,將拋出異常。
1 if (entry.getSize() > TOOBIG) { 2 throw new IllegalStateException("File to be unzipped is huge."); 3 }
重定向參數
應用程序容許未驗證的用戶輸入控制重定向中的URL,可能會致使攻擊者發動釣魚攻擊。
String url = request.getParameter("url");
response.sendRedirect(url);
修復方式
建立一份合法URL列表,用戶只能從中進行選擇,進行重定向操做。
XML實體注入
簡稱XXE攻擊, XML解析器中默認會解析xml中的ENTITY來支持全局變量以及外部文件讀取.
若是從web請求中獲取xml內容, 並在服務器端解析, 則可能致使xxe攻擊.
1 DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); 2 DocumentBuilder db = dbf.newDocumentBuilder(); 3 Document doc = db.parse(xmlFile); 4 NodeList list = doc.getElementsByTagName("active");
修復方式
(1)關閉XML實體解析
(2)使用JSON來替代XML作數據傳輸
1 DocumentBuilderFactory dbf =DocumentBuilderFactory.newInstance(); 2 dbf.setExpandEntityReferences(false);
資源注入
使用用戶輸入控制資源標識符,藉此攻擊者能夠訪問或修改其餘受保護的系統資源。當知足如下兩個條件時,就會發生資源注入:
(1)攻擊者能夠指定已使用的標識符來訪問系統資源。例如,攻擊者可能能夠指定用來鏈接到網絡資源的端口號。
(2)攻擊者能夠經過指定特定資源來獲取某種權限,而這種權限在通常狀況下是不可能得到的。例如,程序可能會容許攻擊者把敏感信息傳輸到第三方服務器。
1 URL url = new URL(request.getParameter("remoteURL")); 2 URLConnection connection = url.openConnection(); 3 ... 4 String remotePort = request.getParameter("remotePort"); 5 ServerSocket srvr = new ServerSocket(remotePort); 6 Socket skt = srvr.accept();
修復方式
使用白名單策略, 限制資源文件讀取和訪問.
日誌僞造
容許日誌記錄未經驗證的用戶輸入,會致使日誌僞造攻擊。攻擊者可能經過破壞文件格式或注入意外的字符,從而使文件沒法使用。
更陰險的攻擊可能會致使日誌文件中的統計信息發生誤差, 掩護攻擊者的跟蹤軌跡.
1 if (loginSuccessful) { 2 logger.severe("User login succeeded for: " + username); 3 } else { 4 logger.severe("User login failed for: " + username); 5 }
攻擊者能夠將username替換爲一個多行字符串,以下所示:
jack
2013-7-30 java.util.logging.LogManager log
Server: User login succeeded for: Tom
修復方式
對不可信賴的數據進行校驗。
另外日誌中不該該出現敏感數據, 例如密碼, 手機號, 郵箱這些信息.
1 if (!Pattern.matches("[A-Za-z0-9_]+", username)) { 2 // Unsanitized username 3 logger.severe("User login failed for unauthorized user"); 4 } else if (loginSuccessful) { 5 logger.severe("User login succeeded for: " + username); 6 }
文件校驗
對於用戶上傳的文件, 須要在先後臺雙重校驗, 校驗後綴, 文件大小, 二進制頭等等.
其餘可輸入項, 也要作先後臺雙重校驗, 防止中間人修改數據.
修復方式
對文件二進制頭進行校驗
1 static { 2 MAGIC_NUMBER.put("jpg", new String[]{"FFD8"}); 3 MAGIC_NUMBER.put("gif", new String[]{"47494638"}); 4 MAGIC_NUMBER.put("png", new String[]{"89504E470D0A1A0A"}); 5 MAGIC_NUMBER.put("pdf", new String[]{"25504446"}); 6 MAGIC_NUMBER.put("doc", new String[]{"D0CF11E0A1B11AE1", "7B5C72746631"}); 7 MAGIC_NUMBER.put("xls", new String[]{"D0CF11E0A1B11AE1"}); 8 MAGIC_NUMBER.put("ppt", new String[]{"D0CF11E0A1B11AE1"}); 9 MAGIC_NUMBER.put("docx", new String[]{"504B0304"}); 10 MAGIC_NUMBER.put("xlsx", new String[]{"504B0304"}); 11 MAGIC_NUMBER.put("pptx", new String[]{"504B0304"}); 12 } 13 14 /** 15 * 在檢驗範圍內(MAGIC_NUMBER.keySet(): jpg, gif, png, pdf, xls, ppt, doc, xlsx, pptx, docx) 16 * 且文件後綴和文件二進制頭不一致。返回false 17 */ 18 public static boolean checkFileType(byte[] content, String suffix) { 19 if (!MAGIC_NUMBER.keySet().contains(suffix)) { 20 return true; 21 } 22 23 byte[] bytes = Arrays.copyOfRange(content, 0, Math.min(content.length, MAGIC_HEADER_LENGTH)); 24 String fileCode = getFileHeader(bytes); 25 for (String magicNumber : MAGIC_NUMBER.get(suffix)) { 26 if (fileCode.toUpperCase().startsWith(magicNumber)) { 27 return true; 28 } 29 } 30 return false; 31 }
硬編碼密碼
程序中採用硬編碼方式處理密碼,一方面會下降系統安全性,另外一方面不易於程序維護。
1 private String rootManagerPassword = DEFAULTADMINPASSWORD; 2 ...... 3 if (password == null) { 4 password = "123456"; 5 }
修復方式
程序中所需密碼應從配置文件中獲取通過加密的密碼值。
弱加密
在安全性要求較高的系統中,使用不安全的加密算法(如DES、RC四、RC5等),將沒法保證敏感數據的保密性。
1 Cipher des = Cipher.getInstance("DES"); 2 SecretKey key = KeyGenerator.getInstance("DES").generateKey();
修復方式
使用安全的加密算法(如AES、3DES、RSA)對敏感數據進行加密。
1 Cipher aes = Cipher.getInstance("AES"); 2 KeyGenerator kg = KeyGenerator.getInstance("AES"); 3 kg.init(128); 4 SecretKey key = kg.generateKey();
不安全的Hash
在安全性要求較高的系統中,不該使用被業界公認的不安全的哈希算法(如MD二、MD四、MD五、SHA、SHA1等)來保證數據的完整性。
1 MessageDigest messageDigest = MessageDigest.getInstance("MD5"); 2 messageDigest.update(stringID.getBytes());
修復方式
採用散列值>=224比特的SHA系列算法(如SHA-22四、SHA-25六、SHA-384和SHA-512)來保證敏感數據的完整性。
1 md = MessageDigest.getInstance("SHA-256"); 2 md.update(bt); 3 strDes = bytes2Hex(md.digest()); // to HexString
不安全的隨機數
Java API中提供了java.util.Random類實現PRNG(),該PRNG是可移植和可重複的,若是兩個java.util.Random類的實例使用相同的種子,會在全部Java實現中生成相同的數值序列。
1 // Random對象r和s設置了相同的種子,所以 i == j 以及數組b[]和c[]的相應值是相等的。 2 Random r = new Random(12345); 3 int i = r.nextInt(); 4 byte[] b = new byte[4]; 5 r.nextBytes(b); 6 7 Random s = new Random(12345); 8 int j = s.nextInt(); 9 byte[] c = new byte[4]; 10 s.nextBytes(c);
修復方式
使用更安全的隨機數生成器,如java.security.SecureRandom類。
1 SecureRandom number = SecureRandom.getInstance("SHA1PRNG"); 2 System.out.println(number.nextInt() + " " + number.nextInt());
XSS
應用程序從數據庫或其它後端數據存儲獲取不可信賴的數據,在未檢驗數據是否存在惡意代碼的狀況下,便將其傳送給了Web用戶,應用程序將易於受到存儲型XSS攻擊。
1 PrintWriter writer = WebUtils.createPrintWriter(res); 2 writer.print(str); 3 writer.flush(); 4 writer.close();
修復方式
對輸出的字符串內容進行html轉義編碼.
public static String replaceScript4Xss(String message) { if (StringUtils.isEmpty(message)) { return StringUtils.EMPTY; } StringBuffer builder = new StringBuffer(message.length() * 2); CharacterIterator it = new StringCharacterIterator(message); for (char ch = it.first(); ch != CharacterIterator.DONE; ch = it.next()) { if ((((ch > '`') && (ch < '{')) || ((ch > '@') && (ch < '['))) || (((ch == ' ') || ((ch > '/') && (ch < ':'))) || (((ch == '.') || (ch == ',')) || ((ch == '-') || (ch == '_'))))) { builder.append(ch); } else { builder.append("&#" + (int) ch + ";"); } } return builder.toString(); }
CSRF跨站
跨站請求僞造(CSRF)是僞造客戶端請求的一種攻擊。應用程序容許用戶提交不包含任何保密信息的請求,將可能致使CSRF攻擊。
如如下代碼片斷用於銀行轉帳功能,若對於該重要敏感的操做沒有進行相應防禦,將易於致使跨站請求僞造攻擊。
1 <form method="GET" action="/transferFunds " > 2 cash: <input type="text" name="cash"> 3 to: <input type=" text " name=「to"> 4 <input type="submit" name="action" value="TransferFunds"> 5 </form>
修復方式
(1)二次驗證,進行重要敏感操做時,要求用戶進行二次驗證。
(2)驗證碼,進行重要敏感操做時,加入驗證碼。
(3)在重要敏感操做的表單中加入隱藏的Token, 服務器端程序響應用戶請求前先驗證Token,判斷請求的合法性。
Cookie屬性
Cookie未設置httponly以及secure屬性.
1 Cookie cookie = new Cookie("userName",userName); 2 response.addCookie(cookie);
修復方式
1 Cookie cookie = new Cookie("userName",userName); 2 cookie.setSecure(true); // Secure flag 3 cookie.setHttpOnly(true);
Cookie生命週期
Cookie生命週期不該該超過一年.
1 Cookie cookie = new Cookie("email", email); 2 cookie.setMaxAge(60*60*24*365);
jsessionid
登陸先後改變jsessionid標識, 修改配置容器, 加強jsessionid算法邏輯.
1 HttpSession oldSession = req.getSession(false); 2 if (oldSession != null) { 3 //廢棄舊的session, 不然每次退出後再登陸, jsessionid不會變化. 4 oldSession.invalidate(); 5 } 6 HttpSession session = req.getSession(true);
SecurityHeaders
配置更安全的HTTP Header
1 res.addHeader("X-Content-Type-Options", "nosniff"); 2 res.addHeader("X-XSS-Protection", "1; mode=block"); 3 res.addHeader("X-Frame-Options", "SAMEORIGIN"); 4 res.addHeader("Content-Security-Policy", "object-src 'self'"); 5 6 res.addHeader("Cache-Control", "no-cache"); 7 res.addHeader("Pragma", "no-cache"); 8 res.addDateHeader("Expires", 0);
日期格式化
SimpleDateFormat 非線程安全的,parse()和format()方法包含一個可致使用戶看到其餘用戶數據的race condition。
1 private static SimpleDateFormat dateFormat;
修復方式
使用ThreadLocal放置SimpleDateFormat或者同步鎖的方式.
訪問權限
程序未進行恰當的訪問權限控制,執行了一個包含用戶控制主鍵的SQL語句,可能會致使攻擊者訪問未經受權的記錄。
以下面代碼片斷中的SQL語句用於查詢與指定標識符相匹配的清單。
1 id = Integer.decode(request.getParameter("invoiceID")); 2 String query = "SELECT * FROM invoices WHERE id = ?"; 3 PreparedStatement stmt = conn.prepareStatement(query); 4 stmt.setInt(1, id); 5 ResultSet results = stmt.execute();
修復方式
先判斷當前用戶權限是否能夠增刪數據, 能夠經過把當前被受權的用戶名做爲查詢語句的一部分來實現。
1 userName = ctx.getAuthenticatedUserName(); 2 id = Integer.decode(request.getParameter("invoiceID")); 3 String query = 4 "SELECT * FROM invoices WHERE id = ? AND user = ?"; 5 PreparedStatement stmt = conn.prepareStatement(query); 6 stmt.setString(1, id); 7 stmt.setString(2, userName); 8 ResultSet results = stmt.execute();
API限流
對於開放的API進行限流操做, 防止資源耗盡. 例如獲取IP城市 或者 天氣等等, 限制每一個IP每小時最多調用1000次之類的.
簡單實現能夠用計數器限流, 另外Guava提供了RateLimiter能夠實現令牌桶算法限流.
1 RateLimiter limiter = caches.get(ip); 2 3 if (limiter.tryAcquire()) { 4 System.out.println(i + " success " + new SimpleDateFormat("HH:mm:ss.sss").format(new Date())); 5 } else { 6 System.out.println(i + " failed " + new SimpleDateFormat("HH:mm:ss.sss").format(new Date())); 7 }
資源釋放
對於一些資源文件, 使用完畢後要在finally語句中進行釋放, 例如connection, 文件句柄, socket等等.
1 try { 2 DatabaseMetaData e = connection.getMetaData(); 3 ResultSet rs1 = e.getTableTypes(); 4 ...... 5 } catch (SQLException e) { 6 return StringUtils.EMPTY; 7 } finally { 8 DBUtils.close(connection); 9 }
路徑遍歷
用程序對用戶可控制的輸入未經合理校驗,就傳送給一個文件API。攻擊者可能會使用一些特殊的字符(如「..」和「/」)擺脫受保護的限制,訪問一些受保護的文件或目錄。
1 String path = getInputPath();
2 if (path.startsWith("/safe_dir/")){ 3 File f = new File(path); 4 f.delete() 5 }
攻擊者可能提供相似下面的輸入:/safe_dir/../important.dat
修復方式
使用白名單策略, 限制資源文件讀取和訪問.
路徑輸出
禁止輸出服務器絕對路徑到前端.
1 PrintWriter writer = WebUtils.createPrintWriter(res); 2 writer.write(file.getAbsolutePath());
修復方式
使用相對路徑
數據跨越信任邊界
數據從一個不可信賴域存儲到一個可信賴域致使程序錯誤信賴未驗證的數據。
1 String name = req.getParameter("userName");
2 HttpSession sess = req.getSession(); 3 sess.setAttribute("user", name);
修復方式
數據跨越信任邊界時須要進行合理的驗證,保證信賴域中數據是安全的。
Session失效配置
將Session的失效時間設置爲30分鐘或更少,既能使用戶在一段時間內與應用程序互動,又提供了一個限制窗口攻擊的合理範圍。
修復方式
<session-config> <session-timeout>30</session-timeout> </session-config>
錯誤頁面
Web應用程序的默認錯誤頁面不該顯示程序的敏感信息。Web應用程序應該爲4xx(如404)錯誤、5xx(如503)錯誤、java.lang.Throwable異常定義一個錯誤頁面,防止攻擊者挖掘應用程序容器內置錯誤響應信息。報錯頁面中不該該包含類名, 方法名, 執行堆棧等信息.
修復方式
應用程序應該在web.xml中配置默認的錯誤頁面。
<error-page> <error-code>403</error-code> <location>/common/403.jsp</location> </error-page> <error-page> <error-code>404</error-code> <location>/common/404.jsp</location> </error-page> <error-page> <error-code>500</error-code> <location>/common/500.jsp</location> </error-page> <error-page> <exception-type>java.lang.Throwable</exception-type> <location>/common/error.jsp</location> </error-page>
不安全的SSLContext
1 SSLContext.getInstance("SSL");
修復方式
配置web容器使用更安全的TLSv1.2協議.
1 SSLContext.getInstance("TLS");
未加密的Socket
1 ServerSocket soc = new ServerSocket(1234); 2 ...... 3 Socket soc = new Socket("www.google.com",80); 4 ......
修復方式
1 ServerSocket soc = SSLServerSocketFactory.getDefault().createServerSocket(1234); 2 ...... 3 Socket soc = SSLSocketFactory.getDefault().createSocket("www.google.com", 443); 4 ......
不安全的FTP協議
代碼中使用SFTP替代FTP
1 Channel channel = session.openChannel("sftp"); 2 channel.connect();