阿里云-云小站(无限量代金券发放中)
【腾讯云】云服务器、云数据库、COS、CDN、短信等热卖云产品特惠抢购

Apache CXF拦截器Interceptor实现WebServices用户验证

103次阅读
没有评论

共计 5797 个字符,预计需要花费 15 分钟才能阅读完成。

Apache CXF 应该都知道,是一个开源的 Services 框架,可以构建基于 SOAP 或 RESTful 的 WebServices,并且可以和 spring 天然地进行无缝集成。
不过这里不是关于 CXF 的系统介绍,而是在开发 WebServices 时遇到一些问题,最终使用 CXF 拦截器完成需求。所以侧重记录使用 CXF 的拦截器。

问题

先说问题,其实也很简单,简言之就是在请求时需要对请求做一些预先处理。
项目框架做成了所谓的前后端分离,即前台 app 使用第三方工具生成,调用后台通过 WebServices。可以理解为前台就是静态的 Html 与 js, 后台就是 WebServices。现在问题是要求是每次请求时需要做下验证,保证当前登录设备的用户没有在其他设备登录,如果有就到跳到登录界面。
具体做法就是,在每次登录时会在系统记录一个唯一编码,然后在每次请求时先比较登录的账号和这个唯一编码和系统中已存的是否一致,如果不一致就到登录也,否则就可以继续请求。当然拦截会排除登录请求。因为前台 app 所有请求都是通过 WebServices,这样可以保证如果同一账号同时只能在一台设备上操作。

分析

CXF 的拦截器是 CXF 功能最主要的扩展点。通过自定义的 Interceptor,可以改变请求和响应的一些消息处理,其中最基本的原理还是一个动态代理。所以对于上面的需求应该可以是满足的.

拦截器接口

查看 cxf 的 API 了解到,cxf 是有拦截器接口定义的。

package org.apache.cxf.interceptor;

import org.apache.cxf.interceptor.Fault;
import org.apache.cxf.message.Message;

public interface Interceptor<T extends Message> {
    void handleMessage(T var1) throws Fault;

    void handleFault(T var1);
}

所以对于上面的问题就想到可以使用拦截器实现了。现在知道要做什么,也知道实现方向,就看下怎么做了。

拦截器实现方式

通过上面的借口定义可知,Interceptor 只有两个方法,一个处理消息 handleMessage,一个是处理错误 handleFault。但这里需要提醒注意的是,在实行具体的 Interceptor 的这两个方法中,千万别调用 Interceptor 内部的成员变量。这是由于 Interceptor 是面向消息来进行处理的,每个 Interceptor 都有可能运行在不同的线程中,如果调用了 Interceptor 中的内部成员变量,就有在 Interceptor 中造成临界资源的访问的情况,而这时的 Interceptor 也就不是线程安全的 Interceptor 了。我在实现过程中,由于没有注意这个问题,花了不少时间处理异常。
通常 cxf 不允许我们直接实现 Interceptor 接口,Interceptor 有一个子接口 PhaseInterceptor,顾名思义就是分阶段的拦截器。cxf 将消息的发送、接收的过程分为很多个阶段,如 send、receive、read、write 等,具体可以参考类 org.apache.cxf.phase.Phase。cxf 要求我们在定义一个拦截器的时候必须指定当前拦截器需要作用的阶段。
cxf 已经为我们提供了一个实现了 PhaseInterceptor 接口的抽象实现类 org.apache.cxf.phase.AbstractPhaseInterceptor,而且建议我们通过继承 AbstractPhaseInterceptor 来实现自定义拦截器。AbstractPhaseInterceptor 中没有定义默认的空构造方法,所以子类在实现的时候必须在构造方法中调用父类的某一个构造方法。以下是一个非常简单的继承自 AbstractPhaseInterceptor 的自定义拦截器的实现。

参数处理

到这里,技术基本上确定了,剩下的就是实现的一些细节问题。
首先,因为我需要比较请求参数和目前的后台保存的数据,所以首要解决怎么取得请求穿过来的参数。
通过上面的 Interceptor 接口定义可知,拦截器在执行时会将请求的参数放在 Message 中。所以只要知道如何解析 Message 应该就可解决问题了。

配置拦截器

因 cxf 和 spring 天然的关系,自然会联想到通过 spring 的方式配置定义拦截器。

解决

通过上面的分析思路基本清楚了, 下面就开始具体编码实现.

拦截器代码

先定义一个拦截器. 完成后代码如下:

package com.mungo.aop;

import com.alibaba.fastjson.JSONObject;
import org.apache.commons.lang.StringUtils;
import org.apache.cxf.interceptor.Fault;
import org.apache.cxf.message.Message;
import org.apache.cxf.message.XMLMessage;
import org.apache.cxf.phase.AbstractPhaseInterceptor;
import org.apache.cxf.phase.Phase;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;

import java.io.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 系统全局拦截器(排除登录服务调用)* 用于校验登录的账号是否已在别处登录
 * 已在别处登录这抛出异常
 * 
 */
public class AuthInterceptor extends AbstractPhaseInterceptor<Message> {
    private Logger logger = Logger.getLogger(this.getClass());

    public AuthInterceptor() {// 定义拦截器阶段 
        super(Phase.RECEIVE);
    }

    /**
     * @Description: 拦截器操作
     * @param message 被拦截到的消息
     * @throws Fault
     */
    @Override
    public void handleMessage(Message message)  {logger.info("===== 自定义拦截器 =======");

        String methodName= getMethod(message);
        // 排除拦截的方法 
        List<String> list = new ArrayList<String>();
        list.add("login");
        list.add("forgotPassword");
        if(!list.contains(methodName)) {Map<String, String> reqParamsMap = new HashMap<String, String>();
            // 根据请求方式区分取得 GET 和 POST 的参数 
            if (message.get(message.HTTP_REQUEST_METHOD).equals("GET")) {//get 参数,=& 格式 
                String reqParams = (String) message.get(message.QUERY_STRING);
                logger.info("请求的参数:" + reqParams);
                // 注:toMap 为自定义方法, 实现将 String 转成 map
                reqParamsMap = StringUtils.toMap(reqParams, "&");
            } else if (message.get(message.HTTP_REQUEST_METHOD).equals("POST")) {InputStream is = message.getContent(InputStream.class);
                BufferedReader in = new BufferedReader(new InputStreamReader(is));
                StringBuffer buffer = new StringBuffer();
                String line = "";
                try {while ((line = in.readLine()) != null) {//post 参数,json 格式 
                        buffer.append(line);
                    }
                    logger.info("请求的参数:" + buffer);
                    JSONObject jasonObject = JSONObject.parseObject(buffer.toString());
                    reqParamsMap = (Map) jasonObject;
                } catch (IOException e) {e.printStackTrace();
                }
                if (is != null) {// 这里一定要加,post 参数流读取结束如果不加这个操作, 会报 io 异常 
                    message.setContent(InputStream.class, new ByteArrayInputStream(buffer.toString().getBytes()));
                }
            }
            if (reqParamsMap != null) {String agentCode = String.valueOf(reqParamsMap.get("agentCode"));
                // 注: 这里是系统用户登陆唯一标志, 使用时自己定义 
                String cid = getCid();
                if (StringUtils.isNotEmpty(cid)&&
                      !StringUtils.equals(cid, String.valueOf(reqParamsMap.get("cid")))) {throw new Fault(new Exception("已在别处已登录"));
                 }
            }
        }
    }

    /**
     * @Description:handleMessage 异常后执行
     * @param message
     */
    @Override
    public void handleFault(Message message) {super.handleFault(message);

        logger.info("=================================:"+"handleFault");
    }

    /**
     * @Description: 取得请求服务的具体方法
     * @param message
     * @return
     */
    private String getMethod(Message message) {// 通过分析 webservice 的 uri 取得实际执行的方法, 该 webservice 使用 cxf 的 RESTFul 形式发布 
        String requestUri = (String) message.get(XMLMessage.REQUEST_URI);
        String[] methods = StringUtils.split(requestUri,"/");
        logger.debug("********method name:" + requestUri);
        return methods!=null && methods.length>0?methods[methods.length-1]:"";
    }
}

拦截器配置

配置拦截器很简单, 尤其是结合 spring.
这里只有简单的说明, 如果要配置成全局的拦截器, 只需要在 spring 的配置文件中增加节点.
需要注意的是, 在使用时候,一定要引入 命名空间 xmlns:cxf=http://cxf.apache.org/core,及其对应的模式 http://cxf.apache.org/schemas/core.xsd.

<!-- 全局 Bus(输入拦截器) -->
    <cxf:bus>
        <cxf:inInterceptors>
            <bean class="com.meyacom.crm.aop.AuthInterceptor"></bean>
        </cxf:inInterceptors>
    </cxf:bus>

如果拦截器只对某个 webservice 进行拦截, 可以使用 或节点配置.

<jaxrs:server id="fileServiceContainer" address="/fileService">
        <jaxrs:serviceBeans>
            <ref bean="fileService" />
        </jaxrs:serviceBeans>
        <jaxrs:extensionMappings>
            <entry key="octet-stream" value="application/json" />
        </jaxrs:extensionMappings>
        <jaxrs:providers>
            <ref bean="jsonProvider" />
        </jaxrs:providers>
        <jaxrs:inInterceptors>
            <bean class="com.meyacom.crm.aop.AuthInterceptor"></bean>
        </jaxrs:inInterceptors>
        <jaxws:outInterceptors>
              <bean class="org.apache.cxf.interceptor.LoggingOutInterceptor"/>
          </jaxws:outInterceptors>
    </jaxrs:server>

如上所示,可以同时为 in 和 out 两个拦截器链 配置 拦截器,其中 in 为 cxf 本身自带的日志拦截器;这里额外为 out 拦截器链配置打印日志显示。
当然配置除了使用配置文件, 也可是使用代码实现, 但这里只是为了解决实际问题不是为了系统介绍拦截器, 所以不在赘述.

后记

至此, 遇到的问题基本解决了. 总共花了大约一天半的时间,cxf 之前没有系统的了解过, 这次也第一次使用. 也是靠着经验一边摸索一边实现, 中间的确是很花时间. 以后得抽个时间把 cxf 好好学习一次.

本文永久更新链接地址 :http://www.linuxidc.com/Linux/2016-12/138291.htm

正文完
星哥说事-微信公众号
post-qrcode
 
星锅
版权声明:本站原创文章,由 星锅 2022-01-21发表,共计5797字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。
【腾讯云】推广者专属福利,新客户无门槛领取总价值高达2860元代金券,每种代金券限量500张,先到先得。
阿里云-最新活动爆款每日限量供应
评论(没有评论)
验证码
【腾讯云】云服务器、云数据库、COS、CDN、短信等云产品特惠热卖中