构建一个rust生产应用读书笔记6-拒绝无效订阅者01
zhezhongyun 2025-01-11 18:45 56 浏览
为了增强您的POST /subscriptions端点的安全性和可靠性,确保输入数据的质量和有效性是非常重要的。当前的实现似乎只做了最基础的验证——即检查name和email字段是否存在。这样的做法可能会让系统暴露于各种潜在的问题之下,例如恶意用户提交无效或格式不正确的数据,或者导致数据库中存储了低质量的数据。
改进输入验证
- 验证数据类型:确保name和email字段是字符串类型。
- 验证长度:为name和email字段设定合理的最小和最大长度限制。
- 格式验证:特别是对于email字段,使用正则表达式或其他方法来验证电子邮件地址的格式是否正确。
- 字符集验证:根据需要限制允许使用的字符,防止特殊字符导致问题。
- 唯一性验证:如果需要,可以检查email是否已经存在于系统中,以避免重复订阅。
- 空格处理:移除前导和尾随空格,防止用户意外地在输入前后加入不必要的空格。
Cargo.toml
[dependencies]
//[..]
unicode-segmentation = "1.7.1"
首先做一个集成测试
//!tests/health_check.rs
use secrecy::Secret;
use sqlx::{Connection, Executor, PgConnection, PgPool};
use std::net::TcpListener;
use std::sync::LazyLock;
use uuid::Uuid;
use zero2prod::configuration::{get_configuration, DatabaseSettings};
use zero2prod::startup::run;
use zero2prod::telemetry::{get_subscriber, init_subscriber};
static TRACKING: LazyLock<()> = LazyLock::new(|| {
let default_filter_level = "info".to_string();
let subscriber_name = "test".to_string();
if std::env::var("TEST_LOG").is_ok() {
let subscriber = get_subscriber(subscriber_name, default_filter_level, std::io::stdout);
init_subscriber(subscriber);
} else {
let subscriber = get_subscriber(subscriber_name, default_filter_level, std::io::sink);
init_subscriber(subscriber);
};
});
pub struct TestApp {
pub address: String,
pub db_pool: PgPool,
}
async fn spawn_app() -> TestApp {
LazyLock::force(&TRACKING);
let listener = TcpListener::bind("127.0.0.1:0").expect("Failed to bind random port");
let port = listener.local_addr().unwrap().port();
let address = format!("http://127.0.0.1:{}", port);
let mut configuration = get_configuration().expect("Failed to read configuration.");
configuration.database.database_name = Uuid::new_v4().to_string();
let connection_pool = configure_database(&configuration.database).await;
let server = run(listener, connection_pool.clone()).expect("Failed to bind address");
let _ = tokio::spawn(server);
TestApp {
address,
db_pool: connection_pool,
}
}
pub async fn configure_database(config: &DatabaseSettings) -> PgPool {
let maintenance_settings = DatabaseSettings {
database_name: "newsletter".to_string(),
username: "postgres".to_string(),
password: Secret::new("postgres".to_string()),
..config.clone()
};
let mut connection = PgConnection::connect_with(&maintenance_settings.connect_options())
.await
.expect("Failed to connect to Postgres");
connection.execute(format!(r#"CREATE DATABASE "{}";"#, config.database_name).as_str())
.await
.expect("Failed to create database.");
let connection_pool = PgPool::connect_with(config.connect_options())
.await
.expect("Failed to connect to Postgres.");
sqlx::migrate!("./migrations").run(&connection_pool)
.await.expect("Failed to migrate the database");
connection_pool
}
#[tokio::test]
async fn subscribe_returns_a_200_when_fields_are_present_but_empty() {
let app = spawn_app().await;
let client = reqwest::Client::new();
let body = "name=zhangsan&email=zhangsan%40126.com";
//Act
let response = client.post(&format!("{}/subscriptions", &app.address))
.header("Content-Type", "application/x-www-form-urlencoded")
.body(body)
.send()
.await
.expect("Failed to execute request.");
// Assert
assert_eq!(200, response.status().as_u16());
let saved = sqlx::query!("SELECT email, name FROM subscriptions",)
.fetch_one(&app.db_pool)
.await
.expect("Failed to fetch saved subscription.");
assert_eq!(saved.email, "zhangsan@126.com");
assert_eq!(saved.name, "zhangsan");
}
执行结果
Testing started at 14:34 ...
warning: unused manifest key: bin.0.plugin
warning: unused manifest key: lib.plugin
warning: unused manifest key: test.0.plugin
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.37s
Running tests/health_check.rs (target/debug/deps/health_check-6616c6a4e3cd7fb3)
域约束
域约束(Domain Constraints)是指在软件开发和数据库设计中,对数据值或行为施加的限制条件。这些约束确保了应用程序中的数据保持有效、一致且符合业务规则。它们可以应用于不同的层面,例如数据库层、业务逻辑层或者用户界面层。以下是关于域约束的一些重要概念:
1. 数据库层的域约束
在数据库设计中,域约束通常通过定义表结构时指定字段的数据类型、长度、格式等来实现。常见的域约束包括但不限于:
- 非空约束 (NOT NULL):确保字段不能包含NULL值。
- 唯一性约束 (UNIQUE):保证字段中的每个值都是唯一的,常用于主键或需要唯一性的列。
- 检查约束 (CHECK):允许你定义一个表达式,该表达式的值必须为真,否则插入或更新操作将被拒绝。
- 外键约束 (FOREIGN KEY):用来维护两个表之间的参照完整性,确保子表中的记录对应于父表中存在的记录。
- 默认值约束 (DEFAULT):当没有提供具体值时,自动为字段设置默认值。
2. 业务逻辑层的域约束
在应用层面上,域约束可能涉及到更复杂的业务规则,而不仅仅是简单的数据验证。例如:
- 数值范围:如年龄必须介于0到120之间。
- 格式验证:如电子邮件地址必须遵循特定格式。
- 复杂依赖关系:如订单状态改变后,某些操作是否被允许。
3. 用户界面层的域约束
在UI层面,可以通过前端验证来防止无效输入直接提交给服务器。这包括但不限于:
- 即时反馈:用户输入时立即显示错误提示信息。
- 控件属性:利用HTML5内置的input类型(如email, url, number等),以及min/max/maxlength等属性来进行基本的输入限制。
- JavaScript验证:编写脚本进行更加灵活和复杂的验证逻辑。
采用名字相同策略
从本结开始,我们可以通过创建一个新的类型SubscriberEmail来定义我们的不变量(“这个字符串代表一个有效的电子邮件”)。这样做有几个好处:
- 提高代码的可读性和意图表达:通过定义一个特定类型的SubscriberEmail,我们可以更清晰地传达该值应该是一个有效的电子邮件地址。这使得代码更容易理解,因为类型本身就在说明它的用途。
- 增强编译时检查:如果使用的是静态类型语言如Java或Kotlin,自定义类型可以在编译阶段就帮助捕获错误。例如,如果尝试将非电子邮件格式的字符串赋值给SubscriberEmail类型的变量,编译器可以立即报错。
- 简化业务逻辑:在应用中传递和处理SubscriberEmail对象而不是原始字符串,可以确保所有地方都遵循同样的规则,减少了在不同位置重复实现验证逻辑的需求。
- 促进不可变性:一旦创建了SubscriberEmail对象,就可以保证它是有效且不可更改的,从而减少意外修改的风险。
- 支持领域驱动设计(DDD):这种做法符合DDD的原则,即通过引入丰富的领域模型来更好地捕捉业务规则和约束条件。
subscriptions.rs 重构
#[allow(clippy::async_yields_async)]
#[tracing::instrument(name = "Adding a new subscriber",
skip(form, pool),
fields(subscriber_email=%form.email,subscriber_name=%form.name))]
pub async fn subscribe(form: web::Form<FormData>, pool: web::Data<PgPool>) -> HttpResponse {
let name = match SubscriberName::parse(form.0.name) {
Ok(name) => name,
Err(_) => return HttpResponse::BadRequest().finish(),
};
let new_subscriber = NewSubscriber {
email: form.0.email,
name,
};
match insert_subscriber(&pool, new_subscriber).await {
Ok(_) => HttpResponse::Ok().finish(),
Err(_) => HttpResponse::InternalServerError().finish(),
}
}
#[tracing::instrument(name = "Save new subscriber detial in database",
skip(new_subscriber, pool))]
pub async fn insert_subscriber(pool: &PgPool, new_subscriber: NewSubscriber) -> Result<(), sqlx::Error> {
sqlx::query!(
r#"insert into subscriptions (id,email,name,subscribed_at) values($1,$2,$3,$4)"#,
Uuid::new_v4(),
new_subscriber.email,
new_subscriber.name.as_ref(),
Utc::now()
)
.execute(pool)
.await
.map_err(|e| {
tracing::error!("Failed to execute query :{:?}", e);
e
})?;
Ok(())
}
Type-Driven Development
Type-Driven Development (TDD) 是一种软件开发方法,但它与更常见的 Test-Driven Development(测试驱动开发)不同。更强调类型系统在开发过程中的作用。Type-Driven Development 强调利用强大的静态类型系统来指导代码的设计和编写,确保程序的正确性和可靠性。
类型驱动开发的核心理念
使用类型作为设计工具:
- 在类型驱动开发中,开发者首先考虑的是如何通过类型系统表达业务逻辑和数据结构。这意味着在编写实现之前,先定义接口、函数签名以及数据类型的形状。
编译器辅助开发:
- 强大的静态类型检查可以在编译时捕捉到许多潜在错误,如类型不匹配、空值引用等。这减少了运行时错误的发生几率,并提高了代码质量。
提高代码可读性和维护性:
- 清晰的类型信息使得代码更容易被理解和维护。其他开发者可以快速了解函数接收什么参数、返回什么结果,而无需深入阅读实现细节。
促进模块化和解耦:
- 通过精心设计的类型接口,可以使各个组件之间的依赖关系更加明确,从而降低系统的耦合度,增加灵活性。
支持重构:
- 当进行大规模重构时,静态类型系统可以帮助识别受影响的部分,确保修改不会破坏现有功能。
实践类型驱动开发的方法
- 从类型开始:在编码之前,优先考虑类型的设计。定义好所有必要的数据类型、枚举、联合类型等。
- 让编译器引导你:当遇到编译错误时,不要急于解决问题,而是思考这些错误是否反映了设计上的不足。调整类型定义以更好地匹配需求。
- 保持类型安全:尽量避免使用动态类型或任何形式的类型转换,除非绝对必要。如果必须这样做,请确保有足够的理由,并且已经充分评估了风险。
- 利用高级类型特性:现代编程语言提供了丰富的类型构造,如泛型、代数数据类型(ADTs)、模式匹配等。合理运用这些特性可以让代码更加简洁且富有表现力。
- 文档即类型:良好的类型注释本身就是一种文档形式。它不仅帮助团队成员理解代码意图,也使得未来维护变得更加容易。
新增domain模块
cargo.toml 增加配置
[dev-dependencies]
claims = "0.7"
//!src/lib.rs
pub mod domain;
domain.rs
use unicode_segmentation::UnicodeSegmentation;
pub struct NewSubscriber {
pub email: String,
pub name: SubscriberName,
}
#[derive(Debug)]
pub struct SubscriberName(String);
impl SubscriberName {
pub fn parse(s: String) -> Result<SubscriberName, String> {
let is_empty_or_whitespace = s.trim().is_empty();
let is_too_long = s.graphemes(true).count() > 256;
let forbidden_characters = ['/', '(', ')', '"', '<', '>', '\\', '{', '}'];
let contains_forbidden_characters = s.chars().any(|g| forbidden_characters.contains(&g));
if is_empty_or_whitespace || is_too_long || contains_forbidden_characters {
Err(format!("{} is not a valid subscriber name.", s))
} else {
Ok(Self(s))
}
}
}
impl AsRef<str> for SubscriberName {
fn as_ref(&self) -> &str {
&self.0
}
}
#[cfg(test)]
mod tests {
use crate::domain::SubscriberName;
use claims::{assert_err, assert_ok};
#[test]
fn a_256_grapheme_long_name_is_valid() {
let name = "a?".repeat(256);
assert_ok!(SubscriberName::parse(name));
}
#[test]
fn a_name_longer_than_256_graphemes_is_rejected() {
let name = "a".repeat(257);
assert_err!(SubscriberName::parse(name));
}
#[test]
fn whitespace_only_names_are_rejected() {
let name = " ".to_string();
assert_err!(SubscriberName::parse(name));
}
#[test]
fn empty_string_is_rejected() {
let name = "".to_string();
assert_err!(SubscriberName::parse(name));
}
#[test]
fn names_containing_an_invalid_character_are_rejected() {
for name in &['/', '(', ')', '"', '<', '>', '\\', '{', '}'] {
let name = name.to_string();
assert_err!(SubscriberName::parse(name));
}
}
#[test]
fn a_valid_name_is_parsed_successfully() {
let name = "Ursula Le Guin".to_string();
assert_ok!(SubscriberName::parse(name));
}
}
- parse 方法接收一个 String 类型的参数 s,并返回一个 Result<SubscriberName, String>,即如果成功则返回一个 SubscriberName 实例,否则返回一个错误信息字符串。
- 方法内部进行了三个主要检查:
- 非空或空白:通过 trim().is_empty() 检查字符串是否为空或仅包含空白字符。
- 长度限制:使用 graphemes(true).count() 来计算图形单元的数量(考虑组合字符),确保不超过256个图形单元。
- 禁止字符:检查字符串中是否包含了预定义的禁止字符列表中的任何一个字符。
如果任意一项检查失败,则构造一个错误消息并返回 Err;否则返回 Ok 包含一个新的 SubscriberName 实例。
此函数实现了如下:
- 编写了多个单元测试来验证 SubscriberName::parse 方法的行为:
- 测试最大允许长度(256个图形单元)的名字是否有效。
- 测试超过256个图形单元的名字是否被拒绝。
- 测试仅由空白字符组成的名字是否被拒绝。
- 测试空字符串是否被拒绝。
- 测试包含禁止字符的名字是否被拒绝。
- 测试一个有效的名字能否成功解析。
总结
rust是一门不太容易掌握的语言,在实际学习过程中遇到了很多问题,很不可思议。不过大多数情况下都可以在网上找到答案,希望正在学习的朋友们不要放弃
相关推荐
- 用豆包生成的BMI计算器(豆包的热量是多少?)
-
<!DOCTYPEhtml><htmllang="en"><head><metacharset="UTF-8...
- Android 开发中文引导-应用小部件
-
应用小部件是可以嵌入其它应用(例如主屏幕)并收到定期更新的微型应用视图。这些视图在用户界面中被叫做小部件,并可以用应用小部件提供者发布。可以容纳其他应用部件的应用组件叫做应用部件的宿主(1)。下面的截...
- Qt推流(视频文件/视频流/摄像头/桌面转流媒体rtmp+hls+webrtc)
-
一、前言说明推流直播就是把采集阶段封包好的内容传输到服务器的过程。其实就是将现场的视频信号从手机端,电脑端,摄影机端打包传到服务器的过程。“推流”对网络要求比较高,如果网络不稳定,直播效果就会很差,观...
- 一看就会!谷歌广告转化跟踪详细设置指南来了
-
在出海推广业务中,投放广告最常见的目的是获取订单,但我们怎么知道有没有达成投放目的呢?谷歌转化跟踪技术就可以做到!熟悉谷歌的卖家朋友都知道,转化跟踪在最近几年变得越来越复杂了,虽然有很多选项可以自定义...
- Android原生编解码接口MediaCodec详解
-
作者:躬行之MediaCodec是Android中的编解码器组件,用来访问底层提供的编解码器,通常与MediaExtractor、MediaSync、MediaMuxer、MediaCrypt...
- 手把手搭建RTSP流媒体服务器(rtsp 流媒体)
-
0.引言本文主要讲解如何搭建RTSP流媒体服务器的过程,使用开源项目ZLMediaKit。通过这个开源项目,推RTSP流到服务器,然后拉流端可以拉取RTSP、RTMP等流。ZLMediaKit码云链接...
- MediaInfo 24.04.0 是一个关于多媒体文件的信息提供工具
-
MediaInfo24.04.0是一个关于多媒体文件的信息提供工具(仅当文件中包含信息时才提供):包括常规信息(标题、作者、导演、专辑、曲目编号、日期、时长等);视频信息(编解码器、画面比例、帧率...
- rmvb格式视频怎么打开,rmvb转MP4认准这个方法
-
一、rmvb是什么格式? RMVB是一种视频文件格式,其中的VB指的是可变比特率。比起上一代的RM格式,RMVB 格式的画面比较清晰,因为它是降低了静态画面下的比特率。 二、制作rmvb ①...
- 教你用Plex Media Server,把铁威马变成你的“私人好莱坞”!
-
TNAS(铁威马NAS)中可以安装多媒体服务器、影视、PlexMediaServer、EmbyServer作为个人媒体服务器使用。PlexMediaServer可以组织整理TNAS上的媒体...
- 你肯定用过!经典Windows软件被抛弃
-
Windows系统这些年持续更新的过程中,不断融入新的软件和功能的同时,一些经典的应用也渐渐成为了历史……Windows媒体播放器被抛弃Windows系统不断地推陈出新,一些老旧的组件也难免被抛弃,在...
- 博思得Q8标签打印全能手(博思得标签打印机安装教程)
-
2014-12-0905:35:00作者:宋达希【中关村在线办公打印频道原创】服装吊牌、洗涤标签、产品说明标签等都要用到标签打印机,这些标签涵盖多种尺寸的长度和宽度以及材质。另外作为一件商品或者产...
- flv文件用什么播放器打开,这样做不踩雷!
-
FLV是FLASHVIDEO的简称,是随着FlashMX的推出发展而来的视频格式。它的出现有效地解决了视频文件导入Flash后,使导出的SWF文件体积庞大,不能在网络上很好的使用等问题。一、...
- media player怎么转换格式?音频转换神器推荐!
-
Windowsmediaplayer怎么转换格式?WindowsMediaPlayer是微软公司出品的一款多媒体播放器,通常简称“WMP”。提供了编辑音频和视频文件的功能。用户可以使用该软件导...
- 视频参数检查工具更新:MediaInfo 23.10
-
MediaInfo提供有关视频或音频文件的技术和标签信息。信息示例包括编解码器、比特率、每秒帧数、宽度、高度、频道数、持续时间、标题、作者、字幕语言和章节名称。多种方式可以查看信息(文本、工作表、树和...
- 多媒体管理软件:JRiver Media Center 31.0.68 (64位)
-
JRiverMediaCenter64位是适用于大量库的完整媒体解决方案。它组织、播放和标记所有类型的媒体文件,并对Xbox、PS3、UPnP、DLNA和TiVo进行翻录、刻录。JRiverM...
- 一周热门
- 最近发表
- 标签列表
-
- HTML 教程 (33)
- HTML 简介 (35)
- HTML 实例/测验 (32)
- HTML 测验 (32)
- JavaScript 和 HTML DOM 参考手册 (32)
- HTML 拓展阅读 (30)
- HTML常用标签 (29)
- HTML文本框样式 (31)
- HTML滚动条样式 (34)
- HTML5 浏览器支持 (33)
- HTML5 新元素 (33)
- HTML5 WebSocket (30)
- HTML5 代码规范 (32)
- HTML5 标签 (717)
- HTML5 标签 (已废弃) (75)
- HTML5电子书 (32)
- HTML5开发工具 (34)
- HTML5小游戏源码 (34)
- HTML5模板下载 (30)
- HTTP 状态消息 (33)
- HTTP 方法:GET 对比 POST (33)
- 键盘快捷键 (35)
- 标签 (226)
- HTML button formtarget 属性 (30)
- CSS 水平对齐 (Horizontal Align) (30)