项目概述
项目名称:知识共享小屋
项目描述:该博客系统使用Spring Boot框架开发,旨在实现用户注册、登录、查看、修改、发布、删除文章、强制登录等功能。
开发环境:MacOS15、IDEA专业版、JDK17、Navicat、Termius
应用技术:Spring 、SpringBoot、MyBatis、MySQL、JWT、Ajax
一、项目架构(框架、配置文件)
项目框架
工欲善其事,必先利其器
因此先将项目架构搭建好,主要分为controller、mapper、model、config、util、、constants、interceptor几个包,并没有太过于复杂的逻辑便省略service。
YML配置
相对于properties,yml采用缩进来表示结构层次,使其更具有可读性
# 数据库配置
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/mycnblog?characterEncoding=utf8&useSSL=false
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
# mybatis配置
mybatis:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # mybatis打印执行SQL语句配置
map-underscore-to-camel-case: true #自动驼峰转换 eg: user_name -> userName
# 日志配置
logging:
file: # 储存打印日志
name: logs/blog.log
二、数据库设计 (结构、SQL、实体)
结构设计
SQL语句
创建数据库代码放在resource文件夹下,方便后期在云服务器上创建数据库,将SQL语句放在Navicat中执行。
-- 创建数据库
CREATE DATABASE IF NOT EXISTS blog CHARACTER SET utf8mb4;
-- 使用数据库
USE blog;
-- 用户表
DROP TABLE IF EXISTS user;
CREATE TABLE user (
`id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
`user_name` VARCHAR(128) NOT NULL,
`password` VARCHAR(128) NOT NULL,
`github` VARCHAR(128) NULL,
`delete_flag` TINYINT(4) NULL DEFAULT 0, -- 采用物理删除方式,默认0显示,1不显示
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, -- 创建时间
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP -- 更改后时间
) DEFAULT CHARACTER SET = utf8mb4 COMMENT = '用户表';
-- 博客表
DROP TABLE IF EXISTS article;
CREATE TABLE article (
`id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
`title` VARCHAR(255) NOT NULL,
`content` TEXT NOT NULL, -- 使用TEXT类型存储文章内容
`user_id` INT NOT NULL, -- 文章作者id
`delete_flag` TINYINT(4) NULL DEFAULT 0, -- 采用物理删除方式,默认0显示,1不显示
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, -- 创建时间
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, -- 更改后时间
FOREIGN KEY (user_id) REFERENCES user(id) -- 通过user_id作为外键和user表中id关联,从而查看作者信息
) DEFAULT CHARACTER SET = utf8mb4 COMMENT = '文章表';
-- 新增用户信息
INSERT INTO user (user_name, password, github) VALUES ("zhangsan", "123456", "https://gitee.com/1");
INSERT INTO user (user_name, password, github) VALUES ("lisi", "123456", "https://gitee.com/2");
-- 新增博客文章
INSERT INTO article (title, content, user_id) VALUES ("第一篇博客", "111我是博客正文我是博客正文我是博客正文", 1);
INSERT INTO article (title, content, user_id) VALUES ("第二篇博客", "222我是博客正文我是博客正文我是博客正文", 2);
实体类
在model文件下创建user、article实体类,进行数据封装便于对数据库数据进行管理和操作,按照数据表进行变量创建。
@Data
public class UserInfo {
private Integer id;
private String userName;
private String password;
private String github;
private Integer deleteFlag;
private Date createTime;
private Date updateTime;
}
@Data
public class BlogInfo {
private Integer id;
private String title;
private String content;
private Integer userId;
private Integer deleteFlag;
private Date createTime;
private Date updateTime;
private boolean isAuthor;
public String getCreateTime() { //重写get方法,对默认时间进行格式化
return DateUtils.format(createTime);
}
}
注:在BlogInfo表中调用DataUtils中format方法,对createTime的get方法进行重写,确保以正确的时间格式显示。
封装数据库操作
先将大体需要调用数据库哪些操作进行编写,方便后续调用,同样分为user和blog操作进行划分,在mapper下创建UserMapper、BlogMapper。
UserMapper:
@Mapper
public interface UserMapper {
//注册用户插入数据库user表中
@Insert("insert into user(user_name, password) values (#{userName}, #{password})")
Integer insertUser(String userName, String password);
//通过id查询用户信息
@Select("select * from user where delete_flag = 0 and id = #{id}")
UserInfo selectById(Integer id);
//通过用户名查询用户信息,验证用户密码是否正确
@Select("select * from user where delete_flag = 0 and user_name = #{userName}")
UserInfo selectByUserName(String userName);
}
BlogMapper:
@Mapper
public interface BlogMapper {
//查询所有博客信息
@Select("select * from article where delete_flag = 0")
List<BlogInfo> selectAllBlog();
//通过博客id查询博客信息
@Select("select * from article where delete_flag = 0 and id = #{blogId}")
BlogInfo selectByBlogId(Integer blogId);
//将默认delete_flag的0,修改成1进行物理删除
@Update("update article set delete_flag = 1 where id = #{blogId}")
boolean delete(Integer blogId);
//将博客信息插入到blog表中
@Insert("insert into article(title, content, user_id) values (#{title}, #{content}, #{userId})")
Integer insertBlog(BlogInfo blogInfo);
//更新博客信息需要判断当前参数是否为空,通过mybatis的注解方式来实现此方法过于繁琐,不利于查看,通常使用xml的方式进行编写
//在此只有一个数据更改较为繁琐,便不使用xml方式
@Update({
"<script>",
"UPDATE article",
"<set>",
"<if test='title != null'>title = #{title},</if>",
"<if test='content != null'>content = #{content},</if>",
"<if test='userId != null'>user_id = #{userId},</if>",
"<if test='deleteFlag != null'>delete_flag = #{deleteFlag},</if>",
"</set>",
"WHERE id = #{id}",
"</script>"
})
Integer updateBlog(BlogInfo blogInfo);
}
三、Utils(Date、Security、JWT)
DateUtil
数据库中current_timestamp默认时间格式为 2024-07-15T14:20:00Z格式,因此要将其转换为 YY-MM-DD HH-mm-ss格式,通过java.text下的SimpleDateFormat方法来进行格式转换。
public class DateUtils {
// 定义时间默认格式
private static final String DEFAULT_DATE_TIME = "yyyy-MM-dd HH:mm:ss";
//根据默认格式返回时间
public static String format(Date date) {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat(DEFAULT_DATE_TIME);
return simpleDateFormat.format(date);
}
}
SecurityUtils
用户隐私信息在数据库中以明文的方式进行存储,数据库信息泄漏隐私显而易见,通过md5的加密方式来对数据进行存储。此项目中对用户注册密码进行加密,用户登录进行校验。
public class SecurityUtils {
/**
* 将注册密码和通过UUID随机生成的盐值(salt)进行mad加密,返回salt目的方便后续解密操作
* @param password
* @return salt + md5(salt + password)
*/
public static String encrypt(String password) {
String salt = UUID.randomUUID().toString().replace("-", "");
String result = DigestUtils.md5DigestAsHex((salt + password).getBytes());
return salt + result;
}
/**
* 加密方法由salt + md5(salt + password)组成六十四位字符进行存储,因此前32位则是salt,
* salt为固定不变的,在将salt和新输入的password进行md5加密,
* 最后将salt和加密后的密码拼接在一起与数据库中存储的加密密码进行比比对,相同则密码正确。
* @param password
* @param sqlPassword
* @return boolean
*/
public static boolean verify(String password, String sqlPassword) {
if(!StringUtils.hasLength(password)) {
return false;
}
if(sqlPassword == null || sqlPassword.length() != 64) {
return false;
}
String salt = sqlPassword.substring(0, 32);
String result = DigestUtils.md5DigestAsHex((salt + password).getBytes());
return (salt + result).equals(sqlPassword);
}
}
JWTUtils
JWT主要应用于信息交换、身份校验,适用于分布式系统中跨域身份验证问题,主要由头部(Header)、载荷(Payload)、签名(Singnature)三部分组成。
在pom.xml文件中导入JWT依赖,创建JWTUtils对生成、解析功能进行封装
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.3</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.3</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.3</version>
<scope>runtime</scope>
</dependency>
public class JWTUtils {
// 定义JWT过期时间(单位为ms)此处设为一小时
private static final long JWT_EXPIRATION = 60 * 60 * 1000;
// 设置签名密钥,通过Keys.secretKeyFor(Keys.HmacShaKeySize.of(256))方法来随机生成
// 可以通过随机生成密钥进一步提高安全性
private static final String KEY = "r2VCxiEsp+Z8W/UZHHJdHoK9VcDbp7KQZNt2+zfQw0Q=";
//通过对header和payload进行加密,生成签名
private static final SecretKey SECRET_KEY = Keys.hmacShaKeyFor(Decoders.BASE64.decode(KEY));
/**
*通过jsonwebtoken中的方法,设置声明、过期时间、签名生成token
* @param claims
* @return token
*/
public static String genJWT(Map<String, Object> claims) {
String token = Jwts.builder().setClaims(claims)
.setExpiration(new Date(System.currentTimeMillis() + JWT_EXPIRATION))
.signWith(SECRET_KEY)
.compact();
return token;
}
/**
* 设置解析签名,构造解析器,解析token中的声明部分
* @param token
* @return 声明(body)
*/
public static Claims parserJWT(String token) {
JwtParserBuilder jwtParserBuilder = Jwts.parserBuilder().setSigningKey(SECRET_KEY);
Claims body = jwtParserBuilder.build().parseClaimsJws(token).getBody();
return body;
}
/**
* 调用parseJWT方法付对token进行解析,后去声明中的id,方便后续通过token查看用户id
* @param token
* @return 用户ID(userID)
*/
public static Integer getIdByToken(String token) {
Claims claims = parserJWT(token);
if(claims != null) {
Integer userId = (Integer) claims.get(Constants.TOKEN_ID);
if(userId > 0){
return userId;
}
}
return null;
}
}
四、统一结果、异常返回
对响应结果和异常进行统一规划,确保了所有接口返回的结果处理的异常都是一致的,提高了代码的可读性,简化了客户端的处理逻辑。
统一结果实体类
@Data
public class Result<T> { //通过泛型来处理,避免了类型转换的的问题
private int code;
private String message;
private T data;
/**
* 业务逻辑执行成功
* @param data
* @return
*/
public static <T> Result <T> success(T data) {
Result result = new Result();
result.setCode(200);
result.setMessage("");
result.setData(data);
return result;
}
/**
* 业务逻辑处理失败,两种处理方法判断是否需要数据进行处理
* @param data、message
* @param message
* @return
*/
public static <T> Result<T> fail(String message) {
Result result = new Result();
result.setCode(500);
result.setMessage(message);
return result;
}
public static <T> Result<T> fail(T data, String message) {
Result result = new Result();
result.setCode(500);
result.setMessage(message);
result.setData(data);
return result;
}
}
统一结果封装
@ControllerAdvice //应用于所有响应和异常的spring注解
public class ResponseAdvice implements ResponseBodyAdvice {
@Autowired
private ObjectMapper objectMapper; //Jackson库中的类,用于处理JSON序列化和反序列化
//判断响应是否处理,true or false;
@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 Result) { //响应类型为Result类型,不做处理直接返回
return body;
}
if (body instanceof String) { //响应结果为String,利用ObjectMapper将其转换成JSON格式
return objectMapper.writeValueAsString(Result.success(body));
}
return Result.success(body); //响应结果为其他,转换为Result格式
}
}
二者主要的区别在于,前者规定了统一返回的格式,后者则规定响应由什么格式返回。
统一异常处理
@Slf4j
@ControllerAdvice
public class ExceptionHandle {
// 捕获程序中未处理的异常,通过统一格式进行返回,不会暴露更多信息
@ExceptionHandler
public Object ExceptionHandle(Exception e) {
log.error("异常路径" + e.getMessage());
return Result.fail("内部错误,请联系管理员");
}
}
五、注册、登录、注销登录
注册
客户端访问blog_register.html页面,输入注册注册信息通过Ajax发送http请求,将参数传递到服务器,服务器经过md5加密后通过mybatis于数据库进行交互。
客户端代码:
<script>
function register() {
$.ajax({
url: "user/register",
type: "post",
data: {
userName : $("#username").val(),
password : $("#password").val()
},
success : function (result) { //服务器正确响应,通过响应数据进行页面跳转
if (result.code == 200 && result.data == "success") {
alert("注册成功!");
location.replace("/blog_login.html");
} else {
alert(result.errMessage);
}
},
error : function (err) {
if(err.status == 401) { //针对拦截器返回状态码做出响应,401表示用户为登录,统一拦截跳转到登录页面
location.replace("/blog_login.html");
}
}
})
};
</script>
登录
登录逻辑同理于注册页面,不同之处在于服务器接受参数后查询用户密码,进行解密验证密码是否正确,正确则跳转到博客详情页面。
客户端代码:
<script>
function login() {
$.ajax({
url : "user/login",
type : "get",
data: {
userName : $("#username").val(),
password : $("#password").val()
},
success : function (result) { //服务器正确响应,通过响应数据进行页面跳转
if (result.code == 200 && result.data != "") { //成功登录后,data中存储的为token信息,只需判断是否为空即可
localStorage.setItem("user_token", result.data); //成功登录后,将服务器设置的token存储到客户端中,并在common.js创建公用函数,将token放在请求头中
location.replace("/blog_list.html");
} else {
alert(result.errMessage);
location.replace("bolg_login.html");
}
},
})
}
function register() {
location.replace("/blog_register.html");
}
</script>
Token存储
创建common.js用于存放共用函数,创建一个全局函数,通过此函数将token放在请求头中,确保每次发送ajax请求时都会携带token,方便服务器进行验证。
$(document).ajaxSend(function (e, xhr, opt){
var item = localStorage.getItem("user_token"); //从本地存储中获取名为"user_token"的元素
xhr.setRequestHeader("user_token",item); //将该元素携带的token放到请求头中,方便服务器进行验证
})
登录、注册服务器代码:
接受客户端传递的参数,通过SecurityUtils工具进行加密解密,成功登录后将用户信息存储通过JWTUtils存储到token中,跳转到博客列表页面。
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserMapper userMapper;
@PostMapping("/register")
public Result register(String userName, String password) {
//判断用户是否正确输入注册信息
if (!StringUtils.hasLength(userName) || !StringUtils.hasLength(password)) {
return Result.fail("用户名或密码为空,请重新输入!");
}
//信息正确通过调用userMapper实现数据存储
try{
//通过md5对用户密码进行加密,在存储到数据库中
String encryptPassword = SecurityUtils.encrypt(password);
Integer result = userMapper.insertUser(userName, encryptPassword);
return Result.success("success");
} catch (NullPointerException e) {
//可能插入失败返回用户为空,手动处理处理异常,不通过统一异常进行返回,手动返回出错信息
log.error("错误信息:", e);
return Result.fail("用户名已存在,请重新输入");
}
}
@GetMapping("/login")
public Result login(String userName, String password) {
//判断输入格式是否正确
if (!StringUtils.hasLength(userName) || !StringUtils.hasLength(password)) {
return Result.fail("用户名或密码为空,请重新输入!");
}
try {
//通过调用verify验证密码
UserInfo userInfo = userMapper.selectByUserName(userName);
boolean result = SecurityUtils.verify(password, userInfo.getPassword());
if(!result) {
return Result.fail("密码错误,请重新输入!");
}
//登录成功,同JWT将用户信息存储在token中
Map<String, Object> map = new HashMap<>();
map.put(Constants.TOKEN_ID, userInfo.getId());
map.put(Constants.TOKEN_USERNAME, userInfo.getUserName());
String token = JWTUtils.genJWT(map);
return Result.success(token);
}catch (NullPointerException e){
//登录失败返回用户为空,手动处理处理异常,不通过统一异常进行返回,手动返回出错信息
log.error("错误信息:", e);
return Result.fail("用户名不存在,请重新输入!");
}
}
}
注销登录
在用户登录后所有页面都显示注销登录按钮,单用户点击此按钮时触发登录事件,删除用户tokon退回到登录页面,此功能为通用功能因此写在common.js文件下。
function logout() {
localStorage.removeItem("user_token"); //用户退出登录后,移除当前所存在的token
location.href = "blog_login.html"; //回到登录页面
}
六、文章列表页面
个人信息
用户登录成功后个人信息存储在token当中,客户端向服务器发起ajax请求,客户端从token获取用户信息进行返回。
客户端代码:
后续还要对博客作者信息进行更新,因此直接将此内容封装成一个函数到commom.js文件中,通过url作为参数进行传递,在博客列表页面直接调用getInfo(user/getUserInfo)函数进行信息交互。
function getInfo(url) {
$.ajax({
url: url,
type: "get",
success : function (result) {
if(result.code == 200 && result.data != "") { //从服务器获取用户信息,对页面内容进行更改
$(".container .left .card h3").text(result.data.userName);
$(".container .left .card a").attr("href", result.data.github);
} else {
alert(result.errMessage);
location.replace("/blog_login.html");
}
},
error : function (err) { //针对拦截器返回状态码做出响应,401表示用户为登录,统一拦截跳转到登录页面
if(err.status == 401) {
location.replace("/blog_login.html");
}
}
})
}
服务器代码:
用户信息更新,同样放在UserController接口下,创建getUserInfo接口进行调用。
@GetMapping("/getUserInfo")
public Result getUserInfo(HttpServletRequest request) {
try{
//从request请求头中获取token,从中获取用户ID,通过用户ID获取用户信息进行返回
String token = request.getHeader("user_token");
Integer userId = JWTUtils.getIdByToken(token);
UserInfo userInfo = userMapper.selectById(userId);
System.out.println(token);
return Result.success(userInfo);
}catch (NullPointerException e) {
log.error("token错误:", e);
return Result.fail("用户未登录,重新登录!");
}
}
博客列表
用户成功登录进入到博客详情页面,通过服务器调用数据库blog表信息,将所有博客信息返回到客户端进行显示。
客户端代码:
服务器返回博客信息列表,依次通过遍历进行赋值,最后拼接到列表详情中。
<script>
getInfo("/user/getUserInfo"); //调用common.js中的getInfo函数,发送ajax请求从服务器获取登录用户信息
$.ajax({
url: "blog/getList",
type: "get",
success: function (result) {
if(result.code == 200 && result.data != "") {
var blogs = result.data; //将博客List列表存储到blogs变量当中,简化代码量
var finalHtml = ''; //定义变量用于拼接博客信息
for(blog of blogs) { //遍历博客列表,依次进行赋值
finalHtml += '<div class="blog">';
finalHtml += '<div class="title">'+ blog.title +'</div>';
finalHtml += '<div class="date">'+ blog.createTime +'</div>';
finalHtml += '<div class="desc">'+ blog.content +'</div>';
finalHtml += '<a class="detail" href="blog_detail.html?blogId= ' + blog.id + '">查看全文>></a>';
finalHtml += '</div>';
}
$(".container .right").html(finalHtml); //将最后的html页面拼接到博客详情下
}
},
error : function (err) {
if(err.status == 401) { //针对拦截器返回状态码做出响应,401表示用户为登录,统一拦截跳转到登录页面
location.replace("/blog_login.html");
}
}
});
</script>
服务器代码:
在controller包下创建BlogController类,用于存放关于博客相关操作接口。
@RestController
@RequestMapping("/blog")
public class BlogController {
@Autowired
private BlogMapper blogMapper;
@GetMapping("/getList")
public Result getList() {
List<BlogInfo> blogInfoList = blogMapper.selectAllBlog();
return Result.success(blogInfoList);
}
}
七、文章详情页面
在博客列表页面点击全文进入博客详情页面,在页面显示博客作者信息、文章详细信息、判断登录用户是否为博客信息作者,是否可以显示删除按钮进行删除操作。
作者个人信息
与博客列表页面显示登录用户信息大同小异,唯一区别在与客户端和服务器说调用的接口不同。
客户端代码:
通过location.search传递查询字符串给服务器,服务器接受参数通过博客id进行查询博客信息。
<script>
//显示博客作者信息
var userUrl = "/user/getAuthorInfo" + location.search; //调用location的search方法,获取url中查询字符串部分,?及以后地址
getInfo(userUrl); //调用common.js里getInfo方法
</script>
服务器代码:
@GetMapping("/getAuthorInfo")
public Result getUserInfo(Integer blogId) {
try{ //通过博客id查询博客信息,通过博客信息里的用户id查询用户信息
BlogInfo blogInfo = blogMapper.selectByBlogId(blogId);
UserInfo userInfo = userMapper.selectById(blogInfo.getUserId());
return Result.success(userInfo);
}catch (NullPointerException e) {
log.error("用户信息异常:", e);
return Result.fail("获取作者信息失败!");
}
}
博客信息、删除权限
显示博客信息同上文显示博客列表,根据服务器返回响应进行赋值,因为要判断是否有删除权限,需要在blogInfo中增加布尔类型isAuthor变量来确定是否有删除权限。
客户端代码:
<script>
getInfo("/user/getAuthorInfo" + location.search); //调用location的search方法,获取url中查询字符串部分,?及以后地址
$.ajax({
url: "blog/getBlogDetail" + location.search, //获取当前博客id
type: "get",
success: function (result){ //从响应中获取博客信息,依次进行赋值
if(result.code == "200" && result.data != ""){
var blog = result.data;
$(".container .title").text(blog.title);
$(".container .date").text(blog.createTime);
$(".container .detail").text(blog.content)
}
if(result.data.author == true){ //判断用户是否为作者本人,决定删除权限
var html = "";
html += '<button οnclick="window.location.href=\'blog_update.html?blogId=' + blog.id + '\'">编辑</button>';
html += '<button οnclick="deleteBlog(' + blog.id + ')">删除</button>';
$(".container .operating").html(html);
}
},
error : function (err) { //对于异常响应进行判断
if(err.status == 401) {
location.replace("/blog_login.html");
}
}
});
function deleteBlog() {
if(confirm("确认删除?")){ //添加提示框,询问用户是否确定删除
$.ajax({
type: "post",
url : "/blog/delete" + location.search,
success: function (result) {
if(result.code == 200 || result.data == "success"){
alert("删除成功");
location.replace("/blog_list.html");
}else {
alert(result.errMessage);
}
},
error : function (err) {
if(err.status == 401) { //针对拦截器返回状态码做出响应,401表示用户为登录,统一拦截跳转到登录页面
location.replace("/blog_login.html");
}
}
})
}
};
</script>
服务器代码:
@GetMapping("/getBlogDetail")
public Result getBlogDetail(Integer blogId, HttpServletRequest request) {
//通过博客id查找博客信息,从博客信息中获取用户信息,
//从token中获取用户信息,判断token中用户信息和博客信息中用户信息是否相等,设置权限
BlogInfo blogInfo = blogMapper.selectByBlogId(blogId);
String token = request.getHeader("user_token");
Integer userId = JWTUtils.getIdByToken(token);
if(blogInfo.getUserId().equals(userId)){
blogInfo.setAuthor(true); //是同一名用户设置为true
return Result.success(blogInfo);
} else {
blogInfo.setAuthor(false); //不是则为false
return Result.success("false");
}
}
@PostMapping("/delete")
public Result delete(Integer blogId) {
//采用物理删除的方式,默认0为正常显示,更改为1表示删除
boolean delete = blogMapper.delete(blogId);
if(delete) {
return Result.success("success");
} else {
return Result.fail("删除失败!");
}
}
八、文章更改页面
发布新博客
用户登录后点击写博客进入blog_edit.html页面,将输入的文章标题、内容通过json的格式传递给服务器进行处理。
服务器代码:
<script type="text/javascript">
$(function () { //编辑文档官方说明
var editor = editormd("editor", {
width: "100%",
height: "550px",
path: "blog-editormd/lib/"
});
});
function submit() {
$.ajax({
type: "post",
url: "/blog/addBlog",
contentType : "application/json", //将js中的对象,序列化成json字符串的形式进行传递,方便服务器进行处理
data: JSON.stringify({
title: $("#title").val(),
content: $("#content").val()
}),
success : function (result) {
if(result.code == 200 || result.data == "success") {
location.replace("blog_list.html");
}else {
alert(result.errMessage);
}
},
error : function (err) {
if(err.status == 401) { //针对拦截器返回状态码做出响应,401表示用户为登录,统一拦截跳转到登录页面
location.replace("/blog_login.html");
}
}
})
};
</script>
服务器代码:
@PostMapping("/addBlog")
public Result addBlog(@RequestBody BlogInfo blogInfo, HttpServletRequest request) { //@RequestBody注解将客户端发送来的json转化为java对象
try {
//从token中获取当前用户信息id,储存到blog表中的userId中
String token = request.getHeader("user_token");
Integer userId = JWTUtils.getIdByToken(token);
blogInfo.setUserId(userId);
blogMapper.insertBlog(blogInfo);
return Result.success("success");
} catch (NullPointerException e) {
log.error("发布失败:", e);
return Result.fail("博客发布失败,请重新发布!");
}
}
显示、更改博客信息
显示博客信息调用上文显示博客详情信息同一个接口即可,更改博客信息等同于写博客接口,唯一区别在于服务器在对逻辑进行处理时,前者在数据库调用insert插入新的SQL,后者则是调用update进行更新。
客户端代码:
<script type="text/javascript">
$(function () { //文档编辑器官方提供的使用说明,下同
var editor = editormd("editor", {
width : "100%",
height : "550px",
path: "blog-editormd/lib/"
});
});
function submit() {
$.ajax({ //提交更新博客参数,成功返回博客列表页
type: "post",
url: "/blog/update",
data: {
id: $("#blogId").val(),
title: $("#title").val(),
content: $("#content").val(),
},
success: function (result) {
if(result.code == 200 || result.data == "success"){
alert("更新成功");
location.replace("/blog_list.html");
}else {
alert(result.errMessage);
}
},
error : function (err) {
if(err.status == 401) { //针对拦截器返回状态码做出响应,401表示用户为登录,统一拦截跳转到登录页面
location.replace("/blog_login.html");
}
}
});
}
function getBlogInfo() {
$.ajax({
url: "blog/getBlogDetail" + location.search, //同样调用getBlogDetail接口,传递当前博客id,收到服务器返回博客信息进行依次赋值
type: "get",
success: function (result) {
if (result.code == 200 && result.data != "") {
var blog = result.data;
$("#blogId").val(blog.id),
$("#title").val(blog.title);
editormd("editor", {
width: "100%",
height: "550px",
path: "blog-editormd/lib/",
onload: function () {
this.watch();
this.setMarkdown(blog.content);
}
});
}else {
alert(result.errMessage);
}
},
error : function (err) {
if(err.status == 401) { //针对拦截器返回状态码做出响应,401表示用户为登录,统一拦截跳转到登录页面
location.replace("/blog_login.html");
}
}
});
}
getBlogInfo(); //页面初始化时调用此函数,在编辑页面显示博客基本信息
</script>
服务器代码:
@PostMapping("/update")
public Result update(BlogInfo blogInfo) {
try {
blogMapper.updateBlog(blogInfo);
return Result.success("success");
} catch (NullPointerException e) {
return Result.fail("更新失败!");
}
}
九、拦截器设置
避免用户使用时通过url直接跳过登录页面进入其他页面当中,或者知道服务器接口构造非法请求入侵,添加拦截器来指定用户只能通过特定url进行访问,增强了项目的安全性。
构建登录拦截器
/**
* 构造登录拦截器,实现HandlerInterceptor接口,重写preHandle方法,刚放在作用于http请求后、解析请求逻辑前进行判断
* 用户已经登录,token中存在用户信息,则返回ture,相反则为false
*/
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
try {
String token = request.getHeader("user_token");
Claims claims = JWTUtils.parserJWT(token);
return true;
} catch (Exception e) {
response.setStatus(401);
}
return false;
}
}
配置拦截器
/**
* 配置拦截器,实现WebMvcConfigurer接口,重写addInterceptors方法,注入配置登录拦截器依赖
*将配置好的拦截器注入到列表中,添加拦截路径设置为所有,设置排除拦截路径,再次用List进行管理
*/
@Configuration
public class LoginConfig implements WebMvcConfigurer {
@Autowired
private LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/**")
.excludePathPatterns(excludes);
}
private final List excludes = Arrays.asList(
"/**/*.html",
"/blog-editormd/**",
"/css/**",
"/js/**",
"/pic/**",
"/user/login",
"/user/register"
);
}
在开发环境下对各种接口进行测试,没有问题即可打成jar包在服务器上运行。
十、注解解析
注解 | 框架 | 解析 |
---|---|---|
@Slf4j | Lombok | 便于打印日志,简化日志记录 |
@Data | Lombok | 主要包含set、get、toString、equal等方法 |
@Autowired | Spring | 注入对象交给Spring管理,完成自动装配的工作 |
@RestController | Spring | Controller、ResponsBody结合体,返回值作为响应体 |
@GetMapping | Spring | 处理HTTP的GET请求 |
@PostMapping | Spring | 处理HTTP的POST请求 |
@RequestMapping | Spring | 自动将返回内容转换为JSON格式,处理GE、POST请求 |
@ControllerAdvice | Spring | 处理全局异常 |
@Component | Spring | 通用的Spring管理Bean注解 |
@Configuration | Spring | Spring中标志配置类 |
总结:
博客管理系统主要在于数据库设计、自定义工具类、统一结果异常返回、接口前后端交互、拦截器处理几大关要素,前端设计因人而异每个人都有属于自己的审美,主要在于理解接口如何定义、怎样实现前后端交互做到方便、快捷、安全。