数据准备
-- 建表SQL
create database if not exists java_blog_spring charset utf8mb4;
use java_blog_spring;
-- 用户表
DROP TABLE IF EXISTS java_blog_spring.user_info;
CREATE TABLE java_blog_spring.user_info(
`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() ON UPDATE 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_info;
CREATE TABLE java_blog_spring.blog_info (
`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() ON UPDATE now(),
PRIMARY KEY (id))
ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '博客表';
-- 新增用户信息
insert into java_blog_spring.user_info (user_name, password,github_url)values("zhangsan","123456","https://gitee.com/bubble-fish666/class-java45");
insert into java_blog_spring.user_info (user_name, password,github_url)values("lisi","123456","https://gitee.com/bubble-fish666/class-java45");
insert into java_blog_spring.blog_info (title,content,user_id) values("第一篇博客","111我是博客正文我是博客正文我是博客正文",1);
insert into java_blog_spring.blog_info (title,content,user_id) values("第二篇博客","222我是博客正文我是博客正文我是博客正文",2);
位置

pom 准备
LomBook,Spring Web,MySQL Driver,Mybatis,Mybatis-Puls,jakarta.validation,JWT
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.14</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.boop</groupId>
<artifactId>blog</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>blog</name>
<description>blog</description>
<url/>
<licenses>
<license/>
</licenses>
<developers>
<developer/>
</developers>
<scm>
<connection/>
<developerConnection/>
<tag/>
<url/>
</scm>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.5</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter-test</artifactId>
<version>3.0.5</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>3.5.5</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</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 ispreferred -->
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<executions>
<execution>
<id>default-compile</id>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</path>
</annotationProcessorPaths>
</configuration>
</execution>
<execution>
<id>default-testCompile</id>
<phase>test-compile</phase>
<goals>
<goal>testCompile</goal>
</goals>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</path>
</annotationProcessorPaths>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
框架搭建



统一结果返回(部分)

package com.boop.blog.pojo.reponse;
import com.boop.blog.enums.ResultCodeEnum;
import lombok.Data;
@Data
public class Result {
private ResultCodeEnum code; //业务状态码
private String errMsg;
private Object data;
public static Result success(Object data){
Result result = new Result();
result.setCode(ResultCodeEnum.SUCCESS);
result.setData(data);
return result;
}
public static Result fail(String errMsg,Object data){
Result result = new Result();
result.setCode(ResultCodeEnum.FAIL);
result.setData(data);
result.setErrMsg(errMsg);
return result;
}
}
package com.boop.blog.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
@AllArgsConstructor
public enum ResultCodeEnum {
SUCCESS(200),
FAIL(-1);
@Getter @Setter
private int code;
}
package com.boop.blog.common.advice;
import com.boop.blog.pojo.reponse.Result;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.annotation.Resource;
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.servlet.mvc.method.annotation.ResponseBodyAdvice;
public class ResponseAdvice implements ResponseBodyAdvice {
@Resource
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 String){
return objectMapper.writeValueAsString(Result.success(body));
}if(body instanceof Result){
return body;
}
return Result.success(body);
}
}
统一异常处理
自定义异常
package com.boop.blog.common.exception;
public class BlogException extends RuntimeException{
private int code;
private String errMsg;
public BlogException(int code,String errMsg) {
this.code = code;
this.errMsg = errMsg;
}
}
异常处理
package com.boop.blog.common.advice;
import com.boop.blog.common.exception.BlogException;
import com.boop.blog.pojo.reponse.Result;
import lombok.Setter;
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;
@Slf4j
@ResponseBody
@ControllerAdvice
public class ExceptionAdvice {
@ExceptionHandler
public Result exceptionHandler(Exception e){
log.error("发生异常,e:",e);
return Result.fail(e.getMessage());
}
@ExceptionHandler
public Result exceptionHandler(BlogException e){
log.error("发生异常,e:",e);
return Result.fail(e.getMessage());
}
}
获取 blogList

BlogServiceImpl
@Service
public class BlogServiceImpl implements BlogService {
@Autowired
private BlogInfoMapper blogInfoMapper;
@Override
public List<BlogInfoResponse> getList() {
QueryWrapper<BlogInfo> queryWrapper = new QueryWrapper<>();
queryWrapper.lambda().eq(BlogInfo::getDeleteFlag,0);//条件构造器
List<BlogInfo> blogInfos = blogInfoMapper.selectList(queryWrapper);
List<BlogInfoResponse> blogListResponse = blogInfos.stream().map(blogInfo -> {
BlogListResponse response = new BlogListResponse();
BeanUtils.copyProperties(blogInfo,response);
return response;
}).collect(Collectors.toList());
return blogInfoResponse;
}
}
BlogService
public interface BlogService {
List<BlogInfoResponse> getList();
}
BlogController
@Slf4j
@RestController
@RequestMapping("/blog")
public class BlogController {
@Resource(name="blogServiceImpl")
private BlogService blogService;
@RequestMapping("/getList")
public List<BlogInfoResponse> getList(){
log.info("获取博客列表");
List<BlogInfoResponse> blogInfos = blogService.getList();
return blogInfos;
}
}
blog_list.html 前端代码
<script>
$.ajax({
type:"get",
url:"blog/getList",
success:function(body) {
if(body.code == "SUCCESS"&&body.data!=null&&body.data.length>0){
let finalHtml = "";
for(var blogInfo of body.data) {
finalHtml += '<div class="blog">';
finalHtml += '<div class="title">' + blogInfo.title + '</div>';
finalHtml += '<div class="date">' + blogInfo.createTime + '</div>';
finalHtml += '<div class="desc">' + blogInfo.content + '</div>';
finalHtml += '<a class="detail" href="blog_detail.html?blogId=' + blogInfo.id + '">查看全文>></a>';
finalHtml += '</div>'
}
$(".container .right").html(finalHtml);
}
}
});
</script>
获取博客详情

BlogController
//获取博客详情
@RequestMapping("/getBlogDetail")
public BlogInfoResponse getBlogDetail(Integer blogId){
log.info("获取博客详情");
return blogService.getBlogDetail(blogId);
}
BlogService
BlogInfoResponse getBlogDetail(Integer blogId);
BlogServiceImpl
@Override
public BlogInfoResponse getBlogDetail(Integer blogId) {
QueryWrapper<BlogInfo> queryWrapper = new QueryWrapper<>();
queryWrapper.lambda().eq(BlogInfo::getId,blogId).eq(BlogInfo::getDeleteFlag,0);
BlogInfo blogInfo = blogInfoMapper.selectOne(queryWrapper);
return BeanTransUtils.trans(blogInfo);//转换为BlogInfoResponse类型便于前端接收
}
BeanTransUtils
public class BeanTransUtils{
public static BlogInfoResponse trans(BlogInfo blogInfo){
if(blogInfo == null){
return null;
}
BlogInfoResponse response = new BlogInfoResponse();
BeanUtils.copyProperties(blogInfo,response);
return response;
}
}
将 BlogInfo 类型转为BlogInfoResponse,减少字段 , 便于前端接收
jakarta.validation(校验参数)

添加 MAVEN
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
用法 : 在需要检验的参数前加上需要的注解
在方法前加上@Validated 注解 , 在方法的参数前加上@NotNull

测试 : 是否需要添加@Validated 注解
有 @Validated 时
使用 Postman 发送请求 : GET http://127.0.0.1:8080/blog/getBlogDetail (不传参数)
校验生效了! 错误信息明确指出是参数校验问题。

去掉 @Validated 后
使用 Postman 发送请求 : GET http://127.0.0.1:8080/blog/getBlogDetail (不传参数)
校验没生效! 错误信息是乱码(业务层空指针异常),说明请求直接进入了业务层。

加入统一异常处理
@ExceptionHandler
public Result exceptionHandler(HandlerMethodValidationException e){
log.error("发生异常,e:",e);
return Result.fail("参数校验失败");
}
前端代码 blog_detail.html
<script>
$.ajax({
type:"get",
url:"blog/getList",
success:function(body) {
if(body.code == "SUCCESS"&&body.data!=null&&body.data.length>0){
let finalHtml = "";
for(var blogInfo of body.data) {
finalHtml += '<div class="blog">';
finalHtml += '<div class="title">' + blogInfo.title + '</div>';
finalHtml += '<div class="date">' + blogInfo.createTime + '</div>';
finalHtml += '<div class="desc">' + blogInfo.content + '</div>';
finalHtml += '<a class="detail" href="blog_detail.html?blogId=' + blogInfo.id + '">查看全文>></a>';
finalHtml += '</div>'
}
$(".container .right").html(finalHtml);
}
}
});
</script>
实现登录
Cookie+Session(传统会话)
- 当用户首次登录时 , 服务端创建 Session , 分配唯一的 SessionId , 存入服务端
- 服务端将 SessionId 写入浏览器的 Cookie , 返回给客户端
- 客户端后续请求自动携带 Cookie , 服务端根据 SessionId 查询会话 , 完成身份验证
- 会话存储在服务端 , 客户端只存标识
缺点 : 当项目为分布式/集群部署时 , Session 得不到共享 ; 并且 Cookie 依赖浏览器 , 对移动端不友好
下面引入 JWT 令牌
JWT : JSON Web Token ; 开放标准 RFC7519,用 JSON 格式在网络间安全传递信息,常用于登录认证、授权、跨域无状态会话
JWT 工作流程
- 用户首次登录通过校验 , 服务端生成加密令牌(JWT) , 令牌内部自带用户信息 , 过期时间
- 服务端不存储任何会话数据 , 直接把 JWT 返回给客户端
- 客户端手动存储 JWT , 后续请求在请求头中主动带上 Token
- 服务端仅做签名校验 , 过期校验 , 解析令牌获取用户信息
- 会话数据存储在令牌里 , 服务端无状态
JWT 组成


使用 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 ispreferred -->
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
接口

UserController
@Slf4j
@RequestMapping("/user")
@RestController
public class UserController {
@Resource(name = "userServiceImpl")
UserService userService;
@RequestMapping("/login")
public UserLoginResponse login(@RequestBody @Validated UserLoginRequest userLoginRequest){
log.info("用户登录");
return userService.checkPassword(userLoginRequest);
}
}
UserService
public interface UserService {
UserLoginResponse checkPassword(UserLoginRequest userLoginRequest);
}
UserServiceImpl
@Service
public class UserServiceImpl implements UserService {
@Autowired
UserInfoMapper userInfoMapper;
@Override
public UserLoginResponse checkPassword(UserLoginRequest userLoginRequest) {
QueryWrapper<UserInfo> queryWrapper = new QueryWrapper<>();
queryWrapper.lambda().eq(UserInfo::getUserName,userLoginRequest.userName)
.eq(UserInfo::getDeleteFlag,0);
//TODO 处理异常
UserInfo userInfo = userInfoMapper.selectOne(queryWrapper);
if(userInfo == null){
throw new BlogException("用户不存在");
}
//判断密码是否正确
if (!userLoginRequest.getPassword().equals(userInfo.getPassword())){
throw new BlogException("用户密码错误");
}
//密码正确
Map<String, Object> map = new HashMap<>();
map.put("id",userInfo.getId());
map.put("name", userInfo.getUserName());
String token = JwtUtils.genToken(map);
return new UserLoginResponse(userInfo.getId(), token);
}
}
JwtUtils
@Slf4j
public class JwtUtils {
//密钥
private static String SECRET_StRING = "kGmiTrem5gU1+BDOlwPssDpkP50fNObF/wygI8oEPTk=";
//生成安全密钥
private static Key key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(SECRET_StRING));
public static String genToken(Map<String, Object> claims){
String compact = Jwts.builder()
.setClaims(claims)
.signWith(key)
.compact();
log.info(compact);
return compact;
}
//验证密钥
public static Claims parseToken(String token){
if (!StringUtils.hasLength(token)){
return null;
}
//创建解析器设置签名密钥
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 claims;
}
}
Postman 测试改动
再此之后所有请求需要带上 user_token
获取方法 : 登录成功后 按 F12 跳转到开发者模式,点击 application(应用程序),本地存储


实体类 UserLoginRequest
@Data
public class UserLoginRequest {
@NotNull(message = "用户名不能为空")
@Length(max = 20)
public String userName;
@NotNull(message = "密码不能为空")
@Length(min = 5,message = "长度不得小于5")
public String password;
}
前端代码 blog_login.html (完善 js)
<script>
function login() {
$.ajax({
type:"post",
url:"user/login",
contentType:"application/json",
data:JSON.stringify({
"userName":$("#username").val(),
"password":$("#password").val()
}),
success:function (result){
if(result.code == "SUCCESS"&&result.data!=null){
let resp = result.data;
localStorage.setItem("user_token",resp.token);
localStorage.setItem("loginUserId",resp.userId);
location.assign("blog_list.html");
}else {
alert("用户名或密码错误");
return ;
}
}
});
}
</script>
common.js
将所有 ajax 的 error 抽取出来
$(document).ajaxError(function(event, xhr, options, exc) {
// 400:参数校验失败
if (xhr.status === 400) {
// 尝试解析后端返回的错误信息(Result 结构)
let msg = "参数校验失败";
try {
let res = JSON.parse(xhr.responseText);
if (res.errMsg) {
msg = res.errMsg;
}
} catch (e) {}
alert(msg);
}
// 401:未登录 / Token 失效
else if (xhr.status === 401) {
alert("请先登录或重新登录");
// 可以直接跳转到登录页
window.location.href = "/blog_login.html";
}
// 500:服务器内部错误
else if (xhr.status === 500) {
alert("服务器繁忙,请稍后再试");
}
// 其他错误
else {
alert("请求失败,状态码:" + xhr.status);
}
});
实现强制登录(拦截器)
LoginInterceptor 注册拦截器
@Component
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String userToken = request.getHeader(Constants.USER_TOKEN_HEADER_KEY);
log.info("从header中获取token:"+userToken);
if(userToken == null){
//拦截
response.setStatus(401);
return false;
}
Claims claims = JwtUtils.parseToken(userToken);
if (claims == null) {
//拦截
response.setStatus(401);
return false;
}
return true;
}
}
WebConfig 配置拦截器
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Resource
private LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/blog/**","/user/**")
.excludePathPatterns("/user/login");
}
}
前端代码 common.js
在 ajax 发送前 自动 带上 user_token
$(document).ajaxSend(function (e,xhr,opt){
let user_token = localStorage.getItem("user_token");
xhr.setRequestHeader("user_token",user_token);
});
实现显示用户信息
需求

接口


UserController
@RequestMapping("/getUserInfo")
public UserInfoResponse getUserInfo(@NotNull Integer userId){
return userService.getUserInfo(userId);
}
@RequestMapping("/getAuthorInfo")
public UserInfoResponse getAuthorInfo(@NotNull Integer blogId){
return userService.getAuthorInfo(blogId);
}
UserService
UserInfoResponse getUserInfo(Integer userId);
UserInfoResponse getAuthorInfo(Integer blogId);
UserServiceImpl
@Override
public UserInfoResponse getUserInfo(Integer userId) {
QueryWrapper<UserInfo> queryWrapper = new QueryWrapper<>();
queryWrapper.lambda().eq(UserInfo::getDeleteFlag,0)
.eq(UserInfo::getId,userId);
UserInfo userInfo = userInfoMapper.selectOne(queryWrapper);
return BeanTransUtils.trans(userInfo);
}
@Override
public UserInfoResponse getAuthorInfo(Integer blogId) {
//根据博客id,获取博客信息(作者id)
BlogInfo blogInfo = blogServiceImpl.getBlogInfo(blogId);
if(blogInfo == null||blogInfo.getUserId()<=0){
throw new BlogException("博客不存在");
}
//根据作者id,获取作者信息
return getUserInfo(blogInfo.getUserId());
}
Postman 测试 :
http://127.0.0.1:8080/user/getUserInfo?userId=1

http://127.0.0.1:8080/user/getAuthorInfo?blogId=1

前端代码
blog_list.html 获取当前用户登录的信息
function getUserInfo(){
$.ajax({
type:"get",
url:"user/getUserInfo?userId="+localStorage.getItem("loginUserId"),
success:function (result){
if(result.code == "SUCCESS"&&result.data!=null){
$(".left .card h3").text(result.data.userName);
$(".left .card a").attr("href",result.data.githubUrl);
}
}
});
}
getUserInfo();
blog_detail.html 获取当前博客作者的信息
function getUserInfo(){
$.ajax({
type:"get",
url:"user/getAuthorInfo"+location.search,
success:function (result){
if(result.code == "SUCCESS"&&result.data!=null){
$(".left .card h3").text(result.data.userName);
$(".left .card a").attr("href",result.data.githubUrl);
}
}
});
}
getUserInfo();
这两个方法本质上类似,可以提取到 common.js 中然后分别调用,此处暂时不做处理
实现用户退出
function logout(){
localStorage.removeItem("user_token")
localStorage.removeItem("loginUserId")
location.href = "/blog_login.html";
}
实现发布博客
接口

BlogController
//更新blog
@RequestMapping("/add")
public Boolean addBlog(@RequestBody @Validated AddBlogRequest addBlogRequest){
log.info("发布博客,userId:{},title:{}",addBlogRequest.getUserId(),addBlogRequest.getTitle());
return blogService.addBlog(addBlogRequest);
}
BlogService
Boolean addBlog(AddBlogRequest addBlogRequest);
BlogServiceImpl
//更新博客
@Override
public Boolean addBlog(AddBlogRequest addBlogRequest) {
BlogInfo blogInfo = BeanTransUtils.trans(addBlogRequest);
try{
Integer result = blogInfoMapper.insert(blogInfo);
if (result == 1) {
return true;
}
return false;
}catch (Exception e){
log.error("博客插入失败");
throw new BlogException("内部错误,请练习管理员");
}
}
测试接口 : http://127.0.0.1:8080/blog/add 并观察数据库

editor.md
开源 markdown 编辑器 Editor.md - 开源在线 Markdown 编辑器

直接将代码下载到本地项目
blog_edit.html 增加 submit() 方法
function submit() {
$.ajax({
type:"post",
url:"/blog/add",
contentType:"application/json",
data:JSON.stringify({
"userId":localStorage.getItem("loginUserId"),
"title":$("title").val(),
"content":$("#content").val()
}),
success:function (result){
if(result.code == "SUCCESS"&&result.data == true){
location.href = "blog_list.html";
alert("发表博客成功");
}else {
alert("发表博客失败");
}
}
});
}
blog_detail.html (修改博客详情页面显示)
添加 id 属性 , 修改背景为父 div 背景

修改博客正文内容的显示

实现删除/编辑博客
完成两个按钮对应的工作 , 删除采用逻辑删除

接口
修改博客

删除博客

BlogController
//更新blog
@RequestMapping("/update")
public Boolean updateBlog(@Validated @RequestBody UpdateBlogRequest updateBlogRequest){
log.info("更新博客 Request:"+updateBlogRequest);
return blogService.updateBlog(updateBlogRequest);
}
//删除博客
@RequestMapping("/delete")
public Boolean deleteBlog(@NotNull(message = "blogId不能为空") Integer blogId){
log.info("删除博客 blogId:"+blogId);
return blogService.delete(blogId);
}
BlogService
Boolean updateBlog(UpdateBlogRequest updateBlogRequest);
Boolean delete(Integer blogId);
BlogServiceImpl
注意逻辑删除调用的是 updateById()
@Override
public Boolean updateBlog(UpdateBlogRequest updateBlogRequest) {
BlogInfo blogInfo = BeanTransUtils.trans(updateBlogRequest);
try{
Integer result = blogInfoMapper.updateById(blogInfo);
return result == 1;
}catch (Exception e){
log.error("更新博客失败,e:",e);
throw new BlogException("内部错误,请联系管理员");
}
}
@Override
public Boolean delete(Integer blogId) {
BlogInfo blogInfo = new BlogInfo();
blogInfo.setId(blogId);
blogInfo.setDeleteFlag(1);
try{
Integer result = blogInfoMapper.updateById(blogId);
return result == 1;
}catch (Exception e){
log.error("删除博客失败,e:",e);
throw new BlogException("内部错误,请联系管理员");
}
}
blog_detial.html
注意删除 html 中的按钮

//判断是否显示编辑/删除按钮 需要注释掉html中的按钮
let loginUserId = localStorage.getItem("loginUserId");
if(result.data.userId==loginUserId){
//当前作者是登录用户, 显示按钮
let blogId = result.data.id;
let finalHtml = '<button onclick="window.location.href=\'blog_update.html?blogId='+blogId+'\'">编辑</button>';
finalHtml += '<button onclick="deleteBlog('+blogId+')">删除</button>';
console.log(finalHtml);
$(".content .operating").html(finalHtml);
}
blog_detial.html
完成 deleteBolg()方法
function deleteBlog(blogId) {
$.ajax({
type:"post",
url:"blog/delete?blogId="+blogId,
success:function (result){
alert("删除博客");
if(result.code == "SUCCESS"&&result.data == true){
alert("删除成功");
location.href = "blog_list.html";
}else {
alert("删除失败");
}
}
});
}
blog_update.html
完成发送和获取博客详情,注意需要注释掉上面的 markdown
function submit() {
$.ajax({
type:"post",
url:"blog/update",
contentType:"application/json",
data:JSON.stringify({
id:$("#blogId").val(),
title: $("#title").val(),
content: $("#content").val()
}) ,
success:function (result){
if(result.code == "SUCCESS"&&result.data == true){
alert("更新成功");
location.href = "blog_list.html";
}else {
alert("更新失败")
}
}
});
}
function getBlogInfo() {
$.ajax({
type:"get",
url:"/blog/getBlogDetail"+location.search,
success:function (result){
if(result.code == "FAIL"){
alert(result.errMsg);
return ;
}
if(result.code == "SUCCESS"&&result.data!=null){
$("#blogId").val(result.data.id);
$("#title").val(result.data.title);
// $("content").val(result.data.content);
//代替上面的markdown
editormd("editor", {
width: "100%",
height: "550px",
path: "blog-editormd/lib/",
onload: function () {
this.watch();
this.setMarkdown(result.data.content);
}
});
}
}
});
}
getBlogInfo();
加密加盐
加密 = 把明文变成看不懂的密文
- 明文:你能看懂的内容(密码:123456)
- 密文:谁都看不懂的字符串(2a10$xxx...)
- 密钥 / 盐:加密时用的 "钥匙"
如果密码明文存储在数据库中 : 容易造成数据泄露
加密算法
哈希算法(单向加密 → 专门存密码)
特点: 只能加密,不能解密 ; 相同内容 → 相同结果 ; 用于:密码存储
常用算法: MD5 ; SHA1 / SHA256 ; BCrypt(最推荐,企业标准)
对称加密(能加密也能解密)
特点: 加密和解密用**同一把钥匙 ;**速度快 ; 用于:数据传输、配置文件加密
**常用算法:**AES(最常用) ; DES
非对称加密(公钥 + 私钥)
**特点:**公钥加密 → 私钥解密 ; 安全极高 ; 用于:https、登录签名、支付
**常用算法:**RSA ; ECC
此处使用 MD5 加密算法(哈希算法)进行加密
流程 :
-
加盐 = 给密码加一串随机字符串
-
加密 = 把 明文密码 + 盐 一起做哈希
-
存数据库 = 存 哈希结果 + 盐
-
校验 = 用户输入密码 + 取出盐 → 哈希 → 对比是否一致
package com.boop.blog.common.utils;
import org.springframework.util.DigestUtils;
import org.springframework.util.StringUtils;import java.nio.charset.StandardCharsets;
import java.util.UUID;public class SecurityUtil {
/**-
加密方法
-
@param password 明文密码
-
md5(salt+明文)
-
@return 盐值 + md5(盐值+明文)
*/
public static String encrypt(String password){
// 1. 生成随机盐(UUID 去横杠,32 位)
String salt = UUID.randomUUID().toString().replace("-","");// 2. 盐 + 明文 拼接 → MD5 加密
String securityPassword = DigestUtils.md5DigestAsHex((salt+password).getBytes(StandardCharsets.UTF_8));// 3. 返回:盐(32位) + 密文(32位) = 总长度 64 位
return salt+securityPassword;
}
/**
-
校验密码
-
@param inputPassword 用户输入的明文
-
@param sqlPassword 数据库里存的 64 位串
-
@return 密码是否正确
*/
public static boolean verify(String inputPassword,String sqlPassword){
// 非空校验
if (!StringUtils.hasLength(inputPassword)){
return false;
}
// 数据库密码格式校验(必须 64 位)
if (sqlPassword==null || sqlPassword.length()!=64){
return false;
}// 1. 从数据库字符串中 截取前 32 位 = 盐
String salt= sqlPassword.substring(0,32);// 2. 用相同盐加密用户输入的密码
String securityPassword = DigestUtils.md5DigestAsHex((salt + inputPassword).getBytes(StandardCharsets.UTF_8));// 3. 对比:盐+新密文 是否 = 数据库存储的串
return sqlPassword.equals(salt+securityPassword);
}
-
修改登录接口
if (!SecurityUtil.verify(userLoginRequest.getPassword(),userInfo.getPassword())){
throw new BlogException("⽤⼾密码不正确");
}

修改数据库密码为密文
update user_info set password='e2377426880545d287b97ee294fc30ea6d6f289424b95a2b2d7f8971216e39b7'
where id=2;