Rust 웹 앱에서 casbin 인증을 사용하는 방법 [파트 - 3]

이 블로그에서 우리는 이전 블로그에서 이야기한 인증 모델을 사용할 새 프로젝트를 만들 것입니다.
다음은 참조용 github 저장소에 대한 링크입니다.
https://github.com/casbin-rs/examples/tree/master/actix-middleware-example

JWT를 지원하는 Actix-web, Casbin 및 Diesel을 사용하여 간단한 익명 포럼 앱을 만들 것입니다.
이 앱에는 adminuser의 2가지 역할이 있습니다.

시작하겠습니다.
먼저 Cargo.toml -

[dependencies]
http = "0.2.1"
actix =  "0.11.0"
actix-web = "3.3.2"
actix-service = "2.0.0"
actix-rt = "1.1.1"
actix-cors = "0.4.0"
futures = "0.3.5"
failure = "0.1.8"
serde = "1.0.116"
serde_derive = "1.0.116"
serde_json = "1.0.57"
derive_more = "0.99.10"
chrono = { version = "0.4.18", features = ["serde"] }
diesel = { version = "1.4.5", features = ["postgres","r2d2", "chrono"] }
diesel_migrations = "1.4.0"
dotenv = "0.15.0"
env_logger = "0.8.1"
log = "0.4.11"
jsonwebtoken = "7.2.0"
bcrypt = "0.9.0"
csv = "1.1.3"
walkdir = "2.3.1"
actix-casbin= {version = "0.4.2", default-features = false, features = [ "runtime-async-std" ]}
actix-casbin-auth = {version = "0.4.4", default-features = false, features = [ "runtime-async-std" ]}
diesel-adapter = { version = "0.8.1", default-features = false, features = ["postgres","runtime-async-std"] }
uuid = {version = "0.8.1", features = ["v4"] }

casbin.conf 를 포함합니다. (리포지토리 참조)
그리고 preset_policy.csv . (리포지토리 참조)
생성.env -

APP_HOST=127.0.0.1
APP_PORT=8080
DATABASE_URL=postgres://databasename:[email protected]:5432/test
POOL_SIZE=8
HASH_ROUNDS=12


그런 다음 src 폴더에서 main.rs -
외부 상자를 먼저 가져오고 모듈(나중에 만들 예정) -

#![allow(proc_macro_derive_resolution_fallback)]

#[macro_use]
extern crate diesel;
#[macro_use]
extern crate log;
#[macro_use]
extern crate diesel_migrations;
#[macro_use]
extern crate serde_derive;
#[macro_use]
extern crate serde_json;

use crate::utils::csv_utils::{load_csv, walk_csv};
use actix::Supervisor;
use actix_casbin::casbin::{
    function_map::key_match2, CachedEnforcer, CoreApi, DefaultModel, MgmtApi, Result,
};
use actix_casbin::CasbinActor;
use actix_casbin_auth::CasbinService;
use actix_cors::Cors;
use actix_web::middleware::normalize::TrailingSlash;
use actix_web::middleware::Logger;
use actix_web::middleware::NormalizePath;
use actix_web::{App, HttpServer};
use diesel_adapter::DieselAdapter;
use std::env;

mod api;
mod config;
mod constants;
mod errors;
mod middleware;
mod models;
mod routers;
mod schema;
mod services;
mod utils;

HttpServer 를 사용하여 서버를 생성합니다.APP_HOST 와 같은 모든 기본값은 .env 에 정의되어 있습니다.
연결 풀을 정의합니다.

let pool = config::db::migrate_and_config_db(&database_url, pool_size);


기본 풀 크기는 8입니다.
우리는 캐빈 모델을 가져옵니다.

let model = DefaultModel::from_file("casbin.conf").await?;
    let adapter = DieselAdapter::new(database_url, pool_size)?;
    let mut casbin_middleware = CasbinService::new(model, adapter).await.unwrap();
    casbin_middleware
        .write()
        .await
        .get_role_manager()
        .write()
        .unwrap()
        .matching_fn(Some(key_match2), None);

    let share_enforcer = casbin_middleware.get_enforcer();
    let clone_enforcer = share_enforcer.clone();
    let casbin_actor = CasbinActor::<CachedEnforcer>::set_enforcer(share_enforcer)?;
    let started_actor = Supervisor::start(|_| casbin_actor);
 let preset_rules = load_csv(walk_csv("."));
    for mut policy in preset_rules {
        let ptype = policy.remove(0);
        if ptype.starts_with('p') {
            match clone_enforcer.write().await.add_policy(policy).await {
                Ok(_) => info!("Preset policies(p) add successfully"),
                Err(err) => error!("Preset policies(p) add error: {}", err.to_string()),
            };
            continue;
        } else if ptype.starts_with('g') {
            match clone_enforcer
                .write()
                .await
                .add_named_grouping_policy(&ptype, policy)
                .await
            {
                Ok(_) => info!("Preset policies(p) add successfully"),
                Err(err) => error!("Preset policies(g) add error: {}", err.to_string()),
            };
            continue;
        } else {
            unreachable!()
        }
    }


그런 다음 모듈을 정의할 수 있습니다.src/ 디렉토리 안에 다음 디렉토리를 만듭니다.
또한 다음 파일을 만드십시오 - api/config/ .middleware/에서 모델 생성 -models/ , routers/ , services/utils/
프로젝트의 루트에서 다음 명령을 실행하십시오.

cargo install diesel_cli --no-default-features --features postgres


.env Diesel이 Postgres 인스턴스의 연결 세부 정보를 가져오는 데 사용할 DATABASE_URL 속성입니다.
이제 프로젝트 루트 폴더에서 constants.rs를 실행하십시오. Diesel은 새로운 데이터베이스(고백)와 일련의 빈 마이그레이션을 생성합니다.

이제 실행 -

diesel migration generate casbin_rules post users ⏎
diesel migration run


이것은 errors.rs 디렉토리에 models/를 생성합니다. 당신은 그것을 확인할 수 있습니다.
이전에 생성한 post.rs 디렉토리에서 데이터베이스 구성을 정의합니다.

pub fn migrate_and_config_db(url: &str, pool_size: u32) -> Pool {
    info!("Migrating and configurating database...");
    let manager = ConnectionManager::<Connection>::new(url);
    let pool = r2d2::Pool::builder()
        .connection_timeout(Duration::from_secs(10))
        .max_size(pool_size)
        .build(manager)
        .expect("Failed to create pool.");
    embedded_migrations::run(&pool.get().expect("Failed to migrate."))
        .expect("Failed to migrate.");

    pool
}


이제 미들웨어를 작성해 보자. response.rs 디렉토리에서 파일 user.rs 을 만듭니다.
여기에서 역할 기반 HTTP 인증을 구현합니다.
외부 크레이트 및 라이브러리 가져오기 -

#![allow(clippy::type_complexity)]
use crate::{
    config::db::Pool, constants, models::response::ResponseBody, utils::token_utils,
};

use actix_casbin_auth::CasbinVals;
use actix_service::{Service, Transform};
use actix_web::{
    dev::{ServiceRequest, ServiceResponse},
    http::{HeaderName, HeaderValue, Method},
    web::Data,
    Error, HttpMessage, HttpResponse,
};
use futures::{
    future::{ok, Ready},
    Future,
};
use std::cell::RefCell;
use std::rc::Rc;
use std::{
    pin::Pin,
    task::{Context, Poll},
};


그런 다음 공개 구조체를 만듭니다.

pub struct Authentication;


이제 특성user_token.rs을 구현하십시오(문서 참조) -

impl<S, B> Transform<S, ServiceRequest> for Authentication
where
    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
    B: MessageBody,
{
    type Response = ServiceResponse<B>;
    type Error = Error;
    type InitError = ();
    type Transform = AuthenticationMiddleware<S>;
    type Future = Ready<Result<Self::Transform, Self::InitError>>;

    fn new_transform(&self, service: S) -> Self::Future {
        ok(AuthenticationMiddleware {
            service: Rc::new(RefCell::new(service)),
        })
    }
}

diesel setup , schema.rs , src/ , config/middleware/ 는 모두 특성 authn.rs 의 기본 구현에 정의된 관련 유형입니다.Transform 함수는 Response 를 반환합니다.

우리는 또 다른 public 구조체를 만듭니다Error -

pub struct AuthenticationMiddleware<S> {
    service: Rc<RefCell<S>>,
}

InitError에 대해 Transform 구현(문서 참조) -

impl<S, B> Service<ServiceRequest> for AuthenticationMiddleware<S>
where 
    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
    B: MessageBody,
{
    type Response = ServiceResponse<B>;
    type Error = Error;
    type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>>>>;
..
..

FutureTransform 특성에 대한 일반 new_transform와 유사하게 Future 작동하도록 하는 기본 메서드입니다.AuthenticationMiddleware impl 내부에 함수Service를 정의하십시오.

fn call(&mut self, mut req: ServiceRequest) -> Self::Future {
..
..
}

AuthenticationMiddleware 함수 내에서 특정 변수를 정의합니다.
스마트 포인터인 casbinpoll_ready을 rust -Future에 저장합니다.
poll를 사용합니까? - 그 이유는 actixFuture가 단일 스레드인 반면 우리의 casbincall은 다중 스레드이므로 서비스가 각 스레드에 대한 포인터이기 때문입니다.
그런 다음 Service , callservice -

let mut srv = self.service.clone();
let mut authenticate_pass: bool = false;
let mut public_route: bool = false;
let mut authenticate_username: String = String::from("");

// Bypass some account routes
let headers = req.headers_mut();
headers.append(
    HeaderName::from_static("content-length"),
    HeaderValue::from_static("true"),
);


이것이 이 파일의 주요 논리입니다.

        if Method::OPTIONS == *req.method() {
            authenticate_pass = true;
        } else {
            for ignore_route in constants::IGNORE_ROUTES.iter() {
                if req.path().starts_with(ignore_route) {
                    authenticate_pass = true;
                    public_route = true;
                }
            }
            if !authenticate_pass {
                if let Some(pool) = req.app_data::<Data<Pool>>() {
                    info!("Connecting to database...");
                    if let Some(authen_header) =
                        req.headers().get(constants::AUTHORIZATION)
                    {
                        info!("Parsing authorization header...");
                        if let Ok(authen_str) = authen_header.to_str() {
                            if authen_str.starts_with("bearer")
                                || authen_str.starts_with("Bearer")
                            {
                                info!("Parsing token...");
                                let token = authen_str[6..].trim();
                                if let Ok(token_data) =
                                    token_utils::decode_token(token.to_string())
                                {
                                    info!("Decoding token...");
                                    if token_utils::verify_token(&token_data, pool)
                                        .is_ok()
                                    {
                                        info!("Valid token");
                                        authenticate_username = token_data.claims.user;
                                        authenticate_pass = true;
                                    } else {
                                        error!("Invalid token");
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }


매우 간단합니다. db에 연결하고, 헤더에서 인증 토큰을 가져오고, 토큰을 구문 분석하고, 토큰을 디코딩하고, 토큰을 확인하고, 인증합니다.
그런 다음 casbin은 특정 사용자가 경로에 액세스할 수 있는 권한이 있는지 확인합니다.

if authenticate_pass {
            if public_route {
                let vals = CasbinVals {
                    subject: "anonymous".to_string(),
                    domain: None,
                };
                req.extensions_mut().insert(vals);
                Box::pin(async move { srv.call(req).await })
            } else {
                let vals = CasbinVals {
                    subject: authenticate_username,
                    domain: None,
                };
                req.extensions_mut().insert(vals);
                Box::pin(async move { srv.clone().call(req).await })
            }
        } else {
            Box::pin(async move {
                Ok(req.into_response(
                    HttpResponse::Unauthorized()
                        .json(ResponseBody::new(
                            constants::MESSAGE_INVALID_TOKEN,
                            constants::EMPTY,
                        ))
                        .into_body(),
                ))
            })
        }


그런 다음 http 서버를 생성할 때 Rc<RefCell<S>>에서 이것을 Rc<RefCell<>> 사용합니다.

    HttpServer::new(move || {
        App::new()
            .data(pool.clone())
            .data(started_actor.clone())
            .wrap(
                Cors::new()
                    .send_wildcard()
                    .allowed_methods(vec!["GET", "POST", "DELETE"])
                    .allowed_headers(vec![
                        http::header::AUTHORIZATION,
                        http::header::ACCEPT,
                    ])
                    .allowed_header(http::header::CONTENT_TYPE)
                    .max_age(3600)
                    .finish(),
            )
            .wrap(NormalizePath::new(TrailingSlash::Trim))
            .wrap(Logger::default())
            .wrap(casbin_middleware.clone())
            .wrap(crate::middleware::authn::Authentication)
            .configure(routers::routes)
    })
    .bind(&app_url)?
    .run()
    .await?;


그게 다야
이것이 actix-web 앱에서 casbin을 사용하는 방법입니다.

좋은 웹페이지 즐겨찾기