2018年9月16日 星期日

OpenCV - Java - 臉部辨識

OpenCV 是一個是一個跨平台的電腦視覺庫,其可讓我們方便的做許多圖像方面的處理,
在這裡我參考了網路上的資料加上修改實測,展示一個簡單的OpenCV範例:

程式語言 : Java
需求:
調用電腦本身的攝影機,在UI上顯示即時截取的畫面,並在辨視出人臉的地方加上方框,並將人臉部份儲存至本地電腦上。

這裡下載OpenCV,我下載的是 3.4.1 的Win pack版本,下載後執行,它會解壓縮生成一個opencv資料,打開它後,
會看到所須的jar檔及dll檔:
opencv-341.jar
並且在x64和x86資料夾內有不同位元的windows dll檔:
opencv_java341.dll

以Eclipse來示範,除了加入opencv0341.jar到lib後,還要如下圖設定Native library location為dll檔所在的資料夾路徑

接下來是程式的部份
import java.io.ByteArrayInputStream;
import java.io.FileInputStream;
import java.util.Calendar;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

import org.opencv.core.Core;
import org.opencv.core.Mat;
import org.opencv.core.MatOfByte;
import org.opencv.core.MatOfRect;
import org.opencv.core.Point;
import org.opencv.core.Rect;
import org.opencv.core.Scalar;
import org.opencv.imgcodecs.Imgcodecs;
import org.opencv.imgproc.Imgproc;
import org.opencv.objdetect.CascadeClassifier;
import org.opencv.videoio.VideoCapture;
import org.opencv.videoio.Videoio;

import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class OpenCV_Test extends Application{

 //是否正在截取攝影機影像
    private boolean isStart = false;
 
    private VideoCapture videoCapture;
    private ScheduledExecutorService scheduledExecutorService;
 
    // JavaFX 元件
    private BorderPane rootNode;
    private VBox vbox;
    private ImageView imageView;
    private Button startStopBtn;
    String startText = "Start";
    String stopText = "Stop";
 
 public static void main(String[] args) {
  Application.launch(args);
 }

 @Override
 public void start(Stage primaryStage) throws Exception {
  
  startStopBtn.setOnAction(new EventHandler<ActionEvent>() {

   @Override
   public void handle(ActionEvent event) {
    if (!isStart) {
     startStopBtn.setText(stopText);
     videoCapture.open(0);
     videoCapture.set(Videoio.CAP_PROP_FRAME_WIDTH, 640);
     videoCapture.set(Videoio.CAP_PROP_FRAME_HEIGHT, 480);
     
     if (videoCapture.isOpened()) {
      isStart = true;
      
      //建立設定截取攝影機的thread
      scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
      scheduledExecutorService.scheduleAtFixedRate(new Runnable() {

       @Override
       public void run() {
        imageView.setImage(captureImage());
       }
       
      }, 0, 16, TimeUnit.MILLISECONDS);
     }else {
      System.err.println("Error: Can not open camera.");
     }
     
    }else {
     startStopBtn.setText(startText);
     isStart = false;
     
     scheduledExecutorService.shutdown();
     try {
      //確保scheduledExecutorService有正確關閉排程
      while(!scheduledExecutorService.awaitTermination(30, TimeUnit.MILLISECONDS)) {
       System.out.println("Not close thread yet");
      }
     } catch (InterruptedException e) {
      System.err.println(e.getMessage());
      e.printStackTrace();
     }
     
     videoCapture.release();
     imageView.setImage(null);
    }
   }
   
  });
  
  //UI畫面
  Scene scene = new Scene(rootNode, 800, 640);
        primaryStage.setTitle("OpenCV Test");
        primaryStage.setScene(scene);
        primaryStage.show();
 }

 //UI畫面設定
 @Override
 public void init() throws Exception {
  super.init();
  
  System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
  videoCapture = new VideoCapture();
  
  rootNode = new BorderPane();
  
  vbox = new VBox();
  vbox.setAlignment(Pos.CENTER);
  rootNode.setCenter(vbox);
  
  imageView = new ImageView();
  imageView.setFitWidth(640);
  imageView.setFitHeight(480);
  imageView.setPreserveRatio(true);
  
  startStopBtn = new Button(startText);
  vbox.getChildren().addAll(imageView, startStopBtn);
  
 }

 //程式關閉時關閉攝影機
 @Override
 public void stop() throws Exception {
  if (videoCapture.isOpened()) {
   videoCapture.release();
  }
  super.stop();
 }
 
 private Image captureImage() {
  Image capturedImage = null;
 
  if (videoCapture.isOpened()) {
   Mat mat = new Mat();
   videoCapture.read(mat);
   if (!mat.empty()) {
    MatOfByte buffer = new MatOfByte();
    //偵測人臉,截取人臉部份存圖,
    //並在要顯示在畫面上的影像用矩形標示人臉部份
    detectDrawAndSaveFace(mat);
          Imgcodecs.imencode(".png", mat, buffer);
          capturedImage = new Image(new ByteArrayInputStream(buffer.toArray()));
   }
  }   
        return capturedImage;
 }
 
 //臉部偵測
    private void detectDrawAndSaveFace(Mat image) {
     CascadeClassifier faceDetector = new CascadeClassifier();
        faceDetector.load("D:\\JavaLib\\opencv\\sources\\data\\haarcascades_cuda\\haarcascade_frontalface_alt2.xml");

        MatOfRect faceDetections = new MatOfRect();
        faceDetector.detectMultiScale(image, faceDetections);
        for (Rect rect : faceDetections.toArray())
        {
         //截detect到人臉的區域,將區域內的圖存檔
            Mat faceImage = new Mat(image, rect);
            String outputDir = "D:\\detectedFaces";
            String outputFilePath = outputDir + "\\detectedFace_" + Calendar.getInstance().getTimeInMillis() + ".png";
            Imgcodecs.imwrite(outputFilePath, faceImage);
            
            //在detect到人臉的區域上畫上矩形邊框,顯示在螢幕上的ImageView上
            Imgproc.rectangle(image, new Point(rect.x, rect.y), new Point(rect.x + rect.width, rect.y + rect.height), new Scalar(0, 255, 0));
        } 
    }
}


Opencv 3.4.1 Win pack 下載分享

參考資料:
  1. Java+opencv3.2.0之人脸检测
  2. [OpenCV] 人臉偵測 (Face Detection)
  3. [OpenCV] 人臉偵測2 (Face Detection)
  4. 利用Java调用OpenCV进行人脸识别OpenCV on Raspberry Pi - Using Java(7)- 使用 OpenCV 截取與顯示影片

2018年9月8日 星期六

Java 圖片旋轉 (Java Image Rotate)

這次要紀錄的是用Java進行圖片旋轉,
我們希望在將圖片旋轉後,能得到能夠剛好容納旋轉後圖片的新片大小,
例如以下的例子,上面的是原圖,下面的是順時鏡旋轉了60度的圖片,
可以看得出來,在此例中,旋轉後的圖其寬高比原圖還要大,原因是為了要容納旋轉後的圖,新圖片的寬高須要重新計算的關係。

在看程式碼之前,我們需要計算旋轉後的新寬高,
首先,例如有一個如下圖所示的,寬為w,高為h的圖片


下圖是旋轉了Θ角後的圖片,並附上了新寬(w')及新高(h')的推導

下圖也是旋轉了θ後的圖片,但是θ為負值,所以sin(θ) 為負值,

上面兩張圖只舉例了θ在-π/2 ~ π/2 的情況,但是在任何角度都是一樣的,都可以推出
w'= h |sin(θ)| + w |cos(θ)|
h' = h |cos(θ)| + w |sin(θ)|

因為Java繪圖會從左上角開始往右下繪圖的關係,在這邊我們稱繪圖區為畫布,
畫布的區域為座標原點右下區域的為置,即第四象限
有了新寬(w')高(h')了以後,我們還需要知道圖片被旋轉了以後,要如何平移才能將旋轉後的圖片放置到畫布中間。

旋轉且平移到畫布中的圖片應如下圖所示:


為了計算方便,我們採取以圖片中心點為旋轉中心的方式來推導,如下圖所示,
δh即δw為圖片以中心點旋轉後要平移的距離,可以看到推導後的結果為
δh = h'/2 - h/2
δw = w'/2 - w/2
經過了上述的推導後,我們得到了以下公式:
w'= h |sin(θ)| + w |cos(θ)|
h' = h |cos(θ)| + w |sin(θ)|
δh = h'/2 - h/2
δw = w'/2 - w/2

於是我們知道了旋轉圖片的程式邏輯:
1. 計算出新圖片的寬(w')高(h')
2.將圖片以中心點(w/2, h/2)為旋轉中心旋轉 θ
3.將圖片進行δw, δh 的平移使其位於第四象限的畫布中

最後,依照上述邏輯寫成的Java程式碼如下:
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;

public class ImageRotateTest {
 public static void main(String[] args) throws IOException {
  String inputFilePath = "D:\\inputImage.png";
  String outputFilePath = "D:\\outputRotatedImage.png";
  
  File inputImage = new File(inputFilePath);
  File outputImage = new File(outputFilePath);
  
  BufferedImage newBufferedImage = rotateImage(ImageIO.read(inputImage), 60);
  
  String outputImageExtension = outputFilePath.substring(outputFilePath.lastIndexOf(".") + 1);
  ImageIO.write(newBufferedImage, outputImageExtension, outputImage);
 }

 public static BufferedImage rotateImage(BufferedImage bufferedimage, int degree) {
  int w = bufferedimage.getWidth();
  int h = bufferedimage.getHeight();
  int type = bufferedimage.getColorModel().getTransparency();
  double radiusDegree = Math.toRadians(degree);

  double newH = Math.abs(h * Math.cos(radiusDegree)) + Math.abs(w * Math.sin(radiusDegree));
  double newW = Math.abs(h * Math.sin(radiusDegree)) + Math.abs(w * Math.cos(radiusDegree));

  double deltaX = (newW - w) / 2;
  double deltaY = (newH - h) / 2;
  
  BufferedImage img = new BufferedImage((int) newW, (int) newH, type);
  Graphics2D graphics2d = img.createGraphics();
  graphics2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);

  // 以下為矩陣相乘, [translate] * [rotate] * [image的x,y]
  // 所以行為為,先rotate,再translate
  graphics2d.translate(deltaX, deltaY);
  graphics2d.rotate(radiusDegree, w / 2, h / 2);  //以中心點旋轉
  graphics2d.drawImage(bufferedimage, 0, 0, null);

  graphics2d.dispose();

  return img;
 }
}


要注意的是Java進行圖形變換 (旋轉、平移、縮放等) 的方法矩陣變換,而連乘的矩陣是右邊的先做運算再輪到左邊,所先先旋轉再平移,在程式上的順序是
先寫 Graphics2D.translate() 再 Graphics2D.rotate()

原始碼下載:
ImageRotateTest.7z
旋轉圖片推導

參考資料:
  1. Black area using java 2D graphics rotation?
  2. 浅谈矩阵变换——Matrix
P.S. 
改使用ImageIcon的程式 (速度比較快)
ImageIcon imageIcon = new ImageIcon(fromSrc);
  File toImage = new File(toSrc);

        int w= imageIcon.getIconWidth();
        int h= imageIcon.getIconHeight();
        int imageType= BufferedImage.TYPE_INT_RGB;
        
        double radiusDegree = Math.toRadians(degree);
        //calculate new width/height of rotated image
        double newW = Math.abs(h * Math.sin(radiusDegree)) + Math.abs(w * Math.cos(radiusDegree));
        double newH = Math.abs(h * Math.cos(radiusDegree)) + Math.abs(w * Math.sin(radiusDegree));
        //calculate new width/height offset of rotated image
        double deltaX = (newW - w) / 2;
        double deltaY = (newH - h) / 2;
        
        BufferedImage img = new BufferedImage((int) newW, (int) newH, imageType);
        Graphics2D graphics2d= img.createGraphics();
        graphics2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
        
        //Image Transoform Matrix : �A [translate] * [rotate] * [x,y of Image]
        //rotate --> translate
        graphics2d.translate(deltaX, deltaY);
        graphics2d.rotate(radiusDegree, w/2, h/2);
        
        graphics2d.drawImage(imageIcon.getImage(), 0, 0, null);
        graphics2d.dispose();
        
        String outputfileExtension = toSrc.substring(toSrc.lastIndexOf(".") + 1);
        ImageIO.write(img, outputfileExtension, toImage);

2018年8月27日 星期一

得到圖片原始的真實大小, 檢查input file的upload image 尺寸 - javascript

在這個例子中,展示了如何使用javascript來讀取input type="file" 上傳的圖片尺寸大小

直接上程式:
html :
<input id="imageUploader" type="file" accept="image/*"/>
<br/>
<img id="imageDisplayer" style="width : 300px; height: auto;"/>
Javascript :
var imageUploader = document.getElementById("imageUploader");
imageUploader.addEventListener("change", function(){
 window.URL;
  var image = new Image();
  image.onload = function(){
   var width = image.naturalWidth | image.width;
    var height = image.naturalHeight | image.height;
   console.log("width: " + width + ", height: " + height);
  };
  var objectURL = window.URL.createObjectURL(this.files[0]);
  image.src = objectURL;
  document.getElementById("imageDisplayer").src = objectURL;
});
主要是例用了window.URL.createObjectURL() 來得到上傳圖片的暫時url,
並建立一個 Image 物件來接收此 url ,並在 onload callback 裡得到 width 和 height,
#imageUploader下面那個#imageDisplayer只是預覽用,可有可無。
其中 Image 物件的 naturalWidth 和 natural Height 為上傳圖片的原始尺寸,
但怕有的瀏覽器沒有支援,所以在沒有支援時使用舊的 width, height 屬性

以下為上例程式碼的 jsfiddle 版本

關於Windows NTLM 登入 憑證 Header 解析時要注意的地方 (Java 的 byte 轉 int)

NTLM 是 Windows 的一種登入方式,它會要求使用者打入帳號密碼登入,
此時瀏覽器會跳出一個登入的對話框,當使用者登入後,
可以經由解析Authorization得到使用者的username和domain,

以下是參考這篇文
NTLM Authentication in Java
寫出的 Java 程式碼,可以得到username和domain,
要注意的是,在 NTLM Authentication in Java 的程式碼中,其忽略了 username 和 doamin 的 length 或 offset, byte 轉成 int 時會變成負數的可能性,
而在我們的程式碼中對此做出了修正。

String auth = request.getHeader("Authorization");
String szUserName = "";
String szDomain = "";
StringBuffer szUserNameBuffer =  new StringBuffer();
StringBuffer szDomainBuffer =  new StringBuffer(); 
if (auth == null)
{
 response.setStatus(response.SC_UNAUTHORIZED);
 response.setHeader("WWW-Authenticate", "NTLM");
 response.flushBuffer();
 return;
}
if (auth.startsWith("NTLM "))
{
 byte[] msg = new sun.misc.BASE64Decoder().decodeBuffer(auth.substring(5));
 int off = 0, length, offset;
 if (msg[8] == 1)
 {
  byte z = 0;
  byte[] msg1 = {(byte)'N', (byte)'T', (byte)'L', (byte)'M', (byte)'S', (byte)'S', (byte)'P', z,(byte)2, z, z, z, z, z, z, z,(byte)40, z, z, z, (byte)1, (byte)130, z, z,z, (byte)2, (byte)2, (byte)2, z, z, z, z, z, z, z, z, z, z, z, z};
  response.setHeader("WWW-Authenticate", "NTLM " + new sun.misc.BASE64Encoder().encodeBuffer(msg1));
  response.sendError(response.SC_UNAUTHORIZED);
  return;
 }
 else if (msg[8] == 3)
 {
  off = 30;
  
  length = (msg[off+17]&0x0FF)*256 + (msg[off+16]&0x0FF);
  offset = (msg[off+19]&0x0FF)*256 + (msg[off+18]&0x0FF);
  String remoteHost = new String(msg, offset, length);
  
  length = (msg[off+1]&0x0FF)*256 + (msg[off]&0x0FF);
  offset = (msg[off+3]&0x0FF)*256 + (msg[off+2]&0x0FF);
  String domain = new String(msg, offset, length);
  
  length = (msg[off+9]&0x0FF)*256 + (msg[off+8]&0x0FF);
  offset = (msg[off+11]&0x0FF)*256 + (msg[off+10]&0x0FF);
  String username = new String(msg, offset, length);
  
  char[] ca = remoteHost.toCharArray();
  String szHost = "";
     for(int i = 0; i < ca.length; i = i + 2){
      szHost = szHost + (char)ca[i];
     }
  
  char[] ca2 = domain.toCharArray();
     for(int i = 0; i < ca2.length; i = i + 2){
      szDomain = szDomain + (char)ca2[i];
     }
     
     char[] ca3 = username.toCharArray();
     for(int i = 0; i < ca3.length; i = i + 2){
      szUserName = szUserName + (char)ca3[i];
     }
 }
}else if(auth.startsWith("Negotiate")){
 byte[] msg = new sun.misc.BASE64Decoder().decodeBuffer(auth.substring(10));
 int off = 0, length, offset;
 if (msg[8] == 1){
  byte z = 0;
  byte[] msg1 = {(byte)'N', (byte)'T', (byte)'L', (byte)'M', (byte)'S', (byte)'S', (byte)'P', 
    z,(byte)2, z, z, z, z, z, z, z,(byte)40, z, z, z, 
    (byte)1, (byte)130, z, z,z, (byte)2, (byte)2,
    (byte)2, z, z, z, z, z, z, z, z, z, z, z, z};
  response.setHeader("WWW-Authenticate", "NTLM " +  new sun.misc.BASE64Encoder().encodeBuffer(msg1));
  response.sendError(response.SC_UNAUTHORIZED);
     return;
 }else if (msg[8] == 3){
  off = 30;

  length = (msg[off+17]&0x0FF)*256 + (msg[off+16]&0x0FF);
  offset = (msg[off+19]&0x0FF)*256 + (msg[off+18]&0x0FF);
  // String remoteHost = new String(msg, offset, length);

  length = (msg[off+1]&0x0FF)*256 + (msg[off]&0x0FF);
  offset = (msg[off+3]&0x0FF)*256 + (msg[off+2]&0x0FF);
  szDomain = new String(msg, offset, length);

  length = (msg[off+9]&0x0FF)*256 + (msg[off+8]&0x0FF);
  offset = (msg[off+11]&0x0FF)*256 + (msg[off+10]&0x0FF);

  szUserName = new String(msg, offset, length , "UTF-8" );
 }
}

String charSet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ_".toLowerCase();
for(int i=0; i<szUserName.length();i++){
 if(charSet.contains(String.valueOf(szUserName.charAt(i)).toLowerCase())){  
  szUserNameBuffer.append(szUserName.charAt(i));
 }
}
for(int i=0; i<szDomain.length();i++){
 if(charSet.contains(String.valueOf(szDomain.charAt(i)).toLowerCase())){  
  szDomainBuffer.append(szDomain.charAt(i));
 }
}


修正的方式很簡單,就是將byte與 0x0FF 進行和(&) 運算
那麼為什麼會需要這樣的修正呢?
我們可以看看 NTLM Header 的 Schema,在 NTLM Authentication Scheme for HTT 中
有說明到
" byte is an 8-bit field; short is a 16-bit field. All fields are unsigned. Numbers are stored in little-endian order. "

以domain length 為例,表示了其是用 short 16-bit 來儲存,
並且是用 little-endian order,即後8-bit 為高位,
最後為unsigned的,也就是一定為正數,即使8-bit中的最高位為1也是一樣為正。

如果在 8-bit 中的最高位為0, byte 轉 int 是沒問題的,
但是如果最高位為1的話,直接轉就會出問題了,
例如: 144 (NTLM- unsigned 10進位)
1001 0000 (byte) 在轉成 int 時,因為 Java 看到最高位為1,誤以為是負數,且 int 為 32-bit,所以就在前面多出的24個位元補上了1,而變成

1111 1111 1111 1111 1111 1111 1001 0000 (int) ==> -112 (Java signed 10 進位)

這樣就出錯了,
很顯然,前面的24個位元應該都要為0才對,所以我們只要在對其與 0xFF進行和(&)運算,
將前面24個位元都變成0就行了。

0000 0000 0000 0000 0000 0000 1001 0000 (int) ==> 144 (NTLD unsigned 10 進位)

參考資料:

  1. NTLM Authentication Scheme for HTT
  2. NTLM Authentication in Java (此處程式碼不夠好,沒有考慮到本文錯誤取到負值的狀況)
  3. java中byte轉換int時為何與0xff進行與運算
  4. Base64



2018年8月4日 星期六

IIS httpd.ini處理url有帶?加參數的情況

IIS 的轉址設定檔httpd.ini中,可以對特定的Url轉換成另一種Url,但如果要被轉的Url是有帶參數的網址,我們想把參數加在轉換後的Url後面,要如何做呢?

在這裡介紹一個可用的方法

首先例如有一下轉換設定:

RewriteRule /url-(.*?).html     /url.jsp?id=$1

可以進行例如以下url的轉換
/url-123.html     ==>     /url.jsp?id=123

但是如果是有帶參數的url就無法成功匹配轉換規則了
/url-123.html?param=abc    ==>     ???

有兩種解法,

一種是多加一條規則去處理有帶參數的情況:
RewriteRule /url-(.*?).html?(.*?)     /url.jsp?id=$1&$2
這樣就可成功轉換,例如以下:
/url-123.html?param=abc     ==>     /url.jsp?id=123&param=abc
但是這樣需要設定兩條規則:
RewriteRule /url-(.*?).html     /url.jsp?id=$1
RewriteRule /url-(.*?).html?(.*?)     /url.jsp?id=$1&$2

另一種寫法只要設定一條規則就行了,規則如下:
RewriteRule /url-(.*?).html(\?.*?)?     /url.jsp(?2$2&:?)id=$1
此寫法可以做到以下url轉換:
/url-123.html     ==>     /url.jsp?id=123
/url-123.html?param=abc     ==>     /url.jsp?param=abc&id=123

接下來來講解第二種方法的原理,
其用到了類似很多程式都有的"三元運算子"規能,規則如下:
(?NtrueExpression:falseExpression)
當有抓到第N匹配群組時,執行trueExpression,沒有時則執行falseEpression,
所以
/url.jsp(?2$2&:?)id=$1
的意恩就是當有抓到2匹配群組時,執行"$2&",否則則執行"?",
而$2就是抓到的url參數部份
在這裡
/url-(.*?).html(\?.*?)?
中的(\?.*?)?   即是代表第2群組  (\?.*?)  出現的次數可為0次或1次,

所以當出現為1次時,就把抓到的參數(連同?),例如上例的 ?param=abc,
放到 /url.jsp的後面再加個 &,而id=$1就放在&的後面,

如果出現為0次時,就直接再 /url.jsp的後面加個 ? ,再把id=$1放在 ? 的後面

參考資料:

  1. ISAPI_Rewrite 2 documentation  的 "Conditional expressions" 部份
  2. Topic: Query String and IIS error msg

2018年3月31日 星期六

Java - HTTPS 檢查證書的安全連線


如何在JDK1.5中支援TLSv1.2這篇文章中,有講到如何對TLSv1.2的server進行HTTPS request,但這只是最簡單的實作例子,沒有考慮到憑證安全的檢查,如果被例如中間人攻擊的話,可能會連線請求中間被竄改了資料而不自知。

在前例中,可以看到在自訂的TrustManager 中,所有的method (getAcceptedIssuers(), checkServerTrusted(), checkClientTrusted())都沒有被實作內容,
如果發生了中間人攻擊,也就是在client端和server端的連線中間被某中間人攔截,
這時中間人可能會修改某些資料偽裝server端回傳資料給我們(client)端,
如果我們沒有對其憑證進行檢查的話,可能就無法察覺此攻擊了。

尤其是行動裝置的場合,例如手機在連到了駭客分享的wifi,程式如果不檢查HTTPS連線有無被劫持,就很容昜造成資安問題。

經過了查詢資料及自行實作後,我在這邊提供一個如何對 HTTPS 的 憑證進行驗證的方法。


  1. 為了簡單化,我使用JDK1.8,跟JDK1.5不同,JDK1.8支持TLSv1.2,
    所以不用跟上一篇文(如何在JDK1.5中支援TLSv1.2) 一樣使用BoncyCastle。
    並且把Excpetion全部throws出去,不用try-catch來使程式碼較為清晰。

    Note:
    其實JDK1.8在對TLSv1.2連線時,是不用自已建立TrustManager的,並且自己就會檢查憑證,並在檢查憑證有問題時拋出錯誤。
    但是要注意如果自己建立了TrustManager來用在連線中,卻不檢查憑證的話,在憑證有問題時是不會拋出錯誤的。
    而JDK1.5因為要使用BoncyCastle並會需要自已建立TrustManager,所以就要特別使用檢查憑證的TrustManager。
  2. 我們可以使用封包監測工具,例如Fiddler,來模擬中間人攻擊,如果我們把Java的HTTPS request 掛上Proxy,讓連線中間經過Fiddler的話,相當於就是Fiddler當了中間人,這時對使用了沒有進行憑證檢查的TrustManager的Java程式,是不會發現有任何異狀的。

步驟:

  1. 這裡要連的實驗對像跟之前一樣是號稱只支持TLSv1.2的fancyssl網站,首先去此網站上下載憑證。
    下載方式如下圖,按下F12打開開發者console後,選Security --> View Certificate --> 詳細資料 --> 複制到檔案。
  2. 使用java的工具 keytool 加入憑證到你要的憑證檔案中
    keytool -import -alias {別名} -keystore {要被放入憑證的憑證檔} -file {從網站上下載的憑證}
    例如:
    keytool -import -alias fancyssl -keystore D:\test_cer -file D:\fancyssl_cer.cer
    之後會要你打密碼,第一次產生憑證檔自己取。如果是本來就有的檔案通常密碼沒改就是預設 : changeit
  3. 接下來我展示在Java1.8下的三種寫法:
    1. HttpsTest_notSafe.java
      (自己建立的,沒有檢查憑證的TrustManager)
      import java.io.BufferedReader;
      import java.io.File;
      import java.io.FileInputStream;
      import java.io.IOException;
      import java.io.InputStream;
      import java.io.InputStreamReader;
      import java.net.HttpURLConnection;
      import java.net.MalformedURLException;
      import java.net.URL;
      import java.security.KeyManagementException;
      import java.security.KeyStore;
      import java.security.KeyStoreException;
      import java.security.NoSuchAlgorithmException;
      import java.security.NoSuchProviderException;
      import java.security.SecureRandom;
      import java.security.cert.Certificate;
      import java.security.cert.CertificateException;
      import java.security.cert.CertificateFactory;
      import java.security.cert.X509Certificate;
      
      import javax.net.ssl.HostnameVerifier;
      import javax.net.ssl.HttpsURLConnection;
      import javax.net.ssl.SSLContext;
      import javax.net.ssl.SSLSession;
      import javax.net.ssl.TrustManager;
      import javax.net.ssl.TrustManagerFactory;
      import javax.net.ssl.X509TrustManager;
      
      public class HttpsTest_notSafe {
      
       public static void main(String[] args)
         throws NoSuchAlgorithmException, KeyManagementException, MalformedURLException, IOException, NoSuchProviderException, KeyStoreException, CertificateException {
        // 將request導向 Fiddler (127.0.0.1:8888) 可模擬中間人攻擊
        System.setProperty("http.proxyHost", "127.0.0.1");
        System.setProperty("http.proxyPort", "8888");
        System.setProperty("https.proxyHost", "127.0.0.1");
        System.setProperty("https.proxyPort", "8888");
        //
        
        // 自訂的TrustManager,沒有檢查憑證,不安全
        TrustManager trustManager = new X509TrustManager() {
         public X509Certificate[] getAcceptedIssuers() {
          return new X509Certificate[0];
         }
         public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
          // do nothing
         }
         public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
          // do nothing
         }
        };
      
        SSLContext sslContext = SSLContext.getInstance("TLSv1.2");
        // 自訂的TrustManager,沒有檢查憑證,不安全
        sslContext.init(null, new TrustManager[] { trustManager }, new SecureRandom());
        HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.getSocketFactory());
        
        HttpsURLConnection httpsUrlConnection = (HttpsURLConnection) (new URL("https://fancynossl.hboeck.de/")).openConnection();
        httpsUrlConnection.connect();
      
        // 印出Response
        printFromInputStream(httpsUrlConnection.getInputStream());
        
        httpsUrlConnection.disconnect();
        
       }
       
       static void printFromInputStream(InputStream in) throws IOException {
        BufferedReader responseBufferedReader = new BufferedReader((new InputStreamReader(in)));
        StringBuffer responseTextStringBuffer = new StringBuffer();
        String tempString = null;
        while ((tempString = responseBufferedReader.readLine()) != null) {
         responseTextStringBuffer.append(tempString + "\n");
        }
        String responseText = responseTextStringBuffer.toString();
        System.out.println(responseText);
       }
      }
    2. HttpsTest_safe1.java
      (JDK1.8 直接使用自帶TLSv1.2的https支持,不用建立TrustManager)
      import java.io.BufferedReader;
      import java.io.File;
      import java.io.FileInputStream;
      import java.io.IOException;
      import java.io.InputStream;
      import java.io.InputStreamReader;
      import java.net.HttpURLConnection;
      import java.net.MalformedURLException;
      import java.net.URL;
      import java.security.KeyManagementException;
      import java.security.KeyStore;
      import java.security.KeyStoreException;
      import java.security.NoSuchAlgorithmException;
      import java.security.NoSuchProviderException;
      import java.security.SecureRandom;
      import java.security.cert.Certificate;
      import java.security.cert.CertificateException;
      import java.security.cert.CertificateFactory;
      import java.security.cert.X509Certificate;
      
      import javax.net.ssl.HostnameVerifier;
      import javax.net.ssl.HttpsURLConnection;
      import javax.net.ssl.SSLContext;
      import javax.net.ssl.SSLSession;
      import javax.net.ssl.TrustManager;
      import javax.net.ssl.TrustManagerFactory;
      import javax.net.ssl.X509TrustManager;
      
      public class HttpsTest_safe1 {
      
       public static void main(String[] args) throws NoSuchAlgorithmException, KeyManagementException, MalformedURLException, IOException, NoSuchProviderException, KeyStoreException, CertificateException {
        // 將request導向 Fiddler (127.0.0.1:8888) 可模擬中間人攻擊
        System.setProperty("http.proxyHost", "127.0.0.1");
        System.setProperty("http.proxyPort", "8888");
        System.setProperty("https.proxyHost", "127.0.0.1");
        System.setProperty("https.proxyPort", "8888");
        //
      
        //建立連線
        HttpsURLConnection httpsUrlConnection = (HttpsURLConnection) (new URL("https://fancynossl.hboeck.de/")).openConnection();
        httpsUrlConnection.connect();
      
        // 印出Response
        printFromInputStream(httpsUrlConnection.getInputStream());
        
        httpsUrlConnection.disconnect();  
       }
       
       static void printFromInputStream(InputStream in) throws IOException {
        BufferedReader responseBufferedReader = new BufferedReader((new InputStreamReader(in)));
        StringBuffer responseTextStringBuffer = new StringBuffer();
        String tempString = null;
        while ((tempString = responseBufferedReader.readLine()) != null) {
         responseTextStringBuffer.append(tempString + "\n");
        }
        String responseText = responseTextStringBuffer.toString();
        System.out.println(responseText);
       }
      }
    3. HttpsTest_safe2.java
      (自已建立的,使用憑證檔產生出TrustManager)
      import java.io.BufferedReader;
      import java.io.File;
      import java.io.FileInputStream;
      import java.io.IOException;
      import java.io.InputStream;
      import java.io.InputStreamReader;
      import java.net.HttpURLConnection;
      import java.net.MalformedURLException;
      import java.net.URL;
      import java.security.KeyManagementException;
      import java.security.KeyStore;
      import java.security.KeyStoreException;
      import java.security.NoSuchAlgorithmException;
      import java.security.NoSuchProviderException;
      import java.security.SecureRandom;
      import java.security.cert.Certificate;
      import java.security.cert.CertificateException;
      import java.security.cert.CertificateFactory;
      import java.security.cert.X509Certificate;
      
      import javax.net.ssl.HostnameVerifier;
      import javax.net.ssl.HttpsURLConnection;
      import javax.net.ssl.SSLContext;
      import javax.net.ssl.SSLSession;
      import javax.net.ssl.TrustManager;
      import javax.net.ssl.TrustManagerFactory;
      import javax.net.ssl.X509TrustManager;
      
      public class HttpsTest_safe2 {
      
       public static void main(String[] args)
         throws NoSuchAlgorithmException, KeyManagementException, MalformedURLException, IOException, NoSuchProviderException, KeyStoreException, CertificateException {
        // 將request導向 Fiddler (127.0.0.1:8888) 可模擬中間人攻擊
        System.setProperty("http.proxyHost", "127.0.0.1");
        System.setProperty("http.proxyPort", "8888");
        System.setProperty("https.proxyHost", "127.0.0.1");
        System.setProperty("https.proxyPort", "8888");
        //  
        
              //從憑證產生TrustManager
              String password = "changeit";  //預設密碼為changeit
              //讀取憑證
              File file = new File("D:/test_cer");
              //通常會將憑證都加到%Java_Home%/lib/security/cacerts/中
              //File file = new File(System.getProperty("java.home") + "/lib/security/cacerts");
              InputStream in = new FileInputStream(file);
              KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
              keyStore.load(in, password.toCharArray());
              in.close();
              //建立TrustManager
              TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance("SunX509", "SunJSSE");
              trustManagerFactory.init(keyStore);
      
        SSLContext sslContext = SSLContext.getInstance("TLSv1.2");
        sslContext.init(null, trustManagerFactory.getTrustManagers(), new SecureRandom());
        HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.getSocketFactory());
      
        HttpsURLConnection httpsUrlConnection = (HttpsURLConnection) (new URL("https://fancynossl.hboeck.de/")).openConnection();
        httpsUrlConnection.connect();
      
        // 印出Response
        printFromInputStream(httpsUrlConnection.getInputStream());
        
        httpsUrlConnection.disconnect();
        
       }
       
       static void printFromInputStream(InputStream in) throws IOException {
        BufferedReader responseBufferedReader = new BufferedReader((new InputStreamReader(in)));
        StringBuffer responseTextStringBuffer = new StringBuffer();
        String tempString = null;
        while ((tempString = responseBufferedReader.readLine()) != null) {
         responseTextStringBuffer.append(tempString + "\n");
        }
        String responseText = responseTextStringBuffer.toString();
        System.out.println(responseText);
       }
      }
我們可以打開 Fiddler,並在 Tools --> Options --> HTTPS,勾選 "Decrypt HTTPS traffic 設定攔截 HTTPS request 模擬中間人,
並且在上述的三個Java程式中,對設定Proxy的程式碼註解或不註解來測試。
可以發現以下結果:
有拋出錯誤沒有拋出錯誤有拋出錯誤沒有拋出錯誤
有經過Fiddler沒有經過Fiddler
HttpsTest_notSafe.java沒有拋出錯誤沒有拋出錯誤
HttpsTest_safe1.java有拋出錯誤沒有拋出錯誤
HttpsTest_safe2.java有拋出錯誤沒有拋出錯誤

可以看到,如果是JDK1.8如果使用了自帶TLSv1.2的支持寫法,即不自己建立TrustManager的話,程式是可以自行檢查出憑證錯誤的。

但如果使用了自已建立的TrustManager時,就要使用有檢查憑證的TrustManager才行,此篇文章使用的是由憑證檔產生TrustManager。
除了由憑證檔產生TrustManager以外,也可由自己撰寫檢查憑證相關的TrustManager method ((getAcceptedIssuers(), checkServerTrusted(), checkClientTrusted())) 。

原始碼下載:
HttpsTest.7z

參考資料
  1. Java 使用自签证书访问https站点
  2. 苹果核 - Android App 安全的HTTPS 通信
  3. Sun Java System Application Server Enterprise Edition 8.2 管理指南
  4. Java Keytool的使用及申請憑證(以Microsoft Active Directory Certificate Services為例)
  5. java中 SSL认证和keystore使用

Java使用Selenium進行瀏覽器自動化操作

Selenium是一個瀏覽器自動化的工具,可以使用它來控制瀏覽器執行各種操作,
在這篇文中我簡單介紹JAVA如何使用Selenium及展示一個範例。

前置步驟:
  1. 下載 Selenium 的 Java Lib
    到Selenium的下載頁面下載
    1. Selenium Client & WebDriver Language Bindings
    2. Selenium Standalone Server (我下載的為3.11.0)
  2. 下載各瀏覽器的WebDriver
    1. Chrome : ChromeDriver
    2. Firefox : geckodriver
  3. 在Java程式中引入Library和指定WebDriver就可以開始控制瀏覽器了。
在這邊展示一個簡單的程式,控制瀏覽器進到Google首頁,在搜尋框中打上關鍵字,然後將10頁的查詢結果Title和Link url印出來。

import java.util.List;
import java.util.concurrent.TimeUnit;

import org.openqa.selenium.By;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.firefox.FirefoxDriver;

public class SeleniumTest1 {

 public static void main(String[] args) {
  System.out.println("Start");
  //自己選擇要用的瀏覽器 WebDriver
  //用Chrome
  System.setProperty("webdriver.chrome.driver","D:\\JavaLib\\Selenium\\webdrivers\\Chrome\\chromedriver.exe");
  WebDriver driver = new ChromeDriver();
  
  //用Firefox
  //System.setProperty("webdriver.gecko.driver","D:\\javaLib\\selenium-java-3.10.0\\Firefox\\geckodriver.exe");
  //WebDriver driver = new FirefoxDriver();
  
  //使用implicitlyWait,抓取DOM時,會等DOM出現才抓,最多等10秒
  driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS);
  driver.get("https://www.google.com.tw/"); //開啟瀏覽器到 Google 首頁
  
  //抓取DOM element,#lst-ib 為Google搜尋框
  WebElement searchInput = driver.findElement(By.id("lst-ib"));
  
  //執行Javascript範例
  //將Google搜尋框打上字,"keyword"
  JavascriptExecutor javascriptExecutor = (JavascriptExecutor) driver;
  javascriptExecutor.executeScript("arguments[0].value='keyword';", searchInput);
  
  //用WebElement物件直接操做DOM element範例
  //抓取DOM element,name=btnK 為Google搜尋按鈕
  WebElement searchBtn = driver.findElement(By.name("btnK"));
  searchBtn.click();
  
  //印出十頁的所有搜尋結果Title和Link url
  for (int i = 0; i < 10; i++) {
   //抓取DOM elements, (.r a) 為Google搜尋結果的link
   List<WebElement> searchReultATagList = driver.findElements(By.cssSelector(".r a"));
   for (WebElement searchReultATag : searchReultATagList) {
    System.out.println(searchReultATag.getText() + " : ");
    System.out.println(searchReultATag.getAttribute("href"));
    System.out.println("=======================");
   }
   //抓取DOM element, #pnnext 為Google搜尋下一頁按鈕
   WebElement nextPageBtn = driver.findElement(By.id("pnnext"));
   nextPageBtn.click();
  }
  
  driver.quit(); //關閉瀏覽器
 }

}



sss
參考資料

  1. Selenium using Java - The path to the driver executable must be set by the webdriver.gecko.driver system property
  2. How to get selenium to wait for ajax response?
  3. Selenium驱动火狐、IE、Edge和Chrome浏览器的方法

2018年3月20日 星期二

使用代理 + fiddler 來監看 java 的 http/https 封包 / 模擬中間人攻擊

在 Java 中進行如下 propxy 的設置,將網路請求都先經過本地端的 fiddler (這裡fiddler的port為8888),就可以在fiddler中監看 java 發出的封包

System.setProperty("http.proxyHost", "127.0.0.1");
System.setProperty("http.proxyPort", "8888");

System.setProperty("https.proxyHost", "127.0.0.1");
System.setProperty("https.proxyPort", "8888");

而此時也可以模擬中間人攻擊,因為http/https請求經過了Fiddler,可以在此情況下測試自己的防範中間人攻擊的程式

或是使用 org.apache.commons.httpclient.HttpClient 的話,用
HttpClient client = new HttpClient();
client.getHostConfiguration().setProxy("127.0.0.1", 8888);


在 Fiddler Scripit 裡的
static function OnBeforeRequest(oSession: Session) { }
裡寫
if(oSession.HostnameIs("xxx.com") && oSession.isHTTPS){
            oSession.oRequest.headers.UriScheme = "http";
            oSession.hostname="127.0.0.1";
}

可以把 xxx.com 的 HTTPS request 轉向導到 127.0.0.1 HTTP request

其他例子:
if(oSession.host == "forum.cyberlink.com"){
            oSession.oRequest.headers.UriScheme = "http";
            oSession.host="127.0.0.1:8081";
}


如何在JDK1.5中支援TLSv1.2

使用Java 傳 HTTPS request 時,如果接收端的server (例如別家公司開放的web api)是只使用 TLSv1.2 時,可能會因為舊版的 jdk 沒有支援,而造成連線錯誤。

如果是使用 JDK1.8,其支持 TLSv1.2,並且且預設就使用 TLSv1.2 來進行 HTTPS 的連線,
寫法可以參考之前寫的這篇文章 "以Java 由Url下載圖片" 裡面的 "getHttpURLConnectionFromHttps()" 方法來進行 HTTPS 的請求。

但是例如如果為較舊的 JDK1.5,則沒有支持 TLSv1.2,此時還用 JDK1.8的寫法去請求 TLSv1.2的server時,就會產生錯誤。

在這篇文章中,我紀錄了我找到的方法,如何讓 JDK1.5 也能進行對 TLSv1.2 server的請求。

=================================

我使用的JDK為 jdk1.5.0_22
測試的網站為 https://fancyssl.hboeck.de/
此網站聲稱只為TLSv1.2的request開放
可以使用線上SSL檢測工具來查看此網站的SSL相關設定
https://www.ssllabs.com/ssltest/analyze.html?d=fancyssl.hboeck.de

需要的 jar

  1. Bouncy Castle 的 Library : 
    1. Provider 
    2. DTLS/TLS API/JSSE Provider
  2. 解開Java密鑰長度限制的
    "Java Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy Files"
    1. JDK 1.5 可到這裡下載 "Java Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy Files 5.0"。


步驟:

  1. 導入Bouncy Castle的 jar
    Bouncy Castle 是一個提供許多密碼演法等的 Java Library,而 TLSv1.2 跟 舊版的差別其中就有密碼演算等不同,其中更深的原理我沒有鑽研,在此只展示找到的可行解決方案。

    首先先到 Bouncy Castle官網 下載 Provider 及 DTLS/TLS API/JSSE Provider 的 jar 檔,我這邊選擇的為 bcprov-jdk15on-159.jarbctls-jdk15on-159.jar。 請下載並加到 Java 專案的 Library中。
  2. 測試程式進對指定的url進行TLSv1.2 HTTPS 請求,並將回應印出來,在這裡會將整個網頁的源始碼印出來。

    測試程式碼如下:
    import java.io.BufferedReader;
    import java.io.IOException;
    import java.io.InputStreamReader;
    import java.io.OutputStream;
    import java.net.HttpURLConnection;
    import java.net.URL;
    import java.security.SecureRandom;
    import java.security.Security;
    import java.security.cert.CertificateException;
    import java.security.cert.X509Certificate;
    
    import javax.net.ssl.HostnameVerifier;
    import javax.net.ssl.HttpsURLConnection;
    import javax.net.ssl.SSLContext;
    import javax.net.ssl.SSLSession;
    import javax.net.ssl.TrustManager;
    import javax.net.ssl.X509TrustManager;
    
    import org.bouncycastle.jce.provider.BouncyCastleProvider;
    import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider;
    
    public class HttpsClientRequestTest {
    
        public static void main(String[] args)
                throws Exception
            {
                Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME);
                Security.insertProviderAt(new BouncyCastleProvider(), 1);
    
                Security.removeProvider(BouncyCastleJsseProvider.PROVIDER_NAME);
                Security.insertProviderAt(new BouncyCastleJsseProvider(), 2);
    
                /*
                 * TEST CODE ONLY. If writing your own code based on this test case, you should configure
                 * your trust manager(s) using a proper TrustManagerFactory, or else the server will be
                 * completely unauthenticated.
                 */
                TrustManager tm = new X509TrustManager()
                {
                    public X509Certificate[] getAcceptedIssuers()
                    {
                        return new X509Certificate[0];
                    }
    
                    public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException
                    {
                     //do nothing
                    }
    
                    public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException
                    {
                     //do nothing
                    }
                };
                
                HostnameVerifier customHostnameVerifier = new HostnameVerifier() {
                     public boolean verify(String arg0, SSLSession arg1) {        
                          return true;
                     }
                };
    
                SSLContext sslContext = SSLContext.getInstance("TLSv1.2", BouncyCastleJsseProvider.PROVIDER_NAME);
                sslContext.init(null, new TrustManager[]{ tm }, new SecureRandom());
                HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.getSocketFactory());
                HttpsURLConnection.setDefaultHostnameVerifier(customHostnameVerifier);
                
                HttpURLConnection httpUrlConnection = (HttpURLConnection) (new URL("https://fancynossl.hboeck.de")).openConnection();
                
                httpUrlConnection.setRequestProperty("User-Agent", "Mozilla/5.0 (Linux; Android 4.2.1; Nexus 7 Build/JOP40D) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.166  Safari/535.19");
                httpUrlConnection.connect();
    
                InputStream is = null;
         if (httpUrlConnection.getResponseCode() >= 400) {  
             is = httpUrlConnection.getErrorStream();  
         } else {  
             is = httpUrlConnection.getInputStream();  
         }
          
                BufferedReader responseBufferedReader = new BufferedReader((new InputStreamReader(is)));
                StringBuffer responseTextStringBuffer = new StringBuffer();
                String tempString = null;
                while((tempString = responseBufferedReader.readLine()) != null) {
                    responseTextStringBuffer.append(tempString + "\n");
                }
                String responseText = responseTextStringBuffer.toString();
                httpUrlConnection.disconnect();
          
                System.out.println(responseText);
            }
    }
    
  3. 因為還沒有開放 Java 的密鑰長度限制,所以應該會出現以下錯誤java.security.InvalidKeyException: Illegal key sizeException
    此時將下載的 "Java Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy Files 5.0" 裡面的 "local_policy.jar" 和 "US_export_policy.jar" 覆蓋到JAVA_HOME\jre\lib\security\ 之下的 "local_policy.jar" 和 "US_export_policy.jar" ,再執行一次,應該就會成功了。
    下載頁面如圖所示:
    程式成功時輸出如圖,可以看到網頁的html源始碼被印了出來 (Eclipse):



參考資料:
  1. java.lang.IllegalArgumentException: TLSv1.2 on JRE 1.5
  2. The Legion of the Bouncy Castle
  3. bcgit/bc-java
  4. 解决java.io.IOException: HTTPS hostname wrong: should be
  5. AES加密时抛出java.security.InvalidKeyException: Illegal key size or default parameters