【项目总结】基于SSM+SpringBoot+Redis的个人博客系统项目总结

news2024/11/15 13:53:14

文章目录

  • 项目介绍(开发背景)
  • 数据库设计
  • 主要使用到的技术点
    • 前端
    • 后端
      • 自定义统一返回对象
      • 自定义拦截器
      • 加盐加密操作
      • 分页功能
      • session持久化
      • 自定义头像的存储和获取
  • 项目编写过程中遇到的困难点
    • 困难点一(小)
    • 困难点二(小)
    • 困难点三(大)
  • 上传部署
  • 总结

项目介绍(开发背景)

        对于一个程序员来说,定期整理总结并写博客是不可或缺的步骤,不管是对近期新掌握的技术或者是遇到bug解决bug过程的记录等,都是非常有必要的。
        目前的博客网站有很多,比如CSDN、掘金、博客园等。本人在整个学习的阶段也都会经常在上面发布文章和见解等,最近学了一些开发所需要的主流框架,因此以做项目代学,做出了一个简易版的个人博客系统。
        在这个博客系统中不需要担心安全问题(因为用户密码采用了加盐加密处理,破解成本高,且在一些私人界面加入了拦截器等)、短时间内不需要重复登录(因为在redis中存储了用户的session信息并进行了持久化处理)、可以随时随地地更换自己喜欢的头像(因为上传的图片发送到服务器并使用Nginx存储起来,稍等一段时间后台刷新后即可看到换的头像)。

数据库设计

        由于这是一个简易版的博客系统,同时也是初代版本,因此在数据库的设计中还是比较简单的,只有两张表:用户表和博客表。后面做下一个版本的时候可以考虑加入文章下评论的表。

        用户表主要存的字段是用户id、用户名、密码、头像、创建时间,其中主键是用户id且是自增的,编写出下面sql语句:

create table userinfo(
    id int primary key auto_increment,
    username varchar(100) not null,
    password varchar(100) not null,
    photo varchar(500) default '',
    createtime timestamp not null default current_timestamp,
    `state` int default 1
) default charset 'utf8mb4';

        博客表主要存的字段是博客id、标题、内容、发布日期、发布用户、浏览量,其中主键是博客id且是自增的,编写出下面sql语句:

create table articleinfo(
    id int primary key auto_increment,
    title varchar(100) not null,
    content text not null,
    createtime timestamp not null default current_timestamp,
    uid int not null,
    rcount int not null default 1,
    `state` int default 1
)default charset 'utf8mb4';

主要使用到的技术点

前端

        首先简单说一下本项目中前端代码的基本实现,由于是前后端分离的项目,所以前端需要发送请求给后端,后接收来自后端的响应,这里前端部分我基本使用的都是jQuery来实现的,而且也基本都是使用POST发送请求(相对来说会比较安全),但是也不是说POST就一定好,只是可能会使用的比较多,具体还是需要看实际项目的开发需求来进行制定。这里额外提供出一篇文章:GET和POST的区别。

后端

自定义统一返回对象

        在主流的开发中基本上都会制定一套统一的返回数据形式,因为这样既可以方便后端程序员在返回数据的时候不需要考虑前端是如何接收的,又可以方便前端程序员在获取数据的时候不需要再去看后端返回的都有什么。不管传输的数据是何种形式,都可以一一种统一的格式被获取到,大大提高了开发的效率。
        在返回对象之前需要先对数据进行一下封装,这一步称为“统一数据返回封装”,对于已经是封装好的对象直接返回即可;对于返回的类型是字符串类型的,则需要额外进行一步操作;对于未封装过的对象,直接对其进行封装后即可返回。而返回这个对象的这一步操作就称为“统一返回对象”。

ResponseAdvice类统一数据返回封装:

@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {
        return true;
    }

    @SneakyThrows
    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        //本身已经是封装好的对象
        if(body instanceof HashMap){
            return body;
        }
        //返回的类型是字符创类型(String)
        if(body instanceof String){
            ObjectMapper objectMapper = new ObjectMapper();
            return objectMapper.writeValueAsString(AjaxResult.success(body));
        }
        return AjaxResult.success(body);
    }
}

AjaxResult类自定义统一返回对象:

public class AjaxResult {
    //业务执行成功时返回的方法
    public static HashMap<String, Object> success(Object data){
        HashMap<String, Object> res = new HashMap<>();
        res.put("code", 200);
        res.put("msg", "");
        res.put("data", data);
        return res;
    }

    public static HashMap<String, Object> success(String msg, Object data){
        HashMap<String, Object> res = new HashMap<>();
        res.put("code", 200);
        res.put("msg", msg);
        res.put("data", data);
        return res;
    }

    //业务执行失败时返回的方法
    public static HashMap<String, Object> fail(int code, String msg){
        HashMap<String, Object> res = new HashMap<>();
        res.put("code", code);
        res.put("msg", msg);
        res.put("data", "");
        return res;
    }

    public static HashMap<String, Object> fail(int code, String msg, Object data){
        HashMap<String, Object> res = new HashMap<>();
        res.put("code", code);
        res.put("msg", msg);
        res.put("data", data);
        return res;
    }
}

自定义拦截器

        有一些页面是需要用户登录后才能打开的,如果未登录的用户强制打开就会跳转到登录页面,很多网站其实基本上也都是这样操作的,设置一个拦截器。
        以下是设置自定义拦截器的一个模板:

@Component
public class LoginInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //session中得到用户信息(如果在session中得到userinfo对象,说明用户已经登录,否则用户未登录)
        HttpSession session = request.getSession(false);
        if(session != null && session.getAttribute(Constant.SESSION_USERINFO_KEY) != null){
            //当前用户已经登录
            return true;
        }
        response.setStatus(401);
        return false;
    }
}

@Configuration
public class AppConfig implements WebMvcConfigurer {
    @Autowired
    private LoginInterceptor loginInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginInterceptor)
                .addPathPatterns("/**")
                .excludePathPatterns(excludes);
    }
}

        addPathPatterns("/**")是拦截项目中全部的url,而如果有一部分不需要拦截的话,只需要在excludePathPatterns(excludes)中的excludes中添加不拦截url的集合即可。
        为什么不直接设置拦截的url,而是先全部拦截后再开放一个集合呢?原因是在大部分常见的项目中,拦截的是要比不拦截的多很多,所以先拦截全部后再开放一部分显然是比较合理的。
        对于本项目目前来说,设置不拦截的url有以下这些:
在这里插入图片描述
        注意:前端代码一定是不能拦截的,不然页面会刷新不出来!

加盐加密操作

        对于加密操作,可能会有很多人会想到MD5加密(因为学校教的就是MD5加密),MD5加密确实是不可逆的,但是它会存在一个问题,每次加密的结果都是一样的,这样的话黑客是可以通过一张“彩虹表”来暴力枚举破解密码,风险还是比较大的。
        而加盐加密则不会,我们这里加的盐其实是随机生成的盐值,以下面这段代码为例:我们制定出一个64位的密码,其中前32位规定为是盐值的存储,后32位规定为是加盐加密后的密码,注册的时候将盐值和加盐加密后的密文拼接在一起存储在数据库中,而登录的时候则是取出密文中前32位的那个盐值和用户登录时输入的那个密码进行同规则的加密后,如果生成的密文与后32位的密文是相等的,那么说明登录成功,否则失败。

public class SecurityUtil {
    //加密操作
    public static String encrypt(String password){
        //每次生成内容不同但长度固定为32位的盐值
        String salt = UUID.randomUUID().toString().replace("-", "");
        String finalPassword = DigestUtils.md5DigestAsHex((salt + password).getBytes());
        return salt + finalPassword;
    }

    //验证操作
    public static boolean decrypt(String password, String finalPassword){
        if(!StringUtils.hasLength(password) || !StringUtils.hasLength(finalPassword)){
            return false;
        }
        if(finalPassword.length() != 64){
            return false;
        }
        String salt = finalPassword.substring(0, 32);
        String securityPassword = DigestUtils.md5DigestAsHex((salt + password).getBytes());
        return (salt + securityPassword).equals(finalPassword);
    }
}

分页功能

        分页功能实现的前提是需要先知道每一页需要显示多少条数据,也就是说,需要先计算出一个偏移量,以便于在MySQL查询中准确地进行获取数据并显示到页面。

    @RequestMapping("list")
    public List<ArticleInfo> getList(Integer pindex, Integer psize){
        if(pindex == null || psize == null){
            return null;
        }
        int offset = (pindex - 1) * psize;  //分页公式计算偏移量
        return articleService.getList(psize, offset);
    }

对应的MyBatis语句:

    <select id="getList" resultType="com.example.demo.model.ArticleInfo">
        select * from articleinfo limit #{psize} offset #{offset}
    </select>

除此之外,还需要计算总页数,注意这是需要页数进行向上取整操作(数据数不满一页也需要计算成一页):

    @RequestMapping("/totalpage")
    public Integer getTotalCount(Integer psize){
        if(psize != null){
            int totalCount = articleService.getTotalCount();
            return (totalCount + psize) / psize;
        }
        return null;
    }

session持久化

        session持久化的目的是不需要让用户退出后短时间登录不再需要输入密码,因为这样会使用户体验更加好。
        对于session的持久化其实不管是mysql还是redis都是可以实现的,但是目前主流的是存储到redis中,所以在此之前需要先提前安装并连接好redis(会在下篇文章中出教程)。
        其中,存的话直接在用户登录的时候就可以将用户的session存起来,之后设置一个超时的时间(这部分是在配置文件中完成),由于不管是哪一个页面,都是需要对用户信息进行验证的,所以可以直接在验证的时候在redis中看看是否有存在符合的session即可。
在这里插入图片描述

在这里插入图片描述

        session在redis中是这样存储的(如果过期则会在redis中清除):
在这里插入图片描述

自定义头像的存储和获取

        在这一步中我想了很多办法,但是最终都是无法正确的实现头像的更换,最后是通过Nginx来搭建一个图床用来存储图片,之后这个图片就存在于公网中,也就可以很容易地被获取到了。
        用户上传过来的图片命名格式是用户本地的设置名字,直接将这个文件名存起来的话,显然是不合适的,原因是如果有多个用户上传过来的图片内容不同但是文件名相同,这不就冲突了吗,所以,我们在获取到图片之后还需要就图片进行重命名。
        对图片进行重命名有两种操作方式,一种是每次都随机生成一个UUID来代表文件名,这样就不会出现重复的现象,但是由于我们在后台就设置了不能注册相同的用户名,所以,我们也可以使用用户名来用作图片的命名,并且将这个名字+图片格式的后缀加到数据库中,其实这样还有一个好处,就是同一个用户对头像进行更换会对就图片进行覆盖,随机的UUID则不会,减少空间的占用(用户量大的时候)。
        之后就是安装Nginx,并设置文件存储的路径。

    @RequestMapping("/upload")
    public Object upload(HttpServletRequest request, MultipartFile file){
        HttpSession session = request.getSession( false);
        UserInfo userInfo = null;
        if(session != null && session.getAttribute(Constant.SESSION_USERINFO_KEY) != null) {
            userInfo = (UserInfo) session.getAttribute(Constant.SESSION_USERINFO_KEY);
        }
        if(file.isEmpty()){
            return AjaxResult.fail(-1, "请选择图片");
        }
        String originalFileName = file.getOriginalFilename();  //原来图片名
        String[] tmp = originalFileName.split("\\.");
        String ext = "." + tmp[tmp.length - 1];  //取到后缀
        String fileName = userInfo.getUsername() + ext;
        //上传图片
        String pre = "/www/wwwroot/43.139.71.60/blog_user_img/";
        String path = pre + fileName;
        try {
            //存到外部文件夹中
            file.transferTo(new File(path));

            userService.updatePhoto(fileName, userInfo.getId());

            return AjaxResult.success("更换成功!", 1);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return AjaxResult.fail(-1, "更换失败");
    }

项目编写过程中遇到的困难点

        因为这是我真正意义上做的第一个项目,所以在这个过程中遇到的困难点还是比较多的。

困难点一(小)

        由于我一直以来都是以写后端代码为主,所以对于前端页面的制作相对来说会比较困难,很多时候会出现一些未知的错误,调式起来也会不太适应(通常都是会先用Postman先测试后端代码后再看前端如何修改等),但是后面写多了,其实也是有重复性的,慢慢地就适应了。

困难点二(小)

        安装redis和远程连接redis客户端需要小心一点。
        由于这个项目是临时想到要添加一个session的持久化的,使用也是临时在linux上安装redis的,过程中出现了大意,在开放了6379端口之后没有对redis设置密码,导致服务器被黑客入侵,后通过重装服务器解决。
        教训:端口不要随便开放,特别是尽量不要全开,对于一些容易被黑客入侵的端口最好设置一个密码更好一点。

困难点三(大)

        用户头像更换这个问题也是困扰比较久的,好在后来通过同学的指点和交流顺利解决了。
        第一阶段我的思路是:因为用户那边上传过来的文件是会默认存放到target文件夹下的嘛,所以我使用了几个getParentFile()方法以及查找的方法来将图片移动到static文件夹下的img文件夹里面,因为这样前端在获取图片的时候就可以直接获取到。当然,这个思路在本地运行是完全没有问题的,但是部署在linux上之后就会出现一个问题了,因为项目部署是需要打包的,而打的jar包在运行的时候执行getParentFile()方法是会直接跳到整个SpringBoot项目外部的,图片存放位置就会出现错误,后来又查阅了很多相关的文章依旧没有解决这个问题……
        第二阶段我的思路是:能不能在项目外找一个文件夹来存储这些图片呢?说干就干,在后端存储图片的时候直接就指定存储的路径,在项目启动之后,这次确实可以将图片成功存储进来,但是无论如何前端一直报错说获取不到这个图片,即使我在img标签下的src属性中使用的是绝对路径,也是不能获取到图片,第二种思路最后也是以失败告终……
        第三阶段我的思路是:突然有一次我就想到了,不是还有图床这种东西吗,我能不能直接自己搭建一个图床呢?但是后来看到需要“氪金”的时候,作为“白嫖”的我也就放弃了……
        第四阶段我的思路是:正当我想要通过“氪金”来解决这个问题的时候,有个同学(大佬)跟我说了可以使用Nginx来搭建自己的图床,而且还不需要“氪金”的时候,更换头像的难题也就迎刃而解了~

上传部署

        当项目在本地能够顺利跑过之后,接下来就是部署到服务器上了。
        在部署项目之前需要提前准备好的东西有:jdk1.8、redis、mysql及其库和表都需要建好。这里给出linux安装redis和mysql的教程(CentOS 7 通过 yum 安装 MariaDB、安装redis、记录远程客户端连接不上redis的解决等过程)。
        完成上面的准备操作之后,就是部署项目了:将打好的jar包拉到服务器上,输入命令:java -jar [jar包名]就可以打开SpringBoot项目;当然,这样是不能让项目持久运行的,只能当成一个提前的测试运行,持久运行需要输入命令:nohup java -jar [jar包名] &

总结

        这是我真正意义上的第一个项目,有很多功能还没有实现,后面版本会持续进行更新……
        简单总结一下这个博客系统项目:
        优点:使用了主流的SSM+SpringBoot框架进行开发、使用Redis存储用户的session已达到持久化、对密码进行加盐加密处理、合理设置拦截器等。
        缺点:页面制作的不够美观、有部分代码写的还是太过于冗余、实现的功能不太够等。
        关于本项目的全部代码我都放在了我的个人Gitee账户下,有需要的可以点击查看:个人博客系统项目存放代码。(注:特别需要看其中的配置文件application.yml以及所需要导入的依赖包pom.xml,这决定了一个项目是否能够正常启动并运行!)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/399651.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

C++11:右值引用和移动语义

文章目录1. 左值和右值表达式1.1 概念1.2 左值和右值2. 左值引用和右值引用2.1 相互引用2.2 示例代码2.3 左值引用使用场景缺点2.4 右值引用和移动语义小结2.5 移动赋值2.6 右值引用的其他使用场景右值引用版本的插入函数3. 完美转发3.1 万能引用3.2 如何实现完美转发3.3 完美转…

u盘拔掉再插上去文件没了原因|文件恢复方法

如果您遇到了“u盘拔了再插文件变空了”的类似问题困扰&#xff0c;请仔细阅读文本&#xff0c;下面将分享几种方法来恢复u盘上丢失的文件&#xff0c;赶紧来试试&#xff01;为什么u盘拔掉再插上去文件没了“我的u盘为什么放进东西后拔出&#xff0c;再插进电脑去东西就没有了…

从零开始学架构——复杂度来源

复杂度来源——高性能 对性能孜孜不倦的追求是整个人类技术不断发展的根本驱动力。例如计算机,从电子管计算机到晶体管计算机再到集成电路计算机,运算性能从每秒几次提升到每秒几亿次。但伴随性能越来越高&#xff0c;相应的方法和系统复杂度也是越来越高。现代的计算机CPU集成…

前端——5.HTML标签_段落标签和换行标签

这篇文章&#xff0c;我们来讲解一下HTML标签中的段落标签和换行标签 目录 1.段落标签 1.1介绍 1.2实际案例 1.3小拓展 2.换行标签 2.1介绍 2.2实际案例 3.小结 1.段落标签 我们首先来讲解一下段落标签 1.1介绍 在网页中&#xff0c;要把文字有条理地显示出来&…

图像主题颜色提取(Median cut)

前言 之前想对图片素材进行分类管理&#xff0c;除了打标签&#xff0c;还有一样是通过主题色进行分类。于是开始寻找能提取主主题色的工具&#xff0c;最后找到了大名鼎鼎的 Leptonica 库&#xff0c;其中就有中位切割算法的实现。下面附上中位切割算法的其它语言版本的实现。…

keras图片数字识别入门AI机器学习

通过使用mnist&#xff08;AI界的helloworld&#xff09;手写数字模型训练集&#xff0c;了解下AI工作的基本流程。 本例子&#xff0c;要基于mnist数据集&#xff08;该数据集包含了【0-9】的模型训练数据集和测试数据集&#xff09;来完成一个手写数字识别的小demo。 mnist…

Linux内核之内存管理知识以及伙伴系统

内存管理知识以及伙伴系统一、Linux 内核架构图二、虚拟内存地址空间布局2.1、用户空间2.2、内核空间2.3、硬件层面2.4、虚拟地址空间划分2.5、用户虚拟地址空间布局2.6、进程的进程描述和内存描述符关系2.7、内核地址空间布局三、SMP/NUMA 架构3.1、SMP3.2、NUMA四、伙伴系统及…

传输线的物理基础(四):传输线的特性阻抗

特性阻抗和控制阻抗对于一条均匀的线&#xff0c;无论我们选择看哪里&#xff0c;我们都会看到沿线传播时相同的瞬时阻抗。有一个表征传输线的瞬时阻抗&#xff0c;我们给它起了一个特殊的名字&#xff1a;特性阻抗。有一个瞬时阻抗是均匀传输线的特征。我们将这种恒定的瞬时阻…

RZ/G2L工业核心板U盘读写速率测试

1. 测试对象HD-G2L-IOT基于HD-G2L-CORE工业级核心板设计&#xff0c;双路千兆网口、双路CAN-bus、2路RS-232、2路RS-485、DSI、LCD、4G/5G、WiFi、CSI摄像头接口等&#xff0c;接口丰富&#xff0c;适用于工业现场应用需求&#xff0c;亦方便用户评估核心板及CPU的性能。HD-G2L…

idm如何下载种子文件和磁力链接 idm如何下载torrent

采用分段式下载技术并支持断点续传的idm下载加速器&#xff0c;几乎可以胜任所有的下载任务。由于该软件强大的下载能力和仅为10MB的小巧体积&#xff0c;idm被来自全球的用户亲切地称为天花板级的下载软件。那么有关idm如何下载种子文件和磁力链接&#xff0c;idm如何下载torr…

基于vivado(语言Verilog)的FPGA学习(1)——了解viviado面板和编译过程

基于vivado&#xff08;语言Verilog&#xff09;的FPGA学习&#xff08;1&#xff09;——了解程序面板和编译过程 每日废话&#xff1a;最近找实习略微一些焦虑&#xff0c;不想找软件开发&#xff0c;虽然有些C和python基础&#xff08;之前上课学的&#xff09;&#xff0c;…

编码技巧——Redis Pipeline

本文介绍Redis pipeline相关的知识点及代码示例&#xff0c;包括Redis客户端-服务端的一次完整的网络请求、pipeline与client执行多命令的区别、pipeline与Redis"事务"、pipeline的使用代码示例&#xff1b; pipeline与client执行多命令的区别 Redis是一种基于客户…

如何挖掘专利创新点?

“无意中发现了一个巨牛的人工智能教程&#xff0c;忍不住分享一下给大家。教程不仅是零基础&#xff0c;通俗易懂&#xff0c;而且非常风趣幽默&#xff0c;像看小说一样&#xff01;觉得太牛了&#xff0c;所以分享给大家。点这里可以跳转到教程。” 对于广大的软件工程师来说…

W806|CKLINK LITE|ICE调试|HardPoint|elf模板|CSDK|Debug|学习(4):CKLINK调试W806

目录 一、硬件连接 接线方式 错误提示 二、调试前准备 正常识别状态 wm_tool.exe缺失错误​ 三、flash配置 增加W806模板 compiler选项卡 Debug选项卡 ICE设置 正常连接信息 四、调试工程 添加硬断点 断点配置 仿真调试 下载固件 参考&#xff1a; 《手把手教…

《MySQL系列-InnoDB引擎28》表-约束详细介绍

约束 1 数据完整性 关系型数据库系统和文件系统的一个不同点是&#xff0c;关系数据库本身能保证存储数据的完整性&#xff0c;不需要应用程序的控制&#xff0c;而文件系统一般需要在程序端进行控制。当前几乎所有的关系型数据库都提供约束(constraint)机制&#xff0c;该机制…

群智能优化计算中的混沌映射

经实验证明&#xff0c;采用混沌映射产生随机数的适应度函数值有明显提高&#xff0c;用混沌映射取代常规的均匀分布的随机数发生器可以得到更好的结果&#xff0c;特别是搜索空间中有许多局部解时&#xff0c;更容易搜索到全局最优解&#xff0c;利用混沌序列进行种群初始化、…

基于Qt WebEngine 的Web仪器面板GUI程控技术

随着IIoT的发展&#xff0c;很多工业仪器也具备了远程管理的GUI。与早期使用串口进行命令交互不同&#xff0c;这些GUI可以直接在远程呈现数据。 作为希望对仪器、软件进行二次开发的小公司来说&#xff0c;会遇到GUI人工操作转自动化的需求。在无法通过串口等传统接口进行自动…

nextjs开发 + vercel 部署 ssr ssg

前言 最近想实践下ssr 就打算用nextjs 做一个人博客 &#xff0c; vercel 部署 提供免费域名&#xff0c;来学习实践下ssr ssg nextjs 一个轻量级的react服务端渲染框架 vercel 由 Next.js 的创建者制作 支持nextjs 部署 免费静态网站托管 初始化项目 npx create-next-app p…

【Linux】目录结构

Linux世界里&#xff0c;一切皆文件。 /bin&#xff1a;是Binary的缩写&#xff0c;这个目录存放着最经常使用的命令。&#xff08;常用&#xff09; /sbin&#xff1a;s就是Super User的意思&#xff0c;这里存放的是系统管理员使用的系统管理程序。 /home&#xff1a;存放普…

关于Pytorch中的张量学习

关于Pytorch中的张量学习 张量的概念和创建 张量的概念 Tensor是pytorch中非常重要且常见的数据结构&#xff0c;相较于numpy数组&#xff0c;Tensor能加载到GPU中&#xff0c;从而有效地利用GPU进行加速计算。但是普通的Tensor对于构建神经网络还远远不够&#xff0c;我们需…