{ }
代码示例
泽汐(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 次/分钟;密码/验证码登录复用网页端防暴力破解