2.【SpringBoot3】用户模块接口开发

文章目录

  • 开发模式和环境搭建
    • 开发模式
    • 环境搭建
  • 1. 用户注册
    • 1.1 注册接口基本代码编写
    • 1.2 注册接口参数校验
  • 2. 用户登录
    • 2.1 登录接口基本代码编写
    • 2.2 登录认证
      • 2.2.1 登录认证引入
      • 2.2.2 JWT 简介
      • 2.2.3 登录功能集成 JWT
      • 2.2.4 拦截器
  • 3. 获取用户详细信息
    • 3.1 获取用户详细信息基本代码编写
    • 3.2 ThreadLocal 优化
  • 4. 更新用户基本信息
    • 4.1 更新用户基本信息基本代码编写
    • 4.2 更新用户基本信息参数校验
  • 5. 更新用户头像
    • 5.1 更新用户头像基本代码编写
    • 5.2 url 参数校验
  • 6. 更新用户密码

开发模式和环境搭建

开发模式

前后端分离开发中,前端向后端发出请求,后台处理完请求后给出响应数据。此时会出现一个问题:前端写代码时,怎么知道后端有哪些接口?后端写代码时,怎么知道前端需要什么样的数据呢?

这时就需要一套约束标准——接口文档。接口文档对每个接口的访问路径、请求方式、请求参数、响应数据进行了明确的说明。

在这里插入图片描述
前后端程序员参考同一份接口文档进行开发,项目就能无缝衔接了。

环境搭建

  • 执行 big_event.sql 脚本,准备数据库表
  • 创建 springboot 工程,引入对应的依赖(web、mybatis、mysql 驱动)
  • 在配置文件 application.yml 中引入 mybatis 的配置信息(将来连接的数据库在哪?用户名和密码是什么?)
  • 创建包结构,并准备实体类

在这里插入图片描述
(1) 创建表

-- 创建数据库
create database big_event;

-- 使用数据库
use big_event;

-- 用户表
create table user (
    id int unsigned primary key auto_increment comment 'ID',
    username varchar(20) not null unique comment '用户名',
    password varchar(32)  comment '密码',
    nickname varchar(10)  default '' comment '昵称',
    email varchar(128) default '' comment '邮箱',
    user_pic varchar(128) default '' comment '头像',
    create_time datetime not null comment '创建时间',
    update_time datetime not null comment '修改时间'
) comment '用户表';

-- 分类表
create table category(
    id int unsigned primary key auto_increment comment 'ID',
    category_name varchar(32) not null comment '分类名称',
    category_alias varchar(32) not null comment '分类别名',
    create_user int unsigned not null comment '创建人ID',
    create_time datetime not null comment '创建时间',
    update_time datetime not null comment '修改时间',
    constraint fk_category_user foreign key (create_user) references user(id) -- 外键约束
);

-- 文章表
create table article(
    id int unsigned primary key auto_increment comment 'ID',
    title varchar(30) not null comment '文章标题',
    content varchar(10000) not null comment '文章内容',
    cover_img varchar(128) not null  comment '文章封面',
    state varchar(3) default '草稿' comment '文章状态: 只能是[已发布] 或者 [草稿]',
    category_id int unsigned comment '文章分类ID',
    create_user int unsigned not null comment '创建人ID',
    create_time datetime not null comment '创建时间',
    update_time datetime not null comment '修改时间',
    constraint fk_article_category foreign key (category_id) references category(id),-- 外键约束
    constraint fk_article_user foreign key (create_user) references user(id) -- 外键约束
)

(2) 创建springboot工程(这次采用手动创建的方式)

在这里插入图片描述

在这里插入图片描述

创建好的 maven 工程缺少 resources 目录:

在这里插入图片描述

于是创建 resources 目录:

在这里插入图片描述

工程需要在 resources 目录下提供一个核心配置文件,即 yml 配置文件

在这里插入图片描述

至此,boot 工程创建完毕。下面引入所需的依赖:

在这里插入图片描述

(3) 在配置文件 application.yml 中引入 mybatis 的配置信息

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    # 3306是mysql默认端口,big_event是数据库名称
    url: jdbc:mysql://localhost:3306/big_event
    username: root
    password: 123456

(4) 创建包结构,并准备实体类

① 创建包结构:controller、service(包括impl)、mapper、pojo、util

在这里插入图片描述

② 准备实体类

import java.time.LocalDateTime;

public class User {
    private Integer id;//主键ID
    private String username;//用户名
    private String password;//密码
    private String nickname;//昵称
    private String email;//邮箱
    private String userPic;//用户头像地址
    private LocalDateTime createTime;//创建时间
    private LocalDateTime updateTime;//更新时间
}
import java.time.LocalDateTime;

public class Article {
    private Integer id;//主键ID
    private String title;//文章标题
    private String content;//文章内容
    private String coverImg;//封面图像
    private String state;//发布状态 已发布|草稿
    private Integer categoryId;//文章分类id
    private Integer createUser;//创建人ID
    private LocalDateTime createTime;//创建时间
    private LocalDateTime updateTime;//更新时间
}
import java.time.LocalDateTime;

public class Category {
    private Integer id;//主键ID
    private String categoryName;//分类名称
    private String categoryAlias;//分类别名
    private Integer createUser;//创建人ID
    private LocalDateTime createTime;//创建时间
    private LocalDateTime updateTime;//更新时间
}

③ boot 工程启动类

SpringBoot 的启动类名称一般是工程名+Application,所以先来为 App 重命名一下:

在这里插入图片描述

现在 BigEventApplication.java 内容是这样:

在这里插入图片描述

改造后的 BigEventApplication.java:

在这里插入图片描述

在用户模块,总共有 6 个接口需要开发:

  • 注册
  • 登录
  • 获取用户详细信息
  • 更新用户基本信息
  • 更新用户头像
  • 更新用户密码

开发流程:

在这里插入图片描述

1. 用户注册

接口文档:

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

1.1 注册接口基本代码编写

需求:输入用户名、密码,点击注册。

在这里插入图片描述

首先来看一下数据库表字段和实体类属性,两者是一一对应的。Java 的属性习惯用驼峰命名法,数据库表字段习惯用下划线命名法。另外可以观察到,用户头像(userPic / user_pic)的数据类型是字符串,这是因为头像的图片会存放在三方服务器上,这里的变量只是存放一个服务器上的访问地址。

在这里插入图片描述

可以看到,实体类中并没有写 getter、setter、toString 等方法,这是因为可以通过 lombok 工具在编译阶段自动生成 getter、setter、toString 等方法。

使用 lombok 需要在 pom.xml 中引入依赖,并在实体类上添加注解。

<!--lombok依赖-->
<dependency>
  <groupId>org.projectlombok</groupId>
  <artifactId>lombok</artifactId>
</dependency>

在这里插入图片描述

添加注解后,重新编译:

在这里插入图片描述

从 target 中找到编译过的 User 类,可以发现,其中已经有 getter、setter、toString 等方法。

在这里插入图片描述

响应数据一般由 code、message、data 三个数据组成。于是可以定义 Result 类,该类含 code、message、data 三个成员变量。

在这里插入图片描述

当我们给浏览器响应一个 Result 对象时,Spring 会将该 Result 对象转换成 json 字符串,此时的响应数据格式就符合了文档要求。

//统一所有接口的响应结果格式
//lombok 提供了两个注解
@NoArgsConstructor   //在编译时生成无参构造方法
@AllArgsConstructor  //在编译时生成全参构造方法
@Data
public class Result<T> {
    private Integer code;//业务状态码  0-成功  1-失败
    private String message;//提示信息
    // data成员变量的类型是泛型T,也就是说data可以对应Object、String、Bean对象等等
    private T data;//响应数据

    //返回操作成功响应结果(带响应数据)
    public static <E> Result<E> success(E data) {
        return new Result<>(0, "操作成功", data);
    }

    //返回操作成功响应结果(不带响应数据,如注册、添加文章)
    public static Result success() {
        return new Result(0, "操作成功", null);
    }

	//返回操作失败响应结果
    public static Result error(String message) {
        return new Result(1, message, null);
    }
}

在上面代码中,使用了 lombok 提供的两个注解:
@NoArgsConstructor:在编译时生成无参构造方法
@AllArgsConstructor:在编译时生成全参构造方法

完成了实体类,下面就来分析一下三层架构:

Controller 中,要声明 register 方法完成注册功能。方法上添加了 @PostMapping,是因为文档中标明该请求是 post 请求。请求路径是 /register,对比文档少了 /user,是因为 Controller 类上会添加 /user,两者拼接后就是文档中要求的路径了。返回值类型是统一的 Result。在方法内部要首先看用户名是否已被占用,然后注册。当然,这些过程需要调用 Service 来完成。

在这里插入图片描述

Service 层要提供对应的两个方法:根据用户名查询用户、注册

在这里插入图片描述

相应地,Mapper 层也需要声明两个方法,分别用于执行查询和插入的 Sql

在这里插入图片描述

下面开始编写代码,首先创建相关的类和接口:

在这里插入图片描述

首先编写 UserController:

在这里插入图片描述

findByUserName 和 register 两个方法爆红是因为 UserService 中还未声明这两个方法。将光标放在爆红的方法上,Ctrl + Enter 就能在 UserService 中生成对应的方法。

在这里插入图片描述

在 UserController 中,userService 爆红是因为还未向 IOC 容器中注入 UserService 的 bean 对象。

下面编写 UserService 的实现类:

@Service
public class UserServiceImpl implements UserService {

    @Autowired
    private UserMapper userMapper;

    //根据用户名查询用户
    @Override
    public User findByUserName(String username) {
        User user = userMapper.findByUserName(username);
        return user;
    }

    //注册(添加用户)
    @Override
    public void register(String username, String password) {
        //要先将密码通过MD5加密,然后调用Mapper层进行注册
        String md5String = Md5Util.getMD5String(password);
        userMapper.add(username, md5String);
    }
}

UserMapper:

@Mapper
public interface UserMapper {
    //根据用户名查询用户
    @Select("select * from user where username = #{username}")
    User findByUserName(String username);

    //注册(添加用户)
    //now()是mysql的函数
    @Insert("insert into user(username, password, create_time, update_time)" +
            " values(#{username}, #{password}, now(), now())")
    void add(String username, String password);
}

postman 测试:

在这里插入图片描述
数据库表已写入:

在这里插入图片描述

现在数据库中已经有了 wangba 用户,如果再用同样的用户名注册,就会出现以下情况:

在这里插入图片描述

1.2 注册接口参数校验

前面开发了注册接口,但忽略了一件事。接口文档中对于 username 和 password 有明确的说明,两者必须是 5~16 位的非空字符。

在这里插入图片描述

所以后端的接口必须能保证:如果前端传递的参数不符合规则,是不能完成注册的。因此,后端接口需要对两个参数进行校验。我们首先想到的方式可能是通过 if-else 来判断,如下:

在这里插入图片描述

这种方式确实能实现预期功能,但是代码看起来很繁琐。

因此,Spring 提供了一个参数校验框架 Spring Validation,使用预定义的注解来完成参数校验。

使用 Spring Validation 对接口参数的合法性进行校验的流程:

  • 引入Spring Validation 起步依赖
  • 在参数前面添加 @Pattern 注解,按照正则表达式的要求进行参数校验
  • 在 Controller 类上添加 @Validated 注解,使类中方法的参数上的注解能够被扫描到(使 @Pattern 生效)

在这里插入图片描述

<!--validationy依赖-->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

在这里插入图片描述

完成以上操作后,发现虽然校验起作用了,但是返回的 json 数据并不符合接口文档要求的 code-message-data 格式:

在这里插入图片描述

此时,可以使用全局异常处理器来解决。

全局异常处理器:参数校验失败异常处理

需要定义一个类,并标注 @RestControllerAdvice 注解,表示该类用于处理异常。由于该注解是 @Restxxx,所以该类中所有方法的返回值都会被转换成 json 字符串相应给浏览器。在类中,需要添加一个方法用于处理异常,方法上添加 @ExceptionHandler,Exception.class 表示处理所有异常。注意方法返回值类型是 Result,如此一来,即使出现异常,返回的结果也是满足接口文档要求的。

在这里插入图片描述

@RestControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(Exception.class)
    public Result handleException(Exception e){//参数上声明一个变量,一旦出现异常,就接收捕获异常对象
        //为了方便调试,将异常信息输出到控制台,否则因为异常被捕获了,就不会输出了
        e.printStackTrace();
        //Exception对象一般都会封装错误提示信息,使用e.getMessage()来获取
        //但有些异常并不会封装错误提示信息,所以需要判断一下
        //借助Spring中String字符串的工具类StringUtils.hasLength(),如果包含错误信息就返回,否则返回“操作失败”
        return Result.error(StringUtils.hasLength(e.getMessage())? e.getMessage() : "操作失败");
    }
}

添加异常处理类之后,返回的错误信息就符合接口文档中的 json 格式要求了:

在这里插入图片描述

2. 用户登录

接口文档:

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

在这里插入图片描述

2.1 登录接口基本代码编写

需求:输入用户名、密码,登录。
在这里插入图片描述

对于登录,仍然是考虑 Controller、Service、Mapper 层。可以发现,Service、Mapper 层需要做的就只是根据用户名查询,而且这部分代码在注册部分已经完成了。

在这里插入图片描述
于是只关注 Controller 即可:

在这里插入图片描述

postman 测试:

① 用户名不存在

在这里插入图片描述

② 用户名密码都正确,登录成功

在这里插入图片描述

③ 密码错误:

在这里插入图片描述

2.2 登录认证

2.2.1 登录认证引入

假如现在已经有了 ArticalController 接口,该接口中有文章列表查询方法。

@RestController
@RequestMapping("/article")
public class ArticleController {
    @GetMapping("/list")
    public Result<String> list(){
        return Result.success("所有的文章数据...");
    }
}

正常情况下,如果用户未登录,应该是不能访问到 ArticalController 的文章列表查询方法的。所以,其他接口就应该在提供服务之前对登录状态进行检查,这个检查的过程称为登录认证。当前程序显然不能达到登录认证的效果。

如何实现登录验证呢?需要借助令牌技术。

浏览器访问登录接口时,如果登录成功,就在后台生成令牌,并把该令牌响应给浏览器。浏览器再访问其他接口时,都需要携带该令牌。其他接口如果看到浏览器已携带令牌,且该令牌合法,就会正常提供服务,否则不提供。这个令牌跟皇帝的令牌差不多,起身份识别的作用。

令牌本质是一个字符串,且满足以下要求:

  • 承载业务数据,减少后续请求查询数据库的次数:
    比如:系统中经常需要知道本次操作是由哪个用户发起的,如果每次都去数据库查询该用户的信息,就会降低系统性能。浏览器每次发起请求都会携带令牌,若能把用户信息封装到令牌中,需要用户数据时就可以从令牌中获取,从而减少数据库查询次数,提高系统性能。
  • 防篡改,保证信息的合法性和有效性:
    令牌要具备防伪功能,否则会有安全隐患

当前满足令牌要求的规范有很多,在 web 开发中最常用的是 JWT。

2.2.2 JWT 简介

JWT 全称:JSON Web Token,即用于 web 领域的基于 json 格式的令牌(https://jwt.io/)

定义了一种简洁的、自包含的格式,用于通信双方以 json 数据格式安全的传输信息。

在这里插入图片描述

上面是一个 JWT 令牌字符串,通过两个 . 将字符串分成了三部分,每个字串对应 token 令牌中的一部分。

第一部分: Head(头),是由一段 json 字符串编码得来,该 json 字符串记录两个信息,alg 是加密算法(防篡改),type 是令牌类型。

第二部分: Payload(有效载荷),是由一段 json 字符串编码得来,该 json 字符串用于存放业务数据,如用户的 id 和 username。

在这两部分中,json 字符串如何才能转换成 token 令牌中展示的这段字符串呢?在 JWT 中会借助 Base64 这种编码方式来完成。把任意数据转换成 64 个字符可打印字符,这些字符的特点就是通用,在任意场景下都能被支持。

为什么要将 json 字符串转换为 64 个可打印字符呢?主要是为了提高 token 的适用性。比如:当 json 中包含中文或空格等字符时,cookie 就不能支持,因此 JWT 中就将 json 字符串通过 Base64 编码转换成 token 中展示的形式。

Base64 仅仅是一种编码方式,并不是加密方式,且这种编码方式是公开的,任何人都能通过 Base64 进行编码和解码。所以在 token 的第二部分(有效载荷)一定不要存放登录密码等私密数据。

第三部分: Signature(数字签名),将第一、二部分借助密钥和加密算法经过加密得来,这里的加密算法就是第一部分(头)中通过 alg 来指定的,密钥可以在程序中单独配置。有了数字签名,就可以防篡改了,确保 token 是安全的。因为即使篡改了前两部分,第三部分也是不能篡改的,因为该部分是加密后的字符串。将来 JWT 在去解析 token 令牌时,会通过解密数字签名来得到第一、二部分,再拿着解密的内容与用户传递的内容进行比对,如果不一样就证明篡改过数据,就不允许访问。

下面在代码中实现:

在用户登录成功后,需要生成 JWT 令牌并响应给浏览器。前面说过:浏览器在访问其他接口时,都需要携带该令牌。其他接口如果看到浏览器已携带令牌,且该令牌合法,就会正常提供服务,否则不提供。所以重点在于如何生成令牌验证令牌

在实际开发中,生成令牌、验证令牌的代码不需要记,一般直接调用现成 api。

(1) 导入坐标

<!--SpringBoot整合单元测试的起步依赖-->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-test</artifactId>
</dependency>

(2) 生成、验证 JWT 令牌

先通过测试类编写代码主体,验证代码的功能。

public class JwtTest {
	//生成JWT令牌
    @Test
    public void testGen() {
        Map<String, Object> claims = new HashMap<>();
        claims.put("id", 1);
        claims.put("username", "张三");
        //生成jwt的代码
        String token = JWT.create()
                .withClaim("user", claims)//添加数据
                //添加过期时间,登录后一个小时不操作令牌就失效
                .withExpiresAt(new Date(System.currentTimeMillis() + 1000 * 60 * 60))
                .sign(Algorithm.HMAC256("itheima"));//指定算法,配置密钥为itheima

        System.out.println(token);
    }
    
	//验证JWT令牌
    @Test
    void testParse() {
        String token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3MDU2NzQzNDYsInVzZXIiOnsiaWQiOjEsInVzZXJuYW1lIjoi5byg5LiJIn19.lgWdmBGpXd6wNXSIxxUxBgx5BcGEH12f17a1iJ-2AaU";
        //申请JWT验证器,解密时采用与加密同样的算法和密钥
        //build方法用于生成验证器
        JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("itheima")).build();
        //调用验证器的方法来验证token,生成解析后的JWT对象
        //如果解析成功,就可以从中得到头部、载荷、签名
        DecodedJWT decodedJWT = jwtVerifier.verify(token);
        //getClaims得到所有的载荷
        Map<String, Claim> claims = decodedJWT.getClaims();
        //只需要得到键为user的载荷
        System.out.println(claims.get("user"));
    }
}

如果解析令牌时抛出异常,可能的原因是:

  • 篡改了头部或载荷部分的数据
  • 解密与加密的密钥不一致
  • token 过期

2.2.3 登录功能集成 JWT

在上节,我们已经能够生成和验证 JWT 令牌,本节将在登录接口中集成 JWT。

登录成功时生成令牌,其他接口在提供服务前验证该令牌,只有令牌合法才提供服务。

当然,第一步仍然是导入坐标

<!--SpringBoot整合单元测试的起步依赖-->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-test</artifactId>
</dependency>

JWT 生成和验证工具类:

public class JwtUtil {

    private static final String KEY = "itheima";
	
	//接收业务数据,生成token并返回
    public static String genToken(Map<String, Object> claims) {
        return JWT.create()
            .withClaim("claims", claims)
            .withExpiresAt(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 12))
            .sign(Algorithm.HMAC256(KEY));
    }

	//接收token,验证token,并返回业务数据
    public static Map<String, Object> parseToken(String token) {
        return JWT.require(Algorithm.HMAC256(KEY))
            .build()
            .verify(token)
            .getClaim("claims")
            .asMap();
    }
    
}

添加了 JWT 令牌生成功能的 UserController:

在这里插入图片描述

//@RestController整合了@ResponseBody注解,将后台传到前端的java对象转为json数据
@RestController
@RequestMapping("/user")
@Validated
public class UserController {

    @Autowired
    private UserService userService;

    @PostMapping("/register")
    //^和$之间是正则表达式的主体,\S表示非空,它前面的\是转义字符,{5,16}表示5~16位
    public Result register(@Pattern(regexp = "^\\S{5,16}$") String username, @Pattern(regexp = "^\\S{5,16}$") String password){
        //查询用户
        User user = userService.findByUserName(username);
        if(user == null){
            //没有占用,注册
            userService.register(username, password);
            return Result.success();
        }else {
            //已占用
            return Result.error("用户名已被占用");
        }
    }

    @PostMapping("/login")
    public Result<String> login(@Pattern(regexp = "^\\S{5,16}$") String username, @Pattern(regexp = "^\\S{5,16}$") String password){
        //根据用户名查询用户
        User user = userService.findByUserName(username);
        //判断用户是否存在
        if (user == null){
            return Result.error("用户名错误");
        }
        //判断密码是否正确,从数据库获取到的user对象的password是密文
        if (Md5Util.getMD5String(password).equals(user.getPassword())){
            //登录成功,生成JWT令牌
            Map<String, Object> claims = new HashMap<>();
            //令牌携带id和username即可
            claims.put("id", user.getId());
            claims.put("username", user.getUsername());
            //调用方法生成JWT令牌
            String token = JwtUtil.genToken(claims);
            return Result.success(token);
        }
        //密码错误
        return Result.error("密码错误");
    }
}

postman 验证:

在这里插入图片描述

在 Article 提供服务之前先验证 token 令牌,这个 token 从哪来呢?之前我们说浏览器访问其他接口时会携带 token,这个 token 是以什么形式携带的呢?是请求头还是请求体?文档中给出了说明:

用户登录成功后,系统会自动下发 JWT 令牌,然后在后续的每次请求中,浏览器将 JWT 令牌包裹在请求头 header 中携带到服务端。JWT 令牌在请求头中的名称为 Authorization。
如果检测到用户未登录,则 http 响应状态码为 401(未授权)。

所以,应该在请求头中去获取浏览器携带的 token。从请求头获取 token 可以直接在参数中声明,并在参数前面添加注解 @RequestHeader(name=请求头名称),这样 token 就能获取到了。获取到 token 之后,再利用 JwtUtil 解析 token,如果解析的代码正常执行说明 JWT 验证成功,否则失败。另外,在验证失败时,http 响应状态码是 401。如何设置响应状态码?需要一个 Response 对象,Response 对象也可以在参数中声明,到时候框架在调用方法时会把该对象传入。

@RestController
@RequestMapping("/article")
public class ArticleController {
    @GetMapping("/list")
    public Result<String> list(@RequestHeader(name = "Authorization") String token, HttpServletResponse response){
        try {
            //如果这行代码正常执行,就解析成功
            Map<String, Object> claims = JwtUtil.parseToken(token);
            return Result.success("所有的文章数据...");
        } catch (Exception e) {
            //设置http响应状态码为401
            response.setStatus(401);
            return Result.error("未登录");
        }
    }

}

完成了 JWT 验证的代码,下面来测试一下是否能达到预期效果:

未登录时,@RequestHeader(name = "Authorization") String token 参数接收不到请求头,抛出异常,文章列表访问不成功(这段 json 是由前面定义的异常处理器返回的):

在这里插入图片描述

如果浏览器要携带请求头,需要写前端代码,如果不写前端代码的话,可以用 postman 辅助验证:

在这里插入图片描述

2.2.4 拦截器

至此就完成了登录认证。但是存在一个问题:Contorller 层有 UserController、ArticleController 等很多 Controller,且每个 Contorller 都提供了很多接口,难道要在每个接口中都写同样的代码完成令牌的校验吗,这显然是不合适的。

在这里插入图片描述
如果多个接口有同样的任务要完成,可以用拦截器实现:

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

要实现拦截器首先要定义一个类,去实现 HandlerInterceptor 接口,并重写 preHandle 方法(意为在目标方法执行之前拦截下来):

@Component//将当前拦截器的对象注入IOC容器
public class LoginInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //拦截请求,拦截下来之后进行令牌验证
        //之前通过参数声明来拿到令牌,这里借助request对象(请求对象)拿到令牌,因为这个对象包含所有的请求数据
        String token = request.getHeader("Authorization");
        //解析令牌
        try {
            //如果这行代码正常执行,就解析成功
            Map<String, Object> claims = JwtUtil.parseToken(token);
            //放行
            return true;
        } catch (Exception e) {//解析失败
            //设置http响应状态码为401
            response.setStatus(401);
            //不放行
            return false;
        }
    }
}

还要写一个 web 配置类,它实现 WebMvcConfigurer 接口,在该类重写的 addInterceptors 方法中将刚刚编写的拦截器注册进去就可以了。注意:登录和注册接口不拦截。

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Autowired
    private LoginInterceptor loginInterceptor;
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //登录接口和注册接口不拦截
        registry.addInterceptor(loginInterceptor).excludePathPatterns("/user/login", "/user/register");
    }
}

现在,我们把 ArticleController 中接口的代码注释掉:

在这里插入图片描述

用 postman 测试:

① 没有登录(携带令牌)时,访问文章列表:

在这里插入图片描述

② 登录(携带令牌)后,访问文章列表:

在这里插入图片描述

3. 获取用户详细信息

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

3.1 获取用户详细信息基本代码编写

需求:当用户登录成功后,需要跳转到首页,首页顶部展示了用户昵称、头像等信息,像这样的数据都需要访问后台接口来获取。在个人中心中有基本资料、更换头像、重置密码功能,也都需要先查询,再展示。

在这里插入图片描述

获取用户详细信息,需要根据已登录的用户名查询用户。
但接口文档中写到,该请求没有请求参数,并没有携带 username。此时,可以从 token 令牌中获取 username,因此可以在方法上声明一个带@RequestHeader(name = "Authorization")注解的参数,从而可以获取 token,进而解析 token 得到用户名。关于根据用户名查询信息在 Service 和 Mapper 层的实现,前面都已经完成了,此处不再赘述。

在这里插入图片描述

代码具体实现

在 UserController 中添加:

@RequestMapping("/userInfo")
public Result<User> userInfo(@RequestHeader(name = "Authorization") String token){
    Map<String, Object> map = JwtUtil.parseToken(token);
    String username = (String) map.get("username");
    User user = userService.findByUserName(username);
    return Result.success(user);
}

postman 测试

未登录(未携带 token 令牌):

在这里插入图片描述
已登录(已携带 token 令牌):

在这里插入图片描述

我们用 postman 测试时,每一个请求都要单独在请求头中设置 token 令牌,比较繁琐。实际上可以为一个集合中所有的请求统一添加请求头:

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

但是,此时还有一个问题:后台在响应获取用户信息的请求时,把用户密码也传过去了,这样是不安全的。

解决方式:在 password 成员变量上添加 @JsonIgnore 注解,这样,SpringMVC 在把当前对象转换成 json 字符串时,就会忽略 password,最终的 json 字符串也就没有 password 这个属性了。

在这里插入图片描述

postman 测试发现,后台传回的数据中确实没有 password 了:

在这里插入图片描述

另外,可以发现传回的数据中 nickname、email、userPic 为空,这时因为用户注册时并没有填写这些信息,后续用户注册成功后才会完善这些信息。

那么,createTime 和 updateTime 为什么为 null 呢?

原因在于:数据库表中的字段是下划线命名方式,而 User 实体类中的成员变量是驼峰命名方式,二者名称不一致,所以 mybatis 就不知道该如何封装了。

在这里插入图片描述

要解决这个问题,需要在配置文件(application.yml)中开启驼峰命名和下划线命名自动转换:

mybatis:
  configuration:
    map-underscore-to-camel-case: true
    #开启驼峰命名和下划线命名自动转换

开启之后,当 mybatis 发现数据库表中是下划线命名时,就会在实体类中去找对应的驼峰命名的属性,然后完成数据封装。此时再次用 postman 测试可以发现,后台已经能返回 createTime 和 updateTime。

在这里插入图片描述

3.2 ThreadLocal 优化

在获取用户信息的代码中,为了获取已登录用户的用户名,需要在方法的参数上声明 token,并且在方法体中解析 token 得到用户名。这看似合理,但是拦截器中已经解析过 token 了,重复写相同的代码不是一种好的编程习惯。因此,我们希望在其他地方复用拦截器中解析得到的结果。如何才能做到呢?

在这里插入图片描述

此时需要用到 ThreadLocal 来优化代码。

ThreadLocal 的作用

  • 提供线程的局部变量。
  • 提供 set() / get() 方法来存取数据: ,使用 ThreadLocal 存储的数据是线程安全的,每个线程之间互不影响。

举个例子,多个线程使用 ThreadLocal 存取用户名:现有蓝色、绿色两个线程,它们都持有 ThreadLocal 的 tl 对象的引用。在两个线程中,都可以使用 set() 方法来存储用户名,蓝色线程存储用户名“萧炎”,绿色线程存储用户名“药尘”。存储完毕后,在蓝色线程中调用 get() 方法获取用户名时,获取到的就是“萧炎”,而不能获取到绿色线程存入的“药尘”;绿色线程中调用 get() 方法时同理。因为 ThreadLocal 会为两个线程分别创建数据存储空间,可以做到线程隔离。

在这里插入图片描述

代码验证:

public class ThreadLocalTest {
    @Test
    public void testThreadLocalSetAndGet(){
        //提供一个ThreadLocal对象
        ThreadLocal tl = new ThreadLocal();
        //开启两个线程
        //传递两个参数:线程任务和线程名字
        new Thread(()->{
            tl.set("萧炎");
            System.out.println(Thread.currentThread().getName()+": "+tl.get());
            System.out.println(Thread.currentThread().getName()+": "+tl.get());
            System.out.println(Thread.currentThread().getName()+": "+tl.get());
        }, "蓝色").start();

        new Thread(()->{
            tl.set("药尘");
            System.out.println(Thread.currentThread().getName()+": "+tl.get());
            System.out.println(Thread.currentThread().getName()+": "+tl.get());
            System.out.println(Thread.currentThread().getName()+": "+tl.get());
        }, "绿色").start();
    }
}

输出结果:

蓝色: 萧炎
蓝色: 萧炎
绿色: 药尘
绿色: 药尘
绿色: 药尘
蓝色: 萧炎

现在,ThreadLocal 的作用已经明白了,但是它与当前需求有什么关系呢?

假设现在程序中有 ArticleController、ArticleService、ArticleDao,它们都有 add() 方法,且方法内部都要用到 userId,那么就得在每个方法上声明 userId 完成参数传递,如果其他地方需要 userId,也需要通过传参来完成。

在这里插入图片描述

这个操作不难,但是很繁琐。如果既不想在方法上声明,又想使用 userId,怎么办呢?此时就可以用 ThreadLocal 来优化。

可以维护一个全局的 ThreadLocal 对象,用来存储用户名这类数据。有了它,可以在请求到达拦截器之后,调用 tl.set() 方法将 userId 存储到 ThreadLocal 中。接下来,当请求到达 ArticleController、ArticleService、ArticleDao 之后,就可以在它们的 add() 方法中调用 tl.get() 方法从 ThreadLocal 中获取 userId 来使用。

Controller、Service、Dao 在容器中一般都是单例的,获取 id 的时候怎么知道是哪个用户的 id 呢?会不会发生线程安全问题呢?

假如现在有两个用户来访问程序,其 userId 分别是 1 和 2,当请求到达 tomcat 之后,服务器会为每个用户开辟一个线程用来提供服务。在为用户 1 提供服务时,会把拦截器中的 preHandle() 方法加载进黑色线程执行。在执行 tl.set(userId) 时,由于用户 1 携带的 userId=1,所以就将 1 这个 userId 设置进去。拦截器放行之后,会依次加载 ArticleController、ArticleService、ArticleDao 中的 add() 方法进栈继续执行,在执行这些 add() 方法时,获取到的 userId 都是 1;为用户 2 提供服务时同理。这样就做到了线程隔离。

在这里插入图片描述

因此可以借助 ThreadLocal 做到两件事:

  • 减少参数传递
  • 同一个线程执行的代码之间共享数据,拦截器中的数据共享到 Controller、Service、Dao 中

明白了 ThreadLocal 的使用场景,下面借助 ThreadLocal 优化一下代码:

(1) 提供 ThreadLocal 工具类

@SuppressWarnings("all")
public class ThreadLocalUtil {
    //提供一个常量THREAD_LOCAL,用来维护一个全局唯一的ThreadLocal对象
    private static final ThreadLocal THREAD_LOCAL = new ThreadLocal();

    //根据键获取值
    public static <T> T get(){
        //调用ThreadLocal对象的get()方法,把得到的数据返回
        //强制类型转换
        return (T) THREAD_LOCAL.get();
    }
	
    //存储键值对
    public static void set(Object value){
        //调用ThreadLocal的set方法把值存进去
        THREAD_LOCAL.set(value);
    }

    //清除ThreadLocal,防止内存泄漏
    //ThreadLocal对象是全局唯一的,生命周期特别长,如果里面的数据一直不清除,有可能造成内存泄漏
    public static void remove(){
        THREAD_LOCAL.remove();
    }
}

(2) 在拦截器中,把 token 令牌(含id、username)的解析结果存入 ThreadLocal

在这里插入图片描述

(3) UserController 中的方法获取 ThreadLocal 存储的 token 令牌解析结果

在这里插入图片描述

前面说过,ThreadLocal 中的数据用完后要清除,应该在哪清除呢?

(4) 在拦截器中重写 afterCompletion() 方法来清除数据

当请求发起后,在拦截器中解析 token 令牌,并将解析结果存入 ThreadLocal;拦截器放行之后,Controller、Service、Dao 中就都能使用到 ThreadLocal 中的数据。那么 ThreadLocal 中的数据什么时候用完呢?当响应完成,即本次请求结束后,该数据就不再使用了。所以就应该在请求结束后将数据移除。

在这里插入图片描述

postman 测试成功:

在这里插入图片描述

4. 更新用户基本信息

接口文档:

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

4.1 更新用户基本信息基本代码编写

需求:当用户在个人中心点击“基本信息”时,页面主区域会展示当前用户的详细信息,用户可以修改“用户昵称”、“用户邮箱”,最后提交修改,从而访问后台接口,更新当前用户信息。而且可以看到,登录名称,即“用户名”是不允许修改的。

在这里插入图片描述

浏览器在请求体中以 json 格式携带 id、username、nicknime、email 等数据,在后台,会用实体类对象 User 来接收这些数据,为了让框架能够将请求体中的 json 数据自动转换成实体类对象,需要在接收参数之前添加 @RequestBody 注解,方法声明完成后,在方法体内调用 Service 层的方法完成更新就可以了。所以 Service 层将来也要提供更新对应的方法,Mapper 层也要执行对应的 SQL:

在这里插入图片描述
UserController 中添加方法:

@PutMapping("/update")
//@RequestBody:将请求体中的json数据自动转换成实体类对象传给参数user
public Result update(@RequestBody User user){
    userService.update(user);
    return Result.success();
}

UserService 及其实现类添加:

//UserService.java 
void update(User user);//更新用户信息

//UserServiceImpl.java
@Override
public void update(User user) {//更新用户信息
    //获取系统当前时间
    user.setUpdateTime(LocalDateTime.now());
    userMapper.update(user);
}

UserMapper 添加:

//更新用户信息
//格式:数据库表字段名=#{实体类属性名}
@Update("update user set nickname=#{nickname},email=#{email},update_time=#{updateTime} where id=#{id}")
void update(User user);

postman测试:

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

4.2 更新用户基本信息参数校验

在接口的方法上声明了一个实体类参数 user,用它来接收前端传过来的参数 id、username、nicknime、email,但刚才并没有对这些参数进行校验,而接口文档中对 id、nicknime、email 这三个参数是由要求的:id 必须传递,不能为 null;nicknime 除了不为 null,还得是 1~10 位的非空字符;email 除了不为 null,还要满足邮箱格式。而 username 在注册之后就不能修改了,所以此时可以不用关注。

在这里插入图片描述

本章节采用 Validation 完成参数校验。

之前注册的时候,已经使用 Validation 进行了请求参数的校验,只不过是通过直接在方法参数上添加 @Pattern 注解的方式。

在这里插入图片描述

但现在把请求数据封装在实体类对象 user 中了,像这样的实体参数应该如何完成校验呢?

① 在实体类的成员变量上添加 Validation 提供的注解,对指定的属性值进行参数校验

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

② 在具体使用该实体参数的地方添加 @Validated 注解,此时实体类属性上的注解才能生效

在这里插入图片描述

5. 更新用户头像

接口文档:

在这里插入图片描述

在这里插入图片描述

5.1 更新用户头像基本代码编写

需求:当用户在个人中心点击了“更换头像”之后,在页面主区域展示出当前用户的头像,用户点击“选择图片”按钮选择本地的一张图片,然后点击“上传头像”按钮,以访问后台接口,完成头像更新。

在这里插入图片描述

Controller 层添加更新头像的方法,由于请求方式是 PATCH,所以方法上添加 @PatchMapping 注解,方法的参数列表上声明 avatarUrl 参数来接收头像地址。方法内部调用 Service 层更新头像的方法,Mapper 层编写 SQL 来更新头像(update_time 也要更新一下)。

在这里插入图片描述

UserController 中添加更新头像的方法:

@PatchMapping("/updateAvatar")
//@RequestParam: 用于接收url地址传参,标明需要从queryString中获取数据
public Result updateAvatar(@RequestParam String avatarUrl){
    userService.updateAvatar(avatarUrl);
    return Result.success();
}

UserService 中添加:

//UserService接口
//更新用户头像
void updateAvatar(String avatarUrl);

//UserService实现类
//更新用户头像
@Override
public void updateAvatar(String avatarUrl) {
    //因为mapper层需要根据id修改用户头像,所以要先从ThreadLocal中把id拿过来
    Map<String,Object> map = ThreadLocalUtil.get();
    Integer id = (Integer) map.get("id");
    userMapper.updateAvatar(avatarUrl, id);
}

UserMapper 中添加:

//更新用户头像
@Update("update user set user_pic=#{avatarUrl}, update_time=now() where id=#{id}")
void updateAvatar(String avatarUrl, Integer id);

postman 测试:
在这里插入图片描述

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

5.2 url 参数校验

在当前更新用户头像代码的基础上,如果传递一个不符合 url 地址格式要求的地址,仍然会操作成功且更新数据库,这是不合理的。因此需要后台要对传递过来的头像地址 avatarUrl 进行参数校验。

如何校验是否为 url 地址呢?

Validation 提供了一个注解 @URL 来完成数据是否是 url 的校验:

在这里插入图片描述

postman 测试:

在这里插入图片描述

6. 更新用户密码

接口文档:

在这里插入图片描述

在这里插入图片描述

需求:用户点击个人中心的“重置密码”后,会在主区域展示重置密码的表单,表单填写完成后,可以点击“修改密码”按钮,从而调用后台接口,完成修改密码操作。

在这里插入图片描述

之前接收前端 json 数据时采用的是 User 实体对象,当时是因为 json 中的键名刚好与 User 实体类中的属性名相同。但是现在更新用户密码时,json 中的键名可能是 oldPwd、newPwd 等,与实体类的属性不一致,所以需要声明一个 Map 集合来接收前端传过来的 json 数据。到时候,SpringMVC 会自动将 json 数据转换成 Map 集合对象。方法声明好之后,同样需要在方法内部调用 Service 层的方法完成密码更新,Mapper 层也需要执行对应的 SQL,除了要更新 password 字段,还要更新 update_time字段,且同样要通过 id 来更新。

在这里插入图片描述
UserController 中添加修改 / 更新密码的方法:

@PatchMapping("/updatePwd")
//@RequestBody:使SpringMVC框架自动读取请求体中的json数据,转换为Map集合对象
public Result updatePwd(@RequestBody Map<String, String> params){
    //Validation提供的注解并不能满足需求,所以要手动校验参数
    String oldPwd = params.get("old_pwd");
    String newPwd = params.get("new_pwd");
    String rePwd = params.get("re_pwd");

    //原密码、新密码、确认密码是否都传过来了
    if (!StringUtils.hasLength(oldPwd) || !StringUtils.hasLength(newPwd) || !StringUtils.hasLength(rePwd)){
        return Result.error("缺少必要的参数");
    }

    //原密码填写是否正确
    //从ThreadLocalUtil的信息中拿到username,从而查询到数据库中的原密码,与用户输入的原密码进行比对
    Map<String,Object> map = ThreadLocalUtil.get();
    String username = (String) map.get("username");
    User user = userService.findByUserName(username);
    if (!user.getPassword().equals(Md5Util.getMD5String(oldPwd))){
        return Result.error("原密码填写不正确");
    }
    //新密码与确认密码是否一致
    if (!newPwd.equals(rePwd)){
        return Result.error("新密码与确认密码不一致");
    }
    //调用Service完成密码更新
    userService.updatePwd(newPwd);
    return Result.success();
}

UserService 中添加:

//UserService接口
//更新用户密码
void updatePwd(String newPwd);

//UserService实现类
//更新用户密码
@Override
public void updatePwd(String newPwd) {
    //因为mapper层需要根据id修改用户密码,所以要先从ThreadLocal中把id拿过来
    Map<String,Object> map = ThreadLocalUtil.get();
    Integer id = (Integer) map.get("id");
    //新密码需要先加密再更新到数据库
    userMapper.updatePwd(Md5Util.getMD5String(newPwd), id);
}

UserMapper 中添加:

//更新用户密码
@Update("update user set password=#{password}, update_time=now() where id=#{id}")
void updatePwd(String password, Integer id);

postman 测试:

在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

数字孪生技术助力澳大利亚绿色能源行业

OpenUtilities可实现变电站智能数字化设计&#xff0c;减少对环境的影响并节省 50% 的成本 将智能数字化设计扩展到小型基建工程 Essential Energy 的电网跨越 73.7 万公里&#xff0c;覆盖了澳大利亚新南威尔士州约 95&#xff05;的地区&#xff0c;为 1,500 个地区、农村和…

C++并发编程:线程启动

启动线程 C中构造 std::thread 对象启动线程 void do_some_work(); std::thread my_thread(do_some_work); 最简单的情况下是无参数无返回的函数。启动一个新的线程执行hello()函数。这种函数在其所属线程上运行&#xff0c;函数执行完毕&#xff0c; 线程结束。为了让编译器…

【深度学习:数据增强 】提高标记数据质量的 5 种方法

【深度学习&#xff1a;数据增强 】提高标记数据质量的 5 种方法 计算机视觉中常见的数据错误和质量问题&#xff1f;为什么需要提高数据集的质量&#xff1f;提高标记数据质量的五种方法使用复杂的本体结构作为标签人工智能辅助标签识别标签错误的数据改进注释者管理 计算机视…

Gitee Reward让开源作者不再为爱发电

一、什么是Gitee Reward&#xff1f; Gitee Reward是Gitee为改善开源开发生命周期提出的新策略。开源项目的支持者们可以更轻松地为其喜爱的项目提供资金&#xff0c;贡献者们也可以因为其不懈的开源贡献得到奖励。 二、Gitee Reward上允许哪些类型的项目&#xff1f; 允许任…

2024最新版Python 3.12.1安装使用指南

2024最新版Python 3.12.1安装使用指南 Installation and Configuration Guide to the latest version Python 3.12.1 in 2024 By Jackson Python编程语言&#xff0c;已经成为全球最受欢迎的编程语言之一&#xff1b;它简单易学易用&#xff0c;以标准库和功能强大且广泛外挂…

瑞_数据结构与算法_二叉树

文章目录 1 什么是二叉树2 二叉树的存储2.1 使用树节点类TreeNode存储&#xff08;代码&#xff09;2.2 使用数组存储 3 二叉树的遍历3.1 广度优先遍历3.2 深度优先遍历3.2.1 深度优先——前序遍历3.2.2 深度优先——中序遍历3.2.3 深度优先——后序遍历 3.3 代码实现3.3.1 递归…

03--数据库连接池

1、数据库连接池 1.1 JDBC数据库连接池的必要性 在使用开发基于数据库的web程序时&#xff0c;传统的模式基本是按以下步骤&#xff1a; 在主程序&#xff08;如servlet、beans&#xff09;中建立数据库连接进行sql操作断开数据库连接 这种模式开发&#xff0c;存在的问题:…

小程序样例2:简单图片分类查看

基本功能&#xff1a; 1、根据分类展示图片&#xff0c;点击类目切换图片&#xff1a; 2、点击分类编辑&#xff0c;编辑分类显示&#xff1a; 3、点击某个分类&#xff0c;控制主页该分类显示和不显示&#xff1a; 类目2置灰后&#xff0c;主页不再显示 4、点击分类跳转到具…

【C++语言1】基本语法

前言 &#x1f493;作者简介&#xff1a; 加油&#xff0c;旭杏&#xff0c;目前大二&#xff0c;正在学习C&#xff0c;数据结构等&#x1f440; &#x1f493;作者主页&#xff1a;加油&#xff0c;旭杏的主页&#x1f440; ⏩本文收录在&#xff1a;再识C进阶的专栏&#x1…

Python语法进阶——类

Python中的数据类型都属于类。int、str、list都是Python定义好的数据类型类。 print(type(list))#<class type> print(type(list()))#<class list> 一、自定义数据类型 一、语法 class 类名():pass #类名 要求首字母大写 #()可写可省略。 #pass在这里只是用来保证…

推荐IDEA一个小插件,实用性很高!!

插件&#xff1a; Convert YAML and Properties File 由于每个人的开发习惯不同&#xff0c;在开发过程中会遇到各种小细节的问题。今天给大家介绍一个小插件&#xff0c;作用不大&#xff0c;细节很足。 就是properties类型文件和yml文件互相自由转换 解决&#xff1a;…

2023年DevOps国际峰会暨 BizDevOps 企业峰会(DOIS北京站):核心内容与学习收获(附大会核心PPT下载)

随着科技的飞速发展&#xff0c;软件开发的模式和流程也在不断地演变。在众多软件开发方法中&#xff0c;DevOps已成为当下热门的软件开发运维一体化模式。特别是在中国&#xff0c;随着越来越多的企业开始认识到DevOps的价值&#xff0c;这一领域的研究与实践活动日益活跃。本…

计算机网络——运输层(2)暨小程送书

计算机网络——运输层&#xff08;2&#xff09;暨小程送书 小程一言专栏链接: [link](http://t.csdnimg.cn/ZUTXU) 运输层&#xff08;2&#xff09;TCP/IP对比TCP&#xff08;传输控制协议&#xff09;&#xff1a;IP&#xff08;互联网协议&#xff09;&#xff1a;总结 拥塞…

【设计模式】适配器和桥接器模式有什么区别?

今天我探讨一下适配器模式和桥接模式&#xff0c;这两种模式往往容易被混淆&#xff0c;我们希望通过比较他们的区别和联系&#xff0c;能够让大家有更清晰的认识。 适配器模式&#xff1a;连接不兼容接口 当你有一个类的接口不兼容你的系统&#xff0c;而你又不希望修改这个…

RabbitMQ的安装使用

RabbitMQ是什么&#xff1f; MQ全称为Message Queue&#xff0c;消息队列&#xff0c;在程序之间发送消息来通信&#xff0c;而不是通过彼此调用通信。 RabbitMQ 主要是为了实现系统之间的双向解耦而实现的。当生产者大量产生数据时&#xff0c;消费者无法快速消费&#xff0c;…

公网环境调试本地配置的Java支付宝沙箱环境模拟支付场景

文章目录 前言1. 下载当面付demo2. 修改配置文件3. 打包成web服务4. 局域网测试5. 内网穿透6. 测试公网访问7. 配置二级子域名8. 测试使用固定二级子域名访问 前言 在沙箱环境调试支付SDK的时候&#xff0c;往往沙箱环境部署在本地&#xff0c;局限性大&#xff0c;在沙箱环境…

【Proteus仿真】【51单片机】遥控小风扇设计

文章目录 一、功能简介二、软件设计三、实验现象联系作者 一、功能简介 本项目使用Proteus8仿真51单片机控制器&#xff0c;使用数码管显示模块、L298N电机驱动模块、按键、直流电机风扇、红外遥控等。 主要功能&#xff1a; 系统运行后&#xff0c;数码管显示风扇速度档位&a…

【操作系统】实验二 Proc文件系统

&#x1f57a;作者&#xff1a; 主页 我的专栏C语言从0到1探秘C数据结构从0到1探秘Linux &#x1f618;欢迎关注&#xff1a;&#x1f44d;点赞&#x1f64c;收藏✍️留言 &#x1f3c7;码字不易&#xff0c;你的&#x1f44d;点赞&#x1f64c;收藏❤️关注对我真的很重要&…

FluoroQuest抗淬灭试剂盒I 适合载玻片成像,能够提高荧光信号的强度和稳定性

您好&#xff0c;欢迎来到新研之家 文章关键词&#xff1a;FluoroQuestAnti-fading Kit I Optimized for Slide Imaging&#xff0c;FluoroQuest抗淬灭试剂盒I 适合载玻片成像 一、基本信息 产品简介&#xff1a;FluoroQuest抗淬灭试剂盒I 适合载玻片成像能够抑制淬灭效应&a…

unity项目《样板间展示》开发:素材导入与整理

第一章&#xff1a;素材导入与整理 前言一、创建项目文件二、导入素材模型三、素材模型整理四、光源模型管理结语 前言 这次带大家从0到1做一个unity项目&#xff1a;《样板间展示》。 顾名思义&#xff0c;项目内容是展示样板间&#xff0c;即玩家可以与房间中的物体、家具进行…