tauri+oauth登录

611 阅读2分钟

oauth + tauri 登录

场景

进行登录时,会向后端登录接口传入一个 redirectUrl,在在登录成功后访问该页面。

  	redirectURL := c.DefaultQuery("redirect_url", viper.GetString("_sso.redirect_url"))

example:

redirectUrl=localhost:3030时

则后端接口返回的认证地址为

http://privilege.test.vvv:8080/ssoChoice?RequestURL=http%3A%2F%2Flocalhost%3A3030%2Fapi%2Flogin

需要在登陆成功后,获取在 redirectURL 后拼接的校验信息 token,并将其写入 tauri 应用程序的 localStorage。

实现方案

  • 在本地使用 rust 开启一个服务,并将该服务地址作为 redirectUrl 传给后端。
  • 通过 tauri::api::shell 唤起默认浏览器打开认证地址。登录成功后,则会访问这个本地服务。
  • 当访问本地 rust 服务时,通过 emit 传递一个事件,并将 req.uri 中的 token 取出作为事件信息传输。
  • 前端 listen 这个事件,取出其中的信息(token)写入 localStorage

启动 rust 服务- actix_web 框架

cargo.toml 添加依赖

  [dependencies]
  actix-web = "4"

搭建服务,新建 server.rs HttpServer接受一个AppHandle(application factory)作为参数,并且为每个线程创建一个应用程序实例, 因此必须多次构建应用程序

如果需要在多个线程中共享数据,需要使用共享对象,eg:Mutex,Arc

这里我们需要emit,使用Mutex包装AppHandle

使用app_data存储该数据,可以在路由中使用提取器获取数据。

use actix_web::{get, post, web, App, HttpResponse, HttpServer, Responder};
use std::{sync::Mutex};

use actix_web::{middleware, web, App, HttpServer};
use tauri::AppHandle;

struct TauriAppState {
    app: Mutex<AppHandle>,
}

#[get("/")]
async fn hello() -> impl Responder {
  HttpResponse::Ok().body("Hello world!")
}

#[post("/echo")]
async fn echo(req_body: String) -> impl Responder {
  HttpResponse::Ok().body(req_body)
}

<!-- 根据需求login函数接受两个参数req和data-->
async fn login(req:HttpRequest,data:web::Data<TauriAppState>) -> impl Responder {

  let app_handle = data.app.lock().unwrap();

  app_handle.emit_all("get_token", Some(req.uri().to_string())).unwrap();

  HttpResponse::Ok().body("login success!")
}


#[actix_web::main]
pub async fn inti(app:AppHandle) -> std::io::Result<()> {

    let tauri_app = web::Data::new(TauriAppState {
        app: Mutex::new(app_handle),
    });

    HttpServer::new(|| {
        App::new()
            .app_data(tauri_app.clone())
            .service(hello)
            .service(echo)

            <!-- 这里不太清楚为什么将login封装为一个函数,仅仅能在初始化的时候会emit,但是后续不会再传递信息-->

            .route("/login", web::get().to(login))

            <!-- 这样写成闭包的形式就不会出现上述情况-->

            .route("/login", web::get().to(
                    |req: actix_web::HttpRequest, data: web::Data<TauriAppState>| async move {
                    let app_handle = data.app.lock().unwrap();
                    app_handle.emit_all("get_token", Some(req.uri().to_string())).unwrap();
                    actix_web::HttpResponse::Ok()
                        .body("Login success,Please close the webpage and return to the application")
                },

            ))
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}

可以使用 scope 或 resource 为路由挂载基本路径

在tauri程序中启动服务,为了防止程序主进程终端,使用thread模块在一个单独的线程中运行服务器

fn main() {
    tauri::Builder::default()
        .setup(|app| {

            let handle = app.handle();
            let boxed_handle = Box::new(handle);

            thread::spawn(move || {
                server::init(*boxed_handle).unwrap();
            });
            Ok(())

        })
        .invoke_handler(tauri::generate_handler![greet])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

shell打开login_webview


#[tauri::command]
fn open_login_webview<R: Runtime>(app_handle: AppHandle<R>, url: &str) -> Result<(), String> {
    shell::open(&app_handle.shell_scope(), format!("{}", url), None).unwrap();
    Ok(())
}

前端


  await invoke('plugin:webview|open_login_webview', {
    url: webviewUrl,
  });
  const unlisten = await listen('get_token', (event) => {
    const url = (event.payload as string).split('token=');
    if (url[1].length > 0) {
      localStg.set('token', url[1]);
      location.reload();
    }
  });