开始试用Rust的Web开发组件actix-web
1. 使用cargo new新建一个项目rust_login用于实现用户登录功能。
2. 在Cargo.toml文件中配置需要的依赖
```
[package]
name = "rust_login"
version = "0.1.0"
authors = ["Tianlang
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服务器、request解析、response生成等功能
actix-rt="1" #actix-rt actix的运行时,用于运行异步函数等,可以理解为Java concurrent下的Executor
#serde用于序列化和反序列化对象的,比如把对象转换成一个Json字符串,就是序列化;
#把Json字符串转换为一个对象,就是反序列化
serde="1"
```
3. 在src/main.rs文件中敲入以下代码
```rust
use actix_web::{post, web, App, HttpServer, Responder};
use serde::Deserialize;
//用于表示请求传来的Json对象
#[derive(Deserialize)]
struct LoginInfo {
username: String,
password: String,
}
#[post("/login")] //声明请求方式和请求路径,接受post方式请求/login路径
async fn index(login_info: web::Json
format!("Hello {}! password:{}",login_info.username , login_info.password)
}
#[actix_rt::main]
async fn main() -> std::io::Result<()> {
//启动http服务器
HttpServer::new(|| App::new().service(index))
.bind("127.0.0.1:8088")?
.run()
.await
}
```
4. 使用cargo run 运行程序
5. 执行curl请求我们编写的login路径
```bash
curl -v -H "Content-Type:application/json" -X POST --data '{"username":"tianalng", password:"tianlang"}' http://127.0.0.1:8088/login
```
没有访问成功:
```
* 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
```
从返回的错误信息
> 400 Bad Request
可以看出这是因为客户端请求不满足服务端也就是我们写的login服务要求造成的
一般看到4开始的http错误码,我们可以认为是客户端没写好。如果是5开头的可以认为是服务端没写好。
也可以搜索下:
```
在 ajax 请求后台数据时比较常见。产生 HTTP 400 错误的原因有:
1、前端提交数据的字段名称或者是字段类型和后台的实体类不一致,导致无法封装;
2、前端提交的到后台的数据应该是 json 字符串类型,而前端没有将对象转化为字符串类型
```
接下来我们检查下curl命令,可以看到password缺少双引号,把双引号加上,再执行下:
```
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
```
这次就成功了
现在我们可以获取到用户提交的用户名密码了,简单起见,接下来我们判断用户名是不是等于密码,如果相等就返回OK告诉客户端登录成功了,如果不相等就返回Error告诉客户端登录失败了。
在index函数中使用if语句判断用户名是否跟密码一致,如果一致就返回成功如果不一致就返回失败,当然这里也可以使用match,代码如下:
```rust
#[post("/login")]
async fn index(login_info: web::Json
if login_info.username == login_info.password {
HttpResponse::Ok().json("success")
} else {
HttpResponse::Forbidden().json("password error")
}
}
```
>其中HttpResponse::Ok设置结果成功也就是对应http的状态码200
>HttpResponse::Forbidden设置结果为拒绝请求也就是对应http的状态码403
你可以继续使用curl分别使用与用户名一致的密码和不一致的密码测试:
```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构造一个post请求:
![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)
这样就可以根据客户端提供的数据返回不同的结果了,代码已提交[github](https://github.com/TianLangStudio/rust_login)
## 现在还存在个问题:
虽然是调用的json设置的返回结果,但返回结果仍然是一个普通的字符串,在前端页面是不能调用JSON.parse()转换为json对象的。接下来我们要定义个struct统一表示返回的数据样式,这样客户端可以统一转换成json方便解析处理。
首先我们定义一个struct用来表示http接口返回的数据,按照传统命名为AjaxResult.
```rust
#[derive(Deserialize)]
#[derive(Serialize)]
struct AjaxResult
msg: String,
data: Option
}
```
需要把它序列化成json,所以需要给它添加
>#[derive(Serialize)]
注解
字段msg用来存储接口执行的结果信息,接口执行成功统一设置为 success,接口执行失败就设置为失败信息。
字段data用来存储返回的数据,数据不是必须的,比如在接口执行失败的时候就没有数据返回,所以data字段是Option类型。
为了方便创建AjaxResut对象我们再添加些关联函数:
```rust
const MSG_SUCCESS: &str = "success";
impl
pub fn success(data_opt: Option
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
}
}
}
```
接下来修改login函数,不再返回一个字符串而是返回AjaxRsult对象:
```rust
#[post("/login")]
async fn index(login_info: web::Json
if login_info.username == login_info.password {
HttpResponse::Ok().json(AjaxResult::
} else {
HttpResponse::Forbidden().json(AjaxResult::
}
}
```
>AjaxResult::
在执行下接口调用:
![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)
这时返回的数据就是标准的json对象了,方便前端解析处理。
以前我们设计AjaxResult对象时,也会包含一个数字类型的code字段用于区分不同的执行结果错误类型。我们这里直接复用http的状态码,就不需要定义这个字段了。
这也是设计Restful API的指导思想:
> 不是把所有的参数都尽量放到path里就是Resulful了,Restful是尽量复用已有的http规范。
> **纯属个人言论,如有误导概不负责**
代码已提交[github](https://github.com/TianLangStudio/rust_login)
## 现在还有个问题:
如果用户已经登录过了就不需要再判断用户名密码了,浪费资源,直接返回就可以了,怎么实现呢? 也就是如果用户已经登录过了,我们怎么知道用户已经登录过了呢?
这个我们可以借助Session实现,Session一般代表从用户打开浏览器访问网站到关闭浏览器无论中间浏览过多少次网页一般都属于一个Session。 ~~注意这里说的一般情况,有的浏览器可能行为不一样~~ 可以在用户第一次登录成功后把用户的登录信息放入到Session中,判断用户名密码之前先在Session中找有没有用户信息如果有就代表用户已经登录过了,如果没有再接着判断用户名密码是否一致。要使用Session需要在Cargo.toml文件中配置actix-session依赖:
```toml
[dependencies]
actix-web="2"
actix-rt="1"
actix-session="0.3"
```
修改login函数中的代码如下:
```rust
const SESSION_USER_KEY: &str = "user_info";
#[post("/login")]
async fn index(session: Session, login_info: web::Json
match session.get::
Ok(Some(user_info)) if user_info == login_info.username => {
println!("already logged in");
HttpResponse::Ok().json(AjaxResult::
}
_ => {
println!("login now");
if login_info.username == login_info.password {
session.set::
HttpResponse::Ok().json(AjaxResult::
} else {
HttpResponse::Forbidden().json(AjaxResult::
}
}
}
}
```
另外需要在创建Server时配置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
}
```
现在我们再使用Postman访问登录接口,第一次控制台会输出:
> login now
第二次就会输出:
> already logged in
在Postman中也可以看到多了一个cookie,细看你细看这就是我们放入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)
当前的actix session中间件只支持cookie存储方式,也可以自己实现基于Redis的存储方式。
## 现在还有个问题
如果一个用户看到了我们的cookie,从cookie的内容就可以看出我们这里就是用户名,那他是不是只要知道了别人的用户名就可以伪造这个cookie模仿其他用户登录?
代码已提交[github](https://github.com/TianLangStudio/rust_login)