本文为您介绍自建标准的三方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();
}
}