当前位置: 首页 > news >正文

博客系统完整开发流程

前言

        通过前⾯课程的学习, 我们掌握了Spring框架和MyBatis的基本使用, 并完成了图书管理系统的常规功能开发, 接下来我们系统的从0到1完成⼀个项⽬的开发.

企业开发的流程

        1. 需求评审(产品经理(PM)会和运营(想口号),UI,测试,开发等沟通) ,会涉及到背景/目标/怎么做,可能会有多次评审(对于需求不清晰的部分,文档需要补充) -> 需求文档    

        2. 开发阶段(前后端并行开发) 1> 任务分工(对于边界不清晰的任务,再进行探讨). 2> 后端进行方案设计 -> 接口涉及,数据库涉及,ER,UML,流程图等,deadLine(前后端把deadline出了,测试人员才会决定测试时间),复杂项目需要出测试列表,上线计划 3> 方案设计评审 4> 开发-> 代码review 5> 自测

        3. 测试(联调):各个端一起来(前端调后端,后端调前端..)

        4. 上线

项目介绍

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

        1. 用户登录

        2. 博客发表⻚

        3. 博客编辑⻚

        4. 博客列表⻚

        5. 博客详情⻚

        功能描述:

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

        页面预览用户登录

        

        博客列表页

        博客详情页

        博客发表/修改页    

        后端需要提供的功能和接口

        1. 用户登录: 根据用户名和密码判断用户输入的信息是否能从数据库找到

        2. 登录用户的个人页: 博客列表显示当前登录用户的信息,根据用户的ID,返回用户相关信息(后面再详细设计)

        3. 博客列表页: 查询你博客的列表(点击查看全文之后)

        4. 作者个人主页: 根据用户ID,返回作者信息(俩种实现可能: 1> 根据用户ID返回 (调用方要先拿到作者ID) 2> 根据博客ID返回) 

        5. 博客详细: 根据博客ID,返回博客信息

点击编辑之后

        6. 博客编辑: 1> 根据博客ID,返回博客信息. 2> 根据用户输入的信息,更新博客

如果是点击删除的话

        7. 博客删除: 根据ID,删除博客

        点击写博客

        8. 写博客: 根据用户输入的信息,添加博客

        我们把上面的功能再转化为接口

        接口

        1. 用户相关

                根据用户名和密码,判断用户输入的信息是否正确

                根据用户的ID,返回用户相关信息

        2. 博客相关

                查询博客列表

                根据博客ID(前端根据博客详情,拿到作者信息,在根据作者信息调用用户信息),返回作者信息

                根据博客ID,返回博客详情

                根据用户输入的信息,更新博客

                根据博客ID,删除博客

                根据用户输入信息,添加博客

        后面的接口我们会更加详细的写,这里只写了大概(会写参数,返回类型,请求方式...) 

1. 准备工作

        数据库设计

        数据的准备工作: 创建用户表和博客表

        架构设计(画图)-> 画实体,实体表和关系表 (此时我们直接拿一个属性user_id进行关联)

-- 建表SQL
create database if not exists java_blog_spring charset utf8mb4;
use java_blog_spring;
-- 用户表
DROP TABLE IF EXISTS java_blog_spring.user;
CREATE TABLE java_blog_spring.user(`id` INT NOT NULL AUTO_INCREMENT,`user_name` VARCHAR ( 128 ) NOT NULL,`password` VARCHAR ( 128 ) NOT NULL,`github_url` VARCHAR ( 128 ) NULL,`delete_flag` TINYINT ( 4 ) NULL DEFAULT 0,`create_time` DATETIME DEFAULT now(),`update_time` DATETIME DEFAULT now(),PRIMARY KEY ( id ),
UNIQUE INDEX user_name_UNIQUE ( user_name ASC )) ENGINE = INNODB DEFAULT CHARACTER 
SET = utf8mb4 COMMENT = '用户表';-- 博客表
drop table if exists java_blog_spring.blog;
CREATE TABLE java_blog_spring.blog (`id` INT NOT NULL AUTO_INCREMENT,`title` VARCHAR(200) NULL,`content` TEXT NULL,`user_id` INT(11) NULL,`delete_flag` TINYINT(4) NULL DEFAULT 0,`create_time` DATETIME DEFAULT now(),`update_time` DATETIME DEFAULT now(),PRIMARY KEY (id))
ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '博客表';-- 新增用户信息
insert into java_blog_spring.user (user_name, password,github_url)values("zhangsan","123456","https://gitee.com/bubble-fish666/class-java45");
insert into java_blog_spring.user (user_name, password,github_url)values("lisi","123456","https://gitee.com/bubble-fish666/class-java45");insert into java_blog_spring.blog (title,content,user_id) values("第一篇博客","111我是博客正文我是博客正文我是博客正文",1);
insert into java_blog_spring.blog (title,content,user_id) values("第二篇博客","222我是博客正文我是博客正文我是博客正文",2);

        创建项目

            四个依赖加上    

        前端代码准备(在资源绑定里面)

        数据库yml配置

spring:datasource:url: jdbc:mysql://127.0.0.1:3306/java_blog_spring?characterEncoding=utf8&useSSL=falseusername: rootpassword: 123456driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:configuration: # 配置打印 MyBatis⽇志map-underscore-to-camel-case: true #配置驼峰⾃动转换log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #打印sql语句mapper-locations: classpath:mapper/*Mapper.xml
# 设置日志文件
logging:file:name: spring-book.log

2. 项目公共模块

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

        公共层和实体层为上面的所有都提供服务.

        2.1 实体类

                1> 用户实体

package org.xiaobai.blog_system.model;import lombok.Data;import java.util.Date;
@Data
public class UserInfo {private Integer id;private String userName;private String password;private String githubUrl;//和数据库字段对应,默认数据库字段的下划线后面的第一个单词大写private Integer deleteFlag;private Date createTime;private Date updateTime;
}

                2> 博客实体

package org.xiaobai.blog_system.model;import lombok.Data;import java.util.Date;@Data
public class BlogInfo {private Integer id;private String title;private Integer userId;private Integer deleteFlag;private Date createTime;private Date updateTime;
}

                3> 结果实体

package org.xiaobai.blog_system.model;import lombok.Data;
import org.xiaobai.blog_system.enums.ResultStatus;//统一结果,通常是业务码
@Data
public class Result<T> {//业务码200-成功 -1失败 -2 未登录private ResultStatus code;//也可以写成枚举类//错误信息private String errMsg;//接口响应的数据private T data;//设置成功的时候的信息public static <T>Result success(T data){Result result = new Result();result.setCode(ResultStatus.SUCCESS);result.setErrMsg("");result.setData(data);return result;}//失败的时候不返回数据public static <T>Result fail(String errMsg){Result result = new Result();result.setCode(ResultStatus.FAIL);result.setErrMsg(errMsg);result.setData(null);return result;}//失败的时候返回数据public static <T>Result fail(String errMsg,T data){Result result = new Result();result.setCode(ResultStatus.FAIL);result.setErrMsg(errMsg);result.setData(data);return result;}
}

        4> 枚举类

package org.xiaobai.blog_system.enums;public enum ResultStatus {SUCCESS,FAIL,NOLOGIN;
}

        5> 统一结果返回

package org.xiaobai.blog_system.ResponseAdvice;import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import org.xiaobai.blog_system.model.Result;
//TODO 统一结果返回
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {@Autowiredprivate ObjectMapper objectMapper;@Overridepublic boolean supports(MethodParameter returnType, Class converterType) {return false;}@Overridepublic Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {//对结果进行进一步的划分if(body instanceof Result<?>){return body;}if(body instanceof String){//如果是String就进行序列化处理try {return objectMapper.writeValueAsString(Result.success(body));} catch (JsonProcessingException e) {throw new RuntimeException(e);}}return Result.success(body);}
}

        5> 统一异常处理

package org.xiaobai.blog_system.ResponseAdvice;import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.xiaobai.blog_system.model.Result;//TODO 统一异常处理
@Slf4j
@ResponseBody
@ControllerAdvice
public class ExceptionHandle {@ExceptionHandlerpublic Result handle(Exception e){//打印异常信息log.error("发生异常,e: ",e);return Result.fail("内部错误,请练习管理员");}
}

3. 业务代码

        3.1 持久层

        接口和数据库操作的关系

                 根据数据操作写mapper

        1> 用户相关的mapper

package org.xiaobai.blog_system.mapper;import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.xiaobai.blog_system.model.UserInfo;@Mapper
public interface UserInfoMapper {//1.根据用户名,查询用户信息@Select("select * from user where delete_flag != 1 and user_name = #{userName}")UserInfo selectByName(String userName );//2.根据ID,查询用户信息@Select("select * from user where delete_flag != 1 and id = #{id}")UserInfo selectById(Integer id);
}

        2> 博客相关的mapper

package org.xiaobai.blog_system.mapper;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 org.xiaobai.blog_system.model.BlogInfo;
import java.util.List;
@Mapper
public interface BlogMapper {//1. 返回博客列表@Select("select * from blog where delete_flag = 0")List<BlogInfo> selectAll();//2. 根据博客ID,返回博客信息@Select("select * from blog where id = #{id}")BlogInfo selectById(Integer id);//3. 更新博客(涉及更新和删除俩部分)//后面再写,使用xml的形式Integer updateBlog(BlogInfo blogInfo);//4. 发表博客(插入博客)@Insert("insert into blog(title,content,user_id) values (#{title},#{content},#{userId})")Integer insertBlog(BlogInfo blogInfo);
}

       更新/删除操作我们使用update,需要进行判断,因此我们需要使用xml的方式

        首先我们先进行配置                对应关系

        我们在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="org.xiaobai.blog_system.mapper.BlogMapper">

        然后我们进行测试(在mapper里面的接口点击生成测试用例)

        测试用户的mapper

        测试博客的mapper

        我们再单独的对更新语句进行测试

        修改一下,就是删除语句测试

        接下来正式进入接口功能实现

        controler -> service -> mapper

  • Controller:接收前端请求,调用Service层。
  • Service:处理业务逻辑,调用Mapper层进行数据操作。
  • Mapper:与数据库交互,执行数据持久化操作。

总结:

  • Controller 处理用户请求和响应。
  • Service 处理业务逻辑,调用 Mapper 层进行数据库操作。
  • Mapper 执行与数据库的直接交互操作。

        3.2 实现博客列表

                约定前后端交互接口

        客⼾端给服务器发送⼀个 /blog/getlist 这样的 HTTP 请求, 服务器给客⼾端返回了⼀个 JSON

格式的数据.

        后端代码

        controller

        service

        

mapper

        我们也可以限制接口请求类型

        前端代码

        操作过程

        编写结果

        结果展示

        3.3 实现博客详情

        
        接口定义

        

        后端代码

                controller

                service

                mapper

        前端代码

       

                 编写过程

                我们也可以把时间的格式进行设计一下: dateFormat

package org.xiaobai.blog_system.utils;import java.text.SimpleDateFormat;
import java.util.Date;//对于时间的公共处理
public class DateUtils {public  static String dateFormat(Date date){//2025-02-24 21:01SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm");return format.format(date);}public static void main(String[] args) {System.out.println(dateFormat(new Date()));}}

                结果:

                 编写结果

                 结果展示

        3.4 实现登陆

        之前我们实现图书管理系统的适合采用的思路

        传统思路:

        • 登陆⻚⾯把⽤⼾名密码提交给服务器.

        • 服务器端验证⽤⼾名密码是否正确, 并返回校验结果给后端

        • 如果密码正确, 则在服务器端创建 Session . 通过 Cookie 把 sessionId 返回给浏览器.

        问题:

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

        原因分析:

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

        

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

        1. ⽤⼾登录 ⽤⼾登录请求, 经过负载均衡(负载理解为压力和流量), 把请求转给了第⼀台服务器, 第⼀台服务器进⾏账号密码验证, 验证成功后, 把Session存在了第⼀台服务器上

        2. 查询操作 ⽤⼾登录成功之后, 携带Cookie(⾥⾯有SessionId)继续执⾏查询操作, ⽐如查询博客列表. 此时请求转发到了第⼆台机器, 第⼆台机器会先进⾏权限验证操作(通过SessionId验证⽤⼾是否登录), 此时第⼆台机器上没有该⽤⼾的Session, 就会出现问题, 提⽰⽤⼾登录, 这是⽤⼾不能忍受的.(各个机器都是独立的,因此在第一个机器里面虽然已经set SessionId了,但是和第二个机器没有关系)

`        解决方案

        Redis简单介绍: Redis就是一个数据结构,就理解为存数据的

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

        令牌技术

        令牌的概念

        之前我们存储在session里面的信息,现在存储在了token里面

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

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

        身份证: 公安局发放,公安局来进行鉴别真假

        1> 存储信息

        2> 不能伪造

        3> 可以辨别真假        

        同理: 我们的令牌token是由服务端生成的,因此我们要在服务端进行鉴别真假.也具备上面身份证的作用

        令牌的逻辑

        服务器: 要能够生成token和校验token

        1> 用户登录,服务器生成token(生成一个字符串)

        2> 把token返回给客户端

        3> 客户端携带token,再次访问,服务器对token进行校验

        

        服务器具备⽣成令牌和验证令牌的能⼒我们使⽤令牌技术, 继续思考上述场景:

        1. ⽤⼾登录 ⽤⼾登录请求, 经过负载均衡, 把请求转给了第⼀台服务器, 第⼀台服务器进⾏账号密码验证, 验证成功后, ⽣成⼀个令牌, 并返回给客⼾端.

        2. 客⼾端收到令牌之后, 把令牌存储起来. 可以存储在Cookie中, 也可以存储在其他的存储空间(⽐如localStorage)

        3. 查询操作 ⽤⼾登录成功之后, 携带令牌继续执⾏查询操作, ⽐如查询博客列表. 此时请求转发到了第⼆台机器, 第⼆台机器会先进⾏权限验证操作. 服务器验证令牌是否有效, 如果有效, 就说明⽤⼾已经执⾏了登录操作, 如果令牌是⽆效的, 就说明⽤⼾之前未执⾏登录操作.

        令牌的优缺点

        优点:

        • 解决了集群环境下的认证问题

        • 减轻服务器的存储压⼒(⽆需在服务器端存储)

        缺点:

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

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

        小总结

        JWT令牌

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

        介绍

        JWT全称: JSON Web Token官⽹:Auth0 | JWT Handbook

        JSON Web Token(JWT)是⼀个开放的⾏业标准(RFC 7519), ⽤于客⼾端和服务器之间传递安全可靠的信息.其本质是⼀个token, 是⼀种紧凑的URL安全⽅法.

        JWT组成

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

        • Header(头部) 头部包括令牌的类型(即JWT)及使⽤的哈希算法(如HMAC SHA256或RSA)

        • Payload(负载) 负载部分是存放有效信息的地⽅, ⾥⾯是⼀些⾃定义内容.

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

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

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

        JWT不是加密的,而是把信息通过Base64的编码表示的

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

        JWT令牌生成和校验


        1. 引⼊JWT令牌的依赖

  <!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-api --><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-api</artifactId><version>0.11.5</version></dependency><!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-impl --><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-impl</artifactId><version>0.11.5</version><scope>runtime</scope></dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-jackson</artifactId> <!-- or jjwt-gson if Gson is
preferred --><version>0.11.5</version><scope>runtime</scope></dependency>

        2. 使⽤Jar包中提供的API来完成JWT令牌的⽣成和校验

        如果设置的签名长度太短了,会报错

        我们使用报错信息所提示的方法去创建一个key然后看三个方法的对应关系

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

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

        但是,我们的登录安全问题,不能只通过JWT令牌来进行保证(当前的JWT只是解决了集群问题(数据不能传递的问题))

        代码
package org.xiaobai.blog_system;import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtParser;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.io.Encoders;
import io.jsonwebtoken.security.Keys;
import org.junit.jupiter.api.Test;import javax.crypto.SecretKey;
import java.security.Key;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;public class JwtUtilsTest {//设置过期时间为1h(毫秒)private static final long JWT_EXPIRATION = 60*60*1000;//生成keyprivate static final String secretStr = "gI3LKBe6f3JDSoeBCONqBt0Ks6qMD6HZxsMadimkJO8=";private static final Key key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretStr));//生成 JWT 令牌@Testpublic void genJwt() {Map<String,Object> claim = new HashMap<>();claim.put("id",1);claim.put("name","zhangsan");//创建解析器,设置签名密钥String token = Jwts.builder().setClaims(claim).setExpiration(new Date(System.currentTimeMillis()+JWT_EXPIRATION))   //设置多久过期.signWith(key) //设置签名.compact();System.out.println(token);}////生成一个随机的 JWT key,用于签名 JWT。@Testpublic void genKey(){SecretKey secretKey = Keys.secretKeyFor(SignatureAlgorithm.HS256);String str = Encoders.BASE64.encode(secretKey.getEncoded());System.out.println(str);}//此方法用于 解析 JWT 令牌 并验证其合法性。
@Test//根据令牌校验合法性public void parseToken(){//此时不管你改哪个部分都不会校验成功,过期了也不能String token = "eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiemhhbmdzYW4iLCJpZCI6MSwiZXhwIjoxNzQwMzcwMDk0fQ.KNxDj9RV3IOLq9-YnCBbqFigtxqGtLY2Aswr3UqgQ94";JwtParser build = Jwts.parserBuilder().setSigningKey(key).build();Claims claims =build.parseClaimsJws(token).getBody(); //解析tokenSystem.out.println(claims);}
}

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

        1. 登陆⻚⾯把⽤⼾名密码提交给服务器.

        2. 服务器端验证⽤⼾名密码是否正确, 如果正确, 服务器⽣成令牌, 下发给客⼾端.

        3. 客⼾端把令牌存储起来(⽐如Cookie, local storage等), 后续请求时, 把token发给服务器

        4. 服务器对令牌进⾏校验, 如果令牌正确, 进⾏下⼀步操作

     把JWT应用到我们的登录接口上

                接口定义

            后端代码

                controller

        

package org.xiaobai.blog_system.controller;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.xiaobai.blog_system.model.Result;
import org.xiaobai.blog_system.model.UserInfo;
import org.xiaobai.blog_system.service.UserService;
import org.xiaobai.blog_system.utils.JwtUtils;import java.util.HashMap;
import java.util.Map;@RestController
@RequestMapping("/user")
public class UserController {@Autowiredprivate UserService userService;@RequestMapping("/login")public Result login(String username, String password) {//1. 后端进行参数的校验(合法性)if (!StringUtils.hasLength(username) || !StringUtils.hasLength(password)) {return Result.fail("账号或者密码不能为空");}//2. 校验密码是否正确//从数据库中查找用户UserInfo userInfo = userService.SelectByName(username);//用户不存在if (userInfo == null) {return Result.fail("用户不存在");}//密码错误if (!password.equals(userInfo.getPassword())) {return Result.fail("密码错误");}//3. 密码正确,返回tokenMap<String,Object> claim = new HashMap<>();claim.put("id",userInfo.getId());claim.put("userName",userInfo.getUserName());//生成tokenString token = JwtUtils.genJwtToken(claim);//返回tokenreturn Result.success(token);}
}
                service
package org.xiaobai.blog_system.service;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.xiaobai.blog_system.mapper.UserInfoMapper;
import org.xiaobai.blog_system.model.UserInfo;@Service
public class UserService {@Autowiredprivate UserInfoMapper userInfoMapper;public UserInfo SelectByName(String username) {return userInfoMapper.selectByName(username);}
}
                mapper
package org.xiaobai.blog_system.mapper;import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.xiaobai.blog_system.model.UserInfo;@Mapper
public interface UserInfoMapper {//1.根据用户名,查询用户信息@Select("select * from user where delete_flag != 1 and user_name = #{userName}")UserInfo selectByName(String userName );//2.根据ID,查询用户信息@Select("select * from user where delete_flag != 1 and id = #{id}")UserInfo selectById(Integer id);
}

                utils:

package org.xiaobai.blog_system.utils;import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtParser;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;import java.security.Key;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@Slf4j
public class JwtUtils {//设置过期时间为1h(毫秒)private static final long JWT_EXPIRATION = 60*60*1000;//生成keyprivate static final String secretStr = "gI3LKBe6f3JDSoeBCONqBt0Ks6qMD6HZxsMadimkJO8=";private static final Key key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretStr));//1. 生成token//根据载荷信息生成public static String genJwtToken(Map<String,Object> claim){claim.put("id",1);claim.put("name","zhangsan");//创建解析器,设置签名密钥String token = Jwts.builder().setClaims(claim).setExpiration(new Date(System.currentTimeMillis()+JWT_EXPIRATION))   //设置多久过期.signWith(key) //设置签名.compact();System.out.println(token);return token;}//2. 校验token: claims为null校验失败public Claims parseToken(String token){//此时不管你改哪个部分都不会校验成功,过期了也不能//产生了异常就失败JwtParser build = Jwts.parserBuilder().setSigningKey(key).build();Claims claims = null;try {claims =build.parseClaimsJws(token).getBody(); //解析token}catch (Exception e){log.error("解析token失败:{}",token);return null;}return claims;}
}

        这里注意

         claim继承了map

        

        验证账号和密码不能为空的时候,我们返回类型的选择

        前端代码

                 编写过程

                 编写结果

                 结果展示

3.5 实现强制要求登陆
        

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

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

        我们客户端保存了token之后,客户端在发送后端请求的时候会带着token,服务端将会接收这个token. 服务器在接收token的时候会有俩个问题:

        1. 客户端把token放在哪里比较合适?(header(常常放在这里),参数,url)

        2. 服务端强制登录: 通过拦截器实现

                1> 从header获取token

                2> 校验token,判断是否放行

以后我们再发送请求的时候,就会把token放到http里面的header里面传给服务器

具体流程

        如果我们的token没用发送到后端

        1. 在我们的common.js中打印日志,判断是否执行了该方法

        2. 如果未执行,把这段代码粘到html页面中

        3. 确认名称的对应关系

     接口定义

        

    后端代码

                controller

        

package org.xiaobai.blog_system.controller;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.xiaobai.blog_system.model.Result;
import org.xiaobai.blog_system.model.UserInfo;
import org.xiaobai.blog_system.service.UserService;
import org.xiaobai.blog_system.utils.JwtUtils;import java.util.HashMap;
import java.util.Map;@RestController
@RequestMapping("/user")
public class UserController {@Autowiredprivate UserService userService;@RequestMapping("/login")public Result login(String username, String password) {//1. 后端进行参数的校验(合法性)if (!StringUtils.hasLength(username) || !StringUtils.hasLength(password)) {return Result.fail("账号或者密码不能为空");}//2. 校验密码是否正确//从数据库中查找用户UserInfo userInfo = userService.SelectByName(username);//用户不存在if (userInfo == null) {return Result.fail("用户不存在");}//密码错误if (!password.equals(userInfo.getPassword())) {return Result.fail("密码错误");}//3. 密码正确,返回tokenMap<String,Object> claim = new HashMap<>();claim.put("id",userInfo.getId());claim.put("userName",userInfo.getUserName());//生成tokenString token = JwtUtils.genJwtToken(claim);//返回tokenreturn Result.success(token);}
}
                service
package org.xiaobai.blog_system.service;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.xiaobai.blog_system.mapper.UserInfoMapper;
import org.xiaobai.blog_system.model.UserInfo;@Service
public class UserService {@Autowiredprivate UserInfoMapper userInfoMapper;public UserInfo SelectByName(String username) {return userInfoMapper.selectByName(username);}
}
                mapper
package org.xiaobai.blog_system.mapper;import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.xiaobai.blog_system.model.UserInfo;@Mapper
public interface UserInfoMapper {//1.根据用户名,查询用户信息@Select("select * from user where delete_flag != 1 and user_name = #{userName}")UserInfo selectByName(String userName );//2.根据ID,查询用户信息@Select("select * from user where delete_flag != 1 and id = #{id}")UserInfo selectById(Integer id);
}
                utils

                生成并解析JWT

        

package org.xiaobai.blog_system.utils;import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtParser;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;import java.security.Key;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@Slf4j
public class JwtUtils {//设置过期时间为1h(毫秒)private static final long JWT_EXPIRATION = 60*60*1000;//生成keyprivate static final String secretStr = "gI3LKBe6f3JDSoeBCONqBt0Ks6qMD6HZxsMadimk2dJO8=";private static final Key key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretStr));//1. 生成token//根据载荷信息生成public static String genJwtToken(Map<String,Object> claim){claim.put("id",1);claim.put("name","zhangsan");//创建解析器,设置签名密钥String token = Jwts.builder().setClaims(claim).setExpiration(new Date(System.currentTimeMillis()+JWT_EXPIRATION))   //设置多久过期.signWith(key) //设置签名.compact();System.out.println(token);return token;}//2. 校验token: claims为null校验失败public static Claims parseToken(String token){//此时不管你改哪个部分都不会校验成功,过期了也不能//产生了异常就失败JwtParser build = Jwts.parserBuilder().setSigningKey(key).build();Claims claims = null;try {claims =build.parseClaimsJws(token).getBody(); //解析token}catch (Exception e){log.error("解析token失败:{}",token);return null;}return claims;}
}
                config

                把拦截器应用到项目

package org.xiaobai.blog_system.config;
//把拦截器应用到项目
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 org.xiaobai.blog_system.interceptor.LoginInterceptor;import java.util.Arrays;
import java.util.List;
@Configuration
public class WebConfig implements WebMvcConfigurer {@Autowiredprivate LoginInterceptor loginInterceptor;//把之前写的拦截器注入进来private final List excludes = Arrays.asList(//排除路径"/**/*.html","/blog-editormd/**","/css/**","/js/**","/pic/**","/user/login");@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(loginInterceptor)//添加拦截器.addPathPatterns("/**")//对哪些路径生效.excludePathPatterns(excludes);//排除那些路径}
}
                interceptor

                拦截器具体内容

package org.xiaobai.blog_system.interceptor;import io.jsonwebtoken.Claims;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.xiaobai.blog_system.utils.JwtUtils;//拦截器的具体内容
@Slf4j
@Component
public class LoginInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {//1. 从header中获取tokenString token = request.getHeader("user_token_header");//2. 校验token,判断是否放行log.info("从header中获取token:{}",token);Claims claims = JwtUtils.parseToken(token);if(claims == null){//校验失败response.setStatus(401);return false;}return true;}
}

        前端代码

                 编写过程

                 编写结果

        

        注意:

        前端请求时, 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 请求中使⽤的选项

                 结果展示

3.6 实现显示用户信息

        我们可以看见,这个信息是写死的
        

        我们期望这个信息可以随着⽤⼾登陆⽽发⽣改变.

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

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

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

     接口定义

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

    后端代码

                controller
 //获取当前登录用户信息的信息,这个从token里面去拿@RequestMapping("/getUserInfo")public UserInfo getLoginUserInfo(HttpServletRequest request){//获取tokenString token = request.getHeader(Constants.REQUEST_HEADER_TOKEN);//从token里面获取登录用户的idInteger userId = JwtUtils.getIdByToken(token);if(userId == null){//用户未登录return null;}UserInfo userInfo = userService.selectById(userId);return userInfo;}

        注意

调试的时候可以根据这个方法来看是哪个文件不存在

关于技术选型: 为什么选JWT不选其他?

        

        关于我们接口返回私密信息问题

                service

                mapper

        前端代码

                 编写过程

        根据前端的返回值去写

                 编写结果

                 结果展示

     接口定义
 

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

    后端代码

                controller
//获取作者信息,根据博客id获取作者信息@RequestMapping("/getAuthorInfo")public UserInfo getAuthorInfo(Integer blogId){//1. 根据博客id获取作者id//2. 根据作者ID,获取作者信息if(blogId <= 0){return null;}UserInfo userInfo = userService.getAuthorInfo(blogId);return userInfo;}
                service

                mapper

        

        前端代码

                 编写过程

        根据后端返回结果来进行编写

                 编写结果

                 结果展示

        代码修改只会,前端页面没用变化(查看网页源代码,代码没有更新)解决方案

        3.7 实现用户退出

                关于用户的登录,我们是通过后端来进行判断的,后端通过拦截器从header里面拿到token

        也就是这个值

        那如果没有值的话,后端就接收不到了,因此前端直接清除掉token即可.

        在js里面使用

        3.8 实现发布博客
        

     接口定义

        

    后端代码

                controller

                service

                mapper

        前端代码

                 编写结果

                 结果展示

        代码亮点,学习其他的写博客的网站,可以对字体大小进行编辑,加一些样式.之前的只是一个空的文本框,写不了格式,然后看一下其他的网站博客的编辑格式是怎么实现的,先调研了其他网站用的什么编辑器,然后选择使用哪个编辑器(markdown),如何实现编辑器(1. 自己写 2. 开源的)

        editor.md 简单介绍

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

        官⽹参见: http://editor.md.ipandao.com/

        代码: https://pandao.github.io/editor.md/

        我们直接引入进来即可

<link rel="stylesheet" href="editormd/css/editormd.css" />
<div id="test-editor"><textarea style="display:none;">### 关于 Editor.md**Editor.md** 是一款开源的、可嵌入的 Markdown 在线编辑器(组件),基于 CodeMirror、jQuery 和 Marked 构建。</textarea>
</div>
<script src="https://cdn.bootcss.com/jquery/1.11.3/jquery.min.js"></script>
<script src="editormd/editormd.min.js"></script>
<script type="text/javascript">$(function() {var editor = editormd("test-editor", {// width  : "100%",// height : "100%",path   : "editormd/lib/"});});
</script>

        因为我的后端返回过来的数据是json,因此我前端取值也要设置接收的为json,前端字符串和json相互转换.

        修改详情⻚⻚⾯显示

        此时会发现详情⻚会显⽰markdown的格式符号, 我们需要把markdown格式转换为html格式

        指定对谁进行渲染

        可以拓展的地方

        

        3.9 实现删除/编辑博客

        进⼊⽤⼾详情⻚时, 如果当前登陆⽤⼾正是⽂章作者, 则在导航栏中显示 [编辑] [删除] 按钮, ⽤⼾点击时则进⾏相应处理.(作者的userId和登录的userId是一样的那么就可以显示)

        需要实现两件事:

        • 判定当前博客详情⻚中是否要显示[编辑] [删除] 按钮

        • 实现编辑/删除逻辑.

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

        约定前后端交互接口        

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

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

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

        

    后端代码

                controller

                service

                mapper

        前端代码

                 编写结果

                 结果展示

         修改博客接口

              后端代码

                controller

                service

                mapper

              前端代码

                代码
        结果演示(此时更新时间没同步,需要改)

        3.10 加密/加盐
        

        加密介绍(JWT,和这个都是学习思想)

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

        比如我们现在数据库里面的信息(密码)是明文显示的,因此我们需要对敏感信息进行加密

        密码算法分类

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

        加密: y = f(x) ,x是明文,y是密文,f(x)是加密算法. 也就是一个算法按照一定的规则把一个字符串(一个信息)->另外一个信息.

        对称密码: y = f(x), x=f(y)

        非对称密码: y=f(x),x=m(y)

      

        1. 对称密码算法 是指加密秘钥和解密秘钥相同的密码算法. 常⻅的对称密码算法有: AES, DES, 3DES,RC4, RC5, RC6 等.(了解即可) 

        2. ⾮对称密码算法 是指加密秘钥和解密秘钥不同的密码算法. 该算法使⽤⼀个秘钥进⾏加密, ⽤另外⼀个秘钥进⾏解密.

        ◦ 加密秘钥可以公开,⼜称为 公钥

        ◦ 解密秘钥必须保密,⼜称为 私钥

        比如我给我的箱子上锁,锁是大家都能看见的,因此叫公钥,但是只能用我自己的钥匙打开,要是就是私钥

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

        上面俩种都可以通过密文得到明文

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

        加密思路

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

        同样的明文,通过MD5进行加密得到的密文都是一样的(不论什么语言,什么网站)

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

        那么如何把简单的密码改成复杂的密码?

        1. 加上复杂的字符串, md5(明文+ 复杂字符串)

        2. 加上随机的复杂字符串,md5(明文+随机复杂字符串)

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

        加密过程图

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

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

        解密过程图

        整体流程: 

        因为 明文 + 盐值 = 密文 ,当我们的盐值和密文一致的时候=>我们的明文是一样的

        当我们输入明文的时候,我们拿一个盐值通过加密算法得到密文,和我们数据库里面的密码加上同样的盐值得到的密文是一样的时候,我们就能够验证我们输入的明文就是一样的.(这个就是验证用户密码是否正确的过程)

        结论: 需要存储的信息: 密文+盐值

        我们先学习怎么使用

        此时提出一个问题,我们该怎么存储盐值?

        我们把盐值和密文存储在一起存,盐值和我们的密文一起进行拼接(怎么拼接看我们自己)

        我们此时就这么实现: 盐值+密文=>存到数据库里面

        数据库存储 盐值 + 密文

                           盐值 + md5(明文+盐值)

        总体流程

        具体的实现: 

package org.xiaobai.blog_system.utils;import org.springframework.util.DigestUtils;
import org.springframework.util.StringUtils;import java.util.UUID;public class SecurityUtils {//加密//password用户注册的时候输入的密码//return: 数据库中存储的信息: 盐值 + md5(明文+盐值)public static String encrypt(String password){//生成随机盐值String salt = UUID.randomUUID().toString().replace("-","");//对 明文 + 盐值 进行MD5加密 =>(明文+盐值)String finalPassword = DigestUtils.md5DigestAsHex((password+salt).getBytes());//返回信息return salt + finalPassword;}//验证密码是否正确//inputPassword用户登录的时候需要确认的密码//sqlPassword 数据库中password字段存储的信息  盐值 + md5(明文+盐值)public static boolean verify(String inputPassword,String sqlPassword){if(!StringUtils.hasLength(inputPassword)){return false;}if(sqlPassword == null || sqlPassword.length() != 64){return false;}//获取盐值String salt = sqlPassword.substring(0,32);//根据用户输入的密码和盐值,进行加密 md5(明文+盐值)和上面对应String finalPassword = DigestUtils.md5DigestAsHex((inputPassword + salt).getBytes());return (salt + finalPassword).equals(sqlPassword);}public static void main(String[] args) {System.out.println(encrypt("123456"));System.out.println(verify("123456", "de32ecf4414243229b11e1b9370d3a530c96c03766fae52a469cf658eb94b9d1"));System.out.println(verify("123457", "de32ecf4414243229b11e1b9370d3a530c96c03766fae52a469cf658eb94b9d1"));}}

        测试结果

        我们引入到项目中去使用

        修改⼀下数据库密码

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

        然后把数据库的密码改成我们生成的密文.

        最后进行部署,把项目搞到云服务器里面(这个后面我们学习了Linux之后再部署)

相关文章:

博客系统完整开发流程

前言 通过前⾯课程的学习, 我们掌握了Spring框架和MyBatis的基本使用, 并完成了图书管理系统的常规功能开发, 接下来我们系统的从0到1完成⼀个项⽬的开发. 企业开发的流程 1. 需求评审(产品经理(PM)会和运营(想口号),UI,测试,开发等沟通) ,会涉及到背景/目标/怎么做,可能会有多…...

【C++】面试常问八股

5、内存管理 野指针 野指针指的是未进行初始化或未清零的指针&#xff0c;不是NULL指针野指针产生原因及解决方案&#xff1a; 指针变量未初始化&#xff1a;指针变量定义时若未初始化&#xff0c;则其指向的地址是随机的&#xff0c;不为NULL&#xff1b;定义时初始化为NULL…...

自定义提交按钮触发avue-form绑定的submit事件

场景 使用avue-form时&#xff0c;提交按钮会绑定至form区域下方&#xff0c;如果想自定义按钮位置&#xff0c;需要通过dialog的footer位置进行编写&#xff0c;例如&#xff1a; <avue-form ref"form" v-model"dataInfo" :option"dataInfoOpti…...

HarmonyOS 无线调试

下载sdk 找到hdc位置> C:\Users\27638\AppData\Local\OpenHarmony\Sdk\14\toolchains 不要去DevEco Studio的窗口不知道为什么调不动 hdc tconn IP:PORT...

Android之APP更新(通过接口更新)

文章目录 前言一、效果图二、实现步骤1.AndroidManifest权限申请2.activity实现3.有版本更新弹框UpdateappUtilDialog4.下载弹框DownloadAppUtils5.弹框背景图 总结 前言 对于做Android的朋友来说&#xff0c;APP更新功能再常见不过了&#xff0c;因为平台更新审核时间较长&am…...

二、大模型微调技术栈全解析

大模型微调技术栈全解析&#xff1a;从微调方法到算力支撑 在大模型的领域中&#xff0c;微调&#xff08;Fine-tuning&#xff09;就像是为模型量身定制的高级裁缝服务&#xff0c;能够让通用的大模型更好地适应特定的任务和场景。而要完成这项精细的工作&#xff0c;需要一整…...

设置 C++ 开发环境

设置 C++ 开发环境 C++ 是一种通用编程语言,现在广泛用于竞争性编程。它具有命令式、面向对象的和通用编程功能。 C++ 可以在许多平台上运行,如 Windows、Linux、Unix、Mac 等。在我们开始使用 C++ 编程之前。我们需要在本地计算机上设置一个环境,以便成功编译和运行我们的…...

计算机基础知识

1、RAM和ROM RAM&#xff1a;随机存取存储器&#xff0c;也叫做主存。是与CPU直接交换数据的内部存储器。这种存储器在断电时将丢失其数据&#xff0c;故主要用于短时间使用的程序。 ROM&#xff1a;即只读存储&#xff0c;是一种只能读出事先所存数据的固态半导体存储器 2、…...

蓝桥杯——按键

一&#xff1a;按键得原理图 二&#xff1a;按键的代码配置 step1 按键原理图对应引脚配置为输入状态 step2 在GPIO中将对应引脚设置为上拉模式 step3 在fun.c中写按键扫描函数 写完后的扫描函数需放在主函数中不断扫描 扫描函数主要通过两个定义变量的值来判断&#xf…...

Zemax OpticStudio 中的扩散器建模

在 Zemax OpticStudio 中构建漫射器涉及创建散射或漫射光的表面或物体。以下是有关如何在 Zemax OpticStudio 中创建漫射器的一般指南&#xff1a; 转到非序列模式 (NSC) 选项卡。NSC 对于模拟与物体而非表面相互作用的非序列射线很有用。 在需要散光器的位置创建新对象。对象…...

网络安全防御:蓝队重保备战与应急溯源深度解析

课程目标 本课程旨在培养专业的网络安全蓝队成员&#xff0c;通过系统化的学习和实战演练&#xff0c;使学员能够掌握网络安全防御的核心技能&#xff0c;包括资产测绘、应急响应、系统安全应急溯源分析、网络层溯源分析以及综合攻防演练等。学员将能够熟练运用各种工具和技术…...

MySQL 和 Elasticsearch 之间的数据同步

MySQL 和 Elasticsearch 之间的数据同步是常见的需求&#xff0c;通常用于将结构化数据从关系型数据库同步到 Elasticsearch 以实现高效的全文搜索、聚合分析和实时查询。以下是几种常用的同步方案及其实现方法&#xff1a; 1. 应用层双写&#xff08;双写模式&#xff09; 原…...

【深度学习】矩阵的核心问题解析

一、基础问题 1. 如何实现两个矩阵的乘法&#xff1f; 问题描述&#xff1a;给定两个矩阵 A A A和 B B B&#xff0c;编写代码实现矩阵乘法。 解法&#xff1a; 使用三重循环实现标准矩阵乘法。 或者使用 NumPy 的 dot 方法进行高效计算。 def matrix_multiply(A, B):m, n …...

汽车开放系统架构(AUTOSAR)中运行时环境(RTE)生成过程剖析

一、引言 在当今高度智能化的汽车电子领域&#xff0c;软件系统的复杂性呈指数级增长。为了应对这一挑战&#xff0c;汽车开放系统架构&#xff08;AUTOSAR&#xff09;应运而生&#xff0c;它为汽车电子软件开发提供了标准化的分层架构和开发方法。其中&#xff0c;运行时环境…...

VC++零基础入门之系列教程 【附录E MFC快速参考指南】

附录E MFC快速参考指南 E.1 创建窗口 使用M F C CWnd wnd; W n d . C r e a t e E x ( E xSt y l e , C l a s s N a m e , Wi n d o w N a m e , S t y l e , x , y, Wi d t h , H e i g h t , P a r e n t , M e n u , P a r a m ) ; 使用A P I HWND hwnd=::CreateWi n d …...

Holoens2开发报错记录02_通过主机获取彩色和深度数据流常见错误

01.E1696 E1696 无法打开源文件 “stdio.h” 解决方法&#xff1a; 更新一下SDK 1&#xff09;打开Visual Studio Installer&#xff0c;点击修改 2&#xff09;安装详细信息中自己系统对应的SDK&#xff0c;点击修改即可 02.WinError 10060 方法来源 解决方法&#xff1a…...

粉色和紫色渐变壁纸怎么设计?

粉色和紫色的渐变壁纸设计可以打造极为浪漫的氛围&#xff0c;这两种颜色的搭配极具梦幻感与浪漫气息&#xff0c;常被用于各种浪漫主题的设计之中。以下是关于粉色和紫色渐变壁纸的设计方法&#xff1a; 一、渐变方向设计 横向渐变&#xff1a;从画面左侧的粉色过渡到右侧的紫…...

maven Problem shading JAR的几个解决方案

1 现象 Error creating shaded jar: Problem shading JAR &#xff1a;xxxxxx.jar entry META-INF/versions/11/com/fasterxml/jackson/core/io/doubleparser/BigSignificand.class: java.lang.IllegalArgumentException -> [Help 1] 2 原因 这个问题通常是由于 maven-s…...

前缀和代码解析

前缀和是指数组一定范围的数的总和,常见的有两种,一维和二维,我会用两道题来分别解析 一维 DP34 【模板】前缀和 题目: 题目解析: 暴力解法 直接遍历数组,遍历到下标为 l 时,开始进行相加,直到遍历到下标为 r ,最后返回总和.这样做的时间复杂度为: O(n) public class Main …...

CaffeineCache自定义缓存时间

文章目录 1、POM文件依赖2、声明缓存3、缓存使用4、测试缓存5、自定义缓存过期时间6、测试自定义超时时间 1、POM文件依赖 <dependency><groupId>com.github.ben-manes.caffeine</groupId><artifactId>caffeine</artifactId><version>3.1…...

keil中出现Error_Handler错误的解决方法

这个错误表明在代码中使用了 Error_Handler 函数但未定义。以下是完整的修复方案&#xff1a; 步骤 1&#xff1a;在 main.h 中添加函数声明 /* main.h */ void Error_Handler(void);步骤 2&#xff1a;在 main.c 中完善错误处理函数 /* main.c */ void Error_Handler(void) …...

low rank decomposition如何用于矩阵的分解

1. 什么是矩阵分解和低秩分解 矩阵分解是将一个矩阵表示为若干结构更简单或具有特定性质的矩阵的组合或乘积的过程。低秩分解&#xff08;Low Rank Decomposition&#xff09;是其中一种方法&#xff0c;旨在将原矩阵近似为两个或多个秩较低的矩阵的乘积&#xff0c;从而降低复…...

Cesium@1.126.0,创建3D瓦片,修改样式

第一步&#xff1a;添加3D建筑 Cesium.createOsmBuildingsAsync()这是一个异步方法&#xff0c;所以要写在一个异步函数里 创建一个函数 const create3DBuilding async (viewer) > {try {// 添加3D建筑const tileset await Cesium.createOsmBuildingsAsync();viewer.scen…...

MFC学习笔记-1

一、编辑框和按钮 //.h文件private:CString str;//给窗口类加了一个变量&#xff08;定义一个成员变量&#xff09;&#xff0c;关联到IDC_EDIT1中&#xff08;要在实现中关联&#xff0c;源文件文件夹中&#xff09;CString str2;//接收button2&#xff0c;和IDC_EDIT2绑定 p…...

Bugku CTF CRYPTO

Bugku CTF CRYPTO 文章目录 Bugku CTF CRYPTO聪明的小羊ok[-<>]散乱的密文.!? 聪明的小羊 描 述: 一只小羊翻过了2个栅栏 fa{fe13f590lg6d46d0d0} 分 析&#xff1a;栅栏密码&#xff0c;分2栏&#xff0c;一个栏里有11个 ①手动解密 f a { f e 1 3 f 5 9 0 l g 6 d 4 …...

Leetcode2502:设计内存分配器

题目描述&#xff1a; 给你一个整数 n &#xff0c;表示下标从 0 开始的内存数组的大小。所有内存单元开始都是空闲的。 请你设计一个具备以下功能的内存分配器&#xff1a; 分配 一块大小为 size 的连续空闲内存单元并赋 id mID 。释放 给定 id mID 对应的所有内存单元。 …...

BERT模型详解及代码复现

架构设计 BERT模型的架构设计是其成功的关键之一,它巧妙地融合了Transformer架构的优势,并针对自然语言处理任务进行了优化。具体来说,BERT的架构主要由三个模块组成: Embedding模块 :负责将输入的文本转换为模型可处理的向量表示。该模块由三种Embedding组成: Token Em…...

面试中自己挖的一些坑

一些面试的细节深度持续更新。。。 1. 这里有4题&#xff0c;单独写成了博客2. 经典的八股文之一 (ArrayList扩容原理)1.博主的回答2.面试官问的一些细节 3.经典的八股文之一 (HashMap扩容原理)1.博主的回答2.面试官问的一些细节 4.SpringBoot的启动原理1. 博主回答2. 面试官问…...

二、环 Ring

文章目录 一、环的定义二、环的分类与变种1、交换环2、含单位元的环3、零环4、非交换环5、整环6、域 三、环的性质与应用四、环与群和域的对比 一、环的定义 一个集合 R 被称为一个环&#xff0c;如果它满足以下条件&#xff1a; 对于 加法 满足&#xff1a; 闭合性&#xff1…...

Python图像处理入门:如何打开图像文件及常见格式

神经网络中的图像处理是一个非常重要的环节&#xff0c;尤其是在计算机视觉领域。作为一名新手&#xff0c;你可能会遇到一个常见的挑战——如何在 Python 中打开并理解图像文件。在本篇文章中&#xff0c;我们将介绍几种常见的图像文件格式&#xff0c;并讲解如何使用 Python …...

银河麒麟高级服务器操作系统在线调整/pro/{PID}/limits文件中nofile的软限制和硬限制参数值操作方法

银河麒麟高级服务器操作系统在线调整/pro/{PID}/limits文件中nofile的软限制和硬限制参数值操作方法 一 系统环境二 使用场景三 操作步骤 一 系统环境 [rootlocalhost ~]# nkvers ############## Kylin Linux Version ################# Release: Kylin Linux Advanced Server…...

网络运维学习笔记 018 HCIA-Datacom综合实验02

文章目录 综合实验2sw3&#xff1a;sw4&#xff1a;gw&#xff1a;core1&#xff08;sw1&#xff09;&#xff1a;core2&#xff08;sw2&#xff09;&#xff1a;ISP 综合实验2 sw3&#xff1a; vlan 2 stp mode stp int e0/0/1 port link-type trunk port trunk allow-pass v…...

深度学习进阶:构建多层神经网络

在上一篇文章中&#xff0c;我们从零开始构建了一个简单的两层神经网络&#xff0c;并通过异或问题&#xff08;XOR&#xff09;展示了神经网络的强大能力。今天&#xff0c;我们将进一步深入&#xff0c;构建一个更复杂的多层神经网络&#xff0c;并引入更多高级概念&#xff…...

高斯消元法

前置数学知识 n元线性方程是具有如下形式的方程&#xff1a; a 1 x 1 a 2 x 2 a 3 x 3 … a n x n b a_1x_1a_2x_2a_3x_3…a_nx_n b a1​x1​a2​x2​a3​x3​…an​xn​b 其中&#xff0c; a 1 , a 2 , . . . a_1,a_2,... a1​,a2​,...以及常数项 b b b均为已知的实数…...

ubuntu 安全策略(等保)

windows 三个帐号屏保设置组策略,密码超时次数/审计记录&#xff1b; linux 应具有登录失败处理功能&#xff0c;应配置并启用结束会话、限制非法登录次数和当登录连接超时自动退出等相关措施。 1、在系统中新建测试用户&#xff0c;使用此用户登录时多次输入错误密码&…...

计算机毕业设计SpringBoot+Vue.js购物推荐系统网站(源码+文档+PPT+讲解)

温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 作者简介&#xff1a;Java领…...

QT线程同步

文章目录 前言1. 使用互斥锁&#xff08;QMutex&#xff09;2.使用QMutexLocker便利类3. 使用读写锁&#xff08;QReadWriteLock&#xff09;4.QReadLocker便利类和QWriteLocker便利类对QReadWriteLock进行加解锁5. 使用信号量&#xff08;QSemaphore&#xff09;6. 使用条件变…...

何为第一二三产业?

第一、第二、第三产业的分类是经济学中对经济活动的划分方式&#xff0c;起源于20世纪30年代经济学家费希尔和克拉克的理论。以下是具体说明&#xff1a; 第一产业&#xff08;Primary Sector&#xff09; 定义&#xff1a;直接利用自然资源进行生产活动的行业。 核心领域&…...

Spring 面试题

Autowired和Resource两个注解的区别 Autowired&#xff1a; 是Spring框架的注解&#xff0c;用于依赖注入。 默认按照类型&#xff08;byType&#xff09;注入&#xff0c;如果存在多个相同类型的Bean&#xff0c;则会报错。 可以通过Qualifier指定具体的Bean名称。 如果没有匹…...

Linux设备驱动开发-SPI驱动开发详解(包含设备树处理详细过程)

基础知识及 SPI 相关结构体介绍 引脚&#xff1a;MISO&#xff08;master 输入&#xff0c;slave 输出&#xff09;&#xff0c;MOSI&#xff08;master 输出&#xff0c;slave 输入&#xff09;&#xff0c;片选引脚&#xff0c;SCK&#xff08;时钟&#xff09; 控制寄存器&…...

物联网平台建设方案一

系统概述 构建物联网全域支撑服务能力&#xff0c;为实现学院涵盖物联网设备的全面感知、全域互联、全程智控、全域数字基底、全过程统筹管理奠定基础&#xff0c;为打造智能化提供坚实后台基石。 物联网平台向下接入各种传感器、终端和网关&#xff0c;向上通过开放的实施分…...

java23种设计模式-桥接模式

桥接模式&#xff08;Bridge Pattern&#xff09;学习笔记 &#x1f31f; 定义 桥接模式属于结构型设计模式&#xff0c;将抽象部分与实现部分分离&#xff0c;使它们可以独立变化。通过组合代替继承的方式&#xff0c;解决多维度的扩展问题&#xff0c;防止类爆炸。 &#x…...

springboot实现文件上传到华为云的obs

一、前言 有时在项目中需要使用一些存储系统来存储文件&#xff0c;那么当项目要接入obs作为存储系统时&#xff0c;就会利用obs来进行文件的上传下载&#xff0c;具体实现如下。 二、如何通过obs实现文件的上传下载&#xff1f; 1.添加相关的obs的maven依赖。 <dependency…...

【红队利器】单文件一键结束火绒6.0

关于我们 4SecNet 团队专注于网络安全攻防研究&#xff0c;目前团队成员分布在国内多家顶级安全厂商的核心部门&#xff0c;包括安全研究领域、攻防实验室等&#xff0c;汇聚了行业内的顶尖技术力量。团队在病毒木马逆向分析、APT 追踪、破解技术、漏洞分析、红队工具开发等多个…...

DeepSeek 助力 Vue 开发:打造丝滑的滚动动画(Scroll Animations)

前言&#xff1a;哈喽&#xff0c;大家好&#xff0c;今天给大家分享一篇文章&#xff01;并提供具体代码帮助大家深入理解&#xff0c;彻底掌握&#xff01;创作不易&#xff0c;如果能帮助到大家或者给大家一些灵感和启发&#xff0c;欢迎收藏关注哦 &#x1f495; 目录 Deep…...

Compose 动画,让页面动起来

Compose 动画&#xff0c;让页面动起来 概述高级别动画APIAnimatedVisibilityMutableTransitionStateModifier.animateEnterExit自定义Enter/Exit动画 AnimatedContentContentTransform自定义动画SizeTranstion定义大小动画定义子元素动画自定义Enter/Exit动画 CrossfadeModifi…...

Windows CMD 命令大全(Complete List of Windows CMD Commands)

Windows CMD 命令大全&#xff1a; Windows CMD 是 Windows 系统内置的命令行工具&#xff0c;用于执行各种命令和管理任务。 称为Command Prompt。它提供了一个通过键入命令来与计算机系统进行交互的方式&#xff0c;类似于早期的DOS操作系统。以下是 CMD 的基础知识和常用命…...

DeepSeek在MATLAB上的部署与应用

在科技飞速发展的当下&#xff0c;人工智能与编程语言的融合不断拓展着创新边界。DeepSeek作为一款备受瞩目的大语言模型&#xff0c;其在自然语言处理领域展现出强大的能力。而MATLAB&#xff0c;作为科学计算和工程领域广泛应用的专业软件&#xff0c;拥有丰富的工具包和高效…...

IP代理在网络数据挖掘中的关键作用(AI大模型数据采集版)

在当今人工智能飞速发展的时代&#xff0c;AI大模型的训练需要海量且多样化的数据。然而&#xff0c;在数据采集过程中&#xff0c;常常面临诸多挑战&#xff0c;而IP代理在其中发挥着至关重要的作用。 数据采集的多样性是影响AI大模型性能的关键因素。如果数据来源单一&#x…...

pandas数据的导出

数据导出 将数据导出到CSV文件 数据对象.to_csv(filepath,sep"",indexFalse,encoding)参数1:文件的路径参数2:分隔符&#xff0c;默认是 ,参数3:是否保留索引 默认 Ture参数4:文件编码代码 &#xff1a; # 将数据导出到CSV # 引用 pandas import pandas as pd # 定…...