Jwt(Json web token)——从Http协议到session+cookie到Token Jwt介绍 Jwt的应用:登陆验证的流程

news2024/12/26 21:44:39

目录

  • 引出
  • 从Http协议到session&cookie到Token
    • HTTP协议
    • session & cookie
      • session
      • cookie
      • 为什么需要session & cookie?
    • JavaEE传统解决长连接方案
    • 问题:分布式不适用
    • 解决方案:令牌Token
  • Jwt,Json web token
    • jwt的结构
    • Header
      • 加密算法
      • Base64 + 图片编码
    • Payload 载荷:比如唯一的用户ID
      • Registered claims
      • Public claims
      • Private claims
      • Payload案例
    • Signature:签名
  • jwt使用初步
    • 导包
    • 生成token
    • 解析token
    • 获取token的内容
    • 验证token
    • 几种token验证的情况
      • token的过期时间
      • token的内容被改过
      • token的载荷被更改
  • Jwt的应用:登陆验证的流程
    • 用户表
    • dao数据库
    • service业务
    • 定义异常
      • 异常的拦截处理
    • controller层
      • swagger测试
    • 使用拦截器统一拦截方式
    • Vo对象给前端
    • DTO对象
      • 日期json问题
      • 枚举类型的json化
    • application.yml配置文件
    • pom文件
  • 总结

引出


1.http协议的特点,无状态,被动请求;
2.session+cookie解决浏览器的无状态问题,但是分布式不适用;
3.Jwt,Json web token,token令牌入门,头部+载荷+签名;
4.Jwt的使用初步,如果过期,报错com.auth0.jwt.exceptions.TokenExpiredException;
5.应用:登陆验证的流程;

从Http协议到session&cookie到Token

HTTP协议

特点

  • 无记忆性(无状态)

  • 请求响应

    短连接

    被动请求

session & cookie

session

Session: Session存在服务器端的对象。

cookie

Cookie就是存储在浏览器的一个对象(客户端)

为什么需要session & cookie?

本质上讲 : http协议的问题(无记忆性)。

在这里插入图片描述

JavaEE传统解决长连接方案

在这里插入图片描述

在这里插入图片描述

问题:分布式不适用

  • 只适合于单体架构,集群/分布式架构的不合适
    • Session要进行同步,比较困难(网络抖动、延迟等),有状态的
    • Cookie: 不支持跨域,前后端分离
    • 移动端(很少使用cookie),比如微信小程序不支持cookie

session需要同步

在这里插入图片描述

  • 对于多服务器集群,每台服务器都要读取Session,共享困难。

在这里插入图片描述

解决方案:令牌Token

在这里插入图片描述

加密技术是对信息进行编码和解码的技术,编码是把原来可读信息(又称明文)译成代码形式(又称密文),其逆过程就是解码(解密),加密技术的要点是加密算法,加密算法可以分为三类:

  • 对称加密,如AES
    • 基本原理:将明文分成N个组,然后使用密钥对各个组进行加密,形成各自的密文,最后把所有的分组密文进行合并,形成最终的密文。
    • 优势:算法公开、计算量小、加密速度快、加密效率高
    • 缺陷:双方都使用同样密钥,安全性得不到保证
  • 非对称加密,如RSA
    • 基本原理:同时生成两把密钥:私钥和公钥,私钥隐秘保存,公钥可以下发给信任客户端
      • 私钥加密,持有私钥或公钥才可以解密
      • 公钥加密,持有私钥才可解密
    • 优点:安全,难以破解
    • 缺点:算法比较耗时
  • 不可逆加密,如MD5,SHA
    • 基本原理:加密过程中不需要使用密钥,输入明文后由系统直接经过加密算法处理成密文,这种加密后的数据是无法被解密的,无法根据密文推算出明文。

RSA算法历史:

1977年,三位数学家Rivest、Shamir 和 Adleman 设计了一种算法,可以实现非对称加密。这种算法用他们三个人的名字缩写:RSA

目前流行的还有oauth2

Jwt,Json web token

JSON Web Token(令牌)

JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact压缩的 and self-contained自包含的 way for securely transmitting information安全传输 between parties as a JSON object。

This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret (with the HMAC algorithm) or a public/private key pair using RSA or ECDSA.

  • 基于JSON格式用于网络传输的令牌。
  • 紧凑的Claims声明格式。
  • Claim有索赔、声称、要求或者权利要求的含义。

jwt的结构

在这里插入图片描述

官网:https://jwt.io/

在这里插入图片描述

如果改前面的,最后的签名会改变,就知道被改动过了

在这里插入图片描述

Header

Header 部分是一个 JSON 对象,描述 JWT 的元数据。

在这里插入图片描述

加密算法

  • HSA256

    SHA(Secure Hash Algorithm,安全散列算法)是一个密码散列函数家族,由美国国家安全局(NSA)设计,并由美国国家标准与技术研究院(NIST)发布,是美国的政府标准。

    • 无论输入多长,都输出64个字符,共32字节(byte),256位(bit)
    • 输出只包含数字0`9`和字母`A`F,大小写不敏感
  • RSA256

    由美国麻 省理工 学院三 位学者 Riv est、Sh amir 及Adleman 研 究发 展出 一套 可实 际使用 的公 开金 钥密码系 统,那 就是RSA(Rivest-Shamir-Adleman)密码系统。

    RS256(带有SHA-256的 RSA 签名)是一种非对称算法,它使用公钥/私钥对:身份提供者拥有用于生成签名的私钥(秘密)密钥,而 JWT 的消费者获得公钥验证签名。由于与私钥相反,公钥不需要保持安全,因此大多数身份提供者都可以让消费者轻松获取和使用(通常通过元数据 URL)。

Base64 + 图片编码

在这里插入图片描述

Base64是一种基于64个可打印字符来表示二进制数据的表示方法。Base64不是加密算法,只是一种编码方式,可以用Base64来简单的“加密”来保护某些数据,所以每 6 个比特为一个单元,对应某个可打印字符。

在这里插入图片描述

“hello” 编码

h—01101000 e —01100101

在这里插入图片描述

在这里插入图片描述

package com.tianju.redisDemo.jwt;

import org.apache.ibatis.logging.stdout.StdOutImpl;

import java.util.Base64;

public class JwtDemo {
    public static void main(String[] args) {
        String s = "h";
        // hz转成2进制:h-->ASCII值(10进制)-->2进制
        String bin = "0" + Integer.toBinaryString(s.charAt(0));
        System.out.println("h转换成2进制:"+bin);
        System.out.println("截取前6位:"+bin.substring(0, 6));
        // 进行base64编码
        String sBase64 = new String(Base64.getEncoder().encode(s.getBytes()));
        System.out.println(sBase64);
    }
}

Base64 在线编码解码 | Base64 加密解密 - Base64.us

在这里插入图片描述

在这里插入图片描述

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<img src="data:image/x-icon;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAQAQAABMLAAATCwAAAAAAAAAAAAAsLTD/CwsJ/xoeIf82QUn/Njw5/0ZGRf9LTFH/WVZX/1JVV/9iZmT/ZmNx/1dha/85Qkz/Jyct/w0HBf8OCgf/Cw0P/yEhIv87O0D/RjpI/0tBRv8uLCj/FxsS/x0eGP8bFhn/IyAl/y8xMv9xd3r/eoCJ/0NFVP8gHSX/CAUC/xQXG/8nKC//OjY4/z86Nv8RDRv/AAAZ/wAAGf8UEyL/MyEg/w8AAP8FAAD/DAgH/2hobv+FhZH/MzVA/xUSEv8+QUb/Ly43/ykiK/8BAT3/BARi/wEBaP8BAFj/LCRk/2NCO/80AAH/LAMD/x0BAf8BAAD/PDk+/2pucv82MjX/SUdY/0EvJ/8KCTH/BwWT/wcEpv8CAJr/AAF6/y0ue/+HSEz/XQAA/1EDB/8+Awb/HAEB/wgAAP9nZmr/Yl5l/01OUP8uJiX/AwN5/wMFzP8FCMj/Awa//wAArP8sMJX/lU9R/34AAP96Awb/VwME/y0EAP8XAAD/MCYp/3t6gP9cVk//JSdE/wECyP8ABu7/AQDx/wEA4v8AAMH/LTG4/6NcW/+WAAD/oQAC/5IDAf9UBQD/NwMA/x0EBf92eX7/eWNi/yQkY/8BAu3/AQD9/xwk6P9HUPD/EhPi/y4z1v+tZGD/nAAA/7UDA//RCAb/fgUB/1ICAf8mAAD/goiK/4RuZv8kHnf/AgH9/wAA9v9gce7/9//5/7+++P+Ji+3/qGZq/6MAAP+oAgn/rQcL/4YEBP9aAwH/KAEB/4+Kj/96dm3/IB5X/wYB+P8BAP3/HyXv/8bN9f//29vt/6teZv+dAAH/iwUA/3MCAf9gAwP/PgQD/xcAAP90bnX/Zm5y/yUiNP8GBcT/AgT//wAA/f8vL/L/eHfx/3J37P+tb2v/qwEA/5oBA/9sAwH/UAMD/ywBAf8mEhn/b212/11ugf9aR1D/AQFg/wUG9v8EAvz/BwD6/wAA5f8wNNT/pG1s/54AAP+NBAP/YwQB/zoCAv8NAAD/T01X/05OWP9Nb4T/Xl9f/0M1TP8AAJz/CArl/wIE6f8AALz/ODq0/7Jwbv93AAD/bgQD/0gEAP8WAAD/GxYZ/15aaP8pLDj/X5bB/2iHnP97c3n/KSg4/wMAfv8EAL7/AACc/zczoP+fYWb/YgAA/1EBAf8WAAD/HxMW/2hpdf87R1n/MTM9/zRtnf+Pxev/gpek/2pjZP9gX2z/JCNW/wAATf8bG1X/VT1C/xoAAP8gDg7/PDQ4/1pNVv9QSVr/SEtd/xIRGP9McYj/R3uU/4a+2f9Wbn//SVFQ/2lmXv9oYV//YlVd/2VVVP9eVlf/WFlf/0dKVv8zQUj/Q0xU/yAYIP8CAAT/gABseQAAMyAAAGRlAAAyNAAAdGUAAGluAAAxLAAAdXQAAG50AABBTQAAUFIAAEVTAABSXwAAVkUAADIzgAFSTw=="
style="width: 100px" height="100px">
</body>
</html>

Payload 载荷:比如唯一的用户ID

Payload 部分也是一个 JSON 对象,用来存放实际需要传递的数据。JWT 规定了7个官方字段,供选用。

Payload 也使用BASE64编码

Registered claims

这些是一组预先定义的声明,它们不是强制性的,但推荐使用,以提供一组有用的,可互操作的声明。 其中一些是:iss(发行者),exp(到期时间),sub(主题),aud(受众)等

序号名称解释
1iss (issuer)签发人
2exp (expiration time)过期时间
3sub (subject)主题
4aud (audience)受众(接收jwt的一方)
5nbf (Not Before)生效时间
6iat (Issued At)签发时间
7jti (JWT ID)jwt的唯一身份标识,
主要用来作为一次性token,
从而回避重放攻击

Public claims

这些可以由使用JWT的人员随意定义。

Private claims

这些是为了同意使用它们但是既没有登记,也没有公开声明的各方之间共享信息,而创建的定制声明。

Payload案例

{  "sub": "456781234",  "name": "Mr.zhang",  "admin": true}

Signature:签名

The signature is used to verify the message wasn’t changed along the way, and, in the case of tokens signed with a private key, it can also verify that the sender of the JWT is who it says it is.

Signature 部分是对前两部分的签名,防止数据篡改。

https://jwt.io/

Signature部分由编码后的Header、Payload和自定义的秘钥使用Header中指定的算法(HSA256)进行加密签名;

在这里插入图片描述

结构

在这里插入图片描述

jwt使用初步

在这里插入图片描述

导包

<!--        jwt的依赖-->
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>4.3.0</version>
        </dependency>

生成token

package com.tianju.redisDemo.jwt;

import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

public class JavaJwtTest {
    public static void main(String[] args) {
        // 定义头部header
        Map<String,Object> header = new HashMap<>();
        header.put("alg", "HS256");

        // 私钥
        String salt = "pet";
        
        String token = JWT.create()
                .withHeader(header)
                .withClaim("username", "tom")
                .withExpiresAt(new Date(System.currentTimeMillis() + 1000 * 60)) // 60s之后过期
                .sign(Algorithm.HMAC256(salt.getBytes()));
        System.out.println(token);

    }
}

获得token

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2ODk3NzY0MTUsInVzZXJuYW1lIjoidG9tIn0.kPmgw03j9g3EJAo2LLzXHd1b7H3uF5zF-0EDEaeonMY

在这里插入图片描述

解析token

DecodedJWT decode = JWT.decode(token);

获取token的内容

System.out.println("Header: " + decode.getHeader());
System.out.println("Payload: " + decode.getPayload());
System.out.println("Audience: " + decode.getAudience());
System.out.println("Signature: " + decode.getSignature());

验证token

JWTVerifier build = JWT.require(Algorithm.HMAC256(user.getPassword())).build();

获取信息

System.out.println(build.verify(token).getClaim("data"));

{“name”:”tomcat”,”username”:”tom”}

几种token验证的情况

token的过期时间

com.auth0.jwt.exceptions.TokenExpiredException

package com.tianju.redisDemo.jwt;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

public class JavaJwtTest1 {
    public static void main(String[] args) throws InterruptedException {
        // 定义头部header
        Map<String,Object> header = new HashMap<>();
        header.put("alg", "HS256");

        // 私钥
        String salt = "pet";

        String token = JWT.create()
                .withHeader(header)
                .withClaim("username", "tom")
                .withExpiresAt(new Date(System.currentTimeMillis() + 1000 * 3)) // 60s之后过期
                .sign(Algorithm.HMAC256(salt.getBytes()));
        System.out.println(token);

        Thread.sleep(1000*5);

        JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("pet")).build();
        DecodedJWT verify = jwtVerifier.verify(token);
        System.out.println(verify.getClaim("username"));
    }
}

在这里插入图片描述

token的内容被改过

com.auth0.jwt.exceptions.JWTDecodeException

package com.tianju.redisDemo.jwt;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

public class JavaJwtTest1 {
    public static void main(String[] args) throws InterruptedException {
        // 定义头部header
        Map<String,Object> header = new HashMap<>();
        header.put("alg", "HS256");

        // 私钥
        String salt = "pet";

        String token = JWT.create()
                .withHeader(header)
                .withClaim("username", "tom")
                .withExpiresAt(new Date(System.currentTimeMillis() + 1000 * 3)) // 60s之后过期
                .sign(Algorithm.HMAC256(salt.getBytes()));
        System.out.println(token);

        token = token.replaceAll("[a-zA-Z]","X");


        JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("pet")).build();
        DecodedJWT verify = jwtVerifier.verify(token);
        System.out.println(verify.getClaim("username"));
    }
}

在这里插入图片描述

token的载荷被更改

在这里插入图片描述

在这里插入图片描述

package com.tianju.redisDemo.jwt;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

public class JavaJwtTest1 {
    public static void main(String[] args) throws InterruptedException {
        // 定义头部header
        Map<String,Object> header = new HashMap<>();
        header.put("alg", "HS256");

        // 私钥
        String salt = "pet";

        String token = JWT.create()
                .withHeader(header)
                .withClaim("username", "tom")
                .withExpiresAt(new Date(System.currentTimeMillis() + 1000 * 60 * 30)) //
                .sign(Algorithm.HMAC256(salt.getBytes()));
        System.out.println("登陆成功产生token:"+token);

//        token = token.replaceAll("[a-zA-Z]","X");

        String hackToken = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2ODk3Nzk1MjMsInVzZXJuYW1lIjoicGV0In0.laRjGZsM5nq8IYPBMSYCbMdxyFMiXaZXKg9F7WL-n-Q";

        System.out.println("被改过的token:"+hackToken);
        JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("pet")).build();
        DecodedJWT verify = jwtVerifier.verify(hackToken);
        System.out.println(verify.getClaim("username"));
        System.out.println("签名:"+ verify.getSignature());
    }
}

Jwt的应用:登陆验证的流程

在这里插入图片描述

整体的流程:

第一步:产生30分钟有效时间的token;

第二步:在redis里面存储token,redis里面有效时间为60分钟;

第三步:token过期,但是Redis里面的token还没有过期,此时进行续期;

第四步:给原有的token续期,需要设置最大续期时间,目前用最大续期次数解决;

用户表

用户名、密码、电话号码、邮箱

在这里插入图片描述

user.java实体类

package com.tianju.redisDemo.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.Date;

@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("user_owner")
public class User {
    @TableId(value = "id",type = IdType.AUTO)
    private Integer id;
    @TableField("username")
    private String username;

    @TableField("realname")
    private String realName;

    @TableField("password")
    private String password;

    @TableField("tel")
    private String tel;

    @TableField("security_key")
    private String securityKey;

    @TableField("create_time")
    private Date createTime;
}

dao数据库

UserMapper.java文件

package com.tianju.redisDemo.dao;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.tianju.redisDemo.entity.User;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface UserMapper extends BaseMapper<User> {

}

service业务

处理登陆业务:抛出异常:

(1)throw new UsernameIsEmptyException(“输入为空异常”);

(2)throw new UsernameNotFoundException(“用户名不存在异常”); 后面用布隆过滤器

(3)throw new UsernameOrPasswordErrorException(“用户名或者密码错误异常”);

package com.tianju.redisDemo.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.tianju.redisDemo.dao.UserMapper;
import com.tianju.redisDemo.entity.User;
import com.tianju.redisDemo.exception.UsernameIsEmptyException;
import com.tianju.redisDemo.exception.UsernameNotFoundException;
import com.tianju.redisDemo.exception.UsernameOrPasswordErrorException;
import com.tianju.redisDemo.service.IUserService;
import com.tianju.redisDemo.vo.UserVo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;

@Service
@Transactional
@Slf4j
public class UserServiceImpl implements IUserService {

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public List<UserVo> queryAll() {
        List<UserVo> userVolist = new LinkedList<>();
        List<User> userList = userMapper.selectList(null);
        for (User user : userList) {
            userVolist.add(
                    new UserVo(user.getUsername(), user.getRealName(), user.getCreateTime())
            );
        }
        return userVolist;

    }


    /**
     * 登陆的业务
     * @param username
     * @param password
     */
    @Override
    public User login(String username, String password) {
        if (username==null || password==null||
           "".equals(username)||"".equals(password)){
            // 抛出异常,USERNAME_IS_EMPTY_EXCEPTION
            throw new UsernameIsEmptyException("输入为空异常");
        }
        // 判断用户名是否存在
        LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>();
        lambdaQueryWrapper.eq(User::getUsername,username);
        User user = userMapper.selectOne(lambdaQueryWrapper);

        if (Objects.isNull(user)){
            // 用户名不存在
            // 抛出异常:USERNAME_NOT_FOUND_EXCEPTION
            throw new UsernameNotFoundException("用户名不存在异常");
        }

        if (!user.getPassword().equals(password)){
            // 密码不对
            // 抛出异常:USERNAME_OR_PASSWORD_ERROR_EXCEPTION
            throw  new UsernameOrPasswordErrorException("用户名或者密码错误异常");
        }

        log.debug("用户名{}登陆系统成功",username);
        return user;
    }
}

定义异常

在这里插入图片描述

在这里插入图片描述

package com.tianju.redisDemo.exception;

/**
 * 非法用户
 */
public class IllegalUserException extends RuntimeException{
    public IllegalUserException(String message) {
        super(message);
    }
}
package com.tianju.redisDemo.exception;

/**
 * 用户名。或者密码未输入异常
 */
public class UsernameIsEmptyException extends RuntimeException{
    public UsernameIsEmptyException(String message) {
        super(message);
    }
}
package com.tianju.redisDemo.exception;

/**
 * 用户未登录
 */
public class UserNotLoginException extends RuntimeException{
    public UserNotLoginException(String message) {
        super(message);
    }
}

异常的拦截处理

controller层调用service时,会抛出异常,捕获controller层的异常,进行处理,返回响应

package com.tianju.redisDemo.exception.handle;

import com.auth0.jwt.exceptions.TokenExpiredException;
import com.tianju.redisDemo.dto.HttpResp;
import com.tianju.redisDemo.dto.ResultCode;
import com.tianju.redisDemo.exception.*;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.util.Date;

/**
 * 拦截controller层的异常,捕获异常,进行处理;
 * 用户登陆异常
 */
@RestControllerAdvice
public class UserExceptionHandler {

    @ExceptionHandler(UsernameIsEmptyException.class)
    public HttpResp usernameIsEmptyHandler(UsernameIsEmptyException e){
        return new HttpResp(ResultCode.USER_LOGIN_ERROR,new Date(),e.getMessage());
    }
    @ExceptionHandler(UsernameNotFoundException.class)
    public HttpResp usernameNotFoundHandler(UsernameNotFoundException e){
        return new HttpResp(ResultCode.USER_LOGIN_ERROR,new Date(),e.getMessage());
    }
    @ExceptionHandler(UsernameOrPasswordErrorException.class)
    public HttpResp usernameOrPasswordErrorHandler(UsernameOrPasswordErrorException e){
        return new HttpResp(ResultCode.USER_LOGIN_ERROR,new Date(),e.getMessage());
    }

    @ExceptionHandler(UserNotLoginException.class)
    public HttpResp userNotLoginHandler(UserNotLoginException e){
        return new HttpResp(ResultCode.USER_LOGIN_ERROR,new Date(),e.getMessage());
    }
    @ExceptionHandler(IllegalUserException.class)
    public HttpResp illegalUserHandler(IllegalUserException e){
        return new HttpResp(ResultCode.USER_LOGIN_ERROR,new Date(),e.getMessage());
    }

//    @ExceptionHandler(TokenExpiredException.class)
//    public HttpResp tokenExpiredHandler(TokenExpiredException e){
//        return new HttpResp(ResultCode.USER_LOGIN_ERROR,new Date(),e.getMessage());
//    }

    @ExceptionHandler(UserNeedRenewTokenException.class)
    public HttpResp userNeedRenewTokenHandler(UserNeedRenewTokenException e){
        return new HttpResp(ResultCode.USER_LOGIN_ERROR,new Date(),e.getMessage());
    }
}

在这里插入图片描述

controller层

UserController.java文件

package com.tianju.redisDemo.controller;

import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.tianju.redisDemo.dto.HttpResp;
import com.tianju.redisDemo.dto.ResultCode;
import com.tianju.redisDemo.entity.User;
import com.tianju.redisDemo.service.IUserService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import io.swagger.models.auth.In;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

@Api(tags = "用户Api")
@RestController
@RequestMapping("/api/user")
@Slf4j
public class UserController {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Autowired
    private IUserService userService;

    @Value("${lifespanToken}")
    private Integer lifespanToken;

    @Value("${lifespanRedis}")
    private Integer lifespanRedis;

    @ApiOperation(value = "login",notes = "用户登陆api")
    @ApiImplicitParams({
            @ApiImplicitParam(name = "username",required = true),
            @ApiImplicitParam(name = "password",required = true)
    })
    @PostMapping("/login")
    public HttpResp login(String username, String password, HttpServletResponse response,HttpServletRequest request){
        User user = userService.login(username, password);// 这里有可能出现异常
        // 能走到这一步,说明登陆成功,产生token

        // 第一步:产生30分钟有效时间的token
        String securityKey = user.getSecurityKey();
        String token = createToken(username, securityKey);

        // 存入redis里面,用户名和秘钥,以token为键,所以只要token被改动,就会认为非法用户
        stringRedisTemplate.opsForHash().put(token, "username", username);
        stringRedisTemplate.opsForHash().put(token, "securityKey", securityKey);

        // redis过期晚一点,双token机制
        // 第二步:在redis里面存储token,redis里面有效时间为60分钟;
        stringRedisTemplate.expireAt(token, new Date(System.currentTimeMillis()+lifespanRedis));

        // 3.token要给前端
        response.addHeader("bm_token",token);

        // 在session里面存续期的次数
        request.getSession().setAttribute("renewTokenTimes", 0);
        return HttpResp.results(ResultCode.USER_LOGIN_SUCCESS,new Date(),token);

    }

    /**
     * 产生token对象
     * @param username 用户名
     * @param securityKey 安全码
     * @return
     */
    private String createToken(String username,String securityKey){
        Map<String,Object> header = new HashMap<>();
        header.put("alg", "HS256");
        // 链式写法
        return JWT.create()
                .withHeader(header)
                .withClaim("username", username)
//                .withExpiresAt(new Date(System.currentTimeMillis() + 1000 * 3)) // 60*30
                // 第一步:产生30分钟有效时间的token
                .withExpiresAt(new Date(System.currentTimeMillis() + lifespanToken)) // 60*30
                .sign(Algorithm.HMAC256(securityKey.getBytes()));
    }

    @ApiOperation(value = "findAll",notes = "用户登陆api")
    @GetMapping("/findAll")
    public HttpResp findAll(){
        // 使用拦截器处理没有token的情况
        return HttpResp.results(ResultCode.USER_FIND_SUCCESS,new Date(),userService.queryAll());
    }
}

swagger测试

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

在这里插入图片描述

在这里插入图片描述

使用拦截器统一拦截方式

AuthInterceptor.java文件
TokenExpiredException异常

在这里插入图片描述

package com.tianju.redisDemo.interceptor;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.tianju.redisDemo.exception.IllegalUserException;
import com.tianju.redisDemo.exception.UserNeedRenewTokenException;
import com.tianju.redisDemo.exception.UserNotLoginException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

@Slf4j
@Component
public class AuthInterceptor implements HandlerInterceptor {
    
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Value("${lifespanToken}")
    private Integer lifespanToken;

    @Value("${lifespanRedis}")
    private Integer lifespanRedis;

    @Value("${MAX_RENEW_TOKEN_TIME}")
    private Integer MAX_RENEW_TOKEN_TIME;// 最大续期次数

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String token = request.getHeader("bm_token");
        if (token==null || "".equals(token)){
            throw new UserNotLoginException("用户没有登陆");
        }
        // 如何获取token
        String securityKey = (String) stringRedisTemplate.opsForHash().get(token, "securityKey");
        if (securityKey==null){
            throw new IllegalUserException("非法用户");
        }

        // 过期异常,篡改异常

        JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(securityKey)).build();
        try {
            // 如果token过期,会抛出com.auth0.jwt.exceptions.TokenExpiredException
            DecodedJWT decodedJWT = jwtVerifier.verify(token);
        } catch (TokenExpiredException e) { // token过期,redis没有过期
            // 第三步:token过期,但是Redis里面的token还没有过期,此时进行续期
            String username = (String) stringRedisTemplate.opsForHash().get(token, "username");

            // 方案一:产生新的token给前端;
//            String newToken = removeTokenMakeNewToken(token, username, securityKey);
//            response.addHeader("bm_token",token);

            // 方案二:给原有的token续期,需要设置最大续期时间
            Integer renewTokenTimes = (Integer) request.getSession().getAttribute("renewTokenTimes");
            request.getSession().setAttribute("renewTokenTimes",renewTokenTimes+1);

            // 产生一个新的token,更新redis
            log.info( ">>>>>>>>>"+username+"的token过期,即将进行续期,续期次数:"+renewTokenTimes);

            if (renewTokenTimes<MAX_RENEW_TOKEN_TIME){
                System.out.println("我输续期");
                stringRedisTemplate.expireAt(token, new Date(System.currentTimeMillis()+lifespanRedis));
            }
            else {
                System.out.println("不再续期");
                throw new UserNeedRenewTokenException("达到最大续期次数,需要重新登陆");
            }
        }
        return true;
    }

    private String removeTokenMakeNewToken(String token,String username,String securityKey){
        // 1.产生新的token
        Map<String,Object> header = new HashMap<>();
        header.put("alg", "HS256");
        // 链式写法
        String newToken = JWT.create()
                .withHeader(header)
                .withClaim("username", username)
//                    .withExpiresAt(new Date(System.currentTimeMillis() + 1000 * 3)) // 60*30
                .withExpiresAt(new Date(System.currentTimeMillis() + lifespanToken)) // 60*30
                .sign(Algorithm.HMAC256(securityKey.getBytes()));

        // 2.更新redis
        stringRedisTemplate.opsForHash().delete(token,"*"); // 删除之前的token
        stringRedisTemplate.opsForHash().put(newToken, "username", username);
        stringRedisTemplate.opsForHash().put(newToken, "securityKey", securityKey);
        // redis过期晚一点,双token机制
        stringRedisTemplate.expireAt(newToken, new Date(System.currentTimeMillis()+lifespanRedis));

        // 3.新的token还要给前端
        return newToken;
//        response.addHeader("bm_token",token);
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-r1VGKnaD-1690192090095)(D:\javalearn\思维导图笔记\mdPictures\image-20230724173507503.png)]

配置拦截器 @Configuration

package com.tianju.redisDemo.config;

import com.tianju.redisDemo.interceptor.AuthInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import javax.annotation.Resource;

@Configuration
public class BmMvcConfig implements WebMvcConfigurer {

    @Resource
    private AuthInterceptor authInterceptor;
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(authInterceptor)
                .addPathPatterns("/api/**")
                .excludePathPatterns("/api/user/login");
    }
}

Vo对象给前端

只给前端部分数据信息

package com.tianju.redisDemo.vo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.Date;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserVo {
    private String username;
    private String name;
    private Date createTime;
}

DTO对象

日期json问题

@JsonFormat(timezone = “GMT+8”)

HttpResp.java文件

package com.tianju.redisDemo.dto;

import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.*;

import java.io.Serializable;
import java.util.Date;

/**
 * 返回给前端的响应
 * @param <T>
 */
@ApiModel("DTO返回数据")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class HttpResp<T> implements Serializable {
    private ResultCode resultCode;
    @ApiModelProperty("time")
    @JsonFormat(pattern = "yyyy-MM-dd hh:mm:ss",timezone = "GMT+8")
    private Date time;
    @ApiModelProperty("results")
    private T result;

    public static <T> HttpResp <T> results(
            ResultCode resultCode,
            Date time,
            T results){

        HttpResp httpResp = new HttpResp();
        httpResp.setResultCode(resultCode);
        httpResp.setTime(time);
        httpResp.setResult(results);
        return httpResp;
    }
}

枚举类型的json化

@JsonFormat(shape = JsonFormat.Shape.OBJECT)

@Getter

ResultCode.java枚举类

package com.tianju.redisDemo.dto;

import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModelProperty;
import lombok.Getter;


/**
 * 枚举类型,http请求的返回值
 */
// 枚举类型的json化,需要有get方法
@JsonFormat(shape = JsonFormat.Shape.OBJECT)
@Getter
public enum ResultCode {
    BOOK_RUSH_SUCCESS(20010,"图书抢购成功"),
    BOOK_RUSH_ERROR(3001,"图书抢购失败"),
    LUA_SCRIPT_ERROR(3002,"Lua脚本操作失败"),
    USER_FIND_ERROR(40010,"非法请求,布隆过滤器不通过"),
    USER_FIND_SUCCESS(20010,"查询用户名成功"),
    USER_LOGIN_ERROR(40030,"用户登陆失败"),
    USER_LOGIN_SUCCESS(20020,"用户登陆成功"),
    ;

    @ApiModelProperty("状态码")
    private Integer code;

    @ApiModelProperty("提示信息")
    private String msg;

    private ResultCode(Integer code,String msg){
        this.code =code;
        this.msg = msg;
    }
}

application.yml配置文件

server:
  port: 9099

# token的过期时间是30分钟;redis的过期时间是90分钟
lifespanToken: 6000 # 1000*60*30
lifespanRedis: 20000 # 1000*60*90
MAX_RENEW_TOKEN_TIME: 1 # 最大续期次数


spring:
  # redis的相关配置
  redis:
    host: localhost
    port: 6379
    database: 0

  # mysql的相关配置
  datasource:
    druid:
      url: jdbc:mysql://127.0.0.1/community?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
      username: root
      password: 123
      driver-class-name: com.mysql.cj.jdbc.Driver

# 日志需要配置一下
logging:
  level:
    com.tianju.redisDemo: debug

# mybatis-plus的日志
mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

# knife需要允许
knife4j:
  enable: true

pom文件

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.tianju.jwt</groupId>
    <artifactId>jwt_demo</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <!--    起步依赖-->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.6.13</version>
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!--        redis的包-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

        <!--        图片验证码-->
        <dependency>
            <groupId>com.oopsguy.kaptcha</groupId>
            <artifactId>kaptcha-spring-boot-starter</artifactId>
            <version>1.0.0-beta-2</version>
        </dependency>


        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.11</version>
        </dependency>

<!--        mysql相关的包-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.2.18</version>
        </dependency>

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.3.1</version>
        </dependency>


        <!--        jwt的依赖-->
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>4.3.0</version>
        </dependency>

        <dependency>
            <groupId>com.github.xiaoymin</groupId>
            <artifactId>knife4j-openapi2-spring-boot-starter</artifactId>
            <version>4.0.0</version>
        </dependency>

    </dependencies>

</project>

总结

1.maven打包springboot项目,jar包;
2.windows安装java环境,以及运行jar包;
3.Linux安装java环境,以及运行jar包;
4.运行jar包template might not exist报错及解决;

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

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

相关文章

MySQL Workbench的使用

MySQL Workbench 是一款专门为 MySQL 设计的可视化数据库管理软件&#xff0c;我们可以在自己的计算机上&#xff0c;使用图形化界面远程管理 MySQL 数据库。 MySQL Workbench 的初始界面如下图所示。 点击方框后会进入这个界面&#xff0c;这样就与数据库连接完毕了 使用 Wo…

Docker 全栈体系(四)

Docker 体系&#xff08;高级篇&#xff09; 一、Docker复杂安装 1. 安装mysql主从复制 主从搭建步骤 新建主服务器容器实例3307 docker run -p 3307:3306 --name mysql-master \ -v /mydata/mysql-master/log:/var/log/mysql \ -v /mydata/mysql-master/data:/var/lib/mysq…

SpringBoot-4

Spring Boot 使用 slf4j 日志 在开发中经常使用 System.out.println()来打印一些信息&#xff0c;但是这样不好&#xff0c;因为大量的使用 System.out 会增加资源的消耗。实际项目中使用的是 slf4j 的 logback 来输出日志&#xff0c;效率挺高的&#xff0c;Spring Boot 提供…

【go语言学习笔记】02 Go语言高效并发

文章目录 一、并发基础1. 协程&#xff08;Goroutine&#xff09;2. Channel2.1 声明2.2 无缓冲 channel2.3 有缓冲 channel2.4 关闭 channel2.5 单向 channel2.6 selectchannel 示例 二、同步原语1. 资源竞争2. 同步原语2.1 sync.Mutex2.2 sync.RWMutex2.3 sync.WaitGroup2.4 …

2023 NVIDIA 创乐博 CUDA 线上训练营笔记

一、学习ubuntu 2.1修改权限 linux指令学习 cd course //进入course目录 ls //列出文件夹 clean //清屏幕//---修改权限 chmod gow text //给text文件夹添加可写权限 chmod gw make.ip chmod 755 text 可读可写可执行&#xff08;user goup o…

MySQL MHA高可用配置及故障切换

1&#xff0e;什么是 MHA MHA&#xff08;Master High Availability&#xff09;是一套优秀的MySQL高可用环境下故障切换和主从复制的软件。 MHA 的出现就是解决MySQL 单点的问题。 MySQL故障切换过程中&#xff0c;MHA能做到0-30秒内自动完成故障切换操作。 MHA能在故障切换的…

uni-app中uni-table的uni-tr无点击事件

uni-app中uni-table的uni-tr无点击事件 问题描述解决方法一解决方法二解决方法三 问题描述 本文记录用于记录使用uni-app开发过程遇到的bug。 在使用uni-table时&#xff0c;想给uni-table的行添加行点击事件&#xff1b;但发现官方并未给uni-tr增加点击行点击事件&#xff08;…

gitee 配置ssh 公钥(私钥)

步骤1&#xff1a;添加/生成SSH公钥&#xff0c;码云提供了基于SSH协议的Git服务&#xff0c;在使用SSH协议访问项目仓库之前&#xff0c;需要先配置好账户/项目的SSH公钥。 绑定账户邮箱&#xff1a; git config --global user.name "Your Name" git config --glob…

Linux静态库+demo

1.什么是Linux静态库呢&#xff1f; Linux静态库&#xff08;Static Library&#xff09;是一种包含已编译的目标代码的文件集合&#xff0c;用于在链接时与其他目标代码一起组成可执行文件。与动态库&#xff08;Dynamic Library&#xff09;不同&#xff0c;静态库的代码在编…

集成学习Boosting - AdaBoost

目录 1. Boosting方法的基本思想 1.1 Bagging VS Boosting 1.2 Boosting算法的基本元素与基本流程 1.3 sklearn中的Boosting算法 2. AdaBoost 3 AdaBoost的基本参数与损失函数 3.1 参数 base_estimator&#xff0c;属性base_estimator_与estimators_ 3.1. 参数 learnin…

构建高效供应商管理体系,提升企业采购能力

随着企业采购规模的不断扩大和全球化竞争的加剧&#xff0c;供应商管理变得越来越重要。构建一个高效的供应商管理体系是企业提升采购能力、降低采购成本的关键一环。本文将重点探讨供应商管理体系的意义和作用&#xff0c;并介绍如何构建一个高效的供应商管理体系。 一、供应商…

Openlayers实战,Openlayers一个车辆转向运动轨迹动画,支持根据轨迹运动方向自动改变车头转向角度,无需定时器,丝滑小车转向运动效果

专栏目录: OpenLayers入门教程汇总目录 前言 本章作为OpenLayers入门文章《OpenLayers入门,OpenLayers动画效果实现,OpenLayers实现轨迹运动动画》的的增强进阶篇章,实现了入门教程中没有实现的小车车头方向根据运动方向自动转向的功能。 二、依赖和使用 "ol&quo…

四、运算符(1)

本章概要 开始使用优先级赋值 方法调用中的别名现象 算术运算符 一元加减运算符 递增和递减 Java 是从 C 的基础上做了一些改进和简化发展而成的。对于 C/C 程序员来说&#xff0c;Java 的运算符并不陌生。如果你已了解 C 或 C&#xff0c;大可以跳过本章和下一章&#xff0c…

微信小程序开店的步骤

越来越多实体店家在寻求入局电商的机会&#xff0c;随着微信推出了小程序&#xff0c;商家们看到了机会&#xff0c;微信小程序开发不仅成本较低&#xff0c;还能借助微信进行推广&#xff0c;帮助商家实现线上线下的对接&#xff0c;是商家实现开店的极佳选择。那么微信小程序…

Linux 网络收包流程

哈喽大家好&#xff0c;我是咸鱼 我们在跟别人网上聊天的时候&#xff0c;有没有想过你发送的信息是怎么传到对方的电脑上的 又或者我们在上网冲浪的时候&#xff0c;有没有想过 HTML 页面是怎么显示在我们的电脑屏幕上的 无论是我们跟别人聊天还是上网冲浪&#xff0c;其实…

伙伴云「页面」公测正式开启,建站、文档、数据分析,丰富的数据展现能力。

一年来&#xff0c;伙伴云收到了许多用户对于仪表盘升级的期待&#xff0c;以及对新功能规划的询问。经过千百次调研、开发、内测&#xff0c;我们开发了一款页面构建引擎&#xff0c;Ta既提供了灵活的可定制性&#xff0c;也不像网页设计器晦涩难懂&#xff0c;且具有丰富的数…

Apache Pulsar 技术系列 - GEO replication 中订阅状态的同步原理

导语 Apache Pulsar 是一个多租户、高性能的服务间消息传输解决方案&#xff0c;支持多租户、低延时、读写分离、跨地域复制&#xff08;GEO Replication&#xff09;、快速扩容、灵活容错等特性&#xff0c;GEO Replication 可以原生支持数据和订阅状态在多个集群之间进行复制…

Docker -v 挂载主机目录到容器中(及数据卷容器)

一、简单挂载 使用 Docker 的过程中&#xff0c;经常需要挂载主机上的目录或文件到 Docker 容器中&#xff0c;以实现数据的共享或持久化。而 docker run -v 命令便是用于挂载主机目录到 Docker 容器中的常用命令。 在创建启动容器时&#xff0c;使用 -v 参数设置数据卷 # 挂载…

【测试笔记】示波器的使用示例(多图形说明,少文字描述)

示波器的使用示例 一、示波器图样二、总体介绍三、分布介绍3.1 分段线设置3.2 单通道波形调整3.3 整体波形图像调整3.4 滚动模式3.5 测量显示选择3.6 储存选择3.7 触发捕获模式选择 前言&#xff1a;本篇文章是我使用示波器后的一个功能记录&#xff0c;多为常用功能记录&#…