總結:我們寫過很多接口,有沒有想想接口的安全性呢?jwt,openid 側重 於 認證(就是用戶是誰),OAuth2 側重於授權(就是說這個東西是否有權限訪問),接口簽名呢 側重於安全
- 請求來源(身份)是否合法?
- 請求參數被篡改?
- 請求的唯一性(不可復制)
簽名介紹:
AccessKey&SecretKey (開放平台)
請求身份
為開發者分配AccessKey(開發者標識,確保唯一)和SecretKey(用於接口加密,確保不易被窮舉,生成算法不易被猜測)。
防止篡改
參數簽名
- 按照請求參數名的字母升序排列非空請求參數(包含AccessKey),使用URL鍵值對的格式(即key1=value1&key2=value2…)拼接成字符串stringA;
- 在stringA最后拼接上Secretkey得到字符串stringSignTemp;
- 對stringSignTemp進行MD5運算,並將得到的字符串所有字符轉換為大寫,得到sign值。
請求攜帶參數AccessKey和Sign,只有擁有合法的身份AccessKey和正確的簽名Sign才能放行。這樣就解決了身份驗證和參數篡改問題,即使請求參數被劫持,由於獲取不到SecretKey(僅作本地加密使用,不參與網絡傳輸),無法偽造合法的請求。
重放攻擊
雖然解決了請求參數被篡改的隱患,但是還存在着重復使用請求參數偽造二次請求的隱患。
timestamp+nonce方案
nonce指唯一的隨機字符串,用來標識每個被簽名的請求。通過為每個請求提供一個唯一的標識符,服務器能夠防止請求被多次使用(記錄所有用過的nonce以阻止它們被二次使用)。
然而,對服務器來說永久存儲所有接收到的nonce的代價是非常大的。可以使用timestamp來優化nonce的存儲。
假設允許客戶端和服務端最多能存在15分鍾的時間差,同時追蹤記錄在服務端的nonce集合。當有新的請求進入時,首先檢查攜帶的timestamp是否在15分鍾內,如超出時間范圍,則拒絕,然后查詢攜帶的nonce,如存在已有集合,則拒絕。否則,記錄該nonce,並刪除集合內時間戳大於15分鍾的nonce(可以使用redis的expire,新增nonce的同時設置它的超時失效時間為15分鍾)。
實現
請求接口:http://api.test.com/test?name=hello&home=world&work=java
-
客戶端
- 生成當前時間戳timestamp=now和唯一隨機字符串nonce=random
- 按照請求參數名的字母升序排列非空請求參數(包含AccessKey)
stringA="AccessKey=access&home=world&name=hello&work=java×tamp=now&nonce=random";
- 拼接密鑰SecretKey
stringSignTemp="AccessKey=access&home=world&name=hello&work=java×tamp=now&nonce=random&SecretKey=secret";
- MD5並轉換為大寫
sign=MD5(stringSignTemp).toUpperCase();
- 最終請求
http://api.test.com/test?name=hello&home=world&work=java×tamp=now&nonce=nonce&sign=sign;
-
服務端
Token&AppKey(APP)
在APP開放API接口的設計中,由於大多數接口涉及到用戶的個人信息以及產品的敏感數據,所以要對這些接口進行身份驗證,為了安全起見讓用戶暴露的明文密碼次數越少越好,然而客戶端與服務器的交互在請求之間是無狀態的,也就是說,當涉及到用戶狀態時,每次請求都要帶上身份驗證信息。
Token身份驗證
- 用戶登錄向服務器提供認證信息(如賬號和密碼),服務器驗證成功后返回Token給客戶端;
- 客戶端將Token保存在本地,后續發起請求時,攜帶此Token;
- 服務器檢查Token的有效性,有效則放行,無效(Token錯誤或過期)則拒絕。
安全隱患:Token被劫持,偽造請求和篡改參數。
Token+AppKey簽名驗證
與上面開發平台的驗證方式類似,為客戶端分配AppKey(密鑰,用於接口加密,不參與傳輸),將AppKey和所有請求參數組合成源串,根據簽名算法生成簽名值,發送請求時將簽名值一起發送給服務器驗證。這樣,即使Token被劫持,對方不知道AppKey和簽名算法,就無法偽造請求和篡改參數。再結合上述的重發攻擊解決方案,即使請求參數被劫持也無法偽造二次重復請求。
實現
登陸和登出請求

后續請求
-
客戶端
和上述開放平台的客戶端行為類似,把AccessKey改為token即可。 -
服務端
// 添加攔截器 | |
@Override | |
public void addInterceptors(InterceptorRegistry registry) { | |
// 接口簽名認證攔截器,該簽名認證比較簡單,實際項目中可以使用Json Web Token或其他更好的方式替代。 | |
if (!"dev".equals(env)) { // 開發環境忽略簽名認證 | |
registry.addInterceptor(new HandlerInterceptorAdapter() { | |
@Override | |
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { | |
// 驗證簽名 | |
boolean pass = validateSign(request); | |
if (pass) { | |
return true; | |
} else { | |
LOGGER.warn("簽名認證失敗,請求接口:{},請求IP:{},請求參數:{}", request.getRequestURI(), getIpAddress(request), JSON.toJSONString(request.getParameterMap())); | |
RetResult<Object> result = new RetResult<Object>(); | |
result.setCode(RetCode.UNAUTHORIZED).setMsg("簽名認證失敗"); | |
responseResult(response, result); | |
return false; | |
} | |
} | |
}); | |
} | |
} | |
/** | |
* @Title: responseResult | |
* @Description: 響應結果 | |
* @param response | |
* @param result | |
* @Reutrn void | |
*/ | |
private void responseResult(HttpServletResponse response, RetResult<Object> result) { | |
response.setCharacterEncoding("UTF-8"); | |
response.setHeader("Content-type", "application/json;charset=UTF-8"); | |
response.setStatus(200); | |
try { | |
response.getWriter().write(JSON.toJSONString(result)); | |
} catch (IOException ex) { | |
LOGGER.error(ex.getMessage()); | |
} | |
} | |
/** | |
* @Title: validateSign | |
* @Description: 一個簡單的簽名認證,規則: 1. 將請求參數按ascii碼排序 2. | |
* 拼接為a=value&b=value...這樣的字符串(不包含sign)3. | |
* 混合密鑰(secret)進行md5獲得簽名,與請求的簽名進行比較 | |
* @param request | |
* @Reutrn boolean | |
*/ | |
private boolean validateSign(HttpServletRequest request) { | |
String requestSign = request.getParameter("sign");// 獲得請求簽名,如sign=19e907700db7ad91318424a97c54ed57 | |
if (StringUtils.isEmpty(requestSign)) { | |
return false; | |
} | |
List<String> keys = new ArrayList<String>(request.getParameterMap().keySet()); | |
keys.remove("sign");// 排除sign參數 | |
Collections.sort(keys);// 排序 | |
StringBuilder sb = new StringBuilder(); | |
for (String key : keys) { | |
sb.append(key).append("=").append(request.getParameter(key)).append("&");// 拼接字符串 | |
} | |
String linkString = sb.toString(); | |
linkString = StringUtils.substring(linkString, 0, linkString.length() - 1);// 去除最后一個'&' | |
String secret = "Potato";// TODO 密鑰,自己修改 | |
String sign = DigestUtils.md5Hex(linkString + secret);// 混合密鑰md5 | |
return StringUtils.equals(sign, requestSign);// 比較 | |
} | |
OAuth簡史: 2007年12月4日發布了OAuth Core 1.0, 此版本的協議存在嚴重的安全漏洞:OAuth Security Advisory: 2009.1,更詳細的安全漏洞介紹可以參考:Explaining the OAuth Session Fixation Attack。2009年6月24日發布了OAuth Core 1.0 Revision A:此版本的協議修復了前一版本的安全漏洞,並成為RFC5849,我們現在使用的OAuth版本多半都是以此版本為基礎。 OAuth 2.0是OAuth協議的下一版本,但不向后兼容OAuth 1.0。 OAuth 2.0關注客戶端開發者的簡易性,同時為Web應用,桌面應用和手機,和起居室設備提供專門的認證流程。
OAuth角色:
- Consumer:消費方
- Service Provider:服務提供者
- User:用戶
- 用戶訪問客戶端的網站,想操作用戶存放在服務提供方的資源。
- 客戶端向服務提供方請求一個臨時令牌。
- 服務提供方驗證客戶端的身份后,授予一個臨時令牌。
- 客戶端獲得臨時令牌后,將用戶引導至服務提供方的授權頁面請求用戶授權。在這個過程中將臨時令牌和客戶端的回調連接發送給服務提供方。
- 用戶在服務提供方的網頁上輸入用戶名和密碼,然后授權該客戶端訪問所請求的資源。
- 授權成功后,服務提供方引導用戶返回客戶端的網頁。
- 客戶端根據臨時令牌從服務提供方那里獲取訪問令牌。
- 服務提供方根據臨時令牌和用戶的授權情況授予客戶端訪問令牌。
- 客戶端使用獲取的訪問令牌訪問存放在服務提供方上的受保護的資源。

OAuth和OpenID的區別: OAuth關注的是authorization授權,即:“用戶能做什么”; 而OpenID側重的是authentication認證,即:“用戶是誰”。 OpenID、OAuth聯合使用例子:
- OpenID 用戶希望訪問其在example.com的賬戶
- example.com(在OpenID的黑話里面被稱為“Relying Party”) 提示用戶輸入他/她/它的OpenID
- 用戶給出了他的OpenID,比如說"http://user.myopenid.com"
- example.com 跳轉到了用戶的OpenID提供商“mypopenid.com”
- 用戶在"myopenid.com"(OpenID provider)提示的界面上輸入用戶名密碼登錄
- “myopenid.com" (OpenID provider) 問用戶是否要登錄到example.com
- 用戶同意后,"myopenid.com" (OpenID provider) 跳轉回example.com
- example.com 允許用戶訪問其帳號
- 用戶在使用example.com時希望從mycontacts.com導入他的聯系人
- example.com (在OAuth的黑話里面叫“Consumer”)把用戶送往mycontacts.com (黑話是“Service Provider”)
- 用戶在mycontacts.com 登錄(可能也可能不用了他的OpenID)
- mycontacts.com問用戶是不是希望授權example.com訪問他在mycontact.com的聯系人
- 用戶確定
- mycontacts.com 把用戶送回example.com
- example.com 從mycontacts.com拿到聯系人
- example.com 告訴用戶導入成功
Google Connect(基於OpenID + OAuth思想的定制):

OAuth 2.0的新特性 - 6種全新流程:
- User-Agent Flow – 客戶端運行於用戶代理內(典型如web瀏覽器)。
- Web Server Flow – 客戶端是web服務器程序的一部分,通過http request接入,這是OAuth 1.0提供的流程的簡化版本。
- Device Flow – 適用於客戶端在受限設備上執行操作,但是終端用戶單獨接入另一台電腦或者設備的瀏覽器
- Username and Password Flow – 這個流程的應用場景是,用戶信任客戶端處理身份憑據,但是仍然不希望客戶端儲存他們的用戶名和密碼,這個流程僅在用戶高度信任客戶端時才適用。
- Client Credentials Flow – 客戶端使用它的身份憑據去獲取access token,這個流程支持2-legged OAuth的場景。
- Assertion Flow – 客戶端用assertion去換取access token,比如SAML assertion。
https://www.jianshu.com/p/c8483eee7c48