观远BI-SSO集成
1. 观远SSO集成概述
观远 SSO 集成是基于RSA非对称加解密的简单 SSO 集成方案,这种方案允许使用第三方的系统账户来验证观远数据平台的用户并进行登录。
对接步骤包括:
-
生成一对RSA公私钥
-
在观远BI中配置RSA公钥
-
使用RSA私钥对登录信息进行加密,生成ssoToken
-
在观远链接后携带ssoToken进行登录并访问
2. SSO方式登录并访问观远系统
2.1. 生成RSA公私钥对并配置
生成RSA公私钥的几种方式:
-
在观远BI“系统集成”界面中生成RSA公私钥对
-
使用3.1 Java示例代码中的程序生成
-
自行创建,格式要求为PKCS#8,长度1024位
然后将公钥配置到观远BI中,如下图所示:
2.2. 加密登录信息,生成ssoToken
2.2.1. 待加密参数
Name | 类型 | 含义 | 是否必填 | 备注 |
domainId | String | 域ID | 否 | 自5.9版本开始,无需此参数。5.9版本之前,domainId默认值为guanbi,获取方式可参考《如何获取domainId信息》 |
externalUserId | String | 第三方系统用户ID | 是 | 需要确保唯一性;建议与观远系统中的账号保持一致,如果不一致,需要人工进行映射关系的维护。 |
timestamp | Integer | 当前时间戳 | 否 | 如果传timestamp参数,需要在timestamp时间起、5min内完成登录,否则登录会失败;不传,则ssoToken永久有效,不会过期。 |
加密前内容示例:
{
"domainId":"abcbi",
"externalUserId": "userId",
"timestamp": 1502079219
}
2.2.2. 加密逻辑
使用RSA私钥对登录信息加密后,即可得到ssoToken。加密步骤如下:
-
使用RSA私钥对内容(JSON格式)进行加密;
-
对加密后的内容用Base64进行编码;
-
由于编码后的字符串可能含有“=”等特殊字符,需要将加密后的字符串转换为hex格式。
代码可参考 “3. RSA加解密代码示例”。
2.3. 携带ssoToken,访问BI链接
在BI链接中携带provider和加密获取到的ssoToken,即可实现免密登录。
URL:
?provider={provider}&ssoToken={ssoToken}
Method: GET
Parameters:
Name | Location | 类型 | 含义 | 备注 |
provider | Path | String | 提供者 | 双方约定的一个字符串, 用于识别客户的系统(以下简称甲方系统)。2019/12之后,prodiver即为客户在观远的domainId,大小写也要保持一致,获取方式可参考《如何获取domainId信息》,一般为guanbi。 |
ssoToken | Path | String | 加密后的用于登录的信息 | 2.2步骤中加密生成的token |
Response:
返回结果即为观远首页页面,如果基础URL是页面的链接,则返回对应的页面。
2.4. 免密登录到表单填报(按需)
如果需要免密登录到表单填报里,则需要在配置的链接里,host_url后加一个固定的路由(如下),再按照第三方集成要求添加其他参数。
?path_url=survey-engine/
例如:
https://app.mayidata.com?path_url=survey-engine%2Fm%2Fsurvey%2F4bd86aa5-89d3-4b72-8186-a06170cdddbd
填报链接做免密登录链接拼接注意点:
(1)在survey-engine前要加指定参数 “path_url=”
注:参数不能直接跟在“/”后面,只能跟在“?”后面
(2)填报链接要做转码
注:转码可以使用一些小工具,如:https://www.bejson.com/enc/urlencode
举例:
表单链接:
https://app.mayidata.com/survey-engine/survey/1b36b7ff-d470-475e-931f-df1ce53b3ed5
拼接后的免密登录链接:
https://app.mayidata.com?path_url=survey-engine%2Fsurvey%2F1b36b7ff-d470-475e-931f-df1ce53b3ed5&……(其他参数)
3. RSA加解密代码示例
3.1. Java示例代码
import javax.crypto.Cipher;
import java.io.ByteArrayOutputStream;
import java.security.*;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.codec.binary.Base64;
public class Main {
public static final String CHARSET = "UTF-8";
public static final String RSA_ALGORITHM = "RSA";
public static final int KEY_SIZE = 1024;
public static Map createKeys() {
//为RSA算法创建一个KeyPairGenerator对象
KeyPairGenerator kpg;
try {
kpg = KeyPairGenerator.getInstance(RSA_ALGORITHM);
} catch (NoSuchAlgorithmException e) {
throw new IllegalArgumentException("No such algorithm-->[" + RSA_ALGORITHM + "]");
}
//初始化KeyPairGenerator对象,密钥长度
kpg.initialize(KEY_SIZE);
//生成密匙对
KeyPair keyPair = kpg.generateKeyPair();
//得到公钥
Key publicKey = keyPair.getPublic();
String publicKeyStr = Base64.encodeBase64String(publicKey.getEncoded());
//得到私钥
Key privateKey = keyPair.getPrivate();
String privateKeyStr = Base64.encodeBase64String(privateKey.getEncoded());
Map keyPairMap = new HashMap<>();
keyPairMap.put("publicKey", publicKeyStr);
keyPairMap.put("privateKey", privateKeyStr);
return keyPairMap;
}
public static RSAPublicKey getPublicKey(String publicKey) throws NoSuchAlgorithmException, InvalidKeySpecException {
//通过X509编码的Key指令获得公钥对象
KeyFactory keyFactory = KeyFactory.getInstance(RSA_ALGORITHM);
X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(Base64.decodeBase64(publicKey));
return (RSAPublicKey) keyFactory.generatePublic(x509KeySpec);
}
public static RSAPrivateKey getPrivateKey(String privateKey) throws NoSuchAlgorithmException, InvalidKeySpecException {
//通过PKCS#8编码的Key指令获得私钥对象
KeyFactory keyFactory = KeyFactory.getInstance(RSA_ALGORITHM);
PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(Base64.decodeBase64(privateKey));
return (RSAPrivateKey) keyFactory.generatePrivate(pkcs8KeySpec);
}
public static String privateEncrypt(String data, RSAPrivateKey privateKey) {
try {
Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, privateKey);
return Base64.encodeBase64String(rsaSplitCodec(cipher, Cipher.ENCRYPT_MODE, data.getBytes(CHARSET), privateKey.getModulus().bitLength()));
} catch (Exception e) {
throw new RuntimeException("加密字符串[" + data + "]时遇到异常", e);
}
}
public static String publicDecrypt(String data, RSAPublicKey publicKey) {
try {
Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, publicKey);
return new String(rsaSplitCodec(cipher, Cipher.DECRYPT_MODE, Base64.decodeBase64(data), publicKey.getModulus().bitLength()), CHARSET);
} catch (Exception e) {
throw new RuntimeException("解密字符串[" + data + "]时遇到异常", e);
}
}
private static byte[] rsaSplitCodec(Cipher cipher, int opMode, byte[] data, int keySize) {
int blockSize = (opMode == Cipher.DECRYPT_MODE) ? keySize / 8 : keySize / 8 - 11;
int offset = 0;
List resultList = new ArrayList<>();
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
while (data.length > offset) {
int length = Math.min(data.length - offset, blockSize);
byte[] encryptedBlock = cipher.doFinal(data, offset, length);
resultList.add(encryptedBlock);
offset += blockSize;
}
for (byte[] block : resultList) {
outputStream.write(block);
}
return outputStream.toByteArray();
} catch (Exception e) {
e.printStackTrace();
}
return new byte[0]; // Return empty array in case of failure
}
public static String toHexString(String s) {
StringBuilder str = new StringBuilder();
for (int i = 0; i < s.length(); i++) {
int ch = s.charAt(i);
str.append(Integer.toHexString(ch));
}
return str.toString();
}
public static void main(String[] args) throws Exception {
// 生成公钥和私钥
Map keyMap = createKeys();
String publicKey = keyMap.get("publicKey");
String privateKey = keyMap.get("privateKey");
System.out.println("公钥: \n\r" + publicKey);
System.out.println("私钥: \n\r" + privateKey);
System.out.println("私钥加密——公钥解密");
// 使用私钥对明文进行加密
String str = "{\"domainId\":\"domId\",\"externalUserId\":\"UID\",\"timestamp\":1521616977}";
System.out.println("\r明文:\r\n" + str);
String encodedData = privateEncrypt(str, getPrivateKey(privateKey));
System.out.println("密文:\r\n" + toHexString(encodedData));
// 使用公钥对密文进行解密
String decodedData = publicDecrypt(encodedData, getPublicKey(publicKey));
System.out.println("解密后文字: \r\n" + decodedData);
}
}
3.2. NodeJS示例代码
var NodeRSA = require('node-rsa');
const encrypt_rsa = function (data) {
const privateKey = new NodeRSA("-----BEGIN PRIVATE KEY-----\n" +
"客户生成私钥\n" +
"-----END PRIVATE KEY-----");
const result = privateKey.encryptPrivate(data, "base64");
const hexResult = Buffer.from(result, 'utf8').toString('hex');
return hexResult;
}
3.3. .Net示例代码
需要先引入BouncyCastle.Crypto
using System;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Security;
using System.Security.Cryptography;
using System.Text;
using System.IO;
using Org.BouncyCastle.OpenSsl;
using Org.BouncyCastle.Crypto.Parameters;
namespace SSOEncrypt
{
public class RSAEncrypt
{
public static string RSAEncryptByPrivateKey(string privateKey, string strEncryptString)
{
//加载私钥
RSACryptoServiceProvider privateRsa = new RSACryptoServiceProvider();
var byteArray = Encoding.ASCII.GetBytes(privateKey);
using (var ms = new MemoryStream(byteArray))
{
using (var sr = new StreamReader(ms))
{
var pr = new Org.BouncyCastle.Utilities.IO.Pem.PemReader(sr);
var rsaParams = pr.ReadPemObject();
var pk = PrivateKeyFactory.CreateKey(rsaParams.Content);
privateRsa.ImportParameters(DotNetUtilities.ToRSAParameters(pk as RsaPrivateCrtKeyParameters));
pr.Reader.Close();
}
}
//转换密钥
AsymmetricCipherKeyPair keyPair = DotNetUtilities.GetKeyPair(privateRsa);
IBufferedCipher c = CipherUtilities.GetCipher("RSA/ECB/PKCS1Padding");// 参数与Java中加密解密的参数一致
c.Init(true, keyPair.Private); //第一个参数为true表示加密,为false表示解密;第二个参数表示密钥
byte[] DataToEncrypt = Encoding.UTF8.GetBytes(strEncryptString);
byte[] outBytes = c.DoFinal(DataToEncrypt);//加密
string strBase64 = Convert.ToBase64String(outBytes);
byte[] byteArr = Encoding.UTF8.GetBytes(strBase64);
StringBuilder sb = new StringBuilder(byteArr.Length * 2);
foreach (byte b in byteArr)
{
sb.AppendFormat("{0:x2}", b);
}
return sb.ToString();
}
static void Main()
{
string data = "{\"domainId\":\"demo\",\"externalUserId\":\"hello1@sso.com\"}";
string pem = "-----BEGIN PRIVATE KEY-----\n" +
// 私钥文本
"MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAK8mzOMZtCxA6md7pYz4pzZv69FrdILu5fd8M9SMGMKPP3lzsFMquaBHQ5fF1wkV/6U/EkAjELLwq4pwQB1S+vXp3jP4OtR8aA+gO7tYrodQswoJKfm49fDEYJ8d0VKIRqM4DeG9ihky2EIAzh5HJJsuqdLSMVllwVoDUaPBMBxBAgMBAAECgYBGMS569JoYAgvuXMcDr8KTNlczHfUbY9IVVFkRHPPvRKkTayGGsuChMu4LrOV4ZrCE8LnHqkXO8FROrp2DIvYfXMWjw2kXmIragHCxYCynz3XHsvig0YtFaDuS6xGjAGEwcYjyGVyRphgOzfYjwueqx6j81XL+8ejjx/aAlxyqAQJBAOXwTltxVlPg9UV3cF/QHJ0/+0sFUF5lrZxWdqDF/FfgeMY7SkfFX7qSmp5O4myz5ry9ZEWE3Xfvc1IwSyeDBSkCQQDDANcZMRh1s5kxGH2mJ2dixCmPRot3m/gbobczT6nqzWZh+srnFSr6oj5KWpFOb/KZf9fpi+y27OdiqaUyielZAkBURmMxuLR/QbAjqccSFuCl8dFUiboPHw0mg7ou6uG2A5vAa/Kpo3mWlCz/YMI0PSuQeYnKwQu67ZRCx1iEPs0hAkAvBLLYlifpqWZUmi0htPqOq/HBZCcYrfjC4NlFe/3iaag4E7p8wXPdfuU6FGBY41FBhbvPyjdHXBPmjDUS3IHxAkEAhKOLVD4jdR1RhXAL19MMNdpn5Qa4X4fxOH1ZXYPhsr2hfM2YFKv5LehNCPy92dXoqgcqSx75II0uqED2xNsrXw==\n" +
"-----END PRIVATE KEY-----";
string encryptData = RSAEncryptByPrivateKey(pem, data);
Console.WriteLine(encryptData);
}
}
}
3.4. 注意事项
- 代码示例中的公私钥、timestamp等数据均为示例,请根据实际情况替换。
4. 常见问题
4.1. Cookie跨站
在采用该单点登录方式做跨系统集成、内嵌时,容易遇到跨站Cookie相关的问题,导致BI服务反复提示登录无法正常使用。
解决方案一(推荐):调整BI服务的域名,使用与外部系统相同的站点(主域名)的次级域名。
解决方案二:联系观远数据技术支持或者运维,启用跨站点功能。但该方式存会降低token的安全性。
4.2. 解密失败
常见原因:
-
公私钥不匹配。请检查加密使用的私钥与观远BI中配置的公钥是否配对。
-
provider不对。2019/12之后,prodiver即为客户在观远的domainId,大小写也要保持一致,获取方式可参考《如何获取domainId信息》,一般为guanbi。