Skip to main content

nacos_sdk/api/plugin/auth/
auth_by_http.rs

1use arc_swap::ArcSwap;
2use rand::Rng;
3use std::ops::{Add, Deref};
4use std::sync::Arc;
5use tokio::time::{Duration, Instant};
6
7use crate::api::plugin::{AuthContext, AuthPlugin, LoginIdentityContext};
8
9use super::RequestResource;
10
11pub const USERNAME: &str = "username";
12
13pub const PASSWORD: &str = "password";
14
15pub(crate) const ACCESS_TOKEN: &str = "accessToken";
16
17#[allow(dead_code)]
18pub(crate) const TOKEN_TTL: &str = "tokenTtl";
19
20/// Http login AuthPlugin.
21pub struct HttpLoginAuthPlugin {
22    login_identity: ArcSwap<LoginIdentityContext>,
23    next_login_refresh: ArcSwap<Instant>,
24}
25
26impl Default for HttpLoginAuthPlugin {
27    fn default() -> Self {
28        Self {
29            login_identity: ArcSwap::from_pointee(LoginIdentityContext::default()),
30            next_login_refresh: ArcSwap::from_pointee(Instant::now()),
31        }
32    }
33}
34
35#[async_trait::async_trait]
36impl AuthPlugin for HttpLoginAuthPlugin {
37    async fn login(&self, server_list: Vec<String>, auth_context: AuthContext) {
38        let now_instant = Instant::now();
39        if now_instant.le(self.next_login_refresh.load().deref()) {
40            tracing::debug!("Http login return because now_instant lte next_login_refresh.");
41            return;
42        }
43
44        let username = auth_context
45            .params
46            .get(USERNAME)
47            .expect("Username parameter should exist for HTTP auth")
48            .to_owned();
49        let password = auth_context
50            .params
51            .get(PASSWORD)
52            .expect("Password parameter should exist for HTTP auth")
53            .to_owned();
54
55        let server_addr = {
56            // random one
57            server_list
58                .get(rand::thread_rng().gen_range(0..server_list.len()))
59                .expect("Server list should not be empty")
60                .to_string()
61        };
62
63        let scheme = if cfg!(feature = "tls") {
64            "https"
65        } else {
66            "http"
67        };
68        let login_url = format!("{scheme}://{server_addr}/nacos/v1/auth/login");
69
70        tracing::debug!("Http login with username={username},password={password}");
71
72        let login_response = {
73            let resp = reqwest::Client::new()
74                .post(login_url)
75                .query(&[(USERNAME, username), (PASSWORD, password)])
76                .send()
77                .await;
78            tracing::debug!("Http login resp={resp:?}");
79
80            match resp {
81                Err(e) => {
82                    tracing::error!("Http login error, send response failed, err={e:?}");
83                    None
84                }
85                Ok(resp) => {
86                    let resp_text = resp
87                        .text()
88                        .await
89                        .expect("Response text conversion should succeed");
90                    let resp_obj = serde_json::from_str::<HttpLoginResponse>(&resp_text);
91                    match resp_obj {
92                        Err(e) => {
93                            tracing::error!("Http login error, resp_text={resp_text}, err={e:?}");
94                            None
95                        }
96                        Ok(resp_obj) => Some(resp_obj),
97                    }
98                }
99            }
100        };
101
102        if let Some(login_response) = login_response {
103            let delay_sec = login_response.token_ttl / 10;
104            let new_login_identity = Arc::new(
105                LoginIdentityContext::default()
106                    .add_context(ACCESS_TOKEN, login_response.access_token),
107            );
108            self.login_identity.store(new_login_identity);
109
110            self.next_login_refresh
111                .store(Arc::new(Instant::now().add(Duration::from_secs(delay_sec))));
112        }
113    }
114
115    fn get_login_identity(&self, _: RequestResource) -> LoginIdentityContext {
116        self.login_identity.load().deref().deref().to_owned()
117    }
118}
119
120#[derive(Default, serde::Deserialize)]
121#[serde(rename_all = "camelCase")]
122struct HttpLoginResponse {
123    access_token: String,
124    token_ttl: u64,
125}
126
127#[cfg(test)]
128mod tests {
129    use crate::api::plugin::{AuthContext, AuthPlugin, HttpLoginAuthPlugin, RequestResource};
130
131    #[tokio::test]
132    #[ignore]
133    async fn test_http_login_auth_plugin() {
134        let _ = tracing_subscriber::fmt()
135            .with_max_level(tracing::Level::DEBUG)
136            .try_init();
137
138        let http_auth_plugin = HttpLoginAuthPlugin::default();
139        let server_list = vec!["127.0.0.1:8848".to_string()];
140
141        let auth_context = AuthContext::default()
142            .add_param(crate::api::plugin::USERNAME, "nacos")
143            .add_param(crate::api::plugin::PASSWORD, "nacos");
144
145        http_auth_plugin
146            .login(server_list.clone(), auth_context.clone())
147            .await;
148        let login_identity_1 = http_auth_plugin.get_login_identity(RequestResource::default());
149        assert_eq!(login_identity_1.contexts.len(), 1);
150
151        tokio::time::sleep(tokio::time::Duration::from_millis(111)).await;
152
153        http_auth_plugin.login(server_list, auth_context).await;
154        let login_identity_2 = http_auth_plugin.get_login_identity(RequestResource::default());
155        assert_eq!(login_identity_1.contexts, login_identity_2.contexts)
156    }
157}