跳到主要内容
版本:5.9.0

观远BI-SSO集成

1. 观远 SSO 集成概述

观远 SSO 集成是基于非对称加解密的简单 SSO 集成方案,这种方案允许使用第三方的系统账户来验证观远数据平台的用户并进行登录,是我们最常用也最推荐的 SSO 集成方案。

2. SSO 方式登录并访问观远数据

URL:

?provider={providerName}&ssoToken={ssoToken}

Method:GET

Parameters:

NameLocation类型含义是否必填备注
providerNamePathString提供者双方约定的一个字符串, 用于识别客户的系统(以下简称甲方系统)2019/12之后,prodiverName即客户在观远的domainid,大小写也要保持一致。
ssoTokenPathString加密后的用于登录的信息需要加密,加密方法以及注意事项请参考 售前阶段-单点登录-RSA非对称加解密。
domainId其他String域ID默认值为guanbi 
externalUserId其他String甲方系统用户ID需要确保唯一性;建议与观远系统中的账号保持一致,如果不一致,需要人工进行映射关系的维护。
timemstamp其他Integer时间戳如果传timestamp参数,则认为ssoToken有效期为5分钟;不传,则ssoToken永久有效,不会过期

SSO Token加密前内容示例:

{
"domainId":"abcbi",
"externalUserId": "userId",
"timestamp": 1502079219
}

按照约定的方式加密后即生成 ssoToken(见3. RSA方式进行内容加解密)。

其中,如果用户是调用11.1.1 统一账户集成文档所述的API创建的,那么当系统的登录方式是 工号登录时, externalUserId 即为 loginId ;当采用 email 方式登录时,external user id 即为 email 地址。

Response:

返回结果即为观远首页页面,如果基础URL是页面的链接,则返回对应的页面。

免密登录到表单填报

如果需要免密登录到表单填报里,则需要在配置的链接里,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方式进行内容加解密

在各个系统对接过程中,有时候需要对部分数据进行加密,下面主要介绍一种常用的加解密方法即RSA。(备注:具体采用哪种方法,需要由与您的观远数据顾问双方进行约定)。

3.1 准备工作

客户(以下简称甲方):准备一对 RSA Private Key 和 Public Key。

格式要求:PKCS#8

甲方:进入管理员设置-系统集成-SSO,选择观远 SSO 集成的集成方式,并将Public Key填入对应区域中。

单点登录1.png

3.2 加密

1)使用 Private Key 对内容(一般为JSON格式)进行加密;

2)对加密后的内容用Base64进行编码;

3)由于编码后的字符串可能含有“=”等特殊字符,需要将加密后的字符串转换为hex格式。

3.3 解密

将收到的内容(hex 格式字符串)转回普通字符串。

我们将客户提供的 Public key 对转换后的内容进行解密,以获得加密。

3.4 示例代码

以下我们提供 Java、NodeJS 和 .NET 三种示例代码供大家参考。

3.4.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.HashMap;

import java.util.Map;

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 MapcreateKeys(){

//为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());

MapkeyPairMap = 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));

RSAPublicKey key = (RSAPublicKey) keyFactory.generatePublic(x509KeySpec);

return key;

}

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));

RSAPrivateKey key = (RSAPrivateKey) keyFactory.generatePrivate(pkcs8KeySpec);

return key;

}

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[] datas, int keySize){

int maxBlock = 0;

if(opmode == Cipher.DECRYPT_MODE){

maxBlock = keySize / 8;

}else{

maxBlock = keySize / 8 - 11;

}

ByteArrayOutputStream out = new ByteArrayOutputStream();

int offSet = 0;

byte[] buff;

int i = 0;

try{

while(datas.length > offSet){

if(datas.length-offSet > maxBlock){

buff = cipher.doFinal(datas, offSet, maxBlock);

}else{

buff = cipher.doFinal(datas, offSet, datas.length-offSet);

}

out.write(buff, 0, buff.length);

i++;

offSet = i * maxBlock;

}

} catch(Exception e){

e.getMessage();

}

byte[] resultDatas = out.toByteArray();

try {

out.close();

} catch(Exception e){

e.getMessage();

}

return resultDatas;

}

public static String toHexString(String s)

{

String str="";

for (int i=0;i< s.length();i++)

{

int ch = (int)s.charAt(i);

str += Integer.toHexString(ch);

}

return str;

}

public static void main (String[] args) throws Exception {

MapkeyMap = 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);

}

}

注意请求参数里的 timestamp 的值需要根据实际情况生成,不要采用示例代码里的 1521616977 。 

3.4.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.4.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);

       }

   }

}

4. 问题排查

4.1 Cookie跨站

在采用该单点登录方式做跨系统集成、内嵌时,容易遇到跨站Cookie相关的问题,导致BI服务反复提示登录无法正常使用。

解决方案一(推荐):调整BI服务的域名,使用与外部系统相同的站点(主域名)的次级域名。

解决方案二:联系观远数据技术支持或者运维,启用跨站点功能。但该方式存会降低token的安全性。

{
"CROSS_SITE_HEADER_TOKEN_ENABLED": true
}

技术原理:使用 Http header 来传递token (默认不启用)。