Spring Boot接口設(shè)計(jì)防篡改、防重放攻擊詳解
本示例主要內(nèi)容
- 請(qǐng)求參數(shù)防止篡改攻擊
- 基于timestamp方案,防止重放攻擊
- 使用swagger接口文檔自動(dòng)生成
API接口設(shè)計(jì)
API接口由于需要供第三方服務(wù)調(diào)用,所以必須暴露到外網(wǎng),并提供了具體請(qǐng)求地址和請(qǐng)求參數(shù),為了防止被別有用心之人獲取到真實(shí)請(qǐng)求參數(shù)后再次發(fā)起請(qǐng)求獲取信息,需要采取很多安全機(jī)制。
- 需要采用https方式對(duì)第三方提供接口,數(shù)據(jù)的加密傳輸會(huì)更安全,即便是被破解,也需要耗費(fèi)更多時(shí)間
- 需要有安全的后臺(tái)驗(yàn)證機(jī)制,達(dá)到防參數(shù)篡改+防二次請(qǐng)求(本示例內(nèi)容)
防止重放攻擊必須要保證請(qǐng)求只在限定的時(shí)間內(nèi)有效,需要通過在請(qǐng)求體中攜帶當(dāng)前請(qǐng)求的唯一標(biāo)識(shí),并且進(jìn)行簽名防止被篡改,所以防止重放攻擊需要建立在防止簽名被串改的基礎(chǔ)之上
防止篡改
- 客戶端使用約定好的秘鑰對(duì)傳輸參數(shù)進(jìn)行加密,得到簽名值sign1,并且將簽名值存入headers,發(fā)送請(qǐng)求給服務(wù)端
- 服務(wù)端接收客戶端的請(qǐng)求,通過過濾器使用約定好的秘鑰對(duì)請(qǐng)求的參數(shù)(headers除外)再次進(jìn)行簽名,得到簽名值sign2。
- 服務(wù)端對(duì)比sign1和sign2的值,如果對(duì)比一致,認(rèn)定為合法請(qǐng)求。如果對(duì)比不一致,說明參數(shù)被篡改,認(rèn)定為非法請(qǐng)求
基于timestamp的方案,防止重放
每次HTTP請(qǐng)求,headers都需要加上timestamp參數(shù),并且timestamp和請(qǐng)求的參數(shù)一起進(jìn)行數(shù)字簽名。因?yàn)橐淮握5腍TTP請(qǐng)求,從發(fā)出到達(dá)服務(wù)器一般都不會(huì)超過60s,所以服務(wù)器收到HTTP請(qǐng)求之后,首先判斷時(shí)間戳參數(shù)與當(dāng)前時(shí)間相比較,是否超過了60s,如果超過了則提示簽名過期(這個(gè)過期時(shí)間最好做成配置)。
一般情況下,黑客從抓包重放請(qǐng)求耗時(shí)遠(yuǎn)遠(yuǎn)超過了60s,所以此時(shí)請(qǐng)求中的timestamp參數(shù)已經(jīng)失效了。
如果黑客修改timestamp參數(shù)為當(dāng)前的時(shí)間戳,則sign參數(shù)對(duì)應(yīng)的數(shù)字簽名就會(huì)失效,因?yàn)楹诳筒恢篮灻罔€,沒有辦法生成新的數(shù)字簽名(前端一定要保護(hù)好秘鑰和加密算法)。
相關(guān)核心思路代碼
過濾器
@Slf4j
@Component
/**
* 防篡改、防重放攻擊過濾器
*/
public class SignAuthFilter implements Filter {
@Autowired
private SecurityProperties securityProperties;
@Override
public void init(FilterConfig filterConfig) {
log.info("初始化 SignAuthFilter");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 防止流讀取一次后就沒有了, 所以需要將流繼續(xù)寫出去
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletRequest requestWrapper = new RequestWrapper(httpRequest);
Set<String> uriSet = new HashSet<>(securityProperties.getIgnoreSignUri());
String requestUri = httpRequest.getRequestURI();
boolean isMatch = false;
for (String uri : uriSet) {
isMatch = requestUri.contains(uri);
if (isMatch) {
break;
}
}
log.info("當(dāng)前請(qǐng)求的URI是==>{},isMatch==>{}", httpRequest.getRequestURI(), isMatch);
if (isMatch) {
filterChain.doFilter(requestWrapper, response);
return;
}
String sign = requestWrapper.getHeader("Sign");
Long timestamp = Convert.toLong(requestWrapper.getHeader("Timestamp"));
if (StrUtil.isEmpty(sign)) {
returnFail("簽名不允許為空", response);
return;
}
if (timestamp == null) {
returnFail("時(shí)間戳不允許為空", response);
return;
}
//重放時(shí)間限制(單位分)
Long difference = DateUtil.between(DateUtil.date(), DateUtil.date(timestamp * 1000), DateUnit.MINUTE);
if (difference > securityProperties.getSignTimeout()) {
returnFail("已過期的簽名", response);
log.info("前端時(shí)間戳:{},服務(wù)端時(shí)間戳:{}", DateUtil.date(timestamp * 1000), DateUtil.date());
return;
}
boolean accept = true;
SortedMap<String, String> paramMap;
switch (requestWrapper.getMethod()) {
case "GET":
paramMap = HttpUtil.getUrlParams(requestWrapper);
accept = SignUtil.verifySign(paramMap, sign, timestamp);
break;
case "POST":
case "PUT":
case "DELETE":
paramMap = HttpUtil.getBodyParams(requestWrapper);
accept = SignUtil.verifySign(paramMap, sign, timestamp);
break;
default:
accept = true;
break;
}
if (accept) {
filterChain.doFilter(requestWrapper, response);
} else {
returnFail("簽名驗(yàn)證不通過", response);
}
}
private void returnFail(String msg, ServletResponse response) throws IOException {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
PrintWriter out = response.getWriter();
String result = JSONObject.toJSONString(AjaxResult.fail(msg));
out.println(result);
out.flush();
out.close();
}
@Override
public void destroy() {
log.info("銷毀 SignAuthFilter");
}
}
簽名驗(yàn)證
@Slf4j
public class SignUtil {
/**
* 驗(yàn)證簽名
*
* @param params
* @param sign
* @return
*/
public static boolean verifySign(SortedMap<String, String> params, String sign, Long timestamp) {
String paramsJsonStr = "Timestamp" + timestamp + JSONObject.toJSONString(params);
return verifySign(paramsJsonStr, sign);
}
/**
* 驗(yàn)證簽名
*
* @param params
* @param sign
* @return
*/
public static boolean verifySign(String params, String sign) {
log.info("Header Sign : {}", sign);
if (StringUtils.isEmpty(params)) {
return false;
}
log.info("Param : {}", params);
String paramsSign = getParamsSign(params);
log.info("Param Sign : {}", paramsSign);
return sign.equals(paramsSign);
}
/**
* @return 得到簽名
*/
public static String getParamsSign(String params) {
return DigestUtils.md5DigestAsHex(params.getBytes()).toUpperCase();
}
}
不做簽名驗(yàn)證的接口做成配置(application.yml)
spring: security: # 簽名驗(yàn)證超時(shí)時(shí)間 signTimeout: 300 # 允許未簽名訪問的url地址 ignoreSignUri: - /swagger-ui.html - /swagger-resources - /v2/api-docs - /webjars/springfox-swagger-ui - /csrf
屬性代碼(SecurityProperties.java)
@Component
@ConfigurationProperties(prefix = "spring.security")
@Data
public class SecurityProperties {
/**
* 允許忽略簽名地址
*/
List<String> ignoreSignUri;
/**
* 簽名超時(shí)時(shí)間(分)
*/
Integer signTimeout;
}
簽名測(cè)試控制器
@RestController
@Slf4j
@RequestMapping("/sign")
@Api(value = "簽名controller", tags = {"簽名測(cè)試接口"})
public class SignController {
@ApiOperation("get測(cè)試")
@ApiImplicitParams({
@ApiImplicitParam(name = "username", value = "用戶名", required = true, dataType = "String"),
@ApiImplicitParam(name = "password", value = "密碼", required = true, dataType = "String")
})
@GetMapping("/testGet")
public AjaxResult testGet(String username, String password) {
log.info("username:{},password:{}", username, password);
return AjaxResult.success("GET參數(shù)檢驗(yàn)成功");
}
@ApiOperation("post測(cè)試")
@ApiImplicitParams({
@ApiImplicitParam(name = "data", value = "測(cè)試實(shí)體", required = true, dataType = "TestVo")
})
@PostMapping("/testPost")
public AjaxResult<TestVo> testPost(@Valid @RequestBody TestVo data) {
return AjaxResult.success("POST參數(shù)檢驗(yàn)成功", data);
}
@ApiOperation("put測(cè)試")
@ApiImplicitParams({
@ApiImplicitParam(name = "id", value = "編號(hào)", required = true, dataType = "Integer"),
@ApiImplicitParam(name = "data", value = "測(cè)試實(shí)體", required = true, dataType = "TestVo")
})
@PutMapping("/testPut/{id}")
public AjaxResult testPut(@PathVariable Integer id, @RequestBody TestVo data) {
data.setId(id);
return AjaxResult.success("PUT參數(shù)檢驗(yàn)成功", data);
}
@ApiOperation("delete測(cè)試")
@ApiImplicitParams({
@ApiImplicitParam(name = "idList", value = "編號(hào)列表", required = true, dataType = "List<Integer> ")
})
@DeleteMapping("/testDelete")
public AjaxResult testDelete(@RequestBody List<Integer> idList) {
return AjaxResult.success("DELETE參數(shù)檢驗(yàn)成功", idList);
}
}
前端js請(qǐng)求示例
var settings = {
"async": true,
"crossDomain": true,
"url": "http://localhost:8080/sign/testGet?username=abc&password=123",
"method": "GET",
"headers": {
"Sign": "46B1990701BCF090E3E6E517751DB02F",
"Timestamp": "1564126422",
"User-Agent": "PostmanRuntime/7.15.2",
"Accept": "*/*",
"Cache-Control": "no-cache",
"Postman-Token": "a9d10ef5-283b-4ed3-8856-72d4589fb61d,6e7fa816-000a-4b29-9882-56d6ae0f33fb",
"Host": "localhost:8080",
"Cookie": "SESSION=OWYyYzFmMDMtODkyOC00NDg5LTk4ZTYtODNhYzcwYjQ5Zjg2",
"Accept-Encoding": "gzip, deflate",
"Connection": "keep-alive",
"cache-control": "no-cache"
}
}
$.ajax(settings).done(function (response) {
console.log(response);
});
注意事項(xiàng)
- 該示例沒有設(shè)置秘鑰,只做了參數(shù)升排然后創(chuàng)建md5簽名
- 示例請(qǐng)求的參數(shù)md5原文本為:Timestamp1564126422{"password":"123","username":"abc"}
- 注意headers請(qǐng)求頭帶上了Sign和Timestamp參數(shù)
- js讀取的Timestamp必須要在服務(wù)端獲取
- 該示例不包括分布試環(huán)境下,多臺(tái)服務(wù)器時(shí)間同步問題
自動(dòng)生成接口文檔
配置代碼
@Configuration
@EnableSwagger2
public class Swagger2Config {
@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("com.easy.sign"))
.paths(PathSelectors.any())
.build();
}
//構(gòu)建 api文檔的詳細(xì)信息函數(shù),注意這里的注解引用的是哪個(gè)
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("簽名示例")
.contact(new Contact("簽名示例網(wǎng)站", "http://www.baidu.com", "test@qq.com"))
.version("1.0.0")
.description("簽名示例接口描述")
.build();
}
}
自動(dòng)生成文檔地址:http://localhost:8080/swagger-ui.html
資料
總結(jié)
以上就是這篇文章的全部?jī)?nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,謝謝大家對(duì)腳本之家的支持。
相關(guān)文章
RocketMQ實(shí)現(xiàn)隨緣分BUG小功能示例詳解
這篇文章主要為大家介紹了RocketMQ實(shí)現(xiàn)隨緣分BUG小功能示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-08-08
設(shè)置JavaScript自動(dòng)提示-Eclipse/MyEclipse
自動(dòng)提示需要2個(gè)組件,分別是:ext-4.0.2a.jsb2||spket-1.6.16.jar,需要的朋友可以參考下2016-05-05
詳解Java如何在業(yè)務(wù)代碼中優(yōu)雅的使用策略模式
這篇文章主要為大家介紹了Java如何在業(yè)務(wù)代碼中優(yōu)雅的使用策略模式,文中的示例代碼講解詳細(xì),具有一定的學(xué)習(xí)價(jià)值,感興趣的可以了解下2023-08-08
Java上傳文件錯(cuò)誤java.lang.NoSuchMethodException的解決辦法
今天小編就為大家分享一篇關(guān)于Java上傳文件錯(cuò)誤java.lang.NoSuchMethodException的解決辦法,小編覺得內(nèi)容挺不錯(cuò)的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來看看吧2019-01-01
Java輸出通過InetAddress獲得的IP地址數(shù)組詳細(xì)解析
由于byte被認(rèn)為是unsigned byte,所以最高位的1將會(huì)被解釋為符號(hào)位,另外Java中存儲(chǔ)是按照補(bǔ)碼存儲(chǔ),所以1000 0111會(huì)被認(rèn)為是補(bǔ)碼形式,轉(zhuǎn)換成原碼便是1111 0001,轉(zhuǎn)換成十進(jìn)制數(shù)便是-1212013-09-09
MyBatis的9種動(dòng)態(tài)標(biāo)簽詳解
大家好,本篇文章主要講的是MyBatis的9種動(dòng)態(tài)標(biāo)簽詳解,感興趣的同學(xué)趕快來看一看吧,感興趣的同學(xué)趕快來看一看吧2021-12-12
一個(gè)MIDP俄羅斯方塊游戲的設(shè)計(jì)和實(shí)現(xiàn)
一個(gè)MIDP俄羅斯方塊游戲的設(shè)計(jì)和實(shí)現(xiàn)...2006-12-12
SpringBoot使用Redisson實(shí)現(xiàn)延遲執(zhí)行的完整示例
這篇文章主要介紹了SpringBoot使用Redisson實(shí)現(xiàn)延遲執(zhí)行的完整示例,文中通過代碼示例講解的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作有一定的幫助,需要的朋友可以參考下2024-06-06

