コードリポジトリ

gitee

プロジェクトの作成

1
cargo new rust-web-starter

ディレクトリ構造

1
2
3
4
5
6
7
8
9
10
11
/src
/handlers
- mod.rs
- posts.rs
- user.rs
/utils
- mod.rs
- jwt.rs
main.rs
.env
dev.db

データベース

sqlxを使用してデータベース操作を行い、テストの便宜のためSQLiteデータベースを使用します。

データベーステーブルの作成

コード内で実装します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#[tokio::main]
async fn main() {
// 環境変数の読み込み
let _ = dotenvy::dotenv();
// ログトレーサー
tracing_subscriber::fmt::init();
info!("サーバー起動");
// データベース接続
let database_url = std::env::var("SQLITE_DB_URL").expect("SQLITE_DB_URLが設定されていません");
let pool = sqlx::SqlitePool::connect(&database_url)
.await
.expect("プール接続エラー");
// テーブルの作成
sqlx::query(
r#"create table users
(
id integer
primary key autoincrement,
username text,
password text,
email text
);
"#,
)
.execute(&pool)
.await;
sqlx::query(
r#"create table posts
(
id integer
primary key autoincrement,
created_at datetime,
updated_at datetime,
deleted_at datetime,
title text,
body text
);
create index idx_posts_deleted_at
on posts (deleted_at);
"#,
)
.execute(&pool)
.await;

}

パスワードの暗号化

適切なものが見つからなかったため、平文を使用します。

JWTトークンの作成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
use chrono::{DateTime, Duration, Utc};
use jsonwebtoken::{DecodingKey, EncodingKey, Header, Validation, decode, encode, errors::Error};
use serde::{Deserialize, Serialize};
use tracing::{debug, error};
#[derive(Debug, Serialize, Deserialize)]
pub struct Claims {
pub sub: String,
pub exp: i64,
pub iat: i64,
}
impl Claims {
pub fn new(sub: String, exp: DateTime<Utc>) -> Self {
Self {
sub,
exp: exp.timestamp(),
iat: Utc::now().timestamp(),
}
}
}
// 32バイトの安全な秘密鍵
pub const SECRET_KEY: &[u8] = b"030c8d02eea6e5e5219096bd076c41e58e955632d59beb7d44fa18e3fbccb0bd12345678901234";
// JWTの生成
pub fn generate_token(user_id: &str) -> Result<String, Error> {
let claims = Claims::new(user_id.to_string(), Utc::now() + Duration::hours(1));
let token = encode(
&Header::default(),
&claims,
&EncodingKey::from_secret(SECRET_KEY),
)?;
debug!("生成されたトークン: {}", token);
Ok(token)
}
// JWTの検証
pub fn validate_token(token: &str) -> Result<Claims, Error> {
debug!("受信したトークン: {}", token);
let mut validation = Validation::new(jsonwebtoken::Algorithm::HS256);
validation.validate_exp = true;
match decode::<Claims>(token, &DecodingKey::from_secret(SECRET_KEY), &validation) {
Ok(data) => {
debug!("デコードされたクレーム: {:?}", data.claims);
Ok(data.claims)
}
Err(e) => {
error!("トークンエラー: {:?}", e);
Err(e)
}
}
}

テストの実行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/// JWTメソッドのコード
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_generate_and_validate_token() {
// トークンの生成
let user_id = "test_user";
let token_result = generate_token(user_id);

assert!(token_result.is_ok(), "トークンの生成に失敗しました");
let token = token_result.unwrap();
// トークンの検証
let claims_result = validate_token(&token);
assert!(claims_result.is_ok(), "トークンの検証に失敗しました");

let claims = claims_result.unwrap();

// クレーム内の情報が正しいか確認
assert_eq!(claims.sub, user_id.to_string());
}
#[test]
fn test_invalid_token() {
// 無効なトークンを提供
let invalid_token = "invalid.token.here";
let claims_result = validate_token(invalid_token);
assert!(claims_result.is_err(), "無効なトークンに対してエラーが期待されます");
}
}

投稿情報テーブル

CRUD操作

posts.rs
投稿情報の取得、投稿、更新、削除を行うAPIインターフェースを提供します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
use axum::{
extract::{Path, State},
http::StatusCode,
Json,
};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use sqlx_core::sqlite::SqlitePool;
#[derive(Serialize, Deserialize)]
pub struct NewPost {
title: String,
body: String,
}
#[derive(Serialize, Deserialize, sqlx::FromRow)]
pub struct Post {
id: i32,
title: String,
body: String,
}
pub async fn create_post(
// グローバルルート状態からデータベース接続プールを取得
State(pool): State<SqlitePool>,
Json(product): Json<NewPost>,
) -> Result<Json<Value>, (StatusCode, String)> {
let resp = sqlx::query("INSERT INTO posts (title, body) values ($1, $2)")
// プレースホルダーを埋める
.bind(&product.title)
.bind(&product.body)
.execute(&pool)
.await
.map_err(|err| {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("エラー: {}", err),
)
})?;
Ok(Json(json!(product)))
}
pub async fn get_posts(
State(pool): State<SqlitePool>,
) -> Result<Json<Vec<Post>>, (StatusCode, String)> {
let result = sqlx::query_as("SELECT * from posts")
// データを構造体に戻す
.fetch_all(&pool)
.await
.map_err(|err| {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("エラー: {}", err),
)
})?;
Ok(Json(result))
}
pub async fn get_one_post(
State(pool): State<SqlitePool>,
Path(id): Path<i32>,
) -> Result<Json<Post>, (StatusCode, String)> {
let result = sqlx::query_as("SELECT * FROM posts WHERE id = $1")
.bind(id)
.fetch_one(&pool)
.await
.map_err(|err| match err {
sqlx::Error::RowNotFound => (StatusCode::NOT_FOUND, format!("エラー: {}", err)),
_ => (
StatusCode::INTERNAL_SERVER_ERROR,
format!("エラー: {}", err),
),
})?;
Ok(Json(result))
}
pub async fn delete_post(
State(pool): State<SqlitePool>,
Path(id): Path<i32>,
) -> Result<Json<Value>, (StatusCode, String)> {
let result = sqlx::query("DELETE FROM posts WHERE id = $1")
.bind(id)
.execute(&pool)
.await
.map_err(|err| match err {
sqlx::Error::RowNotFound => (StatusCode::NOT_FOUND, format!("エラー: {}", err)),
_ => (
StatusCode::INTERNAL_SERVER_ERROR,
format!("エラー: {}", err),
),
})?;
Ok(Json(json!({"msg": "投稿が正常に削除されました"})))
}
pub async fn update_post(
State(pool): State<SqlitePool>,
Path(id): Path<i32>,
Json(product): Json<Post>,
) -> Result<Json<Value>, (StatusCode, String)> {
let result = sqlx::query("UPDATE posts SET title=$1, body=$2 WHERE id=$3")
.bind(&product.title)
.bind(&product.body)
.bind(id)
.execute(&pool)
.await
.map_err(|err| match err {
sqlx::Error::RowNotFound => (StatusCode::NOT_FOUND, format!("エラー: {}", err)),
_ => (
StatusCode::INTERNAL_SERVER_ERROR,
format!("エラー: {}", err),
),
})?;
Ok(Json(json!({"msg": "投稿が正常に更新されました"})))
}

ネットワークインターフェース

ルートの定義

RESTfulスタイルAPI:
HTTPメソッド|操作タイプ|例
GET|リソースの取得|ユーザーリストの取得 /api/users
POST|リソースの作成|新しいユーザーの作成 /api/users
PUT|リソースの更新|指定ユーザーの更新 /api/users/{id}
DELETE|リソースの削除|指定ユーザーの削除 /api/users/{id}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
#[tokio::main]
async fn main() {
/// 前述のデータベース生成コード
// ネットワークインターフェースコード
let postRouter = Router::new()
.route("/posts", get(handlers::posts::get_posts))
.route("/posts/:id", get(handlers::posts::get_one_post))
.route("/posts", post(handlers::posts::create_post))
.route("/posts/:id", patch(handlers::posts::update_post))
.route("/posts/:id", delete(handlers::posts::delete_post))
.route_layer(middleware::from_fn(auth));
let userRouter = Router::new()
.route("/users", post(handlers::uesr::register))
.route("/auth/login", post(handlers::uesr::login));
let userProfileRouter = Router::new()
.route("/auth/profile", get(handlers::uesr::validateUser))
.route_layer(middleware::from_fn(auth));
// CORSミドルウェア
let cors = CorsLayer::new().allow_origin(Any);
let app = Router::new()
// ルーターをマージ
.merge(userRouter)
.merge(postRouter)
.merge(userProfileRouter)
// 状態、全ルートで使用可能なデータ、ここではデータ接続プール
.with_state(pool)
// CORS
.layer(cors)
// コード圧縮レイヤー
.layer(CompressionLayer::new())
// HTTPトレーサー
.layer(TraceLayer::new_for_http());
let listener = tokio::net::TcpListener::bind("127.0.0.1:4000")
.await
.unwrap();
println!("リスニング: {}", listener.local_addr().unwrap());
axum::serve(listener, app).await.unwrap();
}
```

## 認証ミドルウェア

```rust
/// main関数
#[derive(Clone)]
struct AuthHeader {
id: String,
}
async fn auth(
headers: HeaderMap,
mut req: Request,
next: Next,
) -> Result<impl IntoResponse, (StatusCode, String)> {
// Authorizationヘッダーを抽出
let header = headers.get("Authorization").ok_or((
StatusCode::UNAUTHORIZED,
"Authorizationヘッダーがありません".to_string(),
))?;
let header_str = header
.to_str()
.map_err(|_| (StatusCode::BAD_REQUEST, "無効なAuthorizationヘッダー"))
.unwrap();
let token = header_str.replace("Bearer ", "").trim().to_string();
// トークンの検証
let claims = validate_token(token.as_str()).map_err(|e| {
tracing::warn!("トークンの検証に失敗しました: {:?}", e);
(
StatusCode::UNAUTHORIZED,
"無効または期限切れのトークン".to_string(),
)
})?;
// ユーザー情報をリクエストコンテキストに注入
req.extensions_mut().insert(AuthHeader { id: claims.sub });
Ok(next.run(req).await)
}

Apifoxを使用したテスト

Apifoxはインターフェーステストツールです
apifox
このケースのインターフェースは既に共有されています:
インターフェースドキュメント

現在成熟している二度開発フレームワーク

不明

コミュニティ

以下のプラットフォームで連絡できます:


本站总访问量