Files
pixiv/backend/auth.js
T
2025-08-21 10:43:04 +08:00

274 lines
6.9 KiB
JavaScript

const axios = require('axios');
const Crypto = require('crypto');
const { Base64 } = require('js-base64');
const { stringify } = require('qs');
const moment = require('moment');
const { ProxyAgent } = require('proxy-agent');
// OAuth 2.0 配置
const CLIENT_ID = 'MOBrBDS8blbauoSck0ZfDbtuzpyT';
const CLIENT_SECRET = 'lsACyCD94FhDUtGTXi3QzcFE2uU1hqtDaKeqrdwj';
const REDIRECT_URI = 'https://app-api.pixiv.net/web/v1/users/auth/pixiv/callback';
const LOGIN_URL = 'https://app-api.pixiv.net/web/v1/login';
const HASH_SECRET = '28c1fdd170a5204386cb1313c7077b34f83e4aaf4aa829ce78c231e05b0bae2c';
class PixivAuth {
constructor(proxy = null) {
this.accessToken = null;
this.refreshToken = null;
this.user = null;
this.proxy = proxy;
// 创建 axios 实例,支持代理
this.axiosInstance = this.createAxiosInstance();
}
/**
* 创建支持代理的 axios 实例
*/
createAxiosInstance() {
const config = {
timeout: 30000, // 30秒超时
headers: this.getDefaultHeaders()
};
// 如果设置了代理,添加代理配置
if (this.proxy) {
console.log('使用代理:', this.proxy);
config.httpsAgent = new ProxyAgent(this.proxy);
} else {
// 尝试使用系统代理
const systemProxy = process.env.HTTP_PROXY || process.env.HTTPS_PROXY || process.env.http_proxy || process.env.https_proxy;
if (systemProxy) {
console.log('使用系统代理:', systemProxy);
config.httpsAgent = new ProxyAgent(systemProxy);
}
}
return axios.create(config);
}
/**
* 设置代理
*/
setProxy(proxy) {
this.proxy = proxy;
this.axiosInstance = this.createAxiosInstance();
}
/**
* 获取默认头部信息
*/
getDefaultHeaders() {
const datetime = moment().format();
return {
'App-OS': 'android',
'Accept-Language': 'en-us',
'App-OS-Version': '9.0',
'App-Version': '5.0.234',
'User-Agent': 'PixivAndroidApp/5.0.234 (Android 9.0; Pixel 3)',
'X-Client-Time': datetime,
'X-Client-Hash': Crypto.createHash('md5').update(`${datetime}${HASH_SECRET}`).digest('hex')
};
}
/**
* 生成 PKCE 参数
*/
generatePKCE() {
const codeVerifier = Base64.fromUint8Array(Crypto.randomBytes(32), true);
const codeChallenge = Base64.encodeURI(Crypto.createHash('sha256').update(codeVerifier).digest());
return {
code_verifier: codeVerifier,
code_challenge: codeChallenge
};
}
/**
* 获取登录URL
*/
getLoginUrl() {
const pkce = this.generatePKCE();
const params = {
code_challenge: pkce.code_challenge,
code_challenge_method: 'S256',
client: 'pixiv-android'
};
const loginUrl = `${LOGIN_URL}?${stringify(params)}`;
return {
login_url: loginUrl,
code_verifier: pkce.code_verifier
};
}
/**
* 使用授权码获取访问令牌
*/
async getAccessToken(code, codeVerifier) {
try {
console.log('正在获取访问令牌...');
console.log('Code:', code);
console.log('Code Verifier:', codeVerifier);
const data = {
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
code: code,
code_verifier: codeVerifier,
redirect_uri: REDIRECT_URI,
grant_type: 'authorization_code',
include_policy: true
};
console.log('请求数据:', data);
const headers = {
...this.getDefaultHeaders(),
'Content-Type': 'application/x-www-form-urlencoded'
};
console.log('请求头部:', headers);
const response = await this.axiosInstance.post('https://oauth.secure.pixiv.net/auth/token',
stringify(data),
{ headers }
);
console.log('响应状态:', response.status);
console.log('响应数据:', JSON.stringify(response.data, null, 2));
const tokenData = response.data.response;
this.accessToken = tokenData.access_token;
this.refreshToken = tokenData.refresh_token;
this.user = tokenData.user;
console.log('获取访问令牌成功');
return {
success: true,
access_token: tokenData.access_token,
refresh_token: tokenData.refresh_token,
user: tokenData.user
};
} catch (error) {
console.error('获取访问令牌失败:');
console.error('错误对象:', error);
console.error('响应状态:', error.response?.status);
console.error('响应数据:', error.response?.data);
console.error('错误消息:', error.message);
return {
success: false,
error: error.response?.data || error.message
};
}
}
/**
* 使用刷新令牌更新访问令牌
*/
async refreshAccessToken(refreshToken) {
try {
console.log('正在刷新访问令牌...');
const data = {
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
get_secure_url: true,
include_policy: true,
grant_type: 'refresh_token',
refresh_token: refreshToken
};
const headers = {
...this.getDefaultHeaders(),
'Content-Type': 'application/x-www-form-urlencoded'
};
const response = await this.axiosInstance.post('https://oauth.secure.pixiv.net/auth/token',
stringify(data),
{ headers }
);
const tokenData = response.data.response;
this.accessToken = tokenData.access_token;
this.refreshToken = tokenData.refresh_token;
console.log('刷新访问令牌成功');
return {
success: true,
access_token: tokenData.access_token,
refresh_token: tokenData.refresh_token
};
} catch (error) {
console.error('刷新访问令牌失败:', error.response?.data || error.message);
return {
success: false,
error: error.response?.data || error.message
};
}
}
/**
* 获取用户信息
*/
async getUserInfo() {
if (!this.accessToken) {
return { success: false, error: '未登录' };
}
try {
const headers = {
...this.getDefaultHeaders(),
'Authorization': `Bearer ${this.accessToken}`
};
const response = await this.axiosInstance.get('https://app-api.pixiv.net/v1/user/me', {
headers
});
return {
success: true,
user: response.data.user
};
} catch (error) {
console.error('获取用户信息失败:', error.response?.data || error.message);
return {
success: false,
error: error.response?.data || error.message
};
}
}
/**
* 登出
*/
logout() {
this.accessToken = null;
this.refreshToken = null;
this.user = null;
console.log('已登出');
return { success: true };
}
/**
* 获取当前状态
*/
getStatus() {
return {
isLoggedIn: !!this.accessToken,
user: this.user,
hasRefreshToken: !!this.refreshToken
};
}
}
module.exports = PixivAuth;