在兔小巢中传递自己的登录态

tips 说明

默认登录态说明

我们已经在 APP 中默认实现了用户无登录态的接入,或称作匿名登录态、游客态;

在 PC WEB 中默认实现了用户QQ/微信登录。


匿名登录态说明

在 APP 中无登录态时用户无法查看自己的反馈历史,管理员不能在移动端直接管理帖子。取而代之的是用户将获得一个匿名的登录态,系统会给用户分配随机的头像及昵称。匿名态发帖与普通用户发帖并无二异,只是用户无法在反馈历史中追溯到匿名状态下发送过的历史帖子。


为什么需要传递自己的登录态

在 APP 中如果需要用户能够查看以往发送过的帖子或是接收管理员回复的消息提醒,那么你就需要传递一些特定的参数来识别用户身份。具体的方式是通过 HTTP请求传递参数来创造一个登录态。


PC WEB类产品如果想使用自己的用户登录态,也可以使用如下方法。

1. 传递登录态的参数说明

与无登录态的接入不同的是,带登录态的接入需要在请求中携带三个参数,分别代表了用户id、昵称和头像。

参数名 类型 是否必填 说明
openid string 用户唯一标识,由接入方生成
nickname string 用户昵称
avatar string 用户头像,一般是图片链接 必须要支持https

warn 重要提示

以上三个必填参数都不能为空。 如果少了其中任何一个,登录态的构建的都会失败。登录态构建失败不会影响跳转,但会直接以匿名状态登录。

为了方便兼容不同的账号体系,兔小巢只将 openid 作为用户身份的唯一标识,故在构造 openid 时需要考虑其唯一性。 同时为了防止被破解,openid 需要有一定的复杂度。
为了保证最终的视觉效果,用户昵称不超过8个字,用户头像仅支持 pngjpg 格式。

2. 使用传参的方式创造登录态

产品可以配置是否允许传递第三方登录态。

设置入口
配置详情

如果允许传递第三方登录态,可以选择以下方式进行传参。

2.1 明文传递

NOTICE: 明文传递容易被伪造和窃取。安全系数低。

参数名 类型 是否必填 说明
openid string 用户唯一标识,由接入方生成
nickname string 用户昵称
avatar string 用户头像,一般是图片链接。queryString 传输时需要进行 url_encode, 避免截断。 必须要支持https


Step1. 产品设置中允许使用第三方登录态的明文传递。
Step2. 将以上参数拼接成 queryString 格式带入请求体。 或者你可以用任意能够构造 HTTP POST 的工具来实现一样的效果,以下为参考代码:

传递登录态只要使用 Tucao#set 额外传递上述的三个参数即可。

const Tucao = requirePlugin('tucao');
// ...
const userInfo = {
    avatar: `https://txc.qq.com/static/desktop/img/products/def-product-logo.png`,
    nickname: '佚名',
    openid: '自己生成'
};
// ...
Tucao.init(this, {productId:1368, ...userInfo}).go();
NSString *openid = @"tucao_123";// 用户ID
NSString *nickname = @"tucao_test";// 昵称
NSString *avatar = @"https://txc.qq.com/static/desktop/img/products/def-product-logo.png";  // user avatar url


// 获得 webview url,请注意url单词是product而不是products,products是旧版本的参数,用错地址将不能成功提交
NSString *appUrl = @"https://support.qq.com/product/1221"; // 把1221数字换成你的产品ID,否则会不成功
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:appUrl]];

// 设置请求类型为 POST
[request setHTTPMethod: @"POST"];
[request setValue:@"application/x-www-form-urlencoded" forHTTPHeaderField:@"Content-Type"];

// 设置请求体
NSString *body = [NSString stringWithFormat:@"nickname=%@&avatar=%@&openid=%@", nickname, avatar, openid];
[request setHTTPBody:[body dataUsingEncoding:NSUTF8StringEncoding]];
webView = [[UIWebView alloc] initWithFrame:CGRectMake(0, 0, 320, 480)];

[self.view addSubview: webView];
[webView loadRequest:request];
webView = (WebView) findViewById(R.id.webview1);
webView.getSettings().setJavaScriptEnabled(true);
webView.getSettings().setDomStorageEnabled(true); // 这个要加上

String openid = "tucao_123"; // 用户的openid
String nickname = "tucao_test"; // 用户的nickname
String headimgurl = "https://txc.qq.com/static/desktop/img/products/def-product-logo.png";  // 用户的头像url


/* 获得 webview url,请注意url单词是product而不是products,products是旧版本的参数,用错地址将不能成功提交 */
String url = "https://support.qq.com/product/1221"; // 把1221数字换成你的产品ID,否则会不成功
/* 准备post参数 */
String postData = "nickname=" + nickname + "&avatar="+ headimgurl + "&openid=" + openid;
webView.postUrl(url, postData.getBytes());
<!-- 需要使用 form 表单传递的登录态,请注意url单词是product而不是products,products是旧版本的参数,用错地址将不能成功提交 -->
<form method="post" action="https://support.qq.com/product/1221"> <!--把1221数字换成你的产品ID,否则会不成功-->
    <input type="hidden"  name="openid" value="tucao_123">
    <input type="hidden"  name="nickname" value="tucao_test">
    <input type="hidden"  name="avatar" value="https://txc.qq.com/static/desktop/img/products/def-product-logo.png">
    <button type='submit'>
</form>

也可以使用我们提供的工具包 tucao.js,然后调用 Tucao.request()即可。

以下为示例代码:

var data = {
    // nickname,avatar,openid 必填
    "nickname": "tucao_test", 
    "avatar": "https://txc.qq.com/static/desktop/img/products/def-product-logo.png", 
    "openid": "tucao_123",
},
productId = 1221; // 把1221数字换成你的产品ID,否则会不成功

Tucao.request(productId, data);

2.2 明文签名传递

NOTICE:产品密钥需严格保密,openid从用户登录态中获取,签名生成过程建议放在服务端实现。安全系数较高。

参数名 类型 是否必填 说明
openid string 用户唯一标识,由接入方生成
nickname string 用户昵称
avatar string 用户头像,一般是图片链接。queryString 传输时需要进行 url_encode, 避免截断。 必须要支持https
user_signature string 用户签名 :md5(str(openid)+str(nickname)+str(avatar)+str(产品密钥)) 。 * 32 字符的十六进制数形式 *


Step1. 产品设置中获取 产品密钥,并确认允许使用第三方登录态的明文签名传递。
Step2. 使用同之前的明文传递,增加了一个签名参数。防止被伪造。

2.3 密文传递

NOTICE: 产品密钥需严格保密,openid从用户登录态中获取,密文生成过程建议放在服务端实现。安全系数高。

2.3.1 操作流程


Step1. 产品设置中获取 产品密钥产品ID,并确认允许使用第三方登录态的密文传递。


Step2. 加密第三方登录态的信息,获取密文字符串。

加密方案中的第三方登录信息应包括以下字段:

参数名 类型 是否必填 说明
openid string 用户唯一标识,由接入方生成
nickname string 用户昵称
avatar string 用户头像,一般是图片链接 必须要支持https
nonce string 随机值
expired_at string 过期时间的 unix timestamp, 10位数字格式的字符串 。设置为"0"则不过期。

基于 AES 加解密算法来实现,具体如下:

  1. 将包含上述表格字段信息的对象或数组,用 json_encode 生成一个待加密的字符串;
  2. 用 AES 算法进行加密,得到一个密文;
    1. 算法模式: AES-128-CBC;
    2. 算法密钥key``:产品密钥`, 不足16位则右侧用"="补足;
    3. 算法初始向量iv : 产品ID+产品密钥, 不足16位则右侧用"="补足;
    4. 算法选用 CBC 模式,数据采用 PKCS#7 填充:K 为秘钥字节数,Buf 为待加密的内容,N 为其字节数。Buf 需要被填充为 K 的整数倍。在 Buf 的尾部填充(K - N%K)个字节,每个字节的内容 是(K - N%K)。
  3. 密文编码,便于URL传参
    1. 用 base64 进行编码;
    2. 编码后的密文进行替换: "+"替换为"-","/"替换为"_",删除字符串右侧的"=";


Step3. 传递加密的登录态信息。

同明文传递一样,可以通过构造 HTTP 请求来传递加密的登录态信息。

参数名 类型 是否必填 说明
user_data string 上一步获取的密文字符串

将以上参数拼接成 queryString 格式带入请求体。 或者你可以用任意能够构造 HTTP POST 的工具来实现一样的效果。

2.3.2 示例-生成密文

  • PHP 代码示例 - 使用OpenSSL扩展实现
# 参数配置
$productId = '1'; // 产品ID
$productPrivateKey = 'hellotxc'; // 产品密钥
$data = [
    'openid' => 'txc1234567890',
    'nickname' => '晋北',
    'avatar' => 'https://txc.gtimg.com/static/v2/img/avatar/1.svg',
    'nonce' => 'randomstr',
    'expired_at' => '0',
];

$algo = 'AES-128-CBC';
$key = str_pad($productPrivateKey, 16, '=', STR_PAD_RIGHT);
$iv = str_pad($productId.$productPrivateKey, 16, '=', STR_PAD_RIGHT);

# 获取密文
$dataStr = json_encode($data);
## 算法选用 CBC 模式,数据采用 PKCS#7 填充
$blockSize = openssl_cipher_iv_length($algo);
$paddingSize = $blockSize - (strlen($dataStr) % $blockSize);
$dataStr .= str_repeat(chr($paddingSize), $paddingSize);
##
$dataStrEncrypted = openssl_encrypt($dataStr, $algo, $key, OPENSSL_RAW_DATA|OPENSSL_ZERO_PADDING, $iv);

# 对密文进行编码
$dataStrEncryptedEncoded = rtrim(strtr(base64_encode($dataStrEncrypted), '+/', '-_'), '=');
#
echo $dataStrEncryptedEncoded, PHP_EOL;
  • Python 代码示例
#-*- encoding:utf-8 -*-

import hashlib
import json
from Crypto.Cipher import AES
import base64

# 参数配置
product_id = '1'
product_private_key = 'hellotxc'
data = {
    'openid': 'txc1234567890',
    'nickname': '晋北',
    'avatar': 'https://txc.gtimg.com/static/v2/img/avatar/1.svg',
    'nonce': 'randomstr',
    'expired_at': '0',
}
algo = 'AES-128-CBC'
key = product_private_key.ljust(16, '=')
iv = (product_id + product_private_key).ljust(16, '=')

# 获取密文
json_str = json.dumps(data, separators=(',', ':'))
padding_size = AES.block_size - len(json_str) % AES.block_size
padding = bytes([padding_size] * padding_size)
data_str_padded = json_str.encode('utf8') + padding

cipher = AES.new(key.encode('utf8'), AES.MODE_CBC, iv.encode('utf8'))
ciphertext = cipher.encrypt(data_str_padded)

# 对密文进行编码
dataStrEncryptedEncoded = base64.urlsafe_b64encode(ciphertext).rstrip(b'=').decode('utf8')
#
print(dataStrEncryptedEncoded)
  • Go 代码示例
package main

import (
    "bytes"
    "crypto/aes"
    "crypto/cipher"
    "encoding/base64"
    "encoding/json"
    "fmt"
    "strings"
)

type UserInfo struct {
    OpenID    string `json:"openid"`
    Nickname  string `json:"nickname"`
    Avatar    string `json:"avatar"`
    Nonce     string `json:"nonce"`
    ExpiredAt string `json:"expired_at"`
}

func pkcs7Pad(data []byte, block_size int) []byte {
    padding := block_size - (len(data) % block_size)
    padtext := bytes.Repeat([]byte{byte(padding)}, padding)
    return append(data, padtext...)
}

func aes128CBCEncrypt(data []byte, key []byte, iv []byte) []byte {
    block, err := aes.NewCipher(key)
    if err != nil {
        panic(err)
    }
    block_size := block.BlockSize()
    padded := pkcs7Pad(data, block_size)
    ciphertext := make([]byte, len(padded))
    mode := cipher.NewCBCEncrypter(block, iv)
    mode.CryptBlocks(ciphertext, padded)
    return ciphertext
}

func embeddingKey(key string) []byte {
    var builder strings.Builder
    builder.WriteString(key)
    for builder.Len() < 16 {
        builder.WriteString("=")
    }
    return []byte(builder.String())
}

func main() {
    // 参数配置
    productId := "1"
    productPrivateKey := "hellotxc"
    userInfo := UserInfo{
        OpenID:    "txc1234567890",
        Nickname:  "晋北",
        Avatar:    "https://txc.gtimg.com/static/v2/img/avatar/1.svg",
        Nonce:     "randomstr",
        ExpiredAt: "0",
    }
    key := embeddingKey(productPrivateKey)
    iv := embeddingKey(productId + productPrivateKey)

    // 获取密文
    data, err := json.Marshal(userInfo)
    if err != nil {
        panic(err)
    }
    encrypted_data := aes128CBCEncrypt(data, key, iv)

    // 对密文进行编码
    base64_encoded := base64.RawStdEncoding.EncodeToString(encrypted_data)
    result := strings.ReplaceAll(strings.ReplaceAll(base64_encoded, "+", "-"), "/", "_")
    result = strings.TrimRightFunc(result, func(r rune) bool { return r == '=' })
    fmt.Println(result)
}

3. 测试

构造对应的登录态参数,用管理员身份访问 https://txc.qq.com/api/v1/[产品ID]/debug/valid_other_user_info? ,可查看登录态构建是否成功。

参数名 类型 说明
status int 0-成功;1-失败
message string 失败信息

warn 重要提示

传递的登录态不能成为管理员。