SpringBoot整合JWT示例教程

news2024/11/15 10:40:50

1. JWT简介

JSON Web Token (JWT) 是一种开放标准(RFC 7519),它定义了一种紧凑且自包含的方式,用于在各方之间作为 JSON 对象安全地传输信息。由于这些信息是经过数字签名的,因此可以被验证和信任。JWT 通常用于身份验证和信息交换场景,特别是在 Web 应用程序的认证和授权机制中。

组成
JWT 由三部分组成:Header、Payload 和 Signature。这三部分分别用点(.)分隔,形成一个字符串。

Header(头部):
Header 通常由两部分组成:令牌的类型(JWT)和所使用的签名算法(例如,HMAC SHA256 或 RSA)。

{
  "alg": "HS256",
  "typ": "JWT"
}

这个 JSON 对象被 Base64Url 编码后,形成 JWT 的第一部分。
Payload(负载):

  • Payload 包含声明(claims),声明是关于实体(通常是用户)和其他数据的声明。声明有三种类型:
  • Registered claims(注册声明):预定义的声明,如 iss(发行者)、exp(过期时间)、sub(主题)、aud(受众)。
  • Public claims(公共声明):可以自由定义的声明,但为了避免冲突,建议在 IANA JSON Web Token Claims 注册表中注册或使用 URI 作为声明名称的前缀。
  • Private claims(私有声明):自定义的声明,用于共享信息,比如用户角色、权限等。
    例:
{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}

这个 JSON 对象也被 Base64Url 编码后,形成 JWT 的第二部分。
Base64编码方式是可逆的,也就是透过编码后发放的Token内容是可以被解析的。一般而言不建议在Payload放敏感讯息,比如使用者的密码。
Signature(签名):
签名部分用于验证消息在传输过程中未被篡改。
首先,需要指定一个密钥,然后使用指定的签名算法对编码后的 Header 和 Payload 以及一个密钥进行签名。签名的过程实际上是对头部以及负载内容进行签名,防止内容被窜改。如果有人对头部以及负载的内容解码之后进行修改,再进行编码,最后加上之前的签名组合形成新的JWT的话,那么服务器端会判断出新的头部和负载形成的签名和JWT附带上的签名是不一样的。如果要对新的头部和负载进行签名,在不知道服务器加密时用的密钥的话,得出来的签名也是不一样的。

例子:

HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  secret
)

生成的签名也被 Base64Url 编码,形成 JWT 的第三部分。

基于JWT的认证流程
在这里插入图片描述

  • 前端通过Web表单将自己的用户名和密码发送到后端的接口。该过程一般是HTTP的POST请求。建议的方式是通过SSL加密的传输(https协议),从而避免敏感信息被嗅探;
  • 后端核对用户名和密码成功后,将用户的id等其他信息作为JWT Payload(负载),将其与头部分别进行Base64编码拼接后签名,形成一个JWT(Token);
  • 后端将JWT字符串作为登录成功的返回结果返回给前端。前端可以将返回的结果保存在localStorage(浏览器本地缓存)或cookie上,退出登录时前端删除保存的JWT即可;
  • 前端在每次请求时将JWT放入HTTP的Header中的Authorization位。(解决XSS和XSRF问题)HEADER
  • 后端检查是否存在,如存在验证JWT的有效性。例如,检查签名是否正确﹔检查Token是否过期等;
  • 验证通过后后端使用JWT中包含的用户信息进行其他逻辑操作,返回相应结果。

注:Base64编码方式是可逆的,也就是透过编码后发放的Token内容是可以被解析的。一般而言,不建议在有效载荷内放敏感讯息,比如使用者的密码。

2. 准备工作

环境:JDK8 + SpringBoot2.6.13

2.1 生成秘钥对

需要使用到jdk的keytool工具,在jdk安装目录的bin目录内,在cmd控制窗口执行JDK中keytool的命令:

keytool -genkeypair -alias test -keyalg RSA -keysize 2048 -validity 365 -keystore test.jks -storepass test123 -keypass test123 -dname "CN=Sakura, OU=xxb, O=ncu, L=nc, ST=JX, C=CN"

参数解释

  • genkeypair: 生成一个密钥对(包括公钥和私钥)。
  • alias test: 为生成的密钥对指定一个别名 test。别名是用来识别密钥条目的。
  • keyalg RSA: 指定密钥对的算法为 RSA。RSA 是一种常用的公钥加密算法。
  • keysize 2048: 指定密钥的大小为 2048 位。密钥越长,安全性越高,但性能开销也越大。
  • validity 365: 指定证书的有效期为 365 天。
  • keystore test.jks: 指定密钥库文件的名称为 test.jks。如果文件不存在,keytool 会创建一个新的文件。
  • storepass test123: 指定密钥库的密码为 test123。这是保护整个密钥库的密码。
  • keypass test123: 指定密钥的密码为 test123。这是保护单个密钥条目的密码。
  • dname “CN=Sakura, OU=xxb, O=ncu, L=nc, ST=JX, C=CN”: 指定证书的详细信息,依次为名字与姓氏,组织单位,城市,区县,国家代码,使用逗号分隔的格式。

执行完命令后,会警告:

JKS 密钥库使用专用格式。建议使用 keytool -importkeystore -srckeystore test.jks -destkeystore test.jks -deststoretype pkcs12迁移到行业标准格式 PKCS12。
执行下上述命令即可:

keytool -importkeystore -srckeystore test.jks -destkeystore test.jks -deststoretype pkcs12

最后,将生成的test.jks文件放到springboot的resources目录(即类路径下)。

2.2 SpringBoo项目配置

项目目录如下:
在这里插入图片描述
maven 依赖:

server:
  port: 9000  # 服务端口

# 自定义JWT配置
<dependencies>
        <!-- web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- 注解执行器 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
        <!-- validation -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <!-- lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!-- hutool -->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.11</version>
        </dependency>
        <!--加密-->
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-crypto</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-rsa</artifactId>
            <version>1.0.9.RELEASE</version>
        </dependency>
    </dependencies>

application.yml配置文件:

server:
  port: 9000  # 服务端口

# 自定义JWT配置
app:
  jwt:
    location: classpath:test.jks  # JWT密钥存放位置,classpath为resource文件夹
    alias: test  # 别名
    password: test123  # 密码
    tokenTTL: 30m  # Token有效期为30min

  auth:
    excludePaths: # 排除的路径,不需要认证的路径
      - /auth/login

JwtApplication .java

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@SpringBootApplication
@EnableConfigurationProperties
@ConfigurationPropertiesScan("com.jwt.demo.config")
public class JwtApplication {

    public static void main(String[] args) {
        SpringApplication.run(JwtApplication.class, args);
    }

}


AuthProperties.java

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;

import java.util.List;

@Data
@ConfigurationProperties(prefix = "app.auth")
public class AuthProperties {
    /***
     * 指定需要拦截的请求路径
     */
    private List<String> includePaths;

    /**
     * 指定需要放行的请求路径
     */
    private List<String> excludePaths;
}

JwtProperties .java

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.core.io.Resource;

import java.time.Duration;

@Data
@ConfigurationProperties(prefix = "app.jwt")
public class JwtProperties {
    private Resource location;
    private String password;
    private String alias;
    private Duration tokenTTL = Duration.ofMinutes(10);

}


SecurityConfig .java

import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.rsa.crypto.KeyStoreKeyFactory;

import java.security.KeyPair;

@Configuration
@EnableConfigurationProperties(JwtProperties.class)
public class SecurityConfig {

    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    /**
     * 根据配置文件读取jks文件的密钥对
     */
    @Bean
    public KeyPair keyPair(JwtProperties properties){
        // 获取秘钥工厂
        KeyStoreKeyFactory keyStoreKeyFactory =
                new KeyStoreKeyFactory(
                        properties.getLocation(),
                        properties.getPassword().toCharArray());
        //读取钥匙对
        return keyStoreKeyFactory.getKeyPair(
                properties.getAlias(),
                properties.getPassword().toCharArray());
    }
}

JwtTool .java

import cn.hutool.core.exceptions.ValidateException;
import cn.hutool.jwt.JWT;
import cn.hutool.jwt.JWTValidator;
import cn.hutool.jwt.signers.JWTSigner;
import cn.hutool.jwt.signers.JWTSignerUtil;
import com.jwt.demo.constants.UserConstants;
import com.jwt.demo.exception.UnauthorizedException;
import org.springframework.stereotype.Component;

import java.security.KeyPair;
import java.time.Duration;
import java.util.Date;

@Component
public class JwtTool {
    private final JWTSigner jwtSigner;

    public JwtTool(KeyPair keyPair) {
        this.jwtSigner = JWTSignerUtil.createSigner(UserConstants.ALGORITHM, keyPair);
    }

    /**
     * 创建 access-token
     *
     * @param userId 用户id
     * @param ttl    有效时间
     * @return access-token
     */
    public String createToken(Long userId, Duration ttl) {
        // 1.生成jws
        return JWT.create()
                .setPayload(UserConstants.PAY_LOAD, userId) // 设置载荷
                .setExpiresAt(new Date(System.currentTimeMillis() + ttl.toMillis()))    // 设置过期时间
                .setSigner(jwtSigner)
                .sign();
    }

    /**
     * 解析token
     *
     * @param token token
     * @return 解析刷新token得到的用户信息
     */
    public Long parseToken(String token) {
        // 1.校验token是否为空
        if (token == null) {
            throw new UnauthorizedException("未登录");
        }
        // 2.校验并解析jwt
        JWT jwt;
        try {
            jwt = JWT.of(token).setSigner(jwtSigner);
        } catch (Exception e) {
            throw new UnauthorizedException("无效的token", e);
        }
        // 2.校验jwt是否有效
        if (!jwt.verify()) {
            // 验证失败
            throw new UnauthorizedException("无效的token");
        }
        // 3.校验是否过期
        try {
            JWTValidator.of(jwt).validateDate();
        } catch (ValidateException e) {
            throw new UnauthorizedException("token已经过期");
        }
        // 4.数据格式校验
        Object userPayload = jwt.getPayload(UserConstants.PAY_LOAD);
        if (userPayload == null) {
            // 数据为空
            throw new UnauthorizedException("无效的token");
        }

        // 5.数据解析
        try {
            Long userId = Long.valueOf(userPayload.toString());

            return userId;
        } catch (RuntimeException e) {
            // 数据格式有误
            throw new UnauthorizedException("无效的token");
        }
    }
}

UserConstants .java

/**
 * 常量类
 *
 * @date 2024-07-12 11:27
 */
public interface UserConstants {
    /**
     * JWT 载荷字段
     */
    String PAY_LOAD = "user";

    /**
     * 加密算法RSA256
     */
    String ALGORITHM = "rs256";

    /**
     * token对应的请求头字段名称
     */
    String AUTHORAZATION = "authorization";
}

3. 登录拦截器

编写登录拦截器逻辑,并注入到Spring的拦截器链。
AuthInterceptor .java

import cn.hutool.core.text.AntPathMatcher;
import cn.hutool.core.util.StrUtil;
import com.jwt.demo.config.AuthProperties;
import com.jwt.demo.constants.UserConstants;
import com.jwt.demo.exception.UnauthorizedException;
import com.jwt.demo.utils.JwtTool;
import com.jwt.demo.utils.UserContext;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Component
@RequiredArgsConstructor
public class AuthInterceptor implements HandlerInterceptor {

    // 采用构造器注入的方式注入配置类
    private final AuthProperties authProperties;

    private final JwtTool jwtTool;

    private final AntPathMatcher antPathMatcher = new AntPathMatcher();


    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        // 判断是否需要进行登录拦截
        if (isExcludedPath(request.getRequestURI())) {
            // 若不需要登录拦截,则放行
            return true;
        }
        // 若需要登录拦截,则获取token
        String header = String.valueOf(request.getHeader(UserConstants.AUTHORAZATION));
        String token = null;
        // 判断token是否存在
        if (StrUtil.isNotBlank(header)) {
            token = header;
        }
        // 校验并解析token
        Long userId = null;
        try {
            userId = jwtTool.parseToken(token);
        } catch (UnauthorizedException e) {
            // 拦截该请求
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write("{\"error\": \"Unauthorized\", \"message\": \"" + e.getMessage() + "\"}");
            return false; // 返回false以阻止请求的进一步处理

        }
        // 传递用户信息,放置在ThreadLocal中
        UserContext.setUser(userId);
        // 放行该请求
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 清空ThreadLocal
        UserContext.removeUser();
    }

    /**
     * 判断是否需要进行登录拦截
     *
     * @param path 当前请求的路径
     * @return 是否是不需要登录拦截的路径
     */
    private boolean isExcludedPath(String path) {
        // 判断是否是不需要登录拦截的路径
        for (String excludePath : authProperties.getExcludePaths()) {
            // 选择antPathMatcher实现路径匹配
            if (antPathMatcher.match(excludePath, path)) {
                return true;
            }
        }
        return false;
    }
}

MvcConfig .java

import com.jwt.demo.interceptor.AuthInterceptor;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * MVC配置
 *
 * @author: hong.jian
 * @date 2024-03-02 20:02
 */
@Configuration
@ConditionalOnClass(DispatcherServlet.class)
@RequiredArgsConstructor
public class MvcConfig implements WebMvcConfigurer {
    private final AuthInterceptor authInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 将自定义的拦截器进行注册
        registry.addInterceptor(authInterceptor);
    }
}

编写控制器AuthController
AuthController.java

import com.jwt.demo.config.JwtProperties;
import com.jwt.demo.domain.dto.LoginFormDTO;
import com.jwt.demo.domain.po.User;
import com.jwt.demo.domain.vo.UserLoginVO;
import com.jwt.demo.utils.JwtTool;
import com.jwt.demo.utils.UserContext;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
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;

/**
 * @author: hong.jian
 * @date 2024-07-12 10:36
 */
@RestController
@RequiredArgsConstructor
@Slf4j
@RequestMapping("/auth")
public class AuthController {

    private final JwtTool jwtTool;
    private final JwtProperties jwtProperties;

    /**
     * 用户登录后生成token
     *
     * @param loginDTO 登录表单
     * @return 含token的用户信息
     */
    @PostMapping("/login")
    public UserLoginVO login( LoginFormDTO loginDTO) {
        // 1.获取表单信息(省略)
        String username = loginDTO.getUsername();
        String password = loginDTO.getPassword();
        // 2. 登录逻辑校验(需要对接DB,这里使用静态数据模拟)
        User user = User.builder()
                .id(111L)
                .username("Sakura")
                .build();
        // 3.生成TOKEN
        String token = jwtTool.createToken(user.getId(), jwtProperties.getTokenTTL());
        // 4.封装VO返回
        UserLoginVO vo = UserLoginVO.builder().userId(user.getId()) // 用户id
                .username(user.getUsername())   // 用户名
                .token(token)   // token
                .build();
        log.info("UserLoginVO:{}", vo);
        return vo;
    }

    /**
     * 用户登录后生成token
     * 测试接口
     */
    @GetMapping("/test")
    public void test() {
        // 直接从ThreadLocal获取用户信息
        log.info("userId:{}", UserContext.getUser());
    }

}


4. 测试

控制台日志:

2024-07-12 21:29:51.329  INFO 25212 --- [  restartedMain] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 9000 (http) with context path ''
2024-07-12 21:29:51.335  INFO 25212 --- [  restartedMain] com.jwt.demo.JwtApplication              : Started JwtApplication in 2.352 seconds (JVM running for 3.21)
2024-07-12 21:29:51.710  INFO 25212 --- [nio-9000-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring DispatcherServlet 'dispatcherServlet'
2024-07-12 21:29:51.710  INFO 25212 --- [nio-9000-exec-1] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
2024-07-12 21:29:51.711  INFO 25212 --- [nio-9000-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 1 ms
2024-07-12 21:29:58.489  INFO 25212 --- [nio-9000-exec-9] com.jwt.demo.controller.AuthController   : UserLoginVO:UserLoginVO(token=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJ1c2VyIjoxMTEsImV4cCI6MTcyMDc5Mjc5OH0.KjmVdTh1RYUOZ_okycZJoj86qkfqlRuSPrwmjMNYS2uS0IwzM1Ab2D4m53F6z4x2zZxEt4aReC-Rnb_HpAx1uj0-unxAlsbe5mW9ok1GhtWp7EuW0k1rgQRA0nx6DUPwUmxhOXIyM9tdJsN0Sae5KQ5mimKORtB6n-VhIDo-cKqdTvtwKUVSbSiCHoQRryUBI2333TjdwkrYg2o-Fdwt80LkHxWOwoGelqmThDlvIvY-Nfkb0-EFIq1IlA027QBN3-TJdohy_3ATWWXOS1h4zuNzTzeN_ML4BZI-SWa2EajQl1eBpgYWZttWTcduV2WGDhsH-zsafC2IvW9tpz6b3A, userId=111, username=Sakura)
2024-07-12 21:30:49.347  INFO 25212 --- [io-9000-exec-10] com.jwt.demo.controller.AuthController   : userId:111

在这里插入图片描述
登录后请求需要带上token
在这里插入图片描述

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

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

相关文章

深度学习论文: YOLOv5, YOLOv8 and YOLOv10: The Go-To Detectors for Real-time Vision

深度学习论文: YOLOv5, YOLOv8 and YOLOv10: The Go-To Detectors for Real-time Vision YOLOv5, YOLOv8 and YOLOv10: The Go-To Detectors for Real-time Vision PDF:https://arxiv.org/pdf/2407.02988v1 PyTorch: https://github.com/shanglianlm0525/PyTorch-Networks 1 概…

51单片机-第三节-LCD1602调试工具,矩阵键盘

一、LCD调试工具函数&#xff1a; 使用&#xff1a; 所有函数&#xff0c;前两个参数&#xff0c;均为指定显示位置。 四个参数的&#xff0c;第四个参数&#xff0c;为保留位数&#xff0c;少的保留后面&#xff08;123,2 -> 23&#xff09;&#xff0c;多的前面补零。 …

文章解读与仿真程序复现思路——电网技术EI\CSCD\北大核心《天气数据驱动下基于深度主动学习的新型电力系统供需失衡风险快速评估方法 》

本专栏栏目提供文章与程序复现思路&#xff0c;具体已有的论文与论文源程序可翻阅本博主免费的专栏栏目《论文与完整程序》 论文与完整源程序_电网论文源程序的博客-CSDN博客https://blog.csdn.net/liang674027206/category_12531414.html 电网论文源程序-CSDN博客电网论文源…

Apache-Flink未授权访问高危漏洞修复

漏洞等级 高危漏洞!!! 一、漏洞描述 攻击者没有获取到登录权限或未授权的情况下,或者不需要输入密码,即可通过直接输入网站控制台主页面地址,或者不允许查看的链接便可进行访问,同时进行操作。 二、修复建议 根据业务/系统具体情况,结合如下建议做出具体选择: 配…

产品经理-研发流程-敏捷开发-迭代-需求评审及产品规划(15)

敏捷开发是以用户的需求进化为核心&#xff0c;采用迭代、循序渐进的方法进行软件开发。 通俗来说&#xff0c;敏捷开发是一个软件开发流程&#xff0c;是一个采用了迭代方法的开发流程 简单来说&#xff0c;迭代就是把一个大产品拆分出一些最小的实现单位。完成不同的迭代就最…

交易平台Zero Hash现已支持SUI交易

Zero Hash是一家领先的加密货币和稳定币基础设施平台&#xff0c;为包括Stripe、Shift4和Franklin Templeton在内的公司提供支持&#xff0c;现在也支持对SUI的访问。此举使Zero Hash的客户及其终端用户能够使用SUI。 提供API和SDK以及专注于无缝连接法币、加密货币和稳定币的…

Python | Leetcode Python题解之第231题2的幂

题目&#xff1a; 题解&#xff1a; class Solution:BIG 2**30def isPowerOfTwo(self, n: int) -> bool:return n > 0 and Solution.BIG % n 0

【Redis】哨兵(sentinel)

文章目录 一、哨兵是什么&#xff1f;二、 哨兵sentinel文件参数三、 模仿主机redis宕机四、哨兵运行流程和选举原理SDOWN主观下线ODOWN客观下线 五、 使用建议 以下是本篇文章正文内容 一、哨兵是什么&#xff1f; 哨兵巡查监控后台master主机是否故障&#xff0c;如果故障了…

PostgreSQL 如何应对因大量并发删除操作导致的性能问题?

&#x1f345;关注博主&#x1f397;️ 带你畅游技术世界&#xff0c;不错过每一次成长机会&#xff01;&#x1f4da;领书&#xff1a;PostgreSQL 入门到精通.pdf 文章目录 PostgreSQL 如何应对因大量并发删除操作导致的性能问题一、优化索引二、批量删除三、分区表四、调整参…

kotlin数据类型

人不走空 &#x1f308;个人主页&#xff1a;人不走空 &#x1f496;系列专栏&#xff1a;算法专题 ⏰诗词歌赋&#xff1a;斯是陋室&#xff0c;惟吾德馨 Kotlin基本数值类型 基本数据类型包括 Byte、Short、Int、Long、Float、Double 整数类型 类型位宽最小值最大…

GloVe: Global Vectors for Word Representation论文笔记解读

基本信息 作者Jeffrey Penningtondoi10.3115/v1/D14-1162发表时间2014期刊EMNLP网址https://aclanthology.org/D14-1162.pdf 研究背景 1. What’s known 既往研究已证实 全局矩阵分解方法&#xff1a;LSA&#xff0c;考虑整个语料库词频的统计信息得到共现矩阵&#xff0c;通…

访问 Postman OAuth 2.0 授权的最佳实践

OAuth 2.0 代表了 web 安全协议的发展&#xff0c;便于在多个平台上进行授权服务&#xff0c;同时避免暴露用户凭据。它提供了一种安全的方式&#xff0c;让用户可以授权应用程序访问服务。 在 Postman 中开始使用 OAuth 2.0 Postman 是一个流行的API客户端&#xff0c;支持 …

FlinkModule加载HiveModule异常

HiveModule这个模块加载不出来 加在不出来这个模块&#xff0c;网上查说是要加下面这个依赖 <dependency><groupId>org.apache.flink</groupId><artifactId>flink-connector-hive_${scala.binary.version}</artifactId><version>${flink.…

.Net Core 微服务之Consul(二)-集群搭建

引言: 集合上一期.Net Core 微服务之Consul(一)(.Net Core 微服务之Consul(一)-CSDN博客) 。 目录 一、 Consul集群搭建 1. 高可用 1.1 高可用性概念 1.2 高可用集群的基本原理 1.3 高可用集群的架构设计 1.3.1 主从复制架构 1.3.2 共享存储架构 1.3.3 负载均衡…

三维空间中的旋转方向(blender坐标系、python中两种旋转方式、VTK坐标系)

在三维空间中&#xff0c;旋转的方向 通常遵循右手定则&#xff08;右手螺旋法则&#xff09;&#xff1a; 右手握住旋转轴&#xff0c;拇指指向轴的正方向。 其余手指弯曲的方向就是正旋转方向&#xff08;被视为逆时针旋转&#xff09;。 当旋转角度为正值&#xff08;如90度…

基于pytesseract的OCR图片识别

简介 pytesseract是基于谷歌的tesseract的OCR包&#xff0c;支持识别一些简单的数字、字母、中文。 安装 安装引擎 下载地址&#xff1a;https://digi.bib.uni-mannheim.de/tesseract/ 一般是Windows 64位系统最新版&#xff1a; 如果要识别中文&#xff0c;注意选中中文…

数据结构(4.2)——朴素模式匹配算法

字符串模式匹配 在主串中找到模式串相同的子串&#xff0c;并返回其所在的位置。 子串和模式串的区别 子串&#xff1a;主串的一部分&#xff0c;一定存在 模式串&#xff1a;不一定能在主串中找到 字符串模式匹配 朴素模式匹配算法 主串长度为n&#xff0c;模式串长度为…

嵌入式C++、Qt/QML和MQTT:智能工厂设备监控系统的全流程介绍(附代码示例)

1. 项目概述 本项目旨在开发一套先进的智能工厂设备监控系统&#xff0c;集成嵌入式技术、工业通信协议和人机界面等多项技术&#xff0c;实现对工厂设备的全方位实时监控、高精度数据采集和智能化分析。该系统将显著提升工厂设备的运行效率&#xff0c;大幅降低维护成本&…

使用xacro作出摄像头和雷达

机器人模型由多个部件组成&#xff0c;可以将不同组建设置进单独文件&#xff0c;最终通过文件包含实现组建的拼装。 一、编写摄像头和雷达的xacro文件 二、组合文件 编写一个组合文件&#xff0c;组合底盘、摄像头和雷达 三、启动 搭建框架&#xff0c;创建三个文件 摄像…

Excel第31享:基于left函数的截取式数据裂变

1、需求描述 如下图所示&#xff0c;在“Excel第30享”中统计2022年YTD各个人员的“上班工时&#xff08;a2&#xff09;”&#xff0c;需要基于工时明细表里的“日期”字段建立辅助列&#xff0c;生成“年份”字段&#xff0c;本文说明“年份”字段是怎么裂变而来的。 下图为…