需求概述
我們的需求是這樣的:
后臺的rest接口對接 安卓、IOS客戶端、小程序
考慮到安全,我們對請求參數(shù)做了簽名校驗,對部分接口做了登錄態(tài)校驗
安全是安全了,但是 對于我們后臺開發(fā)人員來說,進行接口測試就變得困難起來
接口測試,我們前面是這么做的
階段一
在本地進行接口測試,把關(guān)鍵的Filter 注釋掉
感受:太麻煩了,不小心就會把注釋的代碼提交了(我用的是烏龜git)
階段二
我用Python寫了一個本地代理,postman 配置好代理,讓請求走我的代理
這個代理會模擬客戶端,對參數(shù)進行簽名
工程在這里:proxy
感受:我們有些接口使用到了Protobuf,因為傳輸?shù)氖嵌M制數(shù)據(jù)流,接口返回的數(shù)據(jù),代理想轉(zhuǎn)為可視化的json數(shù)據(jù)很費勁
(目前是先用protobuf生成Python文件,再反序列化,再顯示,生成文件這一步需要經(jīng)常維護)
而且我們的登錄態(tài)用到了jwt,token動不動就過期了,需要更換;雖然使用postman的全局變量可以減少更換次數(shù),但是如果想切換用戶查看接口返回結(jié)果,又需要去數(shù)據(jù)庫找到對應(yīng)的token,再替換,有點費勁
經(jīng)歷過這兩個階段,后面就想著,能不能開發(fā)出這樣一個接口呢:
參數(shù)里寫上url method userId
然后這個接口根據(jù)這些參數(shù),進行分發(fā)(轉(zhuǎn)發(fā)),然后在Filter里面排除這個接口,不就避免了簽名校驗 登陸校驗嗎?
如何實現(xiàn)
剛開始實現(xiàn)的時候,也是一臉懵逼,用“轉(zhuǎn)發(fā)” 關(guān)鍵字 搜了一番,發(fā)現(xiàn)都不能用在Rest 接口
最后沒辦法,想著去了解一下spring是怎么實現(xiàn)的,我再重復(fù)實現(xiàn)一遍不就OK了嗎
于是去看了看 DispatcherServlet 源碼解析文章,找到了思路
先看看 DispatcherServlet 的 核心方法 doDispatch
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
ModelAndView mv = null;
Exception dispatchException = null;
try {
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);
// Determine handler for the current request.
// 找到當(dāng)前request對應(yīng)的 handler
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null || mappedHandler.getHandler() == null) {
noHandlerFound(processedRequest, response);
return;
}
// Determine handler adapter for the current request.
// 找到當(dāng)前 handler 對應(yīng)的 適配器
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// Process last-modified header, if supported by the handler.
String method = request.getMethod();
boolean isGet = "GET".equals(method);
if (isGet || "HEAD".equals(method)) {
long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
if (logger.isDebugEnabled()) {
logger.debug("Last-Modified value for [" + getRequestUri(request) + "] is: " + lastModified);
}
if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
return;
}
}
//執(zhí)行 HandlerExecutionChain 里面 攔截器的 preHandle
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
// Actually invoke the handler.
// 讓適配器運行handler,也就是執(zhí)行 Controller里的某個具體的方法
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
applyDefaultViewName(processedRequest, mv);
//執(zhí)行 HandlerExecutionChain 里面 攔截器的 postHandle
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch (Exception ex) {
dispatchException = ex;
}
catch (Throwable err) {
// As of 4.3, we're processing Errors thrown from handler methods as well,
// making them available for @ExceptionHandler methods and other scenarios.
dispatchException = new NestedServletException("Handler dispatch failed", err);
}
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
catch (Exception ex) {
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
catch (Throwable err) {
triggerAfterCompletion(processedRequest, response, mappedHandler,
new NestedServletException("Handler processing failed", err));
}
finally {
if (asyncManager.isConcurrentHandlingStarted()) {
// Instead of postHandle and afterCompletion
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
}
else {
// Clean up any resources used by a multipart request.
if (multipartRequestParsed) {
cleanupMultipart(processedRequest);
}
}
}
}
關(guān)鍵的地方我都寫了中文注釋
所以 我們自己實現(xiàn) 分發(fā) 效果的時候,參考這個邏輯即可
找到handler --> 找到適配器 --> 執(zhí)行攔截器 preHandle(如果需要) --> 執(zhí)行 handler --> 執(zhí)行攔截器 postHandle(如果需要)
實現(xiàn)代碼
下面是 Controller
package com.xxx.app.skmr.controller;
import java.util.HashMap;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils;
import org.powermock.reflect.Whitebox;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.HandlerAdapter;
import org.springframework.web.servlet.HandlerExecutionChain;
import org.springframework.web.servlet.ModelAndView;
import com.xxx.app.skmr.constant.Constants;
import com.xxx.app.skmr.filter.EncryptRequest;
import com.xxx.app.skmr.properties.CommonConfigProperties;
import com.xxx.app.skmr.service.ReqCacheService;
import com.xxx.app.skmr.util.AssertHelper;
@RequestMapping("/v1")
@RestController
public class InterfaceTestController {
private static final Logger LOGGER = LoggerFactory.getLogger(InterfaceTestController.class);
@Autowired
private DispatcherServlet dispatcherServlet;
@Autowired
private CommonConfigProperties commonConfigProperties;
@RequestMapping(value = "/iTest", method = RequestMethod.POST)
public void receiveRequest(
@RequestParam(value="url", required=true) String url,
@RequestParam(value="method", required=true) String method,
@RequestParam(value="param", required=false) String param,
@RequestParam(value="userId", required=false) String userId,
HttpServletRequest request,
HttpServletResponse response) {
LOGGER.info("InterfaceTestController receiveRequest, url={}", url);
if (! Constants.SKMR_APP_ENV_NAME_DEV.equals(commonConfigProperties.getEnvName())) {
//不是 dev 環(huán)境,直接退出
return;
}
//設(shè)置登錄態(tài)
if (StringUtils.isNotBlank(userId)) {
ReqCacheService.setReqUserId(request, userId);
}
//改變request里的值
EncryptRequest myRequest = (EncryptRequest) request;
//強行設(shè)置 header 里的 accept 為 application/json,為客戶端省點事
myRequest.setHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE);
myRequest.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
//修改方法,方法要大寫
myRequest.setMethod(method.toUpperCase());
//修改 請求path
myRequest.setRequestURI(url);
//這個必須要設(shè)置,不然在 UrlPathHelper.getLookupPathForRequest() 方法里面 有問題,需要讓rest 為空
myRequest.setServletPath(url);
//設(shè)置url 查詢參數(shù),
if (StringUtils.isNotBlank(param)) {
HashMap<String, String> convertParam = convertParam(param);
myRequest.setUseExternalParam(true);
myRequest.setParamMap(convertParam);
}
//轉(zhuǎn)發(fā)
redirect(myRequest, response);
return ;
}
@RequestMapping(value = "/iTest", method = RequestMethod.GET)
public void receiveRequestV2(
@RequestParam(value="url", required=true) String url,
@RequestParam(value="method", required=true) String method,
@RequestParam(value="param", required=false) String param,
@RequestParam(value="userId", required=false) String userId,
HttpServletRequest request,
HttpServletResponse response) {
//這個接口因為是 get 的,所以可以通過瀏覽器直接調(diào)用,不需要postman
//當(dāng)然了,對于有body的,瀏覽器就不行了,還是需要postman
LOGGER.info("InterfaceTestController receiveRequestV2, url={}", url);
receiveRequest(url, method, param, userId, request, response);
return ;
}
/**
* <br>轉(zhuǎn)發(fā) 分發(fā)
* <br>只有自身的url 會走 過濾器,轉(zhuǎn)發(fā)后的 沒有走過濾器
*
* @param request
* @param response
* @author YellowTail
* @since 2019-02-15
*/
private void redirect(HttpServletRequest request, HttpServletResponse response) {
try {
// 1. 得到 HandlerExecutionChain, 調(diào)用方法 getHandler 即可得到
HandlerExecutionChain handlerExecutionChain = Whitebox.invokeMethod(dispatcherServlet, "getHandler", request);
// 2. 取出 HandlerMethod,適配器要用
HandlerMethod handlerMethod = (HandlerMethod) handlerExecutionChain.getHandler();
// 3. 得到 適配器 HandlerAdapter,調(diào)用方法 getHandlerAdapter 得到
HandlerAdapter ha = Whitebox.invokeMethod(dispatcherServlet, "getHandlerAdapter", handlerMethod);
// 4. 執(zhí)行 HandlerExecutionChain 攔截器的 preHandler() 前置方法, CmdHandlerInterceptor 會去設(shè)置 cmd
Whitebox.invokeMethod(handlerExecutionChain, "applyPreHandle", request, response);
// 5. 執(zhí)行 handler
ModelAndView mv = ha.handle(request, response, handlerMethod);
// 6. 執(zhí)行 攔截器的 postHandler() 方法, LogHandlerInterceptor 會去記錄日志
Whitebox.invokeMethod(handlerExecutionChain, "applyPostHandle", request, response, mv);
} catch (Exception e) {
LOGGER.error("error, ", e);
}
}
/**
* <br>將字符串形式的 參數(shù) id=test&type=2 轉(zhuǎn)換為 map,方便使用
*
* @param param
* @return
* @author YellowTail
* @since 2019-02-15
*/
private HashMap<String, String> convertParam(String param) {
HashMap<String, String> map = new HashMap<>();
if (StringUtils.isBlank(param)) {
return map;
}
String[] split = param.split("&");
for(String eachParam : split) {
String[] split2 = eachParam.split("=");
AssertHelper.assertTrue(2 >= split2.length , eachParam + " eachParam should contains one =");
if (2 == split2.length) {
map.put(split2[0], split2[1]);
}
}
return map;
}
}
因為我們需要對 request 做很多操作,所以必須自己實現(xiàn)一個request ,且繼承 HttpServletRequestWrapper
package com.xxx.app.skmr.filter;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.StringReader;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Vector;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.xxx.app.skmr.util.IOSystem;
import com.xxx.app.skmr.util.StringUtils;
public class EncryptRequest extends HttpServletRequestWrapper {
private static final Logger LOGGER = LoggerFactory.getLogger(EncryptRequest.class);
/**
* URL 的方法
*/
private String _method;
/**
* URI
*/
private String _requestURI;
/**
* ServletPath
*/
private String _servletPath;
/**
* 是否使用 擴展的 參數(shù)
*/
private boolean useExternalParam = false;
/**
* 參數(shù) map
*/
private HashMap<String, String> paramMap;
/**
* header
*/
private HashMap<String, String> _headers;
/**
* 存儲requestBody的內(nèi)容
*/
private byte[] requestBody = null;
public EncryptRequest(HttpServletRequest request) {
super(request);
try {
this.requestBody = IOSystem.readToBytes(request.getInputStream());
} catch (IOException e) {
e.printStackTrace();
LOGGER.error("EncryptRequest init getInputStream error", e);
throw new RuntimeException(e);
}
}
/**
* 獲取requestbody
*/
public byte[] getRequestBody() {
return this.requestBody;
}
public String getRequestBodyString() {
return StringUtils.toString(requestBody, this.getRequest().getCharacterEncoding());
}
public void setRequestBody(byte[] requestBody ) {
this.requestBody = requestBody;
}
@Override
public ServletInputStream getInputStream() throws IOException {
// 如果是null 證明首次數(shù)據(jù)獲取失敗
if (requestBody == null) {
requestBody = new byte[0];
}
final ByteArrayInputStream bais = new ByteArrayInputStream(requestBody);
return new ServletInputStream() {
@Override
public int read() throws IOException {
return bais.read();
}
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setReadListener(ReadListener listener) {
}
};
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new StringReader(this.getRequestBodyString()));
}
/**
* 重寫 getMethod,方便 變更 method
*/
@Override
public String getMethod() {
if (null == _method) {
_method = super.getMethod();
}
return _method;
}
/**
* <br>擴展方法:支持修改 method
* <br>在 接口測試的接口里使用到了
*
* @param newMethod
* @author YellowTail
* @since 2019-02-14
*/
public void setMethod(String newMethod) {
_method = newMethod;
}
/**
* 覆蓋,方便變更
*/
@Override
public String getRequestURI() {
if (null == _requestURI) {
_requestURI = super.getRequestURI();
}
return _requestURI;
}
/**
* <br>擴展方法,支持變更 path
* <br>在 接口測試的接口里使用到了
*
* @param value
* @author YellowTail
* @since 2019-02-14
*/
public void setRequestURI(String value) {
_requestURI = value;
}
@Override
public String getServletPath() {
if (null == _servletPath) {
_servletPath = super.getServletPath();
}
return _servletPath;
}
public void setServletPath(String value) {
_servletPath = value;
}
@Override
public String[] getParameterValues(String name) {
if (useExternalParam) {
String string = paramMap.get(name);
if (null == string) {
return null;
}
return new String [] {string};
}
return super.getParameterValues(name);
}
public void setUseExternalParam(boolean useExternalParam) {
this.useExternalParam = useExternalParam;
}
public void setParamMap(HashMap<String, String> paramMap) {
this.paramMap = paramMap;
}
/**
* 覆蓋 header 獲取的方法,進行自定義擴展
*/
@Override
public Enumeration<String> getHeaders(String name) {
//如果某個值被設(shè)置過,那么用自定義的值
if (null != _headers && _headers.containsKey(name)) {
String string = _headers.get(name);
Vector<String> values = new Vector<String>();
values.add(string);
return values.elements();
}
return super.getHeaders(name);
}
/**
* <br>設(shè)置 Header 里的值
*
* @param key
* @param value
* @author YellowTail
* @since 2019-02-15
*/
public void setHeader(String key, String value) {
if (null == _headers) {
_headers = new HashMap<>();
}
_headers.put(key, value);
}
}
實現(xiàn)代碼解釋
因為 DispatcherServlet 的很多方法都是 protected, friendly 懶的搞繼承,直接反射調(diào)用了
關(guān)于攔截器,因為我在實現(xiàn)的時候,是需要攔截器的一些效果(自動上傳日志),所以就執(zhí)行了步驟 4 和 6
大家在實現(xiàn)的時候,可以根據(jù)實際情況進行取舍
為何自定義request
因為 HttpServletRequest 只有一堆的 get 方法,沒有 set 方法
看了下實現(xiàn),反射好費勁,算了,直接繼承一個,復(fù)寫方法
效果
| Type | value |
|---|---|
| 接口地址 | /v1/iTest |
| 接口方法 | Post |
| 接口Header | header里的Accept Content-Type 都不需要設(shè)置,代碼已經(jīng)寫死為application/json設(shè)置為其他值不會生效 |
| 接口參數(shù) | 是否必填 | 解釋 |
|---|---|---|
url |
必填 |
準(zhǔn)備請求哪個接口 |
method |
必填 |
接口的方法(因為有些接口url一樣,但是method不一樣), 大小寫不敏感, get Get GET 都行 |
userId |
非必填 | 設(shè)置登錄態(tài),即想用哪個用戶請求接口 |
param |
非必填 | 請求接口的請求參數(shù),比如對于接口 /v1/xxx/me/list?unitId=2&nextid=&scope=2那么 param就是?后面的字符串,且需要進行url編碼也就是 unitId%3D2%26nextid%3D%26scope%3D2
|
BUG 修復(fù) 2019年2月20日 10:31:35
修復(fù)了 param 里面 參數(shù)值為空拋異常的問題
代碼已在此博客里更新
功能新增:把異常輸出到瀏覽器上,省去看日志的步驟
一旦代碼拋了異常,瀏覽器調(diào)用接口的時候,看不到信息
于是突發(fā)奇想,把 Exception 信息 參考 Logger 那種方式輸出到屏幕上,多方便
于是寫了一下,代碼在下面,沒有更新到 文章開始的那個代碼塊里
public static final String CHANGE_LINE = "\n".intern();
public static final String TAB_INDENT = " at ";
...
catch (Exception e) {
LOGGER.error("error, ", e);
StringBuilder sb = new StringBuilder();
sb.append(e.toString()).append(CHANGE_LINE);
for(StackTraceElement st: e.getStackTrace()) {
sb.append(TAB_INDENT)
.append(st.toString())
.append(CHANGE_LINE);
}
byte[] bytes = sb.toString().getBytes();
HttpEncryptService.setResponseData(bytes, response, false);
}