數據安全系列4:密碼技術的應用-接口調用的身份識別

?傳送門

數據安全系列1:開篇

數據安全系列2:單向散列函數概念

數據安全系列3:密碼技術概述

什么是認證?

一談到認證,多數人的反應可能就是"用戶認證" 。就是應用系統如何識別用戶的身份,直接一點就是常說的"登錄"功能,這可以說是一個系統中最基本的功能了:

認證(Authentication)、授權(Authorization)和憑證(Credentials)這三項可以說是一個系統中最基礎的安全設計了,哪怕是再簡陋的信息系統,大概也不可能忽略掉“用戶登錄”這個功能。

--------------------引自系統如何正確分辨操作用戶的真實身份

而"登錄"又是所有安全功能中的重中之重:沒有經過用戶認證的過程,所有的安全設計都這空中樓閣,這就意義著登錄其實不是一件簡單的事情:不僅僅是校驗一下用戶名、密碼是否正確這么簡單,而是一系列圍繞認證展開的復雜問題:

  • 賬戶和權限信息作為一種必須最大限度保障安全和隱私
  • 同時又要兼顧各個系統模塊、甚至是系統間共享訪問的基礎主數據

所以登錄場景下的用戶,除了一般意義上的真實的人,也可能不是一個真正的人:只要擁有用戶名、密碼并經過了系統的安全認證,就可以被系統所接受了。比如有些黑客程序,或者所謂的"攻擊機器人",其實并不是真正的用戶在操作。但是這里討論的場景中,用戶指的一般開發口中的各種應用系統,以及為了安全性而設計的應用身份識別!

應用身份認證

應用身份認證的場景,其實在開發中還是很常見的(可能對于非開發人員來說,倒不常見,因為一般用戶操作的是時候以自己為主體的,所以不存在什么應用身份認證)。

API接口對于程序員來說(尤其是后端開發)幾乎是每個人都接觸過的,不論是開發API接口還是調用API接口都并不陌生。API接口一般是由應用系統開發出來供別的系統來調用,只要符合接口的規范或約定,一般都能調用成功。這里成功要說明一下:

  • 不考慮網絡環境,默認是通的
  • 也不保證業務執行成功與否,只考慮是否滿足參數、URL、請求方式等

調用API接口如果只滿足基本要求就能調用,在安全性上其實是不夠的。就好比一個系統如果沒有"登錄"這種基本的認證,任何人都能訪問那不是一個道理嗎?

在一般的內網環境里面,因為有防火墻的存在,其實對于應用之間的API接口調用的認證要求,倒并不是很嚴格。但是以下的一些情況卻是不能忽視:

  • 涉及外網業務,提供了對外的API接口調用
  • 涉及敏感操作,比如轉賬匯款、刪除資源的高危操作
  • 涉及集中管理,比如一些開放網關、公共應用平臺系統
  • 其它一些暫時沒有想到的......

有上面這些場景,系統就不能再"裸奔"了!對于具體怎么設計應用身份認證并沒有統一的標準和既定的規范,放之四海皆準。不過還是有一些借鑒模式:

  • 使用Oauth2協議的密碼模式
  • 使用消息認證碼模式

具體使用Oauth2的密碼模式還是消息論證碼模式并沒有明確的規定,主要看應用場景。如果是上面提到的開放網關、平臺類系統,出于安全性及管理的需要,使用Oauht2的密碼模式比較合適。如果是開發小型系統,也不用對接什么平臺類的系統,要自主開發一套應用身份認證功能,可以采用消認證碼模式,接下來可以具體討論一下如何實現及對比之間的差異!

Oauth2密碼模式

對于Oauth2協議前面討論的足夠多了,其中又專門介紹了Oauth2系列4:密碼模式,所以不再贅述。

這里再簡單畫一個示意圖來說明應用場景:

  • A系統開發API接口,并到平臺系統注冊
  • B系統調用API接口 ,也到平臺系統注冊
  • 平臺系統負責管理注冊的應用(包括對應的接口等資源),并負責在系統間接口調用時進行身份論證

那應用身份認證這個場景跟密碼模式具體有什么關系呢,或者說為什么可以采用密碼模式來做API接口調用的控制?這里覺得有必要做一個探討與解釋。我們知道Oauth協議其實是一個授權協議(可參考Oauth2系列1:初識Oauth2):

看一下網站應用微信登錄開發指南

從上面的時序圖可以看出標準場景Oauth2的流程有真實用戶參與,所以為了應對沒有沒有真實用戶參與的情況,比如應用身份認證(一般都是應用間接口調用,比如服務間通過HTTP接口調用),Oauth2制定了密碼模式來應對:將應用模擬為"用戶",并也向應用頒發"賬號-clientID"、"密碼-clientSecret",應用通過賬號、密碼直接獲取token來完成身份認證!上面流程就變成了下面這樣:

消息認證碼

如果說Oauth2的密碼模式適用于平臺類系統,提供了一種通用、與業務無關的身份認證方式,那么消息認證碼就是另外一種相對更底層與業務參數有關的認證方式。關于消息認證碼的概念,可以參考數據安全系列3:密碼技術概述,那么為什么消息論證碼可以達到身份論證的目的呢?再回顧一下消息論證碼的過程:

  • 在這樣的交互過程中,交互的雙方需要共享密鑰,也即是前面的對稱密鑰
  • 要計算MAC值,必須持有共享密鑰,沒有就無法計算MAC值,消息認證碼正是利用此特性來完成所謂的認證的。

除此以外,還需要說明的是這個過程里面還依賴于單向散列函數的不可逆性!

密鑰管理

從Oauh2協議可以看出,可以單獨做一個注冊服務,負責client_id、client_secret的管理,對網關這種這種平臺系統是必要的。如果是對接系統很少甚至就一個,只要雙方約定好"密鑰"就行:比如服務提供方生成一個16位"隨機數",并頒發給調用方作為"密鑰",這樣會更簡單:

UUID.randomUUID().toString()

至于密鑰的具體生成、傳輸、存儲、管理也是一個很大話題,一般可能會涉及到KMS之類系統,這里就不展開了。

接下來模擬一個接口,看下通過消息認證碼如何實現身份認證!假設有一個用戶注冊接口:

    @PostMapping("register")public void register(@RequestParam("userName") String userName, @RequestParam("email") String email) {}

接受2個參數userName、email:規定只能擁有"密鑰"的系統才能調用。

實現-版本1-基本功能

能最直接想到的辦法是,檢驗參數內容是否符合要求:

  • 調用方:將userName、email拼接起來生成消息認證碼,并傳遞給服務方
  • 服務方:接收userName、email,拼接起來生成消息認證碼,并與調用方傳遞的認證碼比較
  • 如果一致,表示認證成功,不一致則不允許調用

通過這個分析,接口就要多加一個參數接收消息認證碼,比如叫signature或digest:

    @PostMapping("register")public void register(@RequestParam String userName, @RequestParam String email, @RequestParam String signature) {System.out.printf("userName:" + userName + ",email:" + email + ",signature:" + signature);}

這里還有一個問題就是如何生成消息認證碼,這里提供一個Hmacsha256方法(可自行選擇算法):

public static String genHmacSha256Sign(String message, String secret) {// 初始化密鑰,這里使用一個示例密鑰(在實際應用中,密鑰應該保密)byte[] secretKeyBytes = secret.getBytes(StandardCharsets.UTF_8);SecretKeySpec secretKey = new SecretKeySpec(secretKeyBytes, "HmacSHA256");try {// 獲取HMAC-SHA256的Mac實例Mac mac = Mac.getInstance("HmacSHA256");mac.init(secretKey);// 要簽名的數據byte[] dataBytes = message.getBytes(StandardCharsets.UTF_8);mac.update(dataBytes);// 執行MAC計算byte[] resultBytes = mac.doFinal();// 編碼為Base64字符串return Base64.getEncoder().encodeToString(resultBytes);} catch (Exception e) {throw new RuntimeException(e);}}

好,現在假定約定的密鑰是:826270b4-542b-4e48-b48c-856bea6453db

注冊的用戶名、email分別是:張三、zhangsan@qq.com,客戶端計算出來signature:

public static void main(String[] args) {String secret = "826270b4-542b-4e48-b48c-856bea6453db";String userName = "張三", email = "zhangsan@qq.com";String message = userName + email;System.out.println(genHmacSha256Sign(message, secret));}

輸出摘要為:uTo95CYO1AchnvRK9uAJ1W+nc2bJo2p1IsOtLOdWpsk=?

服務端的驗證邏輯調成為:

@PostMapping("register")public String register(@RequestParam String userName, @RequestParam String email, @RequestParam String signature) throws UnsupportedEncodingException {System.out.printf("userName:" + userName + ",email:" + email + ",signature:" + signature);String message = userName + email;String sha256Sign = URLDecoder.decode(SignUtil.genHmacSha256Sign(message, "826270b4-542b-4e48-b48c-856bea6453db"), StandardCharsets.UTF_8.name());if (signature.equals(sha256Sign)) {return "success";}return "error";// 省略注冊業務邏輯}

現在啟動一下服務端,通過postman來調用一下:

調用成功,一個最基本的認證功能實現完成了!?

實現-版本2-與業務解耦

上面的方式雖然實現了功能,不過還是會發現還是有一些問題:

  • signature放在業務接口里面
  • 要針對每個接口的參數單獨約定好message的拼接規則(比如哪些參數參與認證、拼接順序)

總之一句話,身份認證與業務接口沒有強綁定了,所以最好把身份認證設計成一個通用的功能:

  • 提供一個過濾器,在里面進行身份認證的檢驗,并且指定攔截的URL
  • 為了統一message的拼接規則,統一規則接口的所有參數都參與拼接

所以約定:

  • 將signature從業務接口里面提出來,入到header中傳遞
  • 接口的入參統一用RequestBody的json形式接收,不再定義成RequestParam

改定代碼,服務接口:

import com.tw.tsm.auth.dto.RegisterDtoReq;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;import java.io.UnsupportedEncodingException;@RestController
public class RegisterController {@PostMapping("register")public String register(@RequestBody RegisterDtoReq register) throws UnsupportedEncodingException {System.out.printf("userName:" + register.getUserName() + ",email:" + register.getEmail() + ",signature:" + signature);// 不再業務代碼里面進行身份認證了//        String message = userName + email;
//        String sha256Sign = URLDecoder.decode(SignUtil.genHmacSha256Sign(message, "826270b4-542b-4e48-b48c-856bea6453db"), StandardCharsets.UTF_8.name());
//        if (signature.equals(sha256Sign)) {
//            return "success";
//        }
//        return "error";// 省略注冊業務邏輯return null;}}@Data
@NoArgsConstructor
@AllArgsConstructor
public class RegisterDtoReq {private String userName;private String email;
}

過濾器:

import com.tw.tsm.base.util.RequestWrapper;
import com.tw.tsm.base.util.SignUtil;
import org.apache.commons.io.IOUtils;import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.nio.charset.StandardCharsets;public class VerityFilter implements Filter {@Overridepublic void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {ServletRequest requestWrapper = null;if (request instanceof HttpServletRequest) {requestWrapper = new RequestWrapper((HttpServletRequest) request);}// 在chain.doFiler方法中傳遞新的request對象if (requestWrapper == null) {chain.doFilter(request, response);} else {verity((HttpServletRequest) requestWrapper);chain.doFilter(requestWrapper, response);}}private void verity(HttpServletRequest requestWrapper) throws IOException {//獲取請求中的流如何,將取出來的字符串,再次轉換成流,然后把它放入到新request對象中。String requestBody = IOUtils.toString(requestWrapper.getInputStream(), StandardCharsets.UTF_8.name()).replaceAll("\r\n", "");System.out.printf(requestBody);String sha256Sign = SignUtil.genHmacSha256Sign(requestBody, "826270b4-542b-4e48-b48c-856bea6453db");String signature = requestWrapper.getHeader("signature");if (signature.equals(sha256Sign)) {return;}throw new IllegalArgumentException("參數異常!");}
}

包裝的HttpServletRequest,用于讀取Body:

import org.apache.commons.io.IOUtils;import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.*;public class RequestWrapper extends HttpServletRequestWrapper {private byte[] requestBody;private HttpServletRequest request;public RequestWrapper(HttpServletRequest request) throws IOException {super(request);this.request = request;}@Overridepublic BufferedReader getReader() throws IOException {return new BufferedReader(new InputStreamReader(getInputStream()));}@Overridepublic ServletInputStream getInputStream() throws IOException {if (requestBody == null) {ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();IOUtils.copy(request.getInputStream(), byteArrayOutputStream);this.requestBody = byteArrayOutputStream.toByteArray();}final ByteArrayInputStream bais = new ByteArrayInputStream(requestBody);return new ServletInputStream() {@Overridepublic int read() throws IOException {return bais.read();}@Overridepublic boolean isFinished() {return false;}@Overridepublic boolean isReady() {return false;}@Overridepublic void setReadListener(ReadListener readListener) {}};}}

注冊Filter:

@Beanpublic FilterRegistrationBean httpServletRequestReplacedRegistration() {FilterRegistrationBean registration = new FilterRegistrationBean();registration.setFilter(new VerityFilter());registration.addUrlPatterns("/register");registration.addInitParameter("paramName", "paramValue");registration.setName("VerityFilter");registration.setOrder(1);return registration;}

客戶端生成signature:

 public static void main(String[] args) {String secret = "826270b4-542b-4e48-b48c-856bea6453db";String userName = "張三", email = "zhangsan@qq.com";
//        String message = userName + email;JSONObject jsonObject = new JSONObject();jsonObject.put("userName", userName);jsonObject.put("email", email);String message = jsonObject.toJSONString();System.out.println(genHmacSha256Sign(message, secret));// System.out.println(genHmacSha256Sign(jsonObject.toString(), secret));}

?現在啟動一下服務端,通過postman來調用一下:

header里面也要傳參數:

實現-版本3-防重放

經過迭代過的版本,已經將身份認證與業務接口解耦開了,不過這里還有一個安全問題,就是防重放攻擊,具體的應對方案也比較成熟:

  • 加時間戳-timestamp。該方法優點是不用額外保存其他信息。缺點是認證雙方需要準確的時間同步,同步越好,受攻擊的可能性就越小。但當系統很龐大,跨越的區域較廣時,要做到精確的時間同步并不是很容易。所以一般會采用在指定時間范圍,比如一分鐘以內的請求才接受。并且單獨使用時間戳,很難完全杜絕重放攻擊
  • 加隨機數-nonce。該方法優點是認證雙方不需要時間同步,雙方記住(客戶端生成、傳遞給服務端)使用過的隨機數,如發現報文中有以前使用過的隨機數,就認為是重放攻擊。缺點是需要額外保存使用過的隨機數,若記錄的時間段較長,則保存和查詢的開銷較大。所以一般會采用時間戳+隨機數方式的:一分鐘以內的+此時間段內不重復的隨機數請求才接受(存儲采用redis,利用reids的TTL機制自動清理數據)

在實際中,常將方法(1)和方法(2)組合使用,這樣就只需保存某個很短時間段內的所有隨機數,而且時間戳的同步也不需要太精確。時間戳一般都是客戶端生成,而nonce可以由客戶端生成、也可以由服務端生成:

  • 服務端生成的話,要額外增加一個接口級客戶端單獨獲取nonce
  • 客戶端生成則不需要,可以簡化調用邏輯

生成timestamp、nonce,也放到header中做為公共參數,并參與message的拼接:message = 摘要算法(業務參數的json字符串+timestamp+nonce)。這里就不再實現了,代碼也不難

?

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/75082.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/75082.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/75082.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

STL之map和set

1. 關聯式容器 vector、list、deque、 forward_list(C11)等,這些容器統稱為序列式容器,因為其底層為線性序列的數據結構,里面存儲的是元素本身。 關聯式容器也是用來存儲數據的,與序列式容器不同的是,其里面存儲的是結…

Vue3 其它API Teleport 傳送門

Vue3 其它API Teleport 傳送門 在定義一個模態框時,父組件的filter屬性會影響子組件的position屬性,導致模態框定位錯誤使用Teleport解決這個問題把模態框代碼傳送到body標簽下

C++練習

1.將File練習題&#xff0c;內部的FILE*描述符&#xff0c;改成int描述符 2。寫一個類Fifo管道類。提高難度&#xff0c;什么都不提示。只要求&#xff1a;使用自己編寫的Fifo類對象&#xff0c;實現2個終端之間互相聊天 file.cpp #include <iostream> #include <c…

《Python Web網站部署應知應會》No4:基于Flask的調用AI大模型的高性能博客網站的設計思路和實戰(上)

基于Flask的調用AI大模型的高性能博客網站的設計思路和實戰&#xff08;上&#xff09; 摘要 本文詳細探討了一個基于Flask框架的高性能博客系統的設計與實現&#xff0c;該系統集成了本地AI大模型生成內容的功能。我們重點關注如何在高并發、高負載狀態下保持系統的高性能和…

實現一個簡易版的前端監控 SDK

【簡易版的前端監控系統】 1、Promise的錯誤如何監控&#xff1f;–promise不是所有都是接口請求 2、接口的報錯如何監控&#xff1f;–全局監控sdk&#xff0c;不改動公共的請求方法、不改動業務代碼&#xff1b;一般接口使用axios請求 3、資源的報錯如何監控&#xff1f; 4、…

【操作系統】軟中斷vs硬中斷

在操作系統中&#xff0c;中斷&#xff08;Interrupt&#xff09; 是 CPU 響應外部事件的重要機制&#xff0c;分為 硬中斷&#xff08;Hardware Interrupt&#xff09; 和 軟中斷&#xff08;Software Interrupt&#xff09;。它們的核心區別在于 觸發方式 和 處理機制。 1. 硬…

力扣刷題-熱題100題-第27題(c++、python)

21. 合并兩個有序鏈表 - 力扣&#xff08;LeetCode&#xff09;https://leetcode.cn/problems/merge-two-sorted-lists/description/?envTypestudy-plan-v2&envIdtop-100-liked 常規法 創建一個新鏈表&#xff0c;遍歷list1與list2&#xff0c;將新鏈表指向list1與list2…

Python包下載路徑 Chrome用戶數據 修改到非C盤

查看 site-packages 是否能通過命令行完成&#xff1f; 可以&#xff0c;使用以下命令&#xff08;不需寫腳本&#xff09;&#xff1a; python -m site輸出包含&#xff1a; sys.path site-packages 路徑&#xff08;全局和用戶級&#xff09; 如果只想看安裝路徑&#…

【鴻蒙5.0】鴻蒙登錄界面 web嵌入(隱私頁面加載)

在鴻蒙應用中嵌入 Web 頁面并加載隱私頁面&#xff0c;可借助 WebView 組件來實現。以下是一個完整示例&#xff0c;展示如何在鴻蒙 ArkTS 里嵌入 Web 頁面并加載隱私政策頁面。 在 HarmonyOS 應用開發中&#xff0c;如果你希望嵌入一個網頁&#xff0c;并且特別關注隱私頁面加…

AI加Python的文本數據情感分析流程效果展示與代碼實現

本文所使用數據來自于梯田景區評價數據。 一、數據預處理 數據清洗 去除重復值、空值及無關字符(如表情符號、特殊符號等)。 提取中文文本,過濾非中文字符。 統一文本格式(如全角轉半角、繁體轉簡體)。 中文分詞與去停用詞 使用 jieba 分詞工具進行分詞。 加載自定義詞…

Microi吾碼界面設計引擎之基礎組件用法大全【內置組件篇·上】

&#x1f380;&#x1f380;&#x1f380; microi-pageengine 界面引擎系列 &#x1f380;&#x1f380;&#x1f380; 一、Microi吾碼&#xff1a;一款高效、靈活的低代碼開發開源框架【低代碼框架】 二、Vue3項目快速集成界面引擎 三、Vue3 界面設計插件 microi-pageengine …

【多線程】單例模式和阻塞隊列

目錄 一.單例模式 1. 餓漢模式 2. 懶漢模式 二.阻塞隊列 1. 阻塞隊列的概念 2. BlockingQueue接口 3.生產者-消費者模型 4.模擬生產者-消費者模型 一.單例模式 單例模式&#xff08;Singleton Pattern&#xff09;是一種常用的軟件設計模式&#xff0c;其核心思想是確保…

終值定理的推導與理解

終值定理的推導與理解 終值定理是控制理論和信號處理中的一個重要工具&#xff0c;它通過頻域的拉普拉斯變換來分析時間域函數的最終穩態值。具體來說&#xff0c;終值定理提供了一個簡便的方法&#xff0c;利用 F ( s ) F(s) F(s)&#xff08; f ( t ) f(t) f(t) 的拉普拉斯…

每日c/c++題 備戰藍橋杯(二分答案模版)

在算法學習中&#xff0c;二分答案算法是一種非常高效且常用的技巧。它的核心思想是通過不斷縮小搜索范圍&#xff0c;逐步逼近目標答案。相比傳統的暴力搜索&#xff0c;二分答案算法的時間復雜度通常為 O(logn)&#xff0c;特別適合處理大規模數據的查找問題。 本文將詳細介…

NLP高頻面試題(二十六)——RAG的retriever模塊作用,原理和目前存在的挑戰

在自然語言處理領域&#xff0c;檢索增強生成&#xff08;Retrieval-Augmented Generation&#xff0c;簡稱RAG&#xff09;是一種將信息檢索與文本生成相結合的技術&#xff0c;旨在提升模型的回答準確性和信息豐富度。其中&#xff0c;Retriever在RAG架構中扮演著關鍵角色&am…

第30周Java分布式入門 分布式基礎

分布式基礎課程筆記 一、什么是分布式&#xff1f; 1. 權威定義 分布式系統定義為&#xff1a;“利用物理架構形成多個自治的處理元素&#xff0c;不共享主內存&#xff0c;通過發送消息合作”。 2. 核心解釋 物理架構與處理元素 &#x1f31f; 多臺獨立服務器/電腦&#x…

Vuex狀態管理

Vuex Vuex是一個專為Vue.js應用程序開發的狀態管理模式。它采用集中式管理應用的所有組件狀態&#xff0c;并以相應的規則保證狀態以一種可預測的方式發生變化。&#xff08;類似于在前端的數據庫&#xff0c;這里的數據存儲在內存當中&#xff09; 一、安裝并配置 在項目的…

從代碼學習深度學習 - 使用塊的網絡(VGG)PyTorch版

文章目錄 前言一、VGG網絡簡介1.1 VGG的核心特點1.2 VGG的典型結構1.3 優點與局限性1.4 本文的實現目標二、搭建VGG網絡2.1 數據準備2.2 定義VGG塊2.3 構建VGG網絡2.4 輔助工具2.4.1 計時器和累加器2.4.2 準確率計算2.4.3 可視化工具2.5 訓練模型2.6 運行實驗總結前言 深度學習…

Baklib激活企業知識管理新動能

Baklib核心技術架構解析 Baklib的底層架構以模塊化設計為核心&#xff0c;融合知識中臺的核心理念&#xff0c;通過分布式存儲引擎與智能語義分析系統構建三層技術體系。數據層采用多源異構數據接入協議&#xff0c;支持文檔、音視頻、代碼片段等非結構化數據的實時解析與分類…

小智機器人中的部分關鍵函數,FreeRTOS中`xEventGroupWaitBits`函數的詳細解析

以下是對FreeRTOS中xEventGroupWaitBits函數的詳細解析&#xff1a; 函數功能 xEventGroupWaitBits用于在事件組中等待指定的位被設置。它可以配置為等待任意一個位或所有位&#xff0c;并支持超時機制。 注意&#xff1a;該函數不能在中斷中調用。 函數原型 EventBits_t xEv…