<!DOCTYPE html>
<html>
<head>
<title>Third-party App Example</title>
<style>
.container { max-width: 800px; margin: 0 auto; padding: 20px; }
.user-info { background: #f5f5f5; padding: 20px; border-radius: 8px; margin: 20px 0; }
.login-section { text-align: center; padding: 40px; }
.btn { padding: 12px 24px; border: none; border-radius: 6px; cursor: pointer; font-size: 16px; }
.btn-primary { background: #007bff; color: white; }
.btn-success { background: #28a745; color: white; }
.btn-secondary { background: #6c757d; color: white; }
.hidden { display: none; }
</style>
</head>
<body>
<div class="container">
<h1>我的第三方应用</h1>
<!-- Loading status -->
<div id="loading">
<p>Checking login status...</p>
</div>
<!-- Logged-in user info -->
<div id="user-info" class="user-info hidden">
<h2>Welcome back!</h2>
<p><strong>Username:</strong> <span id="username"></span></p>
<p><strong>Email:</strong> <span id="email"></span></p>
<p><strong>Account Balance:</strong> <span id="balance"></span></p>
<div style="margin-top: 20px;">
<button class="btn btn-success" onclick="refreshBalance()">Refresh Balance</button>
<button class="btn btn-primary" onclick="goToTopup()">Top Up Account</button>
<button class="btn btn-secondary" onclick="logout()">Log Out</button>
</div>
</div>
<!-- Not logged in status -->
<div id="login-section" class="login-section hidden">
<h2>Please log in to view account info</h2>
<p>Use one-click login for quick access to your account</p>
<button class="btn btn-primary" onclick="oneClickLogin()">
🚀 One-click Login
</button>
</div>
</div>
<script>
// OAuth configuration
const OAUTH_CONFIG = {
authServer: 'https://your-domain.com',
clientId: 'YOUR_CLIENT_ID',
clientSecret: 'YOUR_CLIENT_SECRET', // 生产环境应该在后端处理
redirectUri: window.location.origin + '/oauth/callback.html',
scope: 'profile balance'
};
class OAuthManager {
constructor() {
this.accessToken = localStorage.getItem('oauth_access_token');
this.refreshToken = localStorage.getItem('oauth_refresh_token');
this.tokenExpiresAt = localStorage.getItem('oauth_token_expires_at');
this.isRefreshing = false; // 防止并发刷新
this.init();
}
async init() {
// 检查URL中是否有授权码
const urlParams = new URLSearchParams(window.location.search);
const code = urlParams.get('code');
const state = urlParams.get('state');
if (code) {
await this.handleAuthCallback(code, state);
// 清理URL
window.history.replaceState({}, document.title, window.location.pathname);
} else if (this.accessToken) {
try {
// 检查token是否过期,如果过期尝试刷新
if (this.isTokenExpired()) {
await this.refreshTokenIfNeeded();
}
await this.fetchUserInfo();
this.showUserInfo();
} catch (error) {
console.log('Token可能已过期或无效,需要重新登录');
this.clearTokens();
this.showLoginSection();
}
} else {
this.showLoginSection();
}
document.getElementById('loading').classList.add('hidden');
}
// Check if access token is expired
isTokenExpired() {
if (!this.tokenExpiresAt) return false;
const expiryTime = parseInt(this.tokenExpiresAt);
const bufferTime = 5 * 60 * 1000; // 5 minutes buffer
return Date.now() > (expiryTime - bufferTime);
}
// Automatically refresh token
async refreshTokenIfNeeded() {
if (!this.refreshToken || this.isRefreshing) {
return false;
}
this.isRefreshing = true;
try {
const response = await fetch(`${OAUTH_CONFIG.authServer}/api/oauth2/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: this.refreshToken,
client_id: OAUTH_CONFIG.clientId,
client_secret: OAUTH_CONFIG.clientSecret,
})
});
if (response.ok) {
const tokenData = await response.json();
this.updateTokens(tokenData);
return true;
} else {
throw new Error('Token refresh failed');
}
} catch (error) {
console.error('Failed to refresh token:', error);
this.clearTokens();
return false;
} finally {
this.isRefreshing = false;
}
}
// Update token info
updateTokens(tokenData) {
this.accessToken = tokenData.access_token;
this.refreshToken = tokenData.refresh_token;
this.tokenExpiresAt = Date.now() + (tokenData.expires_in * 1000);
localStorage.setItem('oauth_access_token', this.accessToken);
localStorage.setItem('oauth_refresh_token', this.refreshToken);
localStorage.setItem('oauth_token_expires_at', this.tokenExpiresAt.toString());
}
oneClickLogin() {
const state = this.generateState();
localStorage.setItem('oauth_state', state);
const authUrl = `${OAUTH_CONFIG.authServer}/api/oauth2/authorize?` +
`client_id=${OAUTH_CONFIG.clientId}&` +
`redirect_uri=${encodeURIComponent(OAUTH_CONFIG.redirectUri)}&` +
`response_type=code&` +
`scope=${encodeURIComponent(OAUTH_CONFIG.scope)}&` +
`state=${state}&` +
`auto_authorize=true`; // Enable auto-authorization for true one-click login
// Open authorization page in popup
const popup = window.open(authUrl, 'oauth_login', 'width=500,height=600,scrollbars=yes');
// Listen for popup close
const checkClosed = setInterval(() => {
if (popup.closed) {
clearInterval(checkClosed);
// 检查是否获得了授权
setTimeout(() => this.init(), 1000);
}
}, 1000);
}
async handleAuthCallback(code, state) {
const savedState = localStorage.getItem('oauth_state');
if (state !== savedState) {
console.error('State parameter mismatch');
return;
}
try {
const response = await fetch(`${OAUTH_CONFIG.authServer}/api/oauth2/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'authorization_code',
code: code,
client_id: OAUTH_CONFIG.clientId,
client_secret: OAUTH_CONFIG.clientSecret,
redirect_uri: OAUTH_CONFIG.redirectUri
})
});
const tokenData = await response.json();
if (tokenData.access_token) {
this.updateTokens(tokenData);
localStorage.removeItem('oauth_state');
await this.fetchUserInfo();
this.showUserInfo();
}
} catch (error) {
console.error('获取访问令牌失败:', error);
}
}
// API request method with auto-refresh
async apiRequest(url, options = {}) {
// Check and refresh token if needed
if (this.isTokenExpired()) {
const refreshed = await this.refreshTokenIfNeeded();
if (!refreshed) {
throw new Error('Unable to refresh token');
}
}
const headers = {
'Authorization': `Bearer ${this.accessToken}`,
...options.headers
};
const response = await fetch(url, {
...options,
headers
});
// If a 401 error is received, try to refresh token and retry once
if (response.status === 401 && !options._retry) {
const refreshed = await this.refreshTokenIfNeeded();
if (refreshed) {
return this.apiRequest(url, { ...options, _retry: true });
}
}
return response;
}
async fetchUserInfo() {
const response = await this.apiRequest(`${OAUTH_CONFIG.authServer}/api/oauth2/userinfo`);
if (!response.ok) {
throw new Error('Failed to get user info');
}
this.userInfo = await response.json();
}
showUserInfo() {
document.getElementById('username').textContent = this.userInfo.username;
document.getElementById('email').textContent = this.userInfo.email;
document.getElementById('balance').textContent = this.userInfo.balance_formatted;
document.getElementById('user-info').classList.remove('hidden');
document.getElementById('login-section').classList.add('hidden');
}
showLoginSection() {
document.getElementById('user-info').classList.add('hidden');
document.getElementById('login-section').classList.remove('hidden');
}
async refreshBalance() {
try {
await this.fetchUserInfo();
document.getElementById('balance').textContent = this.userInfo.balance_formatted;
alert('Balance updated');
} catch (error) {
alert('Refresh failed, please log in again');
this.clearTokens();
this.showLoginSection();
}
}
async goToTopup() {
try {
const response = await this.apiRequest(`${OAUTH_CONFIG.authServer}/api/oauth2/topup`);
const data = await response.json();
if (data.topup_url) {
window.open(data.topup_url, '_blank');
}
} catch (error) {
console.error('Failed to get top-up link:', error);
}
}
logout() {
this.clearTokens();
this.showLoginSection();
}
clearTokens() {
this.accessToken = null;
this.refreshToken = null;
this.tokenExpiresAt = null;
this.userInfo = null;
localStorage.removeItem('oauth_access_token');
localStorage.removeItem('oauth_refresh_token');
localStorage.removeItem('oauth_token_expires_at');
}
generateState() {
return Math.random().toString(36).substring(2, 15) +
Math.random().toString(36).substring(2, 15);
}
}
// 全局函数
let oauthManager;
function oneClickLogin() {
oauthManager.oneClickLogin();
}
function refreshBalance() {
oauthManager.refreshBalance();
}
function goToTopup() {
oauthManager.goToTopup();
}
function logout() {
oauthManager.logout();
}
// 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', () => {
oauthManager = new OAuthManager();
});
</script>
</body>
</html>