程設式安全管理


Web 容器的宣告式安全管理,僅能針對 URL 來設定哪些資源必須受到保護,如果打算依不同的角色在同一個頁面中設定可存取的資源,例如只有站長或版面管理員可以看到刪除整個討論串的功能,一般使用者不行,那麼顯然地無法單純使用宣告式安全管理來達成。

在 Servlet 3.0 中,HttpServletRequest 新增了三個與安全有關的方法:authenticate()login()logout()

首先來看到 authenticate() 方法,如果在 web.xml 設定為表單驗證:

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

接下來,在程式流程中,可以使用 authenticate(),只讓通過驗證的使用者才可以觀看:

package cc.openhome;

import java.io.*;
import javax.servlet.*;
import javax.servlet.annotation.*;
import javax.servlet.http.*;

@WebServlet("/user")
public class User extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
                      throws ServletException, IOException {
        if(request.authenticate(response)) {
            response.setContentType("text/html;charset=UTF-8");
            PrintWriter out = response.getWriter();
            out.println("必須驗證過使用者才可以看到的資料");
            out.println("<a href='logout'>登出</a>");
        } 
    } 
}

如果 authenticate() 的結果是 false,表示使用者未曾登入,在 service() 完成後,會自動 forward 至登入表單:

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

在登入表單中,可以決定登入驗證時的 action、請求參數等,執行登入時,可以使用請求物件的 login() 方法:

package cc.openhome;

import java.io.*;

import javax.servlet.*;
import javax.servlet.annotation.*;
import javax.servlet.http.*;

@WebServlet("/login")
public class Login extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response)
                        throws ServletException, IOException {
        String user = request.getParameter("user");
        String passwd = request.getParameter("passwd");
        try {
            request.login(user, passwd);
            response.sendRedirect("user");
        } catch(ServletException ex) {
            response.sendRedirect("login.html");
        }
    } 
}

如果登入成功,Session ID 會更換。若要登出,可以使用請求物件的 logout() 方法:

package cc.openhome;

import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet("/logout")
public class Logout extends HttpServlet {
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        request.logout();
        response.sendRedirect("login.html");
    }
}

在 Servlet 3.0 之前,HttpServletRequest 上就已存在三個與安全相關的方法:getUserPrincipal()getRemoteUser()isUserInRole()

getUserPrincipal() 與 EJB 元件的溝通有關,這邊不加以討論。getRemoteUser() 可以取得登入使用者的名稱(如果驗證成功的話)或是傳回 null(如果沒有驗證成功的使用者),不過並不常用。

比較常用的是 isUserInRole() 方法,可以傳給它一個角色名稱,如果登入的使用者屬於該角色則傳回 true,否則傳回 false(沒有登入就呼叫也會傳回 false)。一個基本的使用方式像是:

if(request.isUserInRole("admin") || request.isUserInRole("manager")) {
    // 進行站長或版面管理員才可以作的事,例如呼叫刪除討論串的方法之類的
}

上面的程式碼中,將角色名稱直接寫死了。如果不想在程式碼中寫死角色的名稱,則有兩個方式可以解決。第一個方式是透過 Servlet 初始參數的設定。第二個方式,則可以在 <servlet> 標籤中設定 <security-role-ref>,透過 <role-link><role-name> 將程式碼中的名稱跟實際角色名稱對應起來。例如若 web.xml的定義如下:

<web-app…>
    <servlet>
        <security-role-ref>
            <role-name>administrator</role-name>
            <role-link>admin</role-link>
        </security-role-ref>
        ..
    </servlet>
    // 略…
    <security-role>
        <role-name>admin</role-name>
        <role-name>manager</role-name>
    </security-role>
</web-app>

如果 Servlet 程式碼中是這麼寫的:

if(request.isUserInRole("administrator")) {
    // 略...
}

則根據 web.xml 中 <security-role-ref> 的設定,administrator 名稱將對應至實際的角色名稱為admin`。