七、接口版本控制
1、簡(jiǎn)介
在SpringBoot項(xiàng)目中,如果要進(jìn)行restful接口的版本控制一般有以下幾個(gè)方向:
- 基于path的版本控制
- 基于header的版本控制
在spring MVC下,url映射到哪個(gè)method是由RequestMappingHandlerMapping
來控制的,那么我們也是通過RequestMappingHandlerMapping
來做版本控制的。
2、Path控制實(shí)現(xiàn)
首先定義一個(gè)注解
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiVersion {
// 默認(rèn)接口版本號(hào)1.0開始,這里我只做了兩級(jí),多級(jí)可在正則進(jìn)行控制
String value() default "1.0";
}
ApiVersionCondition
用來控制當(dāng)前request 指向哪個(gè)method
public class ApiVersionCondition implements RequestCondition<ApiVersionCondition> {
private static final Pattern VERSION_PREFIX_PATTERN = Pattern.compile("v(\\\\d+\\\\.\\\\d+)");
private final String version;
public ApiVersionCondition(String version) {
this.version = version;
}
@Override
public ApiVersionCondition combine(ApiVersionCondition other) {
// 采用最后定義優(yōu)先原則,則方法上的定義覆蓋類上面的定義
return new ApiVersionCondition(other.getApiVersion());
}
@Override
public ApiVersionCondition getMatchingCondition(HttpServletRequest httpServletRequest) {
Matcher m = VERSION_PREFIX_PATTERN.matcher(httpServletRequest.getRequestURI());
if (m.find()) {
String pathVersion = m.group(1);
// 這個(gè)方法是精確匹配
if (Objects.equals(pathVersion, version)) {
return this;
}
// 該方法是只要大于等于最低接口version即匹配成功,需要和compareTo()配合
// 舉例:定義有1.0/1.1接口,訪問1.2,則實(shí)際訪問的是1.1,如果從小開始那么排序反轉(zhuǎn)即可
// if(Float.parseFloat(pathVersion)>=Float.parseFloat(version)){
// return this;
// }
}
return null;
}
@Override
public int compareTo(ApiVersionCondition other, HttpServletRequest request) {
return 0;
// 優(yōu)先匹配最新的版本號(hào),和getMatchingCondition注釋掉的代碼同步使用
// return other.getApiVersion().compareTo(this.version);
}
public String getApiVersion() {
return version;
}
}
PathVersionHandlerMapping
用于注入spring用來管理
public class PathVersionHandlerMapping extends RequestMappingHandlerMapping {
@Override
protected boolean isHandler(Class? beanType) {
return AnnotatedElementUtils.hasAnnotation(beanType, Controller.class);
}
@Override
protected RequestCondition? getCustomTypeCondition(Class? handlerType) {
ApiVersion apiVersion = AnnotationUtils.findAnnotation(handlerType,ApiVersion.class);
return createCondition(apiVersion);
}
@Override
protected RequestCondition? getCustomMethodCondition(Method method) {
ApiVersion apiVersion = AnnotationUtils.findAnnotation(method,ApiVersion.class);
return createCondition(apiVersion);
}
private RequestCondition
WebMvcConfiguration
配置類讓spring來接管
@Configuration
public class WebMvcConfiguration implements WebMvcRegistrations {
@Override
public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
return new PathVersionHandlerMapping();
}
}
最后controller進(jìn)行測(cè)試,默認(rèn)是v1.0,如果方法上有注解,以方法上的為準(zhǔn)(該方法vx.x在路徑任意位置出現(xiàn)都可解析)
@RestController
@ApiVersion
@RequestMapping(value = "/{version}/test")
public class TestController {
@GetMapping(value = "one")
public String query(){
return "test api default";
}
@GetMapping(value = "one")
@ApiVersion("1.1")
public String query2(){
return "test api v1.1";
}
@GetMapping(value = "one")
@ApiVersion("3.1")
public String query3(){
return "test api v3.1";
}
}
3、header控制實(shí)現(xiàn)
總體原理與Path類似,修改ApiVersionCondition
即可,之后訪問時(shí)在header帶上X-VERSION
參數(shù)即可
public class ApiVersionCondition implements RequestCondition<ApiVersionCondition> {
private static final String X_VERSION = "X-VERSION";
private final String version ;
public ApiVersionCondition(String version) {
this.version = version;
}
@Override
public ApiVersionCondition combine(ApiVersionCondition other) {
// 采用最后定義優(yōu)先原則,則方法上的定義覆蓋類上面的定義
return new ApiVersionCondition(other.getApiVersion());
}
@Override
public ApiVersionCondition getMatchingCondition(HttpServletRequest httpServletRequest) {
String headerVersion = httpServletRequest.getHeader(X_VERSION);
if(Objects.equals(version,headerVersion)){
return this;
}
return null;
}
@Override
public int compareTo(ApiVersionCondition apiVersionCondition, HttpServletRequest httpServletRequest) {
return 0;
}
public String getApiVersion() {
return version;
}
}
八、API接口安全
1、簡(jiǎn)介
APP、前后端分離項(xiàng)目都采用[API]接口形式與服務(wù)器進(jìn)行數(shù)據(jù)通信,傳輸?shù)臄?shù)據(jù)被偷窺、被抓包、被偽造時(shí)有發(fā)生,那么如何設(shè)計(jì)一套比較安全的API接口方案至關(guān)重要,一般的解決方案有以下幾點(diǎn):
- Token授權(quán)認(rèn)證,防止未授權(quán)用戶獲取數(shù)據(jù);
- 時(shí)間戳超時(shí)機(jī)制;
- URL簽名,防止請(qǐng)求參數(shù)被篡改;
- 防重放,防止接口被第二次請(qǐng)求,防采集;
- 采用HTTPS通信協(xié)議,防止數(shù)據(jù)明文傳輸;
2、Token授權(quán)認(rèn)證
因?yàn)镠TTP協(xié)議是無狀態(tài)的,Token的設(shè)計(jì)方案是用戶在客戶端使用用戶名和密碼登錄后,服務(wù)器會(huì)給客戶端返回一個(gè)Token,并將Token以鍵值對(duì)的形式存放在緩存(一般是Redis)中,后續(xù)客戶端對(duì)需要授權(quán)模塊的所有操作都要帶上這個(gè)Token,服務(wù)器端接收到請(qǐng)求后進(jìn)行Token驗(yàn)證,如果Token存在,說明是授權(quán)的請(qǐng)求。
Token生成的設(shè)計(jì)要求
- 應(yīng)用內(nèi)一定要唯一,否則會(huì)出現(xiàn)授權(quán)混亂,A用戶看到了B用戶的數(shù)據(jù);
- 每次生成的[Token]一定要不一樣,防止被記錄,授權(quán)永久有效;
- 一般Token對(duì)應(yīng)的是Redis的key,value存放的是這個(gè)用戶相關(guān)緩存信息,比如:用戶的id;
- 要設(shè)置Token的過期時(shí)間,過期后需要客戶端重新登錄,獲取新的Token,如果[Token]有效期設(shè)置較短,會(huì)反復(fù)需要用戶登錄,體驗(yàn)比較差,我們一般采用Token過期后,客戶端靜默登錄的方式,當(dāng)客戶端收到[Token]過期后,客戶端用本地保存的用戶名和密碼在后臺(tái)靜默登錄來獲取新的[Token],還有一種是單獨(dú)出一個(gè)刷新Token的接口,但是一定要注意刷新機(jī)制和安全問題;
根據(jù)上面的設(shè)計(jì)方案要求,我們很容易得到Token=md5(用戶ID+登錄的時(shí)間戳+服務(wù)器端秘鑰)這種方式來獲得Token,因?yàn)橛脩鬒D是應(yīng)用內(nèi)唯一的,登錄的時(shí)間戳保證每次登錄的時(shí)候都不一樣,服務(wù)器端秘鑰是配置在服務(wù)器端參與加密的字符串(即:鹽),目的是提高Token加密的破解難度,注意一定不要泄漏
3、時(shí)間戳超時(shí)機(jī)制
客戶端每次請(qǐng)求接口都帶上當(dāng)前時(shí)間的時(shí)間戳timestamp,服務(wù)端接收到timestamp后跟當(dāng)前時(shí)間進(jìn)行比對(duì),如果時(shí)間差大于一定時(shí)間(比如:1分鐘),則認(rèn)為該請(qǐng)求失效。 時(shí)間戳超時(shí)機(jī)制是防御DOS攻擊的有效手段。 例如http://url/getInfo?id=1&timetamp=1661061696
4、URL簽名
寫過支付寶或微信支付對(duì)接的同學(xué)肯定對(duì)URL簽名不陌生,我們只需要將原本發(fā)送給server端的明文參數(shù)做一下簽名,然后在server端用相同的算法再做一次簽名,對(duì)比兩次簽名就可以確保對(duì)應(yīng)明文的參數(shù)有沒有被中間人篡改過。例如http://url/getInfo?id=1&timetamp=1559396263&sign=e10adc3949ba59abbe56e057f20f883e
簽名算法過程
- 首先對(duì)通信的參數(shù)按key進(jìn)行字母排序放入數(shù)組中(一般請(qǐng)求的接口地址也要參與排序和簽名,那么需要額外添加
url=http://url/getInfo
這個(gè)參數(shù)) - 對(duì)排序完的數(shù)組鍵值對(duì)用&進(jìn)行連接,形成用于加密的參數(shù)字符串
- 在加密的參數(shù)字符串前面或者后面加上私鑰,然后用md5進(jìn)行加密,得到sign,然后隨著請(qǐng)求接口一起傳給服務(wù)器。服務(wù)器端接收到請(qǐng)求后,用同樣的算法獲得服務(wù)器的sign,對(duì)比客戶端的sign是否一致,如果一致請(qǐng)求有效
5、防重放
客戶端第一次訪問時(shí),將簽名sign存放到服務(wù)器的Redis中,超時(shí)時(shí)間設(shè)定為跟時(shí)間戳的超時(shí)時(shí)間一致,二者時(shí)間一致可以保證無論在timestamp限定時(shí)間內(nèi)還是外 URL都只能訪問一次,如果被非法者截獲,使用同一個(gè)URL再次訪問,如果發(fā)現(xiàn)緩存服務(wù)器中已經(jīng)存在了本次簽名,則拒絕服務(wù)。
如果在緩存中的簽名失效的情況下,有人使用同一個(gè)URL再次訪問,則會(huì)被時(shí)間戳超時(shí)機(jī)制攔截,這就是為什么要求sign的超時(shí)時(shí)間要設(shè)定為跟時(shí)間戳的超時(shí)時(shí)間一致。拒絕重復(fù)調(diào)用機(jī)制確保URL被別人截獲了也無法使用(如抓取數(shù)據(jù))
方案流程
- 客戶端通過用戶名密碼登錄服務(wù)器并獲取Token;
- 客戶端生成時(shí)間戳timestamp,并將timestamp作為其中一個(gè)參數(shù);
- 客戶端將所有的參數(shù),包括Token和timestamp按照自己的簽名算法進(jìn)行排序加密得到簽名sign
- 將token、timestamp和sign作為請(qǐng)求時(shí)必須攜帶的參數(shù)加在每個(gè)請(qǐng)求的URL后邊,例:
http://url/request?token=h40adc3949bafjhbbe56e027f20f583a&timetamp=1559396263&sign=e10adc3949ba59abbe56e057f20f883e
- 服務(wù)端對(duì)token、timestamp和sign進(jìn)行驗(yàn)證,只有在token有效、timestamp未超時(shí)、緩存服務(wù)器中不存在sign三種情況同時(shí)滿足,本次請(qǐng)求才有效;
6、采用HTTPS通信協(xié)議
安全套接字層超文本傳輸協(xié)議HTTPS,為了數(shù)據(jù)傳輸?shù)陌踩?,HTTPS在HTTP的基礎(chǔ)上加入了SSL協(xié)議,SSL依靠證書來驗(yàn)證服務(wù)器的身份,并為客戶端和服務(wù)器之間的通信加密。
HTTPS也不是絕對(duì)安全的,比如中間人劫持攻擊,中間人可以獲取到客戶端與服務(wù)器之間所有的通信內(nèi)容
九、總結(jié)
自此整個(gè)后端接口基本體系就構(gòu)建完畢了
- 通過Validator + 自動(dòng)拋出異常來完成了方便的參數(shù)校驗(yàn)
- 通過全局異常處理 + 自定義異常完成了異常操作的規(guī)范
- 通過數(shù)據(jù)統(tǒng)一響應(yīng)完成了響應(yīng)數(shù)據(jù)的規(guī)范
- 多個(gè)方面組裝非常優(yōu)雅的完成了后端接口的協(xié)調(diào),讓開發(fā)人員有更多的經(jīng)歷注重業(yè)務(wù)邏輯代碼,輕松構(gòu)建后端接口
這里再說幾點(diǎn)
- controller做好try-catch工作,及時(shí)捕獲異常,可以再次拋出到全局,統(tǒng)一格式返回前端
- 做好日志系統(tǒng),關(guān)鍵位置一定要有日志
- 做好全局統(tǒng)一返回類,整個(gè)項(xiàng)目規(guī)范好定義好
- controller入?yún)⒆侄慰梢猿橄蟪鲆粋€(gè)公共基類,在此基礎(chǔ)上進(jìn)行繼承擴(kuò)充
- controller層做好入?yún)?shù)校驗(yàn)
- 接口安全驗(yàn)證
-
接口
+關(guān)注
關(guān)注
33文章
9005瀏覽量
153761 -
URL
+關(guān)注
關(guān)注
0文章
141瀏覽量
15868 -
后端
+關(guān)注
關(guān)注
0文章
32瀏覽量
2401 -
SpringBoot
+關(guān)注
關(guān)注
0文章
175瀏覽量
401
發(fā)布評(píng)論請(qǐng)先 登錄
請(qǐng)問可變?cè)鲆娣糯笃鰽D8369后端接模數(shù)轉(zhuǎn)換器AD9268怎么匹配?
SpringBoot應(yīng)用啟動(dòng)運(yùn)行run方法
什么是 SpringBoot?

SpringBoot的核心注解1

SpringBoot的核心注解2

SpringBoot 后端接口規(guī)范(上)

SpringBoot 后端接口規(guī)范(中)

前后端分離必備的接口規(guī)范

評(píng)論