热土用户介绍
什么是热土用户?
热土用户(RTUser)是由热土工作室开发的一套统一用户认证与授权系统,旨在为开发者提供便捷、安全、可靠的用户登录解决方案。通过热土用户,您的软件可以快速接入统一的用户账号体系,无需自行搭建复杂的用户管理系统。
安全认证
支持state参数防CSRF攻击,Token有效期管理完善
快速接入
提供完善的SDK和详细的接入文档,30分钟内完成基础接入
数据存储
支持用户数据段存储,轻松实现跨设备数据同步
多端支持
支持桌面应用、Web应用等多种应用类型接入
系统架构
所有API请求通过代理层转发,确保安全性和稳定性。代理地址:https://appapi.rtstudio.top/
核心流程
用户发起登录
应用打开授权登录页面,用户输入账号密码
授权确认
用户确认授权,系统生成访问Token
Token回调
Token通过回调返回给应用
获取用户信息
应用使用Token调用API获取用户信息
产品优势
为什么选择热土用户?
统一账号体系
用户只需一个热土账号,即可登录所有接入热土用户的软件,无需在每个软件单独注册账号。
安全可靠
采用OAuth-Like授权流程,支持state参数防CSRF攻击,完善的Token有效期管理机制。
开箱即用
提供完整的SDK、详细的接入文档和示例代码,开发者可快速完成接入,无需搭建复杂的用户系统。
数据同步
通过数据段功能,用户数据可在不同设备间同步,提升用户体验。
可视化管理
开发者可通过仪表盘管理软件Token、查看授权用户、发布Beta测试等。
已验证与未验证软件
| 特性 | 已验证软件 | 未验证软件 |
|---|---|---|
| 登录提示 | 显示软件名称和开发者信息 | 显示风险警告 |
| 数据段功能 | 支持(最多10个) | 不支持 |
| 用户信任度 | 高 | 较低 |
| 仪表盘显示 | 已授权应用 | 未授权应用 |
适用场景
典型的应用场景
桌面应用
各类Windows/Mac桌面软件,通过本地HTTP服务器接收登录回调,实现用户认证。
Web应用
网站和Web应用,通过URL重定向接收Token回调,快速实现用户登录。
开发工具
IDE插件、命令行工具等开发者工具,快速接入用户体系。
游戏与娱乐
游戏、娱乐类软件,利用数据段实现设置保存。
不适合的场景
- 需要高度定制化认证流程的应用
- 对用户数据有特殊合规要求的金融、医疗类应用
- 需要完全自主控制用户数据的应用
接入指南
快速接入流程
开发阶段
集成SDK或自行实现登录流程
- 桌面应用:启动本地HTTP服务器监听回调
- Web应用:配置回调URL
- 调用API获取用户信息
测试阶段
使用测试账号验证登录流程
- 验证Token有效性
- 测试用户信息获取
- 测试数据段功能(如需要)
上线阶段
发布软件,用户可使用热土账号登录
登录URL参数说明
| 参数 | 必填 | 说明 |
|---|---|---|
port |
是 | 桌面应用:本地端口;Web应用:回调网址 |
type |
是 | token(已验证)或 name(未验证) |
v |
是 | 软件Token或软件名称 |
state |
是 | 防CSRF的随机字符串(建议16字节) |
env |
可选 | app(桌面)或 web(Web应用) |
https://apilogin.rtstudio.top/?port=3456&type=token&v=your_token&state=random_state&env=app
https://apilogin.rtstudio.top/?port=yourapp.com/callback&type=token&v=your_token&state=random_state&env=web
登录URL详解
URL基本结构
热土用户的登录授权URL遵循以下基本结构:
https://apilogin.rtstudio.top/?[参数列表]
所有参数通过URL查询字符串(Query String)传递,参数之间使用 & 符号分隔。
完整参数说明
| 参数名 | 必填 | 类型 | 说明 |
|---|---|---|---|
port |
必填 | string |
回调接收地址 桌面应用:本地HTTP服务器端口(如 3456)Web应用:完整的回调URL路径(如 yourapp.com/callback)
|
type |
必填 | string |
软件标识类型loginkey - 推荐方式,使用登录Key标识(保护软件Token)token - 直接使用软件Token标识(已弃用)name - 未验证软件,使用软件名称标识
|
v |
必填 | string |
软件标识值 当 type=loginkey 时:填写通过API获取的登录Key当 type=token 时:填写软件Token(已弃用)当 type=name 时:填写软件名称
|
state |
必填 | string |
防CSRF攻击的随机字符串 建议使用16字节以上的随机值 回调时会原样返回,用于验证请求来源 |
env |
可选 | string |
应用环境类型app - 桌面应用(默认)web - Web应用影响登录页面的UI展示和回调处理方式 |
完整URL示例
推荐方式:使用登录Key(loginkey)
通过 loginkey 方式,您的软件Token不会暴露在URL中,有效防止Token泄露。登录Key有效期仅10分钟,过期自动失效,安全性更高。
https://apilogin.rtstudio.top/?port=3456&type=loginkey&v=YOUR_LOGIN_KEY&state=a1b2c3d4e5f6g7h8&env=app
桌面应用(直接传入Token - 已弃用)
直接在URL中传入软件Token(type=token)的方式已弃用。URL可能被日志、浏览器历史记录等记录,导致Token泄露。请使用上方的 loginkey 方式。
https://apilogin.rtstudio.top/?port=3456&type=token&v=abc123def456&state=a1b2c3d4e5f6g7h8&env=app
桌面应用(未验证软件)
https://apilogin.rtstudio.top/?port=3456&type=name&v=我的测试软件&state=random_state_123&env=app
type=name 时,登录页面会显示风险警告提示,建议申请已验证认证。
Web应用
https://apilogin.rtstudio.top/?port=https://yourapp.com/auth/callback&type=token&v=abc123def456&state=random_state&env=web
port 参数需要填写完整的回调URL,用户登录成功后会重定向到此地址。
回调URL详解
用户完成登录授权后,系统会将结果通过回调URL返回给您的应用。
桌面应用回调
系统会向您的本地HTTP服务器发送GET请求:
http://127.0.0.1:3456/?type=success&v=USER_ACCESS_TOKEN&state=YOUR_STATE
http://127.0.0.1:3456/?type=error&v=错误信息&state=YOUR_STATE
| 回调参数 | 说明 |
|---|---|
type |
success 表示成功,error 表示失败 |
v |
成功时为用户访问Token,失败时为错误信息 |
state |
您在登录URL中传入的state值,用于验证 |
Web应用回调
系统会将用户重定向到您指定的回调URL:
https://yourapp.com/auth/callback?type=success&v=USER_ACCESS_TOKEN&state=YOUR_STATE
您需要在回调页面解析URL参数,获取Token并验证state一致性。
获取登录Key API
为了保护您的软件Token不被暴露在URL中,我们提供了登录Key机制。登录Key是一个10分钟有效的临时凭证,用于替代软件Token出现在登录URL中。
工作流程
在您的后端服务器中,调用API用软件Token换取登录Key。
使用 type=loginkey 和获取到的登录Key构建URL,跳转用户到登录页面。
登录页面会自动通过登录Key查询到对应的软件信息,10分钟后登录Key自动失效。
API详情
GET https://appapi.rtstudio.top/login-token?token=YOUR_SOFTWARE_TOKEN
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
token |
string | 必填 | 您的软件Token |
{
"success": true,
"data": {
"login_key": "a1b2c3d4e5f6...",
"expires_at": "2026-05-23T12:10:00.000Z"
}
}
代码示例
// 获取登录Key
func getLoginKey(softwareToken string) (string, error) {
url := fmt.Sprintf("https://appapi.rtstudio.top/login-token?token=%s", softwareToken)
resp, err := http.Get(url)
if err != nil {
return "", err
}
defer resp.Body.Close()
var result struct {
Success bool `json:"success"`
Data struct {
LoginKey string `json:"login_key"`
ExpiresAt string `json:"expires_at"`
} `json:"data"`
Message string `json:"message"`
}
json.NewDecoder(resp.Body).Decode(&result)
if !result.Success {
return "", fmt.Errorf(result.Message)
}
return result.Data.LoginKey, nil
}
// 使用登录Key构建登录URL
func buildLoginURL(softwareToken, port, state string) (string, error) {
loginKey, err := getLoginKey(softwareToken)
if err != nil {
return "", err
}
return fmt.Sprintf("https://apilogin.rtstudio.top/?port=%s&type=loginkey&v=%s&state=%s", port, loginKey, state), nil
}
import requests
def get_login_key(software_token):
"""获取登录Key"""
resp = requests.get(
'https://appapi.rtstudio.top/login-token',
params={'token': software_token}
)
data = resp.json()
if data['success']:
return data['data']['login_key']
raise Exception(data['message'])
def build_login_url(software_token, port, state):
"""构建安全的登录URL"""
login_key = get_login_key(software_token)
return f'https://apilogin.rtstudio.top/?port={port}&type=loginkey&v={login_key}&state={state}'
- 软件Token仅在服务器端使用,不会出现在浏览器URL中
- 登录Key 10分钟后自动失效,即使泄露也无法长期使用
- 每次登录使用不同的登录Key,防止重放攻击
State参数安全说明
state 参数是防止CSRF(跨站请求伪造)攻击的关键机制,请务必正确使用:
- 每次登录请求生成新的随机state值
- 建议使用16字节以上的随机字符串
- 在接收回调时,验证返回的state与发送时一致
- 验证失败时应拒绝此次登录
State生成示例
func generateState() (string, error) {
b := make([]byte, 16)
_, err := rand.Read(b)
if err != nil {
return "", err
}
return hex.EncodeToString(b), nil
}
import secrets
def generate_state():
return secrets.token_hex(16) # 生成32字符的随机字符串
Token使用说明
获取到用户访问Token后,您可以:
- 调用
/verify-access-token验证Token有效性 - 调用
/get-user-by-token获取用户基本信息 - 调用数据段相关API(仅已验证软件)
URL编码注意事项
当参数值包含特殊字符时,需要进行URL编码:
| 场景 | 原始值 | 编码后 |
|---|---|---|
| 软件名称含中文 | 我的测试软件 | %E6%88%91%E7%9A%84%E6%B5%8B%E8%AF%95%E8%BD%AF%E4%BD |
| 回调URL含特殊字符 | app.com/auth?type=login | app.com%2Fauth%3Ftype%3Dlogin |
const softwareName = '我的测试软件';
const encodedName = encodeURIComponent(softwareName);
const loginUrl = `https://apilogin.rtstudio.top/?port=3456&type=name&v=${encodedName}&state=random_state`;
开发准备
获取软件Token
- 软件名称
- 开发者信息
- 软件功能描述
- 软件截图(如有)
- 下载链接(如有)
发送邮件至 miles@rtstu.com
邮件主题:软件认证申请 - [软件名称]
审核周期:1-14个工作日
审核通过后将收到包含软件Token的邮件
环境要求
| 应用类型 | 要求 |
|---|---|
| 桌面应用 | 能够启动本地HTTP服务器、打开浏览器 |
| Web应用 | 能够处理URL重定向和查询参数 |
API地址
API概览
API列表
| 接口 | 方法 | 说明 | 权限要求 |
|---|---|---|---|
/verify-software-token |
POST | 验证软件Token | 无 |
/verify-access-token |
POST | 验证用户Token | 无 |
/get-user-by-token |
POST | 获取用户信息 | 有效Token |
/data-segments |
POST | 创建数据段 | 已验证软件 |
/data-segments |
PUT | 修改数据段 | 已验证软件 |
/data-segments |
DELETE | 删除数据段 | 已验证软件 |
/data-segments/read |
POST | 读取数据段 | 已验证软件 |
通用响应格式
{
"success": true,
"message": "操作成功",
"data": {
// 具体数据
}
}
{
"success": false,
"message": "错误描述"
}
认证相关API
验证软件Token
请求参数
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
token |
string | 是 | 软件Token |
响应示例
{
"success": true,
"data": {
"id": 1,
"name": "测试软件",
"developer": "测试开发者"
}
}
验证用户Token
请求参数
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
token |
string | 是 | 用户授权Token |
响应示例
{
"success": true,
"data": {
"id": 1,
"user_id": 1,
"software_id": 1,
"expires_at": "2027-03-22T12:00:00Z"
}
}
获取用户信息
请求参数
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
token |
string | 是 | 用户授权Token |
响应示例
{
"success": true,
"data": {
"id": 1,
"uuid": "...",
"username": "testuser",
"email": "test@example.com",
"email_verified": true,
"is_beta_user": false
}
}
数据段相关API
数据段功能设计用于存储用户偏好设置、软件配置、非关键业务数据等非敏感信息。
严禁使用数据段存储以下类型的数据:
- VIP/会员状态、订阅权限等核心业务判定数据
- 付费信息、充值记录、消费数据
- 用户等级、积分、特权等可影响业务逻辑的数据
- 密码、密钥、证书等安全凭证
- 个人身份信息(身份证、银行卡等)
原因:数据段存储在用户可访问的系统中,用户可能查看或修改这些数据。将VIP状态等关键数据存储在数据段中,将导致用户可以自行"解锁"VIP功能,造成业务风险。
正确做法:VIP状态、付费信息等核心业务数据应由您的后端服务器独立管理,通过您自己的API进行验证,不应依赖热土用户系统的数据段功能。
数据段说明
创建数据段
请求参数
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
email |
string | 是 | 用户邮箱 |
token |
string | 是 | 用户授权Token |
name |
string | 是 | 数据段名称(唯一标识) |
content |
string | 是 | 数据段内容(JSON字符串) |
响应示例
{
"success": true,
"message": "数据段创建成功",
"data": {
"id": 1,
"name": "user_settings",
"content": "{\"theme\": \"dark\"}"
}
}
读取数据段
请求参数
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
email |
string | 是 | 用户邮箱 |
token |
string | 是 | 用户授权Token |
name |
string | 是 | 数据段名称 |
响应示例
{
"success": true,
"data": {
"id": 1,
"name": "user_settings",
"content": "{\"theme\": \"dark\", \"language\": \"zh-CN\"}",
"created_at": "2026-03-22T12:00:00Z",
"updated_at": "2026-03-22T12:00:00Z"
}
}
修改数据段
请求参数
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
email |
string | 是 | 用户邮箱 |
token |
string | 是 | 用户授权Token |
name |
string | 是 | 数据段名称 |
content |
string | 是 | 新的数据段内容 |
删除数据段
请求参数
| 参数名 | 类型 | 必填 | 说明 |
|---|---|---|---|
email |
string | 是 | 用户邮箱 |
token |
string | 是 | 用户授权Token |
name |
string | 是 | 数据段名称 |
适合存储的数据示例
✅ 用户偏好设置
{"theme": "dark", "language": "zh-CN", "fontSize": 14}
✅ 软件窗口布局
{"width": 800, "height": 600, "x": 100, "y": 100}
✅ 游戏非关键数据
{"ImageQuality": 5, "controls": "default"}
不适合存储的数据示例
❌ VIP状态(错误示例)
{"isVIP": true, "vipLevel": 3, "vipExpires": "2027-12-31"}
⚠️ 用户可以自行修改为VIP状态,绕过付费验证
❌ 用户积分(错误示例)
{"points": 9999, "level": 10, "achievements": ["all"]}
⚠️ 用户可以自行修改积分和等级
Go SDK
完整登录流程实现
以下是完整的Go语言SDK实现代码,包含登录Key获取、登录流程、Token验证、用户信息获取等功能。
此SDK默认使用 loginkey 方式保护软件Token。传入软件Token时,SDK会自动获取登录Key,确保Token不出现在浏览器URL中。
// main.go
// This program implements a login flow for "Retu Studio" users.
// It starts a local HTTP server, opens a browser for authentication,
// receives an access token via callback, and verifies the token.
package main
import (
"bytes"
"crypto/rand"
"encoding/hex"
"encoding/json"
"flag"
"fmt"
"io"
"log"
"net"
"net/http"
"net/url"
"os/exec"
"runtime"
"time"
)
// 配置常量 / Configuration constants
const apiBaseURL = "https://appapi.rtstudio.top"
const loginBaseURL = "https://apilogin.rtstudio.top"
// 命令行参数 / Command line arguments
var (
loginType string // loginkey, token 或 name
value string // 登录key, 软件token或软件名称
)
// init 解析命令行参数 / Parse command line flags
func init() {
flag.StringVar(&loginType, "type", "", "登录类型: loginkey (推荐), token (已弃用), name 或 get_login_token (仅获取登录Key) / Login type: loginkey (recommended), token (deprecated), name or get_login_token (only get login key)")
flag.StringVar(&value, "v", "", "登录key/软件token/软件名称 / Login key, software token or software name")
}
// loginResponse 登录接口返回的结构 / Structure of login API response
type loginResponse struct {
Type string `json:"type"`
V string `json:"v"`
State string `json:"state"`
}
// verifyResponse token验证接口返回的结构 / Structure of token verification API response
type verifyResponse struct {
Success bool `json:"success"`
Data any `json:"data"`
Message string `json:"message"`
}
// loginKeyResponse 获取登录Key接口返回的结构 / Structure of login key API response
type loginKeyResponse struct {
Success bool `json:"success"`
Data struct {
LoginKey string `json:"login_key"`
ExpiresAt string `json:"expires_at"`
} `json:"data"`
Message string `json:"message"`
}
func main() {
flag.Parse()
// 验证参数 / Validate arguments
if loginType != "loginkey" && loginType != "token" && loginType != "name" && loginType != "get_login_token" {
log.Fatal("错误: -type 必须是 'loginkey', 'token', 'name' 或 'get_login_token' / Error: -type must be 'loginkey', 'token', 'name' or 'get_login_token'")
}
if value == "" {
log.Fatal("错误: -v 参数不能为空 / Error: -v argument cannot be empty")
}
// 如果是get_login_token模式,仅获取并输出登录Key
if loginType == "get_login_token" {
loginKey, err := getLoginKey(value)
if err != nil {
log.Fatalf("获取登录Key失败: %v / Failed to get login key: %v", err)
}
// 输出登录Key到stdout
fmt.Print(loginKey)
return
}
// 如果是token类型,提示已弃用 / Warn if using deprecated token type
if loginType == "token" {
log.Println("⚠️ 警告: type=token 已弃用,建议使用 type=loginkey 以保护软件Token安全")
log.Println("⚠️ Warning: type=token is deprecated, use type=loginkey to protect your software token")
}
// loginkey模式直接使用传入的值,不再自动获取
// loginkey mode uses the passed value directly, no auto-fetch
actualLoginType := loginType
actualValue := value
// 启动登录流程 / Start the login flow
token, err := startLoginFlow(actualLoginType, actualValue)
if err != nil {
log.Fatalf("登录失败: %v / Login failed: %v", err)
}
// 验证token有效性 / Verify the obtained token
if err := verifyToken(token); err != nil {
log.Fatalf("token验证失败: %v / Token verification failed: %v", err)
}
// 获取用户信息 / Get user info
userResult, err := getUserByToken(token)
if err != nil {
log.Fatalf("获取用户信息失败: %v / Failed to get user info: %v", err)
}
if userResult.Success {
log.Printf("登录成功,用户: %+v / Login successful, user: %+v", userResult.Data)
}
// 输出token到stdout (供父进程通过管道读取) / Output token to stdout for parent process to read
fmt.Print(token)
}
// isLikelySoftwareToken 判断是否可能是软件Token(64位hex字符串)
// Checks if the value looks like a software token (64-char hex string)
func isLikelySoftwareToken(v string) bool {
if len(v) != 64 {
return false
}
_, err := hex.DecodeString(v)
return err == nil
}
// getLoginKey 通过软件Token获取登录Key
// Get a login key by exchanging a software token (10-minute validity)
func getLoginKey(softwareToken string) (string, error) {
reqURL := fmt.Sprintf("%s/login-token?token=%s", apiBaseURL, url.QueryEscape(softwareToken))
resp, err := http.Get(reqURL)
if err != nil {
return "", fmt.Errorf("请求失败: %v / Request failed: %v", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("读取响应失败: %v / Failed to read response: %v", err)
}
// 输出原始响应用于调试 / Output raw response for debugging
log.Printf("API响应 (status %d): %s", resp.StatusCode, string(body))
var result loginKeyResponse
if err := json.Unmarshal(body, &result); err != nil {
return "", fmt.Errorf("解析响应失败: %v / Failed to parse response: %v", err)
}
if !result.Success {
return "", fmt.Errorf("%s", result.Message)
}
return result.Data.LoginKey, nil
}
// startLoginFlow 启动完整的登录流程 / Starts the complete login flow
// It generates a random state, finds an available port, starts a local HTTP server,
// opens the browser with the login URL, and waits for the callback.
func startLoginFlow(loginType, value string) (string, error) {
// 1. 生成state参数 (16字节随机数,防CSRF) / Generate state parameter (16 random bytes for CSRF protection)
state, err := generateState()
if err != nil {
return "", fmt.Errorf("生成state失败: %v / Failed to generate state: %v", err)
}
// 2. 寻找可用端口并启动本地服务器 / Find an available port and start the local server
port, err := findAvailablePort()
if err != nil {
return "", fmt.Errorf("无法找到可用端口: %v / Cannot find available port: %v", err)
}
// 创建channel用于接收token或错误 / Create channels to receive token or error
tokenChan := make(chan string, 1)
errChan := make(chan error, 1)
// 启动HTTP服务器 / Start HTTP server
server := &http.Server{
Addr: fmt.Sprintf(":%d", port),
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
}
// 注册根路由处理器 / Register root route handler
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
// 只处理GET请求 / Handle only GET requests
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
query := r.URL.Query()
callbackType := query.Get("type")
callbackValue := query.Get("v")
callbackState := query.Get("state")
if callbackType == "success" {
// 验证state参数,防止CSRF攻击 / Verify state parameter to prevent CSRF
if callbackState != state {
errChan <- fmt.Errorf("state参数验证失败,可能是CSRF攻击 / State parameter mismatch, possible CSRF attack")
http.Error(w, "state验证失败 / State verification failed", http.StatusBadRequest)
return
}
tokenChan <- callbackValue
// 返回成功页面 / Return success page
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusOK)
w.Write([]byte(`登录成功 ✅ 登录完成
请关闭此窗口
`))
// 延迟关闭服务器,确保响应发送完成 / Delay server shutdown to ensure response is sent
go func() {
time.Sleep(500 * time.Millisecond)
server.Close()
}()
} else if callbackType == "error" {
errChan <- fmt.Errorf("登录失败: %s / Login failed: %s", callbackValue)
http.Error(w, "登录失败 / Login failed", http.StatusBadRequest)
go func() {
time.Sleep(500 * time.Millisecond)
server.Close()
}()
} else {
http.Error(w, "无效的回调类型 / Invalid callback type", http.StatusBadRequest)
}
})
// 在goroutine中启动服务器 / Start server in a goroutine
go func() {
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
errChan <- fmt.Errorf("HTTP服务器错误: %v / HTTP server error: %v", err)
}
}()
// 等待服务器启动完成 / Wait for server to start
time.Sleep(100 * time.Millisecond)
// 3. 构造登录URL并打开浏览器 / Build login URL and open browser
loginURL := fmt.Sprintf("%s/?port=%d&type=%s&v=%s&state=%s&env=app",
loginBaseURL, port, loginType, url.QueryEscape(value), state)
log.Printf("构造的登录URL: %s", loginURL)
if err := openBrowser(loginURL); err != nil {
return "", fmt.Errorf("打开浏览器失败: %v / Failed to open browser: %v", err)
}
log.Printf("已打开浏览器,请登录 / Browser opened, please log in")
log.Printf("等待登录回调... / Waiting for login callback...")
// 等待登录结果 / Wait for login result
select {
case token := <-tokenChan:
return token, nil
case err := <-errChan:
return "", err
case <-time.After(5 * time.Minute):
server.Close()
return "", fmt.Errorf("登录超时(5分钟) / Login timeout (5 minutes)")
}
}
// generateState 生成16字节随机数作为state参数 / Generate 16 random bytes as state parameter
func generateState() (string, error) {
bytes := make([]byte, 16)
if _, err := rand.Read(bytes); err != nil {
return "", err
}
return hex.EncodeToString(bytes), nil
}
// findAvailablePort 寻找一个可用的TCP端口 / Find an available TCP port
// It binds to port 0 which lets the OS choose a free port, then returns that port number.
func findAvailablePort() (int, error) {
listener, err := net.Listen("tcp", ":0")
if err != nil {
return 0, err
}
defer listener.Close()
return listener.Addr().(*net.TCPAddr).Port, nil
}
// openBrowser 打开系统默认浏览器 / Open the system default browser
func openBrowser(url string) error {
switch runtime.GOOS {
case "windows":
return exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start()
case "darwin":
return exec.Command("open", url).Start()
default:
return exec.Command("xdg-open", url).Start()
}
}
// verifyToken 验证token有效性 / Verify the validity of the token by calling the API
func verifyToken(token string) error {
reqBody := map[string]string{"token": token}
jsonBody, err := json.Marshal(reqBody)
if err != nil {
return err
}
resp, err := http.Post(apiBaseURL+"/verify-access-token", "application/json", bytes.NewBuffer(jsonBody))
if err != nil {
return fmt.Errorf("API请求失败: %v / API request failed: %v", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
var result verifyResponse
if err := json.Unmarshal(body, &result); err != nil {
return err
}
if !result.Success {
return fmt.Errorf("token无效: %s / Invalid token: %s", result.Message)
}
return nil
}
// getUserByToken 通过token获取用户信息 / Get user info by access token
func getUserByToken(token string) (*verifyResponse, error) {
reqBody := map[string]string{"token": token}
jsonBody, _ := json.Marshal(reqBody)
resp, err := http.Post(apiBaseURL+"/get-user-by-token", "application/json", bytes.NewBuffer(jsonBody))
if err != nil {
return nil, fmt.Errorf("API请求失败: %v / API request failed: %v", err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
var result verifyResponse
json.Unmarshal(body, &result)
return &result, nil
}
使用方法
# 已验证软件
go run main.go -type token -v your_software_token
# 未验证软件
go run main.go -type name -v your_software_name
其他语言
Python示例
import requests
import webbrowser
import http.server
import socketserver
import threading
import secrets
import time
API_BASE_URL = "https://appapi.rtstudio.top"
def get_available_port():
with socketserver.TCPServer(("localhost", 0), None) as s:
return s.server_address[1]
def login(login_type, value):
state = secrets.token_hex(16)
port = get_available_port()
token = None
class Handler(http.server.BaseHTTPRequestHandler):
def do_GET(self):
query = parse_qs(urlparse(self.path).query)
if query.get('type') == ['success'] and query.get('state') == [state]:
token = query.get('v', [None])[0]
self.send_response(200)
self.end_headers()
self.wfile.write(b'登录完成
')
server = socketserver.TCPServer(("localhost", port), Handler)
thread = threading.Thread(target=server.serve_forever)
thread.start()
url = f"https://apilogin.rtstudio.top/?port={port}&type={login_type}&v={value}&state={state}&env=app"
webbrowser.open(url)
time.sleep(30) # 等待登录
server.shutdown()
return token
def get_user_by_token(token):
resp = requests.post(f"{API_BASE_URL}/get-user-by-token", json={"token": token})
return resp.json()
更多SDK
更多语言的SDK正在开发中,如有需要请联系我们获取。
常见问题
软件Token用于标识软件身份,由热土工作室颁发;用户Token是用户登录后生成的访问令牌,用于获取用户信息。
用户Token有效期为1年,过期后需重新登录授权。
发送邮件至 miles@rtstu.com,包含软件名称、开发者信息、功能描述等,审核周期1-14个工作日。
不可以。数据段不适合存储VIP状态、付费信息等核心业务数据,因为这些数据用户可能查看或修改,会导致业务风险。VIP状态等应由您的后端独立管理。
state参数用于防止CSRF攻击,建议使用16字节以上的随机字符串,回调时验证state一致性。
联系我们
成为开发者
- 自主创建和管理软件Token
- 发布Beta测试软件
- 查看授权用户数据统计
- 使用开发者控制台(id.rtstu.com)
通过QQ群申请(较慢)
此方式需要等待群主在线回复,处理时间可能较长,建议使用上方表格方式。
加入热土工作室QQ群
私信群主申请加入
入群后,私信群主表达您希望成为开发者的意愿。
必要条件
个人开发者
- 拥有一个热土用户账号
- 拥有对外展示作品的公开渠道
- 支持的渠道:X(Twitter)、Bilibili、GitHub、个人网站、博客等
- 不支持:仅限微信朋友圈、私密社群等非公开渠道
团队/组织
- 满足个人开发者的所有条件
- 拥有团队/组织的官方账号(非个人账号)
- 例如:Bilibili团队官方账号、GitHub组织账号等
- 有明确的团队负责人作为主要联系人
提交材料并确认协议
向群主提交以下材料:
- 您的热土用户账号(邮箱或用户名)
- 展示渠道链接(如Bilibili主页、GitHub主页等)
- 团队需额外提供:团队名称、团队官方账号链接、团队负责人信息
材料审核通过后,需确认开发者协议,即可获得开发者权限。
登录开发者控制台
完成以上步骤后,访问 id.rtstu.com 并选择「开发者」,即可开始管理您的软件。
软件认证申请(邮箱方式)
软件认证申请 - [您的软件名称]
- 软件名称
- 开发者/团队名称
- 软件功能描述(简要)
- 软件截图(如有)
- 下载链接(如有)
miles@rtstu.com
1-3个工作日,审核通过后将收到包含软件Token的邮件回复。