最近我司業(yè)務(wù)上需要對(duì)接第三方各大銀行平臺(tái),調(diào)用第三方接口和提供接口供第三方調(diào)用,這時(shí)候的對(duì)外open接口安全性就得重視了,再有就是之前我在知乎上發(fā)布一篇《Spring Security實(shí)現(xiàn)后端接口權(quán)限驗(yàn)證》的總結(jié),有個(gè)兄弟提出一個(gè)問(wèn)題:只做接口功能菜單權(quán)限檢驗(yàn)還不夠,還得做數(shù)據(jù)權(quán)限檢驗(yàn)才行,舉個(gè)例子:用戶(hù)A有刪除某條數(shù)據(jù)的接口權(quán)限,這個(gè)接口的參數(shù)是傳記錄id來(lái)刪除的(ps:平時(shí)我們開(kāi)發(fā)接口也是這么做的),后端執(zhí)行的邏輯就是通過(guò)登錄信息通過(guò)用戶(hù)認(rèn)證,然后再判斷接口菜單權(quán)限,緊接著就執(zhí)行如下SQL邏輯:
delete from table where id=?
這里的id就是掉接口傳遞的參數(shù),這時(shí)候假如用戶(hù)B知道了怎么調(diào)接口,就根據(jù)id自增長(zhǎng)的特性隨意傳id,就會(huì)刪掉別人的數(shù)據(jù),所以這是一個(gè)嚴(yán)重的問(wèn)題,要解決這問(wèn)題可以像上面說(shuō)的一樣加上數(shù)據(jù)權(quán)限,執(zhí)行邏輯如下:
delete from table where id=? and user_id = userId
這樣就避免數(shù)據(jù)被別人操作了,也就是加上了數(shù)據(jù)權(quán)限判斷,但是卻給業(yè)務(wù)邏輯增加了復(fù)雜性同時(shí)老接口業(yè)務(wù)邏輯難以適配,本質(zhì)上來(lái)說(shuō)web頁(yè)面上看到的數(shù)據(jù)就是根據(jù)用戶(hù)角色做過(guò)數(shù)據(jù)隔離的,可以這么理解你能看到哪些數(shù)據(jù)和你有那些功能菜單操作權(quán)限就差不多避免上面所說(shuō)的情況了,但是保不準(zhǔn)懂代碼的人使用postman等工具惡意調(diào)接口而產(chǎn)生上面的情況,我們還是得正視這個(gè)問(wèn)題,既然通過(guò)數(shù)據(jù)權(quán)限解決該問(wèn)題不太友好,那么我們可以再思考下怎么避免這個(gè)問(wèn)題???這個(gè)問(wèn)題可以轉(zhuǎn)換為怎么避免別人輕易就能調(diào)通接口,解決辦法就是不能在外網(wǎng)暴露接口信息,拒絕接口裸奔,從而有效提高接口安全性,這也是今天我們這篇總結(jié)的核心主旨。當(dāng)然這里強(qiáng)調(diào)一下我這里說(shuō)的是有效提高,不是絕對(duì)保證安全,做不到...
項(xiàng)目推薦:基于SpringBoot2.x、SpringCloud和SpringCloudAlibaba企業(yè)級(jí)系統(tǒng)架構(gòu)底層框架封裝,解決業(yè)務(wù)開(kāi)發(fā)時(shí)常見(jiàn)的非功能性需求,防止重復(fù)造輪子,方便業(yè)務(wù)快速開(kāi)發(fā)和企業(yè)技術(shù)棧框架統(tǒng)一管理。引入組件化的思想實(shí)現(xiàn)高內(nèi)聚低耦合并且高度可配置化,做到可插拔。嚴(yán)格控制包依賴(lài)和統(tǒng)一版本管理,做到最少化依賴(lài)。注重代碼規(guī)范和注釋?zhuān)浅_m合個(gè)人學(xué)習(xí)和企業(yè)使用
Github地址:https://github.com/plasticene/plasticene-boot-starter-parent
Gitee地址:https://gitee.com/plasticene3/plasticene-boot-starter-parent
在Spring Boot項(xiàng)目中提高接口安全的核心所在:加密和加簽,加固接口參數(shù)、驗(yàn)證復(fù)雜度。
加密:對(duì)參數(shù)進(jìn)行加密傳輸,拒絕接口參數(shù)直接暴露,這樣就可以有效做到防止別人輕易準(zhǔn)確地獲取到接口參數(shù)定義和傳參格式要求了。
加簽:對(duì)接口參數(shù)進(jìn)行加簽,可以有效防止接口參數(shù)被篡改和接口參數(shù)被重放惡刷。
現(xiàn)今有許許多多的加密算法,這里就不對(duì)算法進(jìn)行過(guò)度敘述,畢竟不是我們今天的主題,但是加密算法大體分為非對(duì)稱(chēng)加密和對(duì)稱(chēng)加密。
非對(duì)稱(chēng)加密算法是一種密鑰的保密方法。非對(duì)稱(chēng)加密算法需要兩個(gè)密鑰:公開(kāi)密鑰(publickey:簡(jiǎn)稱(chēng)公鑰)和私有密鑰(privatekey:簡(jiǎn)稱(chēng)私鑰)。公鑰與私鑰是一對(duì),如果用公鑰對(duì)數(shù)據(jù)進(jìn)行加密,只有用對(duì)應(yīng)的私鑰才能解密。因?yàn)榧用芎徒饷苁褂玫氖莾蓚€(gè)不同的密鑰,所以這種算法叫作非對(duì)稱(chēng)加密算法。
加密秘鑰和解密秘鑰是一樣,當(dāng)你的密鑰被別人知道后,就沒(méi)有秘密可言了。
經(jīng)過(guò)需求分析和科學(xué)借鑒我們采用了非對(duì)稱(chēng)加密算法RSA和對(duì)稱(chēng)加密算法AES來(lái)完成接口加密。至于這兩種加密算法的原理與實(shí)現(xiàn)有興趣自己去查資料,我這里就說(shuō)一下選它們的原因:
AES 是對(duì)稱(chēng)加密算法,優(yōu)點(diǎn):加密速度快;缺點(diǎn):如果秘鑰丟失,就容易解密密文,安全性相對(duì)比較差
RSA 是非對(duì)稱(chēng)加密算法 , 優(yōu)點(diǎn):安全 ;缺點(diǎn):加密速度慢
接口參數(shù)加解密的流程大致如圖所示:
圖片
具體步驟如下:
簽名驗(yàn)證也是當(dāng)下提高接口安全性主要措施之一,核心就是客戶(hù)端在調(diào)用接口時(shí)按照一定規(guī)則生成簽名sign,服務(wù)端拿到簽名sign之后進(jìn)行驗(yàn)證操作,大致流程如下:
圖片
具體步驟:
在實(shí)現(xiàn)這個(gè)需求時(shí),考慮到全公司的多個(gè)團(tuán)隊(duì)開(kāi)發(fā)使用的通用性和便捷性,所以我們對(duì)加密、加簽操作進(jìn)行了公共的抽取封裝,同時(shí)通過(guò)一個(gè)注解@ApiSecurity來(lái)標(biāo)識(shí)接口是否需要進(jìn)行加密、加簽操作,在業(yè)務(wù)側(cè)極大程度地降低了開(kāi)發(fā)使用成本,不用寫(xiě)冗余代碼,做到了真正的優(yōu)雅。
@Retention(RetentionPolicy.RUNTIME)@Target({ElementType.TYPE, ElementType.METHOD})@Documentedpublic @interface ApiSecurity { @Alias("isSign") boolean value() default true; /** * 是否加簽驗(yàn)證,默認(rèn)開(kāi)啟 * @return */ @Alias("value") boolean isSign() default true; /** * 接口請(qǐng)求參數(shù)是否需要解密 * @return */ boolean decryptRequest() default false; /** * 接口響應(yīng)參數(shù)是否需要加密 * @return */ boolean encryptResponse() default false;}
這里注解屬性可以看到簽名驗(yàn)證默認(rèn)是開(kāi)啟的,因?yàn)槲覀冋J(rèn)為接口安全性加簽是必須的,至于參數(shù)加解密可以視情況而定,通過(guò)屬性配置開(kāi)關(guān),做到了極致的靈活性,這也是優(yōu)雅呀。
使用案例:下面就是一個(gè)需要加密加簽的接口
@PostMapping("/security") @ApiSecurity(encryptResponse = true, decryptRequest = true) public User testApiSecurity(@RequestBody User user) { System.out.println(user); return user; }
可以看到我們?cè)陧?xiàng)目業(yè)務(wù)服務(wù)中只需要@ApiSecurity就可以了,就是這么簡(jiǎn)單,至于怎么實(shí)現(xiàn)的下面我們就來(lái)看看。
為了全公司對(duì)接口加密、加簽功能實(shí)現(xiàn)統(tǒng)一和規(guī)范,我們將實(shí)現(xiàn)抽取,封裝集成在公司自定義的web starter中,這樣只要項(xiàng)目服務(wù)引入這個(gè)starter依賴(lài)就可以使用該功能了
首先我們對(duì)加密傳輸?shù)膮?shù)bean進(jìn)行規(guī)定封裝如下:
@Datapublic class ApiSecurityParam { /** * 應(yīng)用id */ private String appId; /** * RSA加密后的aes秘鑰,需解密 */ private String key; /** * AES加密的json參數(shù) */ private String data; /** * 簽名 */ private String sign; /** * 時(shí)間戳 */ private String timestamp; /** * 請(qǐng)求唯一標(biāo)識(shí) */ private String nonce;}
等于說(shuō)加密、加簽的參數(shù)格式,調(diào)用方需按照上面的對(duì)象傳參,當(dāng)然為了提高拓展性,簽名的相關(guān)信息sign、timestamp、nonce可以放到請(qǐng)求的header里面,也能獲取到。拿到apiSecurityParam我們就可以進(jìn)行請(qǐng)求參數(shù)解密、驗(yàn)簽了,需要通過(guò)判斷是否使用了注解@ApiSecuriy來(lái)決定是否執(zhí)行請(qǐng)求參數(shù)解密、驗(yàn)簽邏輯,這就正好可以使用基于注解的切面實(shí)現(xiàn)啦,在說(shuō)切面之前,先說(shuō)說(shuō)一次接口請(qǐng)求requestBody的輸入流InputStream只能讀取一次,就是說(shuō)request.getInputStream()只能使用一次,原因如下:
因?yàn)榱鲗?duì)應(yīng)的是數(shù)據(jù),數(shù)據(jù)放在內(nèi)存中,有的是部分放在內(nèi)存中。read 一次標(biāo)記一次當(dāng)前位置(mark position),第二次read就從標(biāo)記位置繼續(xù)讀(從內(nèi)存中copy)數(shù)據(jù)。 所以這就是為什么讀了一次第二次是空了。 怎么讓它不為空呢?只要inputstream 中的pos 變成0就可以重寫(xiě)讀取當(dāng)前內(nèi)存中的數(shù)據(jù)。javaAPI中有一個(gè)方法public void reset() 這個(gè)方法就是可以重置pos為起始位置,但是不是所有的IO讀取流都可以調(diào)用該方法!ServletInputStream是不能調(diào)用reset方法,這就導(dǎo)致了只能調(diào)用一次getInputStream()。
而我們需要先讀取出requestBody進(jìn)行解密,然后拿到解密之前的參數(shù)映射到真正的接口方法參數(shù)對(duì)象里,所以必須解決這個(gè)問(wèn)題。
解決方法就是原始的HttpServletRequest的InputStream只能讀取一下,那么我們就重新自定義封裝一個(gè)HttpServletRequest可以實(shí)現(xiàn)多次讀取。
public class RequestBodyWrapper extends HttpServletRequestWrapper { //用于將流保存下來(lái) private String body; public RequestBodyWrapper(HttpServletRequest request) throws IOException { super(request); body = new String(StreamUtils.copyToByteArray(request.getInputStream()), StandardCharsets.UTF_8); } /** * 重寫(xiě)getInputStream, 從body中獲取請(qǐng)求參數(shù) * @return * @throws IOException */ @Override public ServletInputStream getInputStream() throws IOException { final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body.getBytes("UTF-8")); ServletInputStream servletInputStream = new ServletInputStream() { @Override public boolean isFinished() { return false; } @Override public boolean isReady() { return false; } @Override public void setReadListener(ReadListener readListener) { } @Override public int read() throws IOException { return byteArrayInputStream.read(); } }; return servletInputStream; } @Override public BufferedReader getReader() throws IOException { return new BufferedReader(new InputStreamReader(getInputStream())); } public String getBody() { return this.body; } public void setBody(String body) { this.body = body; }}
然后通過(guò)一個(gè)過(guò)濾器filter把自定義封裝的RqequestBodyWapper傳遞下去:
@Slf4jpublic class BodyTransferFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { RequestBodyWrapper requestBodyWrapper = null; try { HttpServletRequest req = (HttpServletRequest)request; requestBodyWrapper = new RequestBodyWrapper(req); }catch (Exception e){ log.warn("requestBodyWrapper Error:", e); } chain.doFilter((Objects.isNull(requestBodyWrapper) ? request : requestBodyWrapper), response); }}
接下來(lái)就可以來(lái)看看切面了:這里是解析請(qǐng)求參數(shù)和驗(yàn)簽和邏輯所在:
@Aspect@Slf4j@Order(value = OrderConstant.AOP_API_DECRYPT)public class ApiSecurityAspect { @Resource private ApiSecurityProperties apiSecurityProperties; @Resource private StringRedisTemplate stringRedisTemplate; private static final String NONCE_KEY = "x-nonce-"; @Pointcut("execution(* com.plasticene..controller..*(..)) && " + "(@annotation(com.plasticene.boot.web.core.anno.ApiSecurity) ||" + " @target(com.plasticene.boot.web.core.anno.ApiSecurity))") public void securityPointcut(){} @Around("securityPointcut()") public Object aroundApiSecurity(ProceedingJoinPoint joinPoint) throws Throwable { //=======AOP解密切面通知======= ApiSecurity apiSecurity = getApiSecurity(joinPoint); boolean isSign = apiSecurity.isSign(); boolean decryptRequest = apiSecurity.decryptRequest(); // 獲取request加密傳遞的參數(shù) HttpServletRequest request = getRequest(); // 只能針對(duì)post接口的請(qǐng)求參數(shù)requestBody進(jìn)行統(tǒng)一加解密和加簽,這是規(guī)定 if (!Objects.equals("POST", request.getMethod())) { throw new BizException("只能POST接口才能加密加簽操作"); } // 獲取controller接口方法定義的參數(shù) Object[] args = joinPoint.getArgs(); Object[] newArgs = args; ApiSecurityParam apiSecurityParam = new ApiSecurityParam(); // 請(qǐng)求參數(shù)解密 if (decryptRequest) { // 不支持多個(gè)請(qǐng)求,因?yàn)榻饷苷?qǐng)求參數(shù)之后會(huì)json字符串,再根據(jù)請(qǐng)求參數(shù)的類(lèi)型映射過(guò)去,如果有多個(gè)參數(shù)就不知道映射關(guān)系了 if (args.length > 1) { throw new BizException("加密接口方法只支持一個(gè)參數(shù),請(qǐng)修改"); } // args.length=0沒(méi)有請(qǐng)求參數(shù),就說(shuō)明沒(méi)必要解密,因?yàn)榻涌趬焊唤邮諈?shù),即使使用者無(wú)腦開(kāi)啟的該接口的參數(shù)加密,這里不做任何邏輯即可 if (args.length == 1) { RequestBodyWrapper requestBodyWrapper; if (request instanceof RequestBodyWrapper) { requestBodyWrapper = (RequestBodyWrapper) request; } else { requestBodyWrapper = new RequestBodyWrapper(request); } String body = requestBodyWrapper.getBody(); apiSecurityParam = JSONObject.parseObject(body, ApiSecurityParam.class); // 通過(guò)RSA私鑰解密獲取到aes秘鑰 String aesKey = RSAUtil.decryptByPrivateKey(apiSecurityParam.getKey(), apiSecurityProperties.getRsaPrivateKey()); // 通過(guò)aes秘鑰解密data參數(shù)數(shù)據(jù) String data = AESUtil.decrypt(apiSecurityParam.getData(), aesKey); //獲取接口入?yún)⒌念?lèi) Class<?> c = args[0].getClass(); //將獲取解密后的真實(shí)參數(shù),封裝到接口入?yún)⒌念?lèi)中 Object o = JSONObject.parseObject(data, c); newArgs = new Object[]{o}; } } // 驗(yàn)簽 if (isSign) { verifySign(request, newArgs.length == 0 ? null : newArgs[0], apiSecurityParam); } return joinPoint.proceed(newArgs); } void verifySign(HttpServletRequest request, Object o, ApiSecurityParam apiSecurityParam) { // 如果請(qǐng)求參數(shù)是加密傳輸?shù)模蔷拖葟腁piSecurityParam獲取簽名和時(shí)間戳等等。 // 如果請(qǐng)求參數(shù)不是加密傳輸?shù)模敲碅piSecurityParam的字段取值都為null,這時(shí)候在請(qǐng)求的header里面獲取參數(shù)信息 String sign = apiSecurityParam.getSign(); if (StringUtils.isBlank(sign)) { sign = request.getHeader("X-Sign"); } if (StringUtils.isBlank(sign)) { throw new BizException("簽名不能為空"); } String nonce = apiSecurityParam.getNonce(); if (StringUtils.isBlank(nonce)) { nonce = request.getHeader("X-Nonce"); } if (StringUtils.isBlank(nonce)) { throw new BizException("唯一標(biāo)識(shí)不能為空"); } String timestamp = apiSecurityParam.getTimestamp(); Long t; if (StringUtils.isBlank(timestamp)) { timestamp = request.getHeader("X-Timestamp"); } if (StringUtils.isBlank(timestamp)) { throw new BizException("時(shí)間戳不能為空"); } else { try { t = Long.valueOf(timestamp); } catch (Exception e) { throw new BizException("非法的時(shí)間戳"); } } // 判斷timestamp時(shí)間戳與當(dāng)前時(shí)間是否超過(guò)簽名有效時(shí)長(zhǎng)(過(guò)期時(shí)間根據(jù)業(yè)務(wù)情況進(jìn)行配置),如果超過(guò)了就提示簽名過(guò)期 long now = System.currentTimeMillis() / 1000; if (now - t > apiSecurityProperties.getValidTime()) { throw new BizException("簽名已過(guò)期"); } // 判斷nonce boolean nonceExists = stringRedisTemplate.hasKey(NONCE_KEY + nonce); if (nonceExists) { //請(qǐng)求重復(fù) throw new BizException("唯一標(biāo)識(shí)nonce已存在"); } // 驗(yàn)簽 SortedMap sortedMap = SignUtil.beanToMap(o); String content = SignUtil.getContent(sortedMap, nonce, timestamp); boolean flag = RSAUtil.verifySignByPublicKey(content, sign, apiSecurityProperties.getRsaPublicKey()); if (!flag) { throw new BizException("簽名驗(yàn)證不通過(guò)"); } stringRedisTemplate.opsForValue().set(NONCE_KEY+ nonce, "1", apiSecurityProperties.getValidTime(), TimeUnit.SECONDS); } private HttpServletRequest getRequest() { ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = requestAttributes.getRequest(); return request; } private ApiSecurity getApiSecurity(JoinPoint joinPoint) { MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); Method method = methodSignature.getMethod(); ApiSecurity apiSecurity = method.getAnnotation(ApiSecurity.class); if (Objects.isNull(apiSecurity)) { apiSecurity = method.getDeclaringClass().getAnnotation(ApiSecurity.class); } return apiSecurity; }}
這代碼沒(méi)什么好講的了,就按照上面的加密、加簽流程圖邏輯實(shí)現(xiàn)的,而且注釋也很清楚,可以自己慢慢消化,這里面涉及的工具類(lèi)如RSAUtil、AESUtil、SignUtil等,礙于文章代碼篇幅,我就這里就在一一展示,我會(huì)在文章后面放上全部代碼的項(xiàng)目github地址以供下載的。
上面的切面只完成了接口參數(shù)的解密和驗(yàn)簽,至于對(duì)響應(yīng)參數(shù)的加密返回我們放到了ResponseBodyAdvice中實(shí)現(xiàn)。
@RestControllerAdvice@Slf4jpublic class ResponseResultBodyAdvice implements ResponseBodyAdvice<Object> { @Resource private ObjectMapper objectMapper; @Resource private ApiSecurityProperties apiSecurityProperties; /** * 判斷類(lèi)或者方法是否使用了 @ResponseResultBody */ @Override public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) { return AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), ResponseResultBody.class) || returnType.hasMethodAnnotation(ResponseResultBody.class) || AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), ApiSecurity.class) || returnType.hasMethodAnnotation(ApiSecurity.class); } /** * 當(dāng)類(lèi)或者方法使用了 @ResponseResultBody 就會(huì)調(diào)用這個(gè)方法 * 如果返回類(lèi)型是string,那么springmvc是直接返回的,此時(shí)需要手動(dòng)轉(zhuǎn)化為json * 因?yàn)楫?dāng)body都為null時(shí),下面的非加密下的if判斷參數(shù)類(lèi)型的條件都不滿足,如果接口返回類(lèi)似為String, * 會(huì)報(bào)錯(cuò)com.shepherd.fast.global.ResponseVO cannot be cast to java.lang.String */ @SneakyThrows @Override public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) { Method method = returnType.getMethod(); Class<?> returnClass = method.getReturnType(); Boolean enable = apiSecurityProperties.getEnable(); ApiSecurity apiSecurity = method.getAnnotation(ApiSecurity.class); if (Objects.isNull(apiSecurity)) { apiSecurity = method.getDeclaringClass().getAnnotation(ApiSecurity.class); } if (enable && Objects.nonNull(apiSecurity) && apiSecurity.encryptResponse() && Objects.nonNull(body)) { // 只需要加密返回data數(shù)據(jù)內(nèi)容 if (body instanceof ResponseVO) { body = ((ResponseVO) body).getData(); } JSONObject jsonObject = encryptResponse(body); body = jsonObject; } else { if (body instanceof String || Objects.equals(returnClass, String.class)) { String value = objectMapper.writeValueAsString(ResponseVO.success(body)); return value; } // 防止重復(fù)包裹的問(wèn)題出現(xiàn) if (body instanceof ResponseVO) { return body; } } return ResponseVO.success(body); } JSONObject encryptResponse(Object result) { String aseKey = AESUtil.generateAESKey(); String content = JSONObject.toJSONString(result); String data = AESUtil.encrypt(content, aseKey); String key = RSAUtil.encryptByPublicKey(aseKey, apiSecurityProperties.getRsaPublicKey()); JSONObject jsonObject = new JSONObject(); jsonObject.put("key", key); jsonObject.put("data", data); return jsonObject; }}
這里就是對(duì)接口返回參數(shù)格式進(jìn)行統(tǒng)一ResponseVO,同時(shí)判斷是否需要進(jìn)行返回參數(shù)加密執(zhí)行相應(yīng)邏輯即可。
至此,對(duì)于Spring Boot如何提高接口安全性的思路與實(shí)現(xiàn)已講完,同時(shí)我們也盡量進(jìn)行了抽取封裝,做到了極致的優(yōu)雅實(shí)現(xiàn)。當(dāng)然這里還是要再次強(qiáng)調(diào)一下以上的思路實(shí)現(xiàn)是不能絕對(duì)保證接口安全性的,只能做到”防君子不妨小人“,可以這么說(shuō)假如不做加密加簽這些保護(hù)措施,黑客破解接口就會(huì)不費(fèi)吹灰之力,自己就經(jīng)歷過(guò)我在個(gè)人的阿里云服務(wù)器部署的MySQL服務(wù),為了訪問(wèn)簡(jiǎn)單省事,我直接在外網(wǎng)暴露了3306端口,同時(shí)用戶(hù)名密碼都是root,就被黑客黑了勒索比特幣。
圖片
幸好里面數(shù)據(jù)不是很重要,我后面把暴露端口映射成其他的了,密碼也改了一個(gè)復(fù)雜的,我看你再給我破解了~~~。說(shuō)這些我就想表達(dá)這里的加密、加簽就是為了加大破解接口的復(fù)雜度,有了加密、加簽保障想破解不費(fèi)九牛二虎之力是不行的。
本文轉(zhuǎn)載自微信公眾號(hào)「Shepherd進(jìn)階筆記」,可以通過(guò)以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系公眾號(hào)。
本文鏈接:http://www.tebozhan.com/showinfo-26-16410-0.htmlSpring Boot如何優(yōu)雅提高接口數(shù)據(jù)安全性
聲明:本網(wǎng)頁(yè)內(nèi)容旨在傳播知識(shí),若有侵權(quán)等問(wèn)題請(qǐng)及時(shí)與本網(wǎng)聯(lián)系,我們將在第一時(shí)間刪除處理。郵件:2376512515@qq.com