1. 程式人生 > >tomcat 安全認證 Realm 及 多種型別 Realm 配置

tomcat 安全認證 Realm 及 多種型別 Realm 配置

web.xml 安全配置

Servlet規範支援安全地訪問 web 資源,只需要通過 web.xml 簡單配置即可,其功能由伺服器提供商實現,

web.xml

<!DOCTYPE web-app PUBLIC
        "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
        "http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>
    <security-constraint>
        <web-resource-collection>
            <web-resource-name>some name</web-resource-name>

            <url-pattern>*.jsp</url-pattern>
            <url-pattern>*.do</url-pattern>

            <http-method>GET</http-method>
            <http-method>PUT</http-method>
            <http-method>HEAD</http-method>
            <http-method>TRACE</http-method>
            <http-method>POST</http-method>
            <http-method>DELETE</http-method>
            <http-method>OPTIONS</http-method>
        </web-resource-collection>

        <auth-constraint>
            <role-name>tomcat</role-name>
            <role-name>admin</role-name>
        </auth-constraint>

        <user-data-constraint>
            <transport-guarantee>NONE</transport-guarantee>
            <!--
                這個可選的元素指出在訪問相關資源時使用任何傳輸層保護。
                它必須包含一個transport-guarantee子元素(合法值為NONE、INTEGRAL或CONFIDENTIAL),
                transport-guarantee為NONE值將對所用的通訊協議不加限制。
                INTEGRAL值表示資料必須以一種防止擷取它的人閱讀它的方式傳送。
                雖然原理上(並且在未來的HTTP版本中),在INTEGRAL和CONFIDENTIAL之間可能會有差別,
                但在當前實踐中,他們都只是簡單地要求用SSL。
            -->
        </user-data-constraint>

    </security-constraint>

    <security-constraint>
        <web-resource-collection>
            <web-resource-name>admin page</web-resource-name>
            <url-pattern>/admin.jsp</url-pattern>
        </web-resource-collection>
        <auth-constraint>
            <role-name>admin</role-name>
        </auth-constraint>
    </security-constraint>

    <security-constraint>
        <web-resource-collection>
            <web-resource-name>tomcat page</web-resource-name>
            <url-pattern>/tomcat.jsp</url-pattern>
        </web-resource-collection>
        <auth-constraint>
            <role-name>tomcat</role-name>
        </auth-constraint>
    </security-constraint>

    <login-config>
        <!--<auth-method>BASIC</auth-method>-->

        <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>
    <security-role>
        <role-name>tomcat</role-name>
    </security-role>
    <security-role>
        <role-name>admin</role-name>
    </security-role>

</web-app>

其內容表示:

安全規則:admin.jsp 只能由 admin 角色訪問,tomcat.jsp 只能由 tomcat 角色訪問,其它任意 *.jsp  *.do 可由 admin 或 tomcat 角色訪問。

認證方式:BASIC 是基礎認證方式,由瀏覽器廠商實現的使用者名稱和密碼接收介面;FORM 是應用定製的使用者名稱和密碼接收頁面;

角色宣告:必需宣告出所有用到的角色

tomcat 使用者和角色配置

在tomcat的conf目錄下找到 tomcat-users.xml,新增以下內容:
  <role rolename="tomcat"/>
  <role rolename="admin"/>
  <user username="tomcat" password="tomcat" roles="tomcat"/>
  <user username="both" password="both" roles="tomcat,admin"/>
  <user username="admin" password="admin" roles="admin"/>


Realm配置

tomcat支援多種Realm:
JDBCRealm
DataSourceRealm
JNDIRealm
UserDatabaseRealm
MemoryRealm
JAASRealm
CombinedRealm
LockOutRealm
每種不同的 Realm 採用了不同的 使用者名稱和密碼儲存和使用方式,tomcat預設使用的是 UserDatabaseRealm。

JDBCRealm

這個 Realm 是基於資料庫的,資料庫儲存了 使用者名稱/密碼和使用者的角色,通過建立資料庫的通訊來維持使用者角色資訊
1.所需要的資料庫指令碼
create table users (
  user_name         varchar(15) not null primary key,
  user_pass         varchar(15) not null
);

create table user_roles (
  user_name         varchar(15) not null,
  role_name         varchar(15) not null,
  primary key (user_name, role_name)
);

insert into users ( user_name , user_pass ) values ( 'admin','admin')
insert into users ( user_name , user_pass ) values ( 'tomcat','admin')
insert into user_roles values ( 'admin','admin')
insert into user_roles values ( 'tomcat','tomcat')

2.修改tomcat server.xml 新增安全域配置:
<Realm className="org.apache.catalina.realm.JDBCRealm"
   driverName="org.h2.Driver"
   connectionURL="jdbc:h2:tcp://localhost//home/conquer/mine/work_space/h2-dbpath/tomcat"
   connectionName="sa"
   connectionPassword=""
   userTable="users" userNameCol="user_name" userCredCol="user_pass"
   userRoleTable="user_roles" roleNameCol="role_name"/>
</Realm>


DataSourceRealm

這個安全域和上面的JDBCRealm實現基本一致,只不過不是建立資料庫連線,而是從jndi上下文獲取資料來源,它所需要的資料庫指令碼和JDBCRealm一致。
修改tomcat server.xml 新增安全域配置:
1.在 GlobalNamingResources 節點下新增:
<Resource 
  name="jdbc/h2"
  type="javax.sql.DataSource"
  username="sa"
  password=""
  driverClassName="org.h2.Driver"
  url="jdbc:h2:tcp://localhost//home/conquer/mine/work_space/h2-dbpath/tomcat"/>
2.繼續新增安全域配置(這裡會使用jndi引用上面註冊的資料來源資源):
<Realm className="org.apache.catalina.realm.DataSourceRealm"
   dataSourceName="jdbc/h2"
   userTable="users" userNameCol="user_name" userCredCol="user_pass"
   userRoleTable="user_roles" roleNameCol="role_name"/>
</Realm>

MemoryRealm

這個是最簡單的的配置,預設是讀取 tomcat-users.xml (可通過pathname屬性配置為其它檔案)裡面配置的使用者角色資訊。
修改server.xml 新增配置:
<Realm className="org.apache.catalina.realm.MemoryRealm"/>


UserDatabaseRealm

這個是基於上述 MemoryRealm 擴展出來的,預設也是讀取tomcat-users.xml (可通過pathname屬性配置為其它檔案)裡面配置的使用者角色資訊,
不過他是通過應用jndi的方式實現的,從設計上支援多種實現方式,預設採用了類似MemoryRealm的實現方式,但是又對其進行了擴充套件,主要是增加了“使用者組”的概念,即使用者除了有所屬角色外,還可以有所屬“使用者組”,,使用者組可以關聯其它多個角色,例如它可以使用下面的tomcat-users.xml配置:
  <role rolename="tomcat"/>
  <role rolename="admin"/>
  <user username="tomcat" password="tomcat" roles="tomcat"/>
  <group groupname="one" roles="tomcat"/>
  <user username="admin" password="admin" roles="admin" groups="one"/>

admin使用者由於關聯了 “one”這個使用者組,“one”使用者組包含角色“tomcat”,所以admin使用者就擁有了“tomcat”角色。

這個安全域的配置:
1. sever.xml 的 GlobalNamingResources 節點下新增:
<Resource name="UserDatabase" auth="Container"
              type="org.apache.catalina.UserDatabase"
              description="User database that can be updated and saved"
              factory="org.apache.catalina.users.MemoryUserDatabaseFactory"
              pathname="conf/tomcat-users.xml" />

2.啟用該安全域的配置:
<Realm className="org.apache.catalina.realm.UserDatabaseRealm"
               resourceName="UserDatabase"/>

JAASRealm

這個是基於java的 jaas 認證和授權服務而設計的,主要就是用到了 jaas的認證部分,不涉及“授權”,通過向 javax.security.auth.Subject#getPrincipals()

裡面新增使用者和角色來完成使用者和角色的認證,注意這裡的技巧在於,tomcat要在server.xml的該安全域裡配置哪些java型別的 Principal 表示使用者,哪些java型別的Principal表示角色,因為jaas的登入模組在驗證成功後只能將使用者和角色資訊都放入到javax.security.auth.Subject#getPrincipals(),通過事先的型別約定來讓tomcat識別使用者和角色資訊。

這個安全域的配置如下:
<Realm className="org.apache.catalina.realm.JAASRealm"
                appName="Sample"
			    userClassNames="jaas.SamplePrincipal"
			    roleClassNames="jaas.SampleRolePrincipal"/>

可以看到,上面的配置中指定了jaas.SamplePrincipal表示使用者,而jaas.SampleRolePrincipal表示角色。
可以通過 confiFile 屬性來配置 jaas 需要的認證配置檔案,或者使用java預設 -Djava.security.auth.login.config=xx/jaas.config 引數來指定。
關於jaas的登入模組的實現,請看:Java認證和授權服務 JAAS 之 認證 http://blog.csdn.net/conquer0715/article/details/78204889
Java認證和授權服務 JAAS 之 授權http://blog.csdn.net/conquer0715/article/details/78205755

注意:如果要使用Java認證和授權服務 JAAS 之 認證中的例子,需要進行如下更改:

1. MyLoginModule 檔案:
package jaas;

import javax.security.auth.Subject;
import javax.security.auth.callback.*;
import javax.security.auth.login.FailedLoginException;
import javax.security.auth.login.LoginException;
import javax.security.auth.spi.LoginModule;
import java.security.Principal;
import java.util.Map;

public class MyLoginModule implements LoginModule {

    // username and password
    private String username;
    private char[] password;

    // the authentication status
    private boolean userPwdSucceeded = false;
    private boolean commitSucceeded = false;

    // user's Principal
    private Principal userPrincipal;


    // initial state
    private Subject subject;
    private CallbackHandler callbackHandler;


    /**
     * Initialize this <code>LoginModule</code>.
     */
    public void initialize(Subject subject,
                           CallbackHandler callbackHandler,
                           Map<java.lang.String, ?> sharedState,
                           Map<java.lang.String, ?> options) {

        this.subject = subject;
        this.callbackHandler = callbackHandler;
    }

    /**
     * Authenticate the user by prompting for a user name and password.
     */
    public boolean login() throws LoginException {
        // prompt for a user name and password
        if (callbackHandler == null)
            throw new LoginException("Error: no CallbackHandler available " +
                    "to garner authentication information from the user");

        Callback[] callbacks = new Callback[2];
        callbacks[0] = new NameCallback("user name");
        callbacks[1] = new PasswordCallback("password", false);
//        callbacks[2] = new TextOutputCallback(TextOutputCallback.INFORMATION, "hello, just a msg!");
//        callbacks[3] = new TextOutputCallback(TextOutputCallback.WARNING, "just warn you!");

        try {
            callbackHandler.handle(callbacks);
            NameCallback nameCallback = (NameCallback) callbacks[0];
            PasswordCallback passwordCallback = (PasswordCallback) callbacks[1];

            username = nameCallback.getName();

            char[] tmpPassword = passwordCallback.getPassword();
            passwordCallback.clearPassword();// clean password in memory space
            if (tmpPassword == null) {
                tmpPassword = new char[0];// treat a NULL password as an empty password
            }
            password = new char[tmpPassword.length];
            System.arraycopy(tmpPassword, 0, password, 0, tmpPassword.length);
        } catch (Exception e) {
            e.printStackTrace();
        }


        // verify the username/password
//        boolean usernameCorrect = false;
//        if (username.equals("user")) usernameCorrect = true;
//
//        if (usernameCorrect &&
//                password.length == 3 &&
//                password[0] == 'p' &&
//                password[1] == 'w' &&
//                password[2] == 'd') {
//
//            userPwdSucceeded = true;
//        } else {
//            userPwdSucceeded = false;
//            cleanUserAndPwdData();
//            if (!usernameCorrect) {
//                throw new FailedLoginException("User Name Incorrect");
//            } else {
//                throw new FailedLoginException("Password Incorrect");
//            }
//        }
//        return userPwdSucceeded;
        userPwdSucceeded=true;
        return true;
    }

    public boolean commit() throws LoginException {
        if (!userPwdSucceeded) return false;

        // add a Principal (authenticated identity) to the Subject
        userPrincipal = new SamplePrincipal(username);
        subject.getPrincipals().add(userPrincipal);
        // for tomcat jaas realm
        if (username.equals("admin")) {
            subject.getPrincipals().add(new SampleRolePrincipal("admin"));
        } else if (username.equals("tomcat")) {
            subject.getPrincipals().add(new SampleRolePrincipal("tomcat"));
        }

        // in any case, clean out state
        cleanUserAndPwdData();

        return commitSucceeded = true;
    }

    public boolean abort() throws LoginException {
        if (!userPwdSucceeded) return false;

        if (commitSucceeded) {
            logout();
        } else {
            cleanState();
        }

        return true;
    }

    public boolean logout() throws LoginException {
        subject.getPrincipals().remove(userPrincipal);
        cleanState();
        userPwdSucceeded = commitSucceeded;
        return true;
    }

    private void cleanState() {
        userPwdSucceeded = false;
        cleanUserAndPwdData();
        userPrincipal = null;
    }

    private void cleanUserAndPwdData() {
        username = null;
        if (password != null) {
            for (int i = 0; i < password.length; i++)
                password[i] = ' ';
            password = null;
        }
    }
}

SamplePrincipal 檔案:

package jaas;

import java.security.Principal;

public class SamplePrincipal implements Principal {
    private String name;

    public SamplePrincipal(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

}

SampleRolePrincipal檔案:
package jaas;

public class SampleRolePrincipal extends SamplePrincipal {
    public SampleRolePrincipal(String name) {
        super(name);
    }
}


說明:
另外注意 userClassNames="jaas.SamplePrincipal" 和 roleClassNames="jaas.SampleRolePrincipal" 兩個實現類的 equals 和 hascode 方法,如果覆蓋不好最好不要覆蓋,否則容易 subject.getPrincipals().add 不進去。

測試:

啟動tomcat,部署web應用後,瀏覽器訪問不同的jsp,可以看到安全規則已經生效: admin.jsp 只能由 admin 角色訪問,tomcat.jsp 只能由 tomcat 角色訪問,其它任意 *.jsp  *.do 可由 admin 或 tomcat 角色訪問。