» Rust:使用Rocket构建REST API » 2. 开发 » 2.12 权限认证

权限认证

在 API 服务器中想对特定资源或功能的访问进行控制就需要实行身份权限验证

换句话说,如果你想要将某些 API 端点或数据限制给具有特殊角色的用户(例如管理员、版主或高级用户等)使用,你就需要进行身份权限验证。

传统用户流程

更新代码

添加 User 领域实体,domain/model/user.rs:

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct User {
    pub id: u32,
    pub email: String,
    pub password: String,
    pub salt: String,
    pub is_admin: bool,
    pub created_at: String,
    pub updated_at: String,
}

声明其业务能力,domain/gateway/user_manager.rs:

use std::error::Error;

use crate::domain::model;

pub trait UserManager: Send + Sync {
    fn create_user(&self, u: &model::User) -> Result<u32, Box<dyn Error>>;
    fn get_user_by_email(&self, email: &str) -> Result<Option<model::User>, Box<dyn Error>>;
}

添加其实现,infrastructure/database/mysql.rs:

@@ -4,7 +4,7 @@ use chrono::Utc;
 use mysql::prelude::Queryable;
 use mysql::{Error as MySQLError, Pool};
 
-use crate::domain::gateway::BookManager;
+use crate::domain::gateway::{BookManager, UserManager};
 use crate::domain::model;
 
 pub struct MySQLPersistence {
@@ -145,3 +145,52 @@ impl BookManager for MySQLPersistence {
         Ok(books)
     }
 }
+
+impl UserManager for MySQLPersistence {
+    fn create_user(&self, u: &model::User) -> Result<u32, Box<dyn Error>> {
+        let mut conn = self.pool.get_conn()?;
+        conn.exec::<usize, &str, (String, String, String, bool, String, String)>(
+            "INSERT INTO users (email, password, salt, is_admin, created_at, updated_at)
+             VALUES (?, ?, ?, ?, ?, ?)",
+            (
+                u.email.clone(),
+                u.password.clone(),
+                u.salt.clone(),
+                u.is_admin,
+                u.created_at.clone(),
+                u.updated_at.clone(),
+            ),
+        )?;
+        Ok(conn.last_insert_id() as u32)
+    }
+
+    fn get_user_by_email(&self, email: &str) -> Result<Option<model::User>, Box<dyn Error>> {
+        let mut conn = self.pool.get_conn()?;
+        let users = conn.query_map(
+            format!(
+                "SELECT * FROM users WHERE email = '{}'",
+                email.replace("'", "")
+            ),
+            |(id, email, password, salt, is_admin, created_at, updated_at): (
+                u64,
+                String,
+                String,
+                String,
+                bool,
+                String,
+                String,
+            )| {
+                model::User {
+                    id: id as u32,
+                    email,
+                    password,
+                    salt,
+                    is_admin,
+                    created_at,
+                    updated_at,
+                }
+            },
+        )?;
+        Ok(users.first().cloned())
+    }
+}

添加依赖randsha1hex

cargo add rand sha1 hex

Cargo.toml 中变化:

@@ -7,12 +7,15 @@ edition = "2021"
 
 [dependencies]
 chrono = { version = "0.4.35", features = ["serde"] }
+hex = "0.4.3"
 lazy_static = "1.4.0"
 mongodb = { version = "2.8.2", default-features = false, features = ["sync"] }
 mysql = "24.0.0"
+rand = "0.8.5"
 redis = "0.25.2"
 rocket = { version = "0.5.0", features = ["json"] }
 rusqlite = "0.31.0"
 serde = { version = "1.0.197", features = ["derive"] }
 serde_json = "1.0.114"
+sha1 = "0.10.6"
 toml = "0.8.11"

实现用户注册和登录功能,application/executor/user_operator.rs:

use std::error::Error;
use std::sync::Arc;

use rand::{thread_rng, Rng};
use sha1::{Digest, Sha1};

use crate::application::dto;
use crate::domain::{gateway, model};

const SALT_LEN: usize = 4;
const ERR_EMPTY_EMAIL: &str = "empty email";
const ERR_EMPTY_PASSWORD: &str = "empty password";

pub struct UserOperator {
    user_manager: Arc<dyn gateway::UserManager>,
}

impl UserOperator {
    pub fn new(u: Arc<dyn gateway::UserManager>) -> Self {
        UserOperator { user_manager: u }
    }

    pub fn create_user(&self, uc: &dto::UserCredential) -> Result<dto::User, Box<dyn Error>> {
        if uc.email.is_empty() {
            return Err(ERR_EMPTY_EMAIL.into());
        }
        if uc.password.is_empty() {
            return Err(ERR_EMPTY_PASSWORD.into());
        }
        let salt = random_string(SALT_LEN);
        let user = model::User {
            id: 0,
            email: uc.email.clone(),
            password: sha1_hash(&(uc.password.clone() + &salt)),
            salt,
            is_admin: false,
            created_at: chrono::Utc::now()
                .format("%Y-%m-%d %H:%M:%S%.3f")
                .to_string(),
            updated_at: chrono::Utc::now()
                .format("%Y-%m-%d %H:%M:%S%.3f")
                .to_string(),
        };
        let uid = self.user_manager.create_user(&user)?;
        Ok(dto::User {
            id: uid,
            email: uc.email.clone(),
        })
    }

    pub fn sign_in(&self, email: &str, password: &str) -> Result<dto::User, Box<dyn Error>> {
        if email.is_empty() {
            return Err(ERR_EMPTY_EMAIL.into());
        }
        if password.is_empty() {
            return Err(ERR_EMPTY_PASSWORD.into());
        }
        let user = self.user_manager.get_user_by_email(email)?;
        if let Some(u) = user {
            let password_hash = sha1_hash(&(password.to_string() + &u.salt));
            if u.password != password_hash {
                return Err("wrong password".into());
            }
            Ok(dto::User {
                id: u.id,
                email: u.email,
            })
        } else {
            Err("user does not exist".into())
        }
    }
}

fn random_string(length: usize) -> String {
    let charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
    let mut rng = thread_rng();
    (0..length)
        .map(|_| rng.gen::<usize>() % charset.len())
        .map(|idx| charset.chars().nth(idx).unwrap())
        .collect()
}

fn sha1_hash(input: &str) -> String {
    let mut h = Sha1::new();
    h.update(input);
    let hash_bytes = h.finalize();
    hex::encode(hash_bytes)
}

函数 random_stringsha1_hash 是用于 salt 生成和密码哈希的辅助函数。

为什么需要盐(salt)?

没有salt,攻击者可以为常见密码预先计算哈希值并将其存储在表中(称为彩虹表)。如果数据库遭到入侵,攻击者可以将哈希密码与预先计算的哈希值进行比较,从而快速识别密码。为每个密码添加 salt 确保即使两个用户使用相同的密码,由于唯一的 salt,它们的哈希密码也会不同。

即使用户选择了弱密码,例如常见的词典单词,salt 也确保了生成的哈希值是唯一的。没有 salt,相同密码的所有实例都会哈希到相同的值,使其易受攻击。

添加 DTO,application/dto/user.rs:

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct UserCredential {
    pub email: String,
    pub password: String,
}

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct User {
    pub id: u32,
    pub email: String,
}

DTO 表示数据传输对象(Data Transfer Object)。

在应用程序中,数据的内部表示通常可能与你想通过 API 公开展示的表示方式不同。数据传输对象(DTO)允许你将内部数据结构转换为更适合 API 消费者的格式。这有助于将内部表示与外部表示解耦,从而实现更好的灵活性和抽象化。

引入依赖,application/wire_helper.rs:

@@ -31,6 +31,10 @@ impl WireHelper {
         Arc::clone(&self.sql_persistence) as Arc<dyn gateway::BookManager>
     }
 
+    pub fn user_manager(&self) -> Arc<dyn gateway::UserManager> {
+        Arc::clone(&self.sql_persistence) as Arc<dyn gateway::UserManager>
+    }
+
     pub fn review_manager(&self) -> Arc<dyn gateway::ReviewManager> {
         Arc::clone(&self.no_sql_persistence) as Arc<dyn gateway::ReviewManager>
     }

最后,添加路由,adapter/router.rs:

@@ -10,6 +10,7 @@ use crate::domain::model;
 pub struct RestHandler {
     book_operator: executor::BookOperator,
     review_operator: executor::ReviewOperator,
+    user_operator: executor::UserOperator,
 }
 
 #[derive(serde::Serialize)]
@@ -219,6 +220,38 @@ pub fn delete_review(
     }
 }
 
+#[post("/users", format = "json", data = "<uc>")]
+pub fn user_sign_up(
+    rest_handler: &rocket::State<RestHandler>,
+    uc: Json<dto::UserCredential>,
+) -> Result<Json<dto::User>, status::Custom<Json<ErrorResponse>>> {
+    match rest_handler.user_operator.create_user(&uc.into_inner()) {
+        Ok(u) => Ok(Json(u)),
+        Err(err) => Err(status::Custom(
+            Status::InternalServerError,
+            Json(ErrorResponse {
+                error: err.to_string(),
+            }),
+        )),
+    }
+}
+
+#[post("/users/sign-in", format = "json", data = "<uc>")]
+pub fn user_sign_in(
+    rest_handler: &rocket::State<RestHandler>,
+    uc: Json<dto::UserCredential>,
+) -> Result<Json<dto::User>, status::Custom<Json<ErrorResponse>>> {
+    match rest_handler.user_operator.sign_in(&uc.email, &uc.password) {
+        Ok(u) => Ok(Json(u)),
+        Err(err) => Err(status::Custom(
+            Status::InternalServerError,
+            Json(ErrorResponse {
+                error: err.to_string(),
+            }),
+        )),
+    }
+}
+
 pub fn make_router(wire_helper: &application::WireHelper) -> RestHandler {
     RestHandler {
         book_operator: executor::BookOperator::new(
@@ -226,5 +259,6 @@ pub fn make_router(wire_helper: &application::WireHelper) -> RestHandler {
             wire_helper.cache_helper(),
         ),
         review_operator: executor::ReviewOperator::new(wire_helper.review_manager()),
+        user_operator: executor::UserOperator::new(wire_helper.user_manager()),
     }
 }

使用 curl 测试

用户注册:

curl -X POST -H "Content-Type: application/json" -d '{"email": "test-rust@example.com", "password": "test-pass"}' http://localhost:8000/users

结果:

{"id":11,"email":"test-rust@example.com"}

如果你检查数据库中的记录,你将看到类似下面内容:

+----+-----------------------+------------------------------------------+------+----------+-------------------------+-------------------------+
| id | email                 | password                                 | salt | is_admin | created_at              | updated_at              |
+----+-----------------------+------------------------------------------+------+----------+-------------------------+-------------------------+
| 11 | test-rust@example.com | bb426d1c455aa993de2f2b47b815bbc299d768bc | 8aUa |        0 | 2024-03-20 04:40:17.060 | 2024-03-20 04:40:17.060 |
+----+-----------------------+------------------------------------------+------+----------+-------------------------+-------------------------+

用户成功登录:

curl -X POST -H "Content-Type: application/json" -d '{"email": "test-user@example.com", "password": "test-pass"}' http://localhost:8000/users/sign-in

结果:

{"id":2,"email":"test-user@example.com"}

用户使用错误密码登录:

curl -X POST -H "Content-Type: application/json" -d '{"email": "test-user@example.com", "password": "wrong-pass"}' http://localhost:8000/users/sign-in

结果:

{"error":"wrong password"}

用户使用不存在的邮箱登录:

curl -X POST -H "Content-Type: application/json" -d '{"email": "non-existent@example.com", "password": "test-pass"}' http://localhost:8000/users/sign-in 

结果:

{"error":"user does not exist"}

注意
为成功和错误响应使用统一的 json 格式是一种最佳实践。
比如:

{"code":0, "message":"ok", "data": {"id":2,"email":"test-user@example.com"}}
{"code":1001, "message":"wrong password", "data": null}

使用 JWT(JSON Web Token)

JWT(JSON Web Token)是一种紧凑、URL 安全的用于表示传输双方之间声明 claims 的方式。它经常用于 Web 应用程序和 API 中的身份验证和授权。

安装 JWT 依赖

cargo add jsonwebtoken

Cargo.toml 中变化:

@@ -8,6 +8,7 @@ edition = "2021"
 [dependencies]
 chrono = { version = "0.4.35", features = ["serde"] }
 hex = "0.4.3"
+jsonwebtoken = "9.2.0"
 lazy_static = "1.4.0"
 mongodb = { version = "2.8.2", default-features = false, features = ["sync"] }
 mysql = "24.0.0"

更新代码

添加 UserPermission 枚举,domain/model/user.go:

@@ -1,3 +1,12 @@
+// UserPermission represents different levels of user permissions.
+#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]
+pub enum UserPermission {
+    PermNone,
+    PermUser,
+    PermAuthor,
+    PermAdmin,
+}
+
 #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
 pub struct User {
     pub id: u32,

声明它的业务能力,domain/gateway/user_manager.rs:

@@ -6,3 +6,18 @@ pub trait UserManager: Send + Sync {
     fn create_user(&self, u: &model::User) -> Result<u32, Box<dyn Error>>;
     fn get_user_by_email(&self, email: &str) -> Result<Option<model::User>, Box<dyn Error>>;
 }
+
+pub trait PermissionManager: Send + Sync {
+    fn generate_token(
+        &self,
+        user_id: u32,
+        email: &str,
+        perm: model::UserPermission,
+    ) -> Result<String, Box<dyn Error>>;
+
+    fn has_permission(
+        &self,
+        token: &str,
+        perm: model::UserPermission,
+    ) -> Result<bool, Box<dyn Error>>;
+}

实现 2 个方法,infrastructure/token/jwt.rs:

use std::error::Error;
use std::time::{Duration, SystemTime};

use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation};
use serde::{Deserialize, Serialize};

use crate::domain::gateway::PermissionManager;
use crate::domain::model;

// Keeper manages user tokens.
pub struct Keeper {
    secret_key: String,
    expire_hours: u64,
}

// UserClaims includes user info.
#[derive(Debug, Serialize, Deserialize)]
pub struct UserClaims {
    user_id: u32,
    user_name: String,
    permission: model::UserPermission,
    exp: usize, // Expiry time in seconds since epoch
}

impl Keeper {
    // NewTokenKeeper constructs a new JWT token keeper
    pub fn new(secret_key: String, expire_in_hours: u32) -> Self {
        Keeper {
            secret_key: secret_key,
            expire_hours: expire_in_hours as u64,
        }
    }

    // extract_token extracts the token from the signed string.
    fn extract_token(&self, token_result: &str) -> Result<UserClaims, Box<dyn Error>> {
        let token_data = decode::<UserClaims>(
            token_result,
            &DecodingKey::from_secret(self.secret_key.as_ref()),
            &Validation::default(),
        )?;
        Ok(token_data.claims)
    }
}

impl PermissionManager for Keeper {
    // generate_token generates a new JWT token.
    fn generate_token(
        &self,
        user_id: u32,
        email: &str,
        perm: model::UserPermission,
    ) -> Result<String, Box<dyn Error>> {
        let exp = SystemTime::now()
            .checked_add(Duration::from_secs(self.expire_hours * 3600))
            .ok_or("Overflow when adding expire time")?;
        let exp = exp.duration_since(SystemTime::UNIX_EPOCH)?.as_secs() as usize;

        let claims = UserClaims {
            user_id,
            user_name: email.to_owned(),
            permission: perm,
            exp,
        };

        let header = Header::default();
        let token = encode(
            &header,
            &claims,
            &EncodingKey::from_secret(self.secret_key.as_ref()),
        )?;
        Ok(token)
    }

    // has_permission checks if user has the given permission.
    fn has_permission(
        &self,
        token_result: &str,
        perm: model::UserPermission,
    ) -> Result<bool, Box<dyn Error>> {
        let claims = self.extract_token(token_result)?;
        Ok(claims.permission >= perm)
    }
}

添加 Tokens 相关的配置项,infrastructure/config/mod.rs:

@@ -26,6 +26,8 @@ pub struct CacheConfig {
 pub struct ApplicationConfig {
     pub port: i32,
     pub page_size: u32,
+    pub token_secret: String,
+    pub token_hours: u32,
 }
 
 pub fn parse_config(file_name: &str) -> Config {

置入其值,config.toml:

@@ -1,6 +1,8 @@
 [app]
 port = 8080
 page_size = 5
+token_secret = "I_Love_LiteRank"
+token_hours = 48
 
 [db]
 file_name = "test.db"

生成并返回 tokens,application/executor/user_operator.rs:

@@ -13,11 +13,15 @@ const ERR_EMPTY_PASSWORD: &str = "empty password";
 
 pub struct UserOperator {
     user_manager: Arc<dyn gateway::UserManager>,
+    perm_manager: Arc<dyn gateway::PermissionManager>,
 }
 
 impl UserOperator {
-    pub fn new(u: Arc<dyn gateway::UserManager>) -> Self {
-        UserOperator { user_manager: u }
+    pub fn new(u: Arc<dyn gateway::UserManager>, p: Arc<dyn gateway::PermissionManager>) -> Self {
+        UserOperator {
+            user_manager: u,
+            perm_manager: p,
+        }
     }
 
     pub fn create_user(&self, uc: &dto::UserCredential) -> Result<dto::User, Box<dyn Error>> {
@@ -48,7 +52,7 @@ impl UserOperator {
         })
     }
 
-    pub fn sign_in(&self, email: &str, password: &str) -> Result<dto::User, Box<dyn Error>> {
+    pub fn sign_in(&self, email: &str, password: &str) -> Result<dto::UserToken, Box<dyn Error>> {
         if email.is_empty() {
             return Err(ERR_EMPTY_EMAIL.into());
         }
@@ -61,14 +65,31 @@ impl UserOperator {
             if u.password != password_hash {
                 return Err("wrong password".into());
             }
-            Ok(dto::User {
-                id: u.id,
-                email: u.email,
+            let perm = if u.is_admin {
+                model::UserPermission::PermAdmin
+            } else {
+                model::UserPermission::PermUser
+            };
+            let token = self.perm_manager.generate_token(u.id, &u.email, perm)?;
+            Ok(dto::UserToken {
+                user: dto::User {
+                    id: u.id,
+                    email: u.email,
+                },
+                token,
             })
         } else {
             Err("user does not exist".into())
         }
     }
+
+    pub fn has_permission(
+        &self,
+        token: &str,
+        perm: model::UserPermission,
+    ) -> Result<bool, Box<dyn Error>> {
+        self.perm_manager.has_permission(token, perm)
+    }
 }
 
 fn random_string(length: usize) -> String {

添加 DTO,application/dto/user.rs:

@@ -9,3 +9,9 @@ pub struct User {
     pub id: u32,
     pub email: String,
 }
+
+#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
+pub struct UserToken {
+    pub user: User,
+    pub token: String,
+}

微调 application/wire_helper.rs:

@@ -3,12 +3,14 @@ use std::sync::Arc;
 use crate::domain::gateway;
 use crate::infrastructure::cache;
 use crate::infrastructure::database;
+use crate::infrastructure::token;
 use crate::infrastructure::Config;
 
 pub struct WireHelper {
     sql_persistence: Arc<database::MySQLPersistence>,
     no_sql_persistence: Arc<database::MongoPersistence>,
     kv_store: Arc<cache::RedisCache>,
+    token_keeper: Arc<token::Keeper>,
 }
 
 impl WireHelper {
@@ -20,10 +22,15 @@ impl WireHelper {
             &c.db.mongo_db_name,
         )?);
         let kv_store = Arc::new(cache::RedisCache::new(&c.cache.redis_uri)?);
+        let token_keeper = Arc::new(token::Keeper::new(
+            c.app.token_secret.clone(),
+            c.app.token_hours,
+        ));
         Ok(WireHelper {
             sql_persistence,
             no_sql_persistence,
             kv_store,
+            token_keeper,
         })
     }
 
@@ -35,6 +42,10 @@ impl WireHelper {
         Arc::clone(&self.sql_persistence) as Arc<dyn gateway::UserManager>
     }
 
+    pub fn perm_manager(&self) -> Arc<dyn gateway::PermissionManager> {
+        Arc::clone(&self.token_keeper) as Arc<dyn gateway::PermissionManager>
+    }
+
     pub fn review_manager(&self) -> Arc<dyn gateway::ReviewManager> {
         Arc::clone(&self.no_sql_persistence) as Arc<dyn gateway::ReviewManager>
     }

添加 PermCheck 中间件到创建、更新、删除路由中,adapter/router.rs:

@@ -2,6 +2,7 @@ use rocket::http::Status;
 use rocket::response::{content, status};
 use rocket::serde::json::Json;
 
+use crate::adapter::middleware::PermCheck;
 use crate::application;
 use crate::application::dto;
 use crate::application::executor;
@@ -10,7 +11,7 @@ use crate::domain::model;
 pub struct RestHandler {
     book_operator: executor::BookOperator,
     review_operator: executor::ReviewOperator,
-    user_operator: executor::UserOperator,
+    pub user_operator: executor::UserOperator,
 }
 
 #[derive(serde::Serialize)]
@@ -73,6 +74,7 @@ pub fn get_book(
 pub fn create_book(
     rest_handler: &rocket::State<RestHandler>,
     book: Json<model::Book>,
+    _perm_check: PermCheck,
 ) -> Result<Json<model::Book>, status::Custom<Json<ErrorResponse>>> {
     match rest_handler.book_operator.create_book(book.into_inner()) {
         Ok(b) => Ok(Json(b)),
@@ -90,6 +92,7 @@ pub fn update_book(
     rest_handler: &rocket::State<RestHandler>,
     id: u32,
     book: Json<model::Book>,
+    _perm_check: PermCheck,
 ) -> Result<Json<model::Book>, status::Custom<Json<ErrorResponse>>> {
     match rest_handler
         .book_operator
@@ -109,6 +112,7 @@ pub fn update_book(
 pub fn delete_book(
     rest_handler: &rocket::State<RestHandler>,
     id: u32,
+    _perm_check: PermCheck,
 ) -> Result<status::NoContent, status::Custom<Json<ErrorResponse>>> {
     match rest_handler.book_operator.delete_book(id) {
         Ok(_) => Ok(status::NoContent),
@@ -240,7 +244,7 @@ pub fn user_sign_up(
 pub fn user_sign_in(
     rest_handler: &rocket::State<RestHandler>,
     uc: Json<dto::UserCredential>,
-) -> Result<Json<dto::User>, status::Custom<Json<ErrorResponse>>> {
+) -> Result<Json<dto::UserToken>, status::Custom<Json<ErrorResponse>>> {
     match rest_handler.user_operator.sign_in(&uc.email, &uc.password) {
         Ok(u) => Ok(Json(u)),
         Err(err) => Err(status::Custom(
@@ -259,6 +263,9 @@ pub fn make_router(wire_helper: &application::WireHelper) -> RestHandler {
             wire_helper.cache_helper(),
         ),
         review_operator: executor::ReviewOperator::new(wire_helper.review_manager()),
-        user_operator: executor::UserOperator::new(wire_helper.user_manager()),
+        user_operator: executor::UserOperator::new(
+            wire_helper.user_manager(),
+            wire_helper.perm_manager(),
+        ),
     }
 }

请求守卫 Request guards是 Rocket 中最强大的工具之一。正如其名称所暗示,请求守卫根据传入请求中包含的信息来保护 handler 函数免受错误调用的影响。可以说,请求守卫是一种表示任意验证策略的类型。验证策略通过 FromRequest 特性实现。每个实现了 FromRequest 特性的类型都是一个请求守卫。

PermCheck 的实现,adapter/middleware.rs:

use rocket::http::Status;
use rocket::request::{self, FromRequest, Request};

use crate::domain::model::UserPermission;
use crate::RestHandler;

// Define a struct to hold the permission level required for the route
pub struct PermCheck;

// Implement FromRequest trait to perform permission check
#[rocket::async_trait]
impl<'r> FromRequest<'r> for PermCheck {
    type Error = &'static str;

    async fn from_request(request: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
        let auth_header = match request.headers().get_one("Authorization") {
            Some(header) => header,
            None => return request::Outcome::Error((Status::Unauthorized, "Token is required")),
        };
        let token = auth_header.trim_start_matches("Bearer ");
        let rest_handler = request.rocket().state::<RestHandler>().unwrap();
        // Check user permission against required permission
        match rest_handler
            .user_operator
            .has_permission(token, UserPermission::PermAuthor)
        {
            Ok(b) => {
                if b {
                    request::Outcome::Success(PermCheck {})
                } else {
                    request::Outcome::Error((Status::Unauthorized, "Unauthorized"))
                }
            }
            Err(_) => request::Outcome::Error((Status::BadRequest, "Invalid token")),
        }
    }
}

以上就是 API 中使用 JWT 的所需更改。让我们再次测试一下服务器功能!

使用 curl 测试

用户登录获得 Token

curl -X POST -H "Content-Type: application/json" -d '{"email": "test-user@example.com", "password": "test-pass"}' http://localhost:8000/users/sign-in

结果:

{
  "user": { "id": 2, "email": "test-user@example.com" },
  "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoyLCJ1c2VyX25hbWUiOiJ0ZXN0LXVzZXJAZXhhbXBsZS5jb20iLCJwZXJtaXNzaW9uIjoiUGVybUFkbWluIiwiZXhwIjoxNzExMDk4NjU5fQ.cHaxxc44Sd7sFWe6vq1NTgFad3HUC7ghX8TJK96_EiU"
}

将 token 放到 https://jwt.io/ 调试器中,可查看 token 的负载和签名是否正常。

JWT Debugger

创建新的图书,携带合法正常的 Token

curl -X POST \
  http://localhost:8000/books \
  -H 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoyLCJ1c2VyX25hbWUiOiJ0ZXN0LXVzZXJAZXhhbXBsZS5jb20iLCJwZXJtaXNzaW9uIjoiUGVybUFkbWluIiwiZXhwIjoxNzExMDk4NjU5fQ.cHaxxc44Sd7sFWe6vq1NTgFad3HUC7ghX8TJK96_EiU' \
  -H 'Content-Type: application/json' \
  -d '{
    "id": 0,
    "title": "Test Book",
    "author": "John Doe",
    "published_at": "2003-01-01",
    "description": "A sample book description",
    "isbn": "1234567890",
    "total_pages": 100,
    "created_at": "",
    "updated_at": ""
}'

提示:你可以添加新的 DTO 类型来使此处 json 包体更“干净”。

成功结果:

{
  "id": 21,
  "title": "Test Book",
  "author": "John Doe",
  "published_at": "2003-01-01",
  "description": "A sample book description",
  "isbn": "1234567890",
  "total_pages": 100,
  "created_at": "",
  "updated_at": ""
}

创建新的图书,携带合法 Token,但是权限不够

curl -X POST \
  http://localhost:8000/books \
  -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoyLCJ1c2VyX25hbWUiOiJ0ZXN0LXVzZXJAZXhhbXBsZS5jb20iLCJwZXJtaXNzaW9uIjoxLCJleHAiOjE3MDkyNzA1MzV9.eJp5Yr9YZMxHFs5eId78bEJ6draO178jysquZ2VV9v8' \
  -H 'Content-Type: application/json' \
  -d '{
    "title": "Test Book",
    "author": "John Doe",
    "published_at": "2003-01-01",
    "description": "A sample book description",
    "isbn": "1234567890",
    "total_pages": 100
}'

Rocket 服务端日志:

POST /books application/json:
   >> Matched: (create_book) POST /books application/json
   >> Request guard `PermCheck` failed: "Invalid token".
   >> Outcome: Error(400 Bad Request)
   >> No 400 catcher registered. Using Rocket default.
   >> Response succeeded.

创建新的图书,不带 Token

curl -X POST \
  http://localhost:8000/books \
  -H 'Content-Type: application/json' \
  -d '{
    "title": "Test Book",
    "author": "John Doe",
    "published_at": "2003-01-01",
    "description": "A sample book description",
    "isbn": "1234567890",
    "total_pages": 100
}'

Rocket 服务端日志:

POST /books application/json:
   >> Matched: (create_book) POST /books application/json
   >> Request guard `PermCheck` failed: "Token is required".
   >> Outcome: Error(401 Unauthorized)
   >> No 401 catcher registered. Using Rocket default.
   >> Response succeeded.

创建新的图书,带一个假 Token

curl -X POST \
  http://localhost:8000/books \
  -H 'Authorization: Bearer FAKE_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{
    "title": "Test Book",
    "author": "John Doe",
    "published_at": "2003-01-01",
    "description": "A sample book description",
    "isbn": "1234567890",
    "total_pages": 100
}'

Rocket 服务端日志:

POST /books application/json:
   >> Matched: (create_book) POST /books application/json
   >> Request guard `PermCheck` failed: "Invalid token".
   >> Outcome: Error(400 Bad Request)
   >> No 400 catcher registered. Using Rocket default.
   >> Response succeeded.

完美! 🎉

现在,你的部分端点已经被权限认证机制保护起来啦。