开始试用Rust的Web开发组件actix-web html
[package] name = "rust_login" version = "0.1.0" authors = ["Tianlang <tianlangstuido@aliyun.com>"] edition = "2018"
[dependencies]
actix-web="2" #使用的actix-web 提供web服务器、request解析、response生成等功能
actix-rt="1" #actix-rt actix的运行时,用于运行异步函数等,能够理解为Java concurrent下的Executor
#serde用于序列化和反序列化对象的,好比把对象转换成一个Json字符串,就是序列化;
#把Json字符串转换为一个对象,就是反序列化
serde="1" 前端
``` git
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<LoginInfo>) -> impl Responder { 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
}github
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
没有访问成功:web
* 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
从返回的错误信息 ajax
400 Bad Request 算法
能够看出这是由于客户端请求不知足服务端也就是咱们写的login服务要求形成的
通常看到4开始的http错误码,咱们能够认为是客户端没写好。若是是5开头的能够认为是服务端没写好。
也能够搜索下:数据库
在 ajax 请求后台数据时比较常见。产生 HTTP 400 错误的缘由有: 一、前端提交数据的字段名称或者是字段类型和后台的实体类不一致,致使没法封装; 二、前端提交的到后台的数据应该是 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告诉客户端登陆失败了。json
在index函数中使用if语句判断用户名是否跟密码一致,若是一致就返回成功若是不一致就返回失败,固然这里也可使用match,代码以下:
#[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设置结果成功也就是对应http的状态码200
HttpResponse::Forbidden设置结果为拒绝请求也就是对应http的状态码403
你能够继续使用curl分别使用与用户名一致的密码和不一致的密码测试:
curl -v -H "Content-Type:application/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"**
curl -v -H "Content-Type:application/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请求:
这样就能够根据客户端提供的数据返回不一样的结果了,代码已提交github
如今还存在个问题:
虽然是调用的json设置的返回结果,但返回结果仍然是一个普通的字符串,在前端页面是不能调用JSON.parse()转换为json对象的。接下来咱们要定义个struct统一表示返回的数据样式,这样客户端能够统一转换成json方便解析处理。
首先咱们定义一个struct用来表示http接口返回的数据,按照传统命名为AjaxResult.
#[derive(Deserialize)] #[derive(Serialize)] struct AjaxResult<T> { msg: String, data: Option<Vec<T>>, }
须要把它序列化成json,因此须要给它添加
#[derive(Serialize)]
注解
字段msg用来存储接口执行的结果信息,接口执行成功统一设置为 success,接口执行失败就设置为失败信息。
字段data用来存储返回的数据,数据不是必须的,好比在接口执行失败的时候就没有数据返回,因此data字段是Option类型。
为了方便建立AjaxResut对象咱们再添加些关联函数: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 } }
}
接下来修改login函数,再也不返回一个字符串而是返回AjaxRsult对象: ```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不是设置返回值数据类型由于咱们也没有返回数据而是为了告诉Rust编译器咱们使用的泛型T的类型,否则它推导不出来就编译出错了。这里的bool能够换成i3二、String等
在执行下接口调用:
这时返回的数据就是标准的json对象了,方便前端解析处理。
之前咱们设计AjaxResult对象时,也会包含一个数字类型的code字段用于区分不一样的执行结果错误类型。咱们这里直接复用http的状态码,就不须要定义这个字段了。
这也是设计Restful API的指导思想:
不是把全部的参数都尽可能放到path里就是Resulful了,Restful是尽可能复用已有的http规范。
纯属我的言论,若有误导概不负责
代码已提交github
如今还有个问题:
若是用户已经登陆过了就不须要再判断用户名密码了,浪费资源,直接返回就能够了,怎么实现呢? 也就是若是用户已经登陆过了,咱们怎么知道用户已经登陆过了呢?记录用户登陆状态
这个咱们能够借助Session实现,Session通常表明从用户打开浏览器访问网站到关闭浏览器不管中间浏览过多少次网页通常都属于一个Session。 注意这里说的通常状况,有的浏览器可能行为不同 能够在用户第一次登陆成功后把用户的登陆信息放入到Session中,判断用户名密码以前先在Session中找有没有用户信息若是有就表明用户已经登陆过了,若是没有再接着判断用户名密码是否一致。要使用Session须要在Cargo.toml文件中配置actix-session依赖:
[dependencies] actix-web="2" actix-rt="1" actix-session="0.3"
修改login函数中的代码以下:
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())) } } } }
另外须要在建立Server时配置Session中间件
#[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的用户信息:![]()
当前的actix session中间件只支持cookie存储方式,也能够本身实现基于Redis的存储方式。
如今还有个问题 :
若是一个用户看到了咱们的cookie,从cookie的内容就能够看出咱们这里就是用户名,那他是否是只要知道了别人的用户名就能够伪造这个cookie模仿其余用户登陆?防止伪造登陆凭证
能够再使用username生成一个签名放到cookie里,用于验证cookie里的用户信息是否是伪造的,这里咱们使用blake2算法生成签名信息,blake2算法跟md5相似,但更加安全。
fn sign(text: &str) -> String { let sign = Blake2b::new() .chain(b"change me every day") .chain(text) .result();
format!("{:X}", sign)
}
**注意生成签名的时候咱们还添加了段文本"change me every day",能够当作生成签名使用的密码,这段文本用户是不知道的,并且会按期变动,这样用户就不能伪造cookie信息了** 在login函数中使用sign函数生成签名信息并在判断用户是否登陆时验证签名信息 ```rust const SESSION_USER_KEY: &str = "user_info"; const SESSION_USER_KEY_SIGN: &str = "user_info_sign"; #[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"); let user_key_sign = sign(&user_info); match session.get::<String>(SESSION_USER_KEY_SIGN) { Ok(Some(user_key_sign_session)) if user_key_sign == user_key_sign_session => { HttpResponse::Ok().json(AjaxResult::<bool>::success_without_data()) } _ => { session.remove(SESSION_USER_KEY_SIGN); session.remove(SESSION_USER_KEY); HttpResponse::Forbidden().json(AjaxResult::<bool>::fail("Login time expired".to_string())) } } } _ => { println!("login now"); if login_info.username == login_info.password { let user_key_sign = sign(&login_info.username); session.set::<String>(SESSION_USER_KEY_SIGN, user_key_sign); 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())) } } } }
如今还存在个问题 :
虽然用户不知道签名信息怎么生成的就很差伪造别人的登陆cookie了,可是还能够经过网络截获的方式也就是在浏览器和服务器之间传递数据时获取别人的cookie用户信息和签名信息,怎么保证cookie在网络传递时是安全的呢?
代码已提交github
若是注意下上面的github访问连接,你会发现它的开头是https而不是http。这是由于https比http要安全的多,具体怎么样安全须要自行搜索https、ssl/tls ,接下来在咱们的程序中集成rustls提供https服务。首先配置cargo.toml文件引入须要的依赖:
[dependencies] actix-web={version = "2.0.0", features=["rustls"]} actix-rt="1" actix-session="0.3" blake2 = "0.8" rustls = "0.16" serde="1" actix-files = "0.2.1"
新增了俩个依赖项:rustls和actix-files 用于读取证书密钥文件,修改了actix-web依赖项,增长features=["rustls"]选项,这跟actix官方的示例配置不同,由于官方示例使用的是1.x版本的actix-web
在main函数中读取证书密钥文件并使用https服务监听8443端口
#[actix_rt::main] async fn main() -> std::io::Result<()> { let mut config = ServerConfig::new(NoClientAuth::new()); let cert_file = &mut BufReader::new(File::open("./conf/cert.pem").unwrap()); let key_file = &mut BufReader::new(File::open("./conf/key.pem").unwrap()); let cert_chain = certs(cert_file).unwrap(); let mut keys = rsa_private_keys(key_file).unwrap(); config.set_single_cert(cert_chain, keys.remove(0)).unwrap(); HttpServer::new(|| App::new() .wrap( CookieSession::signed(&[0; 32]) // <- create cookie based session middleware .secure(false), ).service(index)) .bind_rustls("127.0.0.1:8443", config)? .run() .await }
使用curl访问https服务 :
curl -v -H "Content-Type:application/json" -X POST --insecure /home/tianlang/.local/share/mkcert --data '{"username":"tianlang", "password":"tianlang"}' https://localhost:8443/login
注意 使用了--insecure选项,由于咱们的证书并非权威机构颁发的是咱们本身开发使用的
看到:
{"msg":"success","data":null}
就说明配置成功了!
如今还存在个问题:
咱们通常在开发环境中使用http,环境配置测试起来都方便,在正式环境中才启用https,怎么作到代码编译一次即能在开发环境中使用http用于测试又能发布到正式环境中使用https呢 ?
是否是能够经过features实现?像上面配置actix-web时指定对应的feature启用相应的功能?
不能够,由于features是在代码编译时起做用的,而咱们想在代码运行时控制具体是使用http仍是https.
那怎么办呢? 可使用config引入Config
- 在Cargo.toml配置config依赖
[dependencies] actix-web={version = "2.0.0", features=["rustls"]} actix-rt="1" actix-session="0.3" blake2 = "0.8" rustls = "0.16" serde="1" actix-files = "0.2.1" config="0.10- 在main.rs文件中读取配置文件并根据配置信息启用相应功能
#[actix_rt::main] async fn main() -> std::io::Result<()> { let mut app_config = config::Config::new(); app_config.merge(config::File::with_name("conf/application")).unwrap();
let is_prod = match app_config.get_str("tl.app.mode") { Ok(value) => { let config_file_name = format!("conf/application_{}", value); app_config.merge(config::File::with_name(&config_file_name)).unwrap(); if value == "prod" {true} else {false} } _ => { app_config.merge(config::File::with_name("conf/application_dev")).unwrap(); false } }; app_config.merge(config::Environment::with_prefix("TL_APP")).unwrap(); let server = HttpServer::new(move || App::new() .wrap( CookieSession::signed(&[0; 32]) // <- create cookie based session middleware .secure(is_prod), ).service(index)); if is_prod { let mut config = ServerConfig::new(NoClientAuth::new()); let cert_file = &mut BufReader::new(File::open("./conf/cert.pem").unwrap()); let key_file = &mut BufReader::new(File::open("./conf/key.pem").unwrap()); let cert_chain = certs(cert_file).unwrap(); let mut keys = rsa_private_keys(key_file).unwrap(); config.set_single_cert(cert_chain, keys.remove(0)).unwrap(); server.bind_rustls("127.0.0.1:8443", config)? .run() .await }else { server.bind("127.0.0.1:8088")? .run() .await }
}
代码已提交[github](https://github.com/TianLangStudio/rust_login) 如今还存在个问题: 如今是使用的println把日志信息输出到控制台的,这样日志信息多了很难查找,能不能把日志信息按照等级分类输入到文件中呢? 咱们须要日志支持 ## 添加日志支持 咱们选用log4rs用于日志管理,由于跟Java开发中用到的log4j相似,方便上手。 首先在Cargo.toml文件中配置log和log4rs依赖,log至关于Java开发里用到的slf4j是一个日志门面(参考门面模式)。
og = "0.4"
log4rs="0.12"
在main函数中初始化log4rs ```rust log4rs::init_file("conf/log4rs.yaml", Default::default()).unwrap();
conf/log4rs.yaml是日志配置文件与Java开发中的log4j.properties相似。接下来就能够把main.rs文件里的println!改为info!了
能够经过修改日志配置文件log4rs.yaml将不一样级别的日志写入到不一样文件中
当前程序已经具有了config和log,通常咱们开发正式项目少不了跟数据库打交到,接下来咱们尝试使用rust操做数据库。
Rust开发用什么ORM工具好呢?按照惯例,选一个github上star最多的。此次我选了Diesel。使用diesel把用户登陆信息存储到数据库表中并添加用户注册接口。
至此这个Demo也算五脏俱全了。 能够把这个demo用于开始开发其它项目。
如今不少代码在main.rs文件里,显得有些臃肿不方便后续添加功能。
拆分后的项目目录结构:
rust_cms ├── common │ ├── conf │ ├── log │ └── src ├── doc ├── site │ └── src ├── target │ ├── debug │ └── doc ├── template │ └── src ├── user │ ├── conf -> ../common/conf │ ├── migrations │ └── src └── web └── src
代码拆分方法可参考零基础学新时代编程语言Rust
拆好的代码放在另外一个github仓库中 rust_cms