免登

三方异构系统免登进入V8

版本要求: ctp-user >= 3.2.x

1、概述

平台提供标准的免登认证机制,三方异构系统根据分配的秘钥调用接口获取免登授权码,并按要求拼接免登地址后,即可免登进入COP平台。

2、集成导图

1721380688616

3、集成步骤

3.1、申请APPKey和AppSecret

访问路径:管理后台—>集成平台–>开放平台–>接入应用

1720675794903

1720675856079

1720675898664

3.2、接口调用,获取免登授权码

调用接口【获取免登授权码】接口,接口参数规则说明:

3.3、拼接免登地址

【COP平台访问域名】:COP平台前台访问域名,例如https://saas.seeyonv8.com

【Web端待跳转地址】:免登成功后需要跳转的COP平台地址,例如首页地址:“/main/portal”

【移动端端待跳转地址】:免登成功后需要跳转的COP平台地址,例如首页地址:“/main-mobile/portal”

urlencode:编码函数,为了防止出现参数冲突,【Web端待跳转地址】和【移动端端待跳转地址】需要采用url编码后再拼接到免登地址中

【appKey】:第二步COP平台为每个应用分配的AppKey

【免登授权码】上一步接口获取的免登授权码

免登地址:【COP平台访问域名】/oauth/avoid?web=urlencode(【Web端待跳转地址】)&mobile=urlencode(【移动端端待跳转地址】)&sytype=sytoken&syid=【appKey】&sytoken=【免登授权码】

例如:

https://saas.seeyonv8.com/oauth/avoid?web=%2Fmain%2Fportal&mobile=&sytype=sytoken&syid=cd13f41d30f44d438b05b6588411178f&sytoken=SY-otokx4kfq0wtiwxm

按照以上步骤操作后,URL地址直接写入浏览器URL即可免登进入COP平台首页

3.4、验证免登授权码

注意:联调阶段或者问题定位阶段,使用【验证免登授权码】接口,验证免登授权码是否正常有效;

4、接口清单

4.1、获取免登授权码

请求地址

【COP平台访问域名】/service/ctp-user/auth/avoid/sytoken

请求方式

POST

请求参数(Body) 参数生成示例见 5.2 java 调用示例

参数名称 是否必填 参数类型 参数描述
responseType TRUE string 请求类型,
固定值:create
clientId TRUE string 应用ID,操作步骤中的接入应用-AppKey
例如:cd13f41d30f44d438b05b6588411178f
dataType TRUE string 用户标识键,双方约定的用户标识字段,标识用于生成签名的用户标识键等于COP平台的对应字段
枚举值:
loginName=用户名;
mobile=手机号码;
code=用户编号;
email=邮箱;
userid=用户ID;
dataValue TRUE string AES用户信息加密;当dataType=mobile时,用户手机号码为17300001234,则明文为17300001234
加密配置信息:
      明文:17300001234
      模式:CBC固定模式不可变
      填充:Pkcs7或Pkcs5固定类型不可变
      偏移量:apaasseeyonv8com 固定值不可变
      密文编码:HEX类型不可变
      秘钥:接入应用分配的AppSecret,例如:93ec877511d24dda8cf86a9d7870f681
加密后结果集示例:6d52cb81d4f8ee6359b0559f3aa0bcba
AES在线加密参考网站:http://tool.lvtao.net/aes
signature TRUE string 签名函数,以下四个参数经过自然排序后,拼接成一个字符串,使用SHA256加密
加密前四个参数:
AppKey(接入应用分类的AppKey);
AppSecret(接入应用分配的AppSecret);
data(加密后的用户信息dataValue);
时间戳:请求参数中的timestamp;
如对 “abcd” 进行签名后的结果为 “88d4266fd4e6338d13b845fcf289579d209c897823b9217da3e161936f031589”
SHA256在线加密参考网站:https://crypot.51strive.com/sha256.html
timestamp TRUE string 毫秒级时间戳,参与生成签名;
例如:“1720669311740”

请求参数示例

{ "responseType": "create", "clientId": "1242bc19f9f6493c9599ba007b9774c9", "dataType": "mobile", "dataValue": "6d52cb81d4f8ee6359b0559f3aa0bcba", // 对 17300001234 进行加密后的密文 "signature": "07bf5c43a0297599ea78ca72e85fea72680eb550f4a3dae4ddb4e8575950a148", "timestamp": "1720669311740" }

响应参数(Body)

参数名称 父节点 参数类型 参数描述
data SingleData 返回值数据
content data void 数据对象
expireSeconds content string 授权码有效时长,默认-1,仅可使用一次
sytoken content string 免登授权码
status int32 状态
code string 错误码
message string 返回信息

响应参数示例

{
    "status": 0,
    "code": "BOOT_0000",
    "message": "SUCCESS",
    "data": {
        "content": {
            "expireSeconds": "-1",
            "sytoken": "SY-5fdin8jp0jmj8i4p"
        }
    }
}

4.2、验证免登授权码

请求地址

【COP平台访问域名】/service/ctp-user/auth/avoid/sycheck

请求方式

GET

请求参数(Query)

参数名称 是否必填 参数类型 参数描述
sytoken TRUE string 免登授权码
例如:SY-o5vtq1z3bnvecwta
syid TRUE string 应用ID,操作步骤中的接入应用-AppKey
例如:cd13f41d30f44d438b05b6588411178f

请求参数示例

【COP平台访问域名】/service/ctp-user/auth/avoid/sycheck?sytoken=SY-o5vtq1z3bnvecwta&syid=cd13f41d30f44d438b05b6588411178f

响应参数(Body)

参数名称 父节点 参数类型 参数描述
data SingleData 返回值数据
content data void 数据对象
sytokenValid content string 授权码是否有效
syidValid content string syid/AppKey/ClientId是否有效
validity content string 授权码剩余可使用次数
status int32 状态
code string 错误码
message string 返回信息

响应参数示例

{
    "status": 0,
    "code": "BOOT_0000",
    "message": "SUCCESS",
    "data": {
        "content": {
            "sytokenValid": true,
            "syidValid": true,
            "validity": "once"
        }
    }
}

5、调用示例

5.1、ApiFox

注意:将一下内容导出文件名为V8开放平台.apifox.json后,使用APIFox导入即可;

{"apifoxProject":"1.0.0","$schema":{"app":"apifox","type":"project","version":"1.2.0"},"info":{"name":"V8开放平台","description":"","mockRule":{"rules":[],"enableSystemRule":true}},"apiCollection":[{"name":"根目录","id":12310375,"auth":{},"parentId":0,"serverId":"","description":"","identityPattern":{"httpApi":{"type":"methodAndPath","bodyType":"","fields":[]}},"preProcessors":[{"id":"inheritProcessors","type":"inheritProcessors","data":{}}],"postProcessors":[{"id":"inheritProcessors","type":"inheritProcessors","data":{}}],"inheritPostProcessors":{},"inheritPreProcessors":{},"items":[{"name":"OAuth免登","id":34341447,"auth":{},"parentId":0,"serverId":"","description":"","identityPattern":{"httpApi":{"type":"inherit","bodyType":""}},"preProcessors":[{"type":"customScript","data":"const appkey = pm.environment.get(\"app-key\");\r\nconst appsecret = pm.environment.get(\"app-secret\");\r\nconst signStr = pm.environment.get(\"signStr\");\r\nvar timestamp = new Date().getTime();\r\nconsole.log('timestamp='+timestamp);\r\nvar moment = require('moment');\r\nvar dUTC = new Date();\r\nvar formatDateTime = moment(dUTC).format('YYYY-MM-DD hh:mm:ss');\r\n\r\nvar body = pm.request.body.raw.toString();\r\nconsole.log('body='+body);\r\n\r\n//AES手机号码加密-开始\r\nvar cryptoJs = require(\"crypto-js\");\r\nconst mobile= \"17301103865\"\r\nconst key = CryptoJS.enc.Utf8.parse(appsecret);\r\nconst iv1 = cryptoJs.enc.Utf8.parse('www.seeyonv8.com');\r\nconst encrypted = cryptoJs.AES.encrypt(mobile, key, {\r\n iv: iv1,\r\n mode: cryptoJs.mode.CBC,\r\n padding: cryptoJs.pad.Pkcs7\r\n}).ciphertext.toString();\r\nconsole.log('encrypted='+encrypted);\r\n//AES手机号码加密-结束\r\n\r\n\r\n//签名计算-开始\r\nvar srcDataArr = [];\r\nsrcDataArr=[appkey, appsecret, encrypted, timestamp];\r\nconsole.log('srcDataArr='+srcDataArr);\r\nsrcDataArr.sort();\r\nconsole.log('srcDataArr='+srcDataArr);\r\nvar srcSign = srcDataArr.join(\"\");\r\nconsole.log('srcSign='+srcSign);\r\nvar signature=CryptoJS.SHA256(srcSign).toString();\r\nconsole.log('signature='+signature);\r\nvar newBody= body.replace('{{clientId}}', appkey)\r\n .replace('{{timestamp}}', timestamp)\r\n .replace('{{dataValue}}', encrypted)\r\n .replace('{{signature}}', signature);\r\n console.log('newBody='+srcSign);\r\npm.request.body.raw=newBody;\r\n//签名计算-结束\r\n\r\n","defaultEnable":true,"enable":true,"id":"VGKlxRoDhjQqm1tEhZ2V8","executionTiming":"prerequest"},{"id":"inheritProcessors","type":"inheritProcessors","data":{}}],"postProcessors":[{"id":"inheritProcessors","type":"inheritProcessors","data":{}}],"inheritPostProcessors":{},"inheritPreProcessors":{},"items":[{"name":"获取Token","api":{"id":"175366254","method":"post","path":"/service/ctp-user/auth/avoid/sytoken","parameters":{"path":[],"header":[]},"auth":{},"commonParameters":{"query":[],"body":[],"cookie":[],"header":[]},"responses":[{"id":"453493410","name":"成功","code":200,"contentType":"eventStream","jsonSchema":{"type":"object","properties":{}}}],"responseExamples":[],"requestBody":{"type":"application/json","parameters":[{"required":false,"description":"","type":"string","id":"OpdZpqBeRe","example":"韩","enable":true,"name":"query"}],"jsonSchema":{"type":"object","properties":{}},"example":"{\r\n \"responseType\": \"create\",\r\n \"clientId\": \"1242bc19f9f6493c9599ba007b9774c9\",\r\n \"dataType\": \"mobile\",\r\n \"dataValue\": {{dataValue}},\r\n \"signature\": {{signature}},\r\n \"timestamp\": {{timestamp}}\r\n}"},"description":"","tags":[],"status":"developing","serverId":"","operationId":"","sourceUrl":"","ordering":50,"cases":[],"mocks":[],"customApiFields":"{}","advancedSettings":{"disabledSystemHeaders":{}},"mockScript":{},"codeSamples":[],"commonResponseStatus":{},"responseChildren":["BLANK.453493410"],"preProcessors":[],"postProcessors":[],"inheritPostProcessors":{},"inheritPreProcessors":{}}},{"name":"验证Token","api":{"id":"175898146","method":"post","path":"/service/ctp-user/auth/avoid/sycheck","parameters":{"query":[{"id":"B0Hw9tnMOB","name":"sytoken","example":"SY-wjls766qqv8zru67","required":false,"description":"","enable":true,"type":"string"},{"id":"ROZGpZw2Aj","name":"syid","example":"1242bc19f9f6493c9599ba007b9774c9","required":false,"description":"","enable":true,"type":"string"}]},"auth":{},"commonParameters":{"query":[],"body":[],"cookie":[],"header":[]},"responses":[{"id":"454439745","name":"成功","code":200,"contentType":"json","jsonSchema":{"type":"object","properties":{}}}],"responseExamples":[],"requestBody":{"type":"none","parameters":[],"jsonSchema":{"type":"object","properties":{}},"example":""},"description":"","tags":[],"status":"developing","serverId":"","operationId":"","sourceUrl":"","ordering":60,"cases":[],"mocks":[],"customApiFields":"{}","advancedSettings":{"disabledSystemHeaders":{}},"mockScript":{},"codeSamples":[],"commonResponseStatus":{},"responseChildren":["BLANK.454439745"],"preProcessors":[],"postProcessors":[],"inheritPostProcessors":{},"inheritPreProcessors":{}}}]}]}],"socketCollection":[],"docCollection":[],"responseCollection":[{"_databaseId":2268233,"name":"Root","type":"root","children":[],"parentId":0,"id":2268233,"items":[]}],"schemaCollection":[],"requestCollection":[{"name":"根目录","children":[],"items":[{"id":1521000,"name":"百度翻译","method":"post","path":"https://fanyi.baidu.com/langdetect","requestBody":{"type":"multipart/form-data","parameters":[{"type":"string","name":"query","sampleValue":"周","value":"周","enable":true}]},"parameters":{},"commonParameters":{"query":[],"body":[],"header":[],"cookie":[]},"preProcessors":[],"postProcessors":[],"auth":{},"advancedSettings":{},"folderId":0}]}],"environments":[],"commonScripts":[],"globalVariables":[{"id":"2272068","variables":[{"name":"url","value":"https://71d0daa2-2ac9-4a23-bc72-17c90b15a409.mock.pstmn.io","description":"","isBindInitial":true,"initialValue":"https://71d0daa2-2ac9-4a23-bc72-17c90b15a409.mock.pstmn.io","isSync":true}]}],"commonParameters":null,"projectSetting":{"id":"1776703","auth":{},"servers":[{"name":"Pre地址前缀","id":"default"}],"gateway":[],"language":"zh-CN","apiStatuses":["developing","testing","released","deprecated"],"mockSettings":{},"preProcessors":[],"postProcessors":[],"advancedSettings":{},"initialDisabledMockIds":[],"cloudMock":{"security":"free","enable":true,"tokenKey":"apifoxToken"}},"customFunctions":[],"projectAssociations":[]}

5.2、Java

package com.nowhere.demo; import cn.hutool.crypto.Mode; import cn.hutool.crypto.Padding; import cn.hutool.crypto.symmetric.AES; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Arrays; import java.util.concurrent.ConcurrentHashMap; public class CipAvoidUtils { private static final Logger log = LoggerFactory.getLogger(CipAvoidUtils.class); private static final String AES_IV = "apaasseeyonv8com"; // 确保这是一个合适的IV字符串 private static final ConcurrentHashMap<String, AES> AESCache = new ConcurrentHashMap<>(); public static void main(String[] args) { String clientId = "1242bc19f9f6493c9599ba007b9774c9"; String appSecret = "93ec877511d24dda8cf86a9d7870f681"; String data = "17300001234"; String timestamp = "1720669311740"; // 加密业务参数 // encryptData = 6d52cb81d4f8ee6359b0559f3aa0bcba String encryptData = CipAvoidUtils.encrypt(data, appSecret); System.out.println("encryptData = " + encryptData); // 数据自然排序 // sortData = 1242bc19f9f6493c9599ba007b9774c917206693117406d52cb81d4f8ee6359b0559f3aa0bcba93ec877511d24dda8cf86a9d7870f681 String sortData = CipAvoidUtils.sort(clientId, appSecret, encryptData, timestamp); System.out.println("sortData = " + sortData); // sha256签名 // sign = 07bf5c43a0297599ea78ca72e85fea72680eb550f4a3dae4ddb4e8575950a148 String signature = CipAvoidUtils.sha256(sortData); System.out.println("signature = " + signature); } public static String sha256(String srcData) { try { MessageDigest digest = MessageDigest.getInstance("SHA-256"); byte[] hash = digest.digest(srcData.getBytes(StandardCharsets.UTF_8)); StringBuilder hexString = new StringBuilder(); for (byte b : hash) { String hex = Integer.toHexString(0xff & b); if (hex.length() == 1) hexString.append('0'); hexString.append(hex); } return hexString.toString(); } catch (NoSuchAlgorithmException e) { throw new RuntimeException("SHA-256 algorithm not found", e); } } /** * 将入参进行自然排序 * * @param clientId 客户端ID,对应应用的AppKey, 不能为null或空 * @param clientSecret 客户端密钥,不能为null或空 * @param data 加密后的数据,不能为null或空 * @param timestampPara 时间戳参数,不能为null或空 * @return 排序后的字符串拼接结果 */ public static String sort(String clientId, String clientSecret, String data, String timestampPara) { // 检查输入参数的有效性 if (StringUtils.isAnyBlank(clientId, clientSecret, data, timestampPara)) { throw new IllegalArgumentException("All parameters must not be null or empty."); } String[] srcDataArr = new String[]{clientId, clientSecret, data, timestampPara}; Arrays.sort(srcDataArr); return String.join("", srcDataArr); } /** * 使用指定密钥加密字符串数据 * * @param data 待加密的数据,不能为null或空 * @param key 加密密钥,接入应用分配的 AppSecret 的值 * @return 加密后的字符串 */ public static String encrypt(String data, String key) { // 检查输入数据和密钥是否有效 if (StringUtils.isAnyBlank(data, key)) { throw new IllegalArgumentException("Data and key must not be null or empty."); } try { // 根据密钥获取AES加密器实例 AES aes = getAes(key); // 使用AES加密器加密数据并以十六进制字符串形式返回 return aes.encryptHex(data); } catch (Exception e) { // 记录加密失败的错误信息 log.error("Encryption failed: {}", e.getMessage(), e); // 将加密过程中捕获的异常包装成RuntimeException重新抛出 throw new RuntimeException("Encryption failed", e); } } /** * 解密字符串 * 使用提供的密钥对加密数据进行解密 * * @param encryptedData 加密后的数据,不能为空 * @param key 加密密钥,接入应用分配的 AppSecret 的值 * @return 解密后的字符串 */ public static String decrypt(String encryptedData, String key) { // 检查输入参数的有效性 if (StringUtils.isAnyBlank(encryptedData, key)) { throw new IllegalArgumentException("Encrypted data and key must not be null or empty."); } try { // 根据密钥获取AES实例 AES aes = getAes(key); // 解密数据 return aes.decryptStr(encryptedData); } catch (Exception e) { // 记录解密失败的错误信息 log.error("Decryption failed: {}", e.getMessage(), e); // 将解密过程中发生的异常包装成运行时异常重新抛出 throw new RuntimeException("Decryption failed", e); } } private static AES getAes(String key) { return AESCache.computeIfAbsent(key, k -> { try { return new AES(Mode.CBC, Padding.PKCS5Padding, k.getBytes(StandardCharsets.UTF_8), AES_IV.getBytes(StandardCharsets.UTF_8)); } catch (Exception e) { log.error("Failed to create AES object for key: {}", k, e); throw new RuntimeException("Failed to create AES object", e); } }); } }

6、注意事项