【SpringBoot 项目实战教程】博客系统(中)(含源码+全网最细讲解)

1. 实现登录

传统做法:

  • 登录页面把用户名和密码提交给服务器
  • 服务器端验证用户名和密码是否正确,并返回校验结果
  • 如果密码正确,在服务器端创建Session并存储,通过Cookie把SessionId返回给浏览器
  • 用户再次访问的时候携带着SessionId,后端从Cookie中获取SessionId,根据SessionId获取Session

image.png
存在问题:

  1. Session存储在服务端的内存中,如果服务器重启,session就丢失了。比如:用户刚登录成功,服务器就进行重启,此时Session丢失,客户端需要重新登录,用户体验不好。
  2. 多机部署的情况下无法直接使用Session。

我们开发的项⽬, 很少会部署在⼀台机器上, 容易发⽣单点故障。(单点故障: ⼀旦这台服务器挂了, 整个应用都没法访问了)。 所以通常情况下, ⼀个Web应⽤会部署在多个服务器上, 通过Nginx等进⾏负载均衡。此时, 来⾃⼀个用户的请求就会被分发到不同的服务器上。
image.png
假如我们使⽤Session进⾏会话跟踪, 我们来思考如下场景:

  1. 用户登录:用户登录请求, 经过负载均衡, 把请求转给了第⼀台服务器, 第⼀台服务器进⾏账号密码验证, 验证成功后, 把Session存在了第⼀台服务器上。
  2. 查询操作:用户登录成功之后, 携带Cookie(⾥⾯有SessionId)继续执行查询操作, 比如查询博客列表。此时请求转发到了第⼆台机器, 第⼆台机器会先进行权限验证操作(通过SessionId验证用户是否登录), 此时第⼆台机器上没有该用户的Session, 就会出现问题, 提示用户未登录。

image.png
解决办法:

  1. 数据共享,把Session放到同一个地方,比如redies…
  2. 把数据放到客户端(令牌技术

用户身份(token 令牌)存在客户端,服务器可以对用户身份进行校验。用户身份由服务器发放,交给客户端,客户端访问时,携带着身份证,服务器进行校验。(身份证由公安机关发放,存到个人手里,当去景区等地方要验证身份时,拿出身份证,景区通过公安系统对身份进行校验。个人就是客户端,景区就是服务器)
image.png
服务器具备⽣成令牌和验证令牌的能力。
我们使用令牌技术, 继续思考上述场景:

  1. 用户登录:用户登录请求, 经过负载均衡, 把请求转给了第⼀台服务器, 第⼀台服务器进⾏账号密码验证, 验证成功后, ⽣成⼀个令牌, 并返回给客户端。
  2. 客户端收到令牌之后, 把令牌存储起来。可以存储在Cookie中, 也可以存储在其他的存储空间(比如localStorage)
  3. 查询操作:用户登录成功之后, 携带令牌继续执⾏查询操作, ⽐如查询博客列表。此时请求转发到了第三台机器, 第三台机器会先进⾏权限验证操作。服务器验证令牌是否有效, 如果有效, 就说明用户已经执行了登录操作, 如果令牌是⽆效的, 就说明用户之前未执⾏登录操作。

令牌的优缺点:
优点:

  • 解决了集群环境下的认证问题
  • 减轻服务器的存储压⼒(⽆需在服务器端存储)

缺点:需要⾃⼰实现(包括令牌的⽣成, 令牌的传递, 令牌的校验)

1.1 JWT令牌

令牌本质就是⼀个字符串, 他的实现⽅式有很多, 我们采⽤⼀个JWT令牌来实现。
JWT组成:
JWT由三部分组成, 每部分中间使⽤点 (.) 分隔,⽐如:aaaaa.bbbbb.cccc

  • Header(头部):头部包括令牌的类型(即JWT)及使⽤的哈希算法(如HMAC SHA256或RSA)
  • Payload(负载):负载部分是存放有效信息的地⽅, ⾥⾯是⼀些⾃定义内容。

比如{“userId”:“123”,“userName”:“zhangsan”} , 也可以存在JWT提供的现场字段, ⽐如exp(过期时间戳)等。此部分不建议存放敏感信息,因为此部分可以解码还原原始内容。

  • Signature(签名):此部分⽤于防止JWT内容被篡改, 确保安全性。防止被篡改, 而不是防止被解析。

JWT之所以安全, 就是因为最后的签名。JWT当中任何⼀个字符被篡改, 整个令牌都会校验失败。就好⽐我们的⾝份证, 之所以能标识⼀个⼈的⾝份, 是因为他不能被篡改, ⽽不是因为内容加密。(任何⼈都可以看到⾝份证的信息, JWT 也是)。
image.png
对上⾯部分的信息, 使用Base64Url 进⾏编码, 合并在⼀起就是jwt令牌。Base64是编码方式,⽽不是加密方式。

1.2 JWT令牌的生成和校验

  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>
  1. 使⽤Jar包中提供的API来完成JWT令牌的⽣成和校验

生成令牌:

public class JWTUtilsTest {
    // 过期时间:1小时的毫秒数
    private final static long EXPIRATION_DATE = 60 * 60 * 1000;
    // 签名
    private final static String secretString = "JxSPPTQPYO5n5jPah/MTzpYcM2Ow4/YbA4i91myjmYg=";
    // 生成key
    private final static Key key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretString));

    // 生成令牌
    @Test
    public void genToken() {
        // 自定义信息

        Map<String, Object> claim = new HashMap<>();
        claim.put("id",5);
        claim.put("name","zhangsan");

        String token = Jwts.builder()
                .setClaims(claim)// 自定义内容(载荷)
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_DATE))// 过期时间
                .signWith(key)// 签名算法
                .compact();
        System.out.println(token);
    }
}

注意: 对于密钥key有⻓度和内容有要求, 建议使用
io.jsonwebtoken.security.Keys#secretKeyFor(signaturealgalgorithm)⽅法来创建⼀个密钥

// 生成key
@Test
public void genKey() {
    // 随机生成key
    SecretKey secretKey = Keys.secretKeyFor(SignatureAlgorithm.HS256);
    // 拿到这个key的签名
    String encode = Encoders.BASE64.encode(secretKey.getEncoded());
    System.out.println(encode);
}

运行程序:
image.png
输出的内容, 就是JWT令牌,通过点(.)对三个部分进⾏分割, 我们把⽣成的令牌通过官⽹进⾏解析, 就可以看到我们存储的信息了:
image.png

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

校验令牌:
完成了令牌的⽣成, 我们需要根据令牌, 来校验令牌的合法性(以防客户端伪造)

// 校验令牌
@Test
public void parseToken() {
    String token = "eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiemhhbmdzYW4iLCJpZCI6NSwiZXhwIjoxNzE0MjE1OTkyfQ.apLjuz_lu2lU5U9QPrQyeOKzmGqTTJ-l9aqJyWvSbas";
    JwtParser build = Jwts.parserBuilder().setSigningKey(key).build();// 创建解析器, 设置签名密钥
    // 解析token
    Claims body = null;
    try {
        body = build.parseClaimsJws(token).getBody();

    } catch (Exception e) {
        System.out.println("令牌校验失败");
    }
    System.out.println(body);
}

运行程序,查看结果:
image.png
令牌解析后, 我们可以看到⾥⾯存储的信息,如果在解析的过程当中没有报错,就说明解析成功了。令牌解析时, 也会进⾏时间有效性的校验, 如果令牌过期了, 解析也会失败。修改令牌中的任何⼀个字符, 都会校验失败, 所以令牌⽆法篡改。
学习令牌的使⽤之后, 接下来我们通过令牌来完成用户的登录:

  1. 登录页⾯把用户名密码提交给服务器
  2. 服务器端验证用户名密码是否正确, 如果正确, 服务器⽣成token, 返回给客户端
  3. 客户端把token存储起来(⽐如Cookie, local storage等), 后续请求时, 把token发给服务器
  4. 服务器对token进⾏校验, 如果token正确, 进⾏下⼀步操作

image.png

1.3 约定前后端交互接口:

[请求]
/user/login
userName=test&password=123
[响应]
{
  "code": 200,
  "msg": "",
  "data":
  "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwidXNlcm5hbWUiOiJ6aGFuZ3NhbiIsImlhdCI6MTY5ODM5N
  zg2MCwiZXhwIjoxNjk4Mzk5NjYwfQ.oxup5LfpuPixJrE3uLB9u3q0rHxxTC8_AhX1QlYV--E"
}
//验证成功, 返回token, 验证失败返回错误信息

1.4 实现服务器代码:

创建JwtUtils工具类:

@Slf4j
public class JwtUtils {
    // 过期时间:1小时的毫秒数
    private final static long EXPIRATION_DATE = 60 * 60 * 1000;
    // 签名
    private final static String secretString = "JxSPPTQPYO5n5jPah/MTzpYcM2Ow4/YbA4i91myjmYg=";
    // 生成key
    private final static Key key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretString));

    // 生成令牌
    public static String genToken(Map<String, Object> claim) {
        // 自定义信息
        String token = Jwts.builder()
                .setClaims(claim)// 自定义内容(载荷)
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_DATE))// 过期时间
                .signWith(key)// 签名算法
                .compact();
        return token;
    }


    // 校验令牌
    public static boolean parseToken(String token) {
        JwtParser build = Jwts.parserBuilder().setSigningKey(key).build();// 创建解析器, 设置签名密钥
        // 解析token
        Claims body = null;
        try {
            body = build.parseClaimsJws(token).getBody();
        } catch (ExpiredJwtException e) {
            log.error("token过期,校验失败,token:",token);
            return false;
        } catch (Exception e) {
            log.error("token校验失败,token:",token);
            return false;
        }
        return true;
    }
}

创建 UserController:

@RestController
@RequestMapping("/user")
public class UserController {
    @Autowired
    private UserService userService;

    // 登录接口
    @RequestMapping("/login")
    public Result login(String userName, String password) {
        // 1.校验参数
        // 2.对密码进行校验
        // 3.如果校验成功,返回token
        if (!StringUtils.hasLength(userName) || !StringUtils.hasLength(password)) {
            return Result.fail("用户名或密码为空");
        }
        UserInfo userInfo = userService.queryUserByName(userName);
        if (userInfo == null || userInfo.getId() < 1) {
            return Result.fail("用户不存在");
        }
        if (!password.equals(userInfo.getPassword())) {
            return Result.fail("密码不正确");
        }
        // 密码正确了
        Map<String, Object> claim = new HashMap<>();
        claim.put("id", userInfo.getId());
        claim.put("userName", userInfo.getUserName());
        return Result.success(JwtUtils.genToken(claim));
    }
}

创建UserService:

@Service
public class UserService {
    @Autowired
    private UserMapper userMapper;

    public UserInfo queryUserByName(String userName) {
        return userMapper.selectByName(userName);
    }
}

运行程序,访问http://127.0.0.1:8080/user/login?userName=zhangsan&password=123456,测试后端接口返回结果是否正常:
image.png
image.png
image.png

1.5 实现客户端代码:

服务器给客户端返回token,客户端可以把token存在哪里呢?

  1. cookie
  2. 本地存储
  3. url

我们使用第二种存储方式。
修改 login.html, 完善登录⽅法,前端收到token之后, 保存在localstorage中:

function login() {
    // 发送ajax请求
    $.ajax({
        type: "post",
        url: "/user/login",
        data: {
            "userName": $("#username").val(),
            "password": $("#password").val()

        },
        success: function(result) {
            if (result.code == 200 && result.data != null) {
              // 存储token  
              localStorage.setItem("user_token", result.data);
              // 页面跳转
              location.href = "blog_list.html";
            } else {
                // 其他情况
                alert("用户名或密码错误");
            }
        }
    });
}

image.png
localStorage相关操作:
存储数据:localStorage.setItem(“user_token”,“value”);
读取数据:localStorage.getItem(“user_token”);
删除数据:localStorage.removeItem(“user_token”);
运行程序,访问http://127.0.0.1:8080/blog_login.html,输入用户名和密码,页面跳转:
image.png

2. 实现强制要求登录

用户强制登录:

  1. 客户端访问时,携带token(从localStorage中拿到token,发送http请求时可以放在header或者param中)
  2. 服务器获取token,验证token,如果token验证成功,就放行

当用户访问博客列表页和博客详情页时, 如果用户当前尚未登录,就⾃动跳转到登录页面。我们可以采⽤拦截器来完成, token通常由前端放在header中, 服务器从header中获取token, 并校验token是否合法。

2.1 添加拦截器

/**
 * 用户登录拦截器
 */
@Slf4j
@Component
public class LoginInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1.从header中获取token
        // 2.校验token
        // 3.成功,放行
        String userToken = request.getHeader("user_token_header");
        log.info("获得token,token:"+userToken);
        boolean result = JwtUtils.parseToken(userToken);
        if (result) {
            return true;
        }
        response.setStatus(401);// http状态码
        return false;
    }
}

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Autowired
    private LoginInterceptor loginInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginInterceptor).addPathPatterns("/**")
                .excludePathPatterns(
                        "/user/login",
                        "/**/*.html",
                        "/js/**",
                        "/css/**",
                        "/pic/**",
                        "/blog-editormd/**");
    }
}

2.2 实现客户端代码:

  1. 前端请求时, header中统⼀添加token, 可以写在common.js中
$(document).ajaxSend(function(e, xhr, opt) {
    var user_token = localStorage.getItem("user_token");// 从localStorage中拿到token值user_token
    xhr.setRequestHeader("user_token_header", user_token);// 把拿到的user_token存到header中user_token_header
});

ajaxSend() ⽅法在 AJAX 请求开始时执⾏函数

  • event- 包含 event 对象
  • xhr- 包含 XMLHttpRequest 对象
  • options- 包含 AJAX 请求中使⽤的选项

image.png

  1. 修改 blog_datail.html
error: function(error) {
    if (error!= null && error.status == 401) {
        location.href = "blog_login.html";
    }
}
  1. 修改 blog_list.html
  • 访问⻚⾯时, 添加失败处理代码
  • 使⽤ location.href进⾏⻚⾯跳转.
error: function(error) {
  if (error!= null && error.status == 401) {
      // console.log(userToken);
      alert("用户未登录,即将跳转到登录页!");
      location.href = "blog_login.html";
  }
}

运行程序,验证效果。
访问http://127.0.0.1:8080/blog_list.html,页面显示:
image.png
跳转到登录页面:
image.png
登录成功之后,页面跳转到博客列表页:
image.png

3. 实现显示用户信息

⽬前页面的用户信息部分是写死的。我们期望这个信息可以随着用户登录⽽发⽣改变。
image.png

  • 如果当前页面是博客列表页, 则显示当前登陆用户的信息
  • 如果当前页面是博客详情页, 则显示该博客的作者用户信息

注意:当前我们只是实现了显⽰用户名, 没有实现显⽰用户的头像以及文章数量等信息。

3.1 约定前后端交互接口

在博客列表页, 获取当前登陆用户的信息:

[请求]
/user/getUserInfo
[响应]
{
  userId: 1,
  username: test
  ...
}

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

[请求]
/user/getAuthorInfo?blogId=1
[响应]
{
  userId: 1,
  username: test
}

3.2 实现服务端代码

在 UserController添加代码:

/**
 * 获取当前登录用户的信息
 */
@RequestMapping("/getUserInfo")
public UserInfo getUserInfo(HttpServletRequest request) {
    // 1.获取token,从token中获取用户Id
    // 2.根据用户Id,获取用户信息
    String token = request.getHeader(Constant.USER_TOKEN_HEADER);
    // 解析token,从token中获取用户id
    Integer userId = JwtUtils.getUserIdFromToken(token);
    if (userId == null || userId < 1) {
        return null;
    }
    UserInfo userInfo = userService.queryUserById(userId);
    userInfo.setPassword("");// 修改password为空
    return userInfo;
}

/**
 * 根据博客Id,获取作者信息
 */
@RequestMapping("/getAuthorInfo")
public UserInfo getAuthorInfo(Integer blogId) {
    // 1.根据博客ID,获取作者ID
    // 2.根据作者Id,获取作者信息
    if (blogId == null || blogId < 1) {
        return null;
    }
    UserInfo authorInfo = userService.getAuthorInfoByBlogId(blogId);
    authorInfo.setPassword("");// 设置password为空
    return authorInfo;
}

在UserService中添加代码:

public UserInfo queryUserById(Integer userId) {
    return userMapper.selectById(userId);
}

public UserInfo getAuthorInfoByBlogId(Integer blogId) {
    // 1.根据博客ID,获取作者ID
    // 2.根据作者Id,获取作者信息
    BlogInfo blogInfo =  blogMapper.selectById(blogId);
    if (blogInfo == null || blogInfo.getUserId() < 1) {
        return null;
    }
    Integer userId = blogInfo.getUserId();
    UserInfo userInfo = userMapper.selectById(userId);
    return userInfo;
}

后端先验证接口返回是否正确:
运行程序,先登录页面,获取token:
image.png
携带该token访问http://127.0.0.1:8080/user/getUserInfo,接口返回结果为:
image.png
测试第二个接口,访问http://127.0.0.1:8080/user/getAuthorInfo?blogId=2,接口返回结果:
image.png
如果我们不想在接口返回结果中显示密码,可以把password设为空:
image.png
再次进行访问,返回结果:
image.png

3.3 实现客户端代码:

  1. 修改 blog_list.html

在响应回调函数中, 根据响应中的用户名, 更新界⾯的显⽰:

getUserInfo();
function getUserInfo() {
    $.ajax({
        type: "post",
        url: "/user/getUserInfo",
        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);
            }
        }
    });
}

运行程序,访问 http://127.0.0.1:8080/blog_list.html,页面显示成功:
image.png

  1. 修改 blog_detail.html
getUserInfo();
function getUserInfo() {
    $.ajax({
        type: "get",
        url: "/user/getAuthorInfo"+location.search,
        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);
            }
        }
    });
}

运行程序,访问http://127.0.0.1:8080/blog_detail.html?blogId=3,页面显示成功:
image.png
代码整合: 提取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);
            }
        }
    });
}

引⼊common.js

<script src="js/common.js"></script>

blog_list.html 代码修改

var url = "/user/getUserInfo";
getUserInfo(url);

blog_detail.html 代码修改

//显示博客作者信息
var url = "/user/getAuthorInfo"+location.search;
getUserInfo(url);

4. 实现用户退出

前端直接清除掉token即可。
image.png

4.1 实现客户端代码:

<<注销>> 链接已经提前添加了onclick事件
image.png
在common.js中完善logout⽅法

// 用户注销
function logout() {
    localStorage.removeItem("user_token");
    location.href = "blog_login.html";
}

点击注销,页面跳转:
image.png

5. 实现发布博客

5.1 约定前后端交互接口

[请求]
/blog/add
title=标题&content=正⽂...
[响应]
{
  "code": 200,
  "msg": "",
  "data": true
}
//true 成功
//false 失败

5.2 实现服务端代码:

修改 BlogController, 新增 publishBlog方法:

// 发布博客
@RequestMapping("/add")
public boolean publishBlog(String title, String content, HttpServletRequest request) {
    // 1.校验参数
    // 2.获取当前登录用户
    // 3.博客发布
    log.info("publishBlog, 接收参数:title: {}, content:{}",title, content);
    if (!StringUtils.hasLength(title) || !StringUtils.hasLength(content)) {
        log.error("title or content 为空");
        return false;
    }
    // 获取token,解析token,从token中获取用户ID
    String token = request.getHeader(Constant.USER_CLAIM_ID);
    Integer userId = JwtUtils.getUserIdFromToken(token);
    if (userId == null || userId < 1) {
        log.error("用户不存在");
        return false;
    }

    BlogInfo blogInfo = new BlogInfo(title, content, userId);
    Integer result = blogService.publishBlog(blogInfo);
    if (result < 1) {
        log.error("博客发布失败");
        return false;
    }
    return true;
}

BlogService 添加对应的处理逻辑:

public Integer publishBlog(BlogInfo blogInfo) {
    return blogMapper.insertBlog(blogInfo);
}

5.3 editor.md介绍

editor.md 是⼀个开源的⻚⾯ markdown 编辑器组件。
官⽹参⻅: http://editor.md.ipandao.com/

5.4 实现客户端代码:

修改 blog_edit.html:

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";
          }
          // 失败
      }
  });
}

运行程序,点击写博客,写完之后进行发布:
image.png

5.5 修改详情页页面显示

image.png
此时会发现详情页会显⽰markdown的格式符号, 我们对页面进行处理。

  1. 修改 html 部分, 把博客正⽂的 div 标签, 改成

image.png

  1. 修改博客正⽂内容的显⽰
// 获取博客详情
$.ajax({
    type: "get",
    url: "/blog/getBlogDetail" + location.search,
    success: function (result) {
        if (result.code == 200 && result.data != null) {
            var blog = result.data;
            $(".right .content .title").text(blog.title);
            $(".right .content .date").text(blog.createTime);
            // $(".right .content .detail").text(blog.content);
            editormd.markdownToHTML("detail", {
                markdown: blog.content,
            });
        }
    },
    error: function (error) {
        if (error != null && error.status == 401) {
            location.href = "blog_login.html";
        }
    }
});

image.png
重新运行程序,刷新页面:
image.png
页面中markdown的格式符号已经没有了,但是页面的背景色需要修改,我们希望背景色不变:
修改 html 部分, 把博客正⽂的 div 标签加上style=“background-color: transparent;”
image.png
重新运行程序,刷新页面:
image.png

6. 实现删除/编辑博客

进⼊用户详情⻚时, 如果当前登录用户正是文章作者, 则在导航栏中显⽰ [编辑] [删除] 按钮, 用户点击时则进行相应处理。
需要实现两件事:

  1. 判定当前博客详情页中是否要显⽰[编辑] [删除] 按钮
    1. 实现方式:
      1. 后端提供一个接口,判断当前博客作者和登录用户是否相同
      2. 修改之前的 获取博客信息 的接⼝, 在响应中加上⼀个字段
  2. 实现编辑/删除逻辑(删除采用逻辑删除,所以和编辑其实为同⼀个接⼝)

6.1 约定前后端交互接口

  1. 判定当前博客详情页中是否要显⽰[编辑] [删除] 按钮,我们采用第二种实现方式:
[请求]
/blog/getBlogDetail?blogId=3
[响应]
{
    "code": 200,
    "errMsg": "",
    "data": {
        "id": 3,
        "title": "insert第三篇博客",
        "content": "insert博客内容",
        "userId": 2,
        "deleteFlag": 0,
        "createTime": "2024-04-26 18:08",
        "updateTime": "2024-04-26T10:08:58.000+00:00",
        "isLoginUser": false
    }
}
  1. 修改博客
[请求]
/blog/update
[参数]
Content-Type: application/json
{
  "title": "测试修改⽂章",
  "content": "在这⾥写下⼀篇博客",
  "blogId": "3"
}
[响应]
{
  "code": 200,
  "errMsg": "",
  "data": true
}
  1. 删除博客
[请求]
/blog/delete?blogId=1
[响应]
{
"code": 200,
"msg": "",
"data": true
}

6.2 实现服务器代码:

  1. 给BlogInfo类新增加一个字段isLoginUser:

image.png

  1. 修改 BlogController中 getBlogDeatail 接口中的逻辑
@RequestMapping("/getBlogDetail")
public BlogInfo queryBlogDetail(Integer blogId, HttpServletRequest request) {
    log.info("getBlogDetail, 接收参数:blogId:"+ blogId);
    BlogInfo blogInfo = blogService.queryBlogDetail(blogId);
    // 1.获取登录用户
    // 2.判断登录用户和博客作者是否相同
    String token = request.getHeader(Constant.USER_TOKEN_HEADER);
    Integer userId = JwtUtils.getUserIdFromToken(token);
    if (userId != null && userId == blogInfo.getUserId()) {
        blogInfo.setIsLoginUser(true);
    }else {
        blogInfo.setIsLoginUser(false);
    }

    return blogInfo;
}

注意:当BlogInfo中字段为包装类行和基本类型时,接口返回结果中变量名是不同的,这个结果是会影响到前端使用时的传参的:
image.png
涉及到前端代码:
image.png

  1. 修改 BlogController

增加 update/delete ⽅法, 处理修改/删除逻辑

// 修改博客
@RequestMapping("/update")
public boolean updateBlog(Integer blogId, String title, String content) {
    log.info("update,接收参数:title:{},content:{}",title, content);
    // 1.校验参数
    // 2.根据博客Id,修改博客
    if (blogId == null || !StringUtils.hasLength(title) || !StringUtils.hasLength(content)) {
        log.error("博客不存在或者标题/内容为空");
        return false;
    }
    BlogInfo blogInfo = new BlogInfo();
    blogInfo.setId(blogId);
    blogInfo.setTitle(title);
    blogInfo.setContent(content);
    Integer result = blogService.updateBlog(blogInfo);
    if (result < 1) {
        log.error("未修改成功");
        return false;
    }
    return true;
}

// 删除博客
@RequestMapping("/delete")
public boolean deleteBlog(Integer blogId) {
    BlogInfo blogInfo = new BlogInfo();
    blogInfo.setId(blogId);
    blogInfo.setDeleteFlag(1);
    Integer result = blogService.deleteBlog(blogInfo);
    if (result < 1) {
        log.error("删除失败");
        return false;
    }
    return true;
}

6.3 实现客户端代码:

  1. 判断是否显⽰[编辑] [删除]按钮
// 获取博客详情
$.ajax({
    type: "get",
    url: "/blog/getBlogDetail" + location.search,
    success: function (result) {
        ...
        }
        // 是否显示编辑和删除按钮
        if (result.data.isLoginUser) {
            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);
        }
    },
    ...
});

运行程序,查看博客详情页中编辑和删除按钮是否显示正确:
当登录用户(zhangsan)和文章作者相同时,显示两个按钮:
image.png
当登录用户(zhangsan)和文章作者不同时,不显示:
image.png

  1. 编辑博客逻辑,修改blog_update.html

页面⾯加载时, 请求博客详情:

function getBlogInfo() {
  // 1.页面加载时就调用,获取博客的详细信息
  $.ajax({
      type: "get",
      url: "/blog/getBlogDetail" + location.search,
      success: function (result) {
          if (result.code == 200 && result.data != null) {
              $("#blogId").val(result.data.id);
              // 填title和content
              $("#title").val(result.data.title);
              // $("#content").val(result.data.content);
              editor = editormd("editor", {
                  width: "100%",
                  height: "550px",
                  path: "blog-editormd/lib/",
                  onload: function () {
                      this.watch();
                      this.setMarkdown(result.data.content);
                  }
              });
          }

      }
  });
}

已经在getBlogInfo进⾏markdown编辑器的渲染了, 所以把以下代码删除
image.png

// 更新博客
function submit() {
    $.ajax({
        type: "post",
        url: "/blog/update",
        data: {
            blogId: $("#blogId").val(),
            title: $("#title").val(),
            content: $("#content").val()
        },
        success: function(result) {
            if (result.code == 200 && result.data) {
                alert("博客更新成功!");
                location.href = "blog_list.html";
            }
        }
    });
}

运行程序,先测试更新博客:
更新前:
image.png
更新后:
image.png

// 删除博客
function deleteBlog() {
    if (confirm("确定删除这篇博客吗?")) {
        $.ajax({
            type: "post",
            url: "/blog/delete" + location.search,
            success: function (result) {
                if (result.code == 200 && result.data) {
                    location.href = "blog_list.html";
                }
            }
        });
    }

}

测试删除博客:
删除前博客列表页:
image.png
执行删除操作,删除后博客列表页:
image.png
数据库中这篇博客的delete_flag变为1:
image.png
未完,后续内容见下篇文章!

  • 13
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值