如何在 actix-web 中制作受保护的路线
How can I make protected routes in actix-web
我需要验证用户是否有某些路由的权限。
我做了 3 个“范围”(来宾、auth-user、管理员),现在我不知道如何检查用户是否有权访问这些路由。
我正在尝试实施 auth-middleware,此中间件应检查用户是否拥有正确的 cookie 或令牌。 (我能够从请求 header 中打印出一个 cookie),但我不知道如何导入、使用 actix_identity 以及如何访问此中间件中的 id 参数。
我相信我的问题不仅与 Actix-identity 有关,而且我无法在中间件内部传递参数。
#[actix_rt::main]
async fn main() -> std::io::Result<()> {
let cookie_key = conf.server.key;
// Register http routes
let mut server = HttpServer::new(move || {
App::new()
// Enable logger
.wrap(Logger::default())
.wrap(IdentityService::new(
CookieIdentityPolicy::new(cookie_key.as_bytes())
.name("auth-cookie")
.path("/")
.secure(false),
))
//limit the maximum amount of data that server will accept
.data(web::JsonConfig::default().limit(4096))
//normal routes
.service(web::resource("/").route(web::get().to(status)))
// .configure(routes)
.service(
web::scope("/api")
// guest endpoints
.service(web::resource("/user_login").route(web::post().to(login)))
.service(web::resource("/user_logout").route(web::post().to(logout)))
// admin endpoints
.service(
web::scope("/admin")
// .wrap(AdminAuthMiddleware)
.service(
web::resource("/create_admin").route(web::post().to(create_admin)),
)
.service(
web::resource("/delete_admin/{username}/{_:/?}")
.route(web::delete().to(delete_admin)),
),
)
//user auth routes
.service(
web::scope("/auth")
// .wrap(UserAuthMiddleware)
.service(web::resource("/get_user").route(web::get().to(get_user))),
),
)
});
// Enables us to hot reload the server
let mut listenfd = ListenFd::from_env();
server = if let Some(l) = listenfd.take_tcp_listener(0).unwrap() {
server.listen(l)?
} else {
server.bind(ip)?
};
server.run().await
我尝试过的资源:
为 Actix 创建身份验证中间件 API
https://www.jamesbaum.co.uk/blether/creating-authentication-middleware-actix-rust-react/
Actix-web 中间件中的令牌验证 https://users.rust-lang.org/t/actix-web-token-validation-in-middleware/38205
Actix 中间件示例https://github.com/actix/examples/tree/master/middleware
也许我认为完全错误,auth-middleware 不是解决我的问题的最佳方法。
我希望你能帮我创建“受保护的路线”
以下不使用中间件(需要做更多的工作)但它解决了最小熊的问题,似乎是文档中建议的方法:
#[macro_use]
extern crate actix_web;
use actix::prelude::*;
use actix_identity::{CookieIdentityPolicy, Identity, IdentityService};
use actix_web::{
dev::Payload, error::ErrorUnauthorized, web, App, Error, FromRequest, HttpRequest,
HttpResponse, HttpServer, Responder,
};
use log::{info, warn};
use serde::{Deserialize, Serialize};
use std::{collections::HashMap, pin::Pin, sync::RwLock};
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
struct Sessions {
map: HashMap<String, User>,
}
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
#[serde(rename_all = "camelCase")]
struct Login {
id: String,
username: String,
scope: Scope,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "camelCase")]
enum Scope {
Guest,
User,
Admin,
}
impl Default for Scope {
fn default() -> Self {
Scope::Guest
}
}
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
#[serde(rename_all = "camelCase")]
struct User {
id: String,
first_name: Option<String>,
last_name: Option<String>,
authorities: Scope,
}
impl FromRequest for User {
type Config = ();
type Error = Error;
type Future = Pin<Box<dyn Future<Output = Result<User, Error>>>>;
fn from_request(req: &HttpRequest, pl: &mut Payload) -> Self::Future {
let fut = Identity::from_request(req, pl);
let sessions: Option<&web::Data<RwLock<Sessions>>> = req.app_data();
if sessions.is_none() {
warn!("sessions is empty(none)!");
return Box::pin(async { Err(ErrorUnauthorized("unauthorized")) });
}
let sessions = sessions.unwrap().clone();
Box::pin(async move {
if let Some(identity) = fut.await?.identity() {
if let Some(user) = sessions
.read()
.unwrap()
.map
.get(&identity)
.map(|x| x.clone())
{
return Ok(user);
}
};
Err(ErrorUnauthorized("unauthorized"))
})
}
}
#[get("/admin")]
async fn admin(user: User) -> impl Responder {
if user.authorities != Scope::Admin {
return HttpResponse::Unauthorized().finish();
}
HttpResponse::Ok().body("You are an admin")
}
#[get("/account")]
async fn account(user: User) -> impl Responder {
web::Json(user)
}
#[post("/login")]
async fn login(
login: web::Json<Login>,
sessions: web::Data<RwLock<Sessions>>,
identity: Identity,
) -> impl Responder {
let id = login.id.to_string();
let scope = &login.scope;
//let user = fetch_user(login).await // from db?
identity.remember(id.clone());
let user = User {
id: id.clone(),
last_name: Some(String::from("Doe")),
first_name: Some(String::from("John")),
authorities: scope.clone(),
};
sessions.write().unwrap().map.insert(id, user.clone());
info!("login user: {:?}", user);
HttpResponse::Ok().json(user)
}
#[post("/logout")]
async fn logout(sessions: web::Data<RwLock<Sessions>>, identity: Identity) -> impl Responder {
if let Some(id) = identity.identity() {
identity.forget();
if let Some(user) = sessions.write().unwrap().map.remove(&id) {
warn!("logout user: {:?}", user);
}
}
HttpResponse::Unauthorized().finish()
}
#[actix_rt::main]
async fn main() -> std::io::Result<()> {
env_logger::init();
let sessions = web::Data::new(RwLock::new(Sessions {
map: HashMap::new(),
}));
HttpServer::new(move || {
App::new()
.app_data(sessions.clone())
.wrap(IdentityService::new(
CookieIdentityPolicy::new(&[0; 32])
.name("test")
.secure(false),
))
.service(account)
.service(login)
.service(logout)
.service(admin)
})
.bind("127.0.0.1:8088")?
.run()
.await
}
您可以在此处克隆并运行它:https://github.com/geofmureithi/actix-acl-example
嗯,这在最新的 actix-web 3.0 版中实际上很难实现。我所做的是从 actix-web 1.0 version and modified it to my liking. However this is not plug & play code. Here and here 复制 CookieIdentityPolicy 中间件是我的版本。通常我会避免 actix-web,让线程/actor 在后台生成并让它执行 HTTP 请求是一场噩梦。然后尝试与处理程序共享结果。
改用提取器
为了在 Actix 3 中实现这种模式,我绞尽脑汁尝试使用中间件,主要是做一个守卫,然后弄清楚如何将数据从中间件传递到处理程序。这是痛苦的,最终我意识到我是在对抗 Actix 而不是它。
最后我了解到向处理程序获取信息的方法是创建一个结构(AuthedUser
,也许?)并在该结构上实现 FromRequest
特征。
然后,每个在函数签名中请求 AuthedUser
的处理程序都将被授权,如果用户已登录,则您将在 [=14] 中附加到 AuthedUser
的任何用户信息=]方法。
Actix 将这些实现 FromRequest
的结构称为 提取器 。这有点神奇,可以在指南中多加注意。
我认为 actix-web grants crate 非常适合你。
它允许您使用 Guard
或过程宏(请参阅 examples on github)检查授权。
它还与现有的授权中间件(如 actix-web-httpauth
)很好地集成。
为了清楚起见,举几个例子:
proc-macro
方式
#[get("/secure")]
#[has_permissions("ROLE_ADMIN")]
async fn macro_secured() -> HttpResponse {
HttpResponse::Ok().body("ADMIN_RESPONSE")
}
Guard
方式
App::new()
.wrap(GrantsMiddleware::with_extractor(extract))
.service(web::resource("/admin")
.to(|| async { HttpResponse::Ok().finish() })
.guard(PermissionGuard::new("ROLE_ADMIN".to_string())))
也可以看看actix-casbin-auth(actix集成casbin的实现)
middleware 看起来对它定义的所有泛型和内部类型都不是很友好,但它是一个简单的结构,将下一个服务包装到
叫做。下一个服务是什么由创建应用程序或定义路由时的链式调用决定。您在中间件中使用通用 S,它将在编译时单态化,因此您不必关心中间件将保护哪种具体类型。
以下中间件使用通过 .data() 传递给您的应用程序的简单配置来检查 'token' header 是否包含相同的魔法值。它要么通过下一个服务,要么 return 未授权错误(期货)。
use crate::config::Config;
use actix_service::{Service, Transform};
use actix_web::{
dev::{ServiceRequest, ServiceResponse},
error::ErrorUnauthorized,
web::Data,
Error,
};
use futures::future::{err, ok, Either, Ready};
use std::task::{Context, Poll};
pub struct TokenAuth;
impl<S, B> Transform<S> for TokenAuth
where
S: Service<Request = ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
B: 'static,
{ type Request = ServiceRequest;
type Response = ServiceResponse<B>;
type Error = Error;
type InitError = ();
type Transform = TokenAuthMiddleware<S>;
type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
ok(TokenAuthMiddleware { service })
}
}
pub struct TokenAuthMiddleware<S> {
service: S,
}
impl<S, B> Service for TokenAuthMiddleware<S>
where
S: Service<Request = ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
{ type Request = ServiceRequest;
type Response = ServiceResponse<B>;
type Error = Error;
type Future = Either<S::Future, Ready<Result<Self::Response, Self::Error>>>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.service.poll_ready(cx)
}
fn call(&mut self, req: ServiceRequest) -> Self::Future {
if let Some(token) = req
.headers()
.get("token")
.and_then(|token| token.to_str().ok())
{
if let Some(config) = req.app_data::<Data<Config>>() {
if token == config.token {
return Either::Left(self.service.call(req));
}
}
}
Either::Right(err(ErrorUnauthorized("not authorized")))
}
}
保护您的功能就这么简单
#[post("/upload", wrap="TokenAuth")]
async fn upload(mut payload: Multipart) -> Result<HttpResponse, Error> {
}
请注意,您需要 actix_service 1.x 才能编译。 actix_service 2 删除请求内部类型以使其通用,我无法使用 wrap="" 语法使其工作
我需要验证用户是否有某些路由的权限。 我做了 3 个“范围”(来宾、auth-user、管理员),现在我不知道如何检查用户是否有权访问这些路由。
我正在尝试实施 auth-middleware,此中间件应检查用户是否拥有正确的 cookie 或令牌。 (我能够从请求 header 中打印出一个 cookie),但我不知道如何导入、使用 actix_identity 以及如何访问此中间件中的 id 参数。
我相信我的问题不仅与 Actix-identity 有关,而且我无法在中间件内部传递参数。
#[actix_rt::main]
async fn main() -> std::io::Result<()> {
let cookie_key = conf.server.key;
// Register http routes
let mut server = HttpServer::new(move || {
App::new()
// Enable logger
.wrap(Logger::default())
.wrap(IdentityService::new(
CookieIdentityPolicy::new(cookie_key.as_bytes())
.name("auth-cookie")
.path("/")
.secure(false),
))
//limit the maximum amount of data that server will accept
.data(web::JsonConfig::default().limit(4096))
//normal routes
.service(web::resource("/").route(web::get().to(status)))
// .configure(routes)
.service(
web::scope("/api")
// guest endpoints
.service(web::resource("/user_login").route(web::post().to(login)))
.service(web::resource("/user_logout").route(web::post().to(logout)))
// admin endpoints
.service(
web::scope("/admin")
// .wrap(AdminAuthMiddleware)
.service(
web::resource("/create_admin").route(web::post().to(create_admin)),
)
.service(
web::resource("/delete_admin/{username}/{_:/?}")
.route(web::delete().to(delete_admin)),
),
)
//user auth routes
.service(
web::scope("/auth")
// .wrap(UserAuthMiddleware)
.service(web::resource("/get_user").route(web::get().to(get_user))),
),
)
});
// Enables us to hot reload the server
let mut listenfd = ListenFd::from_env();
server = if let Some(l) = listenfd.take_tcp_listener(0).unwrap() {
server.listen(l)?
} else {
server.bind(ip)?
};
server.run().await
我尝试过的资源:
为 Actix 创建身份验证中间件 API https://www.jamesbaum.co.uk/blether/creating-authentication-middleware-actix-rust-react/
Actix-web 中间件中的令牌验证 https://users.rust-lang.org/t/actix-web-token-validation-in-middleware/38205
Actix 中间件示例https://github.com/actix/examples/tree/master/middleware
也许我认为完全错误,auth-middleware 不是解决我的问题的最佳方法。 我希望你能帮我创建“受保护的路线”
以下不使用中间件(需要做更多的工作)但它解决了最小熊的问题,似乎是文档中建议的方法:
#[macro_use]
extern crate actix_web;
use actix::prelude::*;
use actix_identity::{CookieIdentityPolicy, Identity, IdentityService};
use actix_web::{
dev::Payload, error::ErrorUnauthorized, web, App, Error, FromRequest, HttpRequest,
HttpResponse, HttpServer, Responder,
};
use log::{info, warn};
use serde::{Deserialize, Serialize};
use std::{collections::HashMap, pin::Pin, sync::RwLock};
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
struct Sessions {
map: HashMap<String, User>,
}
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
#[serde(rename_all = "camelCase")]
struct Login {
id: String,
username: String,
scope: Scope,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "camelCase")]
enum Scope {
Guest,
User,
Admin,
}
impl Default for Scope {
fn default() -> Self {
Scope::Guest
}
}
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
#[serde(rename_all = "camelCase")]
struct User {
id: String,
first_name: Option<String>,
last_name: Option<String>,
authorities: Scope,
}
impl FromRequest for User {
type Config = ();
type Error = Error;
type Future = Pin<Box<dyn Future<Output = Result<User, Error>>>>;
fn from_request(req: &HttpRequest, pl: &mut Payload) -> Self::Future {
let fut = Identity::from_request(req, pl);
let sessions: Option<&web::Data<RwLock<Sessions>>> = req.app_data();
if sessions.is_none() {
warn!("sessions is empty(none)!");
return Box::pin(async { Err(ErrorUnauthorized("unauthorized")) });
}
let sessions = sessions.unwrap().clone();
Box::pin(async move {
if let Some(identity) = fut.await?.identity() {
if let Some(user) = sessions
.read()
.unwrap()
.map
.get(&identity)
.map(|x| x.clone())
{
return Ok(user);
}
};
Err(ErrorUnauthorized("unauthorized"))
})
}
}
#[get("/admin")]
async fn admin(user: User) -> impl Responder {
if user.authorities != Scope::Admin {
return HttpResponse::Unauthorized().finish();
}
HttpResponse::Ok().body("You are an admin")
}
#[get("/account")]
async fn account(user: User) -> impl Responder {
web::Json(user)
}
#[post("/login")]
async fn login(
login: web::Json<Login>,
sessions: web::Data<RwLock<Sessions>>,
identity: Identity,
) -> impl Responder {
let id = login.id.to_string();
let scope = &login.scope;
//let user = fetch_user(login).await // from db?
identity.remember(id.clone());
let user = User {
id: id.clone(),
last_name: Some(String::from("Doe")),
first_name: Some(String::from("John")),
authorities: scope.clone(),
};
sessions.write().unwrap().map.insert(id, user.clone());
info!("login user: {:?}", user);
HttpResponse::Ok().json(user)
}
#[post("/logout")]
async fn logout(sessions: web::Data<RwLock<Sessions>>, identity: Identity) -> impl Responder {
if let Some(id) = identity.identity() {
identity.forget();
if let Some(user) = sessions.write().unwrap().map.remove(&id) {
warn!("logout user: {:?}", user);
}
}
HttpResponse::Unauthorized().finish()
}
#[actix_rt::main]
async fn main() -> std::io::Result<()> {
env_logger::init();
let sessions = web::Data::new(RwLock::new(Sessions {
map: HashMap::new(),
}));
HttpServer::new(move || {
App::new()
.app_data(sessions.clone())
.wrap(IdentityService::new(
CookieIdentityPolicy::new(&[0; 32])
.name("test")
.secure(false),
))
.service(account)
.service(login)
.service(logout)
.service(admin)
})
.bind("127.0.0.1:8088")?
.run()
.await
}
您可以在此处克隆并运行它:https://github.com/geofmureithi/actix-acl-example
嗯,这在最新的 actix-web 3.0 版中实际上很难实现。我所做的是从 actix-web 1.0 version and modified it to my liking. However this is not plug & play code. Here and here 复制 CookieIdentityPolicy 中间件是我的版本。通常我会避免 actix-web,让线程/actor 在后台生成并让它执行 HTTP 请求是一场噩梦。然后尝试与处理程序共享结果。
改用提取器
为了在 Actix 3 中实现这种模式,我绞尽脑汁尝试使用中间件,主要是做一个守卫,然后弄清楚如何将数据从中间件传递到处理程序。这是痛苦的,最终我意识到我是在对抗 Actix 而不是它。
最后我了解到向处理程序获取信息的方法是创建一个结构(AuthedUser
,也许?)并在该结构上实现 FromRequest
特征。
然后,每个在函数签名中请求 AuthedUser
的处理程序都将被授权,如果用户已登录,则您将在 [=14] 中附加到 AuthedUser
的任何用户信息=]方法。
Actix 将这些实现 FromRequest
的结构称为 提取器 。这有点神奇,可以在指南中多加注意。
我认为 actix-web grants crate 非常适合你。
它允许您使用 Guard
或过程宏(请参阅 examples on github)检查授权。
它还与现有的授权中间件(如 actix-web-httpauth
)很好地集成。
为了清楚起见,举几个例子:
proc-macro
方式
#[get("/secure")]
#[has_permissions("ROLE_ADMIN")]
async fn macro_secured() -> HttpResponse {
HttpResponse::Ok().body("ADMIN_RESPONSE")
}
Guard
方式
App::new()
.wrap(GrantsMiddleware::with_extractor(extract))
.service(web::resource("/admin")
.to(|| async { HttpResponse::Ok().finish() })
.guard(PermissionGuard::new("ROLE_ADMIN".to_string())))
也可以看看actix-casbin-auth(actix集成casbin的实现)
middleware 看起来对它定义的所有泛型和内部类型都不是很友好,但它是一个简单的结构,将下一个服务包装到 叫做。下一个服务是什么由创建应用程序或定义路由时的链式调用决定。您在中间件中使用通用 S,它将在编译时单态化,因此您不必关心中间件将保护哪种具体类型。
以下中间件使用通过 .data() 传递给您的应用程序的简单配置来检查 'token' header 是否包含相同的魔法值。它要么通过下一个服务,要么 return 未授权错误(期货)。
use crate::config::Config;
use actix_service::{Service, Transform};
use actix_web::{
dev::{ServiceRequest, ServiceResponse},
error::ErrorUnauthorized,
web::Data,
Error,
};
use futures::future::{err, ok, Either, Ready};
use std::task::{Context, Poll};
pub struct TokenAuth;
impl<S, B> Transform<S> for TokenAuth
where
S: Service<Request = ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
B: 'static,
{ type Request = ServiceRequest;
type Response = ServiceResponse<B>;
type Error = Error;
type InitError = ();
type Transform = TokenAuthMiddleware<S>;
type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
ok(TokenAuthMiddleware { service })
}
}
pub struct TokenAuthMiddleware<S> {
service: S,
}
impl<S, B> Service for TokenAuthMiddleware<S>
where
S: Service<Request = ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
{ type Request = ServiceRequest;
type Response = ServiceResponse<B>;
type Error = Error;
type Future = Either<S::Future, Ready<Result<Self::Response, Self::Error>>>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.service.poll_ready(cx)
}
fn call(&mut self, req: ServiceRequest) -> Self::Future {
if let Some(token) = req
.headers()
.get("token")
.and_then(|token| token.to_str().ok())
{
if let Some(config) = req.app_data::<Data<Config>>() {
if token == config.token {
return Either::Left(self.service.call(req));
}
}
}
Either::Right(err(ErrorUnauthorized("not authorized")))
}
}
保护您的功能就这么简单
#[post("/upload", wrap="TokenAuth")]
async fn upload(mut payload: Multipart) -> Result<HttpResponse, Error> {
}
请注意,您需要 actix_service 1.x 才能编译。 actix_service 2 删除请求内部类型以使其通用,我无法使用 wrap="" 语法使其工作