Преглед изворни кода

若干调整,新增get_last_insert_rowid,搭建鉴权api-/api/auth(未验证,验证流程较复杂),将区域关系由sub改为sup(从记录下级改为记录上级),增加跨域处理(*.layer处),根据鉴权调整用户表结构

zii пре 3 месеци
родитељ
комит
63adc9bce1
12 измењених фајлова са 1055 додато и 59 уклоњено
  1. 662 25
      Cargo.lock
  2. 3 1
      Cargo.toml
  3. 53 8
      data_init.py
  4. 18 0
      docs/鉴权机制.md
  5. 109 0
      src/api/auth.rs
  6. 93 7
      src/api/device.rs
  7. 5 8
      src/api/flow_task.rs
  8. 14 1
      src/api/mod.rs
  9. 67 7
      src/api/user.rs
  10. 2 0
      src/datasource/mod.rs
  11. 7 0
      src/datasource/sqlite.rs
  12. 22 2
      src/main.rs

Разлика између датотеке није приказан због своје велике величине
+ 662 - 25
Cargo.lock


+ 3 - 1
Cargo.toml

@@ -12,4 +12,6 @@ serde_json = "1.0.145"
 axum = { version = "0.8.7", features = ["macros"] }
 bb8 = "0.9.0"
 rand = "0.9.2"
-md5 = "0.8.0"
+md5 = "0.8.0"
+tower-http = { version = "0.6", features = ["cors"] }
+reqwest = { version = "0.11", features = ["json"] }

+ 53 - 8
data_init.py

@@ -12,6 +12,9 @@ id integer PRIMARY KEY,
 uname text not null,
 passwd text not null,
 nickname text not null,
+headimgurl text not null default '',
+openid text not null,
+sex Integer not null default 0,
 token text not null default '',
 createtime timestamp not null default current_timestamp,
 lastlogin timestamp,
@@ -27,7 +30,8 @@ create table if not exists device (
 id integer PRIMARY KEY,
 name text not null,
 typo text not null,
-area integer not null default 0 -- 0:未分配到区域 其他:所在区域编号
+area integer not null default 0, -- 0:未分配到区域 其他:所在区域编号
+isdelete integer not null default 0
 )
 ''').fetchall()
 # 创建区域表
@@ -35,7 +39,8 @@ curs.execute('''
 create table if not exists area (
 id integer PRIMARY KEY,
 name text not null,
-sub integer not null -- 0:无上级区域 其他:下级区域编号
+sup integer not null, -- 0:无上级区域 其他:上级区域编号
+isdelete integer not null default 0
 )
 ''').fetchall()
 # 用户-设备映射表
@@ -43,7 +48,8 @@ curs.execute('''
 create table if not exists map_user_device (
 id integer PRIMARY KEY,
 did integer not null,
-uid integer not null
+uid integer not null,
+unique(did,uid)
 )
 ''').fetchall()
 # 区域-设备映射表
@@ -51,7 +57,8 @@ curs.execute('''
 create table if not exists map_area_device (
 id integer PRIMARY KEY,
 did integer not null,
-aid integer not null
+aid integer not null,
+unique(did,aid)
 )
 ''').fetchall()
 # 用户-区域映射表
@@ -59,7 +66,8 @@ curs.execute('''
 create table if not exists map_user_area (
 id integer PRIMARY KEY,
 uid integer not null,
-aid integer not null
+aid integer not null,
+unique(uid,aid)
 )
 ''').fetchall()
 curs.execute('''select * from user''').fetchall()
@@ -84,12 +92,49 @@ conn.commit()
 #
 # 添加用户
 curs.execute('''
-insert into user (uname,passwd,nickname) values 
-('root', 'e10adc3949ba59abbe56e057f20f883e', 'admin'),
-('admin', 'e10adc3949ba59abbe56e057f20f883e', 'test')
+insert into user (uname,passwd,nickname,openid) values 
+('root', 'e10adc3949ba59abbe56e057f20f883e', 'admin', 'XD'),
+('admin', 'e10adc3949ba59abbe56e057f20f883e', 'test', 'LD')
 ''').fetchall()
 conn.commit()
 
 curs.execute('''
 select * from flow_task_share
 ''').fetchall()
+
+# 添加测试设备、区域、映射关系
+curs.execute('''
+insert into device (name,typo,area) values 
+('设备1', 'a',2),
+('设备2', 'a',4),
+('设备3', 'a',0)
+''').fetchall()
+curs.execute('''
+insert into area (name,sup) values 
+('区域1', 2),
+('区域2', 0),
+('区域3', 4),
+('区域4', 0)
+''').fetchall()
+curs.execute('''
+insert into map_user_device (uid,did) values 
+(1, 1),
+(1, 2),
+(1, 3),
+(1, 4),
+(2, 1),
+(2, 2),
+(2, 3),
+(2, 4)
+''').fetchall()
+curs.execute('''
+insert into map_user_area (uid,aid) values 
+(1, 1),
+(1, 2),
+(1, 3),
+(1, 4),
+(2, 1),
+(2, 2),
+(2, 3),
+(2, 4)
+''').fetchall()

+ 18 - 0
docs/鉴权机制.md

@@ -0,0 +1,18 @@
+# 借助微信鉴权
+
+[https://developers.weixin.qq.com/doc/service/guide/h5/auth.html]
+
+借助微信平台的用户的openid作为鉴权
+
+用户首次使用平台借助snsapi_userinfo登录,从这里获取或创建用户
+
+一般使用snsapi_base登录
+
+scope=snsapi_base
+
+## 参考代码所用的api
+
+Gettoken  # 主要
+getjsapiticket
+Createlink
+Getcode

+ 109 - 0
src/api/auth.rs

@@ -0,0 +1,109 @@
+use axum::{extract::{Query, State}, http::HeaderMap, response::Redirect};
+
+use crate::{AppState, datasource::Datasource};
+use serde::Deserialize;
+
+const APPID: &str = "wx664469117500d259";
+const APPSECRET : &str = "wx664469117500d259";
+
+#[derive(serde::Deserialize)]
+pub struct QueryParams {
+    code: String,
+    state: String,
+}
+
+#[allow(dead_code)]
+#[derive(Deserialize)]
+pub struct WxGetToken{
+    access_token: String,
+	expires_in: usize,
+	refresh_token: String,
+	openid: String,
+	scope:String,
+	url:String,
+	ticket: String,
+	errmsg: String,
+	errcode: usize
+}
+
+
+#[allow(dead_code)]
+#[derive(Deserialize)]
+pub struct WxUserInfo{
+    openid: String,
+	nickname: String,
+	sex: u16,
+	headimgurl: String,
+}
+
+pub async fn auth(
+    headers: HeaderMap,
+    State(state): State<AppState>,
+    Query(q): Query<QueryParams>
+) -> Redirect {
+    /*
+    从公众号跳转时会访问 http://{{host}}/api/auth?code={code}&state={state}
+    code为获取token所需中间码
+    state为授权链接的state用于防止越权,用途不明
+     */
+    // 访问以获取微信token
+    let a:WxGetToken = if let Ok(a) = {if let Ok(a) = reqwest::get(format!(
+        "https://api.weixin.qq.com/sns/oauth2/access_token?appid={}&secret={}&code={}&grant_type=authorization_code",
+        APPID,
+        APPSECRET,
+        q.code
+    )).await{a.json().await} else {return Redirect::to("")}}{a} else {return Redirect::to("/")};
+
+    // let host = headers.get("host")
+    //     .and_then(|hv| hv.to_str().ok())
+    //     .unwrap_or("localhost:3000");
+    let host = "192.168.24.102:5432";
+
+    let token=super::token();
+    if let Ok(id) = super::check_openid(&state, a.openid.clone()).await{
+        if let Err(e) = state.db_lite.execute("update user set token=? where id=?", (token.clone(), id)).await{
+            println!("{e}");
+        };
+    } else if a.scope !="snsapi_userinfo"{
+        return Redirect::to(format!("https://open.weixin.qq.com/connect/oauth2/authorize?appid={}&redirect_uri=https://{}/api/auth&response_type=code&scope=snsapi_userinfo&state={}#wechat_redirect",
+        APPID,
+        host,
+        q.state
+    ).as_str())
+    } else {
+        let b:WxUserInfo = if let Ok(b) = {if let Ok(b) = reqwest::get(format!(
+            "https://api.weixin.qq.com/sns/userinfo?access_token={}&openid={}&lang=zh_CN",
+            a.access_token,
+            a.openid.clone()
+        )).await{b.json().await} else {return Redirect::to("")}}{b} else {return Redirect::to("/")};
+        if let Err(e) = state.db_lite.execute("insert into user (nickname,headimgurl,openid,sex,token) values(?,?,?,?,?)", (b.nickname.clone(),b.headimgurl,b.openid,b.sex,token.clone())).await{
+            println!("{e}");
+        } 
+        let uid = match state.db_lite.last_insert_rowid().await{
+            Ok(id) => id,
+            Err(e) => {
+                println!("{e}");
+                0
+            }
+        };
+        if let Err(e) = state.db_lite.execute(
+            "insert into area (name,sup)values(?,0)", 
+            [format!("{}的家",b.nickname.clone())]
+        ).await{println!("{e}")};
+        let area_sup: i64;
+        if let Err(e) = state.db_lite.execute(
+            "insert into map_user_area (uid,aid)values(?,?)", 
+            [uid,{area_sup=match state.db_lite.last_insert_rowid().await{Ok(id)=>id,Err(e)=>{println!("{e}");0}};area_sup}]
+        ).await{println!("{e}")};
+        if let Err(e) = state.db_lite.execute(
+            "insert into area (name,sup)values('客厅',?)", 
+            [area_sup]
+        ).await{println!("{e}")};
+        return Redirect::to(format!("https://{}/api/auth?token={}#{}",host,token.clone(),q.state).as_str());
+    };
+
+
+    // request::Request::get("body");
+    
+    Redirect::to("")
+}

+ 93 - 7
src/api/device.rs

@@ -1,6 +1,6 @@
 use crate::AppState;
 use super::{JsonBack};
-use super::{token_fail,check_login, errcode0};
+use super::{token_fail,check_token, errcode0};
 use crate::datasource::{Datasource, SqliteParams};
 
 use axum::{Json,extract::State};
@@ -10,25 +10,23 @@ pub struct Dedit{
     token: String,
     id: u64,
     name: Option<String>,
-    area0: Option<u64>,
-    area1: Option<u64>,
+    area: Option<u64>,
 }
 
 pub async fn d_edit(
     State(state): State<AppState>,
     Json(u): axum::extract::Json<Dedit>
 ) -> Json<JsonBack> {
-    match check_login(&state, u.token).await {
+    match check_token(&state, u.token).await {
         Ok(_) => {},
         Err(_) => {
             return token_fail();
         }
     }
     let mut params: SqliteParams = vec![Box::new(0)];
-    match state.db_lite.execute(format!("update device set isdelete=?{}{}{} where id=?",
+    match state.db_lite.execute(format!("update device set isdelete=?{}{} where id=?",
         if let Some(filter) = u.name {params.push(Box::new(filter));", name=?"} else {""},
-        if let Some(filter) = u.area0 {params.push(Box::new(filter));", name=?"} else {""},
-        if let Some(filter) = u.area1 {params.push(Box::new(filter));", name=?"} else {""},
+        if let Some(filter) = u.area {params.push(Box::new(filter));", area=?"} else {""},
     ).as_str(), rusqlite::params_from_iter({params.push(Box::new(u.id));params})).await{
         Ok(_) => {},
         Err(_) => {
@@ -37,3 +35,91 @@ pub async fn d_edit(
     }
     errcode0()
 }
+
+
+
+#[derive(serde::Serialize)]
+pub struct Devices{
+    pub id: u64,
+    pub name: String,
+    pub area: u64
+}
+
+#[derive(serde::Serialize)]
+pub struct Area{
+    pub id: u64,
+    pub name: String,
+    pub sup: u64
+}
+
+#[derive(serde::Serialize)]
+pub struct UTokenBack{
+    pub errcode: i16,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub errmsg: Option<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub devices: Option<Vec<Devices>>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub area: Option<Vec<Area>>
+}
+
+
+pub async fn d_all(
+    State(state): State<AppState>,
+    Json(u): axum::extract::Json<super::Ident>
+) -> Json<UTokenBack> {
+    let uid = if let Ok( id) = check_token(&state, u.token).await {
+        id
+    } else {
+        return Json(UTokenBack{errcode: 2000, errmsg: Some("鉴权失败: token无效".to_string()),devices: None,area: None})
+    };
+    
+    Json(UTokenBack{
+        errcode: 0,
+        errmsg: None,
+        devices: Some(
+            match state.db_lite.query_rows(
+                "select d.id,d.name,d.area from device d left join map_user_device m on d.id=m.did where d.isdelete=0 and m.uid=?", 
+                [uid], 
+                |r|{Ok(Devices{
+                id: r.get::<usize,u64>(0)?,
+                name: r.get::<usize,String>(1)?,
+                area: r.get::<usize,u64>(2)?,
+            })}).await{
+                Ok(ans) => ans,
+                Err(e) => {
+                    println!("{e}");
+                    return Json(UTokenBack{
+                        errcode: 3000,
+                        errmsg: Some(format!("设备信息获取失败")),
+                        devices: None,
+                        area: None
+                    })
+                }
+            }
+            ),
+            area: Some(
+            match state.db_lite.query_rows(
+                "select a.id,a.name,a.sup from area a left join map_user_area m on a.id=m.aid where a.isdelete=0 and m.uid=?", 
+                [uid], 
+                |r|{Ok(Area{
+                id: r.get::<usize,u64>(0)?,
+                name: r.get::<usize,String>(1)?,
+                sup: r.get::<usize,u64>(2)?,
+            })}).await{
+                Ok(ans) => ans,
+                Err(e) => {
+                    println!("{e}");
+                    return Json(UTokenBack{
+                        errcode: 3000,
+                        errmsg: Some(format!("设备信息获取失败")),
+                        devices: None,
+                        area: None
+                    })
+                }
+            }
+            )
+        }
+    )
+}
+

+ 5 - 8
src/api/flow_task.rs

@@ -1,6 +1,6 @@
 use crate::AppState;
 use super::{JsonBack, errcode0, token};
-use super::{token_fail,check_login};
+use super::{token_fail,check_token};
 use crate::datasource::Datasource;
 
 use serde::{Serialize, Deserialize};
@@ -24,7 +24,7 @@ pub async fn new_flow_task_share_device(
     State(state): State<AppState>,
     Json(u): axum::extract::Json<TaskOfDeviceShareOrTransfer>
 ) -> Json<UrlBack> {
-    match check_login(&state, u.token).await {
+    match check_token(&state, u.token).await {
         Ok(_) => {},
         Err(_) => {
             return Json(UrlBack{errcode: 3000, errmsg: Some(format!("鉴权失败: token失效")),url:None});
@@ -60,16 +60,13 @@ pub async fn new_flow_task_share_device(
 pub struct QueryParams {
     ticket: String,
 }
-#[derive(Deserialize)]
-pub struct Ident{
-    token: String
-}
+
 pub async fn checkout_flow_task_of_share_device(
     State(state): State<AppState>,
     Query(params): Query<QueryParams>,
-    Json(u): Json<Ident>
+    Json(u): Json<super::Ident>
 ) -> Json<JsonBack> {
-    let uid = match check_login(&state, u.token).await {
+    let uid = match check_token(&state, u.token).await {
         Ok(id) => id,
         Err(_) => {
             return token_fail();

+ 14 - 1
src/api/mod.rs

@@ -1,4 +1,5 @@
 pub mod user;
+pub mod auth;
 pub mod device;
 pub mod flow_task;
 #[cfg(target_arch = "x86_64")]
@@ -8,6 +9,11 @@ mod code_helper;
 use rand::Rng;
 use crate::datasource::Datasource;
 
+#[derive(serde::Deserialize)]
+pub struct Ident{
+    token: String
+}
+
 #[derive(serde::Serialize)]
 pub struct JsonBack{
     pub errcode: i16,
@@ -48,13 +54,20 @@ pub async fn example(axum::extract::State(_): axum::extract::State<crate::AppSta
     })
 }
 
-pub async fn check_login(state: &crate::AppState, token: String) -> Result<u64,()>{
+pub async fn check_token(state: &crate::AppState, token: String) -> Result<u64,()>{
     state.db_lite.query(
         "select id from user where token=? and isdelete=0", 
         [token],  // 这里不能写作 rusqlite::params![token] 
         |r|{r.get::<usize,u64>(0)}).await.map_err(|e| println!("{e}"))
 }
 
+pub async fn check_openid(state: &crate::AppState, openid: String) -> Result<u64,()>{
+    state.db_lite.query(
+        "select id from user where openid=? and isdelete=0", 
+        [openid],  // 这里不能写作 rusqlite::params![token] 
+        |r|{r.get::<usize,u64>(0)}).await.map_err(|e| println!("{e}"))
+}
+
 pub fn token() -> String{
     rand::rng().sample_iter(&rand::distr::Alphanumeric).take(32).map(char::from).collect()
 }

+ 67 - 7
src/api/user.rs

@@ -1,8 +1,12 @@
 use crate::{AppState, datasource::{Datasource,SqliteParams}};
 use super::{JsonBack, DataBack, Page};
-use super::{check_login,token,md5,token_fail};
+use super::{check_token,token,md5,token_fail};
 use serde::{Deserialize,Serialize};
 use axum::{Json, extract::State};
+/*
+即将弃用
+
+*/
 
 #[derive(Deserialize)]
 pub struct Uloggin{
@@ -14,6 +18,14 @@ pub struct Uloggin{
 pub struct Devices{
     pub id: u64,
     pub name: String,
+    pub area: u64
+}
+
+#[derive(serde::Serialize)]
+pub struct Area{
+    pub id: u64,
+    pub name: String,
+    pub sub: u64
 }
 
 #[derive(serde::Serialize)]
@@ -24,7 +36,9 @@ pub struct UTokenBack{
     #[serde(skip_serializing_if = "Option::is_none")]
     pub token: Option<String>,
     #[serde(skip_serializing_if = "Option::is_none")]
-    pub devices: Option<Vec<Devices>>
+    pub devices: Option<Vec<Devices>>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub area: Option<Vec<Area>>
 }
 
 pub async fn u_loggin(
@@ -44,7 +58,8 @@ pub async fn u_loggin(
                     errcode: 2000,
                     errmsg: Some(format!("登录失败")),
                     token: None,
-                    devices: None
+                    devices: None,
+                    area: None
                 })
             }
         };
@@ -52,12 +67,57 @@ pub async fn u_loggin(
     if let Err(e) = state.db_lite.execute("update user set lastlogin=current_timestamp,token=? where id=?", (token.clone(), uid)).await{
         println!("error when loggin {e}")
     }
+    
     Json(UTokenBack{
         errcode: 0,
         errmsg: None,
         token: Some(token),
-        devices: None
-    })
+        devices: Some(
+            match state.db_lite.query_rows(
+                "select d.id,d.name,d.area from device d where d.isdelete=0", 
+                [], 
+                |r|{Ok(Devices{
+                id: r.get::<usize,u64>(0)?,
+                name: r.get::<usize,String>(1)?,
+                area: r.get::<usize,u64>(0)?,
+            })}).await{
+                Ok(ans) => ans,
+                Err(e) => {
+                    println!("{e}");
+                    return Json(UTokenBack{
+                        errcode: 3000,
+                        errmsg: Some(format!("设备信息获取失败")),
+                        token: None,
+                        devices: None,
+                        area: None
+                    })
+                }
+            }
+            ),
+            area: Some(
+            match state.db_lite.query_rows(
+                "select d.id,d.name,d.area from device d where d.isdelete=0", 
+                [], 
+                |r|{Ok(Area{
+                id: r.get::<usize,u64>(0)?,
+                name: r.get::<usize,String>(1)?,
+                sub: r.get::<usize,u64>(0)?,
+            })}).await{
+                Ok(ans) => ans,
+                Err(e) => {
+                    println!("{e}");
+                    return Json(UTokenBack{
+                        errcode: 3000,
+                        errmsg: Some(format!("设备信息获取失败")),
+                        token: None,
+                        devices: None,
+                        area: None
+                    })
+                }
+            }
+            )
+        }
+    )
 }
 
 #[derive(Deserialize, Serialize)]
@@ -87,7 +147,7 @@ pub async fn u_edit(
         errmsg: Some(format!("不允许创建用户"))
     });
     }
-    let uid = match check_login(&state, u.token.unwrap()).await{
+    let uid = match check_token(&state, u.token.unwrap()).await{
         Ok(uid)=> uid,
         Err(()) => return token_fail()
     };
@@ -138,7 +198,7 @@ pub async fn u_list(
 ) -> Json<DataBack<Vec<Uedit>>> {
     // 在这里实现用户列表查询逻辑
     // 可以使用 u.page.page, u.page.size, u.nickname, u.group
-    let _ = match check_login(&state, u.token).await{
+    let _ = match check_token(&state, u.token).await{
         Ok(uid) => uid,
         Err(_) => return Json(DataBack { errcode: 2000, errmsg: Some("鉴权失败: token失效".to_string()), data: None })
     };

+ 2 - 0
src/datasource/mod.rs

@@ -16,4 +16,6 @@ pub trait Datasource {
     async fn execute<P>(&self, query: &str, params:P) -> Result<usize, String>
     where
         P: rusqlite::Params;
+
+    async fn last_insert_rowid(&self) -> Result<i64, String>;
 }

+ 7 - 0
src/datasource/sqlite.rs

@@ -99,6 +99,13 @@ impl crate::datasource::Datasource for SqlitePool{
             Err(_) => return Err("Timeout".to_string())
         }.execute(sql, params).map_err(|e| if e.sqlite_error_code()==Some(rusqlite::ErrorCode::ConstraintViolation) { println!("{e}");String::new() } else{ e.to_string()})
     }
+    async fn last_insert_rowid(&self) -> Result<i64, String> {
+        Ok(match tokio::time::timeout(std::time::Duration::from_secs(5), self.get()).await {
+             Ok(Ok(conn)) => conn,
+            Ok(Err(e)) => return Err(format!("connection err: {}",e.to_string())),
+            Err(_) => return Err("Timeout".to_string())
+        }.last_insert_rowid())
+    }
 }
 
 

+ 22 - 2
src/main.rs

@@ -1,6 +1,7 @@
 mod datasource;
 mod api;
 use datasource::sqlite;
+use tower_http::cors::{Any, CorsLayer}; // 添加Any和CorsLayer的引用
 
 #[derive(Clone)]
 struct AppState{
@@ -9,22 +10,41 @@ struct AppState{
 
 #[tokio::main]
 async fn main() -> Result<(), Box<dyn std::error::Error>> {
-    let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await?;
+    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
 
     let appstat = AppState{db_lite: sqlite::init_sqlite_pool("./db.sqlite", 10).await?};
 
-    use axum::routing::{post};
+    use axum::routing::{post,get};
     let app =  axum::Router::new()
+         // 应用完整的CORS配置
         .nest("/api", axum::Router::new()
             .route("/loggin", post(api::user::u_loggin))
             .route("/user/edit", post(api::user::u_edit))
             .route("/user/list", post(api::user::u_list))
+            .route("/device", post(api::device::d_all))
+
             .route("/device/edit", post(api::device::d_edit))
             .route("/flow/share/new", post(api::flow_task::new_flow_task_share_device))
             .route("/flow/share/checkout", post(api::flow_task::checkout_flow_task_of_share_device))
+            
+            .route("/auth", get(api::auth::auth))
+
+            // layer需要在最后添加,添加视为入栈,生效顺序为出栈顺序
+            .layer(CorsLayer::new()
+                .allow_origin(Any)
+                .allow_methods(Any)
+                .allow_headers(Any))
         )
+        .route("/", get(|| async {"hello".to_string()}))
+        .route("/", post(|| async {"hello".to_string()}))
         // .route("/hello", get(api::example))
+        
         .with_state(appstat)
+
+        .layer(CorsLayer::new()
+            .allow_origin(Any)
+            .allow_methods(Any)
+            .allow_headers(Any))
     ;
     axum::serve(listener, app).await?;
     Ok(())

Неке датотеке нису приказане због велике количине промена