springmvc 自定義訊息轉換器完整例子

weixin_34148340發表於2018-12-10

問題描述:
最近在專案中對接第三方介面,採用http協議,post方法,協議型別:Content-Type: application/json;charset=utf-8,將使用者名稱和密碼等資訊放在header中,用於驗證請求。將業務資料放到body體中,並使用3DES加密。

  • 請求報文樣例如下:

POST /api/GetParkingPaymentInfo HTTP/1.1
Content-Type: application/json;charset=utf-8
user: 123453
pwd: qwerew
Host: 220.160.112.124:9096
Content-Length: 43
Expect: 100-continue
{"data":"DkTwRsUUza33A8/TvrocXI3r+Az1T7bt"}

每個介面都是此加密驗證方式,但是我不想再每個controller方法中都校驗解密一次,故而想到使用springmvc 的自定義訊息轉換器,在訊息轉換器中先解密,然後將報文轉換為對應的java物件,controller入參直接是java物件,這樣校驗使用者名稱密碼和解密就可以單獨處理了。

驗證使用者名稱和密碼,使用攔截器實現

因為使用者名稱和密碼放到了header中,可以在攔截器中獲取請求頭,判斷使用者名稱和密碼是否正確。

  • 建立攔截器
@Component
public class KeyTopInterceptor extends HandlerInterceptorAdapter {

    private static final Logger log = LoggerFactory.getLogger(AuthInterceptor.class);

    private static final String MIME_JSON = "application/json;charset=UTF-8";
    @Value("${keytop.user}")
    private String ktuser;
    @Value("${keytop.pwd}")
    private String ktpwd;
    //在請求進入controller前進行攔截
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {
        String user = request.getHeader("user");
        String pwd = request.getHeader("pwd");
        String host = request.getHeader("Host");
        log.info("===校驗科託請求頭中的使用者名稱和密碼,url={},user={},pwd={},host={}",request.getRequestURI(),user,pwd,host);
        if(ktuser.equals(user) && ktpwd.equals(pwd)){
            return true;
        }else{
            log.info("===校驗科託失敗,配置的使用者名稱和密碼與傳遞的不一致,配置的ktuser={},ktpwd={}",ktuser,ktpwd);
            //根據介面要求返回錯誤資訊
            PrintWriter writer = response.getWriter();
            response.setCharacterEncoding("UTF-8");
            response.setHeader("Content-type", MIME_JSON);
            response.setContentType(MIME_JSON);
            BaseKeyTopRes<?> baseKeyTopRes = new BaseKeyTopRes<>();
            baseKeyTopRes.setFaileInfo("user or pwd incorrectness");
            response.setStatus(HttpStatus.OK.value());
            writer.write(JSONObject.toJSON(baseKeyTopRes).toString());
            writer.close();
            return false;
        }
    }
}
  • 配置攔截器
@Configuration
@EnableWebMvc
public class WebMvcConfigurer extends WebMvcConfigurerAdapter {

    @Autowired
    private KeyTopInterceptor keyTopInterceptor;

    /**
     * 新增攔截器
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //新增科託攔截器
        registry.addInterceptor(keyTopInterceptor)
                .addPathPatterns("/keytop/**");
    }

}

body體解密,轉換為java物件

例如有個介面的data欄位為:{“data”:{"platno":"A1234"}},獲取到的引數為加密之後的:{"data":"DkTwRsUUza33A8/TvrocXI3r+Az1T7bt"}。

  • 建立訊息轉換器
    為了使建立的訊息轉換器只轉換本次業務新增的介面,建立一個請求基類bean物件,沒有任何欄位,只是實現Serializable介面,作為其他業務的父類,例如:BaseKeyTopReq
public class BaseKeyTopReq implements Serializable{

}

建立訊息轉換器如下:

public class KeyTopMsgConverter extends AbstractHttpMessageConverter<BaseKeyTopReq> {
    private static final Logger logger = LoggerFactory.getLogger(KeyTopMsgConverter.class);
    //科託3DES加解密需要的key
    private String ktkey;
    //科託3DES加解密需要的偏移量
    private String ktiv;

    public KeyTopMsgConverter(MediaType supportedMediaType,String ktkey,String ktiv) {
        super(supportedMediaType);
        this.ktiv=ktiv;
        this.ktkey=ktkey;
    }

    /**
     * 如果支援 true支援
     *  會呼叫 readInternal 將http訊息 轉換成方法中被@RequestBody註解的引數
     *  會呼叫writeInternal 將被@ResponseBody註解的返回物件轉換成資料位元組響應給瀏覽器
     */
    @Override
    protected boolean supports(Class<?> clazz) {
        //判斷父類是否為BaseKeyTopReq,如果是則使用該轉換器
        if(clazz.getSuperclass() == BaseKeyTopReq.class){
            return true;
        }
        return false;
    }
    /**
     *解析請求的引數
     */
    @Override
    protected BaseKeyTopReq readInternal(Class<? extends BaseKeyTopReq> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
       //獲取body資訊
        InputStream is=inputMessage.getBody();
        BufferedReader br=new BufferedReader(new InputStreamReader(is));
        StringBuilder stringBuilder = new StringBuilder();
        br.lines().forEach(item->stringBuilder.append(item));
        logger.info("科託解密之前資料:"+stringBuilder.toString());
        JSONObject jsonObject = JSON.parseObject(stringBuilder.toString());
        String data = jsonObject.getString("data");
        //解密
        try {
            String desString = ThreeDESUtil.getDesString(data,ktkey,ktiv);
            logger.info("科託解密之後資料:"+desString);
            //將解密出來的資訊轉換為java物件,注意該物件必須繼承BaseKeyTopReq
            return JSONObject.parseObject(desString,clazz);
        } catch (Exception e) {
            logger.error("科託解密失敗",e);
            throw new BizException(ErrorType.DECODE_ERROR);
        }
    }
    /**
     * 響應給物件的引數
     * 將方法被@ResponseBody註解的返回物件轉換成資料位元組響應給瀏覽器
     */
    @Override
    protected void writeInternal(BaseKeyTopReq baseKeyTopReq, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {

    }
}
  • 配置轉換器
@Configuration
@EnableWebMvc
public class WebMvcConfigurer extends WebMvcConfigurerAdapter {
    @Value("${keytop.key}")
    private String ktkey;
    @Value("${keytop.iv}")
    private String ktiv;
    
    /**
     * 擴充套件訊息轉換器
     * 注意不能使用configureMessageConverters方法,使用configureMessageConverters方法,則只包含你新增的,springmvc預設的訊息轉換器沒有了。
     * @param converters
     */
    @Override
    public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        //增加科託訊息轉換器
        KeyTopMsgConverter converter  = new KeyTopMsgConverter(MediaType.APPLICATION_JSON,ktkey,ktiv);
        converters.add(0,converter);//將自定義的設定為優先順序最高
    }
}

使用測試

例如有個介面PostFreeParkingSpace,data欄位資訊為:plateNo,json格式。
則可以建立一個PostFreeParkingSpaceReq物件,繼承BaseKeyTopReq。

//響應介面對應的物件
public class PostFreeParkingSpaceReq extends BaseKeyTopReq {
    private String plateNo;

    public String getPlateNo() {
        return plateNo;
    }

    public void setPlateNo(String plateNo) {
        this.plateNo = plateNo;
    }
}

則介面呼叫方,傳送的body體資料為:{"data":"DkTwRsUUza33A8/TvrocXI3r+Az1T7bt"},經過訊息轉換器解密(只解密data內容)之後為:{"plateNo":"A12345"},然後將該json字串轉換為java物件。
則在controller中入參物件裡面就有值了。

  • controller
@RestController
@RequestMapping("/keytop")
public class KeyTopController {

    private static final Logger logger = LoggerFactory.getLogger(PubParkingController.class);

    @RequestMapping(value = "/PostFreeParkingSpace", method = RequestMethod.POST)
    public String PostFreeParkingSpace(@RequestBody PostFreeParkingSpaceReq spaceReq) {
        logger.info("科託空閒車位上報:" + JSON.toJSONString(spaceReq));
        /**此時從入參物件中獲取的plateNo值則為A12345,已經解密完且轉換成了對應的實體物件*/
        return spaceReq.getPlateNo();
    }

}

相關文章