{ }

代码示例

泽汐(Evetide)统一登录接入代码:7 种语言复制即用(PHP / Node / Python / Java / Go / Flutter / 微信小程序),外加 App 原生登录直连示例。替换 client_id / client_secret / redirect_uri 即可运行。

复制即用示例代码

所有示例均为完整接入流程:发起授权 → 回调校验 state → code 换 token → 拿用户信息 → 建立本地登录态。只需替换 client_id / client_secret / redirect_uri 即可运行。
// callback.php —— 用户授权后跳回这里
$clientId     = '你的client_id';
$clientSecret = '你的client_secret';
$redirectUri  = 'https://你的网站.com/callback';
$base         = 'https://login.evetide.cn';

// 1. 校验 state(发起登录时保存到 session,回调时比对)
if (!hash_equals($_SESSION['oauth_state'], $_GET['state'] ?? '')) { die('state 不匹配'); }
$code = $_GET['code'] ?? '';

// 2. code 换 token(Basic 认证)
$ch = curl_init($base . '/api_token.php');
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_USERPWD => $clientId . ':' . $clientSecret,
    CURLOPT_POSTFIELDS => http_build_query([
        'grant_type' => 'authorization_code',
        'code' => $code,
        'redirect_uri' => $redirectUri,
    ]),
]);
$res = json_decode(curl_exec($ch), true);
$accessToken = $res['data']['access_token'] ?? '';

// 3. 拿用户信息(Bearer 认证)
$ch = curl_init($base . '/api_userinfo.php');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $accessToken],
]);
$user = json_decode(curl_exec($ch), true)['data'] ?? [];

// 4. 用 $user['sub'] 作为本地用户标识,创建登录态
$_SESSION['user_id'] = $user['sub'];
$_SESSION['nickname'] = $user['name'] ?? '';
header('Location: /');
// server.js —— 使用 express
const express = require('express');
const crypto = require('crypto');
const app = express();
const CLIENT_ID = '你的client_id';
const CLIENT_SECRET = '你的client_secret';
const REDIRECT_URI = 'https://你的网站.com/callback';
const BASE = 'https://login.evetide.cn';

// 1. 发起授权跳转(保存 state 到 session)
app.get('/login', (req, res) => {
  const state = crypto.randomBytes(8).toString('hex');
  req.session.oauthState = state;
  const url = `${BASE}/api_authorize.php?` + new URLSearchParams({
    client_id: CLIENT_ID, redirect_uri: REDIRECT_URI,
    response_type: 'code', scope: 'openid profile email', state,
  });
  res.redirect(url);
});

// 2. 回调:换 token + 拿用户
app.get('/callback', async (req, res) => {
  const { code, state } = req.query;
  if (state !== req.session.oauthState) return res.status(400).send('state 不匹配');
  const r = await fetch(`${BASE}/api_token.php`, {
    method: 'POST',
    headers: {
      Authorization: 'Basic ' + Buffer.from(CLIENT_ID + ':' + CLIENT_SECRET).toString('base64'),
      'Content-Type': 'application/x-www-form-urlencoded',
    },
    body: new URLSearchParams({ grant_type: 'authorization_code', code, redirect_uri: REDIRECT_URI }),
  });
  const { data } = await r.json();
  const info = await (await fetch(`${BASE}/api_userinfo.php`, {
    headers: { Authorization: 'Bearer ' + data.access_token },
  })).json();
  req.session.userId = info.data.sub;
  req.session.nickname = info.data.name;
  res.redirect('/');
});

app.listen(3000);
# callback.py —— 使用 Flask
import hashlib, os, urllib.parse, requests
from flask import Flask, redirect, request, session

app = Flask(__name__)
app.secret_key = '换成随机密钥'
CLIENT_ID = '你的client_id'
CLIENT_SECRET = '你的client_secret'
REDIRECT_URI = 'https://你的网站.com/callback'
BASE = 'https://login.evetide.cn'

@app.route('/login')
def login():
    state = hashlib.md5(os.urandom(16)).hexdigest()
    session['oauth_state'] = state
    params = {
        'client_id': CLIENT_ID,
        'redirect_uri': REDIRECT_URI,
        'response_type': 'code',
        'scope': 'openid profile email',
        'state': state,
    }
    return redirect(BASE + '/api_authorize.php?' + urllib.parse.urlencode(params))

@app.route('/callback')
def callback():
    code = request.args.get('code')
    if request.args.get('state') != session.get('oauth_state'):
        return 'state 不匹配', 400
    # code 换 token
    r = requests.post(BASE + '/api_token.php',
        auth=(CLIENT_ID, CLIENT_SECRET),
        data={'grant_type': 'authorization_code', 'code': code, 'redirect_uri': REDIRECT_URI})
    token = r.json()['data']['access_token']
    # 拿用户信息
    info = requests.get(BASE + '/api_userinfo.php',
        headers={'Authorization': 'Bearer ' + token}).json()['data']
    session['user_id'] = info['sub']
    session['nickname'] = info['name']
    return redirect('/')
// CallbackController.java —— Spring Boot
@RestController
public class AuthController {
    static final String BASE = "https://login.evetide.cn";
    static final String CLIENT_ID = "你的client_id";
    static final String CLIENT_SECRET = "你的client_secret";
    static final String REDIRECT = "https://你的网站.com/callback";

    // 1. 发起授权
    @GetMapping("/login")
    public RedirectView login(HttpSession session) {
        String state = UUID.randomUUID().toString().replace("-", "");
        session.setAttribute("oauth_state", state);
        String url = BASE + "/api_authorize.php?client_id=" + CLIENT_ID
            + "&redirect_uri=" + URLEncoder.encode(REDIRECT, StandardCharsets.UTF_8)
            + "&response_type=code&scope=openid%20profile%20email&state=" + state;
        return new RedirectView(url);
    }

    // 2. 回调
    @GetMapping("/callback")
    public RedirectView callback(@RequestParam String code, @RequestParam String state,
                                 HttpSession session) throws Exception {
        if (!state.equals(session.getAttribute("oauth_state"))) throw new RuntimeException("state 不匹配");
        // code 换 token(Basic 认证)
        String cred = Base64.getEncoder().encodeToString((CLIENT_ID + ":" + CLIENT_SECRET).getBytes());
        HttpRequest req = HttpRequest.newBuilder(URI.create(BASE + "/api_token.php"))
            .header("Authorization", "Basic " + cred)
            .header("Content-Type", "application/x-www-form-urlencoded")
            .POST(BodyPublishers.ofString("grant_type=authorization_code&code=" + code
                + "&redirect_uri=" + URLEncoder.encode(REDIRECT, StandardCharsets.UTF_8)))
            .build();
        String body = HttpClient.newHttpClient().send(req, BodyHandlers.ofString()).body();
        String token = JsonPath.read(body, "$.data.access_token");
        // 拿用户信息
        HttpRequest req2 = HttpRequest.newBuilder(URI.create(BASE + "/api_userinfo.php"))
            .header("Authorization", "Bearer " + token).GET().build();
        String info = HttpClient.newHttpClient().send(req2, BodyHandlers.ofString()).body();
        session.setAttribute("user_id", JsonPath.read(info, "$.data.sub"));
        return new RedirectView("/");
    }
}
// main.go —— 使用 net/http + gorilla/sessions
package main

import (
    "crypto/rand"
    "encoding/base64"
    "encoding/json"
    "net/http"
    "net/url"
    "strings"

    "github.com/gorilla/sessions"
)

var store = sessions.NewCookieStore([]byte("换成随机密钥"))

const (
    base         = "https://login.evetide.cn"
    clientID     = "你的client_id"
    clientSecret = "你的client_secret"
    redirectURI  = "https://你的网站.com/callback"
)

func login(w http.ResponseWriter, r *http.Request) {
    b := make([]byte, 8)
    rand.Read(b)
    state := base64.URLEncoding.EncodeToString(b)
    sess, _ := store.Get(r, "app")
    sess.Values["oauth_state"] = state
    sess.Save(r, w)
    u := base + "/api_authorize.php?client_id=" + clientID +
        "&redirect_uri=" + url.QueryEscape(redirectURI) +
        "&response_type=code&scope=openid+profile+email&state=" + state
    http.Redirect(w, r, u, http.StatusFound)
}

func callback(w http.ResponseWriter, r *http.Request) {
    sess, _ := store.Get(r, "app")
    if r.URL.Query().Get("state") != sess.Values["oauth_state"] {
        http.Error(w, "state 不匹配", http.StatusBadRequest)
        return
    }
    // code 换 token
    form := url.Values{
        "grant_type":  {"authorization_code"},
        "code":        {r.URL.Query().Get("code")},
        "redirect_uri": {redirectURI},
    }
    req, _ := http.NewRequest("POST", base+"/api_token.php", strings.NewReader(form.Encode()))
    req.SetBasicAuth(clientID, clientSecret)
    req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
    resp, _ := http.DefaultClient.Do(req)
    var tr struct{ Data struct{ AccessToken string `json:"access_token"` } `json:"data"` }
    json.NewDecoder(resp.Body).Decode(&tr)
    // 拿用户信息
    req2, _ := http.NewRequest("GET", base+"/api_userinfo.php", nil)
    req2.Header.Set("Authorization", "Bearer "+tr.Data.AccessToken)
    resp2, _ := http.DefaultClient.Do(req2)
    var ui struct{ Data struct{ Sub string `json:"sub"` } `json:"data"` }
    json.NewDecoder(resp2.Body).Decode(&ui)
    sess.Values["user_id"] = ui.Data.Sub
    sess.Save(r, w)
    http.Redirect(w, r, "/", http.StatusFound)
}

func main() {
    http.HandleFunc("/login", login)
    http.HandleFunc("/callback", callback)
    http.ListenAndServe(":3000", nil)
}
// login_service.dart —— 使用 flutter_web_auth + http
import 'dart:convert';
import 'package:flutter_web_auth/flutter_web_auth.dart';
import 'package:http/http.dart' as http;

const BASE = 'https://login.evetide.cn';
const CLIENT_ID = '你的client_id';
const CLIENT_SECRET = '你的client_secret';
const REDIRECT_SCHEME = 'yourapp'; // 你注册的自定义 scheme

Future<Map<String, dynamic>> login() async {
  // 1. 打开系统浏览器授权
  final result = await FlutterWebAuth.authenticate(
    url: '$BASE/api_authorize.php?client_id=$CLIENT_ID'
        '&redirect_uri=$REDIRECT_SCHEME://callback'
        '&response_type=code&scope=openid+profile+email',
    callbackUrlScheme: REDIRECT_SCHEME,
  );
  final code = Uri.parse(result).queryParameters['code'];

  // 2. code 换 token
  final tokenRes = await http.post(
    Uri.parse('$BASE/api_token.php'),
    headers: {
      'Authorization': 'Basic ' + base64Encode(utf8.encode('$CLIENT_ID:$CLIENT_SECRET')),
    },
    body: {
      'grant_type': 'authorization_code',
      'code': code,
      'redirect_uri': '$REDIRECT_SCHEME://callback',
    },
  );
  final token = jsonDecode(tokenRes.body)['data']['access_token'];

  // 3. 拿用户信息
  final infoRes = await http.get(
    Uri.parse('$BASE/api_userinfo.php'),
    headers: {'Authorization': 'Bearer $token'},
  );
  return jsonDecode(infoRes.body)['data'] as Map<String, dynamic>;
}
// 小程序 —— 通过 web-view 完成授权
// 注意:secret 绝不能放前端,code 换 token 必须由你的后端完成

// 1. 你的后端提供登录链接:
//    https://https://login.evetide.cn/api_authorize.php?client_id=xxx&redirect_uri=你的后端回调
//    &response_type=code&scope=openid+profile+email&state=随机串

// 2. 小程序打开 web-view
wx.navigateTo({
  url: '/pages/webview/webview?url=' + encodeURIComponent(loginUrl),
});

// 3. 用户授权后,redirect_uri(你的后端回调)收到 code + state
//    后端:校验 state → code 换 token → 拿用户信息 → 建立业务登录态

// 4. 你的后端返回成功页,或通过支付通知/轮询让小程序的业务接口获得登录态
//    小程序后端示例(Node):
//    const r = await fetch(BASE + '/api_token.php', { method: 'POST',
//      headers: { Authorization: 'Basic ' + btoa(CLIENT_ID + ':' + CLIENT_SECRET) },
//      body: 'grant_type=authorization_code&code=' + code + '&redirect_uri=' + REDIRECT });
//    const token = (await r.json()).data.access_token;
//    const info = await (await fetch(BASE + '/api_userinfo.php',
//      { headers: { Authorization: 'Bearer ' + token } })).json();
// 原生登录 —— App 内直连,免浏览器 / WebView
// 统一入口:POST https://login.evetide.cn/api_native_login.php (Content-Type: application/json)
// 统一响应:{"code":0,"data":{"access_token","refresh_token","user":{...}}}
// 三种方式任选,App 只需保存 client_id(无需 client_secret,凭据是用户自己的)

// ① 邮箱 + 密码
POST /api_native_login.php
{
  "type": "password",
  "client_id": "你的client_id",
  "account": "user@example.com",   // 用户名或邮箱
  "password": "用户密码"
}

// ② 邮箱 + 验证码(先发码,再登录)
POST /api_send_code.php
{ "email": "user@example.com", "method": "login", "captcha_token": "" }

POST /api_native_login.php
{ "type": "email_code", "client_id": "你的client_id", "email": "user@example.com", "code": "123456" }

// ③ 快捷登录(App 端用平台 SDK 拿到 openid / unionid)
POST /api_native_login.php
{
  "type": "social",
  "client_id": "你的client_id",
  "provider": "wechat",           // wechat | qq | github | gitee | dingtalk | feishu
  "provider_sub": "平台openid或unionid",
  "nickname": "可选,首次建档用",
  "avatar": "可选,头像URL",
  "email": "可选,填了会尝试关联已有账号"
}

// 登录成功后:用 access_token 调 /api_userinfo.php 取资料,过期用 refresh_token 续期
// 限流:每应用 100 次/分钟;密码/验证码登录复用网页端防暴力破解
← 返回接入流程 下一章:AI Skill →