URLs,URIs,Proxies和Passwords 解析

類URLEncoder 和 類URLDecoder

web設計者面臨的衆多難題之一便是怎樣處理不同操作系統間的差異性。這些差異性能引起URL方面的問題:例如,一些操作系統允許文件名中含有空格符,有些又不允許。大多數操作系統不會認爲文件名中含有符號“#”會有什麼特殊含義;但是在一個URL中,符號“#”表示該文件名已經結束,後面會緊跟一個fragment(部分)標識符。其他的特殊字符,非字母數字字符集,它們在URL或另一個操作系統上都有其特殊的含義,表述着相似的問題。爲了解決這些問題,我們在URL中使用的字符就必須是一個ASCII字符集的固定字集中的元素,具體如下:
1.大寫字母A-Z
2.小寫字母a-z
3.數字 0-9
4.標點符 - _ . ! ~ * ' (和 ,)

諸如字符: / & ? @ # ; $ + = 和 %也可以被使用,但是它們各有其特殊的用途,如果一個文件名包括了這些字符( / & ? @ # ; $ + = %),這些字符和所有其他字符就應該被編碼。

編碼過程非常簡單,任何字符只要不是ASCII碼數字,字母,或者前面提到的標點符,它們都將被轉換成字節形式,每個字節都寫成這種形式:一個“%”

後面跟着兩位16進制的數值。空格是一個特殊情況,因爲它們太平常了。它除了被編碼成“%20”以外,還能編碼爲一個“+”。加號(+)本身被編碼爲%2B。當/ # = & 和?作爲名字的一部分來使用時,而不是作爲URL部分之間的分隔符來使用時,它們都應該被編碼。

WARNING這種策略在存在大量字符集的異構環境中效果不甚理想。例如:在U.S. Windows 系統中, é 被編碼爲 %E9. 在 U.S. Mac中被編碼爲%8E。這種不確定性的存在是現存的URI的一個明顯的不足。所以在將來URI的規範當中應該通過國際資源標識符(IRIs)進行改善。

類URL並不自動執行編碼或解碼工作。你能生成一個URL對象,它可以包括非法的ASCII和非ASCII字符和/或%xx。當用方法getPath() 和toExternalForm( ) 作爲輸出方法時,這種字符和轉移符不會自動編碼或解碼。你應對被用來生成一個URL對象的字符串繁榮對象負責,確保所有字符都會被恰當地編碼。


幸運的是,java提供了一個類URLEncoder把string編碼成這種形式。Java1.2增加了一個類URLDecoder它能以這種形式解碼string。這兩個類都不用初始化




URLEncoder

在java1.3和早期版本中,類java.net.URLEncoder包括一個簡單的靜態方法encode( ) 它對string以如下規則進行編碼:
public static String encode(String s)
這個方法總是用它所在平臺的默認編碼形式,所以在不同系統上,它就會產生不同的結果。結果java1.4中,這個方法被另一種方法取代了。該方法要求你自己指定編碼形式:




兩種關於編碼的方法,都把任何非字母數字字符轉換成%xx(除了空格,下劃線(_),連字符(—),句號(。),和星號(*))。
兩者也都編碼所以的非ASCII字符。空格被轉換成一個加號。這些方法有一點過分累贅了;它們—也把“~”,“‘”,“()”轉換成%xx,即使它們完全用不着這樣做。儘管這樣,但是這種轉換並沒被URL規範所禁止。所以web瀏覽器會自然地處理這些被過分編碼後的URL。

兩中關於編碼的方法都返回一個新的被編碼後的string,java1.3的方法encode( ) 使用了平臺的默認編碼形式,得到%xx。這些編碼形式典型的有:在 U.S. Unix 系統上的ISO-8859-1, 在U.S. Windows 系統上的Cp1252,在U.S. Macs上的MacRoman,和其他本地字符集等。因爲編碼解碼過程都是與本地操作平臺相關的,所以這些方法是另人不爽的,不能跨平臺的。這就明確地回答了爲什麼在java1.4中這種方法被拋棄了,轉而投向了要求以自己指定編碼形式的這種方法。儘管如此,如果你執意要使用所在平臺的默認編碼形式,你的程序將會像在java1.3中的程序一樣,是本地平臺相關的。在另一種編碼的方法中,你應該總是事業UTF-8,而不是其他什麼。UTF-8比起你選的其他的編碼形式來說,它能與新的web瀏覽器和更多的其他軟件相兼容。

例子7-8是使用URLEncoder.encode( ) 來打印輸出各種被編碼後的string。它需要在java1.4或更新的版本中編譯和運行。
Example 7-8. x-www-form-urlencoded strings




下面就是它的輸出。需要注意的是這些代碼應該以其他編碼形式被保存而不是以ASCII碼的形式,還有就是你選擇的編碼形式應該作爲一個參數傳給編譯器,讓編譯器能據此對源代碼中的非ASCII字符作出正確的解釋。




特別需要注意的是這個方法編碼了符號,“/”
,&,=,和:。它不會嘗試着去規定在一個URL中這些字符怎樣被使用。由此,所以你不得不分塊編譯你的URL,而不是把整個URL一次傳給這個方法。這是很重要的,因爲對類URLEncoder最通常的用法就是查詢string,爲了和服務器端使用GET方法的程序進行交互。例如,假設你想編碼這個查詢sting,它用來搜索AltaVista網站:




這段代碼對其進行編碼:




不幸的是,得到的輸出是:




出現這個問題就是方法URLEncoder.encode( ) 在進行盲目地編碼。它不能區分在URL或者查詢string中被用到的特殊字符(象前面string中的“=”,和“&”)和確實需要被編碼的字符。由此,所以URL需要像下面這樣一次只編碼一塊:




這纔是你真正想得到的輸出:




例子7-9是一個QueryString 類。在一個java對象中,它使用了類URLEncoder 來編碼連續的屬性名和屬性值對,這個java對象全被用來發送數據到服務器端的程序。當你在創建一個QueryString 對象時,你可以把查詢string中的第一個屬性對傳遞給類QueryString 的構造函數,得到初始string 。如果要繼續加入後面的屬性對,就應調用方法add(),它也能接受兩個string作爲參數,能對它們進行編碼。方法getQuery( ) 返回一個屬性對被逐個編碼後得到的整個string。

Example 7-9. -The QueryString class




利用這個類,現在我們就能對前面那個例子中的string進行編碼了:




URLDecoder
與URLEncoder 類相對應的URLDecoder 類有兩種靜態方法。它們解碼以x-www-form-url-encoded這種形式編碼的string。也就是說,它們把所有的加號(+)轉換成空格符,把所有的%xx分別轉換成與之相對應的字符:




第一種解碼方法在java1.3和java1.2中使用。第二種解碼方法在java1.4和更新的版本中使用。如果你拿不定主意用哪種編碼方式,那就選擇UTF-8吧。它比其他任何的編碼形式更有可能得到正確的結果。

如果string包含了一個“%”,但緊跟其後的不是兩位16進制的數或者被解碼成非法序列,該方法就會拋出IllegalArgumentException 異常。當下次再出現這種情況時,它可能就不會被拋出了。這是與運行環境相關的,當檢查到有非法序列拋不拋出IllegalArgumentException 異常,這時到底會發生什麼是不確定的。在Sun's JDK 1.4中,不會拋出什麼異常,它會把一些莫名其妙的字節加進不能被順利編碼的string中。這的確令人頭疼,可能就是一個安全漏洞。

由於這個方法沒有觸及到非轉義字符,所以你可以把整個URL作爲參數傳給該方法,不用像之前那樣分塊進行。例如:




The URI Class

URI是URL的一個抽象,它不僅包括了統一資源定位符(URL),還包括了統一資源名(URN).大多數實際應用中使用的URI都是URL,但是許多規範和標準像XML都是用URI來定義的.在java1.4和更新的版本中, URI被java.net.URI 類所表示.這個類與java.net.URL 相比有如下3點重要的區別:
·        URI 類只關心資源的標識和對URI的解析.它沒有方法來檢索它的URI所標識的資源。
·        URI 類與URL 類相比,它更能適應相關的規範。
·        一個URI 對象能表示一個相對URI 。URL 類在存放之前,就已經對所有的URI進行了“絕對化”的處理。

簡而言之,一個URL 對象就是網絡應用層協議進行網絡檢索的一個代理,而一個URI 對象就只純粹地做string的解析和操作的工作。URI 類沒有進行網絡檢索的能力。URL 類有一些進行string解析的方法。比如getFile( ) 和 getRef( ) 方法,但很多都是蹩腳的方法,總是不完全像有關的規範上所說的那樣好用。假如你現在用的是java1.4版本或更新的版本,這時你就可以做出選擇,如果你想下載一個URL指示的內容時,你應該使用 URL類;如果你想使用URI類來進行標識的工作而不是用來檢索的時候, 你應該用URI類。例如,去標識一個XML namespace 的URI。在一些情況下,當你同時需要實現這兩種功能時,你可以用方法toURL( ) 把一個URI 轉換成一個 URL 。在java1.5中,你還能用類URL 中的方法toURI( )  把一個URL 轉換成一個URI 。

構造一個 URI
URI是由string構成的。不像類URL ,類URI

不依靠底層協議的處理程序。只要URI是語義上正確的,java就不需要知道它的什麼協議,然後才創建其代表的URI對象。因此,不象類URL ,類URI 可以被用到新的試驗性的URI協議中去。

public URI(String uri) throws URISyntaxException

這是一個簡單的構造函數,它用一個合適的string創建了一個新的URI 對象。例如:




如果參數string不符合URI的語法規則—比如,如果它以“:”開始—構造函數就會拋出URISyntaxException 異常。這是一個需要被檢察的異常,所以你要麼捕獲該異常,要麼在構造函數會被執行的方法中,聲明這個方法會拋出該異常。儘管如此,但是有一條語法規則是不能被檢察到的。與URI規範相矛盾的是,在URI中使用的字符不會被限制在ASCII字符集上。它們可以包含其它的Unicode字符,比如ø 和 é。從語義上講,對URI幾乎就沒有什麼限制,特別是一旦不需要對非ASCII字符進行編碼時,一切URI都是被允許的。幾乎任何的string都可以被看成是URI。

public URI(String scheme, String schemeSpecificPart, String fragment) throws URISyntaxException

這個構造函數主要是用在不存在有層次關係的URI中。Scheme是指URI的協議,比如http, urn, tel等。它必須專門有ASCII字母,數字和標點符(+, -, 和.)組成。第一個位置上必須是一個字母。如果省略了參數scheme,就用null代替。這時創建的是一個相對URI。例如:




scheme-specific部分的內容遵從於參數scheme給出的URI協議;對一個http 協議的URL來說,是一種形式,對於mail協議的 URL來說又是另一種形式,對於tel協議的 URI又是其它的什麼形式。因爲類URI 會以%xx對非法字符進行編碼,所以顯而易見,你不會在這步犯語法方面的錯誤。最後,只要有需要的話,第三個參數可以是指一個部分標識符。同樣地,在這個部分標識符中,字符的合法性也順理成章地不需要檢察。如果省略了參數fragment,就用null代替。

public URI(String scheme, String host, String path, String fragment) throws URISyntaxException

這個構造函數被用在有層次關係的URI中,比如http 和 ftp 協議的URL。主機名和路徑名(用“/”分開)一起構成URI的scheme-specific部分。例如:




如果構造函數從得到的幾個參數中不能獲得一個有合法層次關係的URI—例如,假如給定了參數scheme,它要求這個URI是一個絕對的URI可是path參數卻又不是一“/”開始的—它就會拋出異常URISyntaxException

Public URI(String scheme, String authority, String path, String query, String fragment) throws URISyntaxException

這個構造函數和前邊所講的那個基本相同,除了它增加了一個進行查詢的string 參數。例如:




與通常一樣,遇到任何的語法錯誤它都會拋出異常URISyntaxException ,如果其中的哪個參數被省略,就用null代替。

public URI(String scheme, String userInfo, String host, int port, String path, String query, String fragment) throws URISyntaxException

這是構造一個有層次關係的URI的父親構造函數,也就是說,前面提到的兩個構造函數都會調用它。它把認證信息(authority)分成了user info, host, 和 port幾部分。它們中的每一個都具備其自身的語法規則。比如:




儘管這樣,但是最後得到的URI仍然得遵從URI的一般規則。同樣地,如果任何一個參數被省略,就用null代替。
public static URI create(String uri)

它不是個構造函數,而是個靜態factory方法。不象構造函數那樣,它不會拋出異常URISyntaxException 。如果你確信你的URI一定會是合法的,會遵從任一條規則,那麼你就可以用該方法。比如,這次調用創建了一個用來匿名登陸一個FTP的URI ,它用一個郵箱地址當密碼:




如果該URI確實存在問題,這個方法會拋出異常IllegalArgumentException 。它是在運行時才拋出的異常,所以你不能聲明或捕獲它。

The Parts of the URI
一個URI最多就三部分:scheme, scheme-specific , fragment identifier。一般形式:




如果省略了scheme,這個URI就是相對的。如果fragment identifier被省略了,這個URI就很簡單了。類URI有返回方法,它們能返回每個URI對象的這三部分。方法getRawFoo( ) 返回編碼後的URI,與此類似的還有方法getFoo() ,它先解碼%xx形式的字符,然後返回解碼後的URI。




注意: 這裏沒有方法getRawScheme( )  ,因爲在URI規範中所有的協議名應專門由合法的ASCII字符組成。不允許%xx在協議名中出現。
如果URI 對象不具備相關的內容,這些方法都返回null:例如,對一個沒指定協議的相對URI,或一個沒有fragment identifier的http 協議的URI。一個指定了協議(scheme)的URI是一個絕對的URI。沒有指定協議的URI是相對的URI。方法isAbsolute()  返回true如果這個URI是絕對的,如果是相對的就返回false:




scheme-specific在細節上的變化取決於scheme的類型。比如在一個tel URL中,scheme-specific需要用類似電話號碼的語法。儘管如此,但是在許多有用的URI中,包括很常見的file 和 http URL裏面,scheme-specific都有其獨特的描述層次關係的模式,包括authority, path, query string。Authority又被細分爲user info, host,  port。方法isOpaque()  返回false如果這是個有層次關係的URI,否則返回true—也就是說,如果是層次模糊的:




如果URI是層次模糊的,所有你能得到的就只是scheme, scheme-specific ,  fragment identifier。儘管如此,但是如果URI是有層次關係的,下面的getter方法還是能獲取該URI的所有內容:




所有這些方法返回的都是解碼後的內容;換句話說,這種形式%xx,例如%3c,都會被轉換成它本身所表示的字符,比如符號“<”。如果你想得到未經解碼的URI,這裏有5個相併列的方法:




記住類URI  與URI規範是不同的。所以像é 和 ü 這種非ASCII字符絕不會在第一位置以%xx形式出現,所以在用方法getRawFoo() 返回得到的string中,仍將回出現在其中;除非本來被用做構建URI對象的string已被編碼。
注意: 這裏沒有方法getRawPort( ) 和 getRawHost( )  ,因爲這些部分總是被保證由ASCII字符構成的,至少現在是這樣的。國際化的域名時代即將來臨,希望在java以後的版本中能重新思考這一做法。

事實上,具體的URI不會包括這些信息—例如,這個URI,http://www.example.com 就沒有user info, path, port, 或 query string—相應的方法就返回null。方法getPort( )  是唯一例外。因爲它被聲明爲返回一個int ,所以它不能返回null。它就用返回-1來代替,來表示沒有port的信息。
由於各種技術上的也不會有多大實際影響的原因,java不會一開始就在認證部分做語法錯誤檢察。這樣做的一個很自然的後果就是不會返回認證的各個部分:port, host,  user info。這種情況下,你可以調用parseServerAuthority()  讓認證(authority)被強制再解析:




這個最初的URI 不會發生變化(URI對象是永遠不會變的),但是URI 的返回值將會是authority被分開後的部分:user info, host,  port。如果這個authority不能被解析,會拋出URISyntaxException 異常。

例子7-10使用這些方法把輸入在命令行上的URI分成了它的那幾個組成部分。這與例子7-4相似,只是它處理的是語法上正確的URI,而不要求JDK必須提供一個合適的protocal 處理器。
Example 7-10. The parts of a URI




下面就是運行這三個URI實例後的結果:




處理相對的 URI
類URI 提供了三種方法進行相對的和絕對的URI之間的轉換。

public URI resolve(URI uri)

它比較參數uri 和這個URI ,然後用它構成一個新的URI 對象,它會有一個絕對的URI。例如,思考這三行代碼:




在它們執行後,resolved 會有一個絕對的URI
http://www.example.com/images/logo.png.

如果這個被調用的URI 它本身就沒有絕對的URI,那麼方法resolve( )  會盡量的處理這個URI,返回一個新的相對的URI對象。比如,下面這三個語句:




在執行後,resolved 現在有一個相對的URI
javafaq/books/jnp3/examples/07/index.html
就沒有scheme(協議)和authority(認證)

public URI resolve(String uri)
這是個簡便的方法,它能方便地把參數string 轉化成一個URI,然後與被調用的這個URI組合後,返回一個新的URI對象。也就是,它與resolve(newURI(str)) 是等價的。使用這個方法,前面的兩個例子就可以重寫如下:




反向操作這個過程也可以;也就是,從一個絕對的URI到一個相對的URI。方法relativize( )  用參數uri 創建一個新的URI 對象,這個uri 對被調用的URI 是相對的。參數沒有改變。例如:




現在的 URI 對象包含了一個相對的 URI images/logo.png.

一些有用的方法
類URI 有不少普通的但很實用的方法:equals(), hashCode( ), toString( ), 和 compareTo( )
public boolean equals(Object o)

URI之間的比較與你期待中的一樣好。它不是string之間的直接做比較。相同的URI必須要麼是有層次關係的要麼就是層次關係模糊的。Scheme和authority部分的比較是不區分大小寫的。也就是http 和 HTTP 都表示的是相同的協議,authority 的比較www.example.com 和 www.EXAMPLE.com 也是一樣的。URI中剩下的部分就得區分大小寫了,除了用來轉義非法字符的16進制的數字以外。在做比較前,%xx還沒有被解碼。

http://www.example.com/A and http://www.example.com/%41 are unequal URIs.
public int hashCode( )

方法hashCode( )  是個普通的hashCode( )  方法,沒有什麼特別的。相同的URI就會有相同的哈希碼,不同的URI就很難擁有相同的哈希碼了。

public int compareTo(Object o)
URI是可以排序的。排序是基於對各個部分字符比較的結果上的,以下面的順序:
·        如果scheme是不同的,就比較scheme,不考慮大小寫。
·        否則,如果scheme是相同的,我們 就認爲層次關係模糊的URI比有層次關係的URI優先級高。
·        如果比較的兩個URI都是層次關係模糊的,這時就以它們的scheme-specific進行排序。
·        如果scheme和層次關係模糊的scheme-specific都相同了,就比較URI的fragment
·        如果做比較的URI都是有層次關係的,就看它們的authority,比較其中的user info, host, port。
·        如果scheme和authority都相同了,就看它們的path。
·        如果path也相同了,接着比較query string。
·        如果query string也相同了,就比較fragment

URI除了和它們自身比較以外,不能和任何其他的類型做比較。把一個URI 和任何除了URI 以外的做比較會拋出異常ClassCastException

public String toString( )
方法toString( )  返回一個URI 形式的未經編碼的string。也就是說,像é 和 /這種字符就不會被轉換成%xx,除非它們在用來生成URI 的string中已經被轉換了。因此,調用這個方法後得到的結果不能保證它是一個語法正確的URI 。有時,這種形式有助於增強可讀性,但是對檢索來說就不一樣了。

public String toASCIIString( )
方法toASCIIString( ) 返回一個編碼後URI 形式的string。像é 和 /這樣的字符總是是被轉換成了%xx的,不管它們最初是否已經被轉換。很多時間你就應該使用這種string形式 的URI。儘管toString( ) 返回的形式對於人來說更易於理解,但是它們仍然可能被複制粘貼到一些期望得到合法URI的地方。而方法toASCIIString( ) 返回語法正確的URI。


Proxies(代理服務)

許多系統都通過proxy 服務器訪問網站和其他Internet上的不使用HTTP的地方。代理服務器接受從本地客戶機發向遠程服務器的請求,再把這個結果發送給本地客戶機。這樣做有時出於安全的原因,比如阻止遠程服務器獲取關於本地客戶機網絡配置的信息。有時通過過濾發出的請求和限制被訪問的網站,阻止用戶去訪問那些被禁止訪問的的網站。比如像一所小學就會阻止對http://www.playboy.com 的訪問。當然有時這樣做是單純地出於對效率的考慮,當大量用戶需要下載一臺遠程服務器上的相同文件時,就讓他們從一個本地cache上下載,而不是不停地從遠程服務器上下載。Java中基於URL 類的程序可以在大多數代理服務器和協議工作。的確,這就是你寧願想選擇使用URL 類而不願意基於SOCKET自己編寫HTTP包或者其他客戶端程序的原因。

System Properties(系統屬性)
對於基本操作而言,你需要做的就是設置一些系統屬性指定你的本地代理服務器的地址。如果你使用的是一個單純的HTTP代理,把http.proxyHost 設置爲域名或者爲你的代理服務器的IP地址,把http.proxyPort 設置爲代理服務器的端口號(默認爲80)。這裏有許多種方法可以實現這個操作。包括在你的java代碼中調用System.setProperty() 或者當啓動程序時,使用命令選項“-D”。下面這個例子就把代理服務器設置爲192.168.254.254,端口設置爲9000:




你如果不想讓某個主機的訪問通過代理服務,想直接進行連接,那麼就把系統屬性http.nonProxyHosts 設置爲它的主機名或ip地址。如果你需要對多個主機都進行這種操作,就用“|”把它們分開。比如,這段代碼片段表示它會代理所有的除去java.oreilly.com 和 xml.oreilly.com:




你也可以使用“*”作爲一個通配符,表示對所有對所有有特殊域或子域的主機的訪問不需要被代理。例如,對除了對oreilly.com 域的主機不代理外,對其他的都可以:




如果你使用的是個FTP代理服務器,可以用相同的方法設置系統屬性,像ftp.proxyHost, ftp.proxyPort, 和 ftp.nonProxyHosts 。java不支持任何其他應用層的代理,除非如果你對所有的TCP連接都使用傳輸層的SOCKS代理,你可以使用相關的socksProxyHost 和 socksProxyPort 來設置系統屬性。對於SOCKS,java沒有提供對是否需要代理的選擇。你要麼選擇都需要代理,要麼選擇都不需要代理。

The Proxy 類
Java1.5允許java程序對代理服務器進行更精細的操作。特別的是,你可以對不同的遠程主機選擇不同的代理服務器。代理本身是由java.net.Proxy 類的實例表示。這裏仍然只有三種代理方式,HTTP, SOCKS, 和直接連接(也就是不需要代理),由Proxy.Type 型迭代器中的三個常量表示:
·        Proxy.Type.DIRECT
·        Proxy.Type.HTTP
·        Proxy.Type.SOCKS


除了代理的類型外,其他關於代理重要信息包括它的地址和端口,由SocketAddress 的對象表示。例如,這個代碼片段創建了一個代理對象,它表示proxy.example.com 端口80上的一個HTTP代理服務器:




儘管這裏僅有三種代理對象,但是對於不同主機上的不同代理服務器,會有許多相同類型的代理。

The ProxySelector 類
每個java1.5的jvm都有一個java.net.ProxySelector 對象。它可以對不同的連接委派合適的代理服務器。默認的ProxySelector 僅僅是檢測一下各種系統屬性和URL協議,然後決定怎樣連接不同的主機。儘管如此,但是你可以安裝你自己的ProxySelector 子類來代替默認的選擇器(selector),讓它基於協議,主機,路徑,日期,或其它標準來選擇不同的代理。

這個類的關鍵就是抽象方法select( ) :




java傳一個表示需要被連接的主機的URI 對象給該方法。用類URL生成的這個連接對象通常是這種形式:http://www.example.com/ 或者 ftp://ftp.example.com/pub/files/, 或者其他類似的。對於由Socket類生成的單純的TCP連接, URI會是這種形式:socket://host:port:,看一個實例,socket://www.example.com:80 。對象ProxySelector 對這種類型的對象會選擇合適的代理,然後再以形式List<Proxy> 返回它們。這個類的第二個你必須實現的抽象方法是connectFailed( ):




這是個反饋方法,它警告程序代理服務器連接不成功。例子7-11表示一個ProxySelector 試圖對所有的HTTP連接都使用在proxy.example.com 上的代理服務器,除非這個代理服務器在前面,對某個特殊的URL的連接已經失敗了。在這種情況下,它會建議用一個直接連接代替。

Example 7-11. A ProxySelector that remembers what it can connect to




我已經說過的,每個運行的jvm都肯定會有一個ProxySelector 。爲了改變這個ProxySelector ,我們給靜態方法ProxySelector.setDefault( )  傳一個新的選擇器,像這樣:




從這以後,這個jvm打開的所有連接都會詢問這個ProxySelector 該使用哪個合適的代理。正常情況下,你不要在共享環境中使用這段代碼。例如,在一個servlet中,你就不要去改變這個ProxySelector 了,因爲對在相同容器中的所有servlet來說,它們的ProxySelector 也都被改變了。

通過GET與服務器端程序進行交互

類URL 使java applet和application與服務器端程序比如CGIs, servlets, PHP頁面,和其他使用GET 方法程序的交互,變得更容易了。(我們將在15章討論服務器端使用POST 方法會用到URLConnection 類的程序。)所有你需要知道的是程序希望接受名字和值之間採取怎樣的連接,生成一個帶有查詢string的URL,這個string會提供正需要的名字和值。所有的名字和值必須是這樣x-www-form-url-encoded—被方法URLEncoder.encode()  編碼的,這在前面已經討論過了。

對一個特定程序的查詢string,這裏有許多方法來決定它的語法規則。如果你自己已經寫過服務器端程序,那麼你就已經知道了滿足這種程序的名字和值對。如果你已經在自己的服務器上安裝了第三方的程序,這個程序的說明文檔會告訴你它需要什麼樣的名字和值對。另一方面,如果你正在與第三方服務器上的程序進行對話,可能就得麻煩多了。你可以總是問在遠程服務器上的管理員,讓他給你提供怎樣和他的站點進行對話的規範說明。儘管如此,即使他們不介意這樣做,恐怕也沒有誰的工作就是“告訴與我們沒有任何關係的黑客,怎樣才能進入我們的服務器。”因此,除非你恰巧碰到一個特別熱心的或者一個無聊的人,他除了寫一封長長的email郵件來告訴你如何進入他們的服務器以外,就沒有什麼其他事幹,否則的話,你不得不做一些逆向工程。

注意:  這種情況正在得到改變。不少的網站開始認識到對第三方開發人員開放他們系統的重要性,以及發佈一些開發人員需要的插件的重要性。這些插件提供了詳細的信息,利用這些信息來構造URL,對他們網站上的服務進行訪問。像站點Safari 和 Amazon都提供了RESTful, URL-based接口,通過URL 類,它們都很容易被訪問。像eBay和 Google的 SOAP-based服務就很難使用了。

許多程序被設計用來處理表格輸入,如果真是這種情況,就可以直截了當地指出程序需要的是什麼輸入就行了。表格使用的方法應該是FORM 元素的METHOD 屬性的值。這個值要麼是GET ,這時你可以使用這裏所說的這種處理流程,要麼是POST ,這時你可以使用將在15章講的處理流程。FORM 元素的ACTION 屬性的值給出了URL查詢string前面的內容。需要注意這可能是個相對URL,這時你應該找到與之相對應的絕對URL。最後,名字和值對(name-value pairs)就是INPUT 元素的NAME 屬性,TYPE 屬性的值是submit的INPUT 元素例外。

比如,看一下我的Cafe con Leche站點上用於本地搜索引擎的HTML表格。你可以看見它使用了GET 方法。通過URL http://www.google.com/search ,這個處理表格的程序能被訪問。它有四對名字和值對,它們中的三個有默認值:




INPUT 的類型並不重要——例如,它是一個複選框的集合,一個下拉菜單條,或者是一個文本域都不重要——只有你給的每個INPUT 的名字和值纔是重要的。只有submit型的input是個例外,它告知web瀏覽器什麼時候發送數據,但不會給出服務器任何其他信息。在一些情況下,你可能會發現hidden型的INPUT 必須有一個特定的要求的默認值。這個表格有三個hidden型的INPUT 。在一些情形下,正在與你對話的程序不可能處理有歧義的文本型的string輸入值。儘管如此,因爲表格是拿來給大家看和填寫的,所以它應該提供足夠的線索來提示它需要得到什麼樣的輸入;比如,某個域應該是一個兩個字母的縮寫或者是一個電話號碼。

不返回表格的程序對於逆向工程來說會更難。例如,在http://www.ibiblio.org/nywc/bios.phtml,你會發現大量到PHP頁面的鏈接,它們和數據庫對話,用某個作曲家的名字來檢索一堆音樂作品。儘管這樣,這裏沒有與這個程序相關的表格。這些都被hardcoded的URL處理了。這種情況下,你最多能做的就是儘可能多的查看URL,看你通過他們能不能猜出服務器到底期望得到什麼。如果這個設計者不想搞得太晦澀,這些信息是不難得到的。比如,看看在那個頁面上所有能得到的URL:




看到這些,你可以猜出這個程序希望得到三個輸入,分別是first, middle, 和 last,它們的值分別由一個作曲家的first, middle, 和 last名構成。有時的輸入不會是那種讓人一看就明白的名字。這時,你需要做些實驗,首先複製一些已知的值,然後慢慢提煉出哪些值是能或者不能被接受的。在java程序中,你不需要這樣做。你可以簡單地在你的web瀏覽器窗口上的地址欄中編輯URL。

注意: 由於其他的黑客可能也會對你的服務器端程序做這樣的實驗,所以你有理由讓它們更健壯以應對那些意外的輸入。

不管你怎樣確定了這個服務器希望得到的名字和值對,一旦你知道了如何同他們進行交互,這就簡單了。所有你需要做的就是創建一個包括必需的名字和值對的查詢string,然後生成一個包含這個查詢string的URL。發送這個查詢string到服務器和讀取它的反饋的方法,與你連接服務器和檢索一個靜態HTML頁面的方法是相同的。一旦URL被創建,這裏就沒有什麼特殊的協議需要被遵守了。(POST 方法需要遵守一個特殊的協議,儘管這樣,這就是爲什麼它需要在第15章才討論。)爲了表明這個過程,讓我們寫一個非常簡單的命令行程序來看看Netscape Open Directory (http://dmoz.org/)上的標題。站點如圖7-3所示,它有變得真正簡單的優勢。

image
圖7-3。Open Directory的基本用戶界面

Open Directory的基本用戶界面是一個簡單的表格,它擁有一個名爲search 的輸入框;其中的輸入會發送到http://search.dmoz.org/cgi-bin/search 上的一個CGI程序,它執行真正的搜索。表格的HTML語句像下面這樣:




表格裏只有兩個input:一個Submit按紐和一個名爲Search的文本框。因此,如果要向Open Directory發送一個搜索請求,你需要收集搜索string ,把它編碼成一個查詢string,然後發送到http://search.dmoz.org/cgi-bin/search。例如,爲了搜索“java”,你需要打開一個連接URL http://search.dmoz.org/cgi-bin/search?search=java 的連接,然後讀取這個結果輸入流。例子7-12就是按照這樣來做的。

Example 7-12. Do an Open Directory search




當然,在解析和顯示結果上需要更多的努力。但是請注意與服務器進行對話的代碼是多麼的簡單呀!。除去看上有趣的URL和組成它的一些部分需要被x-www-form-url-encoded的可能性更大以外,使用GET 與服務器進行對話的程序不比檢索任何其他HTML頁面的工作困難。

Accessing Password-Protected Sites
(進入受密碼保護的站點)

許多流行的站點,像TheWall Street Journal,需要一個用戶名和密碼才能被訪問。一些網站,像W3C的會員頁面,通過HTTP認證正規地執行這些。其他的像Java Developer Connection,通過cookies 和 HTML非正規地執行這些認證。Java的URL類能訪問使用http認證的站點,儘管你也需要告訴它你的用戶名和密碼。Java對那些使用非標準的、基於cookie認證的站點是提供支持的。一部分原因是在java1.4和早期版本中,java實際上是不支持cookie的,還有部分原因是:如果這樣做,需要解析和發送html表格,最後是因爲cookie與web架構是完全牴觸的。(Java 1.5的確增加了對cookie的支持,我們將在下一章討論它。儘管如此,但它沒有對有認證功能的cookie與其他cookie區分開。)你可以自己提供這種支持,
使用URLConnection 類來讀和寫cookie被設置或被返回的http頭文件。但是,要想這樣做並不簡單,經常需要你要連接站點的本地代碼。這個難度不壓於讓web瀏覽器完全實現對html表格和cookie的支持。訪問受標準的http認證保護的站點就簡單多了。

Authenticator 類
包java.net 中有一個Authenticator 類,你能用它給受http驗證保護的站點提供一個用戶名和密碼:
public abstract class Authenticator extends Object // Java 1.2

因爲Authenticator 是一個抽象類,你必須生成它的子類。不同的子類會用不同的方法檢索信息。例如,一個字符模式的程序可能會叫用戶輸入用戶名和密碼,用System.in 接受它們。一個GUI程序可能會顯示一個對話框,像圖7-4那樣。一個自動機器會從一個加密文件中讀出用戶名。

image
圖7-4。驗證對話框

爲了要讓URL 類使用Authenticator 類的子類,就把它傳遞給靜態方法Authenticator.setDefault() ,這樣就把它安裝成了默認的authenticator:




比如,如果你已經寫好了一個叫DialogAuthenticator 的Authenticator 子類,你得這樣安裝它:




你只需要這樣做一次就行了。從這以後,當URL 類需要用戶名和密碼時,它會叫DialogAuthenticator 使用靜態方法Authenticator.requestPasswordAuthentication()  :




參數address 就是需要進行驗證的主機。參數port 就是那個主機上的端口,參數protocol 就是訪問這個站點時所用的應用層協議。http服務器會提供參數prompt 。一般就是指需要被驗證的訪問域。(像www.ibiblio.org 這樣的大網站會有許多的訪問域,每個都要求用不同的用戶名和密碼。)參數scheme 是用來做驗證的模式。(這裏的單詞scheme 與protocol (協議)不是同義詞。而是一種基本的http驗證模式。)

不被信任的applet不允許讓用戶提供用戶名和密碼。受信任的applet可以這樣做,只要它們有requestPasswordAuthenticationNetPermission
否則的話,Authenticator.requestPasswordAuthentication( ) 拋出SecurityException 異常。Authenticator 的子類必須覆蓋getPasswordAuthentication( )  方法。這個方法裏,你需要從用戶或其他來源處取到用戶名和密碼,然後作爲java.net.PasswordAuthentication 類的實例返回:
java.net.PasswordAuthentication class:




如果你不想驗證這個請求,就返回null,java回告訴服務器它不知道該怎樣驗證這個連接。如果你發送一個不正確的用戶名或密碼,java會再次調用getPasswordAuthentication( ) ,再給你一次機會提交正確的數據。正常情況下,你有5次機會得到正確的用戶名和密碼;在那以後,openStream( )  會拋出ProtocolException 異常。

用戶名和密碼被放在相同的虛擬機對話中。一旦你對一個訪問域使用了正確的密碼進行訪問,你就不會被要求再次使用密碼了,除非你已經很明確地清空了保存它的char 數組。你可以通過調用這些從超類Authenticator 繼承來的方法獲取更多詳細的信息:




這些方法要麼返回上次被requestPasswordAuthentication( ) 調用後得到的信息,要麼返回null,如果那些信息已經不能使用了。(getRequestingPort( ) 返回-1,如果那個端口不能被使用了。)最後一個方法,getRequestingHost( ) ,只有在java1.4和更新的版本中才能被使用;在早期版本里,你可以調用getRequestingSite( ).getHostName( ) 代替。Java1.5又增加了兩個方法到這個類中:




方法getRequestingURL( )  會返回一個被要求使用驗證的完整的URL——這是個重要的 ,如果一個站點的不同文件需要使用不同的用戶名和密碼。方法getRequestorType( ) 會返回下面兩個常量中的一個 Authenticator.RequestorType.PROXY 或者 Authenticator.RequestorType.SERVER  ,以此來區分是服務器還是代理服務器請求驗證。

PasswordAuthentication 類

PasswordAuthentication 是一個非常簡單的final類,它提供兩個只讀屬性:用戶名和密碼。用戶名是string存放的。密碼被存放在char 數組中,所以當不需要時,可以擦除密碼。一個string在它可以被擦除前,要等待垃圾回收器來收集,即使這時它仍有可能存在本地系統內存上的某個地方,也有可能在磁盤上,如果這時這個內存塊已經被交換到作爲虛擬內存的磁盤區域上。用戶名和密碼被設置在這個構造函數中:




每個都是通過getter 方法被訪問的:




JPasswordField 類
Swing中JPasswordField 組件有個用來向用戶獲取密碼的有用工具:




這個輕量級組件和文本框的行爲幾乎是一樣的。但是用戶輸入的會以“*”形式顯示出來。這樣的話,就可以防止其他人在後面看見用戶輸入的密碼了。

JPasswordField 也用char 數組來存放密碼,這樣當你不再需要是,你就可以清空它了。方法getPassword( )  用來返回密碼:




不然的話,大多數時候你得使用繼承超類JTextField 後得到的方法。例子7-13展示了一個來自Swing的Authenticator 的子類,它生成一個對話框向用戶獲取他的用戶名和密碼。下面的大多數代碼都是用來生成GUI。JPasswordField 提取密碼,JTextField 提取用戶名。圖7-4就是下面代碼所生成的一個簡單的對話框。

Example 7-13. A GUI authenticator




例子7-14是經過修改後的SourceViewer 程序,通過DialogAuthenticator 類向用戶獲取用戶名和密碼。
Example 7-14. 用來下載受密碼保護網頁的程序

 
發佈了674 篇原創文章 · 獲贊 7 · 訪問量 561萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章