開(kāi)始試用Rust的Web開(kāi)發(fā)組件actix-web
1. 使用cargo new新建一個(gè)項(xiàng)目rust_login用于實(shí)現(xiàn)用戶登錄功能肌括。
2. 在Cargo.toml文件中配置需要的依賴
```
[package]
name = "rust_login"
version = "0.1.0"
authors = ["Tianlang <tianlangstuido@aliyun.com>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
actix-web="2" #使用的actix-web 提供web服務(wù)器溃睹、request解析筝蚕、response生成等功能
actix-rt="1" #actix-rt actix的運(yùn)行時(shí)运悲,用于運(yùn)行異步函數(shù)等它呀,可以理解為Java concurrent下的Executor
#serde用于序列化和反序列化對(duì)象的海诲,比如把對(duì)象轉(zhuǎn)換成一個(gè)Json字符串,就是序列化页屠;
#把Json字符串轉(zhuǎn)換為一個(gè)對(duì)象阴幌,就是反序列化
serde="1"
```
3.? 在src/main.rs文件中敲入以下代碼
```rust
use actix_web::{post, web, App, HttpServer, Responder};
use serde::Deserialize;
//用于表示請(qǐng)求傳來(lái)的Json對(duì)象
#[derive(Deserialize)]
struct LoginInfo {
? username: String,
? password: String,
}
#[post("/login")] //聲明請(qǐng)求方式和請(qǐng)求路徑,接受post方式請(qǐng)求/login路徑
async fn index(login_info: web::Json<LoginInfo>) -> impl Responder {
? ? format!("Hello {}! password:{}",login_info.username , login_info.password)
}
#[actix_rt::main]
async fn main() -> std::io::Result<()> {
//啟動(dòng)http服務(wù)器
? ? HttpServer::new(|| App::new().service(index))
? ? ? ? .bind("127.0.0.1:8088")?
? ? ? ? .run()
? ? ? ? .await
}
```
4. 使用cargo run 運(yùn)行程序
5. 執(zhí)行curl請(qǐng)求我們編寫的login路徑
```bash
curl -v -H "Content-Type:application/json" -X POST --data '{"username":"tianalng", password:"tianlang"}' http://127.0.0.1:8088/login
```
沒(méi)有訪問(wèn)成功:
```
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 8088 (#0)
> POST /login HTTP/1.1
> Host: 127.0.0.1:8088
> User-Agent: curl/7.58.0
> Accept: */*
> Content-Type:application/json
> Content-Length: 44
>
* upload completely sent off: 44 out of 44 bytes
< HTTP/1.1 400 Bad Request
< content-length: 0
< date: Sat, 16 May 2020 23:20:07 GMT
<
* Connection #0 to host 127.0.0.1 left intact
```
從返回的錯(cuò)誤信息
> 400 Bad Request
可以看出這是因?yàn)榭蛻舳苏?qǐng)求不滿足服務(wù)端也就是我們寫的login服務(wù)要求造成的
一般看到4開(kāi)始的http錯(cuò)誤碼卷中,我們可以認(rèn)為是客戶端沒(méi)寫好。如果是5開(kāi)頭的可以認(rèn)為是服務(wù)端沒(méi)寫好渊抽。
也可以搜索下:
```
在 ajax 請(qǐng)求后臺(tái)數(shù)據(jù)時(shí)比較常見(jiàn)蟆豫。產(chǎn)生 HTTP 400 錯(cuò)誤的原因有:
1、前端提交數(shù)據(jù)的字段名稱或者是字段類型和后臺(tái)的實(shí)體類不一致懒闷,導(dǎo)致無(wú)法封裝十减;
2、前端提交的到后臺(tái)的數(shù)據(jù)應(yīng)該是 json 字符串類型愤估,而前端沒(méi)有將對(duì)象轉(zhuǎn)化為字符串類型
```
接下來(lái)我們檢查下curl命令帮辟,可以看到password缺少雙引號(hào),把雙引號(hào)加上玩焰,再執(zhí)行下:
```
curl -v -H "Content-Type:application/json" -X POST --data '{"username":"tianalng", "password":"tianlang"}' http://127.0.0.1:8088/login
Note: Unnecessary use of -X or --request, POST is already inferred.
*? Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 8088 (#0)
> POST /login HTTP/1.1
> Host: 127.0.0.1:8088
> User-Agent: curl/7.58.0
> Accept: */*
> Content-Type:application/json
> Content-Length: 46
>
* upload completely sent off: 46 out of 46 bytes
< HTTP/1.1 200 OK
< content-length: 33
< content-type: text/plain; charset=utf-8
< date: Sat, 16 May 2020 23:22:48 GMT
<
* Connection #0 to host 127.0.0.1 left intact
Hello tianalng! password:tianlang
```
這次就成功了
現(xiàn)在我們可以獲取到用戶提交的用戶名密碼了由驹,簡(jiǎn)單起見(jiàn),接下來(lái)我們判斷用戶名是不是等于密碼昔园,如果相等就返回OK告訴客戶端登錄成功了蔓榄,如果不相等就返回Error告訴客戶端登錄失敗了并炮。
在index函數(shù)中使用if語(yǔ)句判斷用戶名是否跟密碼一致,如果一致就返回成功如果不一致就返回失敗甥郑,當(dāng)然這里也可以使用match,代碼如下:
```rust
#[post("/login")]
async fn index(login_info: web::Json<LoginInfo>) -> impl Responder {
? ? if login_info.username == login_info.password {
? ? ? ? HttpResponse::Ok().json("success")
? ? } else {
? ? ? ? HttpResponse::Forbidden().json("password error")
? ? }
}
```
>其中HttpResponse::Ok設(shè)置結(jié)果成功也就是對(duì)應(yīng)http的狀態(tài)碼200
>HttpResponse::Forbidden設(shè)置結(jié)果為拒絕請(qǐng)求也就是對(duì)應(yīng)http的狀態(tài)碼403
你可以繼續(xù)使用curl分別使用與用戶名一致的密碼和不一致的密碼測(cè)試:
```bash
curl -v -H "Cication/json" -X POST --data '{"username":"tianlang", "password":"tianlang"}' http://127.0.0.1:8088/login
```
```
Note: Unnecessary use of -X or --request, POST is already inferred.
*? Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 8088 (#0)
> POST /login HTTP/1.1
> Host: 127.0.0.1:8088
> User-Agent: curl/7.58.0
> Accept: */*
> Content-Type:application/json
> Content-Length: 46
>
* upload completely sent off: 46 out of 46 bytes
**< HTTP/1.1 200 OK**
< content-length: 9
< content-type: application/json
< date: Sat, 23 May 2020 11:36:30 GMT
<
* Connection #0 to host 127.0.0.1 left intact
**"success"**
```
```bash
curl -v -H "Content-ication/json" -X POST --data '{"username":"tianlang", "password":"wrong"}' http://127.0.0.1:8088/login
```
```
*? Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 8088 (#0)
> POST /login HTTP/1.1
**> Host: 127.0.0.1:8088**
> User-Agent: curl/7.58.0
> Accept: */*
> Content-Type:application/json
> Content-Length: 43
>
* upload completely sent off: 43 out of 43 bytes
< HTTP/1.1 403 Forbidden
< content-length: 16
< content-type: application/json
< date: Sat, 23 May 2020 11:37:27 GMT
<
* Connection #0 to host 127.0.0.1 left intact
**"password error"**
```
也可以使用postman構(gòu)造一個(gè)post請(qǐng)求:
![rust login success](https://img-blog.csdnimg.cn/20200523204743217.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3RpYW5sYW5nc3R1ZGlv,size_16,color_FFFFFF,t_70)
這樣就可以根據(jù)客戶端提供的數(shù)據(jù)返回不同的結(jié)果了逃魄,代碼已提交[github](https://github.com/TianLangStudio/rust_login)
## 現(xiàn)在還存在個(gè)問(wèn)題:
雖然是調(diào)用的json設(shè)置的返回結(jié)果,但返回結(jié)果仍然是一個(gè)普通的字符串澜搅,在前端頁(yè)面是不能調(diào)用JSON.parse()轉(zhuǎn)換為json對(duì)象的伍俘。接下來(lái)我們要定義個(gè)struct統(tǒng)一表示返回的數(shù)據(jù)樣式,這樣客戶端可以統(tǒng)一轉(zhuǎn)換成json方便解析處理勉躺。
首先我們定義一個(gè)struct用來(lái)表示http接口返回的數(shù)據(jù)癌瘾,按照傳統(tǒng)命名為AjaxResult.
```rust
#[derive(Deserialize)]
#[derive(Serialize)]
struct AjaxResult<T> {
? ? msg: String,
? ? data: Option<Vec<T>>,
}
```
需要把它序列化成json,所以需要給它添加
>#[derive(Serialize)]
注解
字段msg用來(lái)存儲(chǔ)接口執(zhí)行的結(jié)果信息,接口執(zhí)行成功統(tǒng)一設(shè)置為 success,接口執(zhí)行失敗就設(shè)置為失敗信息赂蕴。
字段data用來(lái)存儲(chǔ)返回的數(shù)據(jù)柳弄,數(shù)據(jù)不是必須的,比如在接口執(zhí)行失敗的時(shí)候就沒(méi)有數(shù)據(jù)返回概说,所以data字段是Option類型碧注。
為了方便創(chuàng)建AjaxResut對(duì)象我們?cè)偬砑有╆P(guān)聯(lián)函數(shù):
```rust
const MSG_SUCCESS: &str = "success";
impl<T> AjaxResult<T> {
? ? pub fn success(data_opt: Option<Vec<T>>) -> Self{
? ? ? ? Self {
? ? ? ? ? ? msg: MSG_SUCCESS.to_string(),
? ? ? ? ? ? data: data_opt
? ? ? ? }
? ? }
? ? pub fn success_without_data() -> Self {
? ? ? ? Self::success(Option::None)
? ? }
? ? pub fn success_with_single(single: T) -> Self{
? ? ? ? Self {
? ? ? ? ? ? msg:? MSG_SUCCESS.to_string(),
? ? ? ? ? ? data: Option::Some(vec![single])
? ? ? ? }
? ? }
? ? pub fn fail(msg: String) -> Self {
? ? ? ? Self {
? ? ? ? ? ? msg,
? ? ? ? ? ? data: None
? ? ? ? }
? ? }
}
```
接下來(lái)修改login函數(shù),不再返回一個(gè)字符串而是返回AjaxRsult對(duì)象:
```rust
#[post("/login")]
async fn index(login_info: web::Json<LoginInfo>) -> impl Responder {
? ? if login_info.username == login_info.password {
? ? ? ? HttpResponse::Ok().json(AjaxResult::<bool>::success_without_data())
? ? } else {
? ? ? ? HttpResponse::Forbidden().json(AjaxResult::<bool>::fail("password must match username".to_string()))
? ? }
}
```
>AjaxResult::<bool> 這里的bool不是設(shè)置返回值數(shù)據(jù)類型因?yàn)槲覀円矝](méi)有返回?cái)?shù)據(jù)而是為了告訴Rust編譯器我們使用的泛型T的類型糖赔,不然它推導(dǎo)不出來(lái)就編譯出錯(cuò)了萍丐。這里的bool可以換成i32、String等
在執(zhí)行下接口調(diào)用:
![retun json object](https://img-blog.csdnimg.cn/20200525112021841.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3RpYW5sYW5nc3R1ZGlv,size_16,color_FFFFFF,t_70)
這時(shí)返回的數(shù)據(jù)就是標(biāo)準(zhǔn)的json對(duì)象了放典,方便前端解析處理逝变。
以前我們?cè)O(shè)計(jì)AjaxResult對(duì)象時(shí),也會(huì)包含一個(gè)數(shù)字類型的code字段用于區(qū)分不同的執(zhí)行結(jié)果錯(cuò)誤類型奋构。我們這里直接復(fù)用http的狀態(tài)碼壳影,就不需要定義這個(gè)字段了。
這也是設(shè)計(jì)Restful API的指導(dǎo)思想:
> 不是把所有的參數(shù)都盡量放到path里就是Resulful了弥臼,Restful是盡量復(fù)用已有的http規(guī)范宴咧。
> **純屬個(gè)人言論,如有誤導(dǎo)概不負(fù)責(zé)**?
代碼已提交[github](https://github.com/TianLangStudio/rust_login)
## 現(xiàn)在還有個(gè)問(wèn)題:
如果用戶已經(jīng)登錄過(guò)了就不需要再判斷用戶名密碼了径缅,浪費(fèi)資源掺栅,直接返回就可以了奏属,怎么實(shí)現(xiàn)呢法焰? 也就是如果用戶已經(jīng)登錄過(guò)了,我們?cè)趺粗烙脩粢呀?jīng)登錄過(guò)了呢蚜锨?
這個(gè)我們可以借助Session實(shí)現(xiàn)氏堤,Session一般代表從用戶打開(kāi)瀏覽器訪問(wèn)網(wǎng)站到關(guān)閉瀏覽器無(wú)論中間瀏覽過(guò)多少次網(wǎng)頁(yè)一般都屬于一個(gè)Session沙绝。 ~~注意這里說(shuō)的一般情況,有的瀏覽器可能行為不一樣~~ 可以在用戶第一次登錄成功后把用戶的登錄信息放入到Session中,判斷用戶名密碼之前先在Session中找有沒(méi)有用戶信息如果有就代表用戶已經(jīng)登錄過(guò)了宿饱,如果沒(méi)有再接著判斷用戶名密碼是否一致熏瞄。要使用Session需要在Cargo.toml文件中配置actix-session依賴:
```toml
[dependencies]
actix-web="2"
actix-rt="1"
actix-session="0.3"
```
修改login函數(shù)中的代碼如下:
```rust
const SESSION_USER_KEY: &str = "user_info";
#[post("/login")]
async fn index(session: Session, login_info: web::Json<LoginInfo>) -> impl Responder {
? ? match session.get::<String>(SESSION_USER_KEY) {
? ? ? ? Ok(Some(user_info)) if user_info == login_info.username => {
? ? ? ? ? ? println!("already logged in");
? ? ? ? ? ? HttpResponse::Ok().json(AjaxResult::<bool>::success_without_data())
? ? ? ? }
? ? ? ? _ => {
? ? ? ? ? ? println!("login now");
? ? ? ? ? ? if login_info.username == login_info.password {
? ? ? ? ? ? ? ? session.set::<String>(SESSION_USER_KEY, login_info.username.clone());
? ? ? ? ? ? ? ? HttpResponse::Ok().json(AjaxResult::<bool>::success_without_data())
? ? ? ? ? ? } else {
? ? ? ? ? ? ? ? HttpResponse::Forbidden().json(AjaxResult::<bool>::fail("password must match username".to_string()))
? ? ? ? ? ? }
? ? ? ? }
? ? }
}
```
另外需要在創(chuàng)建Server時(shí)配置Session中間件
```rust
#[actix_rt::main]
async fn main() -> std::io::Result<()> {
? ? HttpServer::new(|| App::new()
? ? ? ? .wrap(
? ? ? ? ? ? CookieSession::signed(&[0; 32]) // <- create cookie based session middleware
? ? ? ? ? ? ? ? .secure(false),
? ? ? ? ).service(index))
? ? ? ? .bind("127.0.0.1:8088")?
? ? ? ? .run()
? ? ? ? .await
}
```
現(xiàn)在我們?cè)偈褂肞ostman訪問(wèn)登錄接口,第一次控制臺(tái)會(huì)輸出:
> login now
第二次就會(huì)輸出:
> already logged in
在Postman中也可以看到多了一個(gè)cookie,細(xì)看你細(xì)看這就是我們放入Session的用戶信息:
![cookie](https://img-blog.csdnimg.cn/2020052920055647.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3RpYW5sYW5nc3R1ZGlv,size_16,color_FFFFFF,t_70)
當(dāng)前的actix session中間件只支持cookie存儲(chǔ)方式谬以,也可以自己實(shí)現(xiàn)基于Redis的存儲(chǔ)方式强饮。
## 現(xiàn)在還有個(gè)問(wèn)題
如果一個(gè)用戶看到了我們的cookie,從cookie的內(nèi)容就可以看出我們這里就是用戶名,那他是不是只要知道了別人的用戶名就可以偽造這個(gè)cookie模仿其他用戶登錄?
代碼已提交[github](https://github.com/TianLangStudio/rust_login)