SpringBoot 3.2.0 基于Spring Security+JWT实现动态鉴权

依赖版本

  • JDK 17
  • Spring Boot 3.2.0
  • Spring Security 6.2.0

工程源码:Gitee

为了能够不需要额外配置就能启动项目,看到配置效果。用例采用模拟数据,可自行修改为对应的ORM操作

编写Spring Security基础配置

导入依赖

<properties><java-jwt.version>4.4.0</java-jwt.version><guava.version>33.0.0-jre</guava.version>
</properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency><dependency><groupId>com.auth0</groupId><artifactId>java-jwt</artifactId><version>${java-jwt.version}</version></dependency><dependency><groupId>com.google.guava</groupId><artifactId>guava</artifactId><version>${guava.version}</version></dependency><dependency><groupId>org.apache.commons</groupId><artifactId>commons-lang3</artifactId></dependency>
</dependencies>

测试Spring Security

默认配置下,Spring Security form表单登录的用户名为user,密码启动时在控制台输出。

编写测试Controller

package com.yiyan.study.controller;import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;/*** 测试接口*/
@RestController
public class SecurityController {@GetMapping("/hello")public String hello() {return "hello spring security";}
}

访问接口测试
springboot3-security-default-test

编写Spring Security基础文件

创建Spring Security模拟数据

package com.yiyan.study.config;import lombok.Getter;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;/*** Spring Security 模拟数据*/
public class SecurityConstant {/*** 模拟用户数据。key:用户名,value:密码*/public static final Map<String, String> USER_MAP = new ConcurrentHashMap<>();/*** 模拟权限数据。key:接口地址,value:所需权限*/public static final Map<String, ConfigAttribute> PERMISSION_MAP = new ConcurrentHashMap<>();/*** 用户权限数据。key:用户名,value:权限*/public static final Map<String, List<PERMISSION>> USER_PERMISSION_MAP = new ConcurrentHashMap<>();/*** 白名单*/public static final String[] WHITELIST = {"/login"};static {// 填充模拟用户数据USER_MAP.put("admin", "$2a$10$KOvypkjLRv/iJo/hU5GOSeFsoZzPYnh2B4r7LPI2x8yBTBZhPLkhy");USER_MAP.put("user", "$2a$10$KOvypkjLRv/iJo/hU5GOSeFsoZzPYnh2B4r7LPI2x8yBTBZhPLkhy");// 填充用户权限USER_PERMISSION_MAP.put("admin", List.of(PERMISSION.ADMIN, PERMISSION.USER));USER_PERMISSION_MAP.put("user", List.of(PERMISSION.USER));// 填充接口权限PERMISSION_MAP.put("/user", new SecurityConfig(PERMISSION.USER.getValue()));PERMISSION_MAP.put("/admin", new SecurityConfig(PERMISSION.ADMIN.getValue()));}/*** 模拟权限*/@Getterpublic enum PERMISSION {ADMIN("admin"), USER("user");private final String value;private PERMISSION(String value) {this.value = value;}}
}

实现 UserDetails

package com.yiyan.study.config;import lombok.Builder;
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;/*** Spring Security用户信息*/
@Data
@Builder
public class SecurityUserDetails implements UserDetails {private String username;private String password;private List<SecurityConstant.PERMISSION> permissions;public SecurityUserDetails(String username, String password, List<SecurityConstant.PERMISSION> permissions) {this.username = username;this.password = password;this.permissions = permissions;}@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {return permissions.stream().map(permission -> new SimpleGrantedAuthority(permission.getValue())).collect(Collectors.toList());}@Overridepublic String getPassword() {return password;}@Overridepublic String getUsername() {return username;}@Overridepublic boolean isAccountNonExpired() {return true;}@Overridepublic boolean isAccountNonLocked() {return true;}@Overridepublic boolean isCredentialsNonExpired() {return true;}@Overridepublic boolean isEnabled() {return true;}
}

实现UserDetailsService,重写loadUserByUsername()方法

package com.yiyan.study.config;import io.micrometer.common.util.StringUtils;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;import java.util.List;@Service
public class SecurityUserDetailsService implements UserDetailsService {@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {// 获取用户信息String password = SecurityConstant.USER_MAP.get(username);if (StringUtils.isBlank(password)) {throw new UsernameNotFoundException("用户名或密码错误");}// 获取用户权限List<SecurityConstant.PERMISSION> permission = SecurityConstant.USER_PERMISSION_MAP.get(username);// 返回SecurityUserDetailsreturn SecurityUserDetails.builder().username(username).password(password).permissions(permission).build();}
}

创建自定义过滤器,用于实现对TOKEN进行鉴权

JWT工具类

package com.yiyan.study.utils;import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import lombok.Data;
import org.apache.commons.lang3.StringUtils;
import org.springframework.util.CollectionUtils;import java.util.Collections;
import java.util.Date;
import java.util.List;/*** JWT工具类*/
public class JwtUtils {/*** 默认JWT标签头*/public static final String HEADER = "Authorization";/*** JWT配置信息*/private static JwtConfig jwtConfig;private JwtUtils() {}/*** 初始化参数** @param header         JWT标签头* @param tokenHead      Token头* @param issuer         签发者* @param secretKey      密钥 最小长度:4* @param expirationTime Token过期时间 单位:秒* @param issuers        签发者列表 校验签发者时使用* @param audience       接受者*/public static void initialize(String header, String tokenHead, String issuer, String secretKey, long expirationTime, List<String> issuers, String audience) {jwtConfig = new JwtConfig();jwtConfig.setHeader(StringUtils.isNotBlank(header) ? header : HEADER);jwtConfig.setTokenHead(tokenHead);jwtConfig.setIssuer(issuer);jwtConfig.setSecretKey(secretKey);jwtConfig.setExpirationTime(expirationTime);if (CollectionUtils.isEmpty(issuers)) {issuers = Collections.singletonList(issuer);}jwtConfig.setIssuers(issuers);jwtConfig.setAudience(audience);jwtConfig.setAlgorithm(Algorithm.HMAC256(jwtConfig.getSecretKey()));}/*** 初始化参数*/public static void initialize(String header, String issuer, String secretKey, long expirationTime) {initialize(header, null, issuer, secretKey, expirationTime, null, null);}/*** 初始化参数*/public static void initialize(String header, String tokenHead, String issuer, String secretKey, long expirationTime) {initialize(header, tokenHead, issuer, secretKey, expirationTime, null, null);}
​
​/*** 生成 Token** @param subject 主题* @return Token*/public static String generateToken(String subject) {return generateToken(subject, jwtConfig.getExpirationTime());}/*** 生成 Token** @param subject        主题* @param expirationTime 过期时间* @return Token*/public static String generateToken(String subject, long expirationTime) {Date now = new Date();Date expiration = new Date(now.getTime() + expirationTime * 1000);return JWT.create().withSubject(subject).withIssuer(jwtConfig.getIssuer()).withAudience(jwtConfig.getAudience()).withIssuedAt(now).withExpiresAt(expiration).sign(jwtConfig.getAlgorithm());}/*** 获取Token数据体*/public static String getTokenContent(String token) {if (StringUtils.isNotBlank(jwtConfig.getTokenHead())) {token = token.substring(jwtConfig.getTokenHead().length()).trim();}return token;}/*** 验证 Token** @param token token* @return 验证通过返回true,否则返回false*/public static boolean isValidToken(String token) {try {token = getTokenContent(token);Algorithm algorithm = Algorithm.HMAC256(jwtConfig.getSecretKey());JWTVerifier verifier = JWT.require(algorithm).build();verifier.verify(token);return true;} catch (JWTVerificationException exception) {// Token验证失败return false;}}/*** 判断Token是否过期** @param token token* @return 过期返回true,否则返回false*/public static boolean isTokenExpired(String token) {try {token = getTokenContent(token);Algorithm algorithm = Algorithm.HMAC256(jwtConfig.secretKey);JWTVerifier verifier = JWT.require(algorithm).build();verifier.verify(token);Date expirationDate = JWT.decode(token).getExpiresAt();return expirationDate != null && expirationDate.before(new Date());} catch (JWTVerificationException exception) {// Token验证失败return false;}}/*** 获取 Token 中的主题** @param token token* @return 主题*/public static String getSubject(String token) {token = getTokenContent(token);return JWT.decode(token).getSubject();}/*** 获取当前Jwt配置信息*/public static JwtConfig getCurrentConfig() {return jwtConfig;}@Datapublic static class JwtConfig {/*** JwtToken Header标签*/private String header;/*** Token头*/private String tokenHead;/*** 签发者*/private String issuer;/*** 密钥*/private String secretKey;/*** Token 过期时间*/private long expirationTime;/*** 签发者列表*/private List<String> issuers;/*** 接受者*/private String audience;/*** 加密算法*/private Algorithm algorithm;}
}

配置JWT

application.yml 添加配置

server:port: 8080# ======== JWT配置 ========
jwt:secret: 1234567890123456expirationTime: 604800issuer: springboot3-securityheader: AuthorizationtokenHead: Bearer

配置JWT启动时加载配置项

package com.yiyan.study.config;
​
​
import com.yiyan.study.utils.JwtUtils;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;/*** JWT 配置*/
@Slf4j
@Component
public class JwtConfig {@Value("${jwt.secret}")private String secretKey;@Value("${jwt.issuer}")private String issuer;@Value("${jwt.expirationTime}")private long expirationTime;@Value("${jwt.header}")private String header;@Value("${jwt.tokenHead}")private String tokenHead;@PostConstructpublic void jwtInit() {JwtUtils.initialize(header, tokenHead, issuer, secretKey, expirationTime);log.info("JwtUtils初始化完成");}
}

自定义拦截器

package com.yiyan.study.config;import com.yiyan.study.utils.JwtUtils;
import jakarta.annotation.Resource;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;import java.io.IOException;/*** 自定义过滤器*/
@Component
public class MyAuthenticationFilter extends OncePerRequestFilter {@Resourceprivate SecurityUserDetailsService securityUserDetailsService;@Overrideprotected void doFilterInternal(HttpServletRequest request,HttpServletResponse response,FilterChain filterChain) throws ServletException, IOException {String requestToken = request.getHeader(JwtUtils.getCurrentConfig().getHeader());// 读取请求头中的tokenif (StringUtils.isNotBlank(requestToken)) {// 判断token是否有效boolean verifyToken = JwtUtils.isValidToken(requestToken);if (!verifyToken) {filterChain.doFilter(request, response);}// 解析token中的用户信息String subject = JwtUtils.getSubject(requestToken);if (StringUtils.isNotBlank(subject) && SecurityContextHolder.getContext().getAuthentication() == null) {SecurityUserDetails userDetails = (SecurityUserDetails) securityUserDetailsService.loadUserByUsername(subject);// 保存用户信息到当前会话UsernamePasswordAuthenticationToken authentication =new UsernamePasswordAuthenticationToken(userDetails,null,userDetails.getAuthorities());// 将authentication填充到安全上下文authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));SecurityContextHolder.getContext().setAuthentication(authentication);}}filterChain.doFilter(request, response);}
}

修改Controller 的登录接口

package com.yiyan.study.controller;import com.yiyan.study.utils.JwtUtils;
import jakarta.annotation.Resource;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;/*** 测试接口*/
@RestController
public class SecurityController {@Resourceprivate AuthenticationManager authenticationManager;
​
​@GetMapping("/hello")public String hello() {return "hello spring security";}@GetMapping("/user")public String helloUser() {return "Hello User";}@GetMapping("/admin")public String helloAdmin() {return "Hello Admin";}@PostMapping("/login")public String doLogin(@RequestParam("username") String username,@RequestParam("password") String password) {UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);Authentication authentication = authenticationManager.authenticate(authenticationToken);// 判断是否验证成功if (null == authentication) {throw new UsernameNotFoundException("用户名或密码错误");}return JwtUtils.generateToken(username);}
}

编写Spring Security配置文件

Spring Security 升级到6.x后,配置方式与前版本不同,多个旧的配置类被启用。新版本采用lambda表达式的方式进行配置,核心配置项没变化。

package com.yiyan.study.config;import jakarta.annotation.Resource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;/*** Spring Security配置类*/
@Configuration
@EnableWebSecurity
public class SecurityConfiguration {@Resourceprivate UserDetailsService userDetailsService;@Resourceprivate MyAuthenticationFilter myAuthenticationFilter;/*** 鉴权管理类*/@Beanpublic AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {return config.getAuthenticationManager();}/*** 加密类*/@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}/*** Spring Security 过滤链*/@Beanpublic SecurityFilterChain filterChain(HttpSecurity http) throws Exception {return http// 禁用明文验证.httpBasic(AbstractHttpConfigurer::disable)// 关闭csrf.csrf(AbstractHttpConfigurer::disable)// 禁用默认登录页.formLogin(AbstractHttpConfigurer::disable)// 禁用默认登出页.logout(AbstractHttpConfigurer::disable)// 禁用session.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))// 配置拦截信息.authorizeHttpRequests(authorization -> authorization// 允许所有的OPTIONS请求.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()// 放行白名单.requestMatchers(SecurityConstant.WHITELIST).permitAll()// 根据接口所需权限进行动态鉴权.anyRequest().access((authentication, object) -> {// 获取当前的访问路径String requestURI = object.getRequest().getRequestURI();PathMatcher pathMatcher = new AntPathMatcher();// 白名单请求直接放行for (String url : SecurityConstant.WHITELIST) {if (pathMatcher.match(url, requestURI)) {return new AuthorizationDecision(true);}}// 获取访问该路径所需权限Map<String, ConfigAttribute> permissionMap = SecurityConstant.PERMISSION_MAP;List<ConfigAttribute> apiNeedPermissions = new ArrayList<>();for (Map.Entry<String, ConfigAttribute> config : permissionMap.entrySet()) {if (pathMatcher.match(config.getKey(), requestURI)) {apiNeedPermissions.add(config.getValue());}}// 如果接口没有配置权限则直接放行if (apiNeedPermissions.isEmpty()) {return new AuthorizationDecision(true);}// 获取当前登录用户权限信息Collection<? extends GrantedAuthority> authorities = authentication.get().getAuthorities();// 判断当前用户是否有足够的权限访问for (ConfigAttribute configAttribute : apiNeedPermissions) {// 将访问所需资源和用户拥有资源进行比对String needAuthority = configAttribute.getAttribute();for (GrantedAuthority grantedAuthority : authorities) {if (needAuthority.trim().equals(grantedAuthority.getAuthority())) {// 权限匹配放行return new AuthorizationDecision(true);}}}return new AuthorizationDecision(false);}))// 注册重写后的UserDetailsService实现.userDetailsService(userDetailsService)// 注册自定义拦截器.addFilterBefore(myAuthenticationFilter, UsernamePasswordAuthenticationFilter.class).build();}
}

测试

在这里插入图片描述

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

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

相关文章

Pikachu靶场 字符型SQL注入

在Pikachu靶场里查询 xx or 11 # or 11 # union select username,password from users# 在 MySQL 的 bin 路径下打开 cmd &#xff0c;输入下面的命令&#xff0c;再输入 数据库的密码 mysql -u root -p 执行命令 show databases; use pikachu show tables; desc member; s…

《PCI Express体系结构导读》随记 —— 第I篇 第1章 PCI总线的基本知识(1)

前言中曾提到&#xff1a;本章主要说明PCI总线涉及的一些基本知识。 PCI总线作为处理器系统的本地总线&#xff0c;其主要目的是为了连接外部设备&#xff0c;而不是作为处理器的系统总线连接Cache和主存储器。但是PCI总线、系统总线和处理器体系结构之间依然存在着紧密的联系…

java练习之abstract (抽象) final(最终) static(静态) 练习

1&#xff1a;分析总结&#xff1a;写出private、abstract、static、final之间能否联动使用&#xff0c;并写出分析原因 private static final 之间可以任意结合 abstract 不可以与private static final 结合使用 2&#xff1a;关于三个修饰符描述不正确的是(AD) A. static …

Linux操作系统基础知识点

Linux是一种计算机操作系统&#xff0c;其内核由林纳斯本纳第克特托瓦兹&#xff08;Linus Benedict Torvalds&#xff09;于1991年首次发布。Linux操作系统通常与GNU套件一起使用&#xff0c;因此也被称为GNU/Linux。它是一种类UNIX的操作系统&#xff0c;设计为多用户、多任务…

计算机组成原理综合6

补码表示&#xff1a; X&#xff1a;1111 1111 1111 1101 Y&#xff1a;1111 1111 1101 1111 Z&#xff1a;0111 1111 1111 1100 转原码表示&#xff1a;从右往左找第一个“1”&#xff0c;左边的所有数值位按位取反 X&#xff1a;1111 1111 1111 1101 1000 0000 00…

常用的快捷键

快捷键能大大提高使用计算机时的效率。以下是一些常用的快捷键&#xff0c;适用于多种操作系统和应用程序&#xff1a; 通用快捷键 复制: Ctrl C (Windows/Linux) | Cmd C (Mac) 粘贴: Ctrl V (Windows/Linux) | Cmd V (Mac) 剪切: Ctrl X (Windows/Linux) | Cmd X (Ma…

Fiber Node的数据结构,以及如何在Reconciliation阶段被使用。

首先&#xff0c;Fiber Node是React用来描述组件树的数据结构&#xff0c;每一个React组件都对应一个Fiber Node。下面是一个Fiber Node的基本结构&#xff1a; const fiber {// 标识这个Fiber Node的类型&#xff08;函数组件&#xff0c;类组件&#xff0c;DOM节点类型等&a…

OGG-MySQL无法正常同步数据问题分析

问题背景: 用户通过OGG从源端一个MySQL从库将数据同步到目标端的另一个MySQL数据库里面&#xff0c;后面由于源端的从库出现了长时间的同步延时&#xff0c;由于延时差距过大最后选择通过重建从库方式进行了修复 从库重建之后&#xff0c;源端的OGG出现了报错ERROR OGG-0014…

45.常用shell之 unset - 删除环境变量或函数 的用法及衍生用法

unset 是一个在类 Unix 系统的 shell 中常用的命令&#xff0c;用于删除环境变量或 shell 函数。这个命令可以帮助用户管理环境变量&#xff0c;防止不必要的变量干扰或占用资源。以下是 unset 命令的基本用法和一些衍生用法的示例&#xff1a; 基本用法 删除环境变量: unset …

java基础之Java的动态代理如何实现

Java实现动态代理的两种方式 JDK动态代理&#xff1a;Java.lang.reflect 包中的Proxy类和InvocationHandler接口提供了生成动态代理类的能力。Cglib动态代理&#xff1a;Cglib (Code Generation Library )是一个第三方代码生成类库&#xff0c;运行时在内存中动态生成一个子类对…

【Qt-QString】

Qt编程指南 ■ QString■ 编码方式■ 下划线■ 制表符■ arg■ arg 数值转字符串补齐长度■ QString 转换为 char■ QString 转换为 char *■ char * 转换为 QString■ char[] 转换为QString■ char[] 转换为QString■ QByteArray 转换为 QString ■ QByteArray■ QByteArray::…

关于Sneaky DogeRAT特洛伊木马病毒网络攻击的动态情报

一、基本内容 作为复杂恶意软件活动的一部分&#xff0c;一种名为DogeRAT的新开源远程访问特洛伊木马&#xff08;RAT&#xff09;主要针对位于印度的安卓用户发动了网络安全攻击。该恶意软件通过分享Opera Mini、OpenAI ChatGOT以及YouTube、Netfilx和Instagram的高级版本等合…

《PySpark大数据分析实战》-19.NumPy介绍ndarray介绍

&#x1f4cb; 博主简介 &#x1f496; 作者简介&#xff1a;大家好&#xff0c;我是wux_labs。&#x1f61c; 热衷于各种主流技术&#xff0c;热爱数据科学、机器学习、云计算、人工智能。 通过了TiDB数据库专员&#xff08;PCTA&#xff09;、TiDB数据库专家&#xff08;PCTP…

饥荒Mod 开发(二三):显示物品栏详细信息

饥荒Mod 开发(二二)&#xff1a;显示物品信息 源码 前一篇介绍了如何获取 鼠标悬浮物品的信息&#xff0c;这一片介绍如何获取 物品栏的详细信息。 拦截 inventorybar 和 itemtile等设置字符串方法 在modmain.lua 文件中放入下面代码即可实现鼠标悬浮到 物品栏显示物品详细信…

适合引流源码声音鉴定神器网站源码,轻松吸引用户关注

源码介绍 声鉴卡HTML5网页源码&#xff0c;完整可运转&#xff0c;调用wx录音&#xff0c;自动判断声音属性&#xff0c;输出结果 安装教程 只需要把源码上传至主机空间就可以 支持上传二级目录访问&#xff01;提示一下&#xff1a;wxvx打开效果是最佳的源码里面生成二维码…

测试服务器带宽(ubuntu)

apt install python3 python3-pippip3 install speedtest-clispeestest-cli

Debezium发布历史27

原文地址&#xff1a; https://debezium.io/blog/2018/01/25/debezium-0-7-2-released/ 欢迎关注留言&#xff0c;我是收集整理小能手&#xff0c;工具翻译&#xff0c;仅供参考&#xff0c;笔芯笔芯. Debezium 0.7.2 发布 一月 25, 2018 作者&#xff1a; Gunnar Morling 发…

Hive05_DML 操作

1 DML 数据操作 1.1 数据导入 1.1.1 向表中装载数据&#xff08;Load&#xff09; 1&#xff09;语法 hive> load data [local] inpath 数据的 path [overwrite] into table student [partition (partcol1val1,…)];&#xff08;1&#xff09;load data:表示加载数据 &…

数据库添加/删除/修改表字段

目录 添加表字段 删除表字段 修改表字段 添加表字段 要在数据库中添加表字段&#xff0c;可以使用ALTER TABLE语句。 语法如下&#xff1a; ALTER TABLE table_name ADD column_name datatype;其中&#xff0c;table_name是要添加字段的表名&#xff0c;column_name是要添…

目标:三年内练就一口流利的英语

置顶&#xff0c;不删。三年后的今天来评论区分享学习成果