springboot博客系统详解与实现(后端实现)
目录
前言:
项目介绍
一、项目的准备工作
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_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 = INNODB
DEFAULT
CHARACTER
SET = 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 = InnoDB
DEFAULT 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文件的全部配置:(后续添加相应依赖时会介绍)
<?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文件中配置如下信息
# 配置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=true
username: 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枚举类
/**
* 定义结果状态枚举类
* 用于表示API响应的状态码
*/
@AllArgsConstructor
public enum ResultStatusEnum {
//200成功 -1 失败
SUCCESS(200),
FAIL(-1);
/**
* 状态码
*/
@Getter
int code;
}
创建Result类
作用:通用用于返回结果类,用于封装接口返回数据
/**
* 通用返回结果类,用于封装接口返回数据
* 该类使用了@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类
/**
* 全局响应建议类,用于统一处理控制器的响应体
* 通过实现ResponseBodyAdvice接口,可以拦截并修改响应体
*/
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
/**
* ObjectMapper用于序列化和反序列化JSON对象
* 在处理响应体时,会用到它来转换对象为JSON字符串
*/
@Autowired
private ObjectMapper objectMapper;
/**
* 判断是否支持给定的控制器方法参数和转换器类型
* 本方法始终返回true,表示支持所有类型的响应体处理
*
* @param returnType 控制器方法的返回类型
* @param converterType 转换器的类型
* @return 始终返回true,表示支持所有类型
*/
@Override
public 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 捕获异常
@Override
public 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类
/**
* 博客异常类,继承自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类
/**
* 全局异常处理类,用于统一处理项目中的异常
*/
@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)
@ExceptionHandler
public 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:
/**
* 博客信息类
* 该类用于表示博客的相关信息,包括博客的标题、内容、作者等
*/
@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:
/**
* 用户信息类,用于表示系统中的用户基本信息
*/
@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:
/**
* 用户信息 Mapper 接口
* 继承自 BaseMapper,用于处理用户信息的数据库操作
*/
@Mapper
public interface UserInfoMapper extends BaseMapper<UserInfo> {
// 此接口没有定义额外的方法,因为它依赖于BaseMapper中定义的通用CRUD操作
}
BlogInfoMapper
/**
* 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"
}
]
}
1. 定义接口返回实体
创建BlogInfoResponse类
@Data
public class BlogInfoResponse {
// 博客ID
private Integer id;
// 博客标题
private String title;
// 博客内容
private String content;
// 博客作者的用户ID
private Integer userId;
// 博客创建时间,以年-月-日 时:分:秒的格式进行序列化和反序列化
// 通过@JsonFormat设置⻚⾯返回的日期格式
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
}
实现Controller
@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){
// 记录获取博客详情的日志,并包含博客ID
log.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){
// 记录更新博客的日志,并包含博客ID
log.info("更新博客, 博客ID: {}", param.getId());
// 调用服务层方法更新博客信息并返回结果
return blogService.updateBlog(param);
}
/**
* 删除特定的博客
*
* @param blogId 博客ID,用于标识需要删除的博客
* @return 删除成功返回true,否则返回false
*/
@PostMapping("/delete")
public Boolean deleteBlog(@NotNull Integer blogId){
// 记录删除博客的日志,并包含博客ID
log.info("删除博客, 博客ID: {}", blogId);
// 调用服务层方法删除特定的博客并返回结果
return blogService.deleteBlog(blogId);
}
}
实现Service
基于SOA理念,Service采⽤接⼝对外提供服务,实现类⽤Impl的后缀与接⼝区别.
SOA(Service-Oriented Architecture,⾯向服务的架构)是⼀种⾼层级的架构设计理念,可通过在 ⽹络上使⽤基于通⽤通信语⾔的服务接⼝,让软件组件可重复使⽤.
public interface BlogService {
List<BlogInfoResponse> getList();
BlogInfoResponse getBlogDetail(Integer blogId);
Boolean addBlog(AddBlogParam param);
Boolean updateBlog(UpBlogParam param);
Boolean deleteBlog(Integer blogId);
}
public interface UserService {
UserLoginResponse login(UserLoginParam userParam);
UserInfoResponse getUserInfoById(Integer userId);
UserInfoResponse getAuthorInfo(Integer blogId);
}
BeanConver类提供了数据对象与传输对象之间的转换功能
/**
* 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;
}
}
@Slf4j
@Service
public class BlogServiceImpl implements BlogService {
@Autowired
private BlogInfoMapper blogMapper;
/**
* 获取博客列表
* @return 博客信息响应列表
*/
@Override
public 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 博客信息响应对象
*/
@Override
public BlogInfoResponse getBlogDetail(Integer blogId) {
//从数据库中查询指定ID的博客信息
BlogInfo blogInfo = selectBlogById(blogId);
//将博客信息转换为响应对象并返回
return BeanConver.trans(blogInfo);
}
/**
* 添加新博客
* @param param 添加博客的参数对象
* @return 添加结果,true表示成功,false表示失败
*/
@Override
public Boolean addBlog(AddBlogParam param) {
//将添加博客参数对象转换为博客信息对象
BlogInfo blogInfo = BeanConver.trans(param);
//尝试插入博客信息到数据库
try {
int result = blogMapper.insert(blogInfo);
//如果插入成功,返回true
if (result==1){
return true;
}
}catch (Exception e){
//如果插入失败,记录错误日志
log.error("博客插入失败. e:{}", e);
}
return false;
}
/**
* 更新博客信息
* @param param 更新博客的参数对象
* @return 更新结果,true表示成功,false表示失败
*/
@Override
public Boolean updateBlog(UpBlogParam param) {
//将更新博客参数对象转换为博客信息对象
BlogInfo blogInfo = BeanConver.trans(param);
//调用更新函数尝试更新博客信息
return update(blogInfo);
}
/**
* 删除博客
* @param blogId 博客ID
* @return 删除结果,true表示成功,false表示失败
*/
@Override
public 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);
//如果更新成功,返回true
if (result==1){
return true;
}
}catch (Exception e){
//如果更新失败,记录错误日志
log.error("更新博客失败, e: {}", e);
}
return false;
}
}
注意:通过Bean的方式注入的话 类名首字母小写
3.3 实现博客列表详情
约定前后端交互接口
/**
* 根据博客ID获取博客详情
*
* @param blogId 博客ID,用于标识特定的博客
* @return 博客信息响应对象
*/
@GetMapping("/getBlogDetail")
public BlogInfoResponse getBlogDetail(@NotNull Integer blogId){
// 记录获取博客详情的日志,并包含博客ID
log.info("获取博客详情, blogId:{}", blogId);
// 调用服务层方法获取特定博客的详情并返回
return blogService.getBlogDetail(blogId);
}
BlogInfoResponse getBlogDetail(Integer blogId);
/**
* 从数据库查询博客详情
*
* @param blogId 博客ID
* @return 博客信息对象
*/
public BlogInfo selectBlogById(Integer blogId) {
//查询数据库中指定ID且未删除的博客信息
return blogMapper.selectOne(new LambdaQueryWrapper<BlogInfo>()
.eq(BlogInfo::getId, blogId).eq(BlogInfo::getDeleteFlag, 0));
}
@Override
public BlogInfoResponse getBlogDetail(Integer blogId) {
//从数据库中查询指定ID的博客信息
BlogInfo blogInfo = selectBlogById(blogId);
//将博客信息转换为响应对象并返回
return BeanConver.trans(blogInfo);
}
行校验.javax.vaLidation是JavaBean ValidationAPI的包名,这个APl允许开发者通过注解
(如@NotNull,@NotBlank,@Null等)来声明对象的验证规则,然后在运行时自动验证这些
对象。
注解
|
数据类型
|
说明
|
@NotBlank
|
CharSequence⼦类型
|
验证注解的元素值不为空(不为null、去除 ⾸位空格后⻓度为0)
|
@NotEmpty
|
CharSequence⼦类型、 Collection、Map、数组
| 验证注解的元素值不为null且不为空(字符串长度不为0、集合大小不为0) |
@NotNull
|
任意类型
|
验证注解的元素值不是null
|
在pom.xml添加以下依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
BlogController的方法声明
@GetMapping("/getBlogDetail")
public BlogInfoResponse getBlogDetail(@NotNull Integer blogId){
// 记录获取博客详情的日志,并包含博客ID
log.info("获取博客详情, blogId:{}", blogId);
// 调用服务层方法获取特定博客的详情并返回
return blogService.getBlogDetail(blogId);
}
/**
* 处理参数验证相关的异常
*
* @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
官网:https://jwt.io/
JSONWebToken(JWT)是一个开放的行业标准(RFC7519),用于客户端和服务器之间传递安全可靠的信息.其本质是一个token,是一种紧凑的URL安全方法.

1.用户登录用户登录请求,经过负载均衡,把请求转给了第一台服务器,第一台服务器进行账号密码验证,验证成功后,生成一个令牌,并返回给客户端,
2.客户端收到令牌之后,把令牌存储起来.可以存储在Cookie中,也可以存储在其他的存储空间(比如localStorage)
3.查询操作,用户登录成功之后,携带令牌继续执行查询操作,比如查询博客列表.此时请求转发到了第二台机器,第二台机器会先进行权限验证操作.服务器验证令牌是否有效,如果有效,就说明用户已经执行了登录操作,如果令牌是无效的,就说明用户之前未执行登录操作.
解决了集群环境下的认证问题
减轻服务器的存储压力(无需在服务器端存储)
需要自己实现(包括令牌的生成,令牌的传递,令牌的校验)
{"userId":"123","userName":"zhangsan"},也可以存在jwt提供的现场字段,比如
exp(过期时间戳)等.此部分不建议存放敏感信息,因为此部分可以解码还原原始内容,
防止被篡改,而不是防止被解析.
JWT之所以安全,就是因为最后的签名.jwt当中任何一个字符被篡改,整个令牌都会校验失败,
就好比我们的身份证,之所以能标识一个人的身份,是因为他不能被篡改,而不是因为内容加密.(任何人都可以看到身份证的信息,jwt也是)

</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>
public class JwtUtilTest {
// JWT密钥,用于签名和验证JWT令牌吗,k
private 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令牌的方法
@Test
public 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密钥的方法
@Test
public 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部分是经过签名算法计算出来的,所以不会解析
解析令牌:
@Test
public 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"
}
}
@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
}
}
@Data
public class UserLoginParam {
@NotBlank(message = "用户名不能为空")
@Length(max = 20, message = "用户名不能超过20")
private String userName;
@NotBlank(message = "密码不能为空")
private String password;
}
响应实体类:
@Data
public class UserLoginResponse {
private Integer userId;
private String token;
}
实现Controller
@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);
}
}
public interface UserService {
UserLoginResponse login(UserLoginParam userParam);
}
@Service
public class UserServiceImpl implements UserService {
@Resource(name = "userInfoMapper")
private UserInfoMapper userInfoMapper;
@Resource(name = "blogInfoMapper")
private BlogInfoMapper blogInfoMapper;
@Override
public 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是否合法.

@Slf4j
@Component
/**
* 登录拦截器,用于拦截请求以验证用户登录状态
*/
public class LoginInteceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//校验token是否正确
//获取token
String 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;
}
}
/**
* 配置类,用于定制Web应用的MVC特性
*/
@Configuration
public class WebConfig implements WebMvcConfigurer {
/**
* 自动注入的登录拦截器,用于拦截请求并进行登录验证
*/
@Autowired
private LoginInteceptor loginInteceptor;
/**
* 添加拦截器配置
*
* @param registry 拦截器注册对象,用于注册自定义拦截器
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInteceptor)
.addPathPatterns("/user/**", "/blog/**") // 添加对/user和/blog路径下所有请求的拦截
.excludePathPatterns("/user/login"); // 排除对/user/login路径的拦截
}
/**
* 添加资源处理器
*
* @param registry 资源处理器注册对象,用于注册静态资源或自定义资源处理器
*/
@Override
public 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"}
实现服务器代码
/**
* 用户信息响应类
* 用于封装用户基本信息的响应数据
*/
@Data
public class UserInfoResponse {
/**
* 用户ID
* 唯一标识用户的信息
*/
private Integer id;
/**
* 用户名
* 用户在系统中的名称,便于展示和查询
*/
private String userName;
/**
* GitHub地址
* 用户在GitHub上的个人主页地址,用于分享和技术交流
*/
private String githubUrl;
}
@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);
}
}
public interface UserService {
UserInfoResponse getUserInfoById(Integer userId);
UserInfoResponse getAuthorInfo(Integer blogId);
}
@Service
public class UserServiceImpl implements UserService {
@Resource(name = "userInfoMapper")
private UserInfoMapper userInfoMapper;
@Resource(name = "blogInfoMapper")
private BlogInfoMapper blogInfoMapper;
@Override
public 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;
}
@Override
public UserInfoResponse getUserInfoById(Integer userId) {
UserInfo userInfo = selectUserInfoById(userId);
return BeanConver.trans(userInfo);
}
@Override
public UserInfoResponse getAuthorInfo(Integer blogId) {
//1. 根据博客id, 拿到作者id
BlogInfo 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 失败}
实现服务器代码
/**
* 添加新博客
*
* @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("/add")
public Boolean addBlog(@Validated @RequestBody AddBlogParam param){
// 记录添加博客的日志,并包含博客标题
log.info("添加博客, 标题:{}", param.getTitle());
// 调用服务层方法添加新博客并返回结果
return blogService.addBlog(param);
}
Boolean addBlog(AddBlogParam param);
/**
* 添加新博客
*
* @param param 添加博客的参数对象
* @return 添加结果,true表示成功,false表示失败
*/
@Override
public Boolean addBlog(AddBlogParam param) {
//将添加博客参数对象转换为博客信息对象
BlogInfo blogInfo = BeanConver.trans(param);
//尝试插入博客信息到数据库
try {
int result = blogMapper.insert(blogInfo);
//如果插入成功,返回true
if (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}
/**
* 更新博客参数类
* 用于封装更新博客时所需的参数,包括博客的唯一标识符、标题和内容
* 通过使用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;
}
/**
* 更新博客信息
*
* @param param 更新博客的参数对象,包含需要更新的博客信息
* @return 更新成功返回true,否则返回false
*/
@PostMapping("/update")
public Boolean updateBlog(@Validated @RequestBody UpBlogParam param){
// 记录更新博客的日志,并包含博客ID
log.info("更新博客, 博客ID: {}", param.getId());
// 调用服务层方法更新博客信息并返回结果
return blogService.updateBlog(param);
}
Boolean addBlog(AddBlogParam param);
/**
* 更新博客信息
*
* @param param 更新博客的参数对象
* @return 更新结果,true表示成功,false表示失败
*/
@Override
public Boolean updateBlog(UpBlogParam param) {
//将更新博客参数对象转换为博客信息对象
BlogInfo blogInfo = BeanConver.trans(param);
//调用更新函数尝试更新博客信息
return update(blogInfo);
}
3.8 实现删除博客功能
[请求]
/blog/delete?blogId=1
[响应]
{
"code": 200,
"msg"1
"data": true}
实现服务器代码
/**
* 删除特定的博客
*
* @param blogId 博客ID,用于标识需要删除的博客
* @return 删除成功返回true,否则返回false
*/
@PostMapping("/delete")
public Boolean deleteBlog(@NotNull Integer blogId){
// 记录删除博客的日志,并包含博客ID
log.info("删除博客, 博客ID: {}", blogId);
// 调用服务层方法删除特定的博客并返回结果
return blogService.deleteBlog(blogId);
}
Boolean deleteBlog(Integer blogId);
/**
* 删除博客
*
* @param blogId 博客ID
* @return 删除结果,true表示成功,false表示失败
*/
@Override
public 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哈希之后的密文是相同的,当存储用户密码的数据库泄露后,攻击者会很容易便能找到相同密码的用户,从而降低了破解密码的难度.因此,在对用户密码进行加密时,需要考虑对密码进行包装,即使是相同的密码,也保存为不同的密文.即使用户输入的是弱密码,也考虑进行增强,从而增加密码被攻破的难度.
解决方案:采用为一个密码拼接一个随机字符来进行加密,这个随机字符我们称之为"盐".假如有一个加盐后的加密串,黑客通过一定手段这个加密串,他拿到的明文并不是我们加密前的字符串,而是加密前的字符串和盐组合的字符串,这样相对来说又增加了字符串的安全性。

加密工具测试类
/**
* 测试生成安全密码
* 此方法展示了如何使用SecurityUtil类来生成一个安全的、加密后的密码
*/
String sqlPassword = sqlPassword = SecurityUtil.encrypt("123456");
String inputPassword = "123456";
@Test
void genPassword() {
//生成加密后的密码
System.out.println(sqlPassword);
}
@Test
void verifyPassword() {
//获取salt
String 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));
}
加密:
解密:
加密工具类
@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;
}
//获取salt
String 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中对加密工具类进行使用
@Override
public 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;
}
修改数据库中用户的密码
明文--> 密文