1. 程式人生 > >How tomcat works——10 安全性

How tomcat works——10 安全性

概述

一些web 應用內容是受限的,只有在有特定許可權的使用者輸入正確的使用者名稱和密碼後才能訪問。Servlet 通過配置部署檔案 web.xml 來對安全性提供技術支援。本章將介紹 container 如何支援安全性控制。

servlet 容器通過一個稱為 authenticator 的閥門(valve)來支援安全認證。當 container 啟動時,authenticator 被新增到container 的 pipeline中。如果忘記了pipeline工作機制,請閱讀第6章。

在 wrapper閥門被呼叫之前,會先呼叫 authenticator閥門,用來對使用者進行認證。若使用者輸入了正確的使用者名稱密碼,則 authenticator 會呼叫下一個閥門,否則會直接返回,不再繼續執行剩餘的閥門。由於驗證失敗,使用者並不能看到請求的 servlet。

在使用者驗證時, authenticator 閥門呼叫的是上下文域(realm)內的authenticate()方法,將使用者名稱和密碼傳遞給它。領域(Realm)可以訪問有效使用者名稱和密碼的集合。

本章會先介紹一些 servlet 程式設計中與安全相關的物件(realm,role,principal 等),然後通過一個應用程式來演示如何為servlet 新增基本的認證功能。

注意:這裡假設大家已經熟悉 servlet 程式設計安全性的相關概念,包括: principals, roles, realms, login configuration等。如果對這些概念還不清楚可以閱讀《Java for the Web with Senlets, JSP,and EJB》或者其它相關書籍。

10.1 領域(Realm)

域(Realm)是用於進行使用者驗證的一個元件。它驗證一使用者名稱&密碼是否是合法。一個域跟一個上下文容器相聯絡,並且一個容器只有一個域。可以使用容器的 setRealm()方法來建立它們之間的聯絡。

一個域是如何驗證一個使用者的合法性的暱?一個域擁有所有的合法使用者的使用者名稱和密碼或
者是可以訪問到儲存它們的地方。至於它們存放在哪裡則取決於域的具體實現。在 Tomcat預設實現裡,合法使用者被儲存在 tomcat-users.xml 檔案裡。但是,也可以使用域的其它實現,如關係資料庫。

在 Catalina 中,一個域用介面 org.apache.catalina.Realm 表示。該介面最重要的方法是4個 過載authenticate()方法:

public Principal authenticate(String username, String credentials);
public Principal authenticate(String username, byte[] credentials);
public Principal authenticate(String username, String digest,
String nonce, String nc, String cnonce, String qop, String realm,String md5a2);
public Principal authenticate(X509Certificate certs[]);

第一個方法最常用。Realm 介面還有一個 getRole()方法,簽名如下:

public boolean hasRole(Principal principal, String role);

另外,域還有 getContainer() 和 setContainer() 方法用於建立域與容器關聯。

抽象類 org.apache.catalina.realm.RealmBase是一個域的基本實現。org.apache.catalina.realm 包中還提供了其它繼承了 RealmBase的一些實現類,如:JDBCRealm, JNDIRealm, MemoryRealm和 UserDatabaseRealm。預設情況下使用的域是 MemoryRealm。當MemoryRealm首次啟動時,它會讀取tomcat-users.xml檔案。在本章Demo中,將會自定義一簡單的Realm來儲存使用者資訊。

注意:在 Catalina 中,authenticator閥門呼叫相關域的 authenticate()方法來驗證一個使用者。

10.2 GenericPrincipal類

一個主體(principal)使用 java.security.Principal 介面來表示,Catalina中的實現類是 org.apache.catalina.realm.GenericPrincipal。一個GenericPrincipal 必須跟一個域相關聯,如下是GenericPrincipal的2個建構函式:

public GenericPrincipal(Realm realm, String name, String password) {
    this(realm, name, password, null);
}
public GenericPrincipal(Realm realm, String name, String password,List roles) {
    super();
    this.realm = realm;
    this.name = name;
    this.password = password;
    if (roles != null) {
        this.roles = new String[roles.size()];
        this.roles = (String[]) roles.toArray(this.roles);
        if (this.roles.length > 0)
            Arrays.sort(this.roles);
        }
}

GenericPrincipal 必須擁有一個使用者名稱和一個密碼,此外還可選擇性的傳遞一角色列表(List roles)給它。我們可以使用 hasRole()方法來檢查一個 principal 是否有一個特定的角色,傳遞的引數為角色的字串表示形式。如下是 Tomcat4 中的 hasRole() 方法:

public boolean hasRole(String role) {
    if (role == null)
        return (false);
    return (Arrays.binarySearch(roles, role) >= 0);
}

Tomcat5 支援 servlet2.4 所以必須支援用*來匹配任何角色。

public boolean hasRole(String role) {
    if ("*".equals(role)) // Special 2.4 role meaning everyone
        return true;
    if (role == null)
        return (false);
    return (Arrays.binarySearch(roles, role) >= 0);
}

10.3 LoginConfig類

一個登入配置包括一個域名,這裡是通過org.apache.catalina.deploy.LoginConfig 不變類表示。LoginConfig 的例項封裝了域名和驗證要用的方法。可以通過LoginConfig例項的 getRealmName()方法來獲得域名,可以使用 getAuthName()方法來驗證使用者。一個驗證的名字必須是下面的之一:BASIC, DIGEST, FORM或者 CLIENT-CERT。如果用到的是基於表單(form)的驗證,該 LoginConfig 物件還包括登入或者錯誤頁面像對應的 URL。

Tomcat 在啟動時,會先讀取 web.xml。如果 web.xml 包括一個login-config元素,那麼Tomcat 則會建立一 LoginConfig 物件並相應地設定它的屬性。驗證閥門呼叫 LoginConfig 的 getRealmName() 方法並將域名傳送給瀏覽器顯示登入表單。如果 getRealmName()名返回值為 null,則將傳送伺服器的名字和埠名給瀏覽器作為代替。圖 10.1 是在Window XP 上 IE6 瀏覽器上顯示的基本驗證會話窗。
這裡寫圖片描述
圖10.1: The basic authentication dialog

10.4 驗證器(Authenticator)

org.apache.catalina.Authenticator 介面用來表示一個驗證器。該介面並沒有方法,只是一個元件的標誌器,這樣就能使用 “instanceof”來檢查一個元件是否為驗證器。

Catalina 提供了 Authenticator 介面的基本實現:org.apache.catalina.authenticator.AuthenticatorBase 類。除了實現Authenticator 介面外,AuthenticatorBase 還繼承了org.apache.catalina.valves.ValveBase 類。這就是說 AuthenticatorBase 也是一個閥門。可以在 org.apache.catalina.authenticator 包中找到該介面的幾個類:BasicAuthenticator 用於基本驗證, FormAuthenticator 用於基於表單的驗證, DigestAuthentication 用於摘要(digest)驗證, SSLAuthenticator 用於 SSL 驗證。NonLoginAuthenticator 用於 Tomcat 沒有指定驗證元素的時候。NonLoginAuthenticator 類表示只是檢查安全限制的驗證器,但是不進行使用者驗證。

org.apache.catalina.authenticator 包中類的 UML 結構圖如圖 10.2 所示:
這裡寫圖片描述
圖10.2: Authenticator-related classes

一個驗證器的主要工作是驗證使用者。因此AuthenticatorBase 類的 invoke 方法呼叫了抽象方法 authenticate()也不足為奇了,該方法的具體實現由子類完成。例如,在BasicAuthenticator中,authenticate()方法使用基本認證來認證使用者。

10.5 安裝驗證器閥門

在部署檔案中,只能出現一個 login-config 元素,login-config 元素包括了auth-method 元素用於定義驗證方法。這也就是說一個上下文容器只能有一個LoginConfig 物件來使用一個 authentication 的實現類。

具體AuthenticatorBase 的子類哪一個在上下文中被用作驗證閥門,這依賴於部署檔案中auth-method 元素的值。表 10.1 為 auth-method 元素的值,可以用於確定驗證器。

表10.1: The authenticator implementation class

Value of the auth-method element Authenticator class
BASIC BasicAuthenticator
FORM FormAuthenticator
DIGEST DigestAuthenticator
CLIENT-CERT SSLAuthenticator

如果沒有使用 auth-method 值,則認為 LoginConfig 物件的 auth-method 屬性值為NONE,那麼NonLoginAuthenticator類將被使用。

因為驗證器類只在執行時知道,所以類是動態載入的。 StandardContext類使用org.apache.catalina.startup.ContextConfig類來配置StandardContext例項的許多設定。 此配置包括例項化驗證器類並將該例項與上下文相關聯。本章應用Demo附帶著一個簡單的上下文配置類:ex10.pyrmont.core.SimpleContextConfig。 正如你所見,這個類的例項負責動態載入BasicAuthenticator類,例項化它,並將其安裝為StandardContext例項中的一個閥門。

注意:我們將在第15章討論org.apache.catalina.startup.ContextConfig類。

10.6 應用Demo

本章附帶的應用程式中使用了幾個與安全約束相關的Catalina類。還使用了類似於第9章中的SimplePipeline,SimpleWrapper和SimpleWrapperValve類。此外,SimpleContextConfig類與第9章中的SimpleContextConfig類也相似,除了它具有authenticatorConfig()方法,該方法向StandardContext新增一個BasicAuthenticator例項 。 這2個應用Demo還使用了PrimitiveServlet和ModernServlet。 這些類在2個附帶的應用Demo中使用。

第1個Demo中使用ex10.pyrmont.startup.Bootstrap1和ex10.pyrmont.realm.SimpleRealm 2個類;第二個Demo中使用了ex10.pyrmont.startup.Bootstrap2 ex10.pyrmont.realm.SimpleUserDatabaseRealm。每個類將在下面子章節中介紹。

10.6.1 ex10.pyrmont.core.SimpleContextConfig類

Listing10.1中的SimpleContextConfig類與第9章中的SimpleContextConfig類似。org.apache.catalina.core.StandardContext例項需要將其配置屬性設定為true。 但是,本章中的SimpleContextConfig類添加了從lifeCycleEvent()方法呼叫的authenticatorConfig()方法。 authenticatorConfig()方法例項化BasicAuthenticator類,並將其作為閥門新增到StandardContext例項管道中。

Listing 10.1: The SimpleContextConfig class

package ex10.pyrmont.core;

import org.apache.catalina.Authenticator;
import org.apache.catalina.Context;
import org.apache.catalina.Lifecycle;
import org.apache.catalina.LifecycleEvent;
import org.apache.catalina.LifecycleListener;
import org.apache.catalina.Pipeline;
import org.apache.catalina.Valve;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.deploy.SecurityConstraint;
import org.apache.catalina.deploy.LoginConfig;

public class SimpleContextConfig implements LifecycleListener {

  private Context context;
  public void lifecycleEvent(LifecycleEvent event) {
    if (Lifecycle.START_EVENT.equals(event.getType())) {
      context = (Context) event.getLifecycle();
      authenticatorConfig();
      context.setConfigured(true);
    }
  }

  private synchronized void authenticatorConfig() {
    // Does this Context require an Authenticator?
    SecurityConstraint constraints[] = context.findConstraints();
    if ((constraints == null) || (constraints.length == 0))
      return;
    LoginConfig loginConfig = context.getLoginConfig();
    if (loginConfig == null) {
      loginConfig = new LoginConfig("NONE", null, null, null);
      context.setLoginConfig(loginConfig);
    }

    // Has an authenticator been configured already?
    Pipeline pipeline = ((StandardContext) context).getPipeline();
    if (pipeline != null) {
      Valve basic = pipeline.getBasic();
      if ((basic != null) && (basic instanceof Authenticator))
        return;
      Valve valves[] = pipeline.getValves();
      for (int i = 0; i < valves.length; i++) {
        if (valves[i] instanceof Authenticator)
        return;
      }
    }
    else { // no Pipeline, cannot install authenticator valve
      return;
    }

    // Has a Realm been configured for us to authenticate against?
    if (context.getRealm() == null) {
      return;
    }

    // Identify the class name of the Valve we should configure
    String authenticatorName = "org.apache.catalina.authenticator.BasicAuthenticator";
    // Instantiate and install an Authenticator of the requested class
    Valve authenticator = null;
    try {
      Class authenticatorClass = Class.forName(authenticatorName);
      authenticator = (Valve) authenticatorClass.newInstance();
      ((StandardContext) context).addValve(authenticator);
      System.out.println("Added authenticator valve to Context");
    }
    catch (Throwable t) {
    }
  }
}

開始,authenticatorConfig()方法檢查在相關聯的上下文中是否存在安全約束。如果沒有,則該方法直接返回而不安裝驗證器。

// Does this Context require an Authenticator?
SecurityConstraint constraints[] = context.findConstraints();
if ((constraints == null) || (constraints.length == 0))
    return;

如果存在安全約束,則繼續檢測上下文中是否存在LoginConfig物件,如果不存在,則建立一個:

LoginConfig loginConfig = context.getLoginConfig();
if (loginConfig == null) {
    loginConfig = new LoginConfig("NONE", null, null, null);
    context.setLoginConfig(loginConfig);
}

然後,authenticatorConfig()方法檢查StandardContext物件管道中的基本閥門或附加閥門是否是驗證器。 由於上下文只能有一個驗證器,所以如果其中一個閥門是認證器,則authenticatorConfig()方法將返回。

// Has an authenticator been configured already?
   Pipeline pipeline = ((StandardContext) context).getPipeline();
    if (pipeline != null) {
      Valve basic = pipeline.getBasic();
      if ((basic != null) && (basic instanceof Authenticator))
        return;
      Valve valves[] = pipeline.getValves();
      for (int i = 0; i < valves.length; i++) {
        if (valves[i] instanceof Authenticator)
        return;
      }
    }else { // no Pipeline, cannot install authenticator valve
      return;
    }

然後它檢查領域(Realm)是否已與上下文相關聯。 如果沒有發現領域,則不需要安裝驗證器,因為使用者不能被認證:

// Has a Realm been configured for us to authenticate against?
if (context.getRealm() == null) {
    return;
}

此時,authenticatorConfig()方法將動態載入BasicAuthenticator類,建立該類的例項,並將其作為閥門新增到StandardContext例項:

// Identify the class name of the Valve we should configure
    String authenticatorName = "org.apache.catalina.authenticator.BasicAuthenticator";
    // Instantiate and install an Authenticator of the requested class
    Valve authenticator = null;
    try {
      Class authenticatorClass=Class.forName(authenticatorName);
      authenticator = (Valve) authenticatorClass.newInstance();
      ((StandardContext) context).addValve(authenticator);
      System.out.println("Added authenticator valve to Context");
    }catch (Throwable t) {}

10.6.2 ex10.pyrmont.realm.SimpleRealm類

Listing 10.2中的SimpleRealm類演示了領域是如何工作。這個類在本章的第一個Demo中使用,包含兩個硬編碼的使用者名稱和密碼。

Listing 10.2: The SimpleRealm Class

package ex10.pyrmont.realm;

import java.beans.PropertyChangeListener;
import java.security.Principal;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Iterator;
import org.apache.catalina.Container;
import org.apache.catalina.Realm;
import org.apache.catalina.realm.GenericPrincipal;


public class SimpleRealm implements Realm {

  public SimpleRealm() {
    createUserDatabase();
  }

  private Container container;
  private ArrayList users = new ArrayList();

  public Container getContainer() {
    return container;
  }

  public void setContainer(Container container) {
    this.container = container;
  }

  public String getInfo() {
    return "A simple Realm implementation";
  }

  public void addPropertyChangeListener(PropertyChangeListener listener) {
  }

  public Principal authenticate(String username, String credentials) {
    System.out.println("SimpleRealm.authenticate()");
    if (username==null || credentials==null)
      return null;
    User user = getUser(username, credentials);
    if (user==null)
      return null;
    return new GenericPrincipal(this, user.username, user.password, user.getRoles());
  }

  public Principal authenticate(String username, byte[] credentials) {
    return null;
  }

  public Principal authenticate(String username, String digest, String nonce,
    String nc, String cnonce, String qop, String realm, String md5a2) {
    return null;
  }

  public Principal authenticate(X509Certificate certs[]) {
    return null;
  }

  public boolean hasRole(Principal principal, String role) {
    if ((principal == null) || (role == null) ||
      !(principal instanceof GenericPrincipal))
      return (false);
    GenericPrincipal gp = (GenericPrincipal) principal;
    if (!(gp.getRealm() == this))
      return (false);
    boolean result = gp.hasRole(role);
    return result;
  }

  public void removePropertyChangeListener(PropertyChangeListener listener) {
  }

  private User getUser(String username, String password) {
    Iterator iterator = users.iterator();
    while (iterator.hasNext()) {
      User user = (User) iterator.next();
      if (user.username.equals(username) && user.password.equals(password))
        return user;
    }
    return null;
  }

  private void createUserDatabase() {
    User user1 = new User("ken", "blackcomb");
    user1.addRole("manager");
    user1.addRole("programmer");
    User user2 = new User("cindy", "bamboo");
    user2.addRole("programmer");

    users.add(user1);
    users.add(user2);
  }

  class User {

    public User(String username, String password) {
      this.username = username;
      this.password = password;
    }

    public String username;
    public ArrayList roles = new ArrayList();
    public String password;

    public void addRole(String role) {
      roles.add(role);
    }
    public ArrayList getRoles() {
      return roles;
    }
  }

}

SimpleRealm類實現了Realm介面。 在建構函式中呼叫createUserDatabase()方法建立2個使用者。 在內部,使用者由內部類User表示。 第一個使用者具有使用者名稱ken和密碼blackcomb。 這個使用者有兩個角色:manager和programmer。 第二個使用者的使用者名稱和密碼分別為cindy和bamboo。 這個使用者擁有programmer的角色。 然後,這2個使用者被新增到變數users中。程式碼如下:

User user1 = new User("ken", "blackcomb");
user1.addRole("manager");
user1.addRole("programmer");
User user2 = new User("cindy", "bamboo");
user2.addRole("programmer");
users.add(user1);
users.add(user2);

SimpleRealm類提供了四種過載驗證方法中的1個實現:

public Principal authenticate(String username, String credentials) {
    System.out.println("SimpleRealm.authenticate()");
    if (username==null || credentials==null)
      return null;
    User user = getUser(username, credentials);
    if (user==null)
      return null;
    return new GenericPrincipal(this, user.username, user.password, user.getRoles());
  }

此authenticate()方法由驗證器呼叫。 如果使用者名稱和密碼作為引數傳遞的使用者不是有效的使用者,則返回null。否則,它返回表示使用者的Principal物件。

10.6.3 ex10.pyrmont.realm.SimpleUserDatabaseRealm類

SimpleUserDatabaseRealm類表示更復雜的領域。 它沒在程式碼中儲存使用者列表。相反,它讀取conf目錄中的tomcat-users.xml檔案,並將內容載入到記憶體中。然後針對該列表進行認證。 在隨附zip檔案的conf目錄中,可以找到tomcat-users.xml檔案的副本,如下所示:

<?xml version='1.0' encoding='utf-8'?>
<tomcat-users>
  <role rolename="tomcat"/>
  <role rolename="role1"/>
  <role rolename="manager"/>
  <role rolename="admin"/>
  <user username="tomcat" password="tomcat" roles="tomcat"/>
  <user username="role1" password="tomcat" roles="role1"/>
  <user username="both" password="tomcat" roles="tomcat,role1"/>
  <user username="admin" password="password" roles="admin,manager"/>
</tomcat-users>

類SimpleUserDatabaseRealm 程式碼如Listing 10.3:

Listing 10.3: The SimpleUserDatabaseRealm class

package ex10.pyrmont.realm;
// modification of org.apache.catalina.realm.UserDatabaseRealm

import java.security.Principal;
import java.util.ArrayList;
import java.util.Iterator;
import org.apache.catalina.Group;
import org.apache.catalina.Role;
import org.apache.catalina.User;
import org.apache.catalina.UserDatabase;
import org.apache.catalina.realm.GenericPrincipal;
import org.apache.catalina.realm.RealmBase;
import org.apache.catalina.users.MemoryUserDatabase;

public class SimpleUserDatabaseRealm extends RealmBase {

  protected UserDatabase database = null;
  protected static final String name = "SimpleUserDatabaseRealm";

  protected String resourceName = "UserDatabase";

  public Principal authenticate(String username, String credentials) {
    // Does a user with this username exist?
    User user = database.findUser(username);
    if (user == null) {
      return (null);
    }

    // Do the credentials specified by the user match?
    // FIXME - Update all realms to support encoded passwords
    boolean validated = false;
    if (hasMessageDigest()) {
      // Hex hashes should be compared case-insensitive
      validated = (digest(credentials).equalsIgnoreCase(user.getPassword()));
    }
    else {
      validated = (digest(credentials).equals(user.getPassword()));
    }
    if (!validated) {
      return null;
    }

    ArrayList combined = new ArrayList();
    Iterator roles = user.getRoles();
    while (roles.hasNext()) {
      Role role = (Role) roles.next();
      String rolename = role.getRolename();
      if (!combined.contains(rolename)) {
        combined.add(rolename);
      }
    }
    Iterator groups = user.getGroups();
    while (groups.hasNext()) {
      Group group = (Group) groups.next();
      roles = group.getRoles();
      while (roles.hasNext()) {
        Role role = (Role) roles.next();
        String rolename = role.getRolename();
        if (!combined.contains(rolename)) {
          combined.add(rolename);
        }
      }
    }
    return (new GenericPrincipal(this, user.getUsername(),
      user.getPassword(), combined));
  }

  // ------------------------------------------------------ Lifecycle Methods


    /**
     * Prepare for active use of the public methods of this Component.
     *
     * @exception LifecycleException if this component detects a fatal error
     *  that prevents it from being started
     */
  protected Principal getPrincipal(String username) {
    return (null);
  }

  protected String getPassword(String username) {
    return null;
  }

  protected String getName() {
    return this.name;
  }

  public void createDatabase(String path) {
    database = new MemoryUserDatabase(name);
    ((MemoryUserDatabase) database).setPathname(path);
    try {
      database.open();
    }
    catch (Exception e)  {
    }
  }
}

在例項化SimpleUserDatabaseRealm類之後,必須呼叫createDatabase()方法。 createDatabase()方法例項化org.apache.catalina.users.MemoryUserDatabase讀取並解析XML文件。

10.6.4 ex10.pyrmont.startup.Bootstrap1類

Bootstrap1在本章第一個Demo中使用,程式碼如Listing 10.4:

Listing 10.4: The Bootstrap1 Class

package ex10.pyrmont.startup;

import ex10.pyrmont.core.SimpleWrapper;
import ex10.pyrmont.core.SimpleContextConfig;
import ex10.pyrmont.realm.SimpleRealm;

import org.apache.catalina.Connector;
import org.apache.catalina.Context;
import org.apache.catalina.Lifecycle;
import org.apache.catalina.LifecycleListener;
import org.apache.catalina.Loader;
import org.apache.catalina.Realm;
import org.apache.catalina.Wrapper;
import org.apache.catalina.connector.http.HttpConnector;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.deploy.LoginConfig;
import org.apache.catalina.deploy.SecurityCollection;
import org.apache.catalina.deploy.SecurityConstraint;
import org.apache.catalina.loader.WebappLoader;

public final class Bootstrap1 {
  public static void main(String[] args) {

  //invoke: http://localhost:8080/Modern or  http://localhost:8080/Primitive

    System.setProperty("catalina.base", System.getProperty("user.dir"));
    Connector connector = new HttpConnector();

    Wrapper wrapper1 = new SimpleWrapper();
    wrapper1.setName("Primitive");
    wrapper1.setServletClass("PrimitiveServlet");

    Wrapper wrapper2 = new SimpleWrapper();
    wrapper2.setName("Modern");
    wrapper2.setServletClass("ModernServlet");

    Context context = new StandardContext();
    // StandardContext's start method adds a default mapper
    context.setPath("/myApp");
    context.setDocBase("myApp");

    LifecycleListener listener = new SimpleContextConfig();
    ((Lifecycle) context).addLifecycleListener(listener);

    context.addChild(wrapper1);
    context.addChild(wrapper2);
    // for simplicity, we don't add a valve, but you can add
    // valves to context or wrapper just as you did in Chapter 6

    Loader loader = new WebappLoader();
    context.setLoader(loader);
    // context.addServletMapping(pattern, name);
    context.addServletMapping("/Primitive", "Primitive");
    context.addServletMapping("/Modern", "Modern");
    // add ContextConfig. This listener is important because it configures
    // StandardContext (sets configured to true), otherwise StandardContext
    // won't start

    // add constraint
    SecurityCollection securityCollection = new SecurityCollection();
    securityCollection.addPattern("/");
    securityCollection.addMethod("GET");

    SecurityConstraint constraint = new SecurityConstraint();
    constraint.addCollection(securityCollection);
    constraint.addAuthRole("manager");
    LoginConfig loginConfig = new LoginConfig();
    loginConfig.setRealmName("Simple Realm");
    // add realm
    Realm realm = new SimpleRealm();

    context.setRealm(realm);
    context.addConstraint(constraint);
    context.setLoginConfig(loginConfig);

    connector.setContainer(context);

    try {
      connector.initialize();
      ((Lifecycle) connector).start();
      ((Lifecycle) context).start();

      // make the application wait until we press a key.
      System.in.read();
      ((Lifecycle) context).stop();
    }
    catch (Exception e) {
      e.printStackTrace();
    }
  }
}

在Bootstrap1的main()方法中建立了PrimitiveServlet和ModernServlet對應的SimpleWrapper。

然後建立設定StandardContext,新增SimpleContextConfig監聽器等基本如第9章一樣。如下程式碼是新的:

// add constraint
SecurityCollection securityCollection = new SecurityCollection();
securityCollection.addPattern("/");
securityCollection.addMethod("GET");

main()方法建立一個SecurityCollection物件並呼叫其addPattern()和addMethod()方法。addPattern()方法指定安全性的URL約束。addMethod()方法新增受此限制的方法約束。addMethod()方法獲取GET,因此HTTP請求的GET方式將受此安全約束的制約。

接下來,main()方法例項化SecurityConstraint類並將其新增到集合中。它還設定了可以訪問受限資源的角色。通過manager,那些擁有manager角色的使用者將能夠檢視資源。注意在SimpleRealm類只有使用者ken具有manager角色,他的密碼是blackcomb。

SecurityConstraint constraint = new SecurityConstraint();
constraint.addCollection(securityCollection);
constraint.addAuthRole("manager");

接下來,建立了LoginConfig和SimpleRealm物件:

LoginConfig loginConfig = new LoginConfig();
loginConfig.setRealmName("Simple Realm");
// add realm
Realm realm = new SimpleRealm();

然後,將realm, constraint和loginConfig物件和StandardContext相關聯:

context.setRealm(realm);
context.addConstraint(constraint);
context.setLoginConfig(loginConfig);

接下來,啟動context。這部分已在前面幾章討論過。

實際上,當前對PrimitiveServlet和ModernServlet的訪問是受限的。 如果是使用者請求任何servlet,他/她必須使用basic驗證認證。只有在他/她鍵入正確的使用者名稱和密碼(在此,ken和blackcomb),他/她將被允許訪問。

10.6.5 ex10.pyrmont.startup.Boo tstrap2類

Bootstrap2類啟動第2個應用Demo。除了它使用SimpleUserDatabase例項作為領域關聯到StandardContext外,幾乎與Bootstrap1類類似。 要訪問PrimitiveServlet和ModernServlet,正確的使用者名稱和密碼分別是:admin和password。

Listing 10.5: The Bootstrap2 class

package ex10.pyrmont.startup;

import ex10.pyrmont.core.SimpleWrapper;
import ex10.pyrmont.core.SimpleContextConfig;
import ex10.pyrmont.realm.SimpleUserDatabaseRealm;

import org.apache.catalina.Connector;
import org.apache.catalina.Context;
import org.apache.catalina.Lifecycle;
import org.apache.catalina.LifecycleListener;
import org.apache.catalina.Loader;
import org.apache.catalina.Realm;
import org.apache.catalina.Wrapper;
import org.apache.catalina.connector.http.HttpConnector;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.deploy.LoginConfig;
import org.apache.catalina.deploy.SecurityCollection;
import org.apache.catalina.deploy.SecurityConstraint;
import org.apache.catalina.loader.WebappLoader;

public final class Bootstrap2 {
  public static void main(String[] args) {

  //invoke: http://localhost:8080/Modern or  http://localhost:8080/Primitive

    System.setProperty("catalina.base", System.getProperty("user.dir"));
    Connector connector = new HttpConnector();
    Wrapper wrapper1 = new SimpleWrapper();
    wrapper1.setName("Primitive");
    wrapper1.setServletClass("PrimitiveServlet");
    Wrapper wrapper2 = new SimpleWrapper();
    wrapper2.setName("Modern");
    wrapper2.setServletClass("ModernServlet");

    Context context = new StandardContext();
    // StandardContext's start method adds a default mapper
    context.setPath("/myApp");
    context.setDocBase("myApp");
    LifecycleListener listener = new SimpleContextConfig();
    ((Lifecycle) context).addLifecycleListener(listener);

    context.addChild(wrapper1);
    context.addChild(wrapper2);
    // for simplicity, we don't add a valve, but you can add
    // valves to context or wrapper just as you did in Chapter 6

    Loader loader = new WebappLoader();
    context.setLoader(loader);
    // context.addServletMapping(pattern, name);
    context.addServletMapping("/Primitive", "Primitive");
    context.addServletMapping("/Modern", "Modern");
    // add ContextConfig. This listener is important because it configures
    // StandardContext (sets configured to true), otherwise StandardContext
    // won't start

    // add constraint
    SecurityCollection securityCollection = new SecurityCollection();
    securityCollection.addPattern("/");
    securityCollection.addMethod("GET");

    SecurityConstraint constraint = new SecurityConstraint();
    constraint.addCollection(securityCollection);
    constraint.addAuthRole("manager");
    LoginConfig loginConfig = new LoginConfig();
    loginConfig.setRealmName("Simple User Database Realm");
    // add realm
    Realm realm = new SimpleUserDatabaseRealm();
    ((SimpleUserDatabaseRealm) realm).createDatabase("conf/tomcat-users.xml");
    context.setRealm(realm);
    context.addConstraint(constraint);
    context.setLoginConfig(loginConfig);

    connector.setContainer(context);

    try {
      connector.initialize();
      ((Lifecycle) connector).start();
      ((Lifecycle) context).start();

      // make the application wait until we press a key.
      System.in.read();
      ((Lifecycle) context).stop();
    }
    catch (Exception e) {
      e.printStackTrace();
    }
  }
}

10.6.6 執行Demo

在 windows 下執行第一個Demo,可以在工作目錄下面如下執行該程式:

java -classpath ./lib/servlet.jar;./lib/commons-collections.jar;./ex10.pyrmont.startup.Bootstrap1

在 Linux 下,使用冒號分開兩個庫:

java -classpath ./lib/servlet.jar:./lib/commons-collections.jar:./ex10.pyrmont.startup.Bootstrap1

在 windows 下執行第二個Demo,可以在工作目錄下面如下執行該程式:

java-classpath ./lib/servlet.jar;./lib/commons-collections.jar;./lib/commons-digester.jar;./lib/commons-logging.jar;./ex10.pyrmont.startup.Bootstrap2

在 Linux 下,使用冒號分開兩個庫:

java-classpath ./lib/servlet.jar:./lib/commons-collections.jar:./lib/commons-digester.jar:./lib/commons-logging.jar :./ex10.pyrmont.startup.Bootstrap2

在二個Demo中,呼叫PrimitiveServlet和ModernServlet,可以分別使用下面的 URL 來請求:

http://localhost:8080/Primitive
http://localhost:8080/Modern

10.7 小結

安全性是servlet程式設計和servlet規範中的一個重要主題,通過提供安全相關的物件來滿足安全的需要,例如主體(principal),角色(roles),安全約束(securityConstraint),登入配置(login Config)等。在本章中,我們已經學習瞭解到servlet容器如何解決安全性問題。