You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Rust命令行下OAuth2.0认证优化方案咨询

问题描述

我正在开发一个基于OAuth2.0的Rust应用,当前认证流程繁琐:生成认证链接后用open crate打开浏览器,用户点击“允许”后需手动复制重定向链接粘贴回命令行。我对该方案不满意,想咨询两个方向的解决办法:

  1. 是否存在无需浏览器的OAuth2.0认证方式?
  2. 能否让认证后的重定向直接指向我的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,用户授权后浏览器会自动跳转至该本地地址,程序可从临时服务器直接获取授权码,无需手动复制粘贴。

实现步骤

  1. 在MyAnimeList的API配置后台,添加http://localhost:8080/callback(端口可自定义)为合法重定向URI;
  2. 修改Rust代码,在打开浏览器前启动临时HTTP服务器,监听指定端口的/callback路径;
  3. 浏览器跳转至该路径时,服务器提取授权码并传递给主程序,随后自动关闭服务器。

修改后的示例代码

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(启用full feature)、lazy_static;
  • 原代码中请求令牌时错误使用了code_challenge,正确参数应为code_verify,示例已修正;
  • 必须在MyAnimeList的API配置中添加对应重定向URI,否则会触发授权错误。

内容的提问来源于stack exchange,提问作者Naginipython

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.03 09:19:57