【JavaEE Spring 项目】博客系统

博客系统

  • 前⾔
  • 项⽬介绍
  • 1. 准备⼯作
    • 1.1 数据准备
    • 1.2 创建项⽬
    • 1.3 准备前端⻚⾯
    • 1.4 配置配置⽂件
    • 1.5 测试
  • 2. 项⽬公共模块
    • 2.1 实体类的编写
    • 2.2 公共层
  • 3. 业务代码
    • 3.1 持久层
    • 3.2 实现博客列表
    • 3.3 实现博客详情
    • 3.4 实现登陆
      • 令牌技术
      • JWT令牌
        • 介绍
        • JWT令牌⽣成和校验
    • 3.5 实现强制要求登陆
    • 3.6 实现显⽰⽤⼾信息
    • 3.7 实现⽤⼾退出
    • 3.8 实现发布博客
      • editor.md 简单介绍
    • 3.9 实现删除/编辑博客
    • 3.10 加密/加盐

前⾔

结合前面学习的JavaEE 与 Spring 基础, 从 0 到 1 实现一个博客系统, 练习前面学习的知识!

项⽬介绍

使⽤SSM框架实现⼀个简单的博客系统

共5个⻚⾯

  1. ⽤⼾登录
  2. 博客发表⻚
  3. 博客编辑⻚
  4. 博客列表⻚
  5. 博客详情⻚

功能描述:

⽤⼾登录成功后, 可以查看所有⼈的博客. 点击 <<查看全⽂>> 可以查看该博客的正⽂内容. 如果该博客作者为当前登录⽤⼾, 可以完成博客的修改和删除操作, 以及发表新博客

⻚⾯预览

⽤⼾登录

在这里插入图片描述
博客列表⻚

在这里插入图片描述
博客详情⻚

在这里插入图片描述
博客发表/修改⻚
在这里插入图片描述

1. 准备⼯作

1.1 数据准备

建表SQL

-- 建表SQL
CREATE DATABASEIF NOT EXISTS java_blog_spring charset utf8mb4;USE java_blog_spring;-- ⽤⼾表
DROP TABLEIF 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 = INNODBDEFAULT CHARACTERSET = utf8mb4 COMMENT = '⽤⼾表';-- 博客表
DROP TABLEIF 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 = INNODBDEFAULT CHARSET = utf8mb4 COMMENT = '博客表';-- 新增⽤⼾信息
INSERT INTO java_blog_spring.USER (user_name,PASSWORD,github_url)
VALUES ("zhangsan","123456","https://gitee.com/bubblefish666/class-java45");INSERT INTO java_blog_spring.USER (user_name,PASSWORD,github_url)
VALUES ("lisi","123456","https://gitee.com/bubblefish666/class-java45");INSERT INTO java_blog_spring.blog (title, content, user_id)
VALUES ("第一篇博客","111我是博客正文我是博客正文我是博客正文",1);INSERT INTO java_blog_spring.blog (title, content, user_id)
VALUES ("第二篇博客","222我是博客正⽂我是博客正文我是博客正文",2);

1.2 创建项⽬

创建SpringBoot项⽬, 添加Spring MVC 和MyBatis对应依赖

在这里插入图片描述

1.3 准备前端⻚⾯

前端页面代码提取仓库

在这里插入图片描述

1.4 配置配置⽂件

spring:datasource:url: jdbc:mysql://127.0.0.1:3306/trans_test?characterEncoding=utf8&useSSL=falseusername: rootpassword: 123456driver-class-name: com.mysql.cj.jdbc.Drivermybatis:configuration: # 配置打印 MyBatis日志log-impl: org.apache.ibatis.logging.stdout.StdOutImplmap-underscore-to-camel-case: true #配置驼峰自动转换# 配置 mybatis xml 的文件路径,在 resources/mapper 创建所有表的 xml 文件mapper-locations: classpath:mapper/**Mapper.xml

1.5 测试

测试程序启动后, 是否可以正常访问前端页面

http://127.0.0.1:8080/blog_login.html

在这里插入图片描述
前端⻚⾯可以正确显⽰, 说明项⽬初始化成功.

2. 项⽬公共模块

项⽬分为控制层(Controller), 服务层(Service), 持久层(Mapper). 各层之间的调⽤关系如下:

在这里插入图片描述
我们先根据需求完成实体类和公共层代码的编写

2.1 实体类的编写

package cn.edu.zxj.springblog.model;import lombok.Data;import java.util.Date;/*** Created with IntelliJ IDEA.* Description:博客信息相关的实体类** @author: zxj* @date: 2024-02-04* @time: 17:42:19*/
@Data
public class BlogInfo {private Integer id;private String title;private String content;private Integer userId;private Integer deleteFlag;private Date createTime;private Date updateTime;
}
package cn.edu.zxj.springblog.model;import lombok.Data;import java.util.Date;/*** Created with IntelliJ IDEA.* Description:用户相关的实体类** @author: zxj* @date: 2024-02-04* @time: 17:44:23*/
@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;
}

2.2 公共层

  1. 统⼀返回结果实体类
    • code: 业务状态码
      • 200: 业务处理成功
      • -1 : 业务处理失败
      • -2 : ⽤⼾未登录
      • 后续有其他异常信息, 可以再补充.
    • msg: 业务处理失败时, 返回的错误信息
    • data: 业务返回数据

定义业务状态码

package cn.edu.zxj.springblog.common;/*** Created with IntelliJ IDEA.* Description:定义业务状态码** @author: zxj* @date: 2024-02-04* @time: 17:49:06*/
public class Constants {public static final Integer RESULT_CODE_SUCCESS = 200;public static final Integer RESULT_CODE_FAIL = -1;public static final Integer RESULT_CODE_UN_LOGIN = -2;
}
package cn.edu.zxj.springblog.model;import cn.edu.zxj.springblog.common.Constants;
import lombok.Data;/*** Created with IntelliJ IDEA.* Description:统一返回结果的实体类:** @author: zxj* @date: 2024-02-04* @time: 17:51:46*/
@Data
public class Result<T> {// 业务状态码private Integer code;// 错误描述private String errorMessage;// 返回的数据private T data;/*** @description: 业务处理流程成功**/public static <T> Result<T> success(T data) {Result<T> result = new Result<>();result.setCode(Constants.RESULT_CODE_SUCCESS);result.setData(data);result.setErrorMessage("");return result;}/*** @description: 业务处理流程失败**/public static <T> Result<T> fail(String errorMessage) {Result<T> result = new Result<>();result.setCode(Constants.RESULT_CODE_SUCCESS);result.setErrorMessage(errorMessage);return result;}/*** @description: 业务处理流程失败, 失败时带回一些数据**/public static <T> Result<T> fail(String errorMessage,T data) {Result<T> result = new Result<>();result.setCode(Constants.RESULT_CODE_SUCCESS);result.setData(data);result.setErrorMessage(errorMessage);return result;}/*** @description: 用户未登录**/public static <T> Result<T> fail() {Result<T> result = new Result<>();result.setCode(Constants.RESULT_CODE_UN_LOGIN);result.setErrorMessage("用户未登录");return result;}
}
  1. 统⼀返回结果
package cn.edu.zxj.springblog.config;import cn.edu.zxj.springblog.model.Result;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.SneakyThrows;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;/*** Created with IntelliJ IDEA.* Description:设置统一返回类** @author: zxj* @date: 2024-02-04* @time: 18:00:36*/
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {@Overridepublic boolean supports(MethodParameter returnType, Class converterType) {// 启用统一结果返回return true;}@SneakyThrows@Overridepublic Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {// 对body进行处理if (body instanceof Result) {return body;}// 对 String 类型特殊处理if (body instanceof String) {ObjectMapper objectMapper = new ObjectMapper();return objectMapper.writeValueAsString(Result.success(body)); }return Result.success(body);}
}
  1. 统⼀异常处理
package cn.edu.zxj.springblog.config;import cn.edu.zxj.springblog.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;/*** Created with IntelliJ IDEA.* Description:** @author: zxj* @date: 2024-02-04* @time: 18:05:44*/
@ControllerAdvice
@Slf4j
@ResponseBody
public class ErrorAdvice {@ExceptionHandlerpublic Result exceptionAdvice(Exception e) {log.error("发生错误, e: {}", e);return Result.fail("内部发生错误, 请联系管理员");}
}

3. 业务代码

3.1 持久层

根据需求, 先⼤致计算有哪些DB相关操作, 完成持久层初步代码, 后续再根据业务需求进⾏完善

  1. ⽤⼾登录
    • 依据用户名来查找用户信息
  2. 博客发表⻚
    • 根据id查询user信息
    • 获取所有博客列表
  3. 博客编辑⻚
    • 根据博客ID查询博客 信息
    • 根据博客ID删除博客(修改 delete_flag = 1)
  4. 博客列表⻚
    • 根据博客ID修改博客信息
  5. 发表博客
    • 插入新的

依据上述分析的数据操作写mapper层的代码

package cn.edu.zxj.springblog.mapper;import cn.edu.zxj.springblog.model.BlogInfo;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;import java.util.List;/*** Created with IntelliJ IDEA.* Description:针对Blog相关的数据库操作** @author: zxj* @date: 2024-02-04* @time: 18:15:52*/
@Mapper
public interface BlogInfoMapper {/*** @description: 获取所有博客列表**/@Select("select id, title, content,user_id,delete_flag,create_time,update_time " +"from blog where delete_flag = 0")List<BlogInfo> selectAll();/*** @description: 根据博客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);/*** @description: 删除博客, 修改 delete_flag 字段为1**/@Update("update blog set delete_flag = 1 where id = #{id}")Integer delete(Integer id);/*** @description: 编辑博客**/Integer update(BlogInfo blogInfo);/*** @description: 插入新的博客**/@Insert("insert into blog (title, content, user_id) values (#{title},#{content},#{userId})")Integer insert(BlogInfo blogInfo);
}
package cn.edu.zxj.springblog.mapper;import cn.edu.zxj.springblog.model.UserInfo;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;/*** Created with IntelliJ IDEA.* Description:针对 user 相关的数据库操作** @author: zxj* @date: 2024-02-04* @time: 18:15:44*/
@Mapper
public interface UserInfoMapper {/*** @description: 依据用户名查询用户信息**/@Select("select id, user_name, password, github_url, delete_flag, create_time, update_time" +" from user where delete_flag = 0 and  user_name = #{name}")UserInfo selectByName(String name);/*** @description: 已经 ID 查询用户信息**/@Select("select id, user_name, password, github_url, delete_flag, create_time, update_time" +" from user where delete_flag = 0 and  id = #{id}")UserInfo selectById(Integer id);}

BlogInfoMapper.xml 相关内容

<?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="cn.edu.zxj.springblog.mapper.BlogInfoMapper"><update id="update">update blog<set><if test="content != null">content = #{content},</if><if test="title != null">title = #{title}</if></set>where id = #{id}</update></mapper>

书写测试用例, 确保 Mapper层的代码的正确性

package cn.edu.zxj.springblog.mapper;import cn.edu.zxj.springblog.model.BlogInfo;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;import static org.junit.jupiter.api.Assertions.*;/*** Created with IntelliJ IDEA.* Description:测试 BlogInfoMapper** @author: zxj* @date: 2024-02-04* @time: 18:33:27*/
@SpringBootTest
class BlogInfoMapperTest {@Autowiredprivate BlogInfoMapper blogInfoMapper;@Testvoid selectAll() {System.out.println(blogInfoMapper.selectAll());}@Testvoid selectById() {System.out.println(blogInfoMapper.selectById(1));}@Testvoid delete() {System.out.println(blogInfoMapper.delete(3));}@Testvoid update() {BlogInfo blogInfo = new BlogInfo();blogInfo.setTitle("测试添加博客111111111000");blogInfo.setContent("测试内容222000");blogInfo.setId(3);blogInfoMapper.update(blogInfo);}@Testvoid insert() {BlogInfo blogInfo = new BlogInfo();blogInfo.setTitle("测试添加博客");blogInfo.setContent("测试内容");blogInfo.setUserId(1);blogInfoMapper.insert(blogInfo);}
}
package cn.edu.zxj.springblog.mapper;import cn.edu.zxj.springblog.model.UserInfo;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;import static org.junit.jupiter.api.Assertions.*;/*** Created with IntelliJ IDEA.* Description:测试 UserInfoMapper** @author: zxj* @date: 2024-02-04* @time: 18:33:37*/
@SpringBootTest
class UserInfoMapperTest {@Autowiredprivate UserInfoMapper userInfoMapper;@Testvoid selectByName() {UserInfo userInfo = userInfoMapper.selectByName("zhangsan");System.out.println(userInfo);}@Testvoid selectById() {System.out.println(userInfoMapper.selectById(6));}
}

3.2 实现博客列表

约定前后端交互接⼝

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

实现服务器代码

Controller 层

    @RequestMapping("/getList")public List<BlogInfo> getList() {log.info("接收到获取所有博客信息请求");return blogInfoService.getList();}

Service 层

    public List<BlogInfo> getList() {try {return blogInfoMapper.selectAll();} catch (Exception e){log.error("查询 blog 所有信息失败, e: {}",e);}return null;}

实现客⼾端代码

修改 blog_list.html, 删除之前写死的博客内容(即 <div class=“blog”> ), 并新增 js 代码处理ajax 请求.

  • 使⽤ ajax 给服务器发送 HTTP 请求.
  • 服务器返回的响应是⼀个 JSON 格式的数据, 根据这个响应数据使⽤ DOM API 构造⻚⾯内容.
  • 响应中的 postTime 字段为 ms 级时间戳, 需要转成格式化⽇期.
  • 跳转到博客详情⻚的 url 形如 blog_detail.html?blogId=1 这样就可以让博客详情⻚知道当前是要访问哪篇博客.

在这里插入图片描述

此时⻚⾯的⽇期显⽰为时间戳, 我们从后端也⽇期进⾏处理

    public static String dateFormat(Date date){// 创建SimpleDateFormat对象,并指定想要的日期格式SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");// 使用format()方法将Date对象转换为字符串return dateFormat.format(date);}

重写获取博客创建时间
在这里插入图片描述
通过 URL http://127.0.0.1:8080/blog_list.html 访问服务器, 验证效果

在这里插入图片描述

3.3 实现博客详情

⽬前点击博客列表⻚的 “查看全⽂” , 能进⼊博客详情⻚, 但是这个博客详情⻚是写死的内容. 我们期望能够根据当前的 博客 id 从服务器动态获取博客内容.

约定前后端交互接⼝

在这里插入图片描述

实现服务器代码

Controller 层

    @RequestMapping("/getBlogDetail")public BlogInfo getBlogDetail(Integer blogId) {log.info("接收到获取博客详细信息请求, blogId: {}",blogId);// 1. 参数校验if (blogId < 1) {return null;}// 2. 进行服务return blogInfoService.getBlogDetail(blogId);}

Service 层

    public BlogInfo getBlogDetail(Integer blogId) {try {return blogInfoMapper.selectById(blogId);} catch (Exception e) {log.error("查询 blogId: {} 详细信息失败, e: {}", blogId, e);}return null;}

部署程序, 验证服务器是否能正确返回数据

在这里插入图片描述

实现客⼾端代码

修改 blog_content.html

  • 根据当前⻚⾯ URL 中的 blogId 参数(使⽤ location.search 即可得到形如 ?blogId=1 的数据), 给服务器发送 GET /blog 请求.
  • 根据获取到的响应数据, 显⽰在⻚⾯上

在这里插入图片描述

3.4 实现登陆

分析

传统思路:

  • 登录页面的用户名密码提交给服务器.
  • 服务器验证用户密码是否正确, 并返回校验结果给后端
  • 如果密码正确, 则在服务器端创建 Session, 通过 Cookie 把 sessionId 返回给游览器.

问题:

集群环境下⽆法直接使⽤Session.

原因分析:

我们开发的项⽬, 在企业中很少会部署在⼀台机器上, 容易发⽣单点故障. (单点故障: ⼀旦这台服务器挂了, 整个应⽤都没法访问了). 所以通常情况下, ⼀个Web应⽤会部署在多个服务器上, 通过Nginx等进⾏负载均衡. 此时, 来⾃⼀个⽤⼾的请求就会被分发到不同的服务器上.

在这里插入图片描述

假如我们使⽤Session进⾏会话跟踪, 我们来思考如下场景:

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

在这里插入图片描述

接下来我们介绍第三种⽅案: 令牌技术

令牌技术

令牌其实就是⼀个⽤⼾⾝份的标识, 名称起的很⾼⼤上, 其实本质就是⼀个字符串.

⽐如我们出⾏在外, 会带着⾃⼰的⾝份证, 需要验证⾝份时, 就掏出⾝份证⾝份证不能伪造, 可以辨别真假.

在这里插入图片描述

服务器具备⽣成令牌和验证令牌的能⼒

我们使⽤令牌技术, 继续思考上述场景:

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

令牌的优缺点

优点:

  • 解决了集群环境下的认证问题
  • 减轻服务器的存储压⼒(⽆需在服务器端存储)
    缺点:
    需要⾃⼰实现(包括令牌的⽣成, 令牌的传递, 令牌的校验)

当前企业开发中, 解决会话跟踪使⽤最多的⽅案就是令牌技术.

JWT令牌

令牌本质就是⼀个字符串, 他的实现⽅式有很多, 我们采⽤⼀个JWT令牌来实现.

介绍

JWT全称: JSON Web Token

官⽹: https://jwt.io/

JWT组成

JWT由三部分组成, 每部分中间使⽤点 (.) 分隔,⽐如:aaaaa.bbbbb.cccc

  • Header(头部) 头部包括令牌的类型(即JWT)及使⽤的哈希算法(如HMAC SHA256或RSA)
  • Payload(负载) 负载部分是存放有效信息的地⽅, ⾥⾯是⼀些⾃定义内容.
  • ⽐如:{“userId”:“123”,“userName”:“zhangsan”} , 也可以存在jwt提供的现场字段, ⽐如exp(过期时间戳)等.

此部分不建议存放敏感信息, 因为此部分可以解码还原原始内容.

  • Signature(签名) 此部分⽤于防⽌jwt内容被篡改, 确保安全性.

防⽌被篡改, ⽽不是防⽌被解析.
JWT之所以安全, 就是因为最后的签名. jwt当中任何⼀个字符被篡改, 整个令牌都会校验失败.
就好⽐我们的⾝份证, 之所以能标识⼀个⼈的⾝份, 是因为他不能被篡改, ⽽不是因为内容加密.(任何⼈都可以看到⾝份证的信息, jwt 也是)

在这里插入图片描述

对上⾯部分的信息, 使⽤Base64Url 进⾏编码, 合并在⼀起就是jwt令牌
Base64是编码⽅式,⽽不是加密⽅式

JWT令牌⽣成和校验
  1. 引⼊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 is preferred --><version>0.11.5</version><scope>runtime</scope>
</dependency>
  1. 使⽤Jar包中提供的API来完成JWT令牌的⽣成和校验

⽣成令牌

package cn.edu.zxj.springblog;import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;import javax.crypto.SecretKey;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;/*** Created with IntelliJ IDEA.* Description:jwt 学习示例** @author: zxj* @date: 2024-02-04* @time: 22:18:59*/
@SpringBootTest
public class JWTUtilsTest {// 过期时间, 单位是 ms, 设置为 30 分钟private static final Long Expiration = 30*60*1000L;// 密钥private static final String secretString = "123456";// 生成安全密钥private static final SecretKey KEY = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretString));/*** @description: 生成令牌**/@Testpublic void genJWT() {Map<String,Object> claim = new HashMap<>();claim.put("name","zhangsan");claim.put("id",1);String token = Jwts.builder().setClaims(claim).setIssuedAt(new Date()).setExpiration(new Date(System.currentTimeMillis() + Expiration)).signWith(KEY).compact();System.out.println(token);}}
  • 注意: 对于密钥有⻓度和内容有要求, 建议使⽤, 密钥太短会报错在这里插入图片描述

  • io.jsonwebtoken.security.Keys#secretKeyFor(signaturealgalgorithm)⽅法来创建⼀个密钥

package cn.edu.zxj.springblog;import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.io.Encoders;
import io.jsonwebtoken.security.Keys;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;import javax.crypto.SecretKey;
import java.security.Key;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;/*** Created with IntelliJ IDEA.* Description:jwt 学习示例** @author: zxj* @date: 2024-02-04* @time: 22:18:59*/
@SpringBootTest
public class JWTUtilsTest {// 过期时间, 单位是 ms, 设置为 30 分钟private static final Long Expiration = 30*60*1000L;// 密钥private static final String secretString = "M6v2NVNUWsHCXB20foSSCquBYMrleVuCbXqVW5fWIgM=";// 生成安全密钥private static final SecretKey KEY = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretString));/*** @description: 生成令牌**/@Testpublic void genJWT() {Map<String,Object> claim = new HashMap<>();claim.put("name","zhangsan");claim.put("id",1);String token = Jwts.builder().setClaims(claim).setIssuedAt(new Date()).setExpiration(new Date(System.currentTimeMillis() + Expiration)).signWith(KEY).compact();System.out.println(token);}/*** @description: 生成密钥**/@Testpublic void genKey() {Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256);String secretStr= Encoders.BASE64.encode(key.getEncoded());// 利用上述得到安全复杂的 secretStringSystem.out.println(secretStr);}public static void main(String[] args) {Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256);String secretStr= Encoders.BASE64.encode(key.getEncoded());// 利用上述得到安全复杂的 secretStringSystem.out.println(secretStr);}}

运行程序, 就可以生成 token

在这里插入图片描述
在这里插入图片描述

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

校验令牌

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

    /*** @description: 验证 token 的合法性, 解析 token**/@Testpublic void parseJWT() {String token = "eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiemhhbmdzYW4iLCJpZCI6MSwiaWF0IjoxNzA3MDU3Njk2LCJleHAiOjE3MDcwNTk0OTZ9.4xXmir0P5cBGnS0z-fT39MzuhY9ACV8Hjt2yF5Mtgp4";// 创建解析器, 设置签名密钥JwtParserBuilder jwtParserBuilder = Jwts.parserBuilder().setSigningKey(KEY);// 解析tokenClaims claims = jwtParserBuilder.build().parseClaimsJws(token).getBody();System.out.println(claims);}

在这里插入图片描述

令牌解析后, 我们可以看到⾥⾯存储的信息,如果在解析的过程当中没有报错,就说明解析成功了.

令牌解析时, 也会进⾏时间有效性的校验, 如果令牌过期了, 解析也会失败.
修改令牌中的任何⼀个字符, 都会校验失败, 所以令牌⽆法篡改

学习令牌的使⽤之后, 接下来我们通过令牌来完成⽤⼾的登录

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

在这里插入图片描述
约定前后端交互接⼝

在这里插入图片描述
实现服务器代码

创建JWT⼯具类

package cn.edu.zxj.springblog.utils;import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtParserBuilder;
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;/*** Created with IntelliJ IDEA.* Description:jwt 生成 token, 并检验 token 中的内容** @author: zxj* @date: 2024-02-04* @time: 22:58:08*/
@Slf4j
public class JWTUtils {// 过期时间, 单位是 ms, 设置为 30 分钟private static final Long Expiration = 30*60*1000L;// 密钥, 可以调用下面 genKey 生成, 并复制粘贴private static final String secretString = "M6v2NVNUWsHCXB20foSSCquBYMrleVuCbXqVW5fWIgM=";// 生成安全密钥private static final SecretKey KEY = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretString));/*** @description: 生成令牌**/public static String genJWT(Map<String,Object> claim) {String token = Jwts.builder().setClaims(claim) // 自定义内容(负载).setIssuedAt(new Date()) // 设置签名时间.setExpiration(new Date(System.currentTimeMillis() + Expiration)) // 设置过期时间.signWith(KEY) // 签名算法.compact();return token;}/*** @description: 验证 token 的合法性, 解析 token**/public static Claims parseJWT(String token) {if (token == null) {return null;}// 创建解析器, 设置签名密钥JwtParserBuilder jwtParserBuilder = Jwts.parserBuilder().setSigningKey(KEY);Claims claims = null;try {// 解析tokenclaims = jwtParserBuilder.build().parseClaimsJws(token).getBody();} catch (Exception e) {// 签名认证失败log.error("解析令牌失败, token: {}", token);}return claims;}/*** @description: 生成密钥**/private static void genKey() {Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256);String secretStr= Encoders.BASE64.encode(key.getEncoded());// 利用上述得到安全复杂的 secretStringSystem.out.println(secretStr);}/*** @description: 从 token 中获取 id**/public static Integer getUserIdFromToken(String jwtToken) {Claims claims = parseJWT(jwtToken);if (claims != null) {Map<String,Object> map = new HashMap<>(claims);return (Integer) map.get("id");}return null;}}

创建 UserInfoController, 实现 login 路径业务

@RequestMapping("/login")public Result login(String username, String password) {log.info("接收到用户登录请求, username: {}", username);// 1. 参数合法校验if (!StringUtils.hasLength(username)|| !StringUtils.hasLength(password)) {return Result.fail("参数存在问题");}// 2. 判断是否正确// 2.1 调用数据库查询用户信息UserInfo userInfo = userInfoService.selectByUsername(username);// 2.2 判断密码是否正确if (userInfo == null || !password.equals(userInfo.getPassword())) {return Result.fail("用户或者密码错误");}// 3. 生成 token 并返回给前端// 3.1 提取 userInfo 中的相关信息, 存储到 token 中Map<String,Object> claim = new HashMap<>();claim.put("id",userInfo.getId());claim.put("userName",userInfo.getUserName());// 3.2 利用 claim 存储到 token 中String token = JWTUtils.genJWT(claim);log.info("依据 claim: {}, 生成 token: {}",claim,token);return Result.success(token);}

Service 层

    public UserInfo selectByUsername(String username) {try {return userInfoMapper.selectByName(username);} catch (Exception e) {log.error("通过用户名查询用户信息出现错误, e: {}",e);}return null;}

实现客⼾端代码

修改 login.html, 完善登录⽅法

前端收到token之后, 保存在localstorage中

在这里插入图片描述

local storage相关操作
存储数据
localStorage.setItem("user_token","value");
读取数据
localStorage.getItem("user_token");
删除数据
localStorage.removeItem("user_token");

部署程序, 验证效果.

在这里插入图片描述

3.5 实现强制要求登陆

当⽤⼾访问 博客列表⻚ 和 博客详情⻚ 时, 如果⽤⼾当前尚未登陆, 就⾃动跳转到登陆⻚⾯.

我们可以采⽤拦截器来完成, token通常由前端放在header中, 我们从header中获取token, 并校验token是否合法

添加拦截器

package cn.edu.zxj.springblog.config;import cn.edu.zxj.springblog.utils.JWTUtils;
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.HandlerInterceptor;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;/*** Created with IntelliJ IDEA.* Description:登录拦截器** @author: zxj* @date: 2024-02-04* @time: 23:45:34*/
@Configuration
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {log.info("正在进行登录拦截校验...");// 1. 从 header 中获得 tokenString token = request.getHeader("user_token");log.info("从 request 中获得 token: {}",token);// 2. 验证 tokenClaims claims = JWTUtils.parseJWT(token);if (claims == null) {// 该 token 是不合法的, 未登录状态, 不放行response.setStatus(401);return false;}// 走到这里, token 合法, 放行return true;}
}
package cn.edu.zxj.springblog.config;import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;import java.util.Arrays;
import java.util.List;/*** Created with IntelliJ IDEA.* Description:登录拦截器的注册** @author: zxj* @date: 2024-02-04* @time: 23:51:25*/
@Configuration
@Slf4j
public class WebConfig implements WebMvcConfigurer {private static final List<String> excludePath = Arrays.asList("/user/login","/**/*.html","/css/**","/blog-editormd/**","/js/**","/pic/**");@AutowiredLoginInterceptor loginInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(loginInterceptor).addPathPatterns("/**").excludePathPatterns(excludePath);}
}

实现客⼾端代码

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

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

  • event - 包含 event 对象
  • xhr - 包含 XMLHttpRequest 对象
  • options - 包含 AJAX 请求中使⽤的选项
  1. 修改 blog_datail.html
  • 访问⻚⾯时, 添加失败处理代码
  • 使⽤ location.assign 进⾏⻚⾯跳转
error: function (error) {console.log(error);if (error != null && error.status == 401) {alert("用户未登录, 即将跳转到登录界面");// 已经被拦截器拦截了, 未登录location.href = "blog_login.html";}}

3.6 实现显⽰⽤⼾信息

⽬前⻚⾯的⽤⼾信息部分是写死的. 形如:

在这里插入图片描述
我们期望这个信息可以随着⽤⼾登陆⽽发⽣改变.

  • 如果当前⻚⾯是博客列表⻚, 则显⽰当前登陆⽤⼾的信息.
  • 如果当前⻚⾯是博客详情⻚, 则显⽰该博客的作者⽤⼾信息.

注意: 当前我们只是实现了显⽰⽤⼾名, 没有实现显⽰⽤⼾的头像以及⽂章数量等信息.

约定前后端交互接⼝

在博客列表⻚, 获取当前登陆的⽤⼾的⽤⼾信息.

在这里插入图片描述

在博客详情⻚, 获取当前⽂章作者的⽤⼾信息

在这里插入图片描述
实现服务器代码

在 UserController添加代码

@RequestMapping("/getUserInfo")public Result getUserInfo(HttpServletRequest request) {log.info("收到获取用户登录信息请求...");// 1. 提取token中的用户IDString token = request.getHeader("user_token");Integer id = JWTUtils.getUserIdFromToken(token);if (id == null || id < 1) {return Result.fail("用户登录状态异常");}// 2. 业务处理UserInfo userInfo = userInfoService.selectById(id);if (userInfo == null) {return Result.fail("用户查询异常");}return Result.success(userInfo);}
/*** @description: 依据博客id 查询 博客信息中的作者 user_id -> 作者信息**/@RequestMapping("/getAuthorInfo")public Result getAuthorInfo(Integer blogId) {log.info("接收到查询博客作者信息请求, blogId", blogId);if (blogId == null || blogId < 1) {return Result.fail("参数存在问题");}UserInfo userInfo = userInfoService.getAuthorInfo(blogId);if (userInfo == null || userInfo.getId() < 1) {return Result.fail("查询用户信息失败");}return Result.success(userInfo);}

Mapper 层

    public UserInfo getAuthorInfo(Integer blogId) {try {BlogInfo blogInfo = blogInfoMapper.selectById(blogId);Integer userId = blogInfo.getUserId();return userInfoMapper.selectById(userId);} catch (Exception e) {log.error("依据博客Id获取作者信息, 查询数据库出现错误, e: {}", e);}return null;}

实现客⼾端代码

  1. 修改 blog_list.html
  • 在响应回调函数中, 根据响应中的⽤⼾名, 更新界⾯的显⽰.
    在这里插入图片描述
  1. 修改 blog_detail.html

修改⽅式同上

在这里插入图片描述
部署程序, 验证效果.

代码整合: 提取common.js

在这里插入图片描述
引⼊common.js

在这里插入图片描述
blog_list.html 代码修改

在这里插入图片描述
blog_detail.html 代码修改

在这里插入图片描述

3.7 实现⽤⼾退出

前端直接清除掉token即可.

实现客⼾端代码

<<注销>> 链接已经提前添加了onclick事件

在common.js中完善logout⽅法

在这里插入图片描述

3.8 实现发布博客

约定前后端交互接⼝

在这里插入图片描述
实现服务器代码

修改 BlogController, 新增 add ⽅法.

@RequestMapping("/add")public Boolean add(BlogInfo blogInfo, HttpServletRequest request) {log.info("接收到添加博客信息请求, blogInfo: {}",blogInfo);// 参数校验if (blogInfo == null) {return false;}// 1. 获取 token 中 UserIdString token = request.getHeader("user_token");Claims claims = JWTUtils.parseJWT(token);if (claims == null) {return false;}Map<String,Object> map = new HashMap<>(claims);Integer userId = (Integer) map.get("id");// 2. 完善 blogInfo 中的信息blogInfo.setUserId(userId);// 3. 处理服务Integer ret = blogInfoService.add(blogInfo);if (ret == null || ret < 0) {return false;}return true;}

BlogService 添加对应的处理逻辑

    public Integer add(BlogInfo blogInfo) {try {return blogInfoMapper.insert(blogInfo);} catch (Exception e) {log.error("插入 blogInfo: {} 失败, e: {}", blogInfo, e);}return null;}

editor.md 简单介绍

editor.md 是⼀个开源的⻚⾯ markdown 编辑器组件.

官⽹参⻅: http://editor.md.ipandao.com/
代码: https://pandao.github.io/editor.md/

实现客⼾端代码

修改 blog_edit.html

  • 完善submit⽅法

在这里插入图片描述
修改详情⻚⻚⾯显⽰

此时会发现详情⻚会显⽰markdown的格式符号, 我们对⻚⾯进⾏也下处理

在这里插入图片描述

  1. 修改 html 部分, 把博客正⽂的 div 标签, 改成
    并且加上style=“background-color: transparent;”

在这里插入图片描述
2. 修改博客正⽂内容的显⽰

在这里插入图片描述

3.9 实现删除/编辑博客

进⼊⽤⼾详情⻚时, 如果当前登陆⽤⼾正是⽂章作者, 则在导航栏中显⽰ [编辑] [删除] 按钮, ⽤⼾点击时则进⾏相应处理.

需要实现两件事:

  • 判定当前博客详情⻚中是否要显⽰[编辑] [删除] 按钮
  • 实现编辑/删除逻辑

删除采⽤逻辑删除, 所以和编辑其实为同⼀个接⼝

约定前后端交互接⼝

  1. 判定是否要显⽰[编辑] [删除] 按钮

修改之前的 获取博客 信息的接⼝, 在响应中加上⼀个字段.

  • loginUser 为 1 表⽰当前博客就是登陆⽤⼾⾃⼰写的.

在这里插入图片描述

  1. 修改博客

在这里插入图片描述

  1. 删除博客
    在这里插入图片描述

实现服务器代码

  1. 给 BlogInfo 类新增⼀个字段

在这里插入图片描述

  1. 修改 BlogController

其他代码不变. 只处理 “getBlogDeatail” 中的逻辑.

 @RequestMapping("/getBlogDetail")public BlogInfo getBlogDetail(Integer blogId, HttpServletRequest request) {log.info("接收到获取博客详细信息请求, blogId: {}",blogId);// 1. 参数校验if (blogId < 1) {return null;}// 2. 获取当前登录的IdString token = request.getHeader("user_token");if (token == null) {return null;}Integer loginId = JWTUtils.getUserIdFromToken(token);// 3. 进行服务BlogInfo blogInfo = blogInfoService.getBlogDetail(blogId);if (blogInfo == null) {return null;}// 4. 设置 LoginUser 字段if (loginId != null && loginId.equals(blogInfo.getUserId())) {blogInfo.setLoginUser(1);}return blogInfo;}
  1. 修改 BlogController

增加 更新删除 功能

 @RequestMapping("/update")public Result update(BlogInfo blogInfo) {log.info("接收到更新博客信息请求, blogInfo: {}",blogInfo);// 参数校验if (blogInfo == null) {return Result.fail("参数存在问题",false);}// 业务处理Integer ret = blogInfoService.update(blogInfo);if (ret == null || ret < 0) {return Result.fail("更新博客出现问题",false);}return Result.success(true);}@RequestMapping("/delete")public Result delete(Integer blogId) {log.info("接收到删除博客请求, blogId: {}",blogId);// 参数校验if (blogId == null) {return Result.fail("参数存在问题",false);}// 业务处理Integer ret = blogInfoService.delete(blogId);if (ret == null || ret < 0) {return Result.fail("删除博客出现问题",false);}return Result.success(true);}

Service 层

public Integer update(BlogInfo blogInfo) {try {return blogInfoMapper.update(blogInfo);} catch (Exception e) {log.error("更新 blogInfo: {} 失败, e: {}", blogInfo, e);}return null;}public Integer delete(Integer blogId) {try {return blogInfoMapper.delete(blogId);} catch (Exception e) {log.error("删除 blogId: {} 失败, e: {}", blogId, e);}return null;}

实现客⼾端代码

  1. 判断是否显⽰[编辑] [删除]按钮

在这里插入图片描述
在这里插入图片描述

编辑博客逻辑:

修改blog_update.html⻚⾯加载时,

请求博客详情

在这里插入图片描述

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

在这里插入图片描述

完善发表博客的逻辑

在这里插入图片描述

3.10 加密/加盐

加密介绍

在MySQL数据库中, 我们常常需要对密码, ⾝份证号, ⼿机号等敏感信息进⾏加密, 以保证数据的安全性.如果使⽤明⽂存储, 当⿊客⼊侵了数据库时, 就可以轻松获取到⽤⼾的相关信息, 从⽽对⽤⼾或者企业造成信息泄漏或者财产损失.

⽬前我们⽤⼾的密码还是明⽂设置的, 为了保护⽤⼾的密码信息, 我们需要对密码进⾏加密

密码算法分类

密码算法主要分为三类: 对称密码算法, ⾮对称密码算法, 摘要算法

在这里插入图片描述

  1. 对称密码算法 是指加密秘钥和解密秘钥相同的密码算法. 常⻅的对称密码算法有: AES, DES, 3DES,RC4, RC5, RC6 等.
  2. ⾮对称密码算法 是指加密秘钥和解密秘钥不同的密码算法. 该算法使⽤⼀个秘钥进⾏加密, ⽤另外⼀个秘钥进⾏解密.
    • 加密秘钥可以公开,⼜称为 公钥
    • 解密秘钥必须保密,⼜称为 私钥

常⻅的⾮对称密码算法有: RSA, DSA, ECDSA, ECC 等

  1. 摘要算法 是指把任意⻓度的输⼊消息数据转化为固定⻓度的输出数据的⼀种密码算法. 摘要算法是不可逆的, 也就是⽆法解密. 通常⽤来检验数据的完整性的重要技术, 即对数据进⾏哈希计算然后⽐较摘要值, 判断是否⼀致. 常⻅的摘要算法有: MD5, SHA系列(SHA1, SHA2等), CRC(CRC8, CRC16,CRC32)

加密思路

博客系统中, 我们采⽤MD5算法来进⾏加密.

问题: 虽然经过MD5加密后的密⽂⽆法解密, 但由于相同的密码经过MD5哈希之后的密⽂是相同的, 当存储⽤⼾密码的数据库泄露后, 攻击者会很容易便能找到相同密码的⽤⼾, 从⽽降低了破解密码的难度. 因此, 在对⽤⼾密码进⾏加密时,需要考虑对密码进⾏包装, 即使是相同的密码, 也保存为不同的密⽂. 即使⽤⼾输⼊的是弱密码, 也考虑进⾏增强, 从⽽增加密码被攻破的难度.

解决⽅案: 采⽤为⼀个密码拼接⼀个随机字符来进⾏加密, 这个随机字符我们称之为"盐". 假如有⼀个加盐后的加密串,⿊客通过⼀定⼿段这个加密串, 他拿到的明⽂并不是我们加密前的字符串, ⽽是加密前的字符串和盐组合的字符串, 这样相对来说⼜增加了字符串的安全性.

在这里插入图片描述

解密流程: MD5是不可逆的, 通常采⽤"判断哈希值是否⼀致"来判断密码是否正确.

如果⽤⼾输⼊的密码, 和盐值⼀起拼接后的字符串经过加密算法, 得到的密⽂相同, 我们就认为密码正确(密⽂相同, 盐值相同, 推测明⽂相同)

在这里插入图片描述
在这里插入图片描述

写加密/解密⼯具类

package cn.edu.zxj.springblog.utils;import org.springframework.util.DigestUtils;
import org.springframework.util.StringUtils;import java.util.UUID;/*** Created with IntelliJ IDEA.* Description:加密解密类 -- 使用 md5** @author: zxj* @date: 2024-02-05* @time: 13:55:38*/
public class SecurityUtil {/*** @description: 对密码进⾏加密**/public static String encrypt(String password) {// 每次⽣成内容不同的,但⻓度固定 32 位的盐值String salt = UUID.randomUUID().toString().replace("-", "");// 最终密码=md5(盐值+原始密码)String finalPassword = DigestUtils.md5DigestAsHex((salt + password).getBytes());// 数据库中存储 盐 + 最终密码的值, 总长度为 64 = 32(salt) + 32(finalPassword)return salt + finalPassword;}/*** 密码验证** @param password      待验证密码* @param finalPassword 最终正确的密码(数据库中加盐的密码)* @return*/public static boolean verify(String password, String finalPassword) {// 非空校验if (!StringUtils.hasLength(password)|| !StringUtils.hasLength(finalPassword)) {return false;}//最终密码不是64位, 则不正确if (finalPassword.length() != 64) {return false;}// 盐值String salt = finalPassword.substring(0,32);// 使⽤盐值+待确认的密码⽣成⼀个最终密码String securityPassword = DigestUtils.md5DigestAsHex((salt + password).getBytes());// 使⽤盐值+最终的密码和数据库的真实密码进⾏对⽐return (salt + securityPassword).equals(finalPassword);}public static void main(String[] args) {String finalPassword = encrypt("123456");System.out.println(finalPassword);System.out.println(verify("1223456",finalPassword));}}

修改⼀下数据库密码

使⽤测试类给密码123456⽣成密⽂:
e2377426880545d287b97ee294fc30ea6d6f289424b95a2b2d7f8971216e39b7

修改数据库明⽂密码为密⽂, 执⾏SQL

在这里插入图片描述
修改登录接⼝

在这里插入图片描述


源代码gitee链接

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

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

相关文章

Netty应用(四) 之 Reactor模型 零拷贝

目录 6.Reactor模型 6.1 单线程Reactor 6.2 主从多线程Reactor (主--->Boss | 从--->Worker | 一主多从机制) 7.扩展与补充 8.Reactor模型的实现 8.1 多线程Reactor模型的实现&#xff08;一个Boss线程&#xff0c;一个Worker线程&#xff09; 8.2 多线程Reactor模…

SolidWorks学习笔记——入门知识1

目录 1、固定最近文档 2、根据需要自定义菜单栏 3、根据需要增添选项卡 4、命令搜索框 5、鼠标右键长按快速切换视图 6、鼠标笔势 自定义鼠标笔势 1、固定最近文档 图1 固定最近文档 2、根据需要自定义菜单栏 图2 根据需要自定义菜单栏 3、根据需要增添选项卡 图3 根据…

架构(十二)动态Excel

一、引言 作者最近的平台项目需要生成excel&#xff0c;excel的导入导出是常用的功能&#xff0c;但是作者想做成动态的&#xff0c;不要固定模板&#xff0c;那就看看怎么实现。 二、后端 先捋一下原理&#xff0c;前后端的交互看起来是制定好的接口&#xff0c;其实根本上是…

OCP使用CLI创建和构建应用

文章目录 环境登录创建project赋予查看权限部署第一个image创建route检查pod扩展应用 部署一个Python应用连接数据库创建secret加载数据并显示国家公园地图 清理参考 环境 RHEL 9.3Red Hat OpenShift Local 2.32 登录 通过 crc console --credentials 可以查看登录信息&…

Stable Video Diffusion图片转视频——Stability AI开源视频模型

我们前期介绍过Stable Diffusion&#xff0c;stable diffusion模型是Stability AI开源的一个text-to-image的扩散模型&#xff0c;其模型在速度与质量上面有了质的突破&#xff0c;玩家们可以在自己消费级GPU上面来运行此模型。 文生图大模型已经火了很长一段时间了&#xff0c…

专业130+总分410+苏州大学837信号系统与数字逻辑考研经验电子信息与通信,真题,大纲,参考书

今年考研总分410&#xff0c;专业837信号系统与数字逻辑130&#xff0c;整体每门相对比较均衡&#xff0c;没有明显的短板&#xff0c;顺利上岸苏大&#xff0c;总结一下自己这大半年的复习经历&#xff0c;希望可以对大家有所帮助&#xff0c;也算是对自己考研做个总结。 专业…

Java:常用API接上篇 --黑马笔记

一、 StringBuilder类 StringBuilder代表可变字符串对象&#xff0c;相当于是一个容器&#xff0c;它里面的字符串是可以改变的&#xff0c;就是用来操作字符串的。 好处&#xff1a;StringBuilder比String更合适做字符串的修改操作&#xff0c;效率更高&#xff0c;代码也更…

Idea里自定义封装数据警告解决 Spring Boot Configuration Annotation Processor not configured

我们自定对象封装指定数据&#xff0c;封装类上面一个红色警告&#xff0c;虽然不影响我们的执行&#xff0c;但是有强迫症看着不舒服&#xff0c; 去除方式&#xff1a; 在pom文件加上坐标刷新 <dependency><groupId>org.springframework.boot</groupId><…

11 插入排序和希尔排序

1. 插入排序 基本思想 直接插入排序是一种简单的插入排序法&#xff0c;基本思想&#xff1a; 把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中&#xff0c;直到所有的记录插入完为止&#xff0c;得到一个新的有序序列 在玩扑克牌时&#xff0c;就用…

【知识整理】招人理念、组织结构、招聘

1、个人思考 几个方面&#xff1a; 新人&#xff1a;选、育、用、留 老人&#xff1a;如何甄别&#xff1f; 团队怎么演进&#xff1f; 有没有什么注意事项 怎么做招聘&#xff1f; 2、 他人考虑 重点&#xff1a; 1、从零开始&#xff0c;讲一个搭建团队的流程 2、标…

Python pandas中read_csv函数的io参数

前言 在数据分析和处理中&#xff0c;经常需要读取外部数据源&#xff0c;例如CSV文件。Python的pandas库提供了一个强大的 read_csv() 函数&#xff0c;用于读取CSV文件并将其转换成DataFrame对象&#xff0c;方便进一步分析和处理数据。在本文中&#xff0c;将深入探讨 read…

【网页设计】春节页面背景模板

无偿下载地址&#xff1a;https://download.csdn.net/download/weixin_47040861/88811143 1.实现效果 2.代码 1.html <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta name"viewport" content&q…

【超高效!保护隐私的新方法】针对图像到图像(l2l)生成模型遗忘学习:超高效且不需要重新训练就能从生成模型中移除特定数据

针对图像到图像生成模型遗忘学习&#xff1a;超高效且不需要重新训练就能从生成模型中移除特定数据 提出背景如何在不重训练模型的情况下从I2I生成模型中移除特定数据&#xff1f; 超高效的机器遗忘方法子问题1: 如何在图像到图像&#xff08;I2I&#xff09;生成模型中进行高效…

推荐系统|召回04_离散特征处理

离散特征处理 离散特征是什么 怎么处理离散特征 One-hot编码 Embedding嵌入 从one-hot到Embedding&#xff0c;已经节省了很多的存储空间&#xff0c;但当数据量大的时候&#xff0c;还是占空间&#xff0c;所以工业界仍会对Embedding进行优化 而一个物品所对应的Embedding参数…

基于JSP的网上购书系统

点击以下链接获取源码&#xff1a; https://download.csdn.net/download/qq_64505944/88825694?spm1001.2014.3001.5503 Java项目-15 源码论文数据库配置文件 基于JSP的网上购书系统 摘要 在当今的社会中&#xff0c; 随着社会经济的快速发展以及计算机网络技术和通讯技术…

8种基本类型的包装类(与String的转换)

java针对8种基本数据类型&#xff0c;定义了相应的引用类型&#xff1a;包装类(封装类)&#xff0c;有了类的特点&#xff0c;就能调用类中的方法&#xff0c;java才是真正的面向对象。 基本数据类型 包装类byte Byteshort Shortint Integerlong Longfloat Floa…

国产光耦2024:发展机遇与挑战全面解析

随着科技的不断进步&#xff0c;国产光耦在2024年正面临着前所未有的机遇与挑战。本文将深入分析国产光耦行业的发展现状&#xff0c;揭示其在技术创新、市场需求等方面的机遇和挑战。 国产光耦技术创新的机遇&#xff1a; 国产光耦作为光电器件的重要组成部分&#xff0c;其技…

Python操作MySQL基础

除了使用图形化工具以外&#xff0c;我们也可以使用编程语言来执行SQL从而操作数据库。在Python中&#xff0c;使用第三方库: pymysql来完成对MySQL数据库的操作。 安装第三方库pymysql 使用命令行,进入cmd&#xff0c;输入命令pip install pymysql. 创建到MySQL的数据库连接…

CSS高级技巧

一、 精灵图 1.1 为什么需要精灵图&#xff1f; 1.2 精灵图&#xff08;sprites&#xff09;的使用 二、 字体图标 2.1 字体图标的产生 2.2 字体图标的优点 2.3 字体图标的下载 icomoom字库 http://icomoon.io 阿里iconfont字库 http://www.iconfont.cn/ 2.4 字体图标的引用…

【EAI 013】BC-Z: Zero-Shot Task Generalization with Robotic Imitation Learning

论文标题&#xff1a;BC-Z: Zero-Shot Task Generalization with Robotic Imitation Learning 论文作者&#xff1a;Eric Jang, Alex Irpan, Mohi Khansari, Daniel Kappler, Frederik Ebert, Corey Lynch, Sergey Levine, Chelsea Finn 论文原文&#xff1a;https://arxiv.org…