栖岛登录对接完整指南(含APP和栖岛小程序)

栖岛登录对接完整指南

本文档面向新手开发者,帮助你快速在自己的 APP 或小程序中集成栖岛 OAuth2.0 登录功能。

📖 目录

  1. 什么是栖岛登录
  2. 整体流程概览
  3. 准备工作
  4. APP端对接(原生应用)
  5. 小程序端对接(栖岛小程序)
  6. 后端对接
  7. 完整代码示例
  8. 常见问题FAQ

什么是栖岛登录

栖岛登录是基于 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攻击

准备工作

第一步:申请栖岛开发者账号

  1. 访问栖岛开发者平台
  2. 注册并登录开发者账号
  3. 创建应用,获取以下信息:

    • 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你的应用IDtest_client
redirect_uri回调地址(需URL编码)myapp://callback
scope权限范围profile email
state随机字符串,防CSRFabc123xyz
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 回调你的应用,并携带 codestate 参数。

// 在 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接口

接口地址说明
获取Tokenhttps://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: 授权后没有收到回调怎么办?

可能原因:

  1. redirect_uri 配置错误
  2. Scheme 未在原生端注册
  3. 栖岛APP未安装

解决方案:

  1. 检查 redirect_uri 是否与栖岛开发者平台配置一致
  2. 确保在原生端(iOS/Android)正确注册了 URL Scheme
  3. 确保用户设备已安装栖岛APP

Q2: 获取Token时返回 invalid_client 错误?

可能原因:

  1. client_idclient_secret 错误
  2. 应用未审核通过

解决方案:

  1. 核对开发者平台的应用凭证
  2. 确认应用状态为"已上线"

Q3: state 验证失败是什么原因?

可能原因:

  1. state 在授权过程中丢失
  2. 存在 CSRF 攻击

解决方案:

  1. 确保 state 正确保存到本地存储
  2. 检查是否有第三方篡改请求

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_token

Q5: 小程序中无法获取 OAuthDialog 模块?

可能原因:

  1. 不在栖岛栖岛APPAPP环境中运行
  2. 栖岛APPAPP版本过低

解决方案:

  1. 确保小程序运行在栖岛栖岛APPAPP内
  2. 更新栖岛APP到最新版本
  3. 如果是独立APP,请使用 Scheme 方式

Q6: 如何在本地测试?

  1. 使用测试账号:client_id: test_client
  2. 配置本地回调地址进行调试
  3. 使用栖岛提供的沙箱环境

安全建议

  1. 永远不要在前端暴露 client_secret
  2. 每次授权生成新的 state 参数
  3. 使用 HTTPS 传输所有敏感数据
  4. refresh_token 必须安全存储在服务器端
  5. 定期检查和更新 Token
  6. 记录所有授权日志,便于排查问题

技术支持

如有问题,请通过以下方式获取帮助:

  • 📧 查看栖岛开发者文档
  • 💬 加入栖岛开发者社区
  • 🐛 提交 Issue 反馈问题

文档版本:v1.0
最后更新:2026年1月

添加新评论