純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.結果。