spring httpinvoker添加服務端安全認證策略

    1 背景 java

    正在經手的項目的web應用之間是經過spring的controller方式暴露接口,而後使用httpClient進行訪問。普普統統的增刪改查功能也得寫上七八個方法才能實現,實在是寫到心累。因而乎想要增長一種遠程調用方式,本着儘可能遵循原有安全驗證策略的原則,對httpinvoker作了些小的調整。不過方案最終被負責人否了,只能繼續寫可愛的httpClient方法。只好抹去業務邏輯,把代碼變成博客安安靜靜的躺在知識庫裏 web

   

    2 思路與實現 spring

    經過對源代碼的解讀,發現spring和httpinvoker在進行遠程調用時,主要是經過RemoteInvocation這個類來傳遞參數。因而調整的思路就是藉助這個類傳遞咱們的認證信息,在服務端讀取的同時進行安全認證。以達到最終的目的。 安全

    首先是自定義安全認證信息類,這個類在客戶端負責存放安全認證信息和生成安全密鑰,在服務端負責解密: ide

/**
 * HttpInvoker調用驗證信息類
 */
public class MyHttpInvokerAuthInfo {
    private static final Logger LOGGER = LoggerFactory.getLogger(MyHttpInvokerAuthInfo.class);
    //用戶名KEY
    public static final String USERNAME_KEY = "USERNAME_KEY";
    //密碼KEY
    public static final String PASSWORD_KEY = "PASSWORD_KEY";
    //隨機生成的KEY1
    public static final String FIRST_KEY = "FIRST_KEY";
    //隨機生成的KEY2
    public static final String SECOND_KEY = "SECOND_KEY";

    private String username;
    private String password;

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    /**
     * 獲取加密信息MAP
     */
    public Map<String, Serializable> getSecurityMap(){
        if(StringUtils.isBlank(password)){
            return null;
        }
        Map<String, Serializable> securityMap = new HashMap<String, Serializable>();
        //TODO 添加本身的安全加密邏輯,並把須要認證的數據放入securityMap中
        return securityMap;
    }

    /**
     * 生成密鑰
     */
    public static String getSecurityKey(String firstKey, String secondKey, String thirdKey) {
        String security = null;
        //TODO 生成本身的密鑰
        return security;
    }

    /**
     * 對認證信息進行校驗
     */
    public static boolean validatePassword(String key, Map<String, Serializable> keyMap) {
        boolean result = false;
        try {
            //TODO 校驗邏輯
        } catch (Exception e) {
            LOGGER.error("密鑰校驗失敗", e);
        }
        return result;
    }
}

 

    而後是客戶端,客戶端須要重寫spring的HttpInvokerProxyFactoryBean類和HttpInvokerClientInterceptor類以便添加咱們的驗證的信息。 ui

    HttpInvokerProxyFactoryBean類的重寫: this

/**
 * 詳情參見Spring的HttpInvokerProxyFactoryBean類,本類與其徹底一致
 */
public class MHttpInvokerProxyFactoryBean extends MyHttpInvokerClientInterceptor 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());
    }


    public Object getObject() {
        return this.serviceProxy;
    }

    public Class<?> getObjectType() {
        return getServiceInterface();
    }

    public boolean isSingleton() {
        return true;
    }
}

    HttpInvokerClientInterceptor類的重寫: 加密

/**
 * 對Spring的HttpInvokerClientInterceptor類的重寫
 * 添加 MyHttpInvokerAuthInfo 這一驗證信息參數
 * 重寫invoke()方法....
 * 重寫getHttpInvokerRequestExecutor()方法,修改默認HttpInvokerRequestExecutor爲CommonHttpInvokerRequestExecutor
 * 其他保持一致
 */
public class MyHttpInvokerClientInterceptor extends RemoteInvocationBasedAccessor
        implements MethodInterceptor, HttpInvokerClientConfiguration {

    private String codebaseUrl;

    private HttpInvokerRequestExecutor httpInvokerRequestExecutor;

    /**
     * 驗證參數,存放服務端須要的認證信息,並用認證信息生成密鑰
     */
    private MyHttpInvokerAuthInfo myHttpInvokerAuthInfo;

    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) {
            CommonsHttpInvokerRequestExecutor executor = new CommonsHttpInvokerRequestExecutor();
            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(myHttpInvokerAuthInfo != null){
                if(invocation.getAttributes() == null){
                    invocation.setAttributes(myHttpInvokerAuthInfo.getSecurityMap());
                }else{
                    invocation.getAttributes().putAll(myHttpInvokerAuthInfo.getSecurityMap());
                }
            }
        }catch (Exception e){
            logger.error("設置驗證參數發生異常,請求將可能被服務端攔截...", e);
        }

        RemoteInvocationResult result = null;
        try {
            result = executeRequest(invocation, methodInvocation);
        }
        catch (Throwable ex) {
            throw convertHttpInvokerAccessException(ex);
        }
        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);
        }
    }

    public MyHttpInvokerAuthInfo getMyHttpInvokerAuthInfo() {
        return myHttpInvokerAuthInfo;
    }

    public void setMyHttpInvokerAuthInfo(MyHttpInvokerAuthInfo myHttpInvokerAuthInfo) {
        this.myHttpInvokerAuthInfo = myHttpInvokerAuthInfo;
    }
}    

    

    而後須要配置xml文件,設置遠程代理類: url

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!-- 安全認證類 -->
    <bean id="myAuthInfo" class="cn.com.test.httpinvoker.MyHttpInvokerAuthInfo">
        <property name="username" value="${username}"/>
        <property name="password" value="${password}"/>
    </bean>

    <!-- 接口申明開始 -->
    <bean id="myTestService" class="cn.com.test.httpinvoker.MyHttpInvokerProxyFactoryBean">
        <property name="myHttpInvokerAuthInfo" ref="myAuthInfo"/>
        <property name="serviceUrl" value="${url}/inner/myTest.service"/>
        <property name="serviceInterface" value="cn.com.test.service.MyTestService"/>
    </bean>

</beans>

    

    最後是服務端,服務端須要經過httpinvoker來暴露本身的接口,因此須要重寫接口暴露類HttpInvokerServiceExporter: spa

/**
 * HttpInvoker接口暴露類,添加驗證支持
 * 繼承自spring的HttpInvokerServiceExporter類,重寫其handleRequest()方法
 */
public class MyHttpInvokerServiceExporter extends HttpInvokerServiceExporter {
    private static final Logger LOGGER = LoggerFactory.getLogger(MyHttpInvokerServiceExporter.class);

    /**
     * 重寫處理方法,添加安全認證
     */
    @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";
                try {
                    response.getWriter().println(message);
                } catch (IOException ex) {
                    LOGGER.error(ex.getMessage(),ex);
                }
                return;
            }

            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){
        try {
            String username = invocation.getAttribute(MyHttpInvokerAuthInfo.USERNAME_KEY).toString();
            return MyHttpInvokerAuthInfo.validatePassword(username, invocation.getAttributes());
        } catch (Exception e) {
            LOGGER.error("讀取驗證信息失敗...", e.getMessage());
        }
        return false;
    }
}        

     

    最後在發佈接口時,把接口暴露類指向重寫後的MyHttpInvokerServiceExporter:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean name="/inner/myTest.service" class="cn.com.test.httpinvoker.MyHttpInvokerServiceExporter">
        <!-- myTestService 經過註釋來申明 -->
        <property name="service" ref="myTestService"/>
        <property name="serviceInterface" value="cn.com.test.service.MyTestService"/>
    </bean>
</beans>

    

    3 小結

    由於改動並不複雜,在代碼裏也寫了些簡單的註釋,就不對源碼作多餘的分析了,有缺陷或者值得改進的地方歡迎指出...

相關文章
相關標籤/搜索