基於Struts2+Spring+iBatis的web應用最佳實踐系列之三(訪問控制篇下)

在本系列的上一篇中我們介紹了一個基於cookie的訪問控制方法,細心的讀者一定會發現,這種方法的實現還是最終基於Struts2的攔截器機制,也就是說它只能保護web應用中的action資源,對於Struts2的應用來說,除了aciton外,一定會有不少的jsp頁面。那麼,我們又該如何實現對於jsp頁面的訪問控制呢?其實對於這樣的問題,筆者在網絡看到過已經不止一次了,下面我們就來介紹一種對於jsp頁面的訪問控制方法。

 

首先,筆者要祭出WEB-INF這個目錄(呵呵,先不要砸我),這個目錄是在servlet規範中定義的,每個web應用都會有,並且用戶是訪問不到的,難麼很自然的一個想法就是把所有的jsp頁面都放在這個目錄下,而所有的請求都通過action來完成。但是這樣也同時引入一個問題,即我們需要爲每個頁面都寫一個對應的action,但是很多情況下,並不需要action中有任何邏輯,僅僅只是做一簡單的跳轉而已。對於這樣的情況,如果我們爲每個jsp頁面都寫一個對應的action就會顯得很繁瑣。

 

筆者在介紹一種可以解決這個問題的方法,首先,我們定義一個dispatcherAction,這個action什麼也不做,只是爲了跳轉而存在,但是繼承一下actionSupport。就象這樣

 

public class DispatcherAction extends ActionSupport {}

 

 然而在struts.xml中我們卻可以這樣配

 

	<package name="demo" namespace="/demo" extends="default">
		<action name="*" class="com.meidusa.demo.web.action.DispatcherAction ">
			<result name="success">/WEB-INF/jsp/demo/{1}.jsp</result>
		</action>
	</package>	

 

 初看上去這種風格的配置有點怪異,不過一旦發現它的強大之處你就會喜歡上它。在這裏,action name我們並沒有指定一個具體的名字,而是用了一個'*'號來表示,'*'號是一個通配符,當沒有全值匹配到一個action name的時候,它可以匹配任何名字,而result頁面中的{1}則會被替換成'*'所代表的值。這樣一來,我們可以只寫一個DispatcherAction(甚至可以不寫),實現對任意jsp頁面的跳轉。假設我們的域名是www.meidusa.com,request請求URL是www.meidusa.com/demo/login.aciton,它會自動跳轉到/WEB-INF/jsp/demo/login.jsp頁面。這種RESTful風格的action Mapping在Struts2的源碼裏作者也直言不諱的指出是受到了Ruby on Rails的啓發,有興趣的朋友可以繼續深入研究一下Restful2ActionMapper這個類,說不定還可以挖掘到更多的寶貝,這裏則不再贅述。

 

好了,現在我們有了DispatcherAction這個類輕鬆實現了對jsp的頁面的跳轉,不過並沒有實現任何的訪問控制功能,下面我們結合上一篇提到的cookie訪問機制,改造一下DispatcherAction這個類。

  

public class DispatcherAction extends ActionSupport implements	ClientCookieAware<Cookie>{

	private Cookie cookie;
	
	public Cookie getCookie() {
		return cookie;
	}
	public void setClientCookie(Cookie cookie) {
		this.cookie = cookie;

	}

}

 

同時我們再寫一個DispatcherCookieNotCareAction類

 

public class DispatcherCookieNotCareAction extends ActionSupport implements  ClientCookieNotCare{

}

 

這樣,當我們需要保護某些jsp頁面,即只有登錄之後才能訪問的頁面使用DispatcherAction這個類來跳轉,而對於不需要保護的頁面使用DispatcherCookieNotCareAction類。不過要記得把我們的cookieInterceptor放到攔截器堆棧中。

 

現在,用我們的DispatcherAction類再結合我們的cookie訪問機制又實現了對除aciton之外的jsp頁面的控制。不過看到這裏,有些朋友可能又要問了,使用'*'通配符那不是匹配了所有的jsp頁面嗎?如果某些jsp頁面僅僅是用作action的返回結果,並不想直接暴露給用戶訪問該怎麼辦呢?在解決這個問題前,我們先來研究一下Struts2中的staticParams攔截器。

 

public class StaticParametersInterceptor extends AbstractInterceptor {

    private boolean parse;
    
    private static final Log LOG = LogFactory.getLog(StaticParametersInterceptor.class);

    public void setParse(String value) {
        this.parse = Boolean.valueOf(value).booleanValue();
    }

    public String intercept(ActionInvocation invocation) throws Exception {
        ActionConfig config = invocation.getProxy().getConfig();
        Object action = invocation.getAction();

        final Map parameters = config.getParams();

        if (LOG.isDebugEnabled()) {
            LOG.debug("Setting static parameters " + parameters);
        }

        // for actions marked as Parameterizable, pass the static parameters directly
        if (action instanceof Parameterizable) {
            ((Parameterizable) action).setParams(parameters);
        }

        if (parameters != null) {
            final ValueStack stack = ActionContext.getContext().getValueStack();

            for (Iterator iterator = parameters.entrySet().iterator();
                 iterator.hasNext();) {
                Map.Entry entry = (Map.Entry) iterator.next();
                stack.setValue(entry.getKey().toString(), entry.getValue());
                Object val = entry.getValue();
                if (parse && val instanceof String) {
                    val = TextParseUtil.translateVariables((String) val, stack);
                }
                stack.setValue(entry.getKey().toString(), val);
            }
        }
        return invocation.invoke();
    }
}

 

這個攔截器已經在默認的攔截器堆棧中,它可以把action配置中的靜態屬性以map的形式注入到action中,當然前題是這個aciton實現Parameterizable的接口

  

public interface Parameterizable {

    public void addParam(String name, Object value);

    void setParams(Map<String, Object> params);

    Map getParams();
}

 

在這裏,我們主要定義兩個屬性,一個includes,一個excludes。includes代表只允許訪問的jsp頁面,excludes代表剔除不允許訪問的頁面之外其他頁面都允許訪問,也就是通常說的黑白名單控制法。這兩個屬性可以都設置,也可以只設置一個,也可以不設置。我們來看一個例子,假設我們有一個profile.jsp放在了/WEB-INF/jsp/demo目錄下。當用戶登錄後跳轉到這個jsp頁面上,顯然這個頁面只有通過login action執行後跳轉而不允許直接訪問。我們可以在action的配置中設置excludes屬性<param name="excludes">profile</param>

 

	<package name="demo" namespace="/demo" extends="default">
		<action name="*" class="com.meidusa.demo.web.action.DispatcherAction ">
			<param name="excludes">profile</param>
			<result name="success">/WEB-INF/jsp/demo/{1}.jsp</result>
			<result name="none">/WEB-INF/jsp/demo/error.jsp</result>
		</action>
	</package>

 

 

現在action已經配置好了,問題是要如何做到對配置好的黑白名單進行控制呢?答案仍舊是用Struts2的攔截器來實現。不知道大家在前面兩個例子中有沒有發現這樣一個有趣的現象,每一個攔截器經常都會有一個配對的由action實現的藉口,比如我們的ClientCookieInterceptor攔截器和ClientCookieAware接口(當然了,還有ClientCookieNotCare);StaticParametersInterceptor攔截器和Parameterizable接口。那在我們討論具體如何實現這個攔截器之前不妨先看一下這個接口應該怎麼實現。 

 

public interface Dispatchable {

	public String getIncludes();
	public String getExcludes();
}

 

 其實很簡單,呵呵,這個Dispatchable接口只有兩個方法getIncludes()和getExcludes()。也就是說這個攔截器在攔截的時候從被攔截的aciton中讀取用StaticParametersInterceptor注入的includes和excludes屬性。但是要注意一點,在配置攔截器堆棧的時候需要把StaticParametersInterceptor放在我們的DispatcherInterceptor攔截器之前,因爲我們的攔截器依賴於它。

 

最後來看一下我們的DispatcherInterceptor攔截器。通過從aciton讀取的includes和excludes屬性判斷這個aciton是否允許被訪問。

 

public class DispatcherInterceptor extends AbstractInterceptor {

	@Override
	public String intercept(ActionInvocation invocation) throws Exception {
		if (invocation.getAction() instanceof Dispatchable){
			Dispatchable action = (Dispatchable)invocation.getAction();
			ActionContext context = ActionContext.getContext();
			
			//判斷這個aciton是否允許被訪問
			boolean allow = apply(action.getExcludes(), action.getIncludes(), context.getName());
			if (!allow){
				return Action.NONE;
			}
		}
		return invocation.invoke();
	}

		//黑白名單控制規則
    public static boolean apply(Set excludes, Set includes, String action) {
    	if (((excludes.contains("*") && !includes.contains("*"))
                || excludes.contains(action))
                && !includes.contains(action)) {
            return false;
        }
        return includes.size() == 0 || includes.contains(action) || includes.contains("*");
    }

    public static boolean apply(String excludes, String includes, String action) {
    	Set includesSet = TextParseUtil.commaDelimitedStringToSet(includes == null? "" : includes);
    	Set excludesSet = TextParseUtil.commaDelimitedStringToSet(excludes == null? "" : excludes);
    	
    	return apply(excludesSet, includesSet, action);
    }
}

  

最後我們再來看一下最終修改後的DispatcherAction

 

public class DispatcherAction extends ActionSupport implements	ClientCookieAware<Cookie>, Dispatchable, Parameterizable{

	private Cookie cookie;
	private Map params;
	
	public String getIncludes() {
		return (String)params.get("includes");
	}
	public String getExcludes() {
		return (String)params.get("excludes");
	}
	public Cookie getCookie() {
		return cookie;
	}
	public void setClientCookie(Cookie cookie) {
		this.cookie = cookie;

	}
    public void addParam(String name, Object value){
    	this.params.put(name, value);
    }

    public void setParams(Map<String, Object> params){
    	this.params = params;
    }

    public Map getParams(){
    	return params;
    }

}

 

 最終修改後的DispatcherCookieNotCareAction

public class DispatcherCookieNotCareAction extends ActionSupport implements
ClientCookieNotCare, Dispatchable, Parameterizable {
	private Map params;
	
	public String getIncludes() {
		return (String)params.get("includes");
	}
	public String getExcludes() {
		return (String)params.get("excludes");
	}
    public void addParam(String name, Object value){
    	this.params.put(name, value);
    }

    public void setParams(Map<String, Object> params){
    	this.params = params;
    }

    public Map getParams(){
    	return params;
    }
}

 

至此,我們同樣成功實現了對jsp頁面的訪問控制。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章