需求分析
编写博客系统的目的是为了更加深入的了解项目相关各种命令及程序流程,使自己熟练的掌握一些基础知识
技术需求
开发使用了前后端分离的技术,使用docker部署
-
server : 使用python语言,fastapi库
- fastapi sqlalchemy aiomysql实现 RESTful API与api文档自动生成,pydantic数据校验
- 前后端鉴权使用OAuth2 实现 Bearer JWT 令牌
- slowapi限制api速率
- Pillow库生成简单验证码,文本文件缓存验证码
-
client :nodejs的前端开发环境
-
vite + ts :前端构建工具
-
vue3 + vue-router + navie-ui : js库,框架
-
openapi-generate-typescript :js库,用于自动生成axios请求代码
-
markdown解析和高亮使用highlight.js,编辑器v-md-editor
-
-
feature
- 访问统计
- 导出excel
- 图片压缩上传
- 暗色模式
功能需求分析
系统主要完成以下几方面的功能:
- 用户管理:用户的注册登录审核,发表文章和评论。
- 文章管理:用户可以在网站中发表删除文章。
- 评论管理:用户可以评论文章和回复其他用户的评论并审核。
- 标签管理:添加和删除标签,给文章设置标签。
用户管理
用户的相关信息如下:用户ID、用户名、用户密码、用户头像、用户组、状态、注册时间、更新时间。
用户注册时需提供用户名、用户密码。
用户登录时可以采用用户名,密码进行登录。
用户可以发布文章、发表评论、回复。审核、删除自己文章的评论,
文章管理
文章的相关信息如下:文章ID、发布日期、发表用户ID、文章标题、文章内容、状态。
文章可以被普通用户发布、修改、删除和评论,但修改和删除仅限于自己发表的。
文章发布时需要设置标签。
评论管理
评论的相关信息如下:评论ID、评论日期、发表用户ID、评论文章ID、评论内容、父评论ID、状态。
评论可以被用户发表和删除以及被其他用户回复。
标签管理
标签的相关信息如下:标签ID、标签名称
用户发表文章时可以设置标签
系统功能及用例图
普通用户
-
个人资料管理
修改头像,修改密码
-
文章管理
发表文章,删除文章
-
评论管理
发表评论,回复评论,删除自己发表的评论,审核/删除自己文章下的评论
-
标签管理
创建标签
管理员
在个人后台和普通用户一致,并且可以访问管理员后台
-
用户管理
审核/封禁用户,修改是否可以注册,查看api接口统计
用例图
数据库表设计
表结构
用户表(tb_user)
字段名 | 数据类型 | 说明 |
---|---|---|
id | int | 用户ID,自增主键 |
username | varchar(255) | 用户名,不可重复 |
password | varchar(255) | 用户密码,加密存储 |
avatar | varchar(255) | 用户头像 |
group_id | int | 用户组ID,用于权限管理 |
state | int | 用户状态ID,0表示未审核,1表示已审核 |
created_at | datetime | 注册时间 |
updated_at | datetime | 最近更新时间 |
文章表(tb_post)
字段名 | 数据类型 | 说明 |
---|---|---|
id | int | 文章ID,自增主键 |
user_id | int | 发表用户ID |
title | varchar(50) | 文章标题 |
content | text | 文章内容 |
state | int | 文章状态ID,0表示未审核,1表示已审核 |
created_at | datetime | 发表时间 |
updated_at | datetime | 最近更新时间 |
文章标签关联表(tb_post_tag)
字段名 | 数据类型 | 说明 |
---|---|---|
post_id | int | 文章ID,外键关联tb_post表 |
tag_id | int | 标签ID,外键关联tb_tag表 |
评论表(tb_comments)
字段名 | 数据类型 | 说明 |
---|---|---|
id | int | 评论ID,自增主键 |
post_id | int | 评论文章ID,外键关联tb_post表 |
parent_id | int | 父评论ID,用于表示评论的层级关系 |
uid | int | 评论用户ID,外键关联tb_user表 |
content | text | 评论内容 |
state | int | 评论状态ID,0表示未审核,1表示已审核 |
created_at | datetime | 发表时间 |
标签表(tb_tag)
字段名 | 数据类型 | 说明 |
---|---|---|
id | int | 标签ID,自增主键 |
name | varchar(50) | 标签名称 |
reference_count | int | 标签被引用次数,用于热门标签的排序 |
以上是blog数据库的表结构设计,其中表之间通过外键关联实现数据的关联和查询。
ER图
sql 建表语句
使用orm 模型生成,sql文件是运行后导出的
模型类,每一个类对应一个数据表
from sqlalchemy import Column, String, Integer, DateTime, func, ForeignKey, Text
from sqlalchemy.orm import declarative_base, relationship
Base = declarative_base()
class User(Base):
__tablename__ = 'tb_user'
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
username = Column(String(255), unique=True, nullable=False, comment='用户名')
password = Column(String(255), unique=False, nullable=False, comment='密码')
avatar = Column(String(255), unique=False, nullable=True, default='', comment='头像')
group_id = Column(Integer, unique=False, nullable=False, comment='用户组id', default=0)
state = Column(Integer, unique=False, nullable=False, comment='状态id', default=0)
created_at = Column(DateTime, default=func.now(), server_default=func.now(), nullable=False, comment='创建时间')
updated_at = Column(DateTime, default=func.now(), server_default=func.now(), nullable=False, onupdate=func.now(),
comment='更新时间')
under_posts = relationship('Post', back_populates='own_user', cascade="all, delete") # 用户拥有的文章
def __repr__(self):
return f'<User>id={self.id},username={self.username},group_id={self.group_id} ...'
class Tag(Base):
__tablename__ = 'tb_tag'
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
name = Column(String(50), nullable=False)
reference_count = Column(Integer)
under_posts = relationship('Post', secondary='tb_post_tag', passive_deletes=True) # 多对多, tag被多个post拥有
def __repr__(self):
return f'<tag> id={self.id},name={self.name}'
class Post(Base):
__tablename__ = 'tb_post'
id = Column(Integer, primary_key=True, index=True, autoincrement=True)
user_id = Column(Integer, ForeignKey("tb_user.id"), nullable=False)
title = Column(String(50), nullable=False)
content = Column(Text, nullable=False)
state = Column(Integer, unique=False, nullable=False, comment='状态id', default=0, server_default='0')
created_at = Column(DateTime, default=func.now(), server_default=func.now(), nullable=False, comment='创建时间')
updated_at = Column(DateTime, default=func.now(), server_default=func.now(), nullable=False, onupdate=func.now(),
comment='更新时间')
own_user = relationship('User', back_populates='under_posts', passive_deletes=True) # 文章作者
own_tags = relationship('Tag', secondary='tb_post_tag', overlaps='under_posts') # 拥有的tag
own_comments = relationship("Comment", back_populates="under_post", cascade="all, delete") # 拥有的评论
def __repr__(self):
return f'<post> id={self.id},title={self.title} ...'
class PostTag(Base): # 中间表
__tablename__ = 'tb_post_tag'
post_id = Column(Integer, ForeignKey("tb_post.id"), primary_key=True)
tag_id = Column(Integer, ForeignKey("tb_tag.id"), primary_key=True)
def __repr__(self):
return f'<post_tage> post_id={self.post_id},tag_id={self.tag_id} ...'
class Comment(Base):
__tablename__ = 'tb_comments'
id = Column(Integer, primary_key=True)
post_id = Column(Integer, ForeignKey('tb_post.id'), nullable=False)
parent_id = Column(Integer, nullable=False)
uid = Column(Integer, ForeignKey('tb_user.id'), nullable=False)
content = Column(Text, nullable=False)
state = Column(Integer, nullable=False, comment='状态id', default=0)
created_at = Column(DateTime, default=func.now(), server_default=func.now(), nullable=False, comment='创建时间')
under_post = relationship("Post", back_populates="own_comments", passive_deletes=True) # 所属文章
def __repr__(self):
return f'<Comment> id={self.id},content={self.content} ...'
建表语句
-- --------------------------------------------------------
-- 主机: 127.0.0.1
-- 服务器版本: 8.0.29 - MySQL Community Server - GPL
-- 服务器操作系统: Win64
-- HeidiSQL 版本: 12.3.0.6589
-- --------------------------------------------------------
CREATE DATABASE IF NOT EXISTS `blog`;
USE `blog`;
-- 导出 表 blog.tb_comments 结构
CREATE TABLE IF NOT EXISTS `tb_comments` (
`id` int NOT NULL AUTO_INCREMENT,
`post_id` int NOT NULL,
`parent_id` int NOT NULL,
`uid` int NOT NULL,
`content` text NOT NULL,
`state` int NOT NULL COMMENT '状态id',
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
KEY `post_id` (`post_id`),
KEY `uid` (`uid`),
CONSTRAINT `tb_comments_ibfk_1` FOREIGN KEY (`post_id`) REFERENCES `tb_post` (`id`),
CONSTRAINT `tb_comments_ibfk_2` FOREIGN KEY (`uid`) REFERENCES `tb_user` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=37 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- 导出 表 blog.tb_post 结构
CREATE TABLE IF NOT EXISTS `tb_post` (
`id` int NOT NULL AUTO_INCREMENT,
`user_id` int NOT NULL,
`title` varchar(50) NOT NULL,
`content` text NOT NULL,
`state` int NOT NULL DEFAULT '0' COMMENT '状态id',
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `user_id` (`user_id`),
KEY `ix_tb_post_id` (`id`),
CONSTRAINT `tb_post_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `tb_user` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=28 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- 导出 表 blog.tb_post_tag 结构
CREATE TABLE IF NOT EXISTS `tb_post_tag` (
`post_id` int NOT NULL,
`tag_id` int NOT NULL,
PRIMARY KEY (`post_id`,`tag_id`),
KEY `tag_id` (`tag_id`),
CONSTRAINT `tb_post_tag_ibfk_1` FOREIGN KEY (`post_id`) REFERENCES `tb_post` (`id`),
CONSTRAINT `tb_post_tag_ibfk_2` FOREIGN KEY (`tag_id`) REFERENCES `tb_tag` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- 导出 表 blog.tb_tag 结构
CREATE TABLE IF NOT EXISTS `tb_tag` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(50) NOT NULL,
`reference_count` int DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `ix_tb_tag_id` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=26 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- 导出 表 blog.tb_user 结构
CREATE TABLE IF NOT EXISTS `tb_user` (
`id` int NOT NULL AUTO_INCREMENT,
`username` varchar(255) NOT NULL COMMENT '用户名',
`password` varchar(255) NOT NULL COMMENT '密码',
`avatar` varchar(255) DEFAULT NULL COMMENT '头像',
`group_id` int NOT NULL COMMENT '用户组id',
`state` int NOT NULL COMMENT '状态id',
`created_at` datetime NOT NULL COMMENT '创建时间',
`updated_at` datetime NOT NULL COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `username` (`username`),
KEY `ix_tb_user_id` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=22 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
代码安全问题检测
使用python库 bandit
Bandit 是一款旨在查找 Python 代码中常见安全问题的工具。为此,Bandit 处理每个文件,从中构建 AST
运行结果
(fastapipg-3.11) 24123@lolik D:\24123\code\python\fastapipg git:use-pdm ~5
> bandit -r app.py sql api config
[main] INFO profile include tests: None
[main] INFO profile exclude tests: None
[main] INFO cli include tests: None
[main] INFO cli exclude tests: None
[main] INFO running on Python 3.11.1
[node_visitor] WARNING Unable to find qualified name for module: app.py
Run started:2023-06-17 08:30:52.391282
Test results:
>> Issue: [B324:hashlib] Use of weak MD5 hash for security. Consider usedforsecurity=False
Severity: High Confidence: High
CWE: CWE-327 (https://cwe.mitre.org/data/definitions/327.html)
More Info: https://bandit.readthedocs.io/en/1.7.5/plugins/b324_hashlib.html
Location: api\userapi.py:144:10
143
144 md5 = hashlib.md5(data).hexdigest()
145 filename = f"{md5}{ext}"
--------------------------------------------------
>> Issue: [B608:hardcoded_sql_expressions] Possible SQL injection vector through string-based query construction.
Severity: Medium Confidence: Low
CWE: CWE-89 (https://cwe.mitre.org/data/definitions/89.html)
More Info: https://bandit.readthedocs.io/en/1.7.5/plugins/b608_hardcoded_sql_expressions.html
Location: config\options.py:162:46
161 sheet = workbook.add_sheet(f'{table_name}', cell_overwrite_ok=True)
162 result = session.execute(text(f"SELECT * FROM {table_name}"))
163 columns = (i[0] for i in result.cursor.description)
--------------------------------------------------
>> Issue: [B605:start_process_with_a_shell] Starting a process with a shell, possible injection detected, security issue.
Severity: High Confidence: High
CWE: CWE-78 (https://cwe.mitre.org/data/definitions/78.html)
More Info: https://bandit.readthedocs.io/en/1.7.5/plugins/b605_start_process_with_a_shell.html
Location: config\options.py:203:4
202 if not args.vite and args.open:
203 os.system(f'start chrome http://127.0.0.1:{args.port}')
204
--------------------------------------------------
>> Issue: [B605:start_process_with_a_shell] Starting a process with a shell, possible injection detected, security issue.
Severity: High Confidence: High
CWE: CWE-78 (https://cwe.mitre.org/data/definitions/78.html)
More Info: https://bandit.readthedocs.io/en/1.7.5/plugins/b605_start_process_with_a_shell.html
Location: config\options.py:208:4
207 f.write(f"export const host:string = 'http://127.0.0.1:{args.port}'")
208 os.system(f'start cmd /k "cd {ppath}static && npm run dev"')
209
--------------------------------------------------
>> Issue: [B101:assert_used] Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.
Severity: Low Confidence: High
CWE: CWE-703 (https://cwe.mitre.org/data/definitions/703.html)
More Info: https://bandit.readthedocs.io/en/1.7.5/plugins/b101_assert_used.html
Location: sql\crud.py:108:12
107 u = (await session.execute(select(User).where(User.username == username_new))).scalar_one_or_none()
108 assert u is not None
109 return UpdateSuccess.from_User(u, "更新成功")
--------------------------------------------------
>> Issue: [B101:assert_used] Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.
Severity: Low Confidence: High
CWE: CWE-703 (https://cwe.mitre.org/data/definitions/703.html)
More Info: https://bandit.readthedocs.io/en/1.7.5/plugins/b101_assert_used.html
Location: sql\crud.py:134:12
133 u = (await session.execute(select(User).where(User.username == user_old.username))).scalar_one_or_none()
134 assert u is not None
135 return UpdateSuccess.from_User(u, "更新成功")
--------------------------------------------------
Code scanned:
Total lines of code: 1604
Total lines skipped (#nosec): 0
Run metrics:
Total issues (by severity):
Undefined: 0
Low: 2
Medium: 1
High: 3
Total issues (by confidence):
Undefined: 0
Low: 1
Medium: 0
High: 5
Files skipped (0):
(fastapipg-3.11) 24123@lolik D:\24123\code\python\fastapipg git:use-pdm ~5
>
问题解析
共有5个问题
-
Issue0
是说Use of weak MD5 hash for security
这里把文件md5作为文件名并没有什么问题,还能避免文件重复
-
Issue1
Possible SQL injection vector through string-based query construction
存在sql注入,这里的sql语句是写死的字符串,且只会在初始化时运行,用来判断数据库是否存在,若不存在则自动创建
-
Issue2,3
Starting a process with a shell, possible injection detected
在开发时用到,生产环境可以直接删除,用来在服务器运行同时打开浏览器,编译前端代码
-
Issue4,5
Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.
断言应该删除,debug时遗留的问题
界面
登录/注册
首页
文章页
文章编辑
文章管理
管理员后台,api访问统计
api文档
项目代码
代码仓库:https://github.com/2412322029/fastapi_test#readme
/docs 目录为对应api文档