通过前⾯课程的学习, 我们掌握了Spring框架和MyBatis的基本使⽤, 并完成了图书管理系统的常规功能 开发, 接下来我们系统的从0到1完成⼀个项⽬的开发;
1. 项⽬介绍
使⽤SSM框架实现⼀个简单的博客系统 共5个⻚⾯
1. 用户登录
2. 博客发表⻚
3. 博客编辑⻚
4. 博客列表⻚
5. 博客详情⻚
1.1 功能描述
⽤⼾登录成功后, 可以查看所有⼈的博客. 点击 《查看原文》 可以查看该博客的正⽂内容;
如果该博客作者为当前登录⽤⼾, 可以完成博客的修改和删除操作, 以及发表新博客 ;
1.2 ⻚⾯预览
登录页面:
博客列表和博客详情⻚:
博客发表和更新页:
2. 准备工作
2.1 数据准备
创建user表和blog表;
create database if not exists spring_blog_240908 charset utf8mb4;
use spring_blog_240908;
drop table if exists user;
create table user (
`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(),
PRIMARY KEY ( id ),
UNIQUE INDEX user_name_UNIQUE ( user_name ASC ))
ENGINE = INNODB DEFAULT
CHARACTER set = utf8mb4 comment = '用户表';
drop table if exists blog;
CREATE TABLE blog (
`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(),
PRIMARY KEY (id))
ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '博客表';
insert into user (user_name,password,github_url)values("shenmengyao","111111","https://gitee.com/smollye/projects");
insert into user (user_name,password,github_url)values("yuanyiqi","222222","https://gitee.com/smollye/projects");
insert into blog (title,content,user_id) values("第⼀篇博客","我是神喵我是神喵我是神喵",1);
insert into blog (title,content,user_id) values("第⼆篇博客","我是小黑我是小黑我是小黑",2);
数据库信息如下所示:
2.2 创建项⽬
创建SpringBoot项⽬, 添加Spring MVC 和MyBatis对应依赖
2.3 准备前端页面
把课博客系统静态⻚⾯拷⻉到static⽬录下
2.4 配置配置⽂件
server:
port: 8087
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/spring_blog_240908?characterEncoding=utf8&useSSL=false
username: root
password: ******
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:
configuration:
map-underscore-to-camel-case: true #配置驼峰自动转换
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #打印Sql语句
mapper-locations: classpath:mapper/**Mapper.xml
# 设置日志路径
logging:
file:
name: spring_blog_240908.log
2.5 测试
访问前端⻚⾯:
前端⻚⾯可以正确显⽰, 说明项⽬初始化成功;
3. 项⽬公共模块
项⽬分为控制层(Controller), 服务层(Service), 持久层(Mapper). 各层之间的调⽤关系如下:
先根据需求完成实体类和公共层代码的编写;
3.1 实体类
@Data
public class Blog {
private Integer id;
private String title;
private String content;
private Integer userId;
private String deleteFlag;
private Date createTime;
private Date updateTime;
}
@Data
public class User {
private Integer id;
private String userName;
private String password;
private String githubUrl;
private Byte deleteFlag;
private Date createTime;
private Date updateTime;
}
3.2 公共层
3.2.1 统⼀返回结果实体类
a. code: 业务状态码
200: 业务处理成功
-1 : 业务处理失败
-2 : ⽤户未登录
后续有其他异常信息, 可以再补充.
b. msg: 业务处理失败时, 返回的错误信息
c. data: 业务返回数据
业务状态码设置代码如下:
public class Constant {
//返回业务的状态码设置
public final static Integer SUCCESS_CODE = 200;
public final static Integer FAIL_CODE = -1;
public final static Integer UNLOGIN_CODE = -2;
}
返回结果实体类设置代码:
package com.example.spring_blog_24_9_8.model;
import com.example.spring_blog_24_9_8.constants.Constant;
import lombok.Data;
@Data
public class Result {
private int code;
private String msg;
private Object data;
/**
* 业务成功时执行的方法
*/
public static Result success(Object data){
Result result = new Result();
result.setCode(Constant.SUCCESS_CODE);
result.setMsg("");
result.setData(data);
return result;
}
/**
* 业务执⾏失败时返回的⽅法
*/
public static Result fail(int code, String msg){
Result result = new Result();
result.setCode(Constant.FAIL_CODE);
result.setMsg(msg);
result.setData("");
return result;
}
/**
* ⽤⼾未登录时返回的⽅法
*/
public static Result unlogin(int code,String msg,Object data){
Result result = new Result();
result.setCode(Constant.UNLOGIN_CODE);
result.setMsg("用户未登录");
result.setData(data);
return result;
}
}
3.2.2. 统⼀返回结果
package com.example.spring_blog_24_9_8.config;
import com.example.spring_blog_24_9_8.model.Result;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.SneakyThrows;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
//哪个接口执行统一结果返回
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return true;
}
@SneakyThrows
@Override
//统一结果返回的具体逻辑
public Object beforeBodyWrite(Object body, MethodParameter returnType,
MediaType selectedContentType, Class selectedConverterType,
ServerHttpRequest request, ServerHttpResponse response) {
if (body instanceof Result){
return body;
}
//对String 类型单独处理
if (body instanceof String){
ObjectMapper mapper = new ObjectMapper();
return mapper.writeValueAsString(Result.success(body));
}
return Result.success(body);
}
}
3.3.3. 统⼀异常处理
@ControllerAdvice
@ResponseBody
public class ExceptionAdvice {
@ExceptionHandler(Exception.class)
public Result exceptionAdvice(Exception e){
return Result.fail(-1,e.getMessage());
}
}
4. 业务代码
4.1 持久层
根据需求, 先⼤致计算有哪些DB相关操作, 完成持久层初步代码, 后续再根据业务需求进⾏完善
1. ⽤⼾登录⻚
a. 根据⽤⼾名查询⽤⼾信息
2. 博客列表⻚
a. 根据id查询user信息
b. 获取所有博客列表
3. 博客详情⻚
a. 根据博客ID查询博客信息
b. 根据博客ID删除博客(修改delete_flag=1)
4. 博客修改⻚
a. 根据博客ID修改博客信息
5. 发表博客
a. 插⼊新的博客数据
根据以上分析, 来实现持久层的代码:
package com.example.spring_blog_24_9_8.mapper;
import com.example.spring_blog_24_9_8.model.Blog;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import java.util.List;
@Mapper
public interface BlogMapper {
@Select("Select id,title,content,user_id,delete_flag,create_time,update_time" +
"from blog where delete_flag = 0 order by create_time desc")
List<Blog> selectAllBlogs();
@Insert("insert into blog (title,content,user_id) values (#{title},#{content},#{userId})")
int insertBlog(Blog record);
@Select("select * from blog where id = #{id}")
Blog selectById(Integer id);
int updateBlog(Blog blog);
}
package com.example.spring_blog_24_9_8.mapper;
import com.example.spring_blog_24_9_8.model.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
@Mapper
public interface UserMapper {
@Select("select id, user_name, password, github_url, delete_flag, create_time " +
"from user where id = #{id}")
User selectById(Integer id);
@Select("select id, user_name, password, github_url, delete_flag, create_time " +
"from user where user_name = #{userName}")
User selectByName(String name);
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.spring_blog_24_9_8.mapper.BlogMapper">
<update id="updateBlog">
update blog
<set>
<if test="title!=null">
title = #{title},
</if>
<if test="content!=null">
content = #{content},
</if>
<if test="deleteFlag!=null">
delete_flag = #{deleteFlag},
</if>
</set>
where id = #{id}
</update>
</mapper>
书写测试⽤例, 简单进⾏单元测试
package com.example.spring_blog_24_9_8.mapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
class UserMapperTest {
@Autowired
private UserMapper userMapper;
@Test
void selectById() {
System.out.println(userMapper.selectById(2));
}
@Test
void selectByName() {
System.out.println(userMapper.selectByName("shenmengyao"));
}
}
selectbyid结果如下:
selestbyname结果如下:
package com.example.spring_blog_24_9_8.mapper;
import com.example.spring_blog_24_9_8.model.Blog;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
class BlogMapperTest {
@Autowired
private BlogMapper blogMapper;
@Test
void selectAllBlogs() {
System.out.println(blogMapper.selectAllBlogs());
}
@Test
void insertBlog() {
Blog blog = new Blog();
blog.setTitle("测试接⼝");
blog.setContent("单元测试测试接⼝测试接⼝");
blog.setUserId(1);
blogMapper.insertBlog(blog);
}
@Test
void selectById() {
System.out.println(blogMapper.selectById(3));
}
@Test
void updateBlog() {
Blog blog = new Blog();
blog.setId(3);
blog.setDeleteFlag(1);
blog.setTitle("测试修改接⼝");
blog.setContent("测试修改接⼝测试修改接⼝测试修改接⼝");
blogMapper.updateBlog(blog);
}
}
selectallblogs结果如下:
insertblog结果如下:
selectbyid结果如下:
updateblog结果如下:
4.2 实现博客列表
4.2.1 约定前后端交互接⼝
[请求]
/blog/getlist
[响应]
{
"code": 200,
"msg": "",
"data": [{
"id": 1,
"title": "第⼀篇博客",
"content": "111我是博客正⽂我是博客正⽂我是博客正⽂",
"userId": 1,
"deleteFlag": 0,
"createTime": "2023-10-21 16:56:57",
"updateTime": "2023-10-21T08:56:57.000+00:00"
},
.....
]
}
客⼾端给服务器发送⼀个 /blog/getlist 这样的 HTTP 请求, 服务器给客户端返回了⼀个 JSON 格 式的数据.
4.2.2 实现服务器代码
控制层代码:
@RestController
@RequestMapping("/blog")
public class BlogController {
@Autowired
private BlogService blogService;
@RequestMapping("/getList")
public List<Blog> getList(){
return blogService.getBlogList();
}
}
服务层代码 :
@Service
public class BlogService {
@Autowired
private BlogMapper blogMapper;
public List<Blog> getBlogList(){
return blogMapper.selectAllBlogs();
}
}
运行程序,浏览器访问http://127.0.0.1:8087/blog/getList,结果如下:
4.2.3 实现客⼾端代码
修改 blog_list.html, 删除之前写死的博客内容, 并新增 js 代码处理 ajax 请求;
使⽤ ajax 给服务器发送 HTTP 请求.;
服务器返回的响应是⼀个 JSON 格式的数据, 根据这个响应数据使⽤ DOM API 构造⻚⾯内容.
响应中的 postTime 字段为 ms 级时间戳, 需要转成格式化⽇期.
跳转到博客详情⻚的 url 形如 blog_detail.html?blogId=1 这样就可以让博客详情⻚知道 当前是要访问哪篇博客.
前端代码修改如下:
function getBlogList() {
$.ajax({
type: "get",
url: "/blog/getList",
success: function (result) {
//逻辑不全
//可以再完善, 比如code==200, 但是data为空的, 页面可以提示: 当前还没有任何博客, 快去写博客吧...
if (result.code == 200 && result.data != null && result.data.length > 0) {
//循环拼接数据到document
var finalHtml = "";
//页面展示
for (var blog of result.data) {
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>';
}
$(".right").html(finalHtml);
}
},
error: function (error) {
console.log("后端返回失败");
}
});
}
如上图所示,我们当前博客列表页显示的时间为时间戳,我们从后端也⽇期进⾏处理;SimpleDateFormat,格式参考官⽅⽂档:
/**
* 日期工具类
*/
public class DateUtils {
public static String format(Date date){
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm");
return simpleDateFormat.format(date);
}
}
重写获取博客创建时间:
@Data
public class Blog {
private Integer id;
private String title;
private String content;
private Integer userId;
private Integer deleteFlag;
private Date createTime;
private Date updateTime;
public String getCreateTime(){
return DateUtils.format(createTime);
}
}
重新访问博客列表页,查看页面结果:
4.3 实现博客详情
⽬前点击博客列表⻚的 "查看全⽂" , 能进⼊博客详情⻚, 但是这个博客详情⻚是写死的内容. 我们期望 能够根据当前的 博客 id 从服务器动态获取博客内容
4.3.1 约定前后端交互接⼝
[请求]
/blog/getBlogDetail?blogId=1
[响应]
{
"code": 200,
"msg": "",
"data": {
"id": 1,
"title": "第⼀篇博客",
"content": "我是神喵我是神喵我是神喵",
"userId": 1,
"deleteFlag": 0,
"createTime": "2023-10-21 16:56:57",
"updateTime": "2023-10-21T08:56:57.000+00:00"
}
}
4.3.2 实现服务器代码
在BlogController中添加getBlogDeatail ⽅法
@RequestMapping("/getBlogDetail")
public Blog getBlogDetail(Integer blogId){
return blogService.getBlogDetail(blogId);
}
在BlogService 中添加getBlogDeatil⽅法
public Blog getBlogDetail(Integer blogId){
return blogMapper.selectById(blogId);
}
访问http://127.0.0.1:8087/blog/getBlogDetail?blogId=1,结果如下:
4.3.3 实现客户端代码
修改 blog_content.html :
根据当前⻚⾯ URL 中的 blogId 参数(使⽤ location.search 即可得到形如 ?blogId=1 的数据), 给服务器发送 GET /blog 请求.
根据获取到的响应数据, 显⽰在⻚⾯上
1. 修改html⻚⾯, 去掉原来写死的博客标题, ⽇期和正⽂部分 ,代码如下:
<div class="content">
<div class="title"></div>
<div class="date"></div>
<div class="detail"></div>
<div class="operating">
<button onclick="window.location.href='blog_update.html'">编辑</button>
<button>删除</button>
</div>
</div>
2. 完善 js 代码, 从服务器获取博客详情数据.
$.ajax({
type: "get",
url: "/blog/getBlogDetail" + location.search,
success: function (result){
console.log(result);
if(result.code == 200 && result.data != null){
$(".title").text(result.data.title);
$(".date").text(result.data.createTime);
$(".detail").text(result.data.detail);
}
}
})
前进入到博客列表页,点击其中的查看全文进入到文章全文页面:
点击编辑进入到文章更新页面:
5. 实现登陆
分析
传统思路:
1、 登陆⻚⾯把⽤⼾名密码提交给服务器.
2、服务器端验证⽤⼾名密码是否正确, 并返回校验结果给后端
3、如果密码正确, 则在服务器端创建 Session . 通过 Cookie 把 sessionId 返回给浏览器.
问题:
集群环境下⽆法直接使⽤Session.
原因分析:
我们开发的项⽬, 在企业中很少会部署在⼀台机器上, 容易发⽣单点故障. (单点故障: ⼀旦这台服务器挂 了, 整个应⽤都没法访问了). 所以通常情况下, ⼀个Web应⽤会部署在多个服务器上, 通过Nginx等进⾏负载均衡,此时, 来⾃⼀个⽤⼾的请求就会被分发到不同的服务器上,请求逻辑如下所示:
假如我们使⽤Session进⾏会话跟踪, 会出现下面的场景:
1. ⽤⼾登录 ⽤⼾登录请求, 经过负载均衡, 把请求转给了第⼀台服务器, 第⼀台服务器进⾏账号密码 验证, 验证成功后, 把Session存在了第⼀台服务器上
2. 查询操作 ⽤⼾登录成功之后, 携带Cookie(⾥⾯有SessionId)继续执⾏查询操作, ⽐如查询博客列 表. 此时请求转发到了第⼆台机器, 第⼆台机器会先进⾏权限验证操作(通过SessionId验证⽤⼾是否 登录), 此时第⼆台机器上没有该⽤⼾的Session, 就会出现问题,来提⽰⽤⼾登录, 这样就会给用户带来不好的体验;该场景的逻辑如下所示:
接下来我们介绍另一种⽅案: 令牌技术
5.1 令牌技术
令牌其实就是⼀个⽤⼾⾝份的标识, 其实本质就是⼀个字符串. ⽐如我们出⾏在外, 会带着⾃⼰的⾝份证, 需要验证⾝份时, 就掏出⾝份证⾝份证不能伪造, 可以辨别真假 ;其运行逻辑如下所示:
服务器具备⽣成令牌和验证令牌的能⼒
我们使⽤令牌技术, 继续思考上述场景:
1. ⽤⼾登录 ⽤⼾登录请求, 经过负载均衡, 把请求转给了第⼀台服务器, 第⼀台服务器进⾏账号密码 验证, 验证成功后, ⽣成⼀个令牌, 并返回给客⼾端.
2. 客⼾端收到令牌之后, 把令牌存储起来. 可以存储在Cookie中, 也可以存储在其他的存储空间(⽐如 localStorage)
3. 查询操作 ⽤⼾登录成功之后, 携带令牌继续执⾏查询操作, ⽐如查询博客列表. 此时请求转发到了 第⼆台机器, 第⼆台机器会先进⾏权限验证操作. 服务器验证令牌是否有效, 如果有效, 就说明⽤⼾已 经执⾏了登录操作, 如果令牌是⽆效的, 就说明⽤⼾之前未执⾏登录操作
令牌的优缺点
优点:
• 解决了集群环境下的认证问题
• 减轻服务器的存储压⼒(⽆需在服务器端存储)
缺点:
需要⾃⼰实现(包括令牌的⽣成, 令牌的传递, 令牌的校验)
当前企业开发中, 解决会话跟踪使⽤最多的⽅案就是令牌技术.
5.2 JWT令牌
令牌本质就是⼀个字符串, 他的实现⽅式有很多, 我们采⽤⼀个JWT令牌来实现.
5.2.1 介绍
JWT全称: JSON Web Token
JSON Web Token(JWT)是⼀个开放的⾏业标准(RFC 7519), ⽤于客⼾端和服务器之间传递安全可靠的信息. 其本质是⼀个token, 是⼀种紧凑的URL安全⽅法.
5.2.2 JWT组成
JWT由三部分组成, 每部分中间使⽤点 (.) 分隔,⽐如:aaaaa.bbbbb.cccc
• Header(头部) 头部包括令牌的类型(即JWT)及使⽤的哈希算法(如HMAC SHA256或RSA)
• Payload(负载) 负载部分是存放有效信息的地⽅, ⾥⾯是⼀些⾃定义内容. ⽐如: {"userId":"123","userName":"shenemngyao"} , 也可以存在jwt提供的现场字段, ⽐如 exp(过期时间戳)等. 此部分不建议存放敏感信息, 因为此部分可以解码还原原始内容.
• Signature(签名) 此部分⽤于防⽌jwt内容被篡改, 确保安全性. 防⽌被篡改, ⽽不是防⽌被解析. JWT之所以安全, 就是因为最后的签名. jwt当中任何⼀个字符被篡改, 整个令牌都会校验失败. 就好⽐我们的⾝份证, 之所以能标识⼀个⼈的⾝份, 是因为他不能被篡改, ⽽不是因为内容加密.(任 何⼈都可以看到⾝份证的信息, jwt 也是)
对上⾯部分的信息, 使⽤Base64Url 进⾏编码, 合并在⼀起就是jwt令牌 Base64是编码⽅式,⽽不是加密⽅式
5.3 JWT令牌⽣成和校验
5.3.1. 引⼊JWT令牌的依赖
<!-- 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>
<!-- 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>
5.3.2. JWT令牌的⽣成和校验
使⽤Jar包中提供的API来完成JWT令牌的⽣成和校验,代码如下:
package com.example.spring_blog_24_9_8;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtParser;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.io.Encoders;
import io.jsonwebtoken.security.Keys;
import org.junit.jupiter.api.Test;
import javax.crypto.SecretKey;
import java.security.Key;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
public class JWTUtisTest {
//过期时间: 1小时的毫秒数
private final static long EXPIRATION_DATE = 60 * 60 * 1000;
//秘钥
private final static String secretString = "MDDOvlf8sQ675YuOsOxu45EXYK1dl/PoAyJq9C8vta0=";
//生成安全秘钥
private final static Key key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretString));
//生成令牌
@Test
public void genToken(){
//⾃定义信息
Map<String, Object> claim = new HashMap<>();
claim.put("userId", 1);
claim.put("userName", "shenmengyao");
String token = Jwts.builder()
.setClaims(claim) //⾃定义内容(负载
.setExpiration(new Date(System.currentTimeMillis()+EXPIRATION_DATE))//设置过期时间
.signWith(key)
.compact();
System.out.println(token);
}
//生成key
@Test
public void genKey(){
//生成key
SecretKey secretKey = Keys.secretKeyFor(SignatureAlgorithm.HS256);
String encode = Encoders.BASE64.encode(secretKey.getEncoded());
System.out.println(encode);
}
//校验令牌
@Test
public void parseToken(){
String token = "eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyTmFtZSI6InNoZW5tZW5neWFvIiwidXNlcklkIjoxLCJleHAiOjE3MjU5NTMzMTl9.Aa7kKOH6vJFo09eEYCLtGCqRh6lBiXucRh-ze-F-IY8";
JwtParser build = Jwts.parserBuilder().setSigningKey(key).build();
Claims body = null;
try {
body = build.parseClaimsJws(token).getBody();
} catch (Exception e) {
System.out.println("令牌校验失败");
}
System.out.println(body);
}
}
生成密钥:
生成令牌:
我们把⽣成的令牌通过官⽹进⾏解析, 就可以看到我们存储的信息了:
令牌校验:
令牌解析后, 我们可以看到⾥⾯存储的信息,如果在解析的过程当中没有报错,就说明解析成功了. 令牌解析时, 也会进⾏时间有效性的校验, 如果令牌过期了, 解析也会失败. 修改令牌中的任何⼀个字符, 都会校验失败, 所以令牌⽆法篡改
学习令牌的使⽤之后, 接下来我们通过令牌来完成⽤⼾的登录
1. 登陆⻚⾯把⽤⼾名密码提交给服务器.
2. 服务器端验证⽤⼾名密码是否正确, 如果正确, 服务器⽣成令牌, 下发给客⼾端.
3. 客⼾端把令牌存储起来(⽐如Cookie, local storage等), 后续请求时, 把token发给服务器
4. 服务器对令牌进⾏校验, 如果令牌正确, 进⾏下⼀步操作
5.4 约定前后端交互接⼝
[请求]
/user/login
username=shenmengyao&password=111111
[响应]
{
"code": 200,
"msg": "",
"data":
"eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwidXNlcm5hbWUiOiJ6aGFuZ3NhbiIsImlhdCI6MTY5ODM5N
zg2MCwiZXhwIjoxNjk4Mzk5NjYwfQ.oxup5LfpuPixJrE3uLB9u3q0rHxxTC8_AhX1QlYV--E"
}
//验证成功, 返回token, 验证失败返回 ""
5.5 实现服务器代码
创建JWT⼯具类
package com.example.spring_blog_24_9_8.utils;
import com.example.spring_blog_24_9_8.constants.Constant;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.JwtParser;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
import java.security.Key;
import java.util.Date;
import java.util.Map;
@Slf4j
public class JwtUtils {
//过期时间: 1小时的毫秒数
private final static long EXPIRATION_DATE = 600000 * 60 * 1000;
private final static String secretString = "2LT0G2Og7zG9aOPJR+Y4cLMGicyUoRVPS4J0xRlpZzg=";
private final static Key key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretString));
//生成令牌
public static String genToken(Map<String, Object> claim){
return Jwts.builder()
.setClaims(claim)
.setExpiration(new Date(System.currentTimeMillis()+EXPIRATION_DATE))
.signWith(key)
.compact();
}
/**
* 解析令牌
* @param token
* @return
*/
public static Claims parseToken(String token){
JwtParser build = Jwts.parserBuilder().setSigningKey(key).build();
Claims body = null;
try {
body = build.parseClaimsJws(token).getBody();
} catch (ExpiredJwtException e) {
log.error("token过期, 校验失败, token:",token);
} catch (Exception e) {
log.error("token校验失败, token:",token);
}
return body;
}
//校验令牌
public static boolean checkToken(String token){
Claims body = parseToken(token);
if (body==null){
System.out.println("555");
return false;
}
System.out.println("444");
return true;
}
public static Integer getUserIdFromToken(String token){
Claims body = parseToken(token);
if (body!=null){
System.out.println("222");
return (Integer) body.get("id");
}
System.out.println("111");
return null;
}
}
创建 UserController
package com.example.spring_blog_24_9_8.controller;
import com.example.spring_blog_24_9_8.model.Result;
import com.example.spring_blog_24_9_8.model.User;
import com.example.spring_blog_24_9_8.service.UserService;
import com.example.spring_blog_24_9_8.utils.JwtUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@RequestMapping("/user")
@RestController
public class UserController {
@Autowired
private UserService userService;
@RequestMapping("/login")
public Result login(HttpServletRequest request, HttpServletResponse
response, String username, String password){
if (!StringUtils.hasLength(username) ||
!StringUtils.hasLength(password)){
log.error("username:"+username+",password:"+password);
return Result.fail(-1, "⽤⼾名或密码为空");
}
//判断账号密码是否正确
User user = userService.getUserInfo(username);
if (user==null || !user.getPassword().equals(password)){
return Result.fail(-1, "⽤⼾名或密码错误");
}
//登录成功, 返回token
Map<String , Object> claims = new HashMap<>();
claims.put("id", user.getId());
claims.put("username", user.getUserName());
String token = JwtUtils.genToken(claims);
System.out.println("⽣成token:"+ token);
return Result.success(token);
}
}
userservice:
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public User getUserInfo(String username){
return userMapper.selectByName(username);
}
}
5.6 实现客⼾端代码
修改 login.html, 完善登录⽅法 前端收到token之后, 保存在localstorage中
function login() {
//发送ajax请求, 获得token
$.ajax({
type:"post",
url: "/user/login",
data:{
"username": $("#username").val(),
"password": $("#password").val()
},
success:function(result){
if(result.code==200 && result.data != ""){
//存储token
localStorage.setItem("user_token", result.data);
location.href = "blog_list.html";
}else{ // 自行补充完整
alert("用户名或密码错误")
}
}
});
}
local storage相关操作
存储数据
localStorage.setItem("user_token","value");
读取数据
localStorage.getItem("user_token");
删除数据
localStorage.removeItem("user_token");
运行服务器,访问登录页面,输入正确的账号和密码:
输入错误的账号和密码,查看返回的结果:
6. 强制登录
当⽤⼾访问 博客列表和博客详情⻚ 时, 如果⽤⼾当前尚未登陆, 就⾃动跳转到登陆⻚⾯. 我们可以采⽤拦截器来完成, token通常由前端放在header中, 我们从header中获取token, 并校验 token是否合法
6.1 添加拦截器
package com.example.spring_blog_24_9_8.config;
import com.example.spring_blog_24_9_8.utils.JwtUtils;
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Slf4j
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse
response, Object handler) throws Exception {
//从header中获取token
String jwtToken = request.getHeader("user_token");
log.info("从header中获取token:{}",jwtToken);
//验证⽤⼾token
Claims claims = JwtUtils.parseToken(jwtToken);
if (claims!=null){
log.info("令牌验证通过, 放⾏");
return true;
}
response.setStatus(401);
return true;
}
}
package com.example.spring_blog_24_9_8.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.Arrays;
import java.util.List;
@Configuration
public class AppConfig implements WebMvcConfigurer {
private final List excludes = Arrays.asList(
"/**/*.html",
"/blog-editormd/**",
"/css/**",
"/js/**",
"/pic/**",
"/login"
);
@Autowired
private LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/**")
.excludePathPatterns(excludes);
}
}
6.2 实现客⼾端代码
1. 前端请求时, header中统⼀添加token, 可以写在common.js中
$(document).ajaxSend(function (e, xhr, opt) {
var user_token = localStorage.getItem("user_token");
xhr.setRequestHeader("user_token", user_token);
});
ajaxSend() ⽅法在 AJAX 请求开始时执⾏函数:
event - 包含 event 对象
xhr - 包含 XMLHttpRequest 对象
options - 包含 AJAX 请求中使⽤的选项
同时我们要注意引入common.js;
<script src="js/jquery.min.js"></script>
<script src="js/common.js"></script>
<script>
getBlogList();
function getBlogList(){
$.ajax({
type: "get",
url: "/blog/getList",
success:function (result){
if(result.data != 0 && result.data.length>0 && result.code == 200 ){
var finalHtml = "";
for(var blog of result.data){
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>';
}
$(".right").html(finalHtml);
}
},
error:function (err){
console.log(err);
if(err!=null && err.status==401){
alert("⽤⼾未登录, 即将跳转到登录⻚!");
//已经被拦截器拦截了, 未登录
location.href ="blog_login.html";
}
}
})
}
</script>
2. 修改 blog_datail.html
访问⻚⾯时, 添加失败处理代码
使⽤ location.href 进⾏⻚⾯跳转;
error:function (err){
console.log(err);
if(err!=null && err.status==401){
alert("⽤⼾未登录, 即将跳转到登录⻚!");
//已经被拦截器拦截了, 未登录
location.href ="blog_login.html";
}
}
修改 blog_list.html的部分代码如上故事;
运行程序:浏览器访问博客列表页面:
结果进入到登录页面;
7. 实现显⽰⽤⼾信息
⽬前⻚⾯的⽤⼾信息部分是写死的. 形如:
我们期望这个信息可以随着⽤⼾登陆⽽发⽣改变. :
如果当前⻚⾯是博客列表⻚, 则显⽰当前登陆⽤⼾的信息.
如果当前⻚⾯是博客详情⻚, 则显⽰该博客的作者⽤⼾信息.
注意: 当前我们只是实现了显⽰⽤⼾名, 没有实现显⽰⽤⼾的头像以及⽂章数量等信息.
7.1 约定前后端交互接⼝
在博客列表⻚, 获取当前登陆的⽤⼾的⽤⼾信息.
[请求]
/user/getUserInfo
[响应]
{
userId: 1,
username: test
...
}
在博客详情⻚, 获取当前⽂章作者的⽤⼾信息
[请求]
/user/getAuthorInfo?blogId=1
[响应]
{
userId: 1,
username: test
}
7.2 实现服务器代码
在 UserController添加代码
/**
* 获取当前登录⽤⼾信息
* @param request
* @return
*/
@RequestMapping("/getUserInfo")
public User getUserInfo(HttpServletRequest request){
//从header中获取token
String jwtToken = request.getHeader("user_token");
//从token中获取⽤⼾id
Integer userId = JwtUtils.getUserIdFromToken(jwtToken);
//根据Userid获取⽤⼾信息
if (userId!=null){
return userService.selectById(userId);
}
return null;
}
/**
* 获取博客作者信息
* @param blogId
* @return
*/
@RequestMapping("/getAuthorInfo")
public Result getAuthorInfo(Integer blogId) {
if (blogId == null && blogId < 1) {
return Result.fail(-1, "博客ID不正确");
}
//根据博客id, 获取作者相关信息
User user = userService.selectAuthorByBlogId(blogId);
return Result.success(user);
}
在UserService中添加代码
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
@Autowired
private BlogMapper blogMapper;
public User getUserInfo(String username){
return userMapper.selectByName(username);
}
public User selectById(Integer Id){
return userMapper.selectById(Id);
}
public User selectAuthorByBlogId(Integer blogId) {
//1. 根据博客ID, 获取作者ID
//2. 根据作者ID, 获取作者信息
Blog blog = blogMapper.selectById(blogId);
if (blog==null && blog.getUserId()<1){
return null;
}
User user = userMapper.selectById(blog.getUserId());
return user;
}
}
usermapper代码:
@Mapper
public interface UserMapper {
@Select("select id, user_name, password, github_url, delete_flag, create_time " +
"from user where id = #{id}")
User selectById(Integer id);
@Select("select id, user_name, password, github_url, delete_flag, create_time " +
"from user where user_name = #{userName}")
User selectByName(String name);
}
7.3实现客⼾端代码
修改 blog_list.html和 blog_detail.html代码
在响应回调函数中, 根据响应中的⽤⼾名, 更新界⾯的显⽰.
function getUserInfo(url) {
$.ajax({
type: "get",
url: url,
success: function (result) {
if (result.code == 200 && result.data != null) {
$(".left .card h3").text(result.data.userName);
$(".left .card a").attr("href", result.data.githubUrl);
}
}
});
}
由于该部分添加的代码一样,所以进行代码整合: 提取common.js,即将该部分代码放入到common.js中
function getUserInfo(url) {
$.ajax({
type: "get",
url: url,
success: function (result) {
if (result.code == 200 && result.data != null) {
$(".left .card h3").text(result.data.userName);
$(".left .card a").attr("href", result.data.githubUrl);
}
}
});
}
分别在两个页面的<script>中引⼊common.js;
blog_list.html 代码修改:
<script src="js/common.js"></script>
var userUrl= "/user/getUserInfo";
getUserInfo(userUrl);
blog_detail.html 代码修改:
<script src="js/common.js"></script>
//获取作者信息
var userUrl= "/user/getAuthorInfo"+location.search;
getUserInfo(userUrl);
效果展示:成功登录到博客列表页;
当前是用户沈梦瑶的博客列表页:
当前是用户袁一琦的博客列表页:
用户沈梦瑶点击袁一琦写的博客的详情页,显示的是作者袁一琦的信息:
在该页面点击github地址,我们的页面跳转到袁一琦的gitee的首页,也就是我的码云首页,如下所示:
8. 实现用户退出
前端直接清除掉token即可.
实现客⼾端代码
<注销>链接已经提前添加了onclick事件 ,在common.js中完善logout⽅法
function logout(){
localStorage.removeItem("user_token");
location.href = "blog_login.html";
}
点击下图注销:
返回到登录页面:
localstorage中的令牌也被清除掉了;
9. 实现发布博客
9.1 约定前后端交互接⼝
[请求]
/blog/add
title=标题&content=正⽂...
[响应]
{
"code": 200,
"msg": "",
"data": true
}
//true 成功
//false 失败
9.2 实现服务器代码
修改 BlogController, 新增 add ⽅法.
@RequestMapping("/add")
public Result insert(String title, String content, HttpServletRequest request){
//获取当前登录⽤⼾ID
String jwtToken = request.getHeader("user_token");
Integer loginUserId = JwtUtils.getUserIdFromToken(jwtToken);
if (loginUserId==null || loginUserId<1){
return Result.fail(-1,"⽤⼾未登录");
}
Blog blog = new Blog();
blog.setUserId(loginUserId);
blog.setTitle(title);
blog.setContent(content);
blogService.insertBlog(blog);
return Result.success(true);
}
BlogService 添加对应的处理逻辑
public int insertBlog(Blog record){
return blogMapper.insertBlog(record);
}
9.2.1 editor.md 简单介绍
editor.md 是⼀个开源的⻚⾯ markdown 编辑器组件. 官⽹参⻅: http://editor.md.ipandao.com/ 代码: https://pandao.github.io/editor.md/ 使⽤⽰例
<link rel="stylesheet" href="editormd/css/editormd.css" />
<div id="test-editor">
<textarea style="display:none;">### 关于 Editor.md
**Editor.md** 是⼀款开源的、可嵌⼊的 Markdown 在线编辑器(组件),基于 CodeMirror、
jQuery 和 Marked 构建。
</textarea>
</div>
<script src="https://cdn.bootcss.com/jquery/1.11.3/jquery.min.js"></script>
<script src="editormd/editormd.min.js"></script>
<script type="text/javascript">
$(function() {
var editor = editormd("test-editor", {
// width : "100%",
// height : "100%",
path : "editormd/lib/"
});
});
</script>
使⽤时引⼊对应依赖就可以了
"test-editor" 为 markdown编辑器所在的div的id名称
path为 editor.md依赖所在的路径
9.3 实现客⼾端代码
修改 blog_edit.html • 完善submit⽅法
function submit() {
$.ajax({
type:"post",
url: "/blog/add",
data:{
title:$("#title").val(),
content:$("#content").val()
},
success:function(result){
if(result.code==200 && result.data==true){
location.href = "blog_list.html";
}
//结果为false, 下面自己补充
else{
alert(result.msg);
return;
}
},
error:function(error){
if(error!=null && error.status==401){
alert("⽤⼾未登录, 登录后再进⾏对应操作")
}
}
});
}
如下图所示,我们发现我们的blog在发布之后,在博客列表和内容细节页正文部分带有一些符号:
修改详情⻚⻚⾯显⽰ ,即详情⻚会显⽰markdown的格式符号, 我们对⻚⾯进⾏也下处理
1. 修改 html 部分, 把博客正⽂的 div 标签, 改成如下内容,并且加上 style="background-color: transparent;"
<!-- 右侧内容详情 -->
<div class="content">
<div class="title"></div>
<div class="date"></div>
<div class="detail" id="detail" style="background-color: transparent;">
</div>
<div class="operating">
<button onclick="window.location.href='blog_update.html'">编辑</button>
<button>删除</button>
</div>
</div>
2. 修改博客正⽂内容的显⽰
$.ajax({
type: "get",
url: "/blog/getBlogDetail" + location.search,
success: function (result){
console.log(result);
if(result.code == 200 && result.data != null){
$(".title").text(result.data.title);
$(".date").text(result.data.createTime);
// $(".detail").text(result.data.content);
editormd.markdownToHTML("detail", {
markdown: result.data.content,
});
}
},
我们之前存在问题的页面被成功如下修正;
10. 实现删除/编辑博客
进⼊⽤⼾详情⻚时, 如果当前登陆⽤⼾正是⽂章作者, 则在导航栏中显⽰ [编辑] [删除] 按钮, ⽤⼾点击时 则进⾏相应处理.
需要实现两件事:
1、 判定当前博客详情⻚中是否要显⽰[编辑] [删除] 按钮
2、实现编辑/删除逻辑. 删除采⽤逻辑删除, 所以和编辑其实为同⼀个接⼝
10.1 约定前后端交互接⼝
1. 判定是否要显⽰[编辑] [删除] 按钮
即修改之前的获取博客信息的接⼝, 在响应中加上⼀个字段.
loginUser 为 1 表⽰当前博客就是登陆⽤⼾⾃⼰写的.
[请求]
/blog/getBlogDetail?blogId=1
[响应]
{
"code": 200,
"msg": "",
"data": {
"id": 1,
"title": "第⼀篇博客",
"content": "111我是博客正⽂我是博客正⽂我是博客正⽂",
"userId": 1,
"loginUser": 1
"deleteFlag": 0,
"createTime": "2023-10-21 16:56:57",
"updateTime": "2023-10-21T08:56:57.000+00:00"
}
}
2. 修改博客
[请求]
/blog/update
[参数]
Content-Type: application/json
{
"title": "测试修改⽂章",
"content": "在这⾥写下⼀篇博客",
"blogId": "4"
}
[响应]
{
"code": 200,
"msg": "",
"data": true
}
3、删除博客
[请求]
/blog/delete?blogId=1
[响应]
{
"code": 200,
"msg": "",
"data": true
}
10.2 实现服务器代码
1. 给Blog类新增⼀个字段
@Data
public class Blog {
private Integer id;
private String title;
private String content;
private Integer userId;
private Integer deleteFlag;
private Date createTime;
private Date updateTime;
private Integer loginUser;
public String getCreateTime(){
return DateUtils.format(createTime);
}
}
2. 修改 BlogController 其他代码不变. 只处理 "getBlogDeatail" 中的逻辑.从请求中获取登录用户的userid,如果登录用户和文章的作者是同一个人的话就给新变量赋值为1;
@RequestMapping("/getBlogDetail")
public Blog getBlogDetail(Integer blogId,HttpServletRequest request){
Blog blog = blogService.getBlogDetail(blogId);
String jwtToken = request.getHeader("user_token");
Integer loginUserId = JwtUtils.getUserIdFromToken(jwtToken);
if (loginUserId!=null && blog.getUserId()==loginUserId){
blog.setLoginUser(1);
}
return blog;
}
3. 修改 BlogController
增加 update/delete ⽅法, 处理修改/删除逻辑.
@RequestMapping("/update")
public Result update(@RequestBody Blog blog){
blogService.updateBlog(blog);
return Result.success(true);
}
@RequestMapping("/delete")
public boolean delete(Integer blogId){
Blog blog = new Blog();
blog.setId(blogId);
blog.setDeleteFlag(1);
blogService.updateBlog(blog);
return true;
}
public Integer updateBlog(Blog blog) {
return blogMapper.updateBlog(blog);
}
10.3 实现客⼾端代码
1. 判断是否显⽰[编辑] [删除]按钮
//获取博客详情
$.ajax({
type: "get",
url: "/blog/getBlogDetail" + location.search,
success: function (result){
console.log(result);
if(result.code == 200 && result.data != null){
$(".title").text(result.data.title);
$(".date").text(result.data.createTime);
// $(".detail").text(result.data.content);
editormd.markdownToHTML("detail", {
markdown: result.data.content,
});
// 是否显示编辑/删除按钮
if(result.data.loginUser){
console.log("显示编辑/删除");
var html = "";
html += '<div class="operating">';
html += '<button onclick="window.location.href=\'blog_update.html'+location.search+'\'">编辑</button>';
html += '<button onclick="deleteBlog()">删除</button>';
html += '</div>';
$(".content").append(html);
}
}
},
error:function (error){
console.log(error);
if(error!=null && error.status==401){
alert("⽤⼾未登录, 即将跳转到登录⻚!");
//已经被拦截器拦截了, 未登录
location.href ="blog_login.html";
}
}
})
//显⽰当前登录⽤⼾的信息
function deleteBlog() {
if(confirm("确定删除这篇博客吗?")){
$.ajax({
type:"post",
url:"/blog/delete"+location.search,
success:function(result){
if(result.code==200 && result.data==true){
alert("删除成功, 即将跳转⾄博客列表⻚");
location.href = "blog_list.html";
}else{
alert("删除失败");
}
}
});
}
}
编辑博客逻辑:
修改blog_update.html ⻚⾯加载时, 请求博客详情
function getBlogInfo() {
$.ajax({
type:"get",
url:"/blog/getBlogDetail"+location.search,
success:function(result){
if (result.code == 200 && result.data != null) {
console.log(result);
$("#blogId").val(result.data.id);
$("#title").val(result.data.title);
// $("#content").val(result.data.content);
editormd("editor", {
width : "100%",
height : "550px",
path: "blog-editormd/lib/",
onload : function() {
this.watch()
this.setMarkdown(result.data.content);
}
});
}
},
error: function (err) {
if (err != null && err.status == 401) {
alert("⽤⼾未登录, 即将跳转到登录⻚!");
//已经被拦截器拦截了, 未登录
location.href = "/blog_login.html";
}
}
});
}
getBlogInfo();
已经在getBlogInfo进⾏markdown编辑器的渲染了, 所以把以下代码删除
$(function () {
var editor = editormd("editor", {
width: "100%",
height: "550px",
path: "blog-editormd/lib/"
});
});
完善发表博客的逻辑
function submit() {
$.ajax({
type: "post",
url: "/blog/update",
contentType: "application/json",
data: JSON.stringify({
"title": $("#title").val(),
"content": $("#content").val(),
"id": $("#blogId").val()
}),
success: function (result) {
if (result != null && result.code == 200 && result.data == true) {
location.href = "blog_list.html";
} else {
alert(result.msg);
return;
}
},
error: function (error) {
if (error != null && error.status == 401) {
alert("⽤⼾未登录, 登录后再进⾏对应操作");
}
}
});
}
运行程序:
修改之前:
修改之后:
删除之前:
删除之后:
点击删除按钮:
点击确定:
进入到博客列表页:
由此删除成功;
ps:关于博客系统的内容就到这里了,谢谢观看!!!