引:Java語言從誕生的那天起,就非常注重網絡編程方面的應用。隨着互聯網應用的飛速發展,Java的基礎類庫也不斷地對網絡相關的API進行加強和擴展。在Java SE 6 當中,圍繞着HTTP協議出現了很多實用的新特性
概述
Java語言從誕生的那天起,就非常注重網絡編程方面的應用。隨着互聯網應用的飛速發展,Java的基礎類庫也不斷地對網絡相關的API進行加強和擴展。在Java SE 6 當中,圍繞着HTTP協議出現了很多實用的新特性:NTLM認證提供了一種Window平臺下較爲安全的認證機制;JDK當中提供了一個輕量級的HTTP服務器;提供了較爲完善的HTTP Cookie管理功能;更爲實用的NetworkInterface;DNS域名的國際化支持等等。
NTLM 認證不可避免,網絡中有很多資源是被安全域保護起來的。訪問這些資源需要對用戶的身份進行認證。下面是一個簡單的例子:
import java.net.*;
import java.io.*; public class Test {public static void main(String[] args) throws Exception { URL url = new URL("http://PROTECTED.com"); URLConnection connection = url.openConnection(); InputStream in = connection.getInputStream(); byte[] data = new byte[1024]; while(in.read(data)>0) { //do something for data } in.close();}} |
當Java程序試圖從一個要求認證的網站讀取信息的時候,也就是說,從聯繫於http://Protected.com這個URLConnection的 InputStream中 read數據時,會引發FileNotFoundException。儘管筆者認爲,這個Exception的類型與實際錯誤發生的原因實在是相去甚遠;但這個錯誤確實是由網絡認證失敗所導致的。
要解決這個問題,有兩種方法:
其一,是給URLConnection設定一個“Authentication”屬性:
String credit = USERNAME + ":" + PASSWORD;
String encoding = new sun.misc.BASE64Encoder().encode (credit.getBytes());connection.setRequestProperty ("Authorization", "Basic " + encoding); |
這裏假設http://PROTECTED.COM使用了基本(Basic)認證類型。
從上面的例子,我們可以看出,設定Authentication屬性還是比較複雜的:用戶必須瞭解認證方式的細節,才能將用戶名/密碼以一定的規範給出,然後用特定的編碼方式加以編碼。Java類庫有沒有提供一個封裝了認證細節,只需要給出用戶名/密碼的工具呢?
這就是我們要介紹的另一種方法,使用java.net.Authentication類。
每當遇到網站需要認證的時候,HttpURLConnection都會向Authentication類詢問用戶名和密碼。
Authentication類不會知道究竟用戶應該使用哪個username/password那麼用戶如何向Authentication類提供自己的用戶名和密碼呢?
提供一個繼承於Authentication的類,實現getPasswordAuthentication方法,在PasswordAuthentication中給出用戶名和密碼:
class DefaultAuthenticator extends Authenticator {
public PasswordAuthentication getPasswordAuthentication () { return new PasswordAuthentication ("USER", "PASSWORD".toCharArray());}} |
然後,將它設爲默認的(全局)Authentication:
Authenticator.setDefault (new DefaultAuthenticator()); |
那麼,不同的網站需要不同的用戶名/密碼又怎麼辦呢?
Authentication提供了關於認證發起者的足夠多的信息,讓繼承類根據這些信息進行判斷,在getPasswordAuthentication方法中給出了不同的認證信息:
getRequestingHost()
getRequestingPort() getRequestingPrompt() getRequestingProtocol() getRequestingScheme() getRequestingURL() getRequestingSite() getRequestorType() |
另一件關於Authentication的重要問題是認證類型。不同的認證類型需要Authentication執行不同的協議。至Java SE 6.0爲止,Authentication支持的認證方式有:
HTTP Basic authentication
HTTP Digest authentication NTLM Http SPNEGO Negotiate Kerberos NTLM |
這裏我們着重介紹NTLM。
NTLM是 NT LAN Manager的縮寫。早期的SMB協議在網絡上明文傳輸口令,這是很不安全的。微軟隨後提出了WindowsNT挑戰/響應驗證機制,即NTLM。
NTLM協議是這樣的:
·客戶端首先將用戶的密碼加密成爲密碼散列;
·客戶端向服務器發送自己的用戶名,這個用戶名是用明文直接傳輸的;
·服務器產生一個16位的隨機數字發送給客戶端,作爲一個challenge(挑戰) ;
·客戶端用步驟1得到的密碼散列來加密這個challenge ,然後把這個返回給服務器;
·服務器把用戶名、給客戶端的challenge 、客戶端返回的response這三個東西,發送域控制器;
·域控制器用這個用戶名在SAM密碼管理庫中找到這個用戶的密碼散列,然後使用這個密碼散列來加密challenge;
·域控制器比較兩次加密的challenge ,如果一樣,那麼認證成功;
Java 6 以前的版本,是不支持NTLM認證的。用戶若想使用HttpConnection連接到一個使用有Windows域保護的網站時,是無法通過NTLM認證的。另一種方法,是用戶自己用Socket這樣的底層單元實現整個協議過程,這無疑是十分複雜的。
終於,Java 6 的Authentication類提供了對NTLM的支持。使用十分方便,就像其他的認證協議一樣:
class DefaultAuthenticator extends Authenticator {
private static String username = "username ";private static String domain = "domain ";private static String password = "password "; public PasswordAuthentication getPasswordAuthentication() { String usernamewithdomain = domain + "/ "+username; return (new PasswordAuthentication(usernamewithdomain, password.toCharArray()));}} |
這裏,根據Windows域賬戶的命名規範,賬戶名爲域名+”/”+域用戶名。如果不想每生成PasswordAuthentication時,每次添加域名,可以設定一個系統變量名“http.auth.ntlm.domain“。
Java 6 中Authentication的另一個特性是認證協商。目前的服務器一般同時提供幾種認證協議,根據客戶端的不同能力,協商出一種認證方式。比如,IIS服務器會同時提供NTLM with kerberos和 NTLM兩種認證方式,當客戶端不支持NTLM with kerberos時,執行NTLM認證。
目前,Authentication的默認協商次序是:
GSS/SPNEGO -> Digest -> NTLM -> Basic
那麼kerberos的位置究竟在哪裏呢?
事實上,GSS/SPNEGO以 JAAS爲基石,而後者實際上就是使用kerberos的。
輕量級 HTTP 服務器Java 6 還提供了一個輕量級的純Java Http服務器的實現。下面是一個簡單的例子:
public static void main(String[] args) throws Exception{
HttpServerProvider httpServerProvider = HttpServerProvider.provider(); InetSocketAddress addr = new InetSocketAddress(7778); HttpServer httpServer = httpServerProvider.createHttpServer(addr, 1) ;httpServer.createContext("/myapp/", new MyHttpHandler()); httpServer.setExecutor(null); httpServer.start(); System.out.println("started"); } static class MyHttpHandler implements HttpHandler{ public void handle(HttpExchange httpExchange) throws IOException { String response = "Hello world!"; httpExchange.sendResponseHeaders(200, response.length()); OutputStream out = httpExchange.getResponseBody(); out.write(response.getBytes()); out.close();} } |
然後,在瀏覽器中訪問http://localhost:7778/myapp/,我們得到:
圖一瀏覽器顯示 |
首先,HttpServer是從HttpProvider處得到的,這裏我們使用了JDK 6 提供的實現。用戶也可以自行實現一個HttpProvider和相應的HttpServer實現。
其次,HttpServer是有上下文(context)的概念的。比如,http://localhost:7778/myapp/中“/myapp/”就是相對於HttpServer Root的上下文。對於每個上下文,都有一個HttpHandler來接收http請求並給出回答。
最後,在HttpHandler給出具體回答之前,一般先要返回一個Http head。這裏使用HttpExchange.sendResponseHeaders(int code, int length)。其中code是 Http響應的返回值,比如那個著名的404。length指的是response的長度,以字節爲單位