宣告式表單驗證


接續〈宣告式基本驗證〉。

如果需要自訂登入的畫面,以及登入錯誤時的頁面,則可以改用容器所提供表單(Form)驗證。要將之前的基本驗證改為表單驗證的話,可以在 web.xml 中修改 <login-config> 的設定:

<login-config>
    <auth-method>FORM</auth-method>
    <form-login-config>
        <form-login-page>/login.html</form-login-page>
        <form-error-page>/error.html</form-error-page>
    </form-login-config>
</login-config>

<auth-method> 的設定從 BASIC 改為 FORM。由於使用表單網頁進行登入,所以必須告訴容器,登入頁面是哪個?登入失敗的頁面又是哪個?這是由 <form-login-page><form-error-page> 來設定,設定時注意必須以斜線開始,也就是從應用程式根目錄開始的 URL 路徑。

再來就可以設計自己的表單頁面,但必須注意!表單發送的 URL 必須是 j_security_check,發送名稱的請求參數必須是 j_username,發送密碼的請求參數必須是 j_password,在 Servlet 3.1 中,要進一步於密碼欄位上加上 autocomplete="off"。以下是個簡單的示範:

<!DOCTYPE html>
<html>
  <head>
    <title>登入</title>
    <meta charset="UTF-8">
  </head>
  <body>
    <form action="j_security_check" method="post">
        名稱:<input type="text" name="j_username"><br>
        密碼:<input type="password" name="j_password" autocomplete="off"><br>
        <input type="submit" value="送出">
    </form>
  </body>
</html> 

一但使用者驗證成功,HttpSession 的逾時或失效(呼叫 invalidate() 方法),使用者就相當於登出。

除了基本驗證與表單驗證之外,在 <auth-method> 中還可以設定 DIGESTCLIENT-CERT

DIGEST 即所謂「摘要驗證」,瀏覽器也會出現對話方塊輸入名稱、密碼,而後透過 Authorization 標頭傳送,只不過並非使用 BASE64 來編碼名稱、密碼。瀏覽器會直接傳送名稱,但對密碼則先進行(MD5)摘要演算(非加密),得到理論上唯一且不可逆的 字串再傳送,伺服端根據名稱從後端取得密碼,以同樣的方式作摘要演算,再比對瀏覽器送來的摘要字串是否符合,如果符合就驗證成功。由於網路上傳送時並不是 真正的密碼,而是不可逆的摘要,密碼不會被得知,理論上比較安全一些。

CLIENT-CERT 也是用對話方塊的方式來輸入名稱與密碼,因為使用 PKC(Public Key Certificate)作加密,可保證資料傳送時的機密性及完整性,但客戶端需要安裝憑證(Certificate),在一般使用者及應用程式之間並不常採用。

在身份驗證的四種方式中,BASICFORMDIGEST 都無法保證資料的機密性與完整性(DIGEST 比較安全一點,但這個機制畢竟不是加密)。CLIENT-CERT 利用 PKC 加密,但客戶端要安裝憑證,比較不適用於一般使用者及應用程式之間的資料傳送。

通常 Web 應用程式要在傳輸過程中保護資料,會採用 HTTP over SSL,就就是俗稱的 HTTPS。在 HTTPS 中,伺服端會提供憑證來證明自己的身份及提供加密用的公鑰,而瀏覽器會利用公鑰加密資訊再傳送給伺服端,伺服端再用對應的私鑰進行解密以取得資訊,客戶端本身不用安裝憑證,因此是在保護資料傳送上是最常採用的方式。

如果要使用 HTTPS 來傳輸資料,則只要在 web.xml 中需要安全傳輸的 <security-contraint> 中設定:

<user-data-constraint>
    <transport-guarantee>CONFIDENTIAL</transport-guarantee>
</user-data-constraint>

<transport-guarantee> 預設值是 NONE,還可以設定的值是 CONFIDENTIALINTEGRAL, 正如其名稱所表達的,CONFIDENTIAL 在保證資料的機密性,也就是資料不可被未經驗證、授權的其他人看到,而 INTEGRAL 在保證完整性,也就是資料不可以被第三方修改。事實上,無論設定 CONFIDENTIALINTEGRAL,都可以保證機密性與完整性,只是大家慣例上都設定 CONFIDENTIAL

可以為之前的表單驗證設定使用 HTTPS:

<?xml version="1.0" encoding="UTF-8"?>
<web-app 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xmlns="http://xmlns.jcp.org/xml/ns/javaee" 
    xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee 
           http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version="4.0">

    // 略...

    <security-constraint>
        <web-resource-collection>
            <web-resource-name>Admin</web-resource-name>
            <url-pattern>/admin/*</url-pattern>
        </web-resource-collection>
        <auth-constraint>
            <role-name>admin</role-name>
        </auth-constraint>
        <user-data-constraint>
            <transport-guarantee>CONFIDENTIAL</transport-guarantee>
        </user-data-constraint>
    </security-constraint>

    <security-constraint>
        <web-resource-collection>
            <web-resource-name>Manager</web-resource-name>
            <url-pattern>/manager/*</url-pattern>
            <http-method>GET</http-method>
            <http-method>POST</http-method>
        </web-resource-collection>
        <auth-constraint>
            <role-name>admin</role-name>
            <role-name>manager</role-name>
        </auth-constraint>
        <user-data-constraint>
            <transport-guarantee>CONFIDENTIAL</transport-guarantee>
        </user-data-constraint>
    </security-constraint>

    // 略...
</web-app> 

就 Web 應用程式來說,只要這樣設定就夠了!若伺服器有支援 SSL 且安裝好憑證,例如,在 Tomcat 中,可以在 server.xml 中找到以下註解:

<!-- Define a SSL/TLS HTTP/1.1 Connector on port 8443
     This connector uses the NIO implementation. The default
     SSLImplementation will depend on the presence of the APR/native
     library and the useOpenSSL attribute of the
     AprLifecycleListener.
     Either JSSE or OpenSSL style configuration may be used regardless of
     the SSLImplementation selected. JSSE style configuration is used below.
-->
<!--
<Connector port="8443" protocol="org.apache.coyote.http11.Http11NioProtocol"
           maxThreads="150" SSLEnabled="true">
    <SSLHostConfig>
        <Certificate certificateKeystoreFile="conf/localhost-rsa.jks"
                     type="RSA" />
    </SSLHostConfig>
</Connector>
-->

Connector 部份的註解去除,並設定好你的憑證,當你請求受保護的資源時,伺服器會要求瀏覽器重新導向使用 HTTPS。

你也可以設定支援 HTTP/2 的 Connector

<!-- Define a SSL/TLS HTTP/1.1 Connector on port 8443 with HTTP/2
     This connector uses the APR/native implementation which always uses
     OpenSSL for TLS.
     Either JSSE or OpenSSL style configuration may be used. OpenSSL style
     configuration is used below.
-->
<!--
<Connector port="8443" protocol="org.apache.coyote.http11.Http11AprProtocol"
           maxThreads="150" SSLEnabled="true" >
    <UpgradeProtocol className="org.apache.coyote.http2.Http2Protocol" />
    <SSLHostConfig>
        <Certificate certificateKeyFile="conf/localhost-rsa-key.pem"
                     certificateFile="conf/localhost-rsa-cert.pem"
                     certificateChainFile="conf/localhost-rsa-chain.pem"
                     type="RSA" />
    </SSLHostConfig>
</Connector>
-->