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

訪問控制對於一個web應用來說幾乎是不可或缺的。當訪問web應用當中的某些資源時,如果你這時還沒有登錄,那麼就會被重定向到登錄頁面,只有在登錄之後纔會被允許訪問。經常上網的朋友對這樣一個場景一定不會陌生。那麼,如何實現對部分訪問受限的url進行保護呢?筆者在這裏向大家介紹一種基於cookie的實現方法。

 

 當用戶登錄的時候我們就在httpResponse中寫入一個cookie,這個cookie就成爲了一個標誌。服務端每次收到http請求的時候就會先檢查所請求的資源是否受到保護,如果是的話則會檢查httpRequest中的cookie是否有效,只有檢查通過了的才被允許訪問,如果cookie無效則會被重定向到登錄頁面。對於那些不受訪問限制的url則一律通過。

 

既然是基於Struts2的web應用,看到這樣一個需求不禁讓人想起了Struts2裏的攔截器,而仔細研究Struts2之後也發現在它的發行包里正好也有一個cookie攔截器。

 

Struts2的cookie攔截器

<interceptor name="cookie" class="org.apache.struts2.interceptor.CookieInterceptor"/>

 

 這個攔截器沒有出現在Struts2的默認攔截器棧中,不過並不妨礙我們對其研究一番。首先這個攔截器有兩個方法分別接受cookieName和cookieValue的參數。

 

    /**
     * Set the <code>cookiesName</code> which if matche will allow the cookie
     * to be injected into action, could be comma-separated string.
     *
     * @param cookiesName
     */
    public void setCookiesName(String cookiesName) {
        if (cookiesName != null)
            this.cookiesNameSet = TextParseUtil.commaDelimitedStringToSet(cookiesName);
    }

    /**
     * Set the <code>cookiesValue</code> which if matched (together with matching
     * cookiesName) will caused the cookie to be injected into action, could be
     * comma-separated string.
     *
     * @param cookiesValue
     */
    public void setCookiesValue(String cookiesValue) {
        if (cookiesValue != null)
            this.cookiesValueSet = TextParseUtil.commaDelimitedStringToSet(cookiesValue);
    }

 

cookieName和cookieValue的參數可以在Struts2的配置文件中配置。

 

 <action ... >
    <interceptor-ref name="cookie">
        <param name="cookiesName">cookie1, cookie2</param>
        <param name="cookiesValue">cookie1value, cookie2value</param>
    </interceptor-ref>
    ....
 </action>

 

 在這個攔截器的核心攔截方法中,它會從httpRequest中讀取cookie,由populateCookieValueIntoStack方法將讀取的cookie保存在一個map中。對於具體讀取哪些cookie是由cookiesName和cookiesValue參數共同決定的,不過規則有些複雜和繁瑣,這裏就不具體展開討論了。

 

    public String intercept(ActionInvocation invocation) throws Exception {

        if (LOG.isDebugEnabled())
            LOG.debug("start interception");

        final ValueStack stack = ActionContext.getContext().getValueStack();
        HttpServletRequest request = ServletActionContext.getRequest();

        // contains selected cookies
        Map cookiesMap = new LinkedHashMap();

        Cookie cookies[] = request.getCookies();
        if (cookies != null) {
            for (int a=0; a< cookies.length; a++) {
                String name = cookies[a].getName();
                String value = cookies[a].getValue();

                if (cookiesNameSet.contains("*")) {
                    if (LOG.isDebugEnabled())
                        LOG.debug("contains cookie name [*] in configured cookies name set, cookie with name ["+name+"] with value ["+value+"] will be injected");
                    populateCookieValueIntoStack(name, value, cookiesMap, stack);
                }
                else if (cookiesNameSet.contains(cookies[a].getName())) {
                    populateCookieValueIntoStack(name, value, cookiesMap, stack);
                }
            }
        }

        injectIntoCookiesAwareAction(invocation.getAction(), cookiesMap);

        return invocation.invoke();
    }

 

 最後將包含cookie的map注入到action中,當然,前提是這個action實現CookiesAware這個接口。

 

    /**
     * Hook that set the <code>cookiesMap</code> into action that implements
     * {@link CookiesAware}.
     *
     * @param action
     * @param cookiesMap
     */
    protected void injectIntoCookiesAwareAction(Object action, Map cookiesMap) {
        if (action instanceof CookiesAware) {
            if (LOG.isDebugEnabled())
                LOG.debug("action ["+action+"] implements CookiesAware, injecting cookies map ["+cookiesMap+"]");
            ((CookiesAware)action).setCookiesMap(cookiesMap);
        }
    }

 

研究完之後我們發現這個攔截器還是挺簡單的,僅僅只是從httpRequest中讀取cookie並注入到action中,裏我們的需求還有很遠的距離。一般的思路是在這個攔截器的基礎上繼續構造我們需要的東西。即在登錄action中在登錄成功之後將身份信息寫入到cookie中,在攔截器中則驗證cookie是否有效,如果無效則重定向到登錄頁面。這個實現方法看似可行,但仔細想想卻存在着很多問題。

 

第一,登錄action和攔截器變得過於臃腫。我們需要造登錄action中不僅要加入寫入cookie的代碼,出於安全性的考慮,還要在寫入cookie前對其進行加密處理,否則惡意用戶可以僞造cookie帶來安全隱患。攔截器中不僅要對cookie解密還要判斷其是否有效。面向對象程序設計的原則告訴我們好的設計需要做到高聚合,也就是說一個類只做一件事情,顯然用戶登錄屬於業務層面的邏輯而處理cookie則屬於比較底層的操作,將這二者放到一起肯定不是好的設計。

 

第二,降低了組件的可複用性。登錄的邏輯是應用特有的,也就是說每一個應用都有它自己的登錄邏輯,但是對cookie的操作或者說對於“只有在登錄之後纔會被允許訪問某些受保護的資源,否則會被重定向到登錄頁面”這樣的需求卻是普遍存在的,既然如此我們應該可以把這樣的需求抽象出來,做成可複用的組件,編入我們自己的工具庫,以供不同的應用使用。

 

 下面我們就來實現一種同樣基於cookie的可複用的解決方案。

 

 首先我們要設計好我們的ClientCookie類,這個ClientCookie類必須是對用戶友好的,也就是說用戶可以對其進行擴展但又不必關心底層的細節,我們可以通過提供一個對用戶關心的ClientCookie與底層的httpCookie的映射來達到這個目的。同時我們把一些底層的數據,比如ip,時間戳等,login等封裝在一個cookieMask中,從而把我們的ClientCookie類解放出來。下面是這個cookieMask類的代碼,同時我們以一定的編碼規則將這個cookieMask類映射到一個單獨的httpCookie中。

 

public class ClientCookieMask implements Serializable{
	private static final long serialVersionUID = 1L;
	public static char SPLITER = '|';
	private long lastTime= -1;
	private long firstTime = -1;
	private String loginId;
	private String clientIp;
	
	private boolean valid = false;;
	/**
	 * mask 編碼規則:
	 * loginId + '|' + IP + '|' + 'first time' +'|'+'last time'
	 */
	public ClientCookieMask(String mask){
		String tmp[] = StringUtils.split(mask, SPLITER);
		
		if(tmp!= null && tmp.length==4){
			loginId = tmp[0];
			clientIp = tmp[1];
			try{
				firstTime = Long.parseLong(tmp[2]);
			}catch(NumberFormatException e){
				firstTime = 0;
				valid = false;
				return;
			}
			try{
				lastTime = Long.parseLong(tmp[3]);
			}catch(NumberFormatException e){
				lastTime = 0;
				valid = false;
				return;
			}
			valid = true;
		}else{
			valid = false;
		}
	}
	
	public boolean isValid() {
		return valid;
	}

	public long getLastTime() {
		return lastTime;
	}
	public void setLastTime(long lastTime) {
		this.lastTime = lastTime;
	}
	public long getFirstTime() {
		return firstTime;
	}
	public void setFirstTime(long firstTime) {
		this.firstTime = firstTime;
	}
	public String getClientIp() {
		return clientIp;
	}
	public void setClientIp(String clientIp) {
		this.clientIp = clientIp;
	}
	public String getLoginId() {
		return loginId;
	}
	public void setLoginId(String loginId) {
		this.loginId = loginId;
	}
	
	public String toString(){
		StringBuffer buffer = new StringBuffer();
		buffer.append(this.getLoginId());
		buffer.append(ClientCookie.SPLITER).append(clientIp);
		buffer.append(ClientCookie.SPLITER).append(firstTime);
		buffer.append(ClientCookie.SPLITER).append(System.currentTimeMillis());
		return buffer.toString();
	}
}

 

 在我們的ClientCookie類中,保存有一個對cookieMask的引用,同時我們還有一個map,每次調用這個cookie 的setter方法的時候,同時把set過來的值保存到這個map中,在代碼中看不到這個過程,因爲這是通過cglib實現的。cglib(Code Generation Library)是一個強大的、高性能、高質量的Code生成類庫。它可以在運行期擴展Java類與實現Java接口。cglib封裝了asm,可以在運行期動態生成新的class。

 

 

public abstract class ClientCookie {
	private ClientCookieWriter cookieWriter;
	public static char SPLITER = '|';
	public static String DATE_FORMAT = "yyyy-MM-dd HH:mm:ss";
	
	//單位分鐘
	private long maxLifeTime = -1;
	
	//單位分鐘
	private long maxIdleTime = -1;
	
	public long getMaxIdleTime() {
		return maxIdleTime;
	}

	public void setMaxIdleTime(long maxIdleTime) {
		this.maxIdleTime = maxIdleTime;
	}

	boolean inited = false;
	
	//對ClientCookie使用setter方式的同時會把set的值保存到這個map中
	Map<String,Object> settedMap;
	boolean ipValidate = false; 
	String clientIp;
	
	//對cookieMask的引用
	private ClientCookieMask cookieMask;
	

	/**
	 * mask 編碼規則:
	 * loginId + '|' + IP + '|' + 'first time' +'|'+'last time'
	 */
	private String mask;
	
	public long getMaxLifeTime() {
		return maxLifeTime;
	}

	public void setMaxLifeTime(long idleTime) {
		this.maxLifeTime = idleTime;
	}

	public String getClientIp() {
		return clientIp;
	}

	public abstract String getLoginId(); 
	
	//判斷cookie是否有效
	public boolean isValid(){
		if(StringUtils.isEmpty(mask))return false;
		ClientCookieMask cookieMask = getCookieMask();
		if(cookieMask != null){
			return cookieMask.isValid() 
				&& StringUtils.equals(getLoginId(), cookieMask.getLoginId())
				&& validDuringTime(cookieMask.getFirstTime())
				&& validIdleTime(cookieMask.getLastTime())
				&& (ipValidate?StringUtils.equals(getClientIp(), cookieMask.getClientIp()):true);
		}else{
			return false;
		}
	}
	
	//生成cookieMask
	protected String generatMask(){
		StringBuffer buffer = new StringBuffer();
		buffer.append(this.getLoginId());
		buffer.append(ClientCookie.SPLITER).append(clientIp);
		buffer.append(ClientCookie.SPLITER).append(System.currentTimeMillis());
		buffer.append(ClientCookie.SPLITER).append(System.currentTimeMillis());
		
		return buffer.toString();
	}
	
	protected boolean validIdleTime(long lastTime){
		if(maxIdleTime >0){
			if((System.currentTimeMillis() - lastTime)<maxIdleTime * 60 * 1000){
				return true;
			}else{
				return false;
			}
		}
		return true;
	}
	
	/**
	 * lastTime
	 * @param lastTime
	 * @return
	 */
	protected boolean validDuringTime(long lastTime){
		if(maxLifeTime >0){
			if((System.currentTimeMillis() - lastTime)<maxLifeTime * 60 * 1000){
				return true;
			}else{
				return false;
			}
		}else{
			return true;
		}
	}
	
	void setCookieWriter(ClientCookieWriter writer){
		this.cookieWriter = writer;
	}
	
	public String getMask() {
		return mask;
	}
	
	public ClientCookieMask getCookieMask() {
		if(cookieMask == null){
			cookieMask = new ClientCookieMask(mask);
		}
		return cookieMask;
	}

	public void setMask(String mask) {
		this.mask = mask;
	}
	
	/**
	 * 持久化cookie(客戶端或者cookieServer)
	 */
	public void save(){
		setMask(generatMask());
		cookieWriter.writeCookie(this);
		settedMap.clear();
	}
	
	public void update(){
		getCookieMask().setLastTime(System.currentTimeMillis());
		setMask(this.getCookieMask().toString());
		cookieWriter.writeCookie(this);
		settedMap.clear();
	}
	
	public void clear(){
		setMask(null);
		cookieWriter.writeCookie(this);
		settedMap.clear();
	}
}

 

至此我們已經有了cookie類,但是還需建立客戶友好的cookie與httpCookie之間的映射,我們把這個映射放在一個cookieMapping.xml的配置文件中。

 

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE cookieMapping SYSTEM "cookieMapping.dtd">
<cookieMapping
	cookieClass="com.meidusa.demo.web.Cookie"
	encryptKey="ei*736TR"
	loginUrl="http://www.meidusa.com:8080/demo/login.html" algorithm="DES" maxLifeTime="1440" maxIdleTime="-1">
	
	<cookie cookieName="meidusa_cookie2" innerCookieName="loginId">
		<property name="age">1000</property>
		<property name="domain">www.meidusa.com</property>
		<property name="writable">true</property>
		<property name="secure">true</property>
		<property name="path">/</property>
	</cookie>

	<cookie cookieName="meidusa_mask" innerCookieName="mask">
		<property name="age">1000</property>
		<property name="domain">www.meidusa.com</property>
		<property name="writable">true</property>
		<property name="secure">true</property>
		<property name="path">/</property>
	</cookie>
</cookieMapping>

 

 dtd定義這裏就不介紹了,cookieClass指的是用戶自己定義的cookie類,這個類需要繼承我們上面的ClientCookie,encryptKey是我們用來對cookie加密的key,這裏的加密指的是在將cookie寫入到httpCookie時的加密,loginUrl當用戶沒有登錄訪問受保護的資源時被重定向到的登錄頁面的url。algorithm是指加密算法的類型,maxLifeTime是指cookie的最長有效時間,maxIdleTime是指最長空閒時間,-1代表沒有這個限制。

 

下面的cookie項代表了每一個cookie映射,cookieName代表的是底層的httpCookie的名字,innerCookieName指的是對客戶友好的cookie名,這裏我們可以看到我們把ClientCookie中的cookeMask拿出來映射成了一個httpCookie,又把loginId單獨拿出來映射成了一個httpCookie。當然,用戶可以根據自己的需要定製自己的ClientCookie類,並自己定義這個cookie的映射關係。

 

 講到這裏,細心的讀者可能已經發現,這個cookie的映射配置文件我們在上一篇文章中在講自動配置的時候其實已經拿出來看過。是的,你猜對了,我們可以進一步的使用我們的autoconfig插件來管理這個配置文件,我們把部署特有的配置放到properties文件中,使用autoconfig通過模板生成我們真正需要的配置文件。

 

cookie.algorithm   = DES
cookie.domain      = www.meidusa.com
cookie.encryptKey  = ei*736TR
cookie.loginUrl    = http://www.meidusa.com:8080/demo/login.xml

 

cookieMapping的模板文件

 

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE cookieMapping SYSTEM "cookieMapping.dtd">
<cookieMapping
	cookieClass="com.meidusa.demo.web.Cookie"
	encryptKey="${cookie_encryptKey}"
	loginUrl="${cookie_loginUrl}" algorithm="${cookie_algorithm}" maxLifeTime="1440" maxIdleTime="-1">
	
	<cookie cookieName="meidusa_cookie2" innerCookieName="loginId">
		<property name="age">1000</property>
		<property name="domain">${cookie_domain}</property>
		<property name="writable">true</property>
		<property name="secure">true</property>
		<property name="path">/</property>
	</cookie>
	
	<cookie cookieName="meidusa_mask" innerCookieName="mask">
		<property name="age">1000</property>
		<property name="domain">${cookie_domain}</property>
		<property name="writable">true</property>
		<property name="secure">true</property>
		<property name="path">/</property>
	</cookie>
</cookieMapping>

 

我們在servlet容器啓動的時候通過我們自己的一個listenser來載入cookieMapping配置文件。

	<context-param>
		<param-name>cookieMappingFile</param-name>
		<param-value>${project.home}/conf/cookieMapping.xml</param-value>
	</context-param>

	<listener>
		<listener-class>
			com.meidusa.toolkit.web.cookie.ClientCookieContextLoaderListener
		</listener-class>
	</listener>

 

這個listener的聲明如下

public class ClientCookieContextLoaderListener implements ServletContextListener,ClientCookieWriter,ClientCookieReader,ClientNoLoginRedirector

 ServletContextListener接口是servlet API的一部分,這裏就不討論了。除了ServletContextListener接口之外,這個listener還實現了ClientCookieWriter,ClientCookieReader,ClientNoLoginRedirector這三個接口。

 

public interface ClientCookieReader {
	public ClientCookie readCookie(HttpServletRequest request) throws Exception;
}

public interface ClientCookieWriter {
	public void writeCookie(ClientCookie cookie);
}

public interface ClientNoLoginRedirector {
	public String getRedirectUrl(HttpServletRequest request);
}

 從上面的聲明中可以看到,這三個接口的功能分別是用來讀取,寫入cookie和重定向httpRequest。

 

也就是說我們的listener不僅負責裝配配置文件,同時還負責對cookie 的底層操作。

 

從下面的代碼中可以看到在容器初始化的時候這個listener負責裝配cookieMapping的配置文件,同時也做一些初始化工作。具體的裝配配置文件的代碼這裏就不貼了。

 

	public void contextInitialized(ServletContextEvent sce) {
		cookieMappingFile = sce.getServletContext().getInitParameter("cookieMappingFile");
		this.setCookieMappingFile(cookieMappingFile);
		init();
	}
	
	public synchronized void init(){
		if(!inited){
			cookieMappingFile = ConfigUtil.filter(cookieMappingFile);
			loadConfig(cookieMappingFile);
			ClientCookieFactory.getInstance().setReader(this);
			ClientCookieFactory.getInstance().setWriter(this);
			ClientCookieFactory.getInstance().setRedirector(this);
			inited = true;
			algorithm = SymmetricAlgorithm.valueOf(SymmetricAlgorithm.class, cookieConfig.getAlgorithm());
		}
	}

 

對底層cookie操作的代碼,在讀取cookie的時候,我們這裏用到了cglib中的Enhancer和MethodInterceptor來攔截通過setter對cookie的訪問,將set過來的值同步保存到一個map中。

	public void writeCookie(ClientCookie cookie) {
		if(cookie.settedMap != null){
	    	for(Map.Entry<String, Object> entryProp : cookie.settedMap.entrySet()){
	    		ClientCookieConfigEntry entry = cookieInnerNameMapping.get(entryProp.getKey());
	    		if(entry!= null && entry.isWritable()){
	    			javax.servlet.http.Cookie httpCookie = new javax.servlet.http.Cookie(entry.getCookieName(),"");
					httpCookie.setDomain(entry.getDomain());
					httpCookie.setPath(entry.getPath());
					httpCookie.setVersion(1);
	    			Object value;
					try {
						value = entryProp.getValue();
						if(value == null){
							httpCookie.setMaxAge(0);
						}else{
							httpCookie.setMaxAge(entry.getAge());
							String cookieValue = value.toString();
							if(cookieValue != null && entry.isSecure()){
								cookieValue = encrypt(cookieValue);
							}
							httpCookie.setValue(cookieValue);
						}
		    			ServletActionContext.getResponse().addCookie(httpCookie);
					} catch (Exception e) {
						logger.error("write cookie error",e);
					}
	    			
	    		}
	    	}
		}
	}
	
	

	public ClientCookie readCookie(HttpServletRequest request) throws Exception {
		ClientCookie cookie = (ClientCookie)Enhancer.create(Class.forName(cookieConfig.getCookieClass()),cookieSetterInterceptor);
		cookie.setCookieWriter(this);
		cookie.ipValidate = this.ipValidate;
		cookie.clientIp = request.getRemoteAddr();
		cookie.setMaxLifeTime(cookieConfig.getMaxLifeTime());
		cookie.setMaxIdleTime(cookieConfig.getMaxIdleTime());
		cookieMapping(cookie,request);
		cookie.inited = true;
		return cookie;
		
	}

	public String getRedirectUrl(HttpServletRequest request) {
		String query = request.getQueryString();
		String fullQuery = request.getRequestURL()+ (query == null?"":("?"+ query));
		String encoded = fullQuery;
		try {
			encoded = URLEncoder.encode(fullQuery,"utf8");
		} catch (UnsupportedEncodingException e) {
		}
		return cookieConfig.getLoginUrl()+"?redirect="+encoded;
	}

 

在readCookie中的cookieSetterInterceptor是一個MethodInterceptor (注意,這裏的MethodInterceptor並不是Struts2中的Interceptor,而是cglib中的MethodInterceptor)

class ClientCookieMothedInterceptor implements MethodInterceptor {
		public Object intercept(Object object, Method method, Object[] args,
				MethodProxy methodProxy) throws Throwable {
			ClientCookie cookie = (ClientCookie)object;
			Object result = methodProxy.invokeSuper(cookie,args);
			if(cookie.inited){
				if((method.getModifiers() & Modifier.PUBLIC)>0){
					String name = method.getName();
					if(name.startsWith("set") && name.length() > 3){
						char[] fieldChars = name.toCharArray();
						fieldChars[3] = Character.toLowerCase(fieldChars[3]);
						
						String fieldName = new String(fieldChars,3,fieldChars.length-3);
						
						if(cookie.settedMap == null){
							cookie.settedMap = new HashMap<String ,Object>();
						}
						cookie.settedMap.put(fieldName, args[0]);
					}
				}
			}
			return result;
		}
	};

 

現在,我們已經有了自己的cookie類,cookie的映射,以及底層操作的api,最後我們再來看一下如何利用Struts2中的攔截器來完成最終的控制訪問。在這裏,通過繼承,我們自己實現了一個攔截器。

 

public class ClientCookieInterceptor extends AbstractInterceptor {
	private static final long serialVersionUID = 1L;
	private static Logger logger = Logger.getLogger(ClientCookieInterceptor.class);
	

	public void init() {
		super.init();
		
    }

	@SuppressWarnings("unchecked")
	@Override
	public String intercept(ActionInvocation invocation) throws Exception {
		ClientCookie cookie = null;
		HttpServletRequest request = ServletActionContext.getRequest();
		try{
			cookie = ClientCookieFactory.getInstance().readCookie(request);
		}catch(Exception e){
			logger.error("mapping cookie error",e);
			return redirect();
		}
		if(!(invocation.getAction() instanceof ClientCookieNotCare)){
			if(!cookie.isValid()){
				return redirect();
			}
		}
		
		if(invocation.getAction() instanceof ClientCookieAware){
			ClientCookieAware cookieAware = (ClientCookieAware)invocation.getAction();
			cookieAware.setClientCookie(cookie);
		}
		request.setAttribute("clientCookie", cookie);
		
		if(!(invocation.getAction() instanceof ClientCookieNotCare)){
			if(cookie != null){
				if(cookie.getCookieMask() != null){
					cookie.update();
				}
			}
		}
		return invocation.invoke();
	}
	
	protected String redirect() throws IOException{
		HttpServletRequest request = ServletActionContext.getRequest();
		String redirectUrl = ClientCookieFactory.getInstance().getRedirectUrl(request);
		ServletActionContext.getResponse().sendRedirect(redirectUrl);
		return Action.NONE;
	}

}

 

在這個攔截器中會檢查,如果用戶action沒有實現ClientCookieNotCare接口並且cookie無效則會被重定向到登錄頁面;如果用戶action實現了ClientCookieAware接口則會自動注入cookie以供用戶使用。

 

ClientCookieNotCare和ClientCookieAware接口的定義

 

public interface ClientCookieAware<T extends ClientCookie> {
	public void setClientCookie(T t);
}

public interface ClientCookieNotCare {

}

 

最後別忘了在struts.xml配置一下這個攔截器,並把它一併放入常用的攔截器棧中

 

<interceptor name="cookieInterceptor"	class="com.meidusa.toolkit.web.cookie.ClientCookieInterceptor" />

 

			<interceptor-stack name="cookieDefaultStack">
				<interceptor-ref name="cookieInterceptor" />
				<interceptor-ref name="defaultStack"></interceptor-ref>
			</interceptor-stack>

 

至此,我們應該已經成功實現了一種基於cookie的訪問控制方法。

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