栖岛登录对接完整指南
本文档面向新手开发者,帮助你快速在自己的 APP 或小程序中集成栖岛 OAuth2.0 登录功能。
📖 目录
什么是栖岛登录
栖岛登录是基于 OAuth2.0 标准协议 的第三方授权登录服务。用户可以使用栖岛账号快速登录你的应用,无需重新注册。
优势:
- 🔐 安全可靠的授权机制
- 🚀 快速集成,减少开发成本
- 👤 获取用户基本信息(头像、昵称等)
- 🔄 支持 Token 刷新机制
整体流程概览
┌─────────────┐ ┌─────────────────┐ ┌─────────────┐
│ 你的APP │ │ 栖岛认证服务 │ │ 你的后端 │
└──────┬──────┘ └────────┬────────┘ └──────┬──────┘
│ │ │
│ 1. 发起授权请求 │ │
│ ─────────────────> │ │
│ │ │
│ 2. 显示授权页面 │ │
│ <───────────────── │ │
│ │ │
│ 3. 用户同意授权 │ │
│ ─────────────────> │ │
│ │ │
│ 4. 返回授权码code │ │
│ <───────────────── │ │
│ │ │
│ 5. 将code发给后端 │ │
│ ──────────────────────────────────────> │
│ │ │
│ │ 6. 用code换token │
│ │ <───────────────── │
│ │ │
│ │ 7. 返回token │
│ │ ─────────────────> │
│ │ │
│ 8. 返回登录成功 │ │
│ <────────────────────────────────────── │
│ │ │关键概念:
| 名词 | 说明 |
|---|---|
client_id | 你在栖岛平台申请的应用ID |
client_secret | 你的应用密钥(仅后端使用,不要暴露给前端!) |
code | 授权码,用于换取 access_token,有效期很短 |
access_token | 访问令牌,用于调用栖岛API获取用户信息 |
refresh_token | 刷新令牌,用于刷新 access_token |
redirect_uri | 授权成功后的回调地址 |
state | 随机字符串,防止CSRF攻击 |
准备工作
第一步:申请栖岛开发者账号
- 访问栖岛开发者平台
- 注册并登录开发者账号
创建应用,获取以下信息:
client_id(应用ID)client_secret(应用密钥)
第二步:配置回调地址
在栖岛开发者平台配置你的回调地址:
APP应用: 配置 Scheme 地址,例如:myapp://callback
小程序: 由栖岛APP应用自动处理,无需配置
第三步:确定需要的权限范围(scope)
常用权限:
profile- 获取用户基本信息(昵称、头像)email- 获取用户邮箱phone- 获取用户手机号
多个权限用空格分隔,例如:profile email
APP端对接(原生应用)
方式一:通过 Scheme 唤起栖岛授权
这是最常用的方式,适用于独立APP。
1. 构造授权请求URL
// 授权请求URL格式
const authUrl = `qidao://authorize?client_id=${client_id}&redirect_uri=${encodeURIComponent(redirect_uri)}&scope=${encodeURIComponent(scope)}&state=${state}&response_type=code`;参数说明:
| 参数 | 必填 | 说明 | 示例 |
|---|---|---|---|
client_id | ✅ | 你的应用ID | test_client |
redirect_uri | ✅ | 回调地址(需URL编码) | myapp://callback |
scope | ✅ | 权限范围 | profile email |
state | ✅ | 随机字符串,防CSRF | abc123xyz |
response_type | ✅ | 固定值 | code |
2. 发起授权请求
// 生成随机state
function generateState() {
return Math.random().toString(36).substring(2, 15) +
Math.random().toString(36).substring(2, 15);
}
// 发起授权
function startOAuth() {
const client_id = 'your_client_id'; // 替换为你的应用ID
const redirect_uri = 'myapp://callback'; // 替换为你的回调地址
const scope = 'profile';
const state = generateState();
// 保存state用于后续验证
uni.setStorageSync('oauth_state', state);
// 构造授权URL
const authUrl = `qidao://authorize?client_id=${client_id}&redirect_uri=${encodeURIComponent(redirect_uri)}&scope=${encodeURIComponent(scope)}&state=${state}&response_type=code`;
// 打开栖岛授权页面
// 方式1:使用plus.runtime.openURL(5+App)
plus.runtime.openURL(authUrl);
// 方式2:使用uni.navigateTo(如果是页面跳转)
// uni.navigateTo({ url: authUrl });
}3. 处理授权回调
当用户授权成功后,栖岛会通过 Scheme 回调你的应用,并携带 code 和 state 参数。
// 在 App.vue 的 onLaunch 或 onShow 中监听
onShow(options) {
// 检查是否是从栖岛授权回调
if (options.query && options.query.code) {
this.handleOAuthCallback(options.query);
}
}
// 处理回调
handleOAuthCallback(query) {
const { code, state, error, error_description } = query;
// 检查是否有错误
if (error) {
console.error('授权失败:', error_description);
uni.showToast({
title: error_description || '授权失败',
icon: 'none'
});
return;
}
// 验证state防止CSRF攻击
const savedState = uni.getStorageSync('oauth_state');
if (state !== savedState) {
console.error('state验证失败,可能存在安全风险');
return;
}
// 将code发送给后端换取token
this.exchangeCodeForToken(code, state);
}
// 发送code给后端
async exchangeCodeForToken(code, state) {
try {
const res = await uni.request({
url: 'https://your-server.com/api/oauth/callback', // 你的后端接口
method: 'POST',
data: { code, state }
});
if (res.data.success) {
// 保存用户信息
uni.setStorageSync('user_info', res.data.user);
uni.setStorageSync('access_token', res.data.access_token);
uni.showToast({
title: '登录成功',
icon: 'success'
});
}
} catch (error) {
console.error('登录失败:', error);
}
}小程序端对接(栖岛小程序)
如果你的应用是运行在栖岛栖岛APPAPP内的小程序,可以使用更简便的方式。
使用 OAuthDialog 模块
// 发起授权
async function startOAuth() {
try {
// 获取栖岛OAuth模块
const oauthModule = uni.requireNativePlugin('OAuthDialog');
if (!oauthModule) {
uni.showToast({
title: '无法获取OAuth模块',
icon: 'none'
});
return;
}
// 调用授权弹窗
// 小程序只需传递client_id,其他参数由栖岛APP自动处理
const result = await new Promise((resolve, reject) => {
oauthModule.showDialog({
client_id: 'your_client_id' // 替换为你的应用ID
}, (res) => {
resolve(res);
});
});
if (result.success) {
console.log('授权成功');
console.log('授权码:', result.code);
console.log('State:', result.state);
console.log('回调地址:', result.redirect_uri);
// 将code发送给后端换取用户信息
await getUserInfo(result.code, result.state);
} else {
console.error('授权失败:', result.error_description);
}
} catch (error) {
console.error('OAuth异常:', error);
}
}
// 获取用户信息
async function getUserInfo(code, state) {
const res = await uni.request({
url: `https://your-server.com/api/oauth/callback?code=${code}&state=${state}`,
method: 'GET'
});
if (res.data.success) {
// 保存用户信息
uni.setStorageSync('user_info', res.data);
uni.showToast({
title: '登录成功',
icon: 'success'
});
}
}后端对接
后端是整个OAuth流程中最关键的部分,负责用授权码换取Token和获取用户信息。
栖岛API接口
| 接口 | 地址 | 说明 |
|---|---|---|
| 获取Token | https://api.qidao.tvcloud.top/oauth2/token | 用code换取access_token |
| 获取用户信息 | https://api.qidao.tvcloud.top/oauth2/userinfo/oauth | 获取用户资料 |
PHP 后端示例
<?php
header('Content-Type: application/json; charset=utf-8');
// ========== 配置区域 ==========
$client_id = 'your_client_id'; // 替换为你的应用ID
$client_secret = 'your_client_secret'; // 替换为你的应用密钥
$redirect_uri = 'https://your-server.com/api/oauth/callback'; // 你的回调地址
// ========== 第一步:接收授权码 ==========
if (!isset($_GET['code'])) {
echo json_encode([
'success' => false,
'error' => 'missing_code',
'error_description' => '未收到授权码'
]);
exit;
}
$code = $_GET['code'];
$state = $_GET['state'] ?? '';
// ========== 第二步:用code换取access_token ==========
$token_url = 'https://api.qidao.tvcloud.top/oauth2/token';
$post_data = [
'grant_type' => 'authorization_code',
'client_id' => $client_id,
'client_secret' => $client_secret,
'code' => $code,
'redirect_uri' => $redirect_uri
];
$ch = curl_init($token_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($post_data));
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/x-www-form-urlencoded']);
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$token_data = json_decode($response, true);
// 检查是否成功获取token
if ($http_code !== 200 || empty($token_data['access_token'])) {
echo json_encode([
'success' => false,
'error' => $token_data['error'] ?? 'token_error',
'error_description' => $token_data['error_description'] ?? '获取Token失败'
]);
exit;
}
$access_token = $token_data['access_token'];
$refresh_token = $token_data['refresh_token'] ?? null;
// ========== 第三步:用access_token获取用户信息 ==========
$user_info_url = 'https://api.qidao.tvcloud.top/oauth2/userinfo/oauth?access_token=' . urlencode($access_token);
$ch = curl_init($user_info_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$user_response = curl_exec($ch);
curl_close($ch);
$user_data = json_decode($user_response, true);
// ========== 第四步:处理业务逻辑 ==========
// 这里你应该:
// 1. 将refresh_token保存到数据库
// 2. 根据用户信息创建或更新本地用户
// 3. 生成你自己的登录态(如JWT)
// 返回结果给前端
echo json_encode([
'success' => true,
'access_token' => $access_token,
'token_type' => $token_data['token_type'] ?? 'Bearer',
'expires_in' => $token_data['expires_in'] ?? null,
'user' => $user_data['data'] ?? null
]);
?>Node.js 后端示例
const express = require('express');
const axios = require('axios');
const app = express();
// 配置
const CLIENT_ID = 'your_client_id';
const CLIENT_SECRET = 'your_client_secret';
const REDIRECT_URI = 'https://your-server.com/api/oauth/callback';
// OAuth回调接口
app.get('/api/oauth/callback', async (req, res) => {
const { code, state } = req.query;
if (!code) {
return res.json({
success: false,
error: 'missing_code',
error_description: '未收到授权码'
});
}
try {
// 第一步:用code换取access_token
const tokenResponse = await axios.post(
'https://api.qidao.tvcloud.top/oauth2/token',
new URLSearchParams({
grant_type: 'authorization_code',
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
code: code,
redirect_uri: REDIRECT_URI
}),
{
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
}
);
const { access_token, refresh_token, expires_in } = tokenResponse.data;
// 第二步:获取用户信息
const userResponse = await axios.get(
`https://api.qidao.tvcloud.top/oauth2/userinfo/oauth?access_token=${access_token}`
);
const userData = userResponse.data;
// 第三步:处理业务逻辑
// TODO: 保存refresh_token到数据库
// TODO: 创建或更新本地用户
res.json({
success: true,
access_token,
expires_in,
user: userData.data
});
} catch (error) {
console.error('OAuth错误:', error.response?.data || error.message);
res.json({
success: false,
error: 'oauth_error',
error_description: error.response?.data?.error_description || '授权失败'
});
}
});
app.listen(3000, () => {
console.log('Server running on port 3000');
});完整代码示例
前端完整示例(Vue页面)
<template>
<view class="login-container">
<!-- 未登录状态 -->
<view class="login-section" v-if="!isLoggedIn">
<image class="logo" src="/static/logo.png" mode="aspectFit"></image>
<text class="title">欢迎使用</text>
<text class="subtitle">使用栖岛账号快速登录</text>
<button class="login-btn" @click="handleLogin" :disabled="isLoading">
{{ isLoading ? '登录中...' : '栖岛账号登录' }}
</button>
</view>
<!-- 已登录状态 -->
<view class="user-section" v-else>
<image class="avatar" :src="userInfo.avatar || '/static/logo.png'" mode="aspectFill"></image>
<text class="username">{{ userInfo.screenName || '用户' }}</text>
<text class="uid">UID: {{ userInfo.uid }}</text>
<button class="logout-btn" @click="handleLogout">退出登录</button>
</view>
</view>
</template>
<script>
export default {
data() {
return {
isLoggedIn: false,
isLoading: false,
userInfo: null,
// OAuth配置 - 替换为你的实际配置
oauthConfig: {
client_id: 'your_client_id',
redirect_uri: 'myapp://callback',
scope: 'profile',
// 后端接口地址
backendUrl: 'https://your-server.com/api/oauth/callback'
}
}
},
onLoad() {
this.checkLoginStatus();
},
onShow() {
this.checkLoginStatus();
},
methods: {
// 检查登录状态
checkLoginStatus() {
const userInfo = uni.getStorageSync('user_info');
if (userInfo && userInfo.user) {
this.isLoggedIn = true;
this.userInfo = userInfo.user;
} else {
this.isLoggedIn = false;
this.userInfo = null;
}
},
// 生成随机state
generateState() {
return Math.random().toString(36).substring(2, 15) +
Math.random().toString(36).substring(2, 15);
},
// 处理登录
async handleLogin() {
this.isLoading = true;
try {
// 判断运行环境
const systemInfo = uni.getSystemInfoSync();
// #ifdef APP-PLUS
// APP环境:检查是否在小程序容器中
const oauthModule = uni.requireNativePlugin('OAuthDialog');
if (oauthModule) {
// 小程序环境:使用OAuthDialog
await this.loginWithOAuthDialog(oauthModule);
} else {
// 独立APP环境:使用Scheme方式
await this.loginWithScheme();
}
// #endif
// #ifdef H5
// H5环境:跳转到授权页面
await this.loginWithRedirect();
// #endif
} catch (error) {
console.error('登录失败:', error);
uni.showToast({
title: '登录失败,请重试',
icon: 'none'
});
} finally {
this.isLoading = false;
}
},
// 方式1:小程序环境 - 使用OAuthDialog模块
async loginWithOAuthDialog(oauthModule) {
return new Promise((resolve, reject) => {
oauthModule.showDialog({
client_id: this.oauthConfig.client_id
}, async (result) => {
if (result.success) {
// 授权成功,获取用户信息
await this.fetchUserInfo(result.code, result.state);
resolve();
} else {
uni.showToast({
title: result.error_description || '授权失败',
icon: 'none'
});
reject(new Error(result.error_description));
}
});
});
},
// 方式2:独立APP环境 - 使用Scheme方式
async loginWithScheme() {
const state = this.generateState();
uni.setStorageSync('oauth_state', state);
const authUrl = `qidao://authorize?client_id=${this.oauthConfig.client_id}&redirect_uri=${encodeURIComponent(this.oauthConfig.redirect_uri)}&scope=${encodeURIComponent(this.oauthConfig.scope)}&state=${state}&response_type=code`;
// 打开栖岛授权
plus.runtime.openURL(authUrl);
},
// 方式3:H5环境 - 页面跳转方式
async loginWithRedirect() {
const state = this.generateState();
uni.setStorageSync('oauth_state', state);
const authUrl = `https://qidao.tvcloud.top/oauth/authorize?client_id=${this.oauthConfig.client_id}&redirect_uri=${encodeURIComponent(window.location.href)}&scope=${encodeURIComponent(this.oauthConfig.scope)}&state=${state}&response_type=code`;
window.location.href = authUrl;
},
// 获取用户信息
async fetchUserInfo(code, state) {
uni.showLoading({ title: '获取用户信息...' });
try {
const res = await uni.request({
url: `${this.oauthConfig.backendUrl}?code=${code}&state=${state}`,
method: 'GET'
});
if (res.data.success) {
// 保存用户信息
uni.setStorageSync('user_info', res.data);
this.isLoggedIn = true;
this.userInfo = res.data.user;
uni.showToast({
title: '登录成功',
icon: 'success'
});
} else {
throw new Error(res.data.error_description || '获取用户信息失败');
}
} finally {
uni.hideLoading();
}
},
// 退出登录
handleLogout() {
uni.showModal({
title: '提示',
content: '确定要退出登录吗?',
success: (res) => {
if (res.confirm) {
uni.removeStorageSync('user_info');
uni.removeStorageSync('oauth_state');
this.isLoggedIn = false;
this.userInfo = null;
uni.showToast({
title: '已退出登录',
icon: 'success'
});
}
}
});
}
}
}
</script>
<style scoped>
.login-container {
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40rpx;
background: #f5f7fa;
}
.login-section, .user-section {
text-align: center;
background: #ffffff;
padding: 60rpx 40rpx;
border-radius: 20rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.08);
width: 100%;
max-width: 600rpx;
}
.logo {
width: 160rpx;
height: 160rpx;
margin-bottom: 30rpx;
}
.title {
display: block;
font-size: 40rpx;
font-weight: bold;
color: #333;
margin-bottom: 16rpx;
}
.subtitle {
display: block;
font-size: 28rpx;
color: #999;
margin-bottom: 60rpx;
}
.login-btn {
width: 100%;
height: 88rpx;
background: linear-gradient(135deg, #1e88e5, #1565c0);
color: #fff;
font-size: 32rpx;
border-radius: 44rpx;
border: none;
}
.login-btn:disabled {
opacity: 0.6;
}
.avatar {
width: 160rpx;
height: 160rpx;
border-radius: 80rpx;
margin-bottom: 20rpx;
}
.username {
display: block;
font-size: 36rpx;
font-weight: bold;
color: #333;
margin-bottom: 10rpx;
}
.uid {
display: block;
font-size: 26rpx;
color: #999;
margin-bottom: 40rpx;
}
.logout-btn {
width: 100%;
height: 88rpx;
background: #f5f5f5;
color: #666;
font-size: 32rpx;
border-radius: 44rpx;
border: none;
}
</style>常见问题FAQ
Q1: 授权后没有收到回调怎么办?
可能原因:
redirect_uri配置错误- Scheme 未在原生端注册
- 栖岛APP未安装
解决方案:
- 检查
redirect_uri是否与栖岛开发者平台配置一致 - 确保在原生端(iOS/Android)正确注册了 URL Scheme
- 确保用户设备已安装栖岛APP
Q2: 获取Token时返回 invalid_client 错误?
可能原因:
client_id或client_secret错误- 应用未审核通过
解决方案:
- 核对开发者平台的应用凭证
- 确认应用状态为"已上线"
Q3: state 验证失败是什么原因?
可能原因:
- state 在授权过程中丢失
- 存在 CSRF 攻击
解决方案:
- 确保 state 正确保存到本地存储
- 检查是否有第三方篡改请求
Q4: access_token 过期了怎么办?
使用 refresh_token 刷新:
$refresh_url = 'https://api.qidao.tvcloud.top/oauth2/token';
$post_data = [
'grant_type' => 'refresh_token',
'client_id' => $client_id,
'client_secret' => $client_secret,
'refresh_token' => $saved_refresh_token
];
// 发送请求获取新的access_tokenQ5: 小程序中无法获取 OAuthDialog 模块?
可能原因:
- 不在栖岛栖岛APPAPP环境中运行
- 栖岛APPAPP版本过低
解决方案:
- 确保小程序运行在栖岛栖岛APPAPP内
- 更新栖岛APP到最新版本
- 如果是独立APP,请使用 Scheme 方式
Q6: 如何在本地测试?
- 使用测试账号:
client_id: test_client - 配置本地回调地址进行调试
- 使用栖岛提供的沙箱环境
安全建议
- 永远不要在前端暴露
client_secret - 每次授权生成新的
state参数 - 使用 HTTPS 传输所有敏感数据
refresh_token必须安全存储在服务器端- 定期检查和更新 Token
- 记录所有授权日志,便于排查问题
技术支持
如有问题,请通过以下方式获取帮助:
- 📧 查看栖岛开发者文档
- 💬 加入栖岛开发者社区
- 🐛 提交 Issue 反馈问题
文档版本:v1.0
最后更新:2026年1月