添加扫码登录功能,新增登录API接口,更新前端页面和样式,删除不必要的文件。

This commit is contained in:
dockermen 2025-04-08 23:29:14 +08:00
parent c61f09393a
commit ccc7168114
9 changed files with 476 additions and 27 deletions

15
main.py
View File

@ -1,7 +1,8 @@
import os import os
import requests import requests
from flask import Flask, render_template, request, redirect, url_for, session,make_response,send_from_directory from flask import Flask, render_template, request, redirect, url_for, session,make_response,send_from_directory,jsonify
from werkzeug.routing import BaseConverter from werkzeug.routing import BaseConverter
from utils.login import login_quark
app = Flask(__name__) app = Flask(__name__)
#app = Flask(__name__,template_folder='templates') #修改模板目录 #app = Flask(__name__,template_folder='templates') #修改模板目录
@ -50,7 +51,19 @@ def demo2():
resp.status = '404 not found' resp.status = '404 not found'
return resp return resp
@app.route('/login',methods=['POST'])
def login():
# 获取POST请求中的JSON数据
data = request.get_json()
token = data.get('token')
if not token:
print('缺少token参数')
status = login_quark(token)
return jsonify({"status": status})
if __name__ == "__main__": if __name__ == "__main__":
app.config.from_pyfile("config.py") app.config.from_pyfile("config.py")
app.run(host="0.0.0.0") app.run(host="0.0.0.0")

View File

@ -1 +0,0 @@
:root{font-family:system-ui,Avenir,Helvetica,Arial,sans-serif;line-height:1.5;font-weight:400;color-scheme:light dark;color:#ffffffde;background-color:#242424;font-synthesis:none;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}a{font-weight:500;color:#646cff;text-decoration:inherit}a:hover{color:#535bf2}body{margin:0;display:flex;place-items:center;min-width:320px;min-height:100vh}h1{font-size:3.2em;line-height:1.1}button{border-radius:8px;border:1px solid transparent;padding:.6em 1.2em;font-size:1em;font-weight:500;font-family:inherit;background-color:#1a1a1a;cursor:pointer;transition:border-color .25s}button:hover{border-color:#646cff}button:focus,button:focus-visible{outline:4px auto -webkit-focus-ring-color}.card{padding:2em}#app{max-width:1280px;margin:0 auto;padding:2rem;text-align:center}@media (prefers-color-scheme: light){:root{color:#213547;background-color:#fff}a:hover{color:#747bff}button{background-color:#f9f9f9}}.read-the-docs[data-v-830e400e]{color:#888}.logo[data-v-962047bb]{height:6em;padding:1.5em;will-change:filter;transition:filter .3s}.logo[data-v-962047bb]:hover{filter:drop-shadow(0 0 2em #646cffaa)}.logo.vue[data-v-962047bb]:hover{filter:drop-shadow(0 0 2em #42b883aa)}

File diff suppressed because one or more lines are too long

108
static/css/style.css Normal file
View File

@ -0,0 +1,108 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
background-color: #f5f5f5;
color: #333;
}
.container {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
padding: 20px;
}
.login-box {
background-color: #fff;
border-radius: 10px;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
padding: 30px;
width: 100%;
max-width: 400px;
text-align: center;
}
h1 {
margin-bottom: 30px;
color: #1e88e5;
font-size: 24px;
}
.scan-area {
position: relative;
width: 100%;
height: 300px;
margin-bottom: 20px;
border: 1px solid #e0e0e0;
border-radius: 8px;
overflow: hidden;
background-color: #f9f9f9;
}
#camera-container {
width: 100%;
height: 100%;
}
#video {
width: 100%;
height: 100%;
object-fit: cover;
}
#scan-placeholder {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100%;
}
#scan-placeholder img {
width: 80px;
height: 80px;
margin-bottom: 20px;
}
.btn {
background-color: #1e88e5;
color: white;
border: none;
border-radius: 5px;
padding: 12px 20px;
font-size: 16px;
cursor: pointer;
transition: background-color 0.3s;
width: 100%;
margin-top: 10px;
}
.btn:hover {
background-color: #1976d2;
}
#stop-button {
background-color: #e53935;
}
#stop-button:hover {
background-color: #d32f2f;
}
#scan-result {
padding: 20px;
text-align: center;
}
#result-content {
margin-top: 10px;
word-break: break-all;
font-size: 14px;
color: #1e88e5;
}

260
static/js/app.js Normal file
View File

@ -0,0 +1,260 @@
document.addEventListener('DOMContentLoaded', function() {
const video = document.getElementById('video');
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const scanButton = document.getElementById('scan-button');
const stopButton = document.getElementById('stop-button');
const cameraContainer = document.getElementById('camera-container');
const scanPlaceholder = document.getElementById('scan-placeholder');
const scanResult = document.getElementById('scan-result');
const resultContent = document.getElementById('result-content');
// 固定的sign_wg和kps_wg值实际应用中可能需要动态生成
const sign_wg = "AAQHaE4ww2nnIPvofH2SfMv3N6OplcPRjxlgScTZozm/ZCMfQP74bsMLyKW883hZCGY=";
const kps_wg = "AARWcp9UM71t5VzV9i5pBJ4dLXjJ7EZL5a9qz2QVVQtkkmcqS4wQGYtk38CRzW6HH4+5c7qsB9/EtUgkWcd8x/k7h9+PmAHUDvxKHUWnX7iL3h2fH86XJ4cEqwvUnQ77QGs=";
let scanning = false;
let stream = null;
let qrCodeToken = null;
// 创建一个显示消息的函数使用Element UI的Message组件
function showMessage(message, type = 'info') {
//确保ELEMENT已定义
// 使用tailwindcss实现消息提示
// 获取或创建消息容器
let messageContainer = document.getElementById('message-container');
if (!messageContainer) {
messageContainer = document.createElement('div');
messageContainer.id = 'message-container';
messageContainer.className = 'fixed top-0 left-0 right-0 flex flex-col items-center mt-16 z-20 pointer-events-none';
document.body.appendChild(messageContainer);
}
const messageDiv = document.createElement('div');
messageDiv.textContent = message;
messageDiv.className = `px-4 py-2 rounded-md shadow-md mb-2 max-w-xs sm:max-w-sm md:max-w-md break-words ${
type === 'success' ? 'bg-green-500 text-white' :
type === 'error' ? 'bg-red-500 text-white' :
type === 'warning' ? 'bg-yellow-500 text-white' :
'bg-blue-500 text-white'
} transition-opacity duration-300 opacity-100`;
messageContainer.appendChild(messageDiv);
// 3秒后自动消失
setTimeout(() => {
messageDiv.classList.replace('opacity-100', 'opacity-0');
setTimeout(() => {
messageContainer.removeChild(messageDiv);
// 如果没有消息了,移除容器
if (messageContainer.children.length === 0) {
document.body.removeChild(messageContainer);
}
}, 300);
}, 3000);
//同时保留console.log以便调试
console.log(message);
}
// 开始扫码
scanButton.addEventListener('click', startScanning);
// 停止扫码
stopButton.addEventListener('click', stopScanning);
function startScanning() {
if (scanning) return;
// 显示摄像头区域,隐藏占位符
cameraContainer.style.display = 'block';
scanPlaceholder.style.display = 'none';
scanResult.style.display = 'none';
scanButton.style.display = 'none';
stopButton.style.display = 'block';
// 请求摄像头权限
navigator.mediaDevices.getUserMedia({
video: {
facingMode: 'environment',
width: { ideal: 1280 },
height: { ideal: 720 }
}
})
.then(function(mediaStream) {
stream = mediaStream;
video.srcObject = mediaStream;
video.setAttribute('playsinline', true);
video.play();
scanning = true;
requestAnimationFrame(tick);
showMessage('摄像头已启动,请对准二维码', 'success');
})
.catch(function(error) {
console.error('无法访问摄像头: ', error);
showMessage('无法访问摄像头,请确保已授予摄像头权限', 'error');
resetScanUI();
});
}
function stopScanning() {
if (!scanning) return;
if (stream) {
stream.getTracks().forEach(track => {
track.stop();
});
}
scanning = false;
resetScanUI();
showMessage('已停止扫码', 'warning');
}
function resetScanUI() {
cameraContainer.style.display = 'none';
scanPlaceholder.style.display = 'flex';
stopButton.style.display = 'none';
scanButton.style.display = 'block';
}
function tick() {
if (!scanning) return;
if (video.readyState === video.HAVE_ENOUGH_DATA) {
// 设置canvas大小与视频一致
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
// 在canvas上绘制当前视频帧
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
// 获取图像数据
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
// 使用jsQR库解析二维码
const code = jsQR(imageData.data, imageData.width, imageData.height, {
inversionAttempts: "dontInvert",
});
// 如果检测到二维码
if (code) {
// 使用Element UI的Message组件显示消息
showMessage("检测到二维码: " + code.data, 'success');
// 处理扫码结果
handleScanResult(code.data);
// 停止扫描
stopScanning();
return;
}
}
// 继续扫描
requestAnimationFrame(tick);
}
function handleScanResult(data) {
// 显示扫码结果
scanResult.style.display = 'block';
resultContent.textContent = "扫码成功,正在处理...";
try {
// 从URL中提取token
const tokenMatch = data.match(/token=([^&]+)/);
if (tokenMatch && tokenMatch[1]) {
qrCodeToken = tokenMatch[1];
showMessage("提取到token: " + qrCodeToken, 'success');
// 调用登录API
showMessage(qrCodeToken,'success');
callLoginAPI(qrCodeToken);
} else {
console.error("无法从二维码中提取token");
showMessage("无法从二维码中提取token请重试", 'error');
resultContent.textContent = "无法从二维码中提取token请重试";
}
} catch (e) {
console.error("处理二维码内容失败:", e);
showMessage("处理二维码内容失败: " + e.toString(), 'error');
resultContent.textContent = "扫码内容处理失败: " + e.toString();
}
}
// 调用夸克网盘登录API
function callLoginAPI(token) {
showMessage(token);
resultContent.textContent = "正在登录...";
showMessage("正在使用token登录...", 'info');
// 生成时间戳作为vcode
const vcode = new Date().getTime();
showMessage(vcode, 'info');
// request_id为vcode+5
const request_id = vcode + 5;
// 构建请求参数
const formData = new FormData();
formData.append('client_id', '532');
formData.append('v', '1.2');
formData.append('request_id', request_id.toString());
formData.append('sign_wg', sign_wg);
formData.append('kps_wg', kps_wg);
formData.append('vcode', vcode.toString());
formData.append('token', token);
// 构建完整的URL
const url = '/login';
// 发送请求
fetch(url, {
method: 'POST',
headers: {
"Content-Type": "application/json; charset=utf-8"
},
body: JSON.stringify({"token":token}),
})
.then(response => {
if (!response.status) {
throw new Error('网络请求失败: ' + response.status);
}
return response.json();
})
.then(data => {
showMessage("登录API响应: " + JSON.stringify(data), 'info');
if (data && data.status) {
// 登录成功设置Cookie
resultContent.textContent = "登录成功";
showMessage("登录成功!", 'success');
sendCookieToPCPage(data.data);
} else {
// 登录失败
resultContent.textContent = "登录失败: " + (data.msg || "未知错误");
showMessage("登录失败: " + (data.msg || "未知错误"), 'error');
}
})
.catch(error => {
console.error("登录请求失败:", error);
resultContent.textContent = "登录请求失败: " + error.toString();
showMessage("登录请求失败: " + error.toString(), 'error');
});
}
// 向PC网页传入Cookie的函数
function sendCookieToPCPage(loginData) {
// 这里需要实现sendCookieToPCPage函数
showMessage("正在设置Cookie...", 'info');
}
// 监听来自iframe的消息
// window.addEventListener('message', function(event) {
// console.log("收到消息:", event.data);
// if (event.data && event.data.status) {
// if (event.data.status === 'success') {
// resultContent.textContent = "Cookie设置成功请返回夸克网盘页面查看";
// showMessage("Cookie设置成功请返回夸克网盘页面查看", 'success');
// } else {
// resultContent.textContent = "Cookie设置失败: " + event.data.message;
// showMessage("Cookie设置失败: " + event.data.message, 'error');
// }
// }
// });
});

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -1,15 +1,64 @@
<!doctype html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/static/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + Vue</title> <title>Vite + Vue</title>
<script type="module" crossorigin src="/static/assets/index-i7anWEo5.js"></script> <script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" crossorigin href="/static/assets/index-BGnqF1DV.css"> <script src="https://cdn.jsdelivr.net/npm/vconsole@latest/dist/vconsole.min.js"></script>
<!-- 引入Vue.js -->
<script src="https://unpkg.com/vue@2.6.14/dist/vue.min.js"></script>
<!-- 引入Element UI的样式 -->
<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css" />
</head> </head>
<body> <body>
<div id="app"></div> <nav class="bg-gray-500 p-4 fixed top-0 left-0 right-0 z-10">
<div class="container mx-auto flex justify-between items-center">
<div class="text-white font-bold text-xl">网站名称</div>
<div class="hidden md:flex space-x-6">
<a href="#" class="text-white hover:text-gray-300 transition duration-300 flex items-center">
<svg class="h-5 w-5 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"></path>
</svg>首页
</a>
<a href="#" class="text-white hover:text-gray-300 transition duration-300">产品</a>
<a href="#" class="text-white hover:text-gray-300 transition duration-300">服务</a>
<a href="#" class="text-white hover:text-gray-300 transition duration-300">关于我们</a>
<a href="#" class="box-decoration-clone bg-gradient-to-r from-indigo-600 to-pink-500 text-white px-2 text-white hover:text-gray-300 transition duration-300">联系我们</a>
</div>
</div>
</nav>
<div class="container mx-auto flex justify-center items-center h-screen p-5 overflow-hidden">
<div class="login-box bg-white rounded-lg shadow-lg p-8 w-full max-w-md text-center">
<h1 class="text-2xl font-bold text-blue-600 mb-6">夸克网盘扫码登录</h1>
<div class="scan-area relative w-full h-80 mb-5 border border-gray-200 rounded-lg overflow-hidden bg-gray-50">
<div id="camera-container" class="w-full h-full" style="display: none;">
<video id="video" class="w-full h-full object-cover" playsinline></video>
<canvas id="canvas" class="hidden" style="display: none;"></canvas>
</div>
<div id="scan-placeholder" class="flex flex-col items-center justify-center h-full p-4">
<p class="text-gray-700 mb-2">点击下方按钮启动扫码</p>
<p class="small-text text-gray-500 text-sm">请将摄像头对准夸克网盘PC端的二维码</p>
</div>
<div id="scan-result" class="flex flex-col items-center justify-center h-full p-4" style="display: none;">
<p class="text-green-600 font-bold mb-2">扫码成功!</p>
<div id="result-content" class="text-gray-700"></div>
</div>
</div>
<button id="scan-button" class="btn bg-blue-500 hover:bg-blue-600 text-white py-2 px-6 rounded-md transition duration-300 w-full">开始扫码</button>
<button id="stop-button" class="btn bg-red-500 hover:bg-red-600 text-white py-2 px-6 rounded-md transition duration-300 w-full" style="display: none;">停止扫码</button>
</div>
<div id="debug-output" style="position: fixed; bottom: 0px;"></div>
</div>
<!-- 引入Element UI组件库 -->
<script src="https://unpkg.com/element-ui/lib/index.js"></script>
<script src="https://cdn.jsdelivr.net/npm/jsqr@1.4.0/dist/jsQR.min.js"></script>
<script src="static/js/app.js"></script>
<script>
var vConsole = new window.VConsole();
//重定向console输出到我们的容器
vConsole.execInContext(document.getElementById('debug-output'));
</script>
</body> </body>
</html> </html>

Binary file not shown.

38
utils/login.py Normal file
View File

@ -0,0 +1,38 @@
import requests
import time
s = requests.Session()
s.headers = {
'Accept': 'application/json, text/plain, */*',
'Content-Type': 'application/x-www-form-urlencoded',
'Origin': 'https://b.quark.cn',
'Referer': 'https://b.quark.cn/',
'User-Agent': 'Mozilla/5.0 (Linux; U; Android 15; zh-CN; 2312DRA50C Build/AQ3A.240912.001) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/123.0.6312.80 Quark/7.9.2.771 Mobile Safari/537.36',
'X-Requested-With': 'com.quark.browser',
'sec-ch-ua': '"Android WebView";v="123", "Not:A-Brand";v="8", "Chromium";v="123"',
'sec-ch-ua-mobile': '?1',
'sec-ch-ua-platform': '"Android"'
}
def login_quark(token):
sign_wg = "AAQHaE4ww2nnIPvofH2SfMv3N6OplcPRjxlgScTZozm/ZCMfQP74bsMLyKW883hZCGY=";
kps_wg = "AARWcp9UM71t5VzV9i5pBJ4dLXjJ7EZL5a9qz2QVVQtkkmcqS4wQGYtk38CRzW6HH4+5c7qsB9/EtUgkWcd8x/k7h9+PmAHUDvxKHUWnX7iL3h2fH86XJ4cEqwvUnQ77QGs=";
vcode = int(time.time() * 1000) # 相当于JavaScript中的Date.now(),返回当前时间的毫秒数
request_id = vcode + 5
is_login = False
url = 'https://uop.quark.cn/cas/ajax/loginWithKpsAndQrcodeToken'
queryParams = 'uc_param_str=dsdnfrpfbivesscpgimibtbmnijblauputogpintnwktprchmt&ds=AANx101uRUMl2l2Ot6hnNFQe%2F%2B%2Fmm2JHIQ2Gw28Yo%2FGa2g%3D%3D&dn=85507213341-12b7840e&fr=android&pf=3300&bi=35825&ve=7.9.2.771&ss=407x853&pc=AASZtg30J6cEZGl0meB9hj0E8U9brcrtYubvdgeP%2BOWEz0vpfLJPZfnxjOm%2Fxul3xW7j9n4com6OOMCVoviOk4QO&gi=bTkwBCbSxTOAXUWiJmZaMx19GVxo&mi=2312DRA50C&ni=bTkwBHemD30PJEvXSga2ki3ciKarGqoowKzIca0Rj7inq8M%3D&la=zh&ut=AANx101uRUMl2l2Ot6hnNFQe%2F%2B%2Fmm2JHIQ2Gw28Yo%2FGa2g%3D%3D&nt=5&nw=0&kt=4&pr=ucpro&ch=kk%40store&mt=73ABKMNLPFhfbAKV15H%2BDiEkS8zWXwui&__dt=31265&__t=' + str(int(time.time() * 1000))
data = {
'client_id': '532',
'v': '1.2',
'request_id': request_id,
'sign_wg': sign_wg,
'kps_wg': kps_wg,
'vcode': vcode,
'token': token
}
res =s.post(url, data=data, params=queryParams)
if res.json().get('status') == 2000000:
is_login = True
return is_login