[Java EE进阶] 博客系统

数据准备

复制代码
-- 建表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 + '">查看全文&gt;&gt;</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 + '">查看全文&gt;&gt;</a>';
                    finalHtml += '</div>'
                }
                $(".container .right").html(finalHtml);
            }
        }
    });
</script>

实现登录

Cookie+Session(传统会话)

  1. 当用户首次登录时 , 服务端创建 Session , 分配唯一的 SessionId , 存入服务端
  2. 服务端将 SessionId 写入浏览器的 Cookie , 返回给客户端
  3. 客户端后续请求自动携带 Cookie , 服务端根据 SessionId 查询会话 , 完成身份验证
  4. 会话存储在服务端 , 客户端只存标识

缺点 : 当项目为分布式/集群部署时 , Session 得不到共享 ; 并且 Cookie 依赖浏览器 , 对移动端不友好

下面引入 JWT 令牌

JWT : JSON Web Token ; 开放标准 RFC7519,用 JSON 格式在网络间安全传递信息,常用于登录认证、授权、跨域无状态会话

JWT 工作流程

  1. 用户首次登录通过校验 , 服务端生成加密令牌(JWT) , 令牌内部自带用户信息 , 过期时间
  2. 服务端不存储任何会话数据 , 直接把 JWT 返回给客户端
  3. 客户端手动存储 JWT , 后续请求在请求头中主动带上 Token
  4. 服务端仅做签名校验 , 过期校验 , 解析令牌获取用户信息
  5. 会话数据存储在令牌里 , 服务端无状态

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;
相关推荐
Juicedata2 小时前
JuiceFS 1.4|大规模元数据操作优化:批量删除、克隆与 Redis 缓存全解析
数据库·redis·缓存
这个DBA有点耶2 小时前
SQL改写实战(续):子查询vs JOIN的深层原理
数据库·sql
yyuuuzz2 小时前
独立站搭建的几个核心技术问题
运维·服务器·网络·数据库·aws
小蒋学算法2 小时前
redis分布式锁实现
数据库·redis·分布式
白菜欣2 小时前
【MySQL】MySQL数据的增删改查(入门版)
数据库·mysql
unicorn312 小时前
r-pan
数据库
AI人工智能+电脑小能手3 小时前
【大白话说Java面试题 第97题】【Mysql篇】第27题:说说分库与分表的设计?
java·开发语言·数据库·分布式·mysql·算法
飞函安全3 小时前
飞函Webhook能力如何帮助企业把监控告警、设备异常第一时间推到对应群组
网络·数据库·安全·私有化im