spring httpinvoker添加服务端安全认证策略

泪湿孤枕 提交于 2019-11-30 18:05:40

    1 背景

    正在经手的项目的web应用之间是通过spring的controller方式暴露接口,然后使用httpClient进行访问。普普通通的增删改查功能也得写上七八个方法才能实现,实在是写到心累。于是乎想要增加一种远程调用方式,本着尽量遵循原有安全验证策略的原则,对httpinvoker做了些小的调整。不过方案最终被负责人否了,只能继续写可爱的httpClient方法。只好抹去业务逻辑,把代码变成博客安安静静的躺在知识库里

   

    2 思路与实现

    通过对源代码的解读,发现spring和httpinvoker在进行远程调用时,主要是通过RemoteInvocation这个类来传递参数。于是调整的思路就是借助这个类传递我们的认证信息,在服务端读取的同时进行安全认证。以达到最终的目的。

    首先是自定义安全认证信息类,这个类在客户端负责存放安全认证信息和生成安全密钥,在服务端负责解密:

/**
 * 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类以便添加我们的验证的信息。

    HttpInvokerProxyFactoryBean类的重写:

/**
 * 详情参见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文件,设置远程代理类:

<?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:

/**
 * 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 小结

    因为改动并不复杂,在代码里也写了些简单的注释,就不对源码做多余的分析了,有缺陷或者值得改进的地方欢迎指出...

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!