客户(机构)的安全凭证,用来验证用户所属企业是否有效。
对企业下的用户账号进行鉴权的安全凭证,用来验证用户账号是否有效。
在开通SDK配置之后,会从腾讯侧获取到SDK接入所需的对接参数。与Token生成相关的具体参数及用途如下:
1、SDK ID:企业SDK应用的唯一标识,在生成SDK Token和SDK初始化时使用。
2、SDK Secret:SDK秘钥,和SDK ID一起,用于生成SDK Token。
3、SSO_URL前缀:用于和ID Token一起拼接成SSO_URL,在SDK登录时使用。
4、公私钥文件:用于生成ID Token。
步骤1:客户的Client端通过自有的协议向客户自己的Server端请求获取sdk_id和sdk_token数据,该流程属于客户自身业务实现,与腾讯会议无关。
步骤2&3:客户Server端返回sdk_id和sdk_token,这里生成的逻辑将在下文介绍。
步骤4:调用SDK的初始化接口,并返回结果。需要注意的是,这里SDK Token并没有被送到腾讯会议后台进行验证,只是在本地做了一个检查,后面调用登录接口的时候才会送到后台进行验证,也就是说如果SDK Token是无效的,不一定会在初始化的时候就报错。
步骤1&2&3:客户的Client和Server端的逻辑,与会议SDK无关。
步骤4&5:生成ID Token并将腾讯侧提供的SSO_URL前缀参数和ID Token拼接成SSO_URL并返回,具体方法见下文。
步骤6:使用从客户Server端获取的SSO_URL调用SDK的登录接口。
步骤7&8:SDK向IDaaS和腾讯会议Server端发送SSO_URL和SDK Token,后台返回鉴权结果。
步骤9:SDK通过OnLogin回调返回登录结果给客户的Client。
SDK Token是JWT的格式,其各个部分定义如下:
Header
{
"alg": "HS256",
"typ": "JWT"
}
以上部分为固定值,按照上面写死即可。签名算法是SHA256。
Payload
{
"aud": "Tencent Meeting", // 受众,固定写死为"Tencent Meeting"
"exp": 1590804000, // Token过期时间,由客户自己决定(单位:秒)
"iat": 1588212000, // Token签发生成时间(当前时间,单位:秒)
"iss": "2012081666" // 申请到的SDK ID
}
数据Payload部分包含以上四个属性,按要求填写即可。 所有涉及到时间的属性,都是Unix时间戳,即从1970年1月1日(UTC/GMT的午夜)开始所经过的秒数,不考虑闰秒。
Signature
客户的Server端将SDK Secret作为签名的secret,对整个数据进行SHA256签名。
ID Token是JWT的格式,其各部分定义如下:
Header
{
"typ": "JWT",
"alg": "RS256"
}
以上字段都是固定值,其中alg字段是RS256,表示用RSA签名。
Payload
{
"sub": "123456789", //IDaaS系统中的username字段,对应腾讯会议的userId字段
"iss": "2012081666", //SDK ID
"name": "tencent_dev04", //IDaaS系统中的displayName字段,对应腾讯会议的username字段,即显示名称
"exp": 1619554966, //Token过期时间(单位:秒)
"iat": 1601387166 //Token签发生成时间(当前时间,单位:秒)
}
数据Payload部分包含以上五个属性,按要求填写即可。 所有涉及到时间的属性,都是Unix时间戳,即从1970年1月1日(UTC/GMT的午夜)开始所经过的秒数,不考虑闰秒。
Signature
客户与IDaaS服务方约定生成的ID Secret,此处客户的Server端使用该ID Secret对整个数据进行签名,RSA签名方式。
根据前面的信息,总结出以下要点需要在后续实现中考虑(部分和SDK端侧接入相关,不在本文实现范围内,这里先提出来):
1、Token生成的代码和密钥要部署在Server端,不可在终端程序上实现。
2、客户Client端在初始化时要向客户Server端获取SDK ID和SDK Token参数。
3、客户Client端在登录时要向客户Server端获取SSO_URL参数。
4、SDK在初始化时只对SDK Token做了简单校验,在调用登录接口时才传到后台进行验证。
5、SDK有本地登录缓存,有效期就是初始化传入的SDK Token的有效期,因此SDK Token的有效期一般要设置的长一点,至少要比客户Client的登录有效期长。过期的条件是时间超过SDK Token设置的有效期或者调用logout接口登出账号。
6、SDK的本地登录缓存用于快速登录,初始化时或者运行过程中调用refreshSDKToken接口会刷新缓存的有效期,使其与新的SDK Token有效期保存一致。
7、ID Token只在登录验证时有效即可,因此有效期可以设置的比较短,一般是5分钟。
8、SSO_URL是由开通SDK时获取的SSO_URL前缀参数和ID Token拼接而成。
本文实现生成SDK Token和ID Token,并且封装后提供给SDK初始化和登录时使用。实现分为以下几个模块:
1、PemUtils:秘钥文件处理工具类。
2、JWTConfig:参数配置。
3、JWTToken:生成SDK Token,返回SDK ID,生成拼接后的SSO_URL。
当使用生成的SDK Token和ID Token进行初始化登录时报错,需要快速排查Token是否有效,可以用下面的方法。
1、在https://jwt.io/ 网页左边框输入生成的SDK Token,右边输入SDK secret参数,不勾选base64 encode。
2、左下角显示Signature Verified并且Header和Payload参数字段都正确,说明Token是有效的。
1、在https://jwt.io/ 网页左边框输入生成的ID Token。
2、检查Header和Payload参数字段是否正确。
3、将拼接后的SSO_URL贴到浏览器地址栏打开,如果能调起腾讯会议客户端并登录成功,说明ID Token有效。
当ID Token无效时,在浏览器的地址栏会有报错提示。例如下图是因为ID Token已过期。
public class PemUtils {
private static byte[] parsePEMFile(File pemFile) throws IOException {
if (!pemFile.isFile() || !pemFile.exists()) {
throw new FileNotFoundException(String.format("The file '%s' doesn't exist.", pemFile.getAbsolutePath()));
}
PemReader reader = new PemReader(new FileReader(pemFile));
PemObject pemObject = reader.readPemObject();
byte[] content = pemObject.getContent();
reader.close();
return content;
}
private static PublicKey getPublicKey(byte[] keyBytes, String algorithm) {
PublicKey publicKey = null;
try {
KeyFactory kf = KeyFactory.getInstance(algorithm);
EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
publicKey = kf.generatePublic(keySpec);
} catch (NoSuchAlgorithmException e) {
System.out.println("Could not reconstruct the public key, the given algorithm could not be found.");
} catch (InvalidKeySpecException e) {
System.out.println("Could not reconstruct the public key");
}
return publicKey;
}
private static PrivateKey getPrivateKey(byte[] keyBytes, String algorithm) {
PrivateKey privateKey = null;
try {
KeyFactory kf = KeyFactory.getInstance(algorithm);
EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
privateKey = kf.generatePrivate(keySpec);
} catch (NoSuchAlgorithmException e) {
System.out.println("Could not reconstruct the private key, the given algorithm could not be found.");
} catch (InvalidKeySpecException e) {
System.out.println("Could not reconstruct the private key");
}
return privateKey;
}
public static PublicKey readPublicKeyFromFile(String filepath, String algorithm) throws IOException {
byte[] bytes = PemUtils.parsePEMFile(new File(filepath));
return PemUtils.getPublicKey(bytes, algorithm);
}
public static PrivateKey readPrivateKeyFromFile(String filepath, String algorithm) throws IOException {
byte[] bytes = PemUtils.parsePEMFile(new File(filepath));
return PemUtils.getPrivateKey(bytes, algorithm);
}
}
public class JWTConfig {
public static final String PUBLIC_KEY_FILE_RSA = "src/test/resources/rsa_public_key.pem";
public static final String PRIVATE_KEY_FILE_RSA = "src/test/resources/rsa_private_key.pem";
public static final String HS256_Secret = "";
public static final String SDK_ID = "";
// 生成SDK token的有效期时长,不短于客户端登录态的有效期,单位毫秒
public static final long SDK_TOKEN_PERIOD_OF_VALIDITY = 30*24*60*60*(1000L);
// 生成ID token的有效期时长,5分钟即可,单位毫秒
public static final long ID_TOKEN_PERIOD_OF_VALIDITY = 5*60*1000;
public static final String SSO_URL = "https://test-idp.id.meeting.qq.com/cidp/custom/ai-2bd60857a7fd42a9b0887e321f16776a/ai-37586921eda647e580c409bef20a1b82?id_token=";
}
public class JWTToken {
private static String SDKTokenEncode(String secret, Map<String, Object> headerClaims, Map<String, Object> payloadClaims) {
try {
Algorithm algorithm = Algorithm.HMAC256(secret);
String token = JWT.create()
.withHeader(headerClaims)
.withAudience((String) payloadClaims.get("aud"))
.withExpiresAt((Date) payloadClaims.get("exp"))
.withIssuedAt((Date) payloadClaims.get("iat"))
.withIssuer((String) payloadClaims.get("iss"))
.sign(algorithm);
return token;
} catch (JWTCreationException exception){
//Invalid Signing configuration / Couldn't convert Claims.
}
return null;
}
public static String generateSDKToken() {
//HMAC
String sdkSecret = JWTConfig.HS256_Secret;
Map<String, Object> sdkTokenHeaderClaims = new HashMap<String, Object>();
Map<String, Object> sdkTokenPayloadClaims = new HashMap<String, Object>();
//JWT Header
sdkTokenHeaderClaims.put("alg", "HS256");
sdkTokenHeaderClaims.put("typ", "JWT");
//JWT Payload
sdkTokenPayloadClaims.put("aud", "Tencent Meeting");
sdkTokenPayloadClaims.put("iss", JWTConfig.SDK_ID);
long iatSDKTokenTimeMillis = System.currentTimeMillis();
long expSDKTokenTimeMillis = iatSDKTokenTimeMillis + JWTConfig.SDK_TOKEN_PERIOD_OF_VALIDITY;
sdkTokenPayloadClaims.put("iat", new Date(iatSDKTokenTimeMillis));
sdkTokenPayloadClaims.put("exp", new Date(expSDKTokenTimeMillis));
String sdkToken = SDKTokenEncode(sdkSecret, sdkTokenHeaderClaims, sdkTokenPayloadClaims);
return sdkToken;
}
private static String IDTokenEncode(RSAPublicKey publicKey, RSAPrivateKey privateKey, Map<String, Object> headerClaims, Map<String, Object> payloadClaims) {
try {
Algorithm algorithm = Algorithm.RSA256(publicKey, privateKey);
String token = JWT.create()
.withHeader(headerClaims)
.withSubject((String) payloadClaims.get("sub"))
.withIssuer((String) payloadClaims.get("iss"))
.withClaim("name", (String) payloadClaims.get("name"))
.withExpiresAt((Date) payloadClaims.get("exp"))
.withIssuedAt((Date) payloadClaims.get("iat"))
.sign(algorithm);
return token;
} catch (JWTCreationException exception){
//Invalid Signing configuration / Couldn't convert Claims.
}
return null;
}
private static String generateIDToken(String userid, String username) throws IOException {
//RSA
RSAPublicKey publicKey = (RSAPublicKey) PemUtils.readPublicKeyFromFile(JWTConfig.PUBLIC_KEY_FILE_RSA, "RSA");//Get the key instance
RSAPrivateKey privateKey = (RSAPrivateKey) PemUtils.readPrivateKeyFromFile(JWTConfig.PRIVATE_KEY_FILE_RSA, "RSA");//Get the key instance
Map<String, Object> idTokenHeaderClaims = new HashMap<String, Object>();
Map<String, Object> idTokenPayloadClaims = new HashMap<String, Object>();
//JWT Header
idTokenHeaderClaims.put("alg", "RS256");
idTokenHeaderClaims.put("typ", "JWT");
//JWT Payload
idTokenPayloadClaims.put("iss", JWTConfig.SDK_ID);
idTokenPayloadClaims.put("sub", userid);
idTokenPayloadClaims.put("name", username);
long iatIdTokenTimeMillis = System.currentTimeMillis();
long expIdTokenTimeMillis = iatIdTokenTimeMillis + JWTConfig.ID_TOKEN_PERIOD_OF_VALIDITY;
idTokenPayloadClaims.put("iat", new Date(iatIdTokenTimeMillis));
idTokenPayloadClaims.put("exp", new Date(expIdTokenTimeMillis));
String idToken = IDTokenEncode(publicKey, privateKey, idTokenHeaderClaims, idTokenPayloadClaims);
return idToken;
}
public static String getSdkId() {
return JWTConfig.SDK_ID;
}
public static String getSsoUrl(String userid, String username) {
String idToken;
try {
idToken = generateIDToken(userid, username);
} catch (IOException e) {
throw new RuntimeException(e);
}
return JWTConfig.SSO_URL+idToken;
}
}
public class main {
public static void main(String[] args) throws IOException {
String userid = "test";
String username = "test";
//SDK初始化参数获取
String sdkId = JWTToken.getSdkId();
String sdkToken = JWTToken.generateSDKToken();
System.out.println("SDK ID: " + sdkId);
System.out.println("SDK Token: " + sdkToken);
//SDK登录参数获取
String ssoUrl = JWTToken.getSsoUrl(userid, username);
System.out.println("username: " + userid);
System.out.println("SSO URL: " + ssoUrl);
}
}
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。