#免登 description: 三方异构系统免登进入V8

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

# 1、概述

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

# 2、集成导图

1744113840811.png

# 3、集成步骤

# 3.1、申请APPKey和AppSecret

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

1744113850308.png

1744113855269.png

1744113860052.png

# 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、注意事项

编撰人:pengfx