Rust命令行下OAuth2.0认证优化方案咨询
问题描述
我正在开发一个基于OAuth2.0的Rust应用,当前认证流程繁琐:生成认证链接后用open crate打开浏览器,用户点击“允许”后需手动复制重定向链接粘贴回命令行。我对该方案不满意,想咨询两个方向的解决办法:
- 是否存在无需浏览器的OAuth2.0认证方式?
- 能否让认证后的重定向直接指向我的Rust命令行程序,无需手动复制粘贴?
相关API为MyAnimeList的OAuth2授权接口,当前实现代码如下:
use std::{error::Error, fs::File, io::Write}; use reqwest::Body; use serde_json::Value; use url::Url; use super::{CLIENT_ID, TOKEN}; pub async fn login() -> Result<(), Box<dyn Error>> { // 创建并打开认证链接 let code_verify = pkce::code_verifier(50); let code_challenge = pkce::code_challenge(&code_verify); open::that(&format!("https://myanimelist.net/v1/oauth2/authorize?response_type=code&client_id={}&code_challenge={code_challenge}", CLIENT_ID.clone()))?; // 接收用户输入的重定向URL println!("认证完成后,请输入重定向的URL:"); let mut input = String::new(); std::io::stdin() .read_line(&mut input) .expect("读取输入失败"); // 从URL中解析授权码 let url = Url::parse(input.trim()).unwrap(); let code = url .query_pairs() .find(|(param, _)| param == "code") .map(|(_, value)| value.into_owned()) .expect("提取授权码失败"); // 请求访问令牌 let client = reqwest::Client::new(); let res = client .post("https://myanimelist.net/v1/oauth2/token") .header("content-type", "application/x-www-form-urlencoded") .body(Body::from(format!( "client_id={}&code={}&code_verifier={}&grant_type=authorization_code", CLIENT_ID.clone(), code, code_challenge ))) .send() .await?; let json: Value = res.json().await?; // 将令牌保存到全局变量和文件 let mut token = TOKEN.lock().unwrap(); *token = json["access_token"].as_str().unwrap_or("").to_string(); let mut file = File::create("token.txt")?; file.write_all(&*token.as_bytes())?; Ok(()) }
解决方案
一、无需浏览器的认证方式
MyAnimeList的OAuth2官方文档显示,它仅支持授权码流程(Authorization Code Flow),该流程强制要求用户通过浏览器跳转至官方登录页面完成身份验证,因此没有完全无需浏览器的官方认证方式。
若你的应用仅用于个人脚本场景,可考虑密码授权流程(Password Grant),但需注意:
- MyAnimeList未公开支持该流程,使用可能存在合规或功能失效风险;
- 该方式要求用户直接输入账号密码到你的应用,违背OAuth2的安全设计,存在账号泄露风险,绝对不推荐用于公开分发的应用。
二、让重定向自动回到命令行程序
可通过在本地启动临时HTTP服务器,将重定向URI设置为http://localhost:<端口>/callback,用户授权后浏览器会自动跳转至该本地地址,程序可从临时服务器直接获取授权码,无需手动复制粘贴。
实现步骤
- 在MyAnimeList的API配置后台,添加
http://localhost:8080/callback(端口可自定义)为合法重定向URI; - 修改Rust代码,在打开浏览器前启动临时HTTP服务器,监听指定端口的
/callback路径; - 浏览器跳转至该路径时,服务器提取授权码并传递给主程序,随后自动关闭服务器。
修改后的示例代码
use std::{error::Error, fs::File, io::Write, net::SocketAddr}; use reqwest::Body; use serde_json::Value; use url::Url; use warp::{Filter, Reply}; use super::{CLIENT_ID, TOKEN}; pub async fn login() -> Result<(), Box<dyn Error>> { // 生成PKCE参数 let code_verify = pkce::code_verifier(50); let code_challenge = pkce::code_challenge(&code_verify); // 定义临时服务器地址与回调路径逻辑 let addr = SocketAddr::from(([127, 0, 0, 1], 8080)); let callback_route = warp::path("callback") .and(warp::query::<std::collections::HashMap<String, String>>()) .map(|query: std::collections::HashMap<String, String>| { // 提取授权码并存储 if let Some(code) = query.get("code") { let mut code_store = CODE_STORE.lock().unwrap(); *code_store = Some(code.clone()); "认证完成!可关闭此页面。".into_response() } else { "未找到授权码!".into_response() } }); // 启动后台临时服务器 let (server, server_handle) = warp::serve(callback_route).bind_with_graceful_shutdown(addr, async { shutdown_signal().await; }); tokio::spawn(server); // 打开带指定重定向URI的认证链接 let auth_url = format!( "https://myanimelist.net/v1/oauth2/authorize?response_type=code&client_id={}&code_challenge={}&redirect_uri=http://localhost:8080/callback", CLIENT_ID.clone(), code_challenge ); open::that(&auth_url)?; // 等待获取授权码 println!("等待认证完成..."); let code = loop { let mut code_store = CODE_STORE.lock().unwrap(); if let Some(code) = code_store.take() { break code; } tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; }; // 关闭临时服务器 server_handle.abort(); // 请求访问令牌(修正原代码错误:此处需用code_verify而非code_challenge) let client = reqwest::Client::new(); let res = client .post("https://myanimelist.net/v1/oauth2/token") .header("content-type", "application/x-www-form-urlencoded") .body(Body::from(format!( "client_id={}&code={}&code_verifier={}&grant_type=authorization_code&redirect_uri=http://localhost:8080/callback", CLIENT_ID.clone(), code, code_verify ))) .send() .await?; let json: Value = res.json().await?; // 保存令牌到全局变量与文件 let mut token = TOKEN.lock().unwrap(); *token = json["access_token"].as_str().unwrap_or("").to_string(); let mut file = File::create("token.txt")?; file.write_all(&*token.as_bytes())?; Ok(()) } // 全局存储授权码(实际项目推荐用tokio::sync::oneshot通道替代) lazy_static::lazy_static! { static ref CODE_STORE: std::sync::Mutex<Option<String>> = std::sync::Mutex::new(None); } // 监听Ctrl+C信号用于关闭服务器 async fn shutdown_signal() { tokio::signal::ctrl_c() .await .expect("无法监听Ctrl+C信号"); }
注意事项
- 需要添加依赖:
warp、tokio(启用fullfeature)、lazy_static; - 原代码中请求令牌时错误使用了
code_challenge,正确参数应为code_verify,示例已修正; - 必须在MyAnimeList的API配置中添加对应重定向URI,否则会触发授权错误。
内容的提问来源于stack exchange,提问作者Naginipython
相关产品推荐
相关产品推荐

