项目的简单介绍
首先,因为我个人有写博客的习惯,所以我就仿造 CSDN 简单实现了一个自己的个人博客系统。该项目由用户模块,博客模块,评论模块组成。用户模块主要负责的是用户的注册和登录,用户头像的修改等。博客模块主要是博客的发布,删除,修改,点赞,浏览等功能。评论模块包含博客的评论,回复等。
项目具有下面几个亮点:
首先是将 Session 存储到了 Redis 中,实现了 Session 的共享,使得其在分布式环境下可以使用。
然后我对密码进行了一个加盐加密操作,保证了密码的安全性。
我使用 Redis 实现了点赞功能,因为 Redis 的写入和读取速度非常快,很适合点赞这种高频率操作。
然后就是我对博客展示列表页面增加了一个分页功能。
目前就是我项目的介绍,老师您如果您有哪些想详细了解的可以指出来。
1.密码加盐加密
MD5密码是一种数字摘要算法,它可以将任意长度的字符串转换成一个128位的散列值,也就是一个16字节的数字。
为什么要使用加盐加密呢? 首先如果只是单纯使用 MD5 加密,由于 MD5 加密每次生成的值都是相同的,很容易通过暴力枚举的方式进行破解。而加盐加密通过生成随机 32 位盐值,每次生成的密码都是不同的,增加密码安全性。
加密功能
- 先使用 UUID 生成一个随机的 32 位的盐值,并用 String 的 replace 方法替换掉其中的横杠。
- 将盐值和输入的密码相加,并使用 DigestUtils 对其进行 MD5 加密。生成一个最终密码。
- 最后将盐值加上最终密码返回。
解密功能
- 首先,从数据库中获取存取的加密过的密码,获取密码的前 32 位盐值。
- 使用盐值加上输入的密码使用 DigestUtils 对其进行 MD5 加密。
- 判断加密后的密码是否与数据库中的密码相等。
/**
* 对密码进行加密
* @param password
* @return
*/
public static String encrypt(String password){
// 随机生成不同的 32 位的盐值
String salt = UUID.randomUUID().toString().replace("-","");//替换“-”为空
// 生成最终密码 (盐值 + 密码) 进行加密
String finalPassword = DigestUtils.md5DigestAsHex((salt + password).getBytes());
return salt + finalPassword;
}
/**
* 解密验证密码正确性
* @param password 待验证的密码
* @param finalPassword 数据库中饭加密了的密码
* @return
*/
public static boolean decrypt(String password,String finalPassword){
// 获得前面 32 位的盐值
String salt = finalPassword.substring(0,32);
// 通过待验证密码 + 盐值生成最终待确认密码
String securityPassword = DigestUtils.md5DigestAsHex((salt + password).getBytes());
return finalPassword.equals(salt + securityPassword);
}
2.分页功能
初始化当前总页面数量函数实现。
- 首先从前端获取到每页的大小。
- 然后通过 MyBatisPlus 的 list() 获取到总博客数量。
- 用博客数量 / 每页大小就得到总页数。如果结果是小数,如 3.5,则页数为 4,其它同理。
/* 初始化总页数 */
@Override
public Object initTotalPage(Integer pageSize) {
List<Blog> blogs = list();
if(blogs != null) {
// Math.ceil: 如果是 2.x 结果为 3
return (int) Math.ceil(blogs.size() * 1.0 / pageSize);
}
return 0;
}
初始化博客列表函数实现。
- 首先先判断传入的 pageIndex 和 pageSize 是否为空。
- 通过页面大小和当前页面索引计算偏移量。
- 通过 MyBatis 进行数据库查询到该页面的博客。
public List<Blog> initBlogs(Integer pageIndex, Integer pageSize){
if(pageIndex != null && pageSize != null && pageIndex > 0 && pageSize > 0) {
Integer offset = pageSize * (pageIndex - 1);
return mapper.initBlogs(pageSize,offset);
}
return null;
}
<select id="initBlogs" resultType="com.example.demo.entity.Blog">
select * from blog_info limit #{pageSize} offset #{offset}
</select>
3.点赞功能
- 首先获取到当前的用户对象。
- 使用字符串结合被点赞的的博客 id 作为 Redis 的 key。
- 通过当 key 和前用户 id 查询 Redis 中的数据,判断是否已经点赞过。
- 如果没有点赞过,则将点赞记录加入到 Redis 和数据库中,数据库中的点赞数 -1。
- 如果点赞过,则将 Redis 和数据库中的点赞记录清除,数据库中的点赞数 + 1。
Redis 中的点赞记录是存储在 Set 中的,每一个 Set 代表一个博客,存储的是给博客点赞过的用户 id。
而数据库中的点赞记录表如下图所示:使用了 blog_id 和 user_id 保证每条记录的唯一性。
@Transactional
public Object likeBlog(HttpServletRequest request,String likedBlogId) {
// 得到当前用户对象
User curUser = SessionUnit.getLoginUser(request);
if(curUser == null) {
return AjaxResult.fail(-1,"当前用户对象为空");
}
// 创建 Redis key: RedisKey + 博客ID
String key = RedisKeyUtils.BLOG_LIKED_KEY + likedBlogId;
// 判断当前用户是否点赞过
/* 这里返回的是一个 Boolean, 可能为空, 这样可以防止空指针异常 */
/**/
boolean liked = Boolean.TRUE.equals(stringRedisTemplate.opsForSet().isMember(key,curUser.getId().toString()));
// 如果该用户已经点赞过
if(liked) {
/* 数据库操作 */
// 点赞数量 - 1
boolean isSuccess = blogService.update().setSql("like_count = like_count - 1").eq("id",likedBlogId).update();
if(!isSuccess) {
return AjaxResult.fail(-1,"数据库更新失败");
}
// 移除数据库中该条记录
QueryWrapper<BlogLike> wrapper = new QueryWrapper<>();
wrapper.eq("blog_id",likedBlogId);
wrapper.eq("user_id",curUser.getId());
remove(wrapper);
/* Redis 操作 */
// 移除 set 中的该条记录
stringRedisTemplate.opsForSet().remove(key,curUser.getId().toString());
} else {
// 用户没有点赞过
// 点赞数量 + 1
boolean isSuccess = blogService.update().setSql("like_count = like_count + 1").eq("id",likedBlogId).update();
if(!isSuccess) {
return AjaxResult.fail(-1,"数据库更新失败");
}
// 新增一条点赞记录
/* 这里一直报类型错误,暂时不知道什么原因,用传统的 MyBatis 好了 */
// BlogLike blogLike = new BlogLike(Integer.valueOf(likedBlogId),curUser.getId());
// save(blogLike);
mapper.saveLike(likedBlogId,curUser.getId().toString());
/* Redis 操作 */
// 新增 set 一条记录
stringRedisTemplate.opsForSet().add(key,curUser.getId().toString());
}
return AjaxResult.success(1,"用户点赞成功");
}
测试用例设计
功能测试:
- 能否正常点赞和取消点赞。(这里没有实现,是商铺点评系统项目的:点赞后的用户是否按照时间顺序排序)。
- 多个用户点赞同一篇博客点,是否记录了每个用户的点赞。
性能测试:
- 点赞的响应是否够迅速。
- 多用户某一时间段同时点赞同一篇博客的响应速度。
- 在点赞数据量非常大的时候的响应速度。
界面测试:
- 点赞的按钮是否美观。
- 按钮的布局是否合理。
安全性测试:
- 用户在未登录的情况下是否可以点赞。
易用性测试:
- 点赞按钮是否容易被用户找到。
- 点赞后成功的反馈是否直观。
兼容性测试:
- 在不同浏览器下能否兼容。
- 在不同的设备能否兼容。如手机,电脑,平板。
如果是按照接口测试来设计的话:
- 正常和异常情况的测试
- 接口方法:更换不同的方法进行测试,如 Post,Delete 等。
- 参数:更改参数的类型,顺序等进行测试。
- 业务逻辑:对该方法的业务逻辑进行测试,检查 Redis,MySQL 中是否有数据。
项目的部署
修改前后端内容并打包
前端:
修改 Axios 的路径 Url,路径记得加 http://,不然拦截器配置即使排除还是会拦截。
// axios.defaults.baseURL = "http://localhost:9090"
axios.defaults.baseURL = "Your own host + Port"
修改完前端配置后,将其打包生成 dist 文件夹:
npm run build
后端:
修改上传图片的文件路径:
// 保存头像的文件路径
// public static final String AVATAR_FILE_PATH = "D:/IDEA Project/MyProject(Github)/MyBlogSystem-III/vue/public/img/avatar/";
// Linux 中存放图片的文件路径
public static final String AVATAR_FILE_PATH = "/home/mycnblog-pplus/vue/dist/img/avatar/";
修改配置文件 application.yml 和 application.properties
修改 Redis 的库序号( database )
# Redis Configuration
spring.redis.host="Your own host"
spring.redis.password="Your own password"
spring.redis.port=6379
spring.redis.database=4
spring.redis.timeout=1800000
spring.redis.lettuce.pool.max-active=20
spring.redis.lettuce.pool.max-wait=-1
spring.redis.lettuce.pool.max-idle=5
spring.redis.lettuce.pool.min-idle=0
修改 MySQL 的密码,如果没有密码则设置为空:
spring:
datasource:
url: jdbc:mysql://localhost:3306/mycnblog_pplus?characterEncoding=utf8&useSSL=false
username: root
#如果要部署到云服务器这里密码要更改(没有密码则为空)
password: "Your own password"
driver-class-name: com.mysql.cj.jdbc.Driver
# 配置数据源类型
type: com.zaxxer.hikari.HikariDataSource
修改完成后,双击 package 即可将项目打成 Jar 包
在 target 文件夹中找到已经打包好的 Jar 包
部署项目
Vue 项目部署:可以看这篇博客,讲的很详细 VUE项目部署-CSDN博客