基于spring的博客系统(总)

news2024/11/25 15:39:23

        通过前⾯课程的学习, 我们掌握了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 + '">查看全文&gt;&gt;</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 Tokens - jwt.io

        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 + '">查看全文&gt;&gt;</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:关于博客系统的内容就到这里了,谢谢观看!!!

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

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

相关文章

零工市场小程序是灵活就业的新趋势?

人力资源社会保障部曾发文《人力资源社会保障部 民政部 财政部 住房和城乡建设部 国家市场监管总局关于加强零工市场建设 完善求职招聘服务的意见》。 找零工在传统情况下会有比较多的困难&#xff0c;比如能能够掌握的信息较少、不知道工作单位是否靠谱等等的问题&#xff0c…

心觉:如何打破用脑学习的瓶颈?教你用心学习,实现真正蜕变!

Hi&#xff0c;我是心觉&#xff0c;与你一起玩转潜意识、脑波音乐和吸引力法则&#xff0c;轻松掌控自己的人生&#xff01; 挑战每日一省写作167/1000天 我们经常听到父母对孩子说要“用心学习&#xff0c;用心学习” 大概意思是告诉孩子学习的时候要专注&#xff0c;要认真…

【9月持续更新】国内ChatGPT-4中文镜像网站大全

一、国内大模型与ChatGPT的区别 &#x1f9e0; 国内大模型&#xff1a;专注于国内市场&#xff0c;支持本土企业及用户&#xff0c;适用于中文语境下的客服、教育、内容生成等应用场景。ChatGPT&#xff1a;全球适用性强&#xff0c;但在中文环境下的本地化程度不如国内大模型…

Arm GIC-v3中断原理及验证(通过kvm-unit-tests)

一、参考连接 gic-v3相关原理可参考https://zhuanlan.zhihu.com/p/520133301 本文主要通过开源测试工具kvm-unit-tests&#xff0c;针对GIC的中断进行一系列验证&#xff0c;这样可以直入中断底层&#xff0c;熟悉整个原理。 kvm-unit-tests官网为kvm-unit-tests / KVM-Unit…

『 Linux 』协议的定制

文章目录 协议的概念序列化和反序列化网络计算器套接字接口的封装服务端大致框架协议的定制Request的序列化与反序列化Response的序列化与反序列化报头的封装的解包网络服务服务端的封装已提取报文的移除客户端的封装客户端的调用服务端接收多个请求 JSON 自动序列化反序列化使…

QT打开摄像头采集

QT打开摄像头采集 今天好不容易把opencv的环境装好&#xff0c;然后想学习一下人脸识别的功能&#xff0c;但是在图书馆坐了4个多小时了&#xff0c;屁股疼就先写个摄像头采集的功能&#xff0c;明天继续学习吧&#xff0c;废话不多&#xff0c;嚼个奶片开始发车&#xff01;&…

3.安卓逆向-java语言控制流和数据类型

免责声明&#xff1a;内容仅供学习参考&#xff0c;请合法利用知识&#xff0c;禁止进行违法犯罪活动&#xff01; 内容参考于&#xff1a;图灵Python学院 上一个内容&#xff1a;2.安卓逆向-初识java语言 上一个内容里写了编写java语言代码的环境搭建&#xff0c;也就是下载…

你的个人生成式AI创新课程

我曾经写过许多博客文章&#xff0c;讨论如何使用生成式AI&#xff08;GenAI&#xff09;工具&#xff0c;例如OpenAI ChatGPT、微软Copilot和Google Gemini来提升专业效率和个人发展。然而&#xff0c;我们必须从仅仅使用这些GenAI工具来提高生产力的思维模式&#xff0c;转变…

echarts 关系图 legend 图例不显示问题

关系图希望显示图例&#xff0c;如下效果&#xff1a; 按照常规echarts图表的配置&#xff0c;配置完legend&#xff0c;图例居然不显示。 legend: {show: true,icon: circle,left: left }经过一番查找和思考&#xff0c;原来是因为series配置中没有设置categories项&#xf…

通信工程学习:什么是SLF签约数据定位功能

SLF&#xff1a;签约数据定位功能 SLF&#xff08;Subscription Locator Function&#xff09;即签约数据定位功能&#xff0c;是IMS&#xff08;IP Multimedia Subsystem&#xff09;IP多媒体子系统体系结构中的一个关键组成部分。其主要功能是在网络运营商部署了多个独立可寻…

Android平台RTMP|RTSP播放器如何回调YUV或RGB数据?

技术选型 我们知道&#xff0c;Android平台一般RTMP|RTSP播放器通常不直接提供回调YUV或RGB数据的功能。如果播放端有视觉分析或类似的需求&#xff0c;需要播放端&#xff0c;能支持YUV或ARG的数据回调&#xff0c;一般来说&#xff0c;可参考的方法如下&#xff1a; 1. 使用…

尚硅谷的尚乐代驾项目

项目源码 乐尚代驾项目: 重做乐尚代驾项目 (gitee.com) 一 项目介绍 1 介绍 【**乐尚代驾**】代驾是一种新型的出行服务模式&#xff0c;主营业务&#xff1a;酒后代驾、商务代驾、长途代驾&#xff0c;其主要特点是通过线上平台为用户提供代驾服务&#xff0c;伴随中国家…

Node.js学习记录(二)

目录 一、express 1、初识express 2、安装express 3、创建并启动web服务器 4、监听 GET&POST 请求、响应内容给客户端 5、获取URL中携带的查询参数 6、获取URL中动态参数 7、静态资源托管 二、工具nodemon 三、express路由 1、express中路由 2、路由的匹配 3、…

Navicat备份数据库

Navicat备份数据库 &#x1f4d4; 千寻简笔记介绍 千寻简文库已开源&#xff0c;Gitee与GitHub搜索chihiro-doc&#xff0c;包含笔记源文件.md&#xff0c;以及PDF版本方便阅读&#xff0c;文库采用精美主题&#xff0c;阅读体验更佳&#xff0c;如果文章对你有帮助请帮我点一…

【Hot100】LeetCode—300. 最长递增子序列

目录 1- 思路题目识别动规五部曲 2- 实现⭐最长递增子序列——题解思路 3- ACM 实现 原题链接&#xff1a;300. 最长递增子序列 1- 思路 题目识别 识别1 &#xff1a;给出一个数组输入 nums识别2&#xff1a;严格递增的子序列&#xff0c;子序列可以是不连续的 动规五部曲 …

数据处理与统计分析篇-day02-Linux进阶

day02-Linux进阶 补充昨日内容 Linux基础 文件编辑 命令模式 编辑模式 esc回到命令模式 正常编辑 底行(底线)模式 查看Linux命令帮助文档 # --help 可以查看命令的详细信息 命令名 --help ​ # 例如: ls --help ​ # man 可以查看命令的主要(简单)信息 man 命令名…

树莓派替代台式计算机?树莓派上七款最佳的轻量级操作系统!

​Raspberry Pi 是一款超级实惠的单板计算机&#xff08;SBC&#xff09;&#xff0c;可用于各种不同的项目。Raspberry Pi 的一些最流行用途包括将其变成媒体播放器或模拟机器。鉴于该系统的多功能性&#xff0c;有人想知道它是否可以替代台式计算机。好吧&#xff0c;它可以&…

【CTF Web】BUUCTF Upload-Labs-Linux Pass-13 Writeup(文件上传+PHP+文件包含漏洞+PNG图片马)

Upload-Labs-Linux 1 点击部署靶机。 简介 upload-labs是一个使用php语言编写的&#xff0c;专门收集渗透测试和CTF中遇到的各种上传漏洞的靶场。旨在帮助大家对上传漏洞有一个全面的了解。目前一共20关&#xff0c;每一关都包含着不同上传方式。 注意 1.每一关没有固定的…

Postman做接口测试时一些实用的操作

Postman 之前是作为Chrome 的一个插件&#xff0c;现在要下载应用才能使用。 以下是postman 的界面&#xff1a; 各个功能区的使用如下&#xff1a; 快捷区&#xff1a; 快捷区提供常用的操作入口&#xff0c;包括运行收藏夹的一组测试数据&#xff0c;导入别人共享的收藏夹测…

论文解读:利用大模型进行基于上下文的OCR校正

论文地址&#xff1a;https://arxiv.org/pdf/2408.17428 背景概述 研究问题&#xff1a;这篇文章要解决的问题是如何利用预训练的语言模型&#xff08;LMs&#xff09;来改进光学字符识别&#xff08;OCR&#xff09;的质量&#xff0c;特别是针对报纸和期刊等复杂布局的文档。…