springboot博客系统详解与实现(后端实现)

server/2025/2/26 17:43:41/

目录

前言:

项目介绍

一、项目的准备工作

1.1 数据准备

1.2 项目创建

1.3 前端页面的准备

1.4 配置配置文件

二、公共模块

2.1 根据需求完成公共层代码的编写

2.1.1 定义业务状态枚举

2.1.2 统一返回结果

2.1.3 定义项目异常

2.1.4 统一异常处理

三、业务代码

3.1 持久层与Mapper的编写

3.1.1、实体类(userInfo,BlogInfo)

3.1.2 创建BlogInfoMapper和UserInfoMapper接口

3.2 实现博客列表

3.3 实现博客列表详情

3.4 实现用户登录

3.4.1 JWT

3.4.2 实现强制登录功能

3.5 实现显示用户信息功能

3.6 实现发布博客功能

3.7 实现修改博客功能

3.8 实现删除博客功能

3.9 实现密码加密功能


前言:

通过spring框架和MyBatis的基本使用,完成博客系统从0到1的实现。

gitee也可下载整个项目 

链接  --> blog-system: 前端jQuery 后端 Springboot 数据库 MySQL

项目介绍

前端使用JQuery框架,后端使用SpringBoot ,数据库使用MySQL

一共5个界面 分别是:

1、登录页面

2、文章列表页面

3、文章详情页面

4、文章编辑页面

5、文章发表页面

功能描述:

用户完整登录,可以查看所有用户的博客信息 通过点击查看全文可以查看该博客的全部内容。如果该博客的作者是当前用户,那么该用户可以完成修改文章、删除文章、发布文章等操作。

页面预览:

登录页面:

文章列表页面:

文章详情页面:

文章编辑页面:

 文章发表页面:

 下面是各个页面的实现

一、项目的准备工作

1.1 数据准备

创建SQL表user_info(用户表)blog_info(博客表)

java">-- 创建java_blog_spring数据库
create database if not exists java_blog_spring charset utf8mb4;
use java_blog_spring;-- 用户表
DROP TABLE IF EXISTS java_blog_spring.user_info;
CREATE TABLE java_blog_spring.user_info
(`id`          INT          NOT NULL AUTO_INCREMENT,`user_name`   VARCHAR(128) NOT NULL,`password`    VARCHAR(128) NOT NULL,`github_url`  VARCHAR(128) NULL,`delete_flag` TINYINT(4)   NULL DEFAULT 0,`create_time` DATETIME          DEFAULT now(),`update_time` DATETIME          DEFAULT now() ON UPDATE now(),PRIMARY KEY (id),UNIQUE INDEX user_name_UNIQUE (user_name ASC)
) ENGINE = INNODBDEFAULTCHARACTERSET = utf8mb4 COMMENT = '用户表';-- 博客表
drop table if exists java_blog_spring.blog_info;
CREATE TABLE java_blog_spring.blog_info
(`id`          INT          NOT NULL AUTO_INCREMENT,`title`       VARCHAR(200) NULL,`content`     TEXT         NULL,`user_id`     INT(11)      NULL,`delete_flag` TINYINT(4)   NULL DEFAULT 0,`create_time` DATETIME          DEFAULT now(),`update_time` DATETIME          DEFAULT now() ON UPDATE now(),PRIMARY KEY (id)
)ENGINE = InnoDBDEFAULT CHARSET = utf8mb4 COMMENT = '博客表';-- 新增用户信息
insert into java_blog_spring.user_info (user_name, password, github_url)
values ('zhangsan', '123456', 'https://gitee.com/bubblefish666/class-java45');
insert into java_blog_spring.user_info (user_name, password, github_url)
values ('lisi', '123456', 'https://gitee.com/bubblefish666/class-java45');-- 新增博客信息
insert into java_blog_spring.blog_info (title, content, user_id)
values ('第⼀篇博客', '111我是博客正⽂我是博客正⽂我是博客正⽂', 1);
insert into java_blog_spring.blog_info (title, content, user_id)
values ('第⼆篇博客', '222我是博客正⽂我是博客正⽂我是博客正⽂', 2);

1.2 项目创建

创建SpringBoot项目,添加SpringMVC和MyBatis对应依赖

pom.xml文件的全部配置:(后续添加相应依赖时会介绍)

java"><?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>3.3.6</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>com.bite</groupId><artifactId>spring-boot-blog</artifactId><version>0.0.1-SNAPSHOT</version><name>spring-boot-blog</name><description>Demo project for Spring Boot</description><url/><licenses><license/></licenses><developers><developer/></developers><scm><connection/><developerConnection/><tag/><url/></scm><properties><java.version>17</java.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>com.mysql</groupId><artifactId>mysql-connector-j</artifactId><scope>runtime</scope></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-spring-boot3-starter</artifactId><version>3.5.5</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId></dependency><!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-api --><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-api</artifactId><version>0.11.5</version></dependency><!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-impl --><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-impl</artifactId><version>0.11.5</version><scope>runtime</scope></dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-jackson</artifactId> <!-- or jjwt-gson if Gson is preferred --><version>0.11.5</version><scope>runtime</scope></dependency></dependencies><profiles><profile><id>dev</id><!--自定义的属性--><properties><profile.name>dev</profile.name><mysql.password>root</mysql.password></properties><activation><activeByDefault>true</activeByDefault></activation></profile><profile><id>prod</id><!--自定义的属性--><properties><profile.name>prod</profile.name><mysql.password>BITE@yyds.666</mysql.password></properties></profile></profiles><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><configuration><excludes><exclude><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></exclude></excludes></configuration></plugin></plugins></build></project>

1.3 前端页面的准备

注意:前端不做过多的解释,本文章重点对后端进行解析!

在static文件夹下粘贴如下文件

1.4 配置配置文件

在application.yml文件中配置如下信息

java"># 配置Spring应用的基本信息
spring:# 应用名称application:name: spring-boot-blog# 数据源配置datasource:# 数据库连接URL,包含数据库的地址、端口、数据库名以及连接参数url: jdbc:mysql://127.0.0.1:3306/java_blog_spring?characterEncoding=utf8&useSSL=false&allowPublicKeyRetrieval=trueusername: root # 数据库用户名password: 123456  # 数据库密码driver-class-name: com.mysql.cj.jdbc.Driver # 数据库驱动类名mybatis-plus:configuration:map-underscore-to-camel-case: true  # 是否开启下划线到驼峰命名的自动转换log-impl: org.apache.ibatis.logging.stdout.StdOutImpl  # 日志实现类# 日志配置
logging:file:name: spring-blog.log # 日志文件名

二、公共模块

项目分为控制层(Controller),服务层(Service),持久层(Mapper).各层之间的调用关系如下:

2.1 根据需求完成公共层代码的编写

在项目中创建common文件 其文件中包含如下文件

1. 统⼀返回结果实体类

(1). code: 业务状态码

        200:业务处理成功

        -1:业务处理失败 , 后续有其他异常信息,可以再补充.

(2). errMsg: 业务处理失败时,返回的错误信息

(3). data: 业务返回数据

2.1.1 定义业务状态枚举

创建ResultStatusEnum枚举类

java">/*** 定义结果状态枚举类* 用于表示API响应的状态码*/
@AllArgsConstructor
public enum ResultStatusEnum {//200成功   -1 失败SUCCESS(200),FAIL(-1);/*** 状态码*/@Getterint code;}

创建Result类

作用:通用用于返回结果类,用于封装接口返回数据

java">/*** 通用返回结果类,用于封装接口返回数据* 该类使用了@Data注解,自动为所有字段生成getter和setter方法* @param <T> 泛型参数,用于表示返回数据的类型*/
@Data
public class Result<T> {// 响应码private int code;// 错误消息private String errMsg;// 返回的数据private T data;/*** 创建一个成功的返回结果* 该方法用于简化成功结果的创建过程,直接设置状态码和数据* @param data 返回的数据,可以是任意类型* @param <T> 泛型参数,表示返回数据的类型* @return 返回一个成功的Result对象*/public static <T> Result<T> ok(T data){Result result = new Result();result.setCode(ResultStatusEnum.SUCCESS.getCode());result.setErrMsg("");result.setData(data);return result;}/*** 创建一个失败的返回结果* 该方法用于简化失败结果的创建过程,直接设置状态码和错误消息* @param errMsg 错误消息* @param <T> 泛型参数,表示返回数据的类型* @return 返回一个失败的Result对象*/public static <T> Result<T> fail(String errMsg){Result result = new Result();result.setCode(ResultStatusEnum.FAIL.getCode());result.setErrMsg(errMsg);return result;}/*** 创建一个带有返回数据的失败返回结果* 该方法用于简化带有返回数据的失败结果的创建过程,直接设置状态码、错误消息和数据* @param errMsg 错误消息* @param data 返回的数据,可以是任意类型* @param <T> 泛型参数,表示返回数据的类型* @return 返回一个带有数据的失败Result对象*/public static <T> Result<T> fail(String errMsg, T data){Result result = new Result();result.setCode(ResultStatusEnum.FAIL.getCode());result.setErrMsg(errMsg);result.setData(data);return result;}
}

2.1.2 统一返回结果

创建ResponseAdvice类

java">/*** 全局响应建议类,用于统一处理控制器的响应体* 通过实现ResponseBodyAdvice接口,可以拦截并修改响应体*/
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {/*** ObjectMapper用于序列化和反序列化JSON对象* 在处理响应体时,会用到它来转换对象为JSON字符串*/@Autowiredprivate ObjectMapper objectMapper;/*** 判断是否支持给定的控制器方法参数和转换器类型* 本方法始终返回true,表示支持所有类型的响应体处理** @param returnType 控制器方法的返回类型* @param converterType 转换器的类型* @return 始终返回true,表示支持所有类型*/@Overridepublic boolean supports(MethodParameter returnType, Class converterType) {return true;}/*** 在响应体写入前进行处理,以统一响应格式* 如果响应体已经是Result类型,则直接返回* 如果响应体是String类型,则将其包装为Result对象并序列化为JSON字符串* 否则,将响应体包装为Result对象并返回** @param body 响应体对象* @param returnType 控制器方法的返回类型* @param selectedContentType 选择的内容类型* @param selectedConverterType 选择的转换器类型* @param request 当前请求对象* @param response 当前响应对象* @return 处理后的响应体* @throws IOException 如果序列化失败*/@SneakyThrows //为以下方法添加try catch 捕获异常@Overridepublic Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {// 如果响应体已经是Result类型,则直接返回if (body instanceof Result<?>){return body;}// 对String类型进行特殊处理, 通常情况下, 我们不返回string类型if (body instanceof String){return objectMapper.writeValueAsString(Result.ok(body));}// 对其他类型,将其包装为Result类型并返回return Result.ok(body);}
}

2.1.3 定义项目异常

创建BlogException类

java">/*** 博客异常类,继承自RuntimeException* 用于处理博客系统中的自定义异常* 包含异常状态码和异常信息*/
@Data
public class BlogException extends RuntimeException{private int code;  // 异常状态码private String message;  // 异常信息/*** 默认构造函数*/public BlogException() {}/*** 根据异常信息构造异常对象** @param message 异常信息*/public BlogException(String message) {this.message = message;}/*** 根据异常状态码和异常信息构造异常对象** @param code    异常状态码* @param message 异常信息*/public BlogException(int code, String message) {this.code = code;this.message = message;}
}

2.1.4 统一异常处理

创建类ExceptionAdvice类

java">/*** 全局异常处理类,用于统一处理项目中的异常*/
@Slf4j
@ResponseBody
@ControllerAdvice
public class ExceptionAdvice {/*** 处理所有类型的异常** @param e 异常对象* @return 结果对象,包含错误信息*/@ExceptionHandler(Exception.class)public Result handler(Exception e){log.error("发生异常, e: {}", e);return Result.fail("发生错误"+e.getMessage());}/*** 处理自定义的BlogException异常** @param e 异常对象* @return 结果对象,包含错误信息*/@ExceptionHandler(BlogException.class)public Result blogExceptionHandler(Exception e){log.error("发生错误, e: {}", e.getMessage());return Result.fail(e.getMessage());}/*** 处理资源未找到的异常** @param e 异常对象* @return 结果对象,包含错误信息*/@ResponseStatus(HttpStatus.NOT_FOUND)@ExceptionHandlerpublic Result handler(NoResourceFoundException e){log.error("文件不存在, e: {}", e.getResourcePath());return Result.fail("文件不存在:"+e.getResourcePath());}/*** 处理参数验证相关的异常** @param e 异常对象* @return 结果对象,包含错误信息*/@ResponseStatus(HttpStatus.BAD_REQUEST)@ExceptionHandler({HandlerMethodValidationException.class, MethodArgumentNotValidException.class})public Result argHandler(Exception e){log.error("参数校验失败, e: {}", e.getMessage());return Result.fail("参数不合法");}}

三、业务代码

3.1 持久层与Mapper的编写

3.1.1、实体类(userInfo,BlogInfo)

BlogInfo:

java">/*** 博客信息类* 该类用于表示博客的相关信息,包括博客的标题、内容、作者等*/
@Data
public class BlogInfo {/*** 博客ID* 该字段用于唯一标识一篇博客,使用自动增长策略生成ID*//*** 用户信息类,用于表示系统中的用户基本信息*/
@Data
public class UserInfo {/*** 用户ID,作为数据库中的唯一标识符* 使用自动增长策略生成ID*/@TableId(value = "id", type = IdType.AUTO)private Integer id;/*** 用户名,用于用户登录和显示*/private String userName;/*** 用户密码,用于登录验证* 注意:实际应用中,密码应进行加密处理,以提高安全性*/private String password;/*** GitHub地址,用于展示用户的GitHub主页* 这有助于用户在社区中建立个人品牌和专业形象*/private String githubUrl;/*** 删除标志,用于逻辑删除用户信息* 注意:实际应用中,应定义枚举或常量来表示删除状态,以增强代码的可读性和可维护性*/private Integer deleteFlag;/*** 创建时间,记录用户信息的创建日期*/private LocalDate createTime;/*** 更新时间,记录用户信息的最后修改日期*/private LocalDate updateTime;
}@TableId(value = "id", type = IdType.AUTO)private Integer id;/*** 博客标题* 该字段用于存储博客的标题*/private String title;/*** 博客内容* 该字段用于存储博客的正文内容*/private String content;/*** 用户ID* 该字段用于标识博客的作者,通过用户的ID进行关联*/private Integer userId;/*** 删除标志* 该字段用于标记博客是否被删除,通常0表示未删除,1表示已删除*/private Integer deleteFlag;/*** 创建时间* 该字段用于记录博客的创建时间*/private LocalDateTime createTime;/*** 更新时间* 该字段用于记录博客的最后更新时间*/private Date updateTime;
}

UserInfo:

java">/*** 用户信息类,用于表示系统中的用户基本信息*/
@Data
public class UserInfo {/*** 用户ID,作为数据库中的唯一标识符* 使用自动增长策略生成ID*///value = "id":指定主键字段名为id,type = IdType.AUTO:设置主键生成策略为数据库自增。@TableId(value = "id", type = IdType.AUTO)private Integer id;/*** 用户名,用于用户登录和显示*/private String userName;/*** 用户密码,用于登录验证* 注意:实际应用中,密码应进行加密处理,以提高安全性*/private String password;/*** GitHub地址,用于展示用户的GitHub主页* 这有助于用户在社区中建立个人品牌和专业形象*/private String githubUrl;/*** 删除标志,用于逻辑删除用户信息* 注意:实际应用中,应定义枚举或常量来表示删除状态,以增强代码的可读性和可维护性*/private Integer deleteFlag;/*** 创建时间,记录用户信息的创建日期*/private LocalDate createTime;/*** 更新时间,记录用户信息的最后修改日期*/private LocalDate updateTime;
}

3.1.2 创建BlogInfoMapper和UserInfoMapper接口

UserInfoMapper:

java">/*** 用户信息 Mapper 接口* 继承自 BaseMapper,用于处理用户信息的数据库操作*/
@Mapper
public interface UserInfoMapper extends BaseMapper<UserInfo> {// 此接口没有定义额外的方法,因为它依赖于BaseMapper中定义的通用CRUD操作
}

BlogInfoMapper

java">/*** BlogInfoMapper接口是MyBatis Plus的映射接口,用于定义对BlogInfo表的操作* 它继承自BaseMapper,专注于BlogInfo实体类的CRUD操作** @author [Your Name]* @since [Your Version]*/
@Mapper
public interface BlogInfoMapper extends BaseMapper<BlogInfo> {// 此接口没有定义额外的方法,因为它依赖于BaseMapper中定义的通用CRUD操作
}

3.2 实现博客列表

约定前后端交互接口

[请求]

/blog/getList GET

[响应]

{

    "code": 200,

    "errMsg": "",

    "data": [

        {

            "id": 16,

            "title": "第二篇博客",

            "content": "222我是博客正文我是博客正文我是博客正文",

            "userId": 2,

            "createTime": "2025-02-14 16:00:25"

        },

        {

            "id": 15,

            "title": "第一篇文章",

            "content": "欢迎 欢迎 热烈欢迎!",

            "userId": 1,

            "createTime": "2025-02-14 16:00:25"

        }

    ]

}

客户端给服务器发送⼀个 /blog/getlist 这样的 HTTP 请求, 服务器给客户端返回了⼀个 JSON 格
式的数据.

1. 定义接口返回实体

创建BlogInfoResponse类

java">@Data
public class BlogInfoResponse {// 博客IDprivate Integer id;// 博客标题private String title;// 博客内容private String content;// 博客作者的用户IDprivate Integer userId;// 博客创建时间,以年-月-日 时:分:秒的格式进行序列化和反序列化// 通过@JsonFormat设置⻚⾯返回的日期格式@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")private LocalDateTime createTime;
}

实现Controller

java">@Slf4j
// 博客控制器,处理与博客相关的HTTP请求
@RequestMapping("/blog")
@RestController
public class BlogController {// 注入博客服务接口的实现,用于执行博客相关的业务逻辑@Resource(name = "blogServiceImpl")private BlogService blogService;/*** 获取博客列表** @return 博客信息响应列表*/@GetMapping("/getList")public List<BlogInfoResponse> getList(){// 记录获取博客列表的日志log.info("获取博客列表");// 调用服务层方法获取博客列表并返回return blogService.getList();}/*** 根据博客ID获取博客详情** @param blogId 博客ID,用于标识特定的博客* @return 博客信息响应对象*/@GetMapping("/getBlogDetail")public BlogInfoResponse getBlogDetail(@NotNull Integer blogId){// 记录获取博客详情的日志,并包含博客IDlog.info("获取博客详情, blogId:{}", blogId);// 调用服务层方法获取特定博客的详情并返回return blogService.getBlogDetail(blogId);}/*** 添加新博客** @param param 添加博客的参数对象,包含博客的相关信息* @return 添加成功返回true,否则返回false*/@PostMapping("/add")public Boolean addBlog(@Validated @RequestBody AddBlogParam param){// 记录添加博客的日志,并包含博客标题log.info("添加博客, 标题:{}", param.getTitle());// 调用服务层方法添加新博客并返回结果return blogService.addBlog(param);}/*** 更新博客信息** @param param 更新博客的参数对象,包含需要更新的博客信息* @return 更新成功返回true,否则返回false*/@PostMapping("/update")public Boolean updateBlog(@Validated @RequestBody UpBlogParam param){// 记录更新博客的日志,并包含博客IDlog.info("更新博客, 博客ID: {}", param.getId());// 调用服务层方法更新博客信息并返回结果return blogService.updateBlog(param);}/*** 删除特定的博客** @param blogId 博客ID,用于标识需要删除的博客* @return 删除成功返回true,否则返回false*/@PostMapping("/delete")public Boolean deleteBlog(@NotNull Integer blogId){// 记录删除博客的日志,并包含博客IDlog.info("删除博客, 博客ID: {}", blogId);// 调用服务层方法删除特定的博客并返回结果return blogService.deleteBlog(blogId);}
}

实现Service

基于SOA理念,Service采⽤接⼝对外提供服务,实现类⽤Impl的后缀与接⼝区别.

SOA(Service-Oriented Architecture,⾯向服务的架构)是⼀种⾼层级的架构设计理念,可通过在 ⽹络上使⽤基于通⽤通信语⾔的服务接⼝,让软件组件可重复使⽤.

java">public interface BlogService {List<BlogInfoResponse> getList();BlogInfoResponse getBlogDetail(Integer blogId);Boolean addBlog(AddBlogParam param);Boolean updateBlog(UpBlogParam param);Boolean deleteBlog(Integer blogId);
}
java">
public interface UserService {UserLoginResponse login(UserLoginParam userParam);UserInfoResponse getUserInfoById(Integer userId);UserInfoResponse getAuthorInfo(Integer blogId);
}

 BeanConver类提供了数据对象与传输对象之间的转换功能

java">/*** BeanConver类提供了数据对象与传输对象之间的转换功能* 这类中的方法主要用于将业务对象转换为API响应对象,或将请求参数转换为业务对象*/
public class BeanConver {/*** 将BlogInfo对象转换为BlogInfoResponse对象* 此方法主要用于准备API响应,将业务对象中的数据复制到响应对象中** @param blogInfo 博客信息的业务对象,包含博客的相关数据* @return 返回一个填充了来自blogInfo数据的BlogInfoResponse对象*/public static BlogInfoResponse trans(BlogInfo blogInfo){BlogInfoResponse blogInfoResponse = new BlogInfoResponse();if (blogInfo!=null){BeanUtils.copyProperties(blogInfo, blogInfoResponse);}return blogInfoResponse;}
}
java">@Slf4j
@Service
public class BlogServiceImpl implements BlogService {@Autowiredprivate BlogInfoMapper blogMapper;/*** 获取博客列表* @return 博客信息响应列表*/@Overridepublic List<BlogInfoResponse> getList() {//查询数据库中未删除的博客信息,并按照ID降序排列List<BlogInfo> blogInfos = blogMapper.selectList(new LambdaQueryWrapper<BlogInfo>().eq(BlogInfo::getDeleteFlag, 0).orderByDesc(BlogInfo::getId));//将查询到的博客信息转换为响应对象列表List<BlogInfoResponse> blogInfoResponses = blogInfos.stream().map(blogInfo -> BeanConver.trans(blogInfo)).collect(Collectors.toList());return blogInfoResponses;}/*** 获取博客详情* @param blogId 博客ID* @return 博客信息响应对象*/@Overridepublic BlogInfoResponse getBlogDetail(Integer blogId) {//从数据库中查询指定ID的博客信息BlogInfo blogInfo = selectBlogById(blogId);//将博客信息转换为响应对象并返回return BeanConver.trans(blogInfo);}/*** 添加新博客* @param param 添加博客的参数对象* @return 添加结果,true表示成功,false表示失败*/@Overridepublic Boolean addBlog(AddBlogParam param) {//将添加博客参数对象转换为博客信息对象BlogInfo blogInfo = BeanConver.trans(param);//尝试插入博客信息到数据库try {int result = blogMapper.insert(blogInfo);//如果插入成功,返回trueif (result==1){return true;}}catch (Exception e){//如果插入失败,记录错误日志log.error("博客插入失败. e:{}", e);}return false;}/*** 更新博客信息* @param param 更新博客的参数对象* @return 更新结果,true表示成功,false表示失败*/@Overridepublic Boolean updateBlog(UpBlogParam param) {//将更新博客参数对象转换为博客信息对象BlogInfo blogInfo = BeanConver.trans(param);//调用更新函数尝试更新博客信息return update(blogInfo);}/*** 删除博客* @param blogId 博客ID* @return 删除结果,true表示成功,false表示失败*/@Overridepublic Boolean deleteBlog(Integer blogId) {//创建一个博客信息对象,并设置ID和删除标志BlogInfo blogInfo = new BlogInfo();blogInfo.setId(blogId);blogInfo.setDeleteFlag(1);//调用更新函数尝试逻辑删除博客return update(blogInfo);}/*** 从数据库查询博客详情* @param blogId 博客ID* @return 博客信息对象*/public BlogInfo selectBlogById(Integer blogId){//查询数据库中指定ID且未删除的博客信息return blogMapper.selectOne(new LambdaQueryWrapper<BlogInfo>().eq(BlogInfo::getId, blogId).eq(BlogInfo::getDeleteFlag, 0));}/*** 更新博客数据* @param blogInfo 博客信息对象* @return 更新结果,true表示成功,false表示失败*/public Boolean update(BlogInfo blogInfo){//尝试更新博客信息到数据库try {Integer result = blogMapper.updateById(blogInfo);//如果更新成功,返回trueif (result==1){return true;}}catch (Exception e){//如果更新失败,记录错误日志log.error("更新博客失败, e: {}", e);}return false;}
}

注意:通过Bean的方式注入的话 类名首字母小写 

部署程序, 验证服务器是否能正确返回数据 (使⽤ URL http://127.0.0.1:8080/blog/getList 即可).

3.3 实现博客列表详情

约定前后端交互接口

在 BlogController 中添加getBlogDeatail ⽅法
java">/*** 根据博客ID获取博客详情** @param blogId 博客ID,用于标识特定的博客* @return 博客信息响应对象*/@GetMapping("/getBlogDetail")public BlogInfoResponse getBlogDetail(@NotNull Integer blogId){// 记录获取博客详情的日志,并包含博客IDlog.info("获取博客详情, blogId:{}", blogId);// 调用服务层方法获取特定博客的详情并返回return blogService.getBlogDetail(blogId);}
在BlogService 中添加getBlogDetail方法
java">BlogInfoResponse getBlogDetail(Integer blogId);
在BlogServiceImpl进行实现
java"> /*** 从数据库查询博客详情** @param blogId 博客ID* @return 博客信息对象*/public BlogInfo selectBlogById(Integer blogId) {//查询数据库中指定ID且未删除的博客信息return blogMapper.selectOne(new LambdaQueryWrapper<BlogInfo>().eq(BlogInfo::getId, blogId).eq(BlogInfo::getDeleteFlag, 0));}@Overridepublic BlogInfoResponse getBlogDetail(Integer blogId) {//从数据库中查询指定ID的博客信息BlogInfo blogInfo = selectBlogById(blogId);//将博客信息转换为响应对象并返回return BeanConver.trans(blogInfo);}
部署程序, 验证服务器是否能正确返回数据 (使⽤ URL http://127.0.0.1:8080/blog/getBlogDetail?blogId=1  即可)
参数校验: jakarta.validation
这个接口中,blogld不能为空,可以借助jakarta.validation帮我们完成参数校验,免去繁琐的串
行校验.javax.vaLidation是JavaBean ValidationAPI的包名,这个APl允许开发者通过注解
(如@NotNull,@NotBlank,@Null等)来声明对象的验证规则,然后在运行时自动验证这些
对象。
注解
数据类型
说明
@NotBlank
CharSequence⼦类型
验证注解的元素值不为空(不为null、去除 ⾸位空格后⻓度为0)
@NotEmpty
CharSequence⼦类型、 Collection、Map、数组
验证注解的元素值不为null且不为空(字符串长度不为0、集合大小不为0)
@NotNull
任意类型
验证注解的元素值不是null

在pom.xml添加以下依赖

java"><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId></dependency>

BlogController的方法声明

java">@GetMapping("/getBlogDetail")public BlogInfoResponse getBlogDetail(@NotNull Integer blogId){// 记录获取博客详情的日志,并包含博客IDlog.info("获取博客详情, blogId:{}", blogId);// 调用服务层方法获取特定博客的详情并返回return blogService.getBlogDetail(blogId);}
针对校验出现的异常, 进行处理
java"> /*** 处理参数验证相关的异常** @param e 异常对象* @return 结果对象,包含错误信息*/@ResponseStatus(HttpStatus.BAD_REQUEST)@ExceptionHandler({HandlerMethodValidationException.class, MethodArgumentNotValidException.class})public Result argHandler(Exception e){log.error("参数校验失败, e: {}", e.getMessage());return Result.fail("参数不合法");}

3.4 实现用户登录

传统实现思路:

登陆页面把用户名密码提交给服务器,服务器端验证用户名密码是否正确,并返回校验结果给后端。如果密码正确,则在服务器端创建Session.通过Cookie把sessionld返回给浏览器。但是集群环境下无法直接使用Session. 

为解决上述问题 引入 令牌技术。
令牌其实就是⼀个⽤⼾⾝份的标识, 其实本质就是⼀个字符串. 例如 我们出行在外, 会带着自己的⾝份证, 需要验证⾝份时, 就掏出⾝份证 ⾝份证不能伪造, 可以辨别真假。

3.4.1 JWT

全称:JSONWebToken
官网:https://jwt.io/
JSONWebToken(JWT)是一个开放的行业标准(RFC7519),用于客户端和服务器之间传递安全可靠的信息.其本质是一个token,是一种紧凑的URL安全方法.
服务器具备生成令牌和验证令牌的能力
我们使用令牌技术,继续思考上述场景:
1.用户登录用户登录请求,经过负载均衡,把请求转给了第一台服务器,第一台服务器进行账号密码验证,验证成功后,生成一个令牌,并返回给客户端,
2.客户端收到令牌之后,把令牌存储起来.可以存储在Cookie中,也可以存储在其他的存储空间(比如localStorage)
3.查询操作,用户登录成功之后,携带令牌继续执行查询操作,比如查询博客列表.此时请求转发到了第二台机器,第二台机器会先进行权限验证操作.服务器验证令牌是否有效,如果有效,就说明用户已经执行了登录操作,如果令牌是无效的,就说明用户之前未执行登录操作.
令牌的优缺点
优点:
解决了集群环境下的认证问题
减轻服务器的存储压力(无需在服务器端存储)
缺点:
需要自己实现(包括令牌的生成,令牌的传递,令牌的校验)
JWT令牌
令牌本质就是⼀个字符串, 他的实现⽅式有很多, 我们采用JWT令牌来实现。
JWT由三部分组成,每部分中间使用点()分隔,比如:aaaaa.bbbbb.cccc
 
1、Header(头部)头部包括令牌的类型(即JWT)及使用的哈希算法(如HMACSHA256或RSA)
 
2、、Payload(负载)负载部分是存放有效信息的地方,里面是一些自定义内容.比如:
{"userId":"123","userName":"zhangsan"},也可以存在jwt提供的现场字段,比如
exp(过期时间戳)等.此部分不建议存放敏感信息,因为此部分可以解码还原原始内容,

 
3、Signature(签名)此部分用于防止jwt内容被篡改,确保安全性.
防止被篡改,而不是防止被解析.
JWT之所以安全,就是因为最后的签名.jwt当中任何一个字符被篡改,整个令牌都会校验失败,
就好比我们的身份证,之所以能标识一个人的身份,是因为他不能被篡改,而不是因为内容加密.(任何人都可以看到身份证的信息,jwt也是)
JWT令牌生成和校验
1. 引入JWT令牌的依赖
java"></dependency>
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-api -->
<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-api</artifactId><version>0.11.5</version>
</dependency>
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-impl -->
<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-impl</artifactId><version>0.11.5</version><scope>runtime</scope>
</dependency>
<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-jackson</artifactId> <!-- or jjwt-gson if Gson is preferred --><version>0.11.5</version><scope>runtime</scope>
</dependency>
2.使用Jar包中提供的API来完成JWT令牌的生成和校验
生成令牌
java">public class JwtUtilTest {// JWT密钥,用于签名和验证JWT令牌吗,kprivate static final String secret = "FzG6p48J80L6vFxLrQqy2JVN27NiYbgjtGuYCTpeX7w=";
//    private static final String secret = "FzG6p48J80L6vFxLrQqy2JVN27NiYbgddddddddddddddddddd";// 将密钥转换为JWT可使用的格式private static final Key key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secret));// JWT令牌的过期时间,这里设置为24小时private static final long expiration = 10 * 1000;// 生成JWT令牌的方法@Testpublic void genToken(){// 创建一个Map来存放JWT中的负载信息Map<String, Object> map = new HashMap<>();map.put("id", 156666);map.put("name", "zhangsan");// 构建JWT令牌并打印出来String compact = Jwts.builder().setClaims(map).signWith(key).setIssuedAt(new Date()).setExpiration(new Date(System.currentTimeMillis()+expiration)).compact();System.out.println(compact);}// 生成JWT密钥的方法@Testpublic void genKey(){// 生成一个HS256算法的密钥SecretKey secretKey = Keys.secretKeyFor(SignatureAlgorithm.HS256);// 将密钥编码为Base64格式并打印出来String encode = Encoders.BASE64.encode(secretKey.getEncoded());System.out.println(encode);}
}

运行结果:

1.HEADER部分可以看到,使用的算法为HS256
2.PAYLOAD部分是我们自定义的内容,eXp表示过期时间
3.VERIFYSIGNATURE部分是经过签名算法计算出来的,所以不会解析 

解析令牌:

java">@Testpublic void parseToken() {String token = "eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiemhhbmdzYW4iLCJpZCI6MTU2NjY2LCJpYXQiOjE3NDAzOTc2NzUsImV4cCI6MTc0MDQzMzY3NX0.JJiNFRgIiU-o0LHSFxK8bAxJL_nwfj3HIwsYJ5n9ct8";// 创建JWT解析器JwtParser build = Jwts.parserBuilder().setSigningKey(key).build();try {// 解析JWT令牌并打印其负载信息Object body = build.parse(token).getBody();System.out.println(body);} catch (Exception e) {// 如果解析失败,打印异常信息e.printStackTrace();System.out.println("token 非法");}}

运行结果:

通过令牌来实现用户的登录
1.登陆页面把用户名密码提交给服务器,
2.服务器端验证用户名密码是否正确,如果正确,服务器生成令牌,下发给客户端,
3.客户端把令牌存储起来(比如Cookie,localstorage等),后续请求时,把token发给服务器
4.服务器对令牌进行校验,如果令牌正确,进行下一步操作(如访问列表详情)

约定前后端交互接口
[ 请求 ]
/user/login
[ 参数 ]
{
"userName" : "zhangsan" ,
"password" : "123456"
}
[响应]

{

    "code": 200,

    "errMsg": "",

    "data": {

        "userId": 1,

        "token": "eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiemhhbmdzYW4iLCJpZCI6MSwiZXhwIjoxNzQwNDg1NzMwLCJpYXQiOjE3NDAzOTkzMzB9.RFUyonj_2lZo1bXVoM5tWrpSNMmTvpcvtWl4FK1SEy8"

    }

}

实现服务器代码
创建JWT⼯具类
java">@Slf4j
public class JwtUtil {// JWT密钥,用于签名private static final String secret = "FzG6p48J80L6vFxLrQqy2JVN27NiYbgjtGuYCTpeX7w=";// 将密钥转换为Key对象private static final Key key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secret));// JWT的过期时间,单位为毫秒,这里设置为一天private static final long expiration = 24 * 60 * 60 * 1000;/*** 生成JWT token* @param map 包含要放置到JWT载荷中的声明,例如用户信息* @return 生成的JWT token字符串*/public static String genToken(Map<String, Object> map){return Jwts.builder().setClaims(map)  // 设置JWT的声明
//                .setSubject()  // 载荷信息也可以放置在这里.setExpiration(new Date(System.currentTimeMillis()+expiration))  // 设置过期时间.setIssuedAt(new Date())  // 设置签发时间.signWith(key)  // 使用密钥签名.compact();  // 生成紧凑的JWT字符串}/*** 校验JWT token并解析其中的声明* @param token 要校验的JWT token字符串* @return 如果token有效,则返回声明;否则返回null*/public static Claims parseToken(String token){// 检查token是否为空if (!StringUtils.hasLength(token)){return null;}// 创建JWT解析器JwtParser build = Jwts.parserBuilder().setSigningKey(key).build();Claims body = null;try {// 解析JWT并获取其中的声明body = build.parseClaimsJws(token).getBody();return body;}catch (ExpiredJwtException e){// 处理过期异常log.error("token过期, token:{}", token);}catch (SignatureException e){// 处理签名不匹配异常log.error("签名不匹配, token:{}", token);}catch (Exception e){// 处理其他异常log.error("token解析失败, token: {}", token);}return null;  // 如果解析失败,则返回null}
}
创建请求和响应实体类
请求实体类:
java">@Data
public class UserLoginParam {@NotBlank(message = "用户名不能为空")@Length(max = 20, message = "用户名不能超过20")private String userName;@NotBlank(message = "密码不能为空")private String password;
}

响应实体类:

java">@Data
public class UserLoginResponse {private Integer userId;private String token;
}

实现Controller

java">@Slf4j
@RequestMapping("/user")
@RestController
public class UserController {@Resource(name = "userServiceImpl")private UserService userService;@PostMapping("/login")public UserLoginResponse login(@Validated @RequestBody UserLoginParam userParam){log.info("用户登录, userName:{}", userParam.getUserName());return userService.login(userParam);}
}
实现Service
java">
public interface UserService {UserLoginResponse login(UserLoginParam userParam);
}
java">@Service
public class UserServiceImpl implements UserService {@Resource(name = "userInfoMapper")private UserInfoMapper userInfoMapper;@Resource(name = "blogInfoMapper")private BlogInfoMapper blogInfoMapper;@Overridepublic UserLoginResponse login(UserLoginParam userParam) {//判断用户是否存在//根据用户名, 去查询用户信息UserInfo userInfo = userInfoMapper.selectOne(new LambdaQueryWrapper<UserInfo>().eq(UserInfo::getUserName, userParam.getUserName()).eq(UserInfo::getDeleteFlag, 0));if (userInfo==null){throw new BlogException("用户不存在");}//用户存在, 校验密码是否正确if (!SecurityUtil.verify(userParam.getPassword(), userInfo.getPassword())){throw new BlogException("密码错误");}//密码正确UserLoginResponse response = new UserLoginResponse();response.setUserId(userInfo.getId());//载荷Map<String, Object> map = new HashMap<>();map.put("id", userInfo.getId());map.put("name", userInfo.getUserName());response.setToken(JwtUtil.genToken(map));return response;}
}

3.4.2 实现强制登录功能

当用户访问博客列表页和博客详情页时,如果用户当前尚未登陆,就自动跳转到登陆页面.我们可以 采用拦截器来完成,token通常由前端放在header中,我们从header中获取token,并校验
token是否合法.
添加拦截器
java">@Slf4j
@Component
/*** 登录拦截器,用于拦截请求以验证用户登录状态*/
public class LoginInteceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {//校验token是否正确//获取tokenString token = request.getHeader("user_header_token");log.info("从header中获取到token, token: {}", token);//校验token是否存在Claims claims = JwtUtil.parseToken(token);//token不合法if (claims==null){response.setStatus(401);return false;}//TODO 也可以再做的细一点//比如校验id和name是否匹配, 是否存在//claims.get("name")return true;}
}
javascript">/*** 配置类,用于定制Web应用的MVC特性*/
@Configuration
public class WebConfig implements WebMvcConfigurer {/*** 自动注入的登录拦截器,用于拦截请求并进行登录验证*/@Autowiredprivate LoginInteceptor loginInteceptor;/*** 添加拦截器配置** @param registry 拦截器注册对象,用于注册自定义拦截器*/@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(loginInteceptor).addPathPatterns("/user/**", "/blog/**") // 添加对/user和/blog路径下所有请求的拦截.excludePathPatterns("/user/login"); // 排除对/user/login路径的拦截}/*** 添加资源处理器** @param registry 资源处理器注册对象,用于注册静态资源或自定义资源处理器*/@Overridepublic void addResourceHandlers(ResourceHandlerRegistry registry) {WebMvcConfigurer.super.addResourceHandlers(registry);}
}

3.5 实现显示用户信息功能

我们期望这个信息可以随着用户登陆而发生改变
如果当前页面是 博客列表页,则 显示当前登陆用户的信息,
如果当前页面是 博客详情页,则 显示该博客的作者用户信息,
注意:当前我们只是实现了显示用户名,没有实现显示用户的头像以及文章数量等信息.
约定前后端交互接口
在博客列表页, 获取当前登陆的用户的用户信息.
[请求]
/user/getUserInfo?userId=1

 
[响应]
{
"id":1,
"username": "zhangsan"
"githubUrl":"
http://https://gitee.com/pinkboyXXX "
}

 在博客详情页,获取当前文章作者的用户信息

[请求]
/user/getAuthorInfo?blogId=1
[响应]
{
"id":1,
"username": "zhangsan"
"githuburl":" https://gitee.com/pinkboyXXX"

}

实现服务器代码 

定义用户响应实体类
java">/*** 用户信息响应类* 用于封装用户基本信息的响应数据*/
@Data
public class UserInfoResponse {/*** 用户ID* 唯一标识用户的信息*/private Integer id;/*** 用户名* 用户在系统中的名称,便于展示和查询*/private String userName;/*** GitHub地址* 用户在GitHub上的个人主页地址,用于分享和技术交流*/private String githubUrl;
}
在 UserController添加代码
java">@Slf4j
@RequestMapping("/user")
@RestController
public class UserController {@Resource(name = "userServiceImpl")private UserService userService;@GetMapping("/getUserInfo")public UserInfoResponse getUserInfo(@NotNull Integer userId){log.info("获取用户信息, userId: {}", userId);return userService.getUserInfoById(userId);}
}
在UserService中添加代码
java">public interface UserService {UserInfoResponse getUserInfoById(Integer userId);UserInfoResponse getAuthorInfo(Integer blogId);
}
在UserServiceImpl中添加代码 java">
java">@Service
public class UserServiceImpl implements UserService {@Resource(name = "userInfoMapper")private UserInfoMapper userInfoMapper;@Resource(name = "blogInfoMapper")private BlogInfoMapper blogInfoMapper;@Overridepublic UserLoginResponse login(UserLoginParam userParam) {//判断用户是否存在//根据用户名, 去查询用户信息UserInfo userInfo = userInfoMapper.selectOne(new LambdaQueryWrapper<UserInfo>().eq(UserInfo::getUserName, userParam.getUserName()).eq(UserInfo::getDeleteFlag, 0));if (userInfo==null){throw new BlogException("用户不存在");}//用户存在, 校验密码是否正确if (!SecurityUtil.verify(userParam.getPassword(), userInfo.getPassword())){throw new BlogException("密码错误");}//密码正确UserLoginResponse response = new UserLoginResponse();response.setUserId(userInfo.getId());//载荷Map<String, Object> map = new HashMap<>();map.put("id", userInfo.getId());map.put("name", userInfo.getUserName());response.setToken(JwtUtil.genToken(map));return response;}@Overridepublic UserInfoResponse getUserInfoById(Integer userId) {UserInfo userInfo = selectUserInfoById(userId);return BeanConver.trans(userInfo);}@Overridepublic UserInfoResponse getAuthorInfo(Integer blogId) {//1. 根据博客id, 拿到作者idBlogInfo blogInfo = blogInfoMapper.selectOne(new LambdaQueryWrapper<BlogInfo>().eq(BlogInfo::getId, blogId).eq(BlogInfo::getDeleteFlag, 0));//2. 根据作者id, 拿到作者详情if (blogInfo==null){throw new BlogException("博客不存在");}UserInfo userInfo = selectUserInfoById(blogInfo.getUserId());return BeanConver.trans(userInfo);}public UserInfo selectUserInfoById(Integer userId){return userInfoMapper.selectOne(new LambdaQueryWrapper<UserInfo>().eq(UserInfo::getId, userId).eq(UserInfo::getDeleteFlag, 0));}
}

3.6 实现发布博客功能

约定前后端交互接口
[请求]
/blog/ add
[参数]
{
"userId": 1 ,
"title":" 标题 ",
"content":" 正⽂ "
}
[响应]
{
"code": 200 ,
"msg": "",
"data": true   // true 成功  false 失败
}

 实现服务器代码

定义实体类
java">/*** 添加新博客** @param param 添加博客的参数对象,包含博客的相关信息* @return 添加成功返回true,否则返回false*/@PostMapping("/add")public Boolean addBlog(@Validated @RequestBody AddBlogParam param){// 记录添加博客的日志,并包含博客标题log.info("添加博客, 标题:{}", param.getTitle());// 调用服务层方法添加新博客并返回结果return blogService.addBlog(param);}
修改 BlogController, 新增 add 方法
java">    /*** 添加新博客** @param param 添加博客的参数对象,包含博客的相关信息* @return 添加成功返回true,否则返回false*/@PostMapping("/add")public Boolean addBlog(@Validated @RequestBody AddBlogParam param){// 记录添加博客的日志,并包含博客标题log.info("添加博客, 标题:{}", param.getTitle());// 调用服务层方法添加新博客并返回结果return blogService.addBlog(param);}
实现Service
BlogService
java">Boolean addBlog(AddBlogParam param);
BlogServiceImpl
java">/*** 添加新博客** @param param 添加博客的参数对象* @return 添加结果,true表示成功,false表示失败*/@Overridepublic Boolean addBlog(AddBlogParam param) {//将添加博客参数对象转换为博客信息对象BlogInfo blogInfo = BeanConver.trans(param);//尝试插入博客信息到数据库try {int result = blogMapper.insert(blogInfo);//如果插入成功,返回trueif (result == 1) {return true;}} catch (Exception e) {//如果插入失败,记录错误日志log.error("博客插入失败. e:{}", e);}return false;}

3.7 实现修改博客功能

进入用户详情页时,如果当前登陆用户正是文章作者,则在导航栏中显示[编辑][删除]按钮,用户点击时则进行相应处理.
需要实现两件事:
1、判定当前博客详情页中是否要显示[编辑][删除]按钮
2、实现编辑/删除逻辑.
删除采用逻辑删除

约定前后端交互接口
[请求]
/blog/ update
[参数]
Content-Type: application/json
{
"id": "4",
"title": " 测试修改⽂章 ",
"content": " 在这⾥写下⼀篇博客 "
}
[响应]
{
"code": 200 ,
"msg": "",
"data": true
}
实现服务器代码
定义修改接口实体类
java">/*** 更新博客参数类* 用于封装更新博客时所需的参数,包括博客的唯一标识符、标题和内容* 通过使用Lombok的@Data注解,自动生成getter和setter方法,简化了代码*/
@Data
public class UpBlogParam {/*** 博客的唯一标识符* 使用此字段来标识数据库中的特定博客记录* 通过@NotNull注解,确保在更新博客时必须提供此字段,防止更新操作因缺少主键而失败*/@NotNull(message = "博客id不能为空" )private Integer id;/*** 博客的标题* 通过@NotBlank注解,确保标题不为空,因为标题是博客的重要信息之一* 这个字段用于更新博客的标题信息*/@NotBlank(message = "标题不能为空")private String title;/*** 博客的内容* 通过@NotBlank注解,确保内容不为空,因为内容是博客的核心部分* 这个字段用于更新博客的具体内容信息*/@NotBlank(message = "内容不能为空")private String content;
}
BlogController 增加update方法,处理修改逻辑
java">/*** 更新博客信息** @param param 更新博客的参数对象,包含需要更新的博客信息* @return 更新成功返回true,否则返回false*/@PostMapping("/update")public Boolean updateBlog(@Validated @RequestBody UpBlogParam param){// 记录更新博客的日志,并包含博客IDlog.info("更新博客, 博客ID: {}", param.getId());// 调用服务层方法更新博客信息并返回结果return blogService.updateBlog(param);}
定义update接口
java"> Boolean addBlog(AddBlogParam param);
实现接口
java"> /*** 更新博客信息** @param param 更新博客的参数对象* @return 更新结果,true表示成功,false表示失败*/@Overridepublic Boolean updateBlog(UpBlogParam param) {//将更新博客参数对象转换为博客信息对象BlogInfo blogInfo = BeanConver.trans(param);//调用更新函数尝试更新博客信息return update(blogInfo);}

3.8 实现删除博客功能

[请求]
/blog/delete?blogId=1
 

[响应]

{
"code": 200,
"msg"1
"data": true

}

 实现服务器代码

修改 BlogController  增加delete方法的删除逻辑
java">/*** 删除特定的博客** @param blogId 博客ID,用于标识需要删除的博客* @return 删除成功返回true,否则返回false*/@PostMapping("/delete")public Boolean deleteBlog(@NotNull Integer blogId){// 记录删除博客的日志,并包含博客IDlog.info("删除博客, 博客ID: {}", blogId);// 调用服务层方法删除特定的博客并返回结果return blogService.deleteBlog(blogId);}
定义delete接口
java">Boolean deleteBlog(Integer blogId);
实现接口
java">/*** 删除博客** @param blogId 博客ID* @return 删除结果,true表示成功,false表示失败*/@Overridepublic Boolean deleteBlog(Integer blogId) {//创建一个博客信息对象,并设置ID和删除标志BlogInfo blogInfo = new BlogInfo();blogInfo.setId(blogId);blogInfo.setDeleteFlag(1);//调用更新函数尝试逻辑删除博客return update(blogInfo);}

3.9 实现密码加密功能

在MySQL数据库中,我们常常需要对密码,身份证号,手机号等敏感信息进行加密,以保证数据的安全性,如果使用明文存储,当黑客入侵了数据库时,就可以轻松获取到用户的相关信息,从而对用户或者企业造成信息泄漏或者财产损失.目前我们用户的密码还是明文设置的,为了保护用户的密码信息,我们需要对密码进行加密.

密码算法分类
密码算法主要分为三类:对称密码算法,非对称密码算法,摘要算法

1.对称密码算法是指加密秘钥和解密秘钥相同的密码算法.常见的对称密码算法有:AES,DES,3DES,RC4,RC5,RC6等
2.非对称密码算法是指加密秘钥和解密秘钥不同的密码算法.该算法使用一个秘钥进行加密,用另外一个秘钥进行解密. 加密秘钥可以公开,又称为公钥 解密秘钥必须保密,又称为私钥
常见的非对称密码算法有:RSA,DSA,ECDSA,ECC等
3.摘要算法是指把任意长度的输入消息数据转化为固定长度的输出数据的一种密码算法.摘要算法是
不可逆的
,也就是无法解密.通常用来检验数据的完整性的重要技术,即对数据进行哈希计算然后比较摘要值,判断是否一致.常见的摘要算法有:MD5,SHA系列(SHA1,SHA2等),CRC(CRC8,CRC16,CRC32)

加密思路:
博客系统中,我们采用MD5算法来进行加密,
问题:虽然经过MD5加密后的密文无法解密,但由于相同的密码经过MD5哈希之后的密文是相同的,当存储用户密码的数据库泄露后,攻击者会很容易便能找到相同密码的用户,从而降低了破解密码的难度.因此,在对用户密码进行加密时,需要考虑对密码进行包装,即使是相同的密码,也保存为不同的密文.即使用户输入的是弱密码,也考虑进行增强,从而增加密码被攻破的难度.
 

解决方案:采用为一个密码拼接一个随机字符来进行加密,这个随机字符我们称之为"盐".假如有一个加盐后的加密串,黑客通过一定手段这个加密串,他拿到的明文并不是我们加密前的字符串,而是加密前的字符串和盐组合的字符串,这样相对来说又增加了字符串的安全性。

解密流程: MD5是不可逆的, 通常采用"判断哈希值是否⼀致"来判断密码是否正确.
如果用户输⼊的密码, 和盐值⼀起拼接后的字符串经过加密算法, 得到的密⽂相同, 我们就认为密码正确 (密文相同, 盐值相同, 推测明文相同)

加密工具测试类

java"> /*** 测试生成安全密码* 此方法展示了如何使用SecurityUtil类来生成一个安全的、加密后的密码*/String sqlPassword = sqlPassword = SecurityUtil.encrypt("123456");String inputPassword = "123456";@Testvoid genPassword() {//生成加密后的密码System.out.println(sqlPassword);}@Testvoid verifyPassword() {//获取saltString salt = sqlPassword.substring(0, 32);//md5(盐值+inputPassword), 得到32位16进制的密文String securityPassword = DigestUtils.md5DigestAsHex((salt + inputPassword).getBytes(StandardCharsets.UTF_8));System.out.println(sqlPassword.equals(salt + securityPassword));}

加密:

解密:

 

加密工具类

java">@Slf4j
public class SecurityUtil {/*** 加密密码* 使用UUID生成唯一的盐值,并与明文密码一起通过MD5算法生成密文* 盐值和密文一起返回,以便存储到数据库中** @param password 明文密码* @return 加密后的密码,包括盐值和MD5密文* @throws BlogException 如果密码为空,则抛出异常*/public static String encrypt(String password){if (!StringUtils.hasLength(password)){throw new BlogException("密码不能为空");}//生成盐值String salt = UUID.randomUUID().toString().replace("-", "");//md5(盐值+password), 得到32位16进制的密文String securityPassword = DigestUtils.md5DigestAsHex((salt + password).getBytes(StandardCharsets.UTF_8));//数据库中应存储 盐值和密文   得到64位16进制的数据return salt+securityPassword;}/*** 验证密码* 通过盐值对输入的密码进行加密,然后与数据库中的密码进行比较** @param inputPassword 用户输入的明文密码* @param sqlPassword 数据库中存储的加密密码* @return 如果密码匹配则返回true,否则返回false*/public static boolean verify(String inputPassword, String sqlPassword){//参数校验if (!StringUtils.hasLength(inputPassword) || !StringUtils.hasLength(sqlPassword)){return false;}if (sqlPassword.length()!=64){return false;}//获取saltString salt = sqlPassword.substring(0, 32);//md5(盐值+inputPassword), 得到32位16进制的密文String securityPassword = DigestUtils.md5DigestAsHex((salt + inputPassword).getBytes(StandardCharsets.UTF_8));return sqlPassword.equals(salt+securityPassword);}
}

在UserController中对加密工具类进行使用

java"> @Overridepublic UserLoginResponse login(UserLoginParam userParam) {//判断用户是否存在//根据用户名, 去查询用户信息UserInfo userInfo = userInfoMapper.selectOne(new LambdaQueryWrapper<UserInfo>().eq(UserInfo::getUserName, userParam.getUserName()).eq(UserInfo::getDeleteFlag, 0));if (userInfo==null){throw new BlogException("用户不存在");}//用户存在, 校验密码是否正确if (!SecurityUtil.verify(userParam.getPassword(), userInfo.getPassword())){throw new BlogException("密码错误");}//密码正确UserLoginResponse response = new UserLoginResponse();response.setUserId(userInfo.getId());//载荷Map<String, Object> map = new HashMap<>();map.put("id", userInfo.getId());map.put("name", userInfo.getUserName());response.setToken(JwtUtil.genToken(map));return response;}

修改数据库中用户的密码

明文--> 密文


http://www.ppmy.cn/server/170808.html

相关文章

vscode多文件编译构建(CMake)和调试C++

目录 1. CMake 基础构建工具及作用相关配置文件 2. 配置 tasks.json关键字段详细解释 3. 配置 launch.json关键字段详细解释 4. 配置 CMakeLists.txt关键部分详细解释 5. 构建和调试项目1. 仅构建项目1.1 任务执行顺序1.2 cmake 任务执行详情1.3 build 任务执行详情1.4 构建后的…

Java实现斗地主-做牌以及对牌排序

卡牌类 public class Card {private String size;//大小private String color;//花色private int value;//权值public Card() {}public Card(String size, String color, int value) {this.size size;this.color color;this.value value;}public String toString(){return …

请谈谈 React 中的状态管理,如何使用 Context API 和 Redux 进行状态管理?

一、Context API 深度应用 1. 核心实现原理 通过createContext创建上下文对象&#xff0c;使用Provider组件包裹需要共享状态的组件树&#xff0c;子组件通过useContext Hook或Consumer组件消费数据。 代码示例&#xff08;主题切换场景&#xff09;&#xff1a; // 创建上…

【Mysql】我在广州学Mysql 系列——Mysql 性能优化

ℹ️大家好&#xff0c;我是练小杰&#xff0c;今天又是美好的星期一了&#xff0c;新的工作又要开始了&#xff0c;努力&#xff01;&#xff01;奋斗&#xff01;&#xff01;&#x1f606; 本文是针对Mysql 性能优化知识进行学习与讨论&#xff0c;后续将添加更多相关知识噢…

华为昇腾910b服务器部署DeepSeek翻车现场

最近到祸一台HUAWEI Kunpeng 920 5250&#xff0c;先看看配置。之前是部署的讯飞大模型&#xff0c;发现资源利用率太低了。把5台减少到3台&#xff0c;就出了他 硬件配置信息 基本硬件信息 按照惯例先来看看配置。一共3块盘&#xff0c;500G的系统盘&#xff0c; 2块3T固态…

Http模块及练习

### 作业 1. 静态文件服务器 js const http await import(http) const fs await import(fs) const proc ((req,res)>{ let file ./public${req.url} let FilePath file.replace(favicon.ico,"") // 检查文件是否存在 if (!fs.existsSync(FilePa…

二叉树练习题

目录 练习题1&#xff1a;单值二叉树 1.判断二叉树是单值二叉树的思路 2.代码实现 练习题2&#xff1a;检查两颗树是否相同 1.判断两棵树是相同的树的思路 2.代码实现 练习题3&#xff1a;翻转二叉树 1.翻转二叉树的思路 2.代码实现 练习题4&#xff1a;对称二叉树 …

Lua 面向对象

Lua 面向对象 Lua 是一种轻量级的编程语言&#xff0c;广泛用于游戏开发、嵌入式系统等领域。Lua 提供了多种编程范式&#xff0c;其中面向对象编程&#xff08;OOP&#xff09;是其中一种重要的编程范式。本文将详细介绍 Lua 的面向对象编程&#xff0c;包括类的定义、继承、…