【Java】毕业设计个人博客系统 ---- 代码+详解

博客系统项目分析

  • 使用SSM框架(SpringMVC+Spring+Mybatis)实现一个简单的博客系统,一共有五个页面:用户登录、博客发表页、博客编辑页、博客列表页和博客详情页;

  • 后端需要提供(实现)的功能:

    1. 用户登录:根据用户名和密码,判断用户输入的信息是否正确
    2. 登录用户个人页:博客列表页显示当前登录用户信息,根据用户ID,返回用户相关信息
    3. 博客列表页:查询博客列表
    4. 作者个人主页:根据用户ID,返回作者信息
    5. 博客详细信息:根据博客ID,返回博客信息
    6. 博客编辑:根据博客ID,返回博客信息;根据用户输入的信息,更新博客
    7. 删除博客:根据博客ID,进行博客删除
    8. 写博客:根据用户输入的信息,添加博客
  • 接口设计:

    1. 用户相关: 根据用户名和密码,判断用户输入的信息是否正确;根据用户ID,返回用户相关信息
    2. 博客相关 :查询博客列表;根据博客ID,返回作者信息(这里的数据库操作流程:博客ID->作者ID->作者信息);根据博客ID,返回博客详情;根据用户输入的信息,更新博客;根据博客ID,删除博客;根据用户输入的信息,添加博客。
  • 实体:

    1. 用户实体
    2. 博客实体
    3. 结果实体

1. 数据库设计

1.1 设计库表

建一个库和两个表:用户表(user)、博客表(blog)

  1. 用户表 user(id,user_name,password,github_url,delete_flag,create_time,update_time)
    在这里插入图片描述

  2. 博客表blog (id,title,content,user_id,delete_flag,create_time,update_time)
    在这里插入图片描述

1.2 代码实现

-- 建表SQL
create database if not exists java_blog_spring charset utf8mb4;

-- 用户表
DROP TABLE IF EXISTS java_blog_spring.user;
CREATE TABLE java_blog_spring.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 java_blog_spring.blog;
CREATE TABLE java_blog_spring.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 java_blog_spring.user (user_name, password,github_url)values("zhangsan","123456","https://gitee.com/bubble-fish666/class-java45");
insert into java_blog_spring.user (user_name, password,github_url)values("lisi","123456","https://gitee.com/bubble-fish666/class-java45");

insert into java_blog_spring.blog (title,content,user_id) values("第一篇博客","这是我的第一篇博客路过一下",1);
insert into java_blog_spring.blog (title,content,user_id) values("第二篇博客","这是我的第二篇博客好好加油冲啊",2);

1.3 创建项目

创建Spring项目,添加SpringMVC和Mybatis依赖
在这里插入图片描述

1.4 配置application.yml配置文件

spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/java_blog_spring?characterEncoding=utf8&useSSL=false
    username: root
    password: root
    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  #xml实现update操作
# 设置日志文件的文件名
logging:
  file:
    name: spring-blog.log

2. 项目公共模块

项目公共模块:Controller(控制层)、Service(服务层)、Mapper(持久层),各层的调用关系:
在这里插入图片描述

2.1 实体类

博客类(BlogInfo)和用户类(UserInfo),这里把这2个实体类放入model里,类中引用Lombok依赖,在运行时,使用Lombok会自动生成getter、setter方法,构造函数等功能

  1. 用户类
  • UserInfo类
package com.example.blog.model;
import lombok.Data;
import java.util.Date;

@Data
public class UserInfo {
    private Integer id;
    private String userName;
    private String password;
    private String githubUrl;
    private Integer deleteFlag;
    private Date createTime;
    private Date updateTime;
}
  1. 博客类
  • BlogInfo类
package com.example.blog.model;

import com.example.blog.utils.DateUtils;
import lombok.Data;
import java.util.Date;

@Data
public class BlogInfo {
    private Integer id;
    private String title;
    private String content;
    private Integer userId;
    private boolean isLoginUser;
    private Integer deleteFlag;
    private Date createTime;
    private Date updateTime;

    public String getCreateTime() {
        return DateUtils.format(createTime);
    }
}

2.2 公共层

2.2.1 统一返回结果实体类

  • code,业务状态码:
    200 : 业务处理成功,-1:业务处理失败,-2:用户未登录
  • errMsg,错误信息:
  • data,接口响应的数据

使用枚举来定义:对应code状态码,200 - SUCCESS,-1 - FAIL, -2 - UNLOGIN,

package com.example.blog.enums;

public enum ResultStatus {
    SUCCESS,
    FAIL,
    UNLOGIN;
}

Result实体类:

package com.example.blog.model;

import com.example.blog.enums.ResultStatus;
import lombok.Data;

@Data
public class Result<T> {
    // 业务码 200 成功 -1:失败 -2:未登录
    private ResultStatus code;
    // 错误信息
    private String errMsg;
    // 接口响应的数据
    private T data;

    /**
     *  业务执行成功返回
     */
    public static <T> Result<T> success(T data) {
        Result result = new Result();
        result.setCode(ResultStatus.SUCCESS);
        result.setErrMsg("");
        result.setData(data);
        return result;
    }

    /**
     *  业务执行失败返回
     */
    public static <T> Result<T> fail(String errMsg) {
        Result result = new Result();
        result.setCode(ResultStatus.FAIL);
        result.setErrMsg(errMsg);
        result.setData(null);
        return result;
    }

    public static <T> Result<T> fail(String errMsg,T data) {
        Result result = new Result();
        result.setCode(ResultStatus.FAIL);
        result.setErrMsg(errMsg);
        result.setData(data);
        return result;
    }
}

2.2.2 统一返回结果处理

统一数据返回处理不懂的可以参考本文博客详解:点击链接跳转:统一返回结果处理

package com.example.blog.config;

import com.example.blog.model.Result;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectWriter;
import lombok.SneakyThrows;
import org.springframework.beans.factory.annotation.Autowired;
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 {
    @Autowired
    private ObjectMapper objectMapper;
    @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类型,使用SpringBoot内置提供的Jackson来实现信息的序列化
        if (body instanceof String) {
            return objectMapper.writeValueAsString(Result.success(body));
        }
        return Result.success(body);
    }
}

2.2.3 统一异常处理

对于统一异常处理详解博客:点击链接跳转 统一异常处理详解

package com.example.blog.config;

import com.example.blog.enums.ResultStatus;
import com.example.blog.model.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.resource.NoResourceFoundException;

@Slf4j
@ResponseBody
@ControllerAdvice
public class ExceptionHandle {
    @ExceptionHandler
    public Result handle(Exception e) {
        log.info("发生异常,e",e);
        return Result.fail("发生内部错误,请联系管理员");
    }

    @ExceptionHandler
    public Result handle(NoResourceFoundException e) {
        log.info("文件不存在,e:{}",e.getResourcePath());
        return Result.fail("发生内部错误,请联系管理员");
    }
}

3. 获取博客列表

在这里插入图片描述

3.1 持久层数据库相关操作

获取博客列表

package com.example.blog.mapper;

import com.example.blog.model.BlogInfo;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;

import java.util.List;

// Mapper 持久层:负责与数据库进行交互,包括数据的增删改查操作。保持独立,不涉及业务逻辑处理

@Mapper
public interface BlogMapper {
    //返回博客列表
    @Select("select id,title,content,user_id,delete_flag,create_time,update_time from blog " +
            "where delete_flag = 0 ")
    List<BlogInfo> selectAll();
}

3.2 约定前后端交互接口

客户端给服务器发送一个/blog/getList的HTTP请求,服务器给客户端返回了一个JSON格式的数据
在这里插入图片描述

package com.example.blog.controller;

import com.example.blog.constant.Constants;
import com.example.blog.model.BlogInfo;
import com.example.blog.model.Result;
import com.example.blog.model.UserInfo;
import com.example.blog.service.BlogService;
import com.example.blog.utils.JwtUtils;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@Slf4j
@RestController
@RequestMapping("/blog")
public class BlogController {
    @Autowired
    private BlogService blogService;

    // 获取博客列表
    @RequestMapping("/getList")
    public List<BlogInfo> getList() {
        log.info("获取博客列表...");
        return blogService.getList();
    }
}

3.3 实现服务器代码

package com.example.blog.service;

import com.example.blog.mapper.BlogMapper;
import com.example.blog.mapper.UserInfoMapper;
import com.example.blog.model.BlogInfo;
import com.example.blog.model.UserInfo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;

@Slf4j
@Service
public class BlogService {
    @Autowired
    private BlogMapper blogMapper;

    @Autowired
    private UserInfoMapper userInfoMapper;
    public List<BlogInfo> getList() {
        return blogMapper.selectAll();
    }
}

3.4 实现客户端代码

  • 使用Ajax给服务器发送HTTP请求。
  • 服务器返回的响应结果是一个JSON格式的数据,根据这个这个响应数据使用DOM API构造页面内容。
<script src="js/jquery.min.js"></script>
    <script src="js/common.js"></script>
    <script>
    
        $.ajax({
            type: "get",
            url: "/blog/getList",
            success: function (result) {
                if (result.code == "SUCCESS") {
                    var blogs = result.data;
                    var finalHtml = "";
                    for (var blog of blogs) {
                        finalHtml += '<div class="blog">';
                        finalHtml += '<div class="title">' + blog.title + '</div>';
                        finalHtml += '<div class="date">' + blog.createTime + '</div>';
                        finalHtml += '<div class="desc">' + blog.content + '</div>';
                        finalHtml += '<a class="detail" href="blog_detail.html?blogId=' + blog.id + '">查看全文&gt;&gt;</a>';
                        finalHtml += '</div>';
                    }
                    $(".container .right").html(finalHtml);
                }
            },
            error:function(error) {
                console.log("是否进入error");
                if(error!=null && error.status==401) {
                    location.href = "/blog_login.html";
                }
            }
        });
    </script>

4. 使用SimpleDateFormat修改后端时间表示方式

在这里插入图片描述

  • 在utils包中写一个修改时间的DateUtils类,使用SimpleDateFormat自定义修改自己想要的时间格式。

在这里插入图片描述

package com.example.blog.utils;

import java.text.SimpleDateFormat;
import java.util.Date;

public class DateUtils {
    public static String format(Date date) {
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        return dateFormat.format(date);
    }
}
  • 在博客实体类中重写博客获取时间
    public String getCreateTime() {
        return DateUtils.format(createTime);
    }

5. 实现博客详情

在这里插入图片描述

5.1 持久层数据库操作

  • 根据博客ID,返回博客信息
    @Select("select id,title,content,user_id,delete_flag,create_time,update_time from blog " +
            "where delete_flag = 0 and id = #{id}" )
    BlogInfo selectById(Integer id);

5.2 约定前后端接口

在这里插入图片描述

    @RequestMapping("/getBlogDetail")
    public BlogInfo getBlogDetail(Integer blogId,HttpServletRequest request) {
        log.info("根据博客ID返回博客详情....blogId:{}",blogId);
        BlogInfo blogDetail = blogService.getBlogDetail(blogId);
        }
}

5.3 实现服务器代码

    public BlogInfo getBlogDetail(Integer blogId) {
        log.info("这是service里的getBlogDetail:{}",blogMapper.selectById(blogId));
        return blogMapper.selectById(blogId);
    }
}

5.4 实现客户端代码

  • location.search:获取url中的参数。即下面获取到?blogId=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);
					}
				}
			});

6. 实现用户登录-使用JWT令牌实现

在集群环境下,使用Session会话跟踪时:

  • 如果只部署在一台服务器上时,一旦服务器挂了,就会出现单点故障,整个应用就无法访问。
  • 部署多个服务器时,用户通过登录之后携带Cookie(带有sessionId)进行博客列表查询操作,服务器2先通过验证是否登录,通过sessionId进行查找,服务器2没有该用户的Session,就会出现查询报错。

存在上述问题,此时就需要其他技术来验证登录,这里接下来使用JWT令牌技术来实现用户验证登录,JWT令牌详解JWT令牌详解

6.1 引入JWT令牌的依赖

把依赖导入pom.xml文件中。


		<!-- 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>

6.2 约定前后端接口

在这里插入图片描述

  1. 登录页面把用户名密码提交服务器。
  2. 服务器端验证用户名密码是否正确,如果正确,服务器生成令牌,发给客户端。
  3. 客户端把令牌存储起来(可以存在Cookie、local storage等),后续请求时把token发送给服务器
  4. 服务器对令牌进行校验,如果令牌正确,进行下一步操作。

在工具类中写JwtUtils类,在校验时,令牌解析后,可以看到token里面存储的信息,如果在解析的过程当中没有报错,说明解析成功。

  • 令牌解析时,也会进行时间有限性的校验,如果令牌过期了,解析也会失败
  • 如果令牌被篡改了,校验同样会失败

6.3 实现服务器代码

  1. 创建JWT工具类
package com.example.blog.utils;

import com.example.blog.constant.Constants;
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 lombok.extern.slf4j.Slf4j;

import javax.crypto.SecretKey;
import java.security.Key;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

@Slf4j
public class JwtUtils {
    // JWT过期时间
    public static final long JWT_EXPIRATION = 60*60*60*1000;
    // 生成key
    private static final String secretStr = "DuJXRS2W3AJHqyFhAplBmsPNawnEdFYFNmlNdMbyU9w=";
    private static final Key key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretStr));

    /**
     *  生成token
     */
    public static String genJwtToken(Map<String,Object> claim) {
        String token = Jwts.builder().setClaims(claim)
                .setExpiration(new Date(System.currentTimeMillis()+JWT_EXPIRATION))
                .signWith(key)
                .compact();
        return token;
    }


    /**
     *  校验token
     *  Claims 为空,表示jwt校验失败
     *
     */
    public static Claims parseToken(String token) {
        // 创建解析器,设置签名密钥
        JwtParser build = Jwts.parserBuilder().setSigningKey(key).build();
        Claims claims = null;
        try {
            // 解析token
            claims = build.parseClaimsJws(token).getBody();
        }catch (Exception e){
            log.error("解析token失败,token:{}",token);
            return null;
        }
        return claims;
    }
}

  1. 用户登录控制层
    UserController类
package com.example.blog.controller;

import com.example.blog.constant.Constants;
import com.example.blog.model.Result;
import com.example.blog.model.UserInfo;
import com.example.blog.service.UserService;
import com.example.blog.utils.JwtUtils;
import com.example.blog.utils.SecurityUtils;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;

@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {
    @Autowired
    private UserService userService;
    @RequestMapping("/login")
    public Result login(String username,String password) {
        log.info("UserController#login接收参数:username:{},password:{}",username,password);
        // 1.参数校验
        if (!StringUtils.hasLength(username) || !StringUtils.hasLength(password)) {
            return Result.fail("账号或密码不能为空");
        }
        // 2.检验密码是否正确
        UserInfo userInfo = userService.selectByName(username);
        if (userInfo == null) {
            log.error("用户不存在");
            return Result.fail("用户不存在");
        }
        // 3.密码正确,返回token
        Map<String,Object> claim = new HashMap<>();
        claim.put(Constants.TOKEN_ID,userInfo.getId());
        claim.put(Constants.TOKEN_USERNAME,userInfo.getUserName());
        String token = JwtUtils.genJwtToken(claim);
        log.info("UserController#login返回结果token:{}",token);
        return Result.success(token);
        // 密码错误,返回错误信息
    }
}

  1. 服务层UserService类
package com.example.blog.service;

import com.example.blog.mapper.BlogMapper;
import com.example.blog.mapper.UserInfoMapper;
import com.example.blog.model.BlogInfo;
import com.example.blog.model.UserInfo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Slf4j
@Service
public class UserService {

    @Autowired
    private UserInfoMapper userInfoMapper;
    public UserInfo selectByName(String userName) {
        return userInfoMapper.selectByName(userName);
    }
}

  1. 持久层UserInfoMapper
package com.example.blog.mapper;

import com.example.blog.model.UserInfo;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;

@Mapper
public interface UserInfoMapper {
    // 根据用户名,查询用户信息
    @Select("select id, user_name, password, github_url, delete_flag,create_time from user " +
            "where delete_flag != 1 and user_name = #{userName}")
    UserInfo selectByName(String userName);

6.4 实现客户端代码

  • 把token存储在localstorage里,
  • localstorage一些操作:
    存储数据:localStorage.setItem(“user_token”,“value”);
    读取数据:localStorage.getItem(“user_token”);
    删除数据:localStorage.removeItem(“user_token”);
	<script>
        function login() {
            $.ajax({
                type:"post",
                url:"/user/login",
                data:{
                    username : $("#username").val(),
                    password:$("#password").val()
                },
                success:function(result) {
                    // 使用localStorage保存令牌
                    if (result.code == "SUCCESS" && result.data!=null) {
                        // 后端处理成功,存储数据
                        localStorage.setItem("user_token",result.data);
                        // 页面进行跳转
                        location.href = "blog_list.html";
                    }else {
                        alert(result.errMsg);
                    }
                },
            });
        }
    </script>

在这里插入图片描述

7. 实现强制登录

  • 用户在没有登录的情况下访问博客列表和博客详情页面时,也能显示页面内容,所以在用户当前没有登录的情况下,就自动跳转到登录页面。

7.1 使用拦截器实现强制登录

难点重点:拦截器博客详解: 拦截器详解点击跳转

  1. token由前端放在header中,从header中获取token
  2. 校验token是否放行,解析token的claims内容为空,校验失败,说明用户未登录,前端进行用户登录页的跳转。不为空则校验成功。
  • 添加拦截器:
package com.example.blog.interceptor;

import com.example.blog.constant.Constants;
import com.example.blog.utils.JwtUtils;
import io.jsonwebtoken.Claims;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

// 1.客户端把token放在header里
// 2.服务端强制登录,通过拦截器实现
    //1)从header获取token
    // 2)校验token,判断是否放行
@Slf4j
@Component
public class LoginInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //1)从header获取token
        String token = request.getHeader(Constants.USER_TOKEN_HEADER);
        log.info("从header获取的token:{}",token);
        // 2)校验token,判断是否放行
        Claims claims = JwtUtils.parseToken(token);
        if (claims == null) {
            // 校验失败
            response.setStatus(401);
            return false;
        }
        log.info("token通过,放行,token:{}",token);
        return true;
    }
}

  • 注册配置拦截器
package com.example.blog.config;

import com.example.blog.interceptor.LoginInterceptor;
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.ArrayList;
import java.util.Arrays;
import java.util.List;

@Configuration
public class WebConfig implements WebMvcConfigurer {
    private final List excludes = Arrays.asList(
            "/**/*.html",
            "/blog-editormd/**",
            "/css/**",
            "/js/**",
            "/pic/**",
            "/user/login"
    );
    // 添加自定义的拦截器依赖
    @Autowired
    private LoginInterceptor loginInterceptor;
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 注册自定义拦截器对象
        registry.addInterceptor(loginInterceptor)
                .addPathPatterns("/**")
                .excludePathPatterns(excludes);
    }
}

  • 定义一个数组,把不需要拦截的路径放进数组(包括静态资源和用户登录请求),其他所有请求都需要拦截进行是否登录验证。

7.2 实现客户端代码

  1. 前端请求时,在header中统一添加header,写在common.js中难点重点
    • 使用ajaxSend()方法在AJAX请求开始时执行函数
    • event-包含event对象
    • xhr-包含XMLHttpRequest对象
    • options-包含AJAX请求中使用的选项
$(document).ajaxSend(function (e, xhr, opt) {
    var token = localStorage.getItem("user_token");
    xhr.setRequestHeader("user_token_header", token);
});
  1. 修改博客列表页和详情页客户端代码
    • 访问页面时,添加失败(也就是未登录出现的401报错)处理代码
    • 使用location.heaf进行页面跳转
        error:function(error) {
            if(error!=null && error.status == 401){
                location.href="blog_login.html";
            }
        }

8. 实现显示用户信息

在这里插入图片描述

  • 当页面是博客列表页,显示当前登录用户的信息
  • 当页面是博客详情页,显示当前博客作者的用户信息

8.1 约定前后端接口

在这里插入图片描述

8.2 实现服务器代码

  • 获取当前登录信息
    1. 先从header里面获取token
    2. 从token中获取登录用户Id
    3. 再根据userId获取当前登录用户信息

UserController类:

    @RequestMapping("/getUserInfo")
    public UserInfo getUserInfo(HttpServletRequest request) {
        log.info("获取用户信息....");
        // 获取toeken
        String token = request.getHeader(Constants.USER_TOKEN_HEADER);
        // 从token中获取登录用户ID
        Integer userId = JwtUtils.getIdByToken(token);
        if (userId == null) {
            //登录未成功
            return null;
        }

        UserInfo userInfo = userService.selectById(userId);
        return userInfo;
    }

UserService类:

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

UserInfoMapper接口类:

    @Select("select id, user_name, password, github_url, delete_flag,create_time from user " +
            "where delete_flag != 1 and id = #{id}")
    UserInfo selectById(Integer id);
  • 获取博客作者信息
    1. 根据博客ID获取作者Id
    2. 根据作者Id获取作者信息

Usercontroller类:

    @RequestMapping("/getAuthorInfo")
    public Result getAuthorInfo(Integer blogId) {
        log.info("获取博客作者信息.....");
        if (blogId<=0) {
            return Result.fail("博客ID不正确");
        }
        UserInfo userInfo = userService.getAuthorInfo(blogId);
        return Result.success(userInfo);
    }

UserService类:

  1. 根据博客ID获取作者Id
  2. 根据作者Id获取作者信息
    @Autowired
    private BlogMapper blogMapper;
    public UserInfo getAuthorInfo(Integer blogId) {
        BlogInfo blogInfo = blogMapper.selectById(blogId);
        if (blogInfo == null && blogInfo.getUserId() <= 0) {
            log.error("图书不存在或者图书作者信息不合法,blogId:{}",blogId);
            return null;
        }
        UserInfo userInfo = userInfoMapper.selectById(blogInfo.getUserId());
        return userInfo;
    }

接口类同上。

8.3 实现客户端代码

博客列表页和博客详情页都需要显示信息,所以把共同的代码放进common.js里面,只需要各自调用即可。

  • common.js:
function getUserInfo(url) {
    console.log("getUserInfo");
    $.ajax({
        type:"get",
        url: url,
        success:function(result){
            console.log(result);
            if(result.code=="SUCCESS" && result.data!=null) {
                var userInfo = result.data;
                $(".left .card h3").text(userInfo.userName);
                $(".left .card a").attr("href",userInfo.githubUrl);//赋值给a标签某一个属性时使用 attr
            }else {
                alert(result.errMsg)
            }
        },
        error:function(error) {
            if(error!=null && error.status == 401){
                location.href="blog_login.html";
            }
        }
    });
}

  • blog_list.html:
getUserInfo("/user/getUserInfo");

测试的博客作者是zhangsan
在这里插入图片描述

  • blog_detail.html:
	 //显示博客作者信息
        var userUrl = "/user/getAuthorInfo" + location.search;
        getUserInfo(userUrl);

第三篇博客作者是lisi,显示的也是lisi
在这里插入图片描述

9. 实现用户退出

在这里插入图片描述

  • 业务逻辑:
    根据上面用户登录使用的是token(JWT技术),所以这里只需要将生成验证的user_token删除即可。删除之后完成退出功能操作,然后跳转到登录页面。

9.1 实现客户端代码

  • 在注销按钮中完成onclick点击事件,多个页面都有所以写在公共代码common.js中。
  1. 由于token是存储在local Storage中,使用removeItem删除。
  2. 跳转到登录页面
function logout() {
    localStorage.removeItem("user_token");
    location.href = "/blog_login.html";
}

9.2 约定前后端接口

在这里插入图片描述

9.3 实现服务器代码

  • 在添加博客之前要进行参数校验和判断用户是否登录,添加博客是在登录的前提下完成。
  1. 参数校验
  2. 判断用户是否登录,
    • 从header里面获取token
    • 在根据token获取用户id
    • 用户id不为空则表示登录并设置用户id
  3. 添加博客
  • BlogController类:
    @RequestMapping("/add")
    public Result addBlog(@RequestBody BlogInfo blogInfo, HttpServletRequest request) {
        log.info("添加博客blogInfo:{}",blogInfo);
        // 参数校验
        if (!StringUtils.hasLength(blogInfo.getTitle()) || !StringUtils.hasLength(blogInfo.getContent())) {
            return Result.fail("标题或者内容为null");
        }
        // 判断是否登录
        String token = request.getHeader(Constants.USER_TOKEN_HEADER);
        Integer userId = JwtUtils.getIdByToken(token);
        if (userId == null || userId <= 0) {
            // 用户未登录
            return Result.fail("用户未登录");
        }
        blogInfo.setUserId(userId);
        blogService.insertBlog(blogInfo);
        return Result.success(true);
    }
  • BlogService类:
    public boolean insertBlog(BlogInfo blogInfo) {
        try {
            Integer result = blogMapper.insertBlog(blogInfo);
            if (result == 1){
                return true;
            }
        }catch (Exception e){
            log.error("添加图书失败,e",e);
        }
        return false;
    }
  • 根据用户输入的信息,添加博客,BlogMapper接口:
    @Insert("insert into blog (title,content,user_id) values (#{title},#{content},#{userId})")
    Integer insertBlog(BlogInfo blogInfo);

9.4 引入Markdown编辑器

在添加博客时,意识到文本编辑器该怎么办,是自己写还是使用开源的?

通过观察博客园,CSDN使用的有Markdown编辑器、富文本编辑器(Rich Text Editor)、beautifulsoup。然后我就找了个简单容易使用的markdown开源的编辑器。官网链接: 点击跳转

  • 在前端添加依赖:
    header加入:
    <link rel="stylesheet" href="blog-editormd/css/editormd.css" />

body加入:

		<div id="editor">
            <textarea style="display:none;" id="content" name="content">##在这里写下一篇博客</textarea>
        </div>
        <script src="blog-editormd/editormd.min.js"></script>
	    <script src="js/common.js"></script>
<script type="text/javascript">

        $(function () {
            var editor = editormd("editor", {
                width: "100%",
                height: "550px",
                path: "blog-editormd/lib/"
            });
        });
        function submit(){
        }
    </script>

9.5 实现客户端代码

后端使用JSON来接收需要注意很多细节点
运行时的报错记录,即需要注意的细节点

  1. Required request body is missing。
    • 原因:如果后端使用json 接收 用了@RequestBody,前端需要使用POST请求
  2. org.springframework.web.HttpMediaTypeNotSupportedException: Content-Type ‘application/x-www-form-urlencoded;charset=UTF-8’ is not supported
    • 由于使用的是JSON格式接收,前端数据data需要使用JSON.stringify()把字符串转化为json
    • 还需要添加contentType:“application/json”
      在这里插入图片描述

完善submit方法,

        function submit() {
            $.ajax({
                type:"post",
                url:"/blog/add",
                contentType:"application/json",
                data:JSON.stringify({
                    "title":$("#title").val(),
                    "content":$("#content").val()
                }),
                success:function(result){
                    if(result.code == "SUCCESS" && result.data == true) {
                        // 发表成功
                        location.href = "blog_list.html";
                    }else {
                        // 后端没有对错误进行分析,统一返回了false
                        alert(result.errMsg);
                    }
                },
                error:function(error) {
                    if(error != null && error.status == 401){
                        location.href = "/blog_login.html";
                    }
                }
            });
        }

修改详情页面时出现了markdown格式符号问题

在这里插入图片描述

  1. 把这块的背景色修改成父类的背景色,在前端right、content标签下找到detail,添加
<div class="detail" id="detail" style="background-color: transparent;"></div>
  1. 在markdown官网里有使用实例,markdown to html ,然后发现都是一些效果,需要点击查看网页源代码 markdown to html,下面有个markdown : markdown -> blog.content
    修改博客详情页html:
   //$(".content .detail").text(blog.content);
   editormd.markdownToHTML("detail", {
       markdown: blog.content
   });

10. 实现编辑/删除博客

在这里插入图片描述

  • 在博客详情页面有编辑和删除两个按钮,但是在能编辑和删除的前提下是博客作者和登录用户相同才能进行这两个操作,即得先判断用户ID是否等于博客作者ID,是则显示这两个按钮,否则不显示

10.1 判断是否是登录用户

  • 在博客实体类中写一个isLoginUser表示博客作者是否是登录用户,博客详情接口新增一个判断是否是登录用户功能

BlogInfo类:

    private boolean isLoginUser;

BlogController类:修改博客详情后端接口
在这里插入图片描述

    @RequestMapping("/getBlogDetail")
    public BlogInfo getBlogDetail(Integer blogId,HttpServletRequest request) {
        log.info("根据博客ID返回博客详情....blogId:{}",blogId);
        BlogInfo blogDetail = blogService.getBlogDetail(blogId);
        // 获取token
        String token = request.getHeader(Constants.USER_TOKEN_HEADER);
        // 从token获取登录用户ID
        Integer userId = JwtUtils.getIdByToken(token);
        log.info("blogDetail:{}",blogDetail);
        //判断作者是否为登录用户
        if (userId != null && userId == blogDetail.getUserId()) {
            blogDetail.setLoginUser(true);
        }else {
            blogDetail.setLoginUser(false);
        }
        return blogDetail;
    }
  • 修改前端获取博客详情
    在获取博客详情得AJAX请求中,添加判断是否显示编辑和删除按钮:
    在这里插入图片描述
 // 判断是否显示编辑和删除按钮
 if (blog.loginUser) {
     //显示
     var html = "";
     html += '<button οnclick="window.location.href=\'blog_update.html?blogId=' + blog.id + '\'">编辑</button>';
     html += '<button οnclick="deleteBlog(' + blog.id + ')">删除</button>';
     $(".content .operating").html(html);
 }

10.2 约定前后端接口

在这里插入图片描述

  1. 先进行参数校验,编辑博客的前提是博客存在和标题或者内容不为null
  2. 调用服务层和持久层

10.3 实现服务器代码

  • 后端接口BlogController类:
    @RequestMapping("/update")
    public Result updateBlog(BlogInfo blogInfo) {
        //参数校验
        if (blogInfo.getId() == null
                || !StringUtils.hasLength(blogInfo.getTitle())
                || !StringUtils.hasLength(blogInfo.getContent())) {
            return Result.fail("博客不存在或者标题和内容为null");
        }
        blogService.update(blogInfo);
        return Result.success(true);
    }
  • BlogService类:
    public Integer update(BlogInfo blogInfo) {
        return blogMapper.updateBlog(blogInfo);
    }
  • Mapper,多态更新博客内容,通过xml来实现与数据库的交互
    Integer updateBlog(BlogInfo blogInfo);
<?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.blog.mapper.BlogMapper">
    <update id="updateBlog">
        update blog
        <set>
            <if test="title!=null">
                title = #{title},
            </if>
            <if test="content!=null">
                content = #{content},
            </if>
            <if test="userId!=null">
                user_id = #{userId},
            </if>
            <if test="deleteFlag!=null">
                delete_flag = #{deleteFlag},
            </if>
        </set>
        where id = #{id}
    </update>
</mapper>

10.4 实现客户端代码

blog_update.html:通过AJAX发送post请求

<script type="text/javascript">
	//点击更新
    function submit() {
        $.ajax({
            type: "post",
            url: "/blog/update",
            data: {
                id: $("#blogId").val(),
                title: $("#title").val(),
                content: $("#content").val()
            },
            success: function (result) {
                console.log(blogId);
                console.log(title);
                console.log(content);
                if (result.code == "SUCCESS" && result.data) {
                    // 删除成功
                    location.href = "blog_list.html";
                } else {
                    alert(result.errMsg);
                }
            },
            error: function (error) {
                if (error != null && error.status == 401) {
                    location.href = "/blog_login.html";
                }
            }
        });

    }

</script>

出现的问题:就是点击编辑跳转到博客编辑页时,原来的博客内容并没有一起带过去
所以在blog_update.html需要把博客详情数据一起带过去:

  • blog_update.html:
    function getBlogInfo() {
        $.ajax({
            type: "get",
            url: "/blog/getBlogDetail" + location.search,
            success: function (result) {
                if (result.code == "SUCCESS" && result.data != null) {
                    // 填充页面
                    var blog = result.data;
                    $("#blogId").val(blog.id);
                    $("#title").val(blog.title);
                    //$("#content").val(blog.content);
                    editormd("editor", {
                        width: "100%",
                        height: "550px",
                        path: "blog-editormd/lib/",
                        onload: function () {
                            this.watch();
                            this.setMarkdown(blog.content);
                        }
                    });

                } else {
                    alert(result.errMsg);
                }
            },
            error: function (error) {
                if (error != null && error.status == 401) {
                    location.href = "blog_login.html";
                }
            }
        });
    }
    getBlogInfo();

删除博客

在这里插入图片描述

删除博客其实实现的是进行逻辑删除,而不是真正的把数据库里面的数据进行删除,而是把那一条博客的DeleteFlag设为1

  • 后端接口实现:
    @RequestMapping("/delete")
    public boolean delete(Integer blogId) {
        log.info("删除博客,blogId:{}",blogId);
        if (blogId <= 0) {
            return false;
        }
        BlogInfo blogInfo = new BlogInfo();
        blogInfo.setId(blogId);
        blogInfo.setDeleteFlag(1);
        blogService.update(blogInfo);
        return true;
    }
  • 前端代码实现:
function deleteBlog(blogId) {
    if (confirm("确定删除吗?")) {
        $.ajax({
            type: "post",
            url: "/blog/delete" + location.search,
            success: function (result) {
                if (result.code == "SUCCESS" && result.data == true) {
                    // 删除成功
                    location.href = "blog_list.html";
                } else {
                    alert(result.errMsg);
                }
            },
            error:function(error){
                if(error != null && error.status == 401) {
                    location.href = "/blog_login.html";
                }
            }
        });
    }
}

11. 使用MD5加密

11.1 加密

在MySQL数据库中,一般都需要把密码、身份证、电话号码等信息进行加密,以确保数据的安全性。如果使用明文来存储,当数据库被入侵的时候,那么用户的这些重要信息就会泄露,从而造成很大的损失。

11.2 实现MD5加密

  • 写入加密工具类:
package com.example.blog.utils;

import org.springframework.util.DigestUtils;
import org.springframework.util.StringUtils;

import java.util.UUID;

public class SecurityUtils {
    /**
     *  加密
     * password : 用户注册时输入的密码
     * @return :数据库存储的信息: 盐值 + md5(明文+盐值)
     */
    public static String encrypt(String password) {
        // 生成盐值
        String salt = UUID.randomUUID().toString().replace("-", "");
        // 对明文+盐值 进行MD5加密
        String finalPassword = DigestUtils.md5DigestAsHex((password + salt).getBytes());
        // 存储在数据库的信息
        return salt+finalPassword;
    }

    /**
     *  验证密码是否正确
     * @param inputPassword  用户登录输入的密码
     * @param sqlPassword  数据库中password字段存储的信息 盐值 + md5(明文+盐值)
     * @return
     */
    public static boolean varify(String inputPassword,String sqlPassword) {
        if (!StringUtils.hasLength(inputPassword)) {
            return false;
        }
        if (sqlPassword == null || sqlPassword.length()!=64) {
            return false;
        }
        // 获取盐值
        String salt = sqlPassword.substring(0,32);
        // 根据用户输入的密码和盐值,进行加密 md5(明文+盐值)
        String finalPassword = DigestUtils.md5DigestAsHex((inputPassword + salt).getBytes());
        return (salt+finalPassword).equals(sqlPassword);
        // 4a9dd978dcdf426abfe421eeffab551464e313e228331b7b366c8e060b09139f
        // e148877506374d7fa5b55e371a568911d87211901ebc32baae8c37cb1aa198b9
    }
}

  • 修改登录接口:
    @RequestMapping("/login")
    public Result login(String username,String password) {
        log.info("UserController#login接收参数:username:{},password:{}",username,password);
        // 1.参数校验
        if (!StringUtils.hasLength(username) || !StringUtils.hasLength(password)) {
            return Result.fail("账号或密码不能为空");
        }
        // 2.检验密码是否正确
        UserInfo userInfo = userService.selectByName(username);
        if (userInfo == null) {
            log.error("用户不存在");
            return Result.fail("用户不存在");
        }

        // 使用加密后的密码校验
        if(!SecurityUtils.varify(password,userInfo.getPassword())) {
            log.error("密码错误");
            return Result.fail("密码错误");
        }
        // 3.密码正确,返回token
        Map<String,Object> claim = new HashMap<>();
        claim.put(Constants.TOKEN_ID,userInfo.getId());
        claim.put(Constants.TOKEN_USERNAME,userInfo.getUserName());
        String token = JwtUtils.genJwtToken(claim);
        log.info("UserController#login返回结果token:{}",token);
        return Result.success(token);
        // 密码错误,返回错误信息
    }

11.3 修改数据库密码

进行测试登录的时候,需要把数据库中的密码改为改密码生成的密文。
在这里插入图片描述

最后

本篇博客系统的前端代码就没有全部贴上了,如果有需要可以私信我,另外希望本文能给正在做该类毕业设计的读者们带来帮助

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值