本文為您介紹自建標準的三方SSO對接協議及配置說明。
僅獨立部署版本支持對接自建三方SSO協議,可自行完成對接。若需要緊急支持或專家指導,請聯系Quick BI運營負責人。
背景信息
自建標準的三方SSO對接,主要針對于Quick BI與客戶的自有登錄系統的賬戶對接認證。
很多客戶,賬戶登錄系統并沒有提供標準的行業登錄規范(例如:OAuth2/SAML/LDAP等協議),還是采用自有的協議。為了降低業務系統與客戶的SSO server 的對接成本,因此設計了自有的簡單登錄規范協議,如下:
降低業務側與客戶SSO server的對接成本(一個協議,支持眾多客戶)。
降低企業的對接成本(不用開發支持標準的行業登錄認證規范協議、提供jar支持)。
應用場景
三方系統根據Quick BI的協議說明提供相關接口或配置,實現以下場景:
支持同根域、跨域的登錄場景對接。
同域支持同步登錄登出。
跨域支持同步登出。
快速開始
1. 接入前準備
1.1 前置依賴
租戶側按照協議對接說明完成自有賬號登錄系統的開發。
1.2 網絡要求
對接前,請確保您的鑒權服務器地址和Quick BI服務地址的網絡連通性!
2. 在Quick BI進行對接配置
2.1 開啟三方SSO登錄
步驟一:使用登錄認證超管賬號進入超級管理員后臺。
若賬號無權限登錄時,會出現以下界面,提示“無權限禁止訪問”。請聯系Quick BI運維人員添加權限。
步驟二: 選擇登錄系統管理->登錄全局開關,開啟/關閉指定的三方登錄方式,保存后立即生效。
2.2 標準SSO登錄對接配置
步驟一:打開登錄認證配置頁面
在運維中心->登錄策略配置或開放平臺->登錄認證位置打開。兩個位置打開的配置效果是一樣的,下邊以在運維中心打開對應頁面為例,頁面如下:
步驟二:添加登錄策略
若您已配置過登錄策略,則跳過步驟二。
每個登錄策略都允許配置一套域名/IP攔截策略,用來指定訪問指定域名或IP時Quick BI支持的登錄方式。
登錄策略配置請參見自定義企業登錄門戶。
步驟三:選中需要開啟標準SSO登錄的策略,并點擊編輯。
步驟四:在「編輯策略」頁面,選擇基礎設置。
步驟五:配置標準SSO三方登錄
在您進行標準SSO三方登錄配置前,若您未開啟Quick BI賬號,建議開啟,原因為:1)防止三方登錄設置錯誤后,無法登錄Quick BI;2)自定義賬號配置成功且能正常登錄后,可根據需要關閉Quick BI賬號。
Quick BI賬號配置頁面如下。
相關配置項請參見內置賬號配置說明。
開啟標準SSO登錄
在彈出窗口填寫標準SSO的配置信息,
其中,標準SSO協議相關的配置項及說明如下:
具體的接口參數請參見接口參數規范。
配置項 | 是否必填 | 說明 | 參考值 |
系統名稱 | 是 | 對接的系統名稱,用于在登錄項的按鈕組標題顯示。 | 三方系統SSO |
系統圖標 | 否 | 對接的系統圖標,用于在登錄項中顯示圖標,最大32KB。 | |
登錄態名稱 | 是 | 跨域場景下,為登錄態參數的名稱,其他場景下為 Cookie 名稱。 | user_ticket |
是否跨域 | 是 | 可設置為跨域或不跨域。 | 跨域或不跨域 |
登錄地址 | 是 | 三方系統登錄頁全稱,必需攜帶回跳參數,并以 = 結尾。 | http://dev.sso.aliyun.test:XXXX/login.htm?redirectUrl= |
登出地址 | 是 | 分兩種場景: 1:不跨域,為三方登出頁的地址,必需攜帶回跳參數,并以 = 結尾 2:跨域,登出接口的地址,參考跨域登出接口規范 | http://dev.sso.aliyun.test:XXXX/logout.do?redirectUrl= |
登錄態校驗接口 (請參見接口參數規范) | 是 | 三方提供的開放接口, 用于校驗登錄態的有效性。GET 方法。 | http://dev.sso.aliyun.test:XXXX/valid |
用戶信息接口 (請參見接口參數規范) | 是 | 三方提供的開放接口, 用于獲取登錄態用戶信息。GET 方法。 | http://dev.sso.aliyun.test:XXXX/queryUser |
簽名密鑰-公鑰ak (請參見簽名校驗) | 否 | 接口的簽名公鑰;如不填寫,則接口不攜帶加簽參數。 | 123xxxxxx |
簽名密鑰-私鑰sk (請參見簽名校驗) | 否 | 接口的簽名密鑰;如不填寫,則接口不攜帶加簽參數。 | abcxxxxhijklmn |
Session失效時間 | 是 | 指定登錄態Session的失效時間,失效后,重新發起登錄校驗。單位秒,默認86400。 | 86400 |
步驟六: 保存發布
保存發布后立即生效,請慎重操作。
推薦新建無痕模式窗口或者打開其他瀏覽器測試登錄配置是否成功,以防退出登錄后由于登錄配置錯誤導致無法登錄。
3.登錄驗證
在您完成標準SSO登錄對接配置后,請訪問Quick BI服務地址,并點擊標準SSO登錄,進行登錄驗證。
為了防止其他因素干擾,推薦新建無痕窗口用來測試登錄。
需要注意的是所有的無痕窗口共用cookies,通過關閉窗口清空登錄態時需要保證所有無痕窗口被關閉。
登錄對接成功效果:當您登錄后,出現以下頁面,則說明已對接成功。 拋錯原因是您當前登錄的三方賬號還未同步到Quick BI組織中,鑒權不通過,此時,您需要進行三方賬號同步,請參見獨立部署:三方賬號同步方案。
4.登錄常見問題
4.1 AE0580800018 權限不足禁止訪問,請聯系組織管理員添加到具體組織
無組織用戶默認無法訪問Quick BI,需要先將三方賬號添加到Quick BI,請參見獨立部署:三方賬號同步方案。
4.2. Quick BI沒有調用登錄態校驗接口
跨域場景下,回跳到Quick BI的鏈接中沒有ticket或者ticket參數名/格式錯誤。
同域場景下
cookies的域名和Qucik BI的域名沒有滿足同源。
cookies的名稱和策略中配置的[登錄態名稱]不一致。
cookies的屬性設置問題:
HttpOnly開啟后http無法獲取https的cookie。
SameSite屬性設置不為None。
4.3. Quick BI請求三方接口失敗,例如獲取登錄態或者用戶信息失敗
請聯系運維同學查看日志報錯。
協議對接說明
1.前提條件
三方在登錄時返回的自身的userId,accountName和nick必須保持唯一性
Quick BI調用三方接口的默認配置,三方可以按以下方式:
Content-Type:application/json
傳參方式:url+body同時傳參
2.流程圖
根據以上說明,提供同域和跨域方案,以供不同場景選擇。
2.1同根域方案
業務側系統與單點登錄系統(SSO server) 保持同樣的根域名,SSO server將登錄態Cookie的域名設置為根域名下,通過瀏覽器帶入到業務側系統,以實現登錄態的共享。
以公共云Quick BI為例,Quick BI域名為bi.aliyun.com,阿里云登錄域名為account.aliyun.com;阿里云的登錄態Cookie:login_aliyun_ticket的域名為.aliyun.com,子域名都可以獲取這個Cookie。流程如下:
1.登錄態Cookie的注入和注銷清除,統一由三方SSO負責,業務側不負責,只負責拿到登錄態Cookie進行有效性校驗和身份校驗。
2.登錄態Cookie名稱自定義,由SSO server自定義,請參見配置標準SSO三方登錄。
3.因為是同根域,故支持同步登入和登出。
登錄流程
未登錄用戶訪問Quick BI頁面,會重定向到三方登錄頁面,登錄并寫入Cookie后跳轉回Quick BI自動登錄。
三方訪問Quick BI頁面:http(s)//:xxx.qbi.com/home
未在cookies中拿到ticket,跳轉三方登錄鏈接
登出流程
業務側會調用SSO server的登出地址,SSO server負責清理掉登錄態Cookie實現登出。
2.2 跨域方案
存在部分場景,業務側系統與SSO server根域名不一致,例如,使用IP的方式訪問服務(常見于部分國企或者政府機構)。因此,無法通過Cookie的方式做到登錄態的共享。跨域方案有如下流程:
登錄態業務側系統自行維護。身份校驗成功之后,登錄層維護了自有的登錄態Cookie:x_login_ck。x_login_ck的過期時間,以及對應的Session過期時間,支持配置。
跨域特性,不支持同步登入登出。但預留了接口,可以在登出的時候調用。支持雙向的登出接口(業務側調SSO server/SSO server 掉業務側登出)。提供登出的安全校驗機制,需要SSO server按照規范實現。
登錄流程
未登錄用戶訪問Quick BI頁面,會重定向到三方登錄頁面,登錄后攜帶登錄態參數重定向回Quick BI自動登錄。
登出流程
因為跨域,登出的方式只能通過接口調用的方式支持,分為以下兩種情況
三方系統登出后,調用Qucik BI接口實現同步登出
Qucik BI登出后調用三方系統接口進行同步登出
接口的安全校驗機制,請參見下圖:
3.接口參數規范
3.1登錄態校驗接口(必須)
接口描述:通過ticket,校驗登錄態的有效性,如果有效,返回對應的userId信息。
接口API:接口路徑自定義 (GET)
接口參數:
參數名 | 類型 | 是否必選 | 說明 |
ticket | string | 是 | 登錄態ticket的參數。 |
accessKey | string | / | 簽名的ak,用于安全校驗。 若配置了accessKey, 請求API時自動帶上,客戶系統決定是否消費。 |
timestamp | string | / | 請求的時間戳,用于安全校驗。 若配置了accessKey, 請求API時自動帶上,客戶系統決定是否消費。 |
nonce | string | / | 隨機字符串,用于重放攻擊的安全校驗。 若配置了accessKey, 請求API時自動帶上,客戶系統決定是否消費 |
signature | string | / | 簽名字符串,用于安全校驗。 若配置了accessKey, 請求API時自動帶上,客戶系統決定是否消費。簽名算法參考以下說明 |
請求示例:
GET
http://bi.douson.com/ticket/valid?ticket=c5f5628-21db-446b-8226-e76291e99380×tamp=1610703757345&nonce=e76291e99380&signature=LAtufg1ssx-1Addkddl
返回參數:
參數名 | 類型 | 是否必選 | 說明 |
code | string | 否 | 接口狀態碼,不實際消費,僅用于日志提示,例如出錯時錯誤碼。 |
message | string | 否 | 接口狀態信息,不實際消費,僅用于日志提示,例如出錯時錯誤碼。 |
success | boolean | 是 | 標記接口請求狀態。
|
data | json | 是 | 登錄態校驗信息。 |
|__isLogin | boolean | 是 | 登錄態是否有效。
|
|__userId | string | isLogin=true時必需 | 用戶唯一ID。
|
|__redirectUrl | string | 否 | 重定向地址。
|
# 登錄態校驗成功結果返回案例
{
"code":"200",
"message":"獲取成功",
"success":true, // 請求成功,
"data":{
"isLogin":true, // 登錄態校驗有效
"userId":"1089987878", // 登錄態校驗有效,返回登錄態對應的用戶ID
"redirectUrl":""
}
}
# 登錄態校驗失敗結果返回案例
{
"code":"400",
"message":"校驗失敗",
"success":true, // 請求成功,
"data":{
"isLogin":false, // 登錄態校驗失敗
"userId":"",
"redirectUrl":"http://aliyun.com/xxxxx" // 按照該重定向地址跳轉
}
}
isLogin不要序列化成login。
Quick BI存量三方SSO對接客戶。配置的API存在兩種形式,Quick BI直接把參數拼接在配置的URL后。對于這類歷史存量客戶,參考配置:
standard.sso.is.old.version = true
攜帶URL參數。例如
http://aaa.com/valid?userToken=
。如果切換,接口新增一個ticket參數即可。路徑參數。例如
http://aaa.com/valid/
。 如果切換,需要新增一個接口,并且從參數ticket中解析。
3.2獲取賬戶登錄信息接口(必須)
接口描述:通過userId,獲取具體的用戶信息。
接口API:接口路徑自定義 (GET)
接口參數:
參數名 | 類型 | 是否必選 | 說明 |
userId | string | 是 | 用戶唯一ID。 |
accessKey | string | / | 簽名的ak,用于安全校驗。 若配置了accessKey,請求API時自動帶上,客戶系統決定是否消費。 |
timestamp | string | / | 請求的時間戳,用于安全校驗。 若配置了accessKey,請求API時自動帶上,客戶系統決定是否消費。 |
nonce | string | / | 隨機字符串,用于重放攻擊的安全校驗。 若配置了accessKey,請求API時自動帶上,客戶系統決定是否消費。 |
signature | string | / | 簽名字符串,用于安全校驗。 若配置了accessKey,請求API時自動帶上,客戶系統決定是否消費。 |
示例1:
GET
http://bi.douson.com/query/userinfo?userId=c5f5628-21db-446b-8226-e76291e99380×tamp=1610703757345&nonce=e76291e99380&signature=LAtufg1ssx-1Addkddl
返回參數:
參數名 | 類型 | 是否必選 | 說明 |
code | string | 否 | 接口狀態碼,不實際消費,僅用于日志提示,例如出錯時錯誤碼。 |
message | string | 否 | 接口狀態信息,不實際消費,僅用于日志提示,例如出錯時錯誤碼。 |
success | boolean | 是 | 標記接口請求狀態。
|
data | json | 是 | 用戶賬戶信息字段。 |
|__userId | string | 是 | 用戶唯一ID,必需且唯一。 |
|__userName | string | 是 | 用戶賬戶名,必需且唯一。 |
|__nick | string | 是 | 用戶賬戶的顯示名稱。必需且唯一。 |
|__userEmail | string | 否 | 用戶賬戶郵箱。 |
|__userPhone | string | 否 | 用戶賬戶電話。 |
|__extraInfo | Map<String,String> | 否 | 其他擴展字段,自定義。 透傳到業務系統,登錄層不做消費。 |
{
"code":"200",
"message":"獲取成功",
"success":true,
"data":{
"userId":"1089987878",
"userName":"alibaba",
"nick":"阿里巴巴測試賬號",
"extraInfo":{
"tag":"自定義擴展字段"
}
}
}
Quick BI存量三方SSO對接客戶。配置的API存在兩種形式,Quick BI直接把參數拼接在配置的URL后。
攜帶URL參數。例如
http://aaa.com/getUserInfo?userId=
。如果切換,接口新增一個userId參數即可。路徑參數的。例如
http://aaa.com/getUserInfo/
。如果切換,需要新增一個接口或者參數,并且從參數ticket中解析。
3.3 SSO server登出,通知業務同步登出接口(可選)
接口調用方:SSO server
接口描述:用于跨域場景下,業務側(例如Quick BI)被動接收登出的通知。SSO server在收到登出的通知的時候調用該接口,通知業務側系統做同步登出。?
接口API: /auth_sso/login/crossDomain/logout.do(POST) Content-Type:application/x-www-form-urlencoded;charset-utf8
接口參數:
參數名 | 類型 | 是否必選 | 說明 |
accountId | string | 是 | 需要登出的賬戶ID。 |
accessKey | string | 是 | 簽名的ak,用于安全校驗。 |
timestamp | string | 是 | 請求的時間戳,用于安全校驗。 |
nonce | string | 是 | 隨機字符串,用于重放攻擊的安全校驗。 |
signature | string | 是 | 簽名字符串,用于安全校驗。 |
返回參數:
{
"code":"200",
"message":"獲取成功",
"success":true,
"data": true, // 登出清理是否成功
"traceId": "xxxxxxxxxxxx" // 請求唯一ID
}
3.4 業務側登出,通知SSO server 側同步登出接口規范(可選)
接口調用方:業務側(例如Quick BI), 接口可選。
接口描述:用于跨域場景。業務側登出,業務清理掉自身登錄態Cookie后,調用配置的SSO server 該接口。SSO server收到通知后,做同步登出操作。該接口需要SSO server 按照標準協議規范開發并提供配置過來。
接口API:接口路徑自定義 (POST) Content-Type:application/x-www-form-urlencoded;charset-utf8
。
接口參數:
表單參數 | 類型 | 是否必選 | 說明 |
userId | string | 是 | 用戶賬戶的唯一ID。 |
accessKey | string | / | 簽名的ak,用于安全校驗。 請求API時自動帶上,客戶系統決定是否消費。 |
timestamp | string | / | 請求的時間戳,用于安全校驗。 請求API時自動帶上,SSO server決定是否消費。 |
nonce | string | / | 隨機字符串,用于重放攻擊的安全校驗。 請求API時自動帶上,SSO server決定是否消費 |
signature | string | / | 簽名字符串,用于安全校驗。 請求API時自動帶上,SSO server決定是否消費。 |
返回參數:
{
"code":"200",
"message":"獲取成功",
"success":true, // 用于判斷接口是否調用成功
"data": true, // 用于判斷登出是否執行成功
}
4.簽名校驗
4.1 接口安全加簽規則 & SDK算法
簽名采用行業通用的簽名算法,簽名算法如下:
主體簽名分成兩個步驟:
構建待簽名字符串
string_to_sign
。使用密鑰(secret_key)對拼接的
string_to_sign
進行簽名操作,加密算法使用HMAC-SHA256和base64。
4.2 構建待簽名字符串(string_to_sign)
拼接待簽名字符串string_to_sign
的拼接規則如下:
string_to_sign =
Request_Method\n
Request_Uri\n
Request_QueryString\n
上述各部分,以換行符\n
分割連接(非字符串"\n"
),各部分說明如下:
表1-1 待簽名字符串拼接說明
拼接部分 | 說明 | 示例 |
Request_Method | http方法名: 格式為大寫。 | GET |
Request_Uri | 原始請求的相對路徑,不包含host和URL請求參數。 重要 如果uri中攜帶符號 | / |
Request_QueryString | 主要由請求中所有Query參數、所有表單參數拼接而成。 拼接規則:
說明
| 對于請求: 其中,Query參數鍵值對:
按照參數名排序,順序為: key->pageNo->pageSize->status 由于key的參數為
|
4.3 生成簽名字符串(signature)
生成了待簽名字符串后,使用secret_key
私鑰對待簽名字符串進行加密,生成最終的簽名(signature)。具體的簽名規則如下:
signature = HMAC-SHA256-BASE64(sk, percentURLEncode(string_to_sign))
其中,需要說明的是:
percentURLEncode
:表示待簽名字符串的編碼以及特殊字符的處理。編碼規則如下:對于字符 A~Z、a~z、0~9 以及字符、短劃線(-)、下劃線(_)、半角句號(.)、波浪線(~)不編碼。
對于其它字符編碼成 %XY 的格式,其中 XY 是字符對應 ASCII 碼的 16 進制表示。例如英文的雙引號(”)對應的編碼為 %22。
對于擴展的
UTF-8
字符,編碼成%XY%ZA…
的格式。英文空格( )要編碼成
%20
,而不是加號(+)。
該編碼方式和一般采用的application/x-www-form-urlencoded MIME
格式編碼算法(比如 Java 標準庫中的 java.net.URLEncoder
的實現)相似,但又有所不同。實現時,可以先用標準庫的方式進行編碼,再把編碼后的字符串中加號(+)替換成 %20
、星號(*)替換成 %2A
、%7E
替換回波浪線(~),即可得到上述規則描述的編碼字符串。這個算法可以用下面的percentEncode方法來實現。
private static String percentEncode(String value) throws UnsupportedEncodingException {
return value != null ? URLEncoder.encode(value, "UTF-8").replace("+", "%20").replace("*", "%2A").replace("%7E", "~") : null;
}
HMAC-SHA256-BASE64
:表示先進行SHA256編碼,而后采用BASE64對生成的結果進行加密。
4.4 JAVA方式實現簽名校驗
目前,統一登錄層提供了開發jar包,可以直接使用。jar包下載地址:sso-signature-1.1.0-SNAPSHOT.jar
如果是集團域內環境下,可以直接引用maven:
<dependency>
<groupId>com.alibaba.quickbi</groupId>
<artifactId>sso-signature</artifactId>
<version>1.1.0-SNAPSHOT</version>
</dependency>
jar包中主要包含了三個類:
NonceUtil
:隨機字符串生成工具類。SignatureUtil
:簽名生成工具類。主要用于生成簽名方式。例如,3.2.3需要調取業務側的跨域登出接口,涉及簽名校驗。RequestSignatureUtil
:http請求的簽名校驗工具類。
主要用于解析待簽名的請求,例如:3.2.1/3.2.2/3.2.4接口,調用SSO server提供的接口,SSO server通過RequestSignatureUtil.validRequestSignature()
校驗請求的合法性。
SignatureUtil類如下:
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
/**
* 簽名生成組件
*
* @author gengnan.wy
* @date 2021-01-18
*/
public class SignatureUtil {
/**
* 構建待簽名的字符串
*
* @param uri 請求的URI
* @param method 方法,GET、POST
* @param parameters 參與簽名的請求參數。
* @return
*/
public static String buildStringToSign(String uri, String method,
Map<String, String> parameters) {
if (null == uri || "".equalsIgnoreCase(uri.trim())
|| null == method || "".equalsIgnoreCase(method.trim())) {
throw new IllegalArgumentException("input parameter error, uri or method can not be null");
}
// URL中。按照 原始符號 --> 瀏覽器URL編碼 --> spring web解析接收,對于 加號(+)和空格, 有如下問題:
// + -> %2B --> 空格;
// 空格 -> %20 --> 空格;因此,對于spring web接收到的請求,并不清楚空格的原始對應,是 + 還是空格
// 因此,此處對于源頭所有的+,按照空格處理
uri = uri.replace("+", " ");
// method
StringBuilder sb = new StringBuilder();
sb.append(method.toUpperCase());
sb.append("\n");
// uri
sb.append(uri);
sb.append("\n");
// paramters
if (null != parameters && parameters.size() > 0) {
String queryString = buildSortedString(parameters, "=", "&");
if (null != queryString) {
sb.append(queryString);
sb.append("\n");
}
}
return sb.toString();
}
/**
* 簽名函數
*
* @param stringToSign :待簽名的字符串
* @param secretKey 簽名加密的密鑰
* @return
*/
public static String sign(String stringToSign, String secretKey) {
if (null == stringToSign || null == secretKey) {
throw new IllegalArgumentException("input parameter error");
}
try {
String encodeString = percentEncode(stringToSign);
return sha256(encodeString, secretKey);
} catch (Exception e) {
e.printStackTrace();
throw new IllegalStateException("error in encode string");
}
}
private static String percentEncode(String value) throws UnsupportedEncodingException {
return value != null ? URLEncoder.encode(value, "utf-8")
.replace("+", "%20")
.replace("*", "%2A")
.replace("%7E", "~") : null;
}
/**
* 將map中的元素,按照key的字母順序,進行排序
*
* @param maps
* @return
*/
private static String buildSortedString(Map<String, String> maps, String symbol1, String symbol2) {
StringBuilder sb = new StringBuilder();
List<String> keys = new LinkedList<String>();
for (String key : maps.keySet()) {
keys.add(key);
}
Collections.sort(keys);
for (int i = 0; i < keys.size(); i++) {
String key = keys.get(i);
String value = maps.get(key);
// key或者value為空或null,不加入字符串的拼接
if (null == key || key.trim().length() == 0
|| null == value || value.trim().length() == 0) {
continue;
}
sb.append(key);
sb.append(symbol1);
sb.append(value);
if (i != keys.size() - 1) {
sb.append(symbol2);
}
}
return sb.toString();
}
/**
* sha256加密處理
*
* @param content 待加密字符串
* @param secret 密鑰
* @return
*/
public static String sha256(String content, String secret) throws NoSuchAlgorithmException,
UnsupportedEncodingException, InvalidKeyException {
Mac hamcSha256 = Mac.getInstance("HmacSHA256");
byte[] keyBytes = secret.getBytes("UTF-8");
SecretKeySpec secretKey = new SecretKeySpec(keyBytes, 0, keyBytes.length, "HmacSHA256");
hamcSha256.init(secretKey);
byte[] result = hamcSha256.doFinal(content.getBytes("UTF-8"));
return new String(Base64.getEncoder().encode(result));
}
}
RequestSignatureUtil如下:
import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Http請求的簽名校驗工具類
*
* @author gengnan.wy
* @date 2021-01-18
*/
public class RequestSignatureUtil {
private static final String SIGNATURE_PARAM_NAME = "signature";
private static final int NONCE_SIZE = 16;
/**
* 校驗request請求的合法性。
*
* @param request
* @return
*/
public static boolean validRequestSignature(HttpServletRequest request, String sk) {
if (null == request || null == sk) {
return false;
}
String uri = request.getRequestURI();
String method = request.getMethod();
Map<String, String> parameters = extractQueryParameters(request);
String requestSign = "";
if (!parameters.containsKey(SIGNATURE_PARAM_NAME)) {
return false;
} else {
requestSign = parameters.get(SIGNATURE_PARAM_NAME);
parameters.remove(SIGNATURE_PARAM_NAME);
}
String stringToSign = SignatureUtil.buildStringToSign(uri, method, parameters);
String sign = SignatureUtil.sign(stringToSign, sk);
if (sign.equalsIgnoreCase(requestSign)) {
return true;
}
return false;
}
/**
* 提取請求參數信息;如果一個參數具有多個參數值,則將多個參數值按照字母順序,從小到大排序,以英文逗號連接;
*
* @param request
* @return
*/
private static Map<String, String> extractQueryParameters(HttpServletRequest request) {
Map<String, String[]> parameterMap = request.getParameterMap();
Map<String, String> result = new HashMap<String, String>();
if (null == parameterMap || parameterMap.size() == 0) {
return result;
}
for (Map.Entry<String, String[]> entry : parameterMap.entrySet()) {
String key = entry.getKey();
String[] values = entry.getValue();
// 參數為空的,不加入簽名計算
if (null == values || values.length == 0) {
continue;
}
if (values.length == 1) {
result.put(key, values[0]);
} else {
String value = sortArraysToString(values, ",");
result.put(key, value);
}
}
return result;
}
/**
* 按照字典順序排序
*
* @param arrays
* @return
*/
private static String sortArraysToString(String[] arrays, String sep) {
List<String> list = Arrays.asList(arrays);
Collections.sort(list);
StringBuilder sb = new StringBuilder();
for (int i = 0; i < list.size(); i++) {
sb.append(list.get(i));
if (i != list.size() - 1) {
sb.append(sep);
}
}
return sb.toString();
}
}