🔐 保障我们程序的安全
🎯 本节目标
嘿,开发者们! 👋 是时候给我们的Solana
电影数据库程序穿上一件防弹衣了!🦺 在这一节中,我们将把一个普通的程序变成一个铜墙铁壁般的安全堡垒。
🎪 今天的精彩节目包括:
- 🛡️ 安全防护升级 - 让黑客无处下手!
- ✅ 输入验证大法 - 垃圾数据?门都没有!
- 🔄 更新功能 - 让用户可以修改他们的影评
- 💡 最佳实践 - 专业开发者的秘密武器
🚀 快速开始
想要立即开始编码?点击这个 ⚡ Playground魔法传送门 一键启动!
📁 项目文件结构一览
📦 movie-review-program
┣ 📜 lib.rs # 📚 模块注册中心
┣ 📜 entrypoint.rs # 🚪 程序的大门
┣ 📜 instruction.rs # 📨 指令的邮局
┣ 📜 processor.rs # 🧠 处理逻辑的大脑
┣ 📜 state.rs # 💾 状态存储仓库
┗ 📜 error.rs # ⚠️ 错误处理专家(新成员!)
🔧 初始配置调整
📐 固定账户大小 - 告别动态烦恼!
在 processor.rs
中,我们要做一个聪明的改变:
// 🎯 在 account_len 函数里
// ❌ 旧方式:动态计算(麻烦且容易出错)
// let account_len: usize = 1 + 1 + (4 + title.len()) + (4 + description.len());
// ✅ 新方式:固定大小(简单粗暴有效!)
let account_len: usize = 1000; // 💡 足够大,省心省力!
💡 Pro Tip: 固定大小意味着更新评论时不用重新计算租金,这就像买了个大房子,再也不用担心装不下新家具了!
🏗️ 状态管理升级
在 state.rs
中添加一些魔法咒语:
// 🎭 实现 Sealed 特性 - 给编译器一个优化的机会
impl Sealed for MovieAccountState {}
// 🔍 实现初始化检查 - 防止操作未初始化的账户
impl IsInitialized for MovieAccountState {
fn is_initialized(&self) -> bool {
self.is_initialized // 返回初始化标志
}
}
🚨 自定义错误系统 - 让错误信息更友好!
📝 错误场景清单
想象一下这些灾难场景:
- 😱 用户试图更新一个不存在的评论
- 🎭 有人伪造了PDA地址
- 📏 评论内容比《战争与和平》还长
- ⭐ 有人想给电影打100颗星(虽然热情可嘉,但不符合规则)
🎨 创建专属错误类型
在 error.rs
中,让我们创建一个错误艺术馆:
use solana_program::program_error::ProgramError;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum ReviewError {
// 错误 0️⃣ - 账户还在睡觉
#[error("Account is not initialized yet! 😴")]
UninitializedAccount,
// 错误 1️⃣ - PDA身份证不匹配
#[error("PDA mismatch! This is not the droid you're looking for 🤖")]
InvalidPDA,
// 错误 2️⃣ - 数据太长了
#[error("Data is too long! Keep it concise, please 📏")]
InvalidDataLength,
// 错误 3️⃣ - 评分不合理
#[error("Rating must be between 1-5 stars! ⭐")]
InvalidRating,
}
// 🔄 转换魔法 - 让自定义错误变成程序错误
impl From<ReviewError> for ProgramError {
fn from(e: ReviewError) -> Self {
ProgramError::Custom(e as u32)
}
}
别忘了在 processor.rs
中引入我们的新朋友:
// 🎯 在 processor.rs 顶部
use crate::error::ReviewError;
🛡️ 强化 add_movie_review
函数
1️⃣ 签名验证 - 确认身份!
// 🔍 获取账户信息
let account_info_iter = &mut accounts.iter();
let initializer = next_account_info(account_info_iter)?;
let pda_account = next_account_info(account_info_iter)?;
let system_program = next_account_info(account_info_iter)?;
// ✍️ 检查签名 - 没签名?没门!
if !initializer.is_signer {
msg!("🚫 Hey! You forgot to sign! No signature, no service!");
return Err(ProgramError::MissingRequiredSignature)
}
2️⃣ PDA验证 - 防伪认证!
// 🔐 生成预期的PDA
let (pda, bump_seed) = Pubkey::find_program_address(
&[initializer.key.as_ref(), title.as_bytes().as_ref()],
program_id
);
// 🎯 验证PDA是否匹配
if pda != *pda_account.key {
msg!("❌ PDA doesn't match! Nice try, but no cigar!");
return Err(ProgramError::InvalidArgument)
}
3️⃣ 数据验证 - 质量把关!
// ⭐ 检查评分范围(1-5星)
if rating > 5 || rating < 1 {
msg!("🌟 Rating must be 1-5 stars! We're not Michelin!");
return Err(ReviewError::InvalidRating.into())
}
// 📏 检查数据长度
let total_len: usize = 1 + 1 + (4 + title.len()) + (4 + description.len());
if total_len > 1000 {
msg!("📚 Your review is longer than a novel! Please keep it under 1000 bytes!");
return Err(ReviewError::InvalidDataLength.into())
}
🎯 Fun Fact: 为什么是1000字节?因为这足够写一篇精彩的影评,但又不会让区块链变成图书馆!
🆕 实现更新功能 - 让用户改变主意!
📋 第一步:更新指令枚举
在 instruction.rs
中添加新变体:
pub enum MovieInstruction {
// 🎬 添加新评论
AddMovieReview {
title: String,
rating: u8,
description: String
},
// ✏️ 更新已有评论(新功能!)
UpdateMovieReview {
title: String,
rating: u8,
description: String
}
}
🎯 第二步:解包逻辑升级
impl MovieInstruction {
pub fn unpack(input: &[u8]) -> Result<Self, ProgramError> {
// 🎪 分离变体类型和数据
let (&variant, rest) = input.split_first()
.ok_or(ProgramError::InvalidInstructionData)?;
// 📦 解析负载数据
let payload = MovieReviewPayload::try_from_slice(rest).unwrap();
// 🎭 根据变体类型返回相应指令
Ok(match variant {
0 => Self::AddMovieReview { // 🆕 新增
title: payload.title,
rating: payload.rating,
description: payload.description
},
1 => Self::UpdateMovieReview { // ✏️ 更新
title: payload.title,
rating: payload.rating,
description: payload.description
},
_ => {
msg!("❓ Unknown instruction variant!");
return Err(ProgramError::InvalidInstructionData)
}
})
}
}
🎮 第三步:处理器路由
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8]
) -> ProgramResult {
// 📨 解包指令
let instruction = MovieInstruction::unpack(instruction_data)?;
// 🚦 路由到对应的处理函数
match instruction {
MovieInstruction::AddMovieReview { title, rating, description } => {
msg!("➕ Processing AddMovieReview...");
add_movie_review(program_id, accounts, title, rating, description)
},
MovieInstruction::UpdateMovieReview { title, rating, description } => {
msg!("✏️ Processing UpdateMovieReview...");
update_movie_review(program_id, accounts, title, rating, description)
}
}
}
🎨 实现 update_movie_review
函数
🏗️ 基础框架
pub fn update_movie_review(
program_id: &Pubkey,
accounts: &[AccountInfo],
_title: String, // 💡 注意:title带下划线,因为我们不会修改它
rating: u8,
description: String
) -> ProgramResult {
msg!("🎬 Lights, Camera, Update! Starting movie review update...");
// 🎯 获取账户迭代器
let account_info_iter = &mut accounts.iter();
// 📦 解包账户
let initializer = next_account_info(account_info_iter)?;
let pda_account = next_account_info(account_info_iter)?;
// 更多逻辑即将到来...
Ok(())
}
🔒 安全检查大礼包
// 1️⃣ 所有权检查 - 确保程序拥有这个账户
if pda_account.owner != program_id {
msg!("🚫 This account doesn't belong to our program!");
return Err(ProgramError::IllegalOwner)
}
// 2️⃣ 签名检查 - 确保是本人操作
if !initializer.is_signer {
msg!("✍️ Please sign your transaction!");
return Err(ProgramError::MissingRequiredSignature)
}
// 3️⃣ 解包账户数据
msg!("📦 Unpacking account data...");
let mut account_data = try_from_slice_unchecked::<MovieAccountState>(
&pda_account.data.borrow()
).unwrap();
msg!("✅ Account data unpacked successfully!");
🎯 深度验证
// 🔐 验证PDA
let (pda, _bump_seed) = Pubkey::find_program_address(
&[
initializer.key.as_ref(),
account_data.title.as_bytes().as_ref()
],
program_id
);
if pda != *pda_account.key {
msg!("❌ PDA validation failed!");
return Err(ReviewError::InvalidPDA.into())
}
// 🔍 检查账户是否已初始化
if !account_data.is_initialized() {
msg!("😴 Account is not initialized yet!");
return Err(ReviewError::UninitializedAccount.into());
}
// ⭐ 验证评分
if rating > 5 || rating < 1 {
msg!("🌟 Invalid rating! Must be 1-5 stars");
return Err(ReviewError::InvalidRating.into())
}
// 📏 检查数据长度
let total_len: usize = 1 + 1 + (4 + account_data.title.len()) + (4 + description.len());
if total_len > 1000 {
msg!("📚 Data too long! Maximum 1000 bytes");
return Err(ReviewError::InvalidDataLength.into())
}
💾 保存更新
// 🎨 更新数据
account_data.rating = rating;
account_data.description = description;
// 💾 序列化并保存
account_data.serialize(&mut &mut pda_account.data.borrow_mut()[..])?;
msg!("🎉 Review updated successfully!");
Ok(())
🧪 测试你的杰作!
🛠️ 构建和部署
# 🔨 构建程序
cargo build-sbf
# 🚀 部署到链上
solana program deploy
# 📋 复制程序地址
echo "Don't forget to copy your program address!"
🎨 前端测试
# 📦 克隆前端项目
git clone https://github.com/all-in-one-solana/solana-movie-frontend
# 📁 进入项目目录
cd solana-movie-frontend
# 🎯 切换到更新功能分支
git checkout solution-update-reviews
# 📦 安装依赖
npm install
# 🚀 启动应用
npm run dev
🎉 成功啦! 现在你的前端应该可以:
- 📝 添加新的电影评论
- ✏️ 更新已有的评论
- 🎨 展示所有评论
🏆 终极挑战 - 学生介绍项目
🎯 任务清单
现在轮到你大展身手了!拿起你的键盘,让我们升级学生介绍项目:
📋 必做任务:
-
➕ 添加更新功能
- 允许学生修改他们的自我介绍
- 保持名字不变,只更新留言内容
-
🔐 安全升级包
- ✅ 签名验证
- 🔍 PDA验证
- 📏 数据长度检查
- 🎯 初始化状态检查
🎁 加分项:
- 🌟 创意功能(可选)
- 添加时间戳
- 实现点赞功能
- 添加标签系统
🚀 起始代码
从这里开始你的冒险:📦 起始代码传送门
💡 专业建议
🧠 智慧锦囊:
- 先实现基础功能,再添加花哨的特性
- 每添加一个检查,都要写对应的测试
- 错误信息要友好且有帮助
- 记得给你的代码添加有趣的注释!
🏁 卡住了?
别担心!这里有一份参考答案:🎯 解决方案
但是记住:
- 🎨 你的实现可能和答案不同,那也很棒!
- 💡 重要的是理解概念,而不是复制代码
- 🚀 创新和改进永远受欢迎!
🎊 总结
恭喜你!🎉 你已经成功地:
- 🔐 加固了程序安全
- ✅ 实现了完整的验证系统
- 🔄 添加了更新功能
- 🧠 学会了最佳实践
🌟 下一步?
- 尝试添加删除功能
- 实现评论的评论(嵌套评论)
- 创建一个评分排行榜
- 天空才是你的极限!
记住: 安全的程序 = 快乐的用户 = 成功的项目! 🚀
💬 有问题? 在社区里提问,我们都在这里帮助你!
🔗 分享你的成果 在Twitter上 @我们,展示你的杰作!
祝编码愉快!Happy Coding! 🎉👨💻👩💻