Spring Httpinvoker服務端安全策略 - 純Java Config版

純Java Config非XML設定之純Spring Httpinvoker搭配簡易Token。

httpclient-4.5.10.jar、httpcore-4.4.12.jar。

在設計應用程式時,考量到安全性,企業會將伺服器分成前後台,前台負責產出操作介面讓使用者使用,後台則是真正的商業邏輯或是連線資料庫之動作。

前台放在網域開放區,後台藏在防火牆內的安全區域,由防火牆控管只能由允許的IP才能打到後台提供的服務。 類似RMI的概念,後台提供服務,但為了確保不被攻擊,用防火牆的方式做控管。

但防火牆還是有可能被攻破的,一旦失守,後台就如同失去城牆的皇宮,被人狂打服務任人宰割了。 在經過一些資料搜尋後,我發現一篇很有趣的文章

https://my.oschina.net/GameKing/blog/422303

此文章的作者表示單純用Controller暴露接口的寫法太過於繁雜,因此用httpinvoker的機制做了些小調整使其有安全性功能做改寫(不過被負責人否決,所以只能將此技術分享在部落格)。

我看了之後覺得很有趣,並且認為這技術應該實際上應用是可行的。原文是用XML Based的方式完成,我個人比較喜好用Java Annotation的方式,因此實作了一個純Java Config的版本。

 

先介紹整體實作架構(請參考下圖),我認為Spring MVC httpinvoker的主要精神是,利用Java的介面(Interface)與實作類別完成頁面邏輯(前台顯示)與商業邏輯(後台計算)的切割。

藉由繼承與實作相關Invoker的類別與介面(圖中自訂的TkuHttpInvokerProxyFactoryBean),設定好介面、實作類別、後台呼叫位置與入口(如下圖中的前台呼叫點),最後在Controller裡呼叫介面的方法達到呼叫後台實作類別的目的。

以下開始介紹程式碼的部分(我只放重點部分)。

1.首先是我用來呼叫的TaskInterfaceBO介面類別,裡面有一個方法,預設這個方法回傳一個叫做TransferBean的自定義類別。

public interface TaskInterfaceBO {
	
	public TransferBean getResultFromBackEnd();

}
public class TransferBean implements Serializable {
	
	private String result = "";

	public String getResult() {
		return result;
	}

	public void setResult(String result) {
		this.result = result;
	}
	
}

2.接下來是他的實作類別TaskInterfaceBOImpl,實作時會產生一個TransferBean物件實體並塞入"You Got Result"字串回傳。

public class TaskInterfaceBOImpl implements TaskInterfaceBO {
	
	@Override
	public TransferBean getResultFromBackEnd() {
		
		TransferBean tb = new TransferBean();
		tb.setResult("You Got Result");
		
		return tb;
	}
}

3.設定前台呼叫點的TkuHttpInvokerProxyFactoryBean,繼承了一個自定義的類別TkuHttpInvokerClientInterceptor(這段期時就跟原文的Code是一樣的,只是我在Token的地方改用Autowired的方式生成)。

public class TkuHttpInvokerProxyFactoryBean extends TkuHttpInvokerClientInterceptor implements FactoryBean<Object> {
	private Object serviceProxy;

	@Override
	public void afterPropertiesSet() {
		super.afterPropertiesSet();
		if (getServiceInterface() == null) {
			throw new IllegalArgumentException("Property 'serviceInterface' is required");
		}
		this.serviceProxy = new ProxyFactory(getServiceInterface(),this).getProxy(getBeanClassLoader());
	}
	
	@Override
	public Object getObject() {
		return this.serviceProxy;
	}
	
	@Override
	public Class<?> getObjectType() {
		return getServiceInterface();
	}
	
	@Override
	public boolean isSingleton() {
		return true;
	}

}
public class TkuHttpInvokerClientInterceptor extends RemoteInvocationBasedAccessor
        implements MethodInterceptor,HttpInvokerClientConfiguration {

    private String codebaseUrl;

    private HttpInvokerRequestExecutor httpInvokerRequestExecutor;

    /**
     * 驗證參數,存放服務端需要的認證信息,並用認證信息生成密鑰
     */
    @Autowired private TokenBean tokenBean;

    public void setCodebaseUrl(String codebaseUrl) {
        this.codebaseUrl = codebaseUrl;
    }

    public String getCodebaseUrl() {
        return this.codebaseUrl;
    }

    public void setHttpInvokerRequestExecutor(HttpInvokerRequestExecutor httpInvokerRequestExecutor) {
        this.httpInvokerRequestExecutor = httpInvokerRequestExecutor;
    }

    /**
     * 返回一个HTTP請求執行器
     * 將默認執行器修改為CommonsHttpInvokerRequestExecutor
     */
    public HttpInvokerRequestExecutor getHttpInvokerRequestExecutor() {
        if (this.httpInvokerRequestExecutor == null) {
        	HttpComponentsHttpInvokerRequestExecutor  executor = new HttpComponentsHttpInvokerRequestExecutor ();
            executor.setBeanClassLoader(getBeanClassLoader());
            this.httpInvokerRequestExecutor = executor;
        }
        return this.httpInvokerRequestExecutor;
    }
    
    @Override
    public void afterPropertiesSet() {
        super.afterPropertiesSet();
        // Eagerly initialize the default HttpInvokerRequestExecutor, if needed.
        getHttpInvokerRequestExecutor();
    }

    /**
     * 重寫調用方法,向RemoteInvocation中添加項目需要的驗證信息
     */
    public Object invoke(MethodInvocation methodInvocation) throws Throwable {
        if (AopUtils.isToStringMethod(methodInvocation.getMethod())) {
            return "HTTP invoker proxy for service URL [" + getServiceUrl() + "]";
        }

        RemoteInvocation invocation = createRemoteInvocation(methodInvocation);
        try {
            //生成並寫入驗證信息
            if(tokenBean != null){
                if(invocation.getAttributes() == null){
                    invocation.setAttributes(tokenBean.getSecurityMap());
                }else{
                    invocation.getAttributes().putAll(tokenBean.getSecurityMap());
                }
            }
        }catch (Exception e){
            logger.error("設置驗證參數發生異常,請求將可能被服務端攔截...", e);
        }

        RemoteInvocationResult result = null;

            result = executeRequest(invocation, methodInvocation);
  

        try {
            return recreateRemoteInvocationResult(result);
        }
        catch (Throwable ex) {
            if (result.hasInvocationTargetException()) {
                throw ex;
            }
            else {
                throw new RemoteInvocationFailureException("Invocation of method [" + methodInvocation.getMethod() +
                        "] failed in HTTP invoker remote service at [" + getServiceUrl() + "]", ex);
            }
        }
    }


    protected RemoteInvocationResult executeRequest(
            RemoteInvocation invocation, MethodInvocation originalInvocation) throws Exception {

        return executeRequest(invocation);
    }

    protected RemoteInvocationResult executeRequest(RemoteInvocation invocation) throws Exception {
        return getHttpInvokerRequestExecutor().executeRequest(this, invocation);
    }


    protected RemoteAccessException convertHttpInvokerAccessException(Throwable ex) {
        if (ex instanceof ConnectException) {
            throw new RemoteConnectFailureException(
                    "Could not connect to HTTP invoker remote service at [" + getServiceUrl() + "]", ex);
        }
        else if (ex instanceof ClassNotFoundException || ex instanceof NoClassDefFoundError ||
                ex instanceof InvalidClassException) {
            throw new RemoteAccessException(
                    "Could not deserialize result from HTTP invoker remote service [" + getServiceUrl() + "]", ex);
        }
        else {
            throw new RemoteAccessException(
                    "Could not access HTTP invoker remote service at [" + getServiceUrl() + "]", ex);
        }
    }
    
}

4.設定前後台的Bean資訊與呼叫點資訊。

首先是前台,Token自動生成時帶入key與token當作後台secutrity的驗證,taskInterfaceBO則是使用TaskInterfaceBO呼叫時回傳使用的入口點。

@Bean(name="tokenBean")
TokenBean getHessianBean() {
    TokenBean tokenBean = new TokenBean();
    tokenBean.setKey("key");
    tokenBean.setToken("token");
    return tokenBean;
}
    
@Bean(name="taskInterfaceBO")
TkuHttpInvokerProxyFactoryBean tkuHttpInvokerProxyFactoryBean() {
    TkuHttpInvokerProxyFactoryBean tkuBean = new TkuHttpInvokerProxyFactoryBean();
    tkuBean.setServiceInterface(TaskInterfaceBO.class);
    tkuBean.setServiceUrl("http://localhost:8080/spring_backend"+"/TaskInterfaceBO");
    return tkuBean;
}

接著是後台,有用到一個自定義的ServiceExporterOverrider類別,裡面有檢查token的流程。

@Autowired TaskInterfaceBOImpl taskInterfaceBOImpl;

@Bean(name="taskInterfaceBOImpl")
TaskInterfaceBOImpl getTaskInterfaceBOImpl() {
	return new TaskInterfaceBOImpl();
}
	
@Bean(name="tokenBean")
TokenBean getHessianBean() {
    TokenBean tokenBean = new TokenBean();
    tokenBean.setKey("key");
    tokenBean.setToken("token");
    return tokenBean;
}
    
@Bean(name="/TaskInterfaceBO")
ServiceExporterOverrider getServiceExporterOverrider() {
    ServiceExporterOverrider SEOBean = new ServiceExporterOverrider();
    SEOBean.setService(taskInterfaceBOImpl);
    SEOBean.setServiceInterface(TaskInterfaceBO.class);
    return SEOBean;
}
public class ServiceExporterOverrider  extends HttpInvokerServiceExporter {
    Logger log = Logger.getLogger(this.getClass());
	
    @Autowired
    private TokenBean tokenBean;

    @Override
    public void handleRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        try {
            RemoteInvocation invocation = readRemoteInvocation(request);
            if(!isSecurityRequest(invocation)){
                String message = "Security Forbidden,this is not security request";                       
				throw new IOException(message);
            }

            RemoteInvocationResult result = invokeAndCreateResult(invocation, getProxy());
            writeRemoteInvocationResult(request, response, result);
        } catch (ClassNotFoundException e) {
            throw new NestedServletException("Class not found during deserialization", e);
        }
    }

    protected boolean isSecurityRequest(RemoteInvocation invocation){
    	boolean flag=true;
        try {
        	
        	Object keyObject = invocation.getAttribute("key");
        	Object tokenObject = invocation.getAttribute("token");
        	
        	//根本沒有帳密物件
        	if(keyObject == null 
        			|| tokenObject == null 
        			|| StringUtils.isBlank(keyObject.toString())
        			|| StringUtils.isBlank(tokenObject.toString())) {
        		
        		log.info("No hessian validation Object, access denied");
        		return flag=false;
        	}
        	
        	String key = keyObject.toString();
            String token = tokenObject.toString();
            
            if (!StringUtils.equals(key,tokenBean.getKey())
            		||!StringUtils.equals(token,tokenBean.getToken())) {
            	log.info("Account or Password not match");
            	return flag=false;
            }
            
        } catch (Exception e) {
            log.info("hessian validation failed");
        }
        return flag;
    }

	public TokenBean getHessianBean() {
		return tokenBean;
	}

	public void setHessianBean(TokenBean hessianBean) {
		this.tokenBean = hessianBean;
	}

}

5.Spring Controller的發動起始點,利用Autowired宣告一個TaskInterfaceBO介面呼叫getResultFromBackEnd()函式,發動後端服務呼叫。

@Autowired
TaskInterfaceBO taskInterfaceBO;

@RequestMapping(value = "/serviceInvokerMethod", method = RequestMethod.POST)
@ResponseBody
public Map<String,Object> serviceInvokerMethod() throws Exception {
		
	TransferBean tb = taskInterfaceBO.getResultFromBackEnd();
	//準備一個Map供回傳Ajax讀取結果使用
	Map<String,Object> result = new HashMap<String,Object>();
	result.put("result", tb.getResult());
	
	return result;
}

6.寫一個簡單呼叫前台Controller的頁面程式觸發他。

function onReady() {
	
	alert("準備導頁");
	
	$.ajax({
		url:'/spring_ryuichi/serviceInvokerMethod',
		type:"post",
		processData:false,
		contentType:false,
		success:function(data) {
			alert("success " + data.result);
		},
		error:function(data) {
			alert("error " + data.result);
		}
	});
}

7.結果。