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()));
    }/**
     * 模拟权限
     */
    @Getter
    public 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;
    }@Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return permissions.stream()
                .map(permission -> new SimpleGrantedAuthority(permission.getValue()))
                .collect(Collectors.toList());
    }@Override
    public String getPassword() {
        return password;
    }@Override
    public String getUsername() {
        return username;
    }@Override
    public boolean isAccountNonExpired() {
        return true;
    }@Override
    public boolean isAccountNonLocked() {
        return true;
    }@Override
    public boolean isCredentialsNonExpired() {
        return true;
    }@Override
    public 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 {
    @Override
    public 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);
        // 返回SecurityUserDetails
        return 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;
    }@Data
    public 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: 1234567890123456
  expirationTime: 604800
  issuer: springboot3-security
  header: Authorization
  tokenHead: 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;@PostConstruct
    public 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 {@Resource
    private SecurityUserDetailsService securityUserDetailsService;@Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {
        String requestToken = request.getHeader(JwtUtils.getCurrentConfig().getHeader());
        // 读取请求头中的token
        if (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 {@Resource
    private 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 {@Resource
    private UserDetailsService userDetailsService;
    @Resource
    private MyAuthenticationFilter myAuthenticationFilter;/**
     * 鉴权管理类
     */
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
        return config.getAuthenticationManager();
    }/**
     * 加密类
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }/**
     * Spring Security 过滤链
     */
    @Bean
    public 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();
    }
}

测试

在这里插入图片描述

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

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

相关文章

(已解决)(pytorch指定了gpu但还是占用了一点0号gpu)以及错误(cuDNN error: CUDNN_STATUS_INTERNAL_ERROR)

文章目录 错误原因解决问题 错误原因 出现错误cuDNN error: CUDNN_STATUS_INTERNAL_ERROR&#xff0c;从这个名字就可以看出&#xff0c;出错原因其实有可能有很多种&#xff0c;我这里说一种比较常见的&#xff0c;就是&#xff1a;显存不足。 一个困惑点在于&#xff0c;在…

为什么 export 导出一个字面量会报错而使用 export default 不会报错

核心 其实总的来说就是 export 导出的是变量的句柄&#xff08;或者说符号绑定、近似于 C 语言里面的指针&#xff0c;C里面的变量别名&#xff09;&#xff0c;而 export default 导出的是变量的值。 需要注意的是&#xff1a;模块里面的内容只能在模块内部修改&#xff0c;…

联合办公行业即将走向寒冬?如何重拾创业者信心

近年来&#xff0c;联合办公行业固然经历了迅猛发展&#xff0c;但现在似乎遭遇了一个潜在的拐点。面对经济的下行压力&#xff0c;一些人士担忧联合办公行业可能会步入寒冬。就在这个关键时刻&#xff0c;如何重拾创业者的信心成为行业内急需解决的问题。 首先要认识到的是&am…

c语言-位操作符练习题

文章目录 前言一、n&(n-1)的运用场景(n为整数)二、&1 和 >>的应用场景总结 前言 本篇文章介绍利用c语言的位操作符解决一些练习题&#xff0c;目的是掌握各个位操作符的使用和应用场景。 表1.1为c语言中的位操作符 操作符含义&按位与|按位或^按位异或~按位…

猪目标检测数据集VOC格式600张

猪是一种常见的哺乳动物&#xff0c;通常被人们认为是肉食动物&#xff0c;但实际上猪是杂食性动物&#xff0c;以植物性食物为主&#xff0c;也有偶尔食肉的习性。猪的体型较大&#xff0c;圆胖的体型和圆润的脸庞使其显得憨态可掬。它们主要通过嗅觉来感知周围环境&#xff0…

【持续更新ing】uniapp+springboot实现个人备忘录系统【前后端分离】

目录 &#xff08;1&#xff09;项目可行性分析 &#xff08;2&#xff09;需求描述 &#xff08;3&#xff09;界面原型 &#xff08;4&#xff09;数据库设计 &#xff08;5&#xff09;后端工程 接下来我们使用uniappspringboot实现一个简单的前后端分离的小项目----个…

TinyXml2基础操作大全,tinyxml深度解析,一文精通tinyxml之xml中的操作

&#x1f4cb; 前言 &#x1f5b1; 博客主页&#xff1a;在下马农的碎碎念&#x1f917; 欢迎关注&#x1f50e;点赞&#x1f44d;收藏⭐️留言&#x1f4dd;✍ 本文由在下马农原创&#xff0c;首发于CSDN&#x1f4c6; 首发时间&#xff1a;2021/12/25&#x1f4c5; 最近更新时…

Dockerfile - 基于 SpringBoot 项目自定义镜像(项目上线全过程)

目录 一、Dockerfile 自定义项目镜像 1.1、创建 SpringBoot 项目并编写 1.2、打包项目&#xff08;jar&#xff09; 1.3、编写 Dockerfile 文件&#xff0c;构建镜像 1.4、运行镜像并测试 一、Dockerfile 自定义项目镜像 1.1、创建 SpringBoot 项目并编写 a&#xff09;简…

手把手教你自己动手使用ONLYOFFICE制作2024年历日记本

手把手教你自己动手使用ONLYOFFICE制作2024年历日记本 又到了岁末年初的时候了&#xff0c;按照自己的习惯&#xff0c;是又该上淘宝买一个年历日记本了&#xff1a; 这也太便宜了吧&#xff01;这里我坚决要把价格打上去&#xff01; 把价格打上去&#xff0c;就是亲自动手制…

NFC物联网智能购物车设计方案

智能购物车是综合利用计算机网络、射频识别技术、数据库技术、单片机于一体的设备具有先进性、便于管理性、经济性、普适性。基于NFC (Near Field Communication&#xff0c;近场通信)技术的智能购物车&#xff0c;能够大幅缩短结账排队时间&#xff0c;实现“无感支付”。NFC是…

【C++】题解:三道只出现一次的数字问题

文章目录 只出现一次的数字i只出现一次的数ii只出现一次的数iii总结 本文介绍了三道只出现一次的数字问题的解法&#xff0c;分别是使用异或运算的方法、使用位运算的方法和使用异或运算和位运算相结合的方法。这三种方法都满足了题目中要求的线性时间复杂度和常数额外空间的条…

【教程】自动检测和安装Python脚本依赖的第三方库

转载请注明出处&#xff1a;小锋学长生活大爆炸[xfxuezhang.cn] 背景说明 对于新python环境&#xff0c;要运行某个脚本&#xff0c;可能需要安装很多库&#xff0c;一般可以通过提供的requirements.txt来自动安装。但如果没有这个txt&#xff0c;那就得手动一个一个安装&#…

这一年,熬过许多夜,也有些许收获 | 2023年终总结

大家好&#xff0c;我是小悟 时间如白驹过隙&#xff0c;一如流光匆匆&#xff0c;转瞬即逝。它如同沙漏中的细沙&#xff0c;无声无息地从指间溜走&#xff0c;留给我们无尽的思索。 我们总是无知地忙碌着&#xff0c;而忽略了时间无形的步伐&#xff0c;却发现它已经一去不…

8个plotly绘图技巧

文章目录 什么是Plotlyplolty绘图如何添加标题&#xff0c;及控制标题的颜色和大小&#xff1f;plotly绘图如何自定义x轴和y轴的名称饼图如何同时显示百分比和数值柱状图宽度如何添加注释如何绘制多子图如何添加图例以及控制其颜色、大小、位置等桑基图Python技术资源分享1、Py…

打开3d模型时显示不匹配怎么办---模大狮模型网

当3d模型打开时&#xff0c;显示不匹配的情况可能有以下几个原因和解决方法&#xff1a; 文件格式不匹配&#xff1a;检查您所使用的3D软件是否支持打开该模型文件格式。不同的软件支持不同的文件格式&#xff0c;如果文件格式不匹配&#xff0c;可能无法正确加载和显示模型。尝…

新能源汽车制造设备状态监测:无线温振传感器的应用

随着全球对环境保护的关注度不断增加&#xff0c;新能源汽车的市场需求正在逐步扩大。而为了满足这一需求&#xff0c;新能源汽车制造企业必须依赖高效、可靠的设备来进行生产制造。然而&#xff0c;设备状态的监测与维护对于保证生产线的稳定运行至关重要。无线温振传感器作为…

【JVM篇】Java是如何实现平台无关的?

Java是如何实现平台无关的? ✔️什么是平台无关性✔️平台无关性的实现✔️Java虚拟机✔️字节码✔️Java语言规范 ✔️扩展知识仓✔️平台无关性的好处✔️ 有哪些语言实现了平台无关?✔️Java中基本数据类型的大小都是确定的吗? ✔️什么是平台无关性 平台无关性就是一种语…

dds 问题记录

Q1. 2023.12.29 一个participant内部的数据也会放到topic中进行发布、订阅吗&#xff1f;为什么&#xff1f;如图中的topic3。 (from 车载通信架构 —— DDS协议介绍https://mp.weixin.qq.com/s/IasCCsVJ7w-CHeyXGM6soQ)

Java创建线程执行任务的方法(一)

目录 1.继承Thread类 2.实现Runnab类 2.1实现Runnable类 2.2使用Lambda表达式 3.实现Callable类 3.1返回Integer类型数据 3.2返回String类型数据 3.3返回Object类型数据 4.匿名内部类 创建线程的方法&#xff1a;继承Thread类&#xff1b;实现Runnab类&#xff1b;匿名…

深度解析高防产品---游戏盾

游戏盾是针对游戏行业所推出的高度可定制的网络安全解决方案&#xff0c;游戏盾是高防产品系列中针对游戏行业的安全解决方案。游戏盾专为游戏行业定制&#xff0c;针对性解决游戏行业中复杂的DDoS攻击、游戏CC攻击等问题。游戏盾通过分布式的抗D节点&#xff0c;可以防御TB级大…