您好,登錄后才能下訂單哦!
使用Spring Boot接口怎么實現防篡改、防重放攻擊?相信很多沒有經驗的人對此束手無策,為此本文總結了問題出現的原因和解決方法,通過這篇文章希望你能解決這個問題。
API接口設計
API接口由于需要供第三方服務調用,所以必須暴露到外網,并提供了具體請求地址和請求參數,為了防止被別有用心之人獲取到真實請求參數后再次發起請求獲取信息,需要采取很多安全機制。
需要采用https方式對第三方提供接口,數據的加密傳輸會更安全,即便是被破解,也需要耗費更多時間
需要有安全的后臺驗證機制,達到防參數篡改+防二次請求(本示例內容)
防止重放攻擊必須要保證請求只在限定的時間內有效,需要通過在請求體中攜帶當前請求的唯一標識,并且進行簽名防止被篡改,所以防止重放攻擊需要建立在防止簽名被串改的基礎之上
防止篡改
客戶端使用約定好的秘鑰對傳輸參數進行加密,得到簽名值sign1,并且將簽名值存入headers,發送請求給服務端
服務端接收客戶端的請求,通過過濾器使用約定好的秘鑰對請求的參數(headers除外)再次進行簽名,得到簽名值sign2。
服務端對比sign1和sign2的值,如果對比一致,認定為合法請求。如果對比不一致,說明參數被篡改,認定為非法請求
基于timestamp的方案,防止重放
每次HTTP請求,headers都需要加上timestamp參數,并且timestamp和請求的參數一起進行數字簽名。因為一次正常的HTTP請求,從發出到達服務器一般都不會超過60s,所以服務器收到HTTP請求之后,首先判斷時間戳參數與當前時間相比較,是否超過了60s,如果超過了則提示簽名過期(這個過期時間最好做成配置)。
一般情況下,黑客從抓包重放請求耗時遠遠超過了60s,所以此時請求中的timestamp參數已經失效了。
如果黑客修改timestamp參數為當前的時間戳,則sign參數對應的數字簽名就會失效,因為黑客不知道簽名秘鑰,沒有辦法生成新的數字簽名(前端一定要保護好秘鑰和加密算法)。
相關核心思路代碼
過濾器
@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 { // 防止流讀取一次后就沒有了, 所以需要將流繼續寫出去 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("當前請求的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("時間戳不允許為空", response); return; } //重放時間限制(單位分) Long difference = DateUtil.between(DateUtil.date(), DateUtil.date(timestamp * 1000), DateUnit.MINUTE); if (difference > securityProperties.getSignTimeout()) { returnFail("已過期的簽名", response); log.info("前端時間戳:{},服務端時間戳:{}", 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("簽名驗證不通過", 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"); } }
簽名驗證
@Slf4j public class SignUtil { /** * 驗證簽名 * * @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); } /** * 驗證簽名 * * @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(); } }
不做簽名驗證的接口做成配置(application.yml)
spring: security: # 簽名驗證超時時間 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; /** * 簽名超時時間(分) */ Integer signTimeout; }
簽名測試控制器
@RestController @Slf4j @RequestMapping("/sign") @Api(value = "簽名controller", tags = {"簽名測試接口"}) public class SignController { @ApiOperation("get測試") @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參數檢驗成功"); } @ApiOperation("post測試") @ApiImplicitParams({ @ApiImplicitParam(name = "data", value = "測試實體", required = true, dataType = "TestVo") }) @PostMapping("/testPost") public AjaxResult<TestVo> testPost(@Valid @RequestBody TestVo data) { return AjaxResult.success("POST參數檢驗成功", data); } @ApiOperation("put測試") @ApiImplicitParams({ @ApiImplicitParam(name = "id", value = "編號", required = true, dataType = "Integer"), @ApiImplicitParam(name = "data", value = "測試實體", required = true, dataType = "TestVo") }) @PutMapping("/testPut/{id}") public AjaxResult testPut(@PathVariable Integer id, @RequestBody TestVo data) { data.setId(id); return AjaxResult.success("PUT參數檢驗成功", data); } @ApiOperation("delete測試") @ApiImplicitParams({ @ApiImplicitParam(name = "idList", value = "編號列表", required = true, dataType = "List<Integer> ") }) @DeleteMapping("/testDelete") public AjaxResult testDelete(@RequestBody List<Integer> idList) { return AjaxResult.success("DELETE參數檢驗成功", idList); } }
前端js請求示例
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); });
注意事項
該示例沒有設置秘鑰,只做了參數升排然后創建md5簽名
示例請求的參數md5原文本為:Timestamp1564126422{"password":"123","username":"abc"}
注意headers請求頭帶上了Sign和Timestamp參數
js讀取的Timestamp必須要在服務端獲取
該示例不包括分布試環境下,多臺服務器時間同步問題
自動生成接口文檔
配置代碼
@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(); } //構建 api文檔的詳細信息函數,注意這里的注解引用的是哪個 private ApiInfo apiInfo() { return new ApiInfoBuilder() .title("簽名示例") .contact(new Contact("簽名示例網站", "http://www.baidu.com", "test@qq.com")) .version("1.0.0") .description("簽名示例接口描述") .build(); } }
springboot一種全新的編程規范,其設計目的是用來簡化新Spring應用的初始搭建以及開發過程,SpringBoot也是一個服務于框架的框架,服務范圍是簡化配置文件。
看完上述內容,你們掌握使用Spring Boot接口怎么實現防篡改、防重放攻擊的方法了嗎?如果還想學到更多技能或想了解更多相關內容,歡迎關注億速云行業資訊頻道,感謝各位的閱讀!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。