Spring Security + JWT 实现登录认证和权限控制

news2024/11/24 14:38:18

Spring Security + JWT 实现登录认证和权限控制

准备步骤

准备好一些常用的工具类,比如jwtUtil,redisUtil等。引入数据库,mybatis等,配置好controller,service,mapper,保证能够正常的数据请求。这里就省略了

1. 实体类User
package com.example.logindemo.domain.entity;

import lombok.Data;

import java.io.Serializable;

@Data
public class User implements Serializable {
    private static final long serialVersionUID = 1L;
    private Integer id;
    private String username;
    private String account;		//账号。我是用的这个登录,没用username
    private String password;
    private String empCode;
    private Integer sex;
    private Integer age;
    private String role;
}

2. LoginUser类

由于Security默认需要一个UserDetails,所以单独用了这个LoginUser类来实现UserDetails接口,把我们自己的User放进来

/**
 * @title: LoginUser
 * @Author DengMj
 * @Date: 2024/5/6 11:32
 * @Version 1.0
 */
@Data
@AllArgsConstructor
public class LoginUser implements UserDetails {
    private User user;

    /**
     * @return 返回用户权限,我这里直接放的角色
     */
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Set<GrantedAuthority> authorities = new HashSet<>();
        authorities.add(new SimpleGrantedAuthority("ROLE_" + this.user.getRole()));
        return authorities;
    }

    @Override
    public String getPassword() {
        return this.user.getPassword();
    }

    @Override
    public String getUsername() {
        return this.user.getAccount();
    }

    @Override
    public boolean isAccountNonExpired() {
        return false;
    }

    @Override
    public boolean isAccountNonLocked() {
        return false;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return false;
    }

    @Override
    public boolean isEnabled() {
        return false;
    }
}
3. 重写loadUserByUsername方法

让我们的UserService接口去继承UserDetailsService

public interface UserService extends UserDetailsService {
    User getUserById(int id);
}

UserDetailsService类有一个loadUserByUsername方法需要在我们的UserServiceImpl中重写

@Service
public class UserServiceImpl implements UserService {

    @Autowired
    private UserMapper userMapper;

    @Override
    public User getUserById(int id) {
        return userMapper.getUserById(id);
    }

    //这里需要返回一个UserDetails,所以我们定义了LoginUser类,当然也可以直接用User类去实现UserDetails接口
    @Override
    public UserDetails loadUserByUsername(String account) throws UsernameNotFoundException {
        User user = userMapper.getUserByAccount(account);
        if (null == user){
            throw new UsernameNotFoundException("账号不存在!");
        }

        return new LoginUser(user);
    }
}

4. 登录页面login.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Login</title>
</head>
<body>

<h2>Login</h2>

<form action="/login" method="post">
    <div><label>account:<input type="text" name="account"></label></div>
    <div><label>password:<input type="password" name="password"></label></div>
    <div><input type="submit"></div>
</form>
</body>
</html>

关键步骤

1.SecurityConfig配置类

在这里需要重写两个configure方法

  1. void configure(AuthenticationManagerBuilder auth),用来自定义登录验证的逻辑
  2. void configure(HttpSecurity http),用来配置请求拦截等策略
/**
 * @title: SecrityConfig
 * @Author DengMj
 * @Date: 2024/5/5 16:09
 * @Version 1.0
 */
@Slf4j
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)  // 开启权限注解,默认是关闭的
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    UserAuthenticationProvider userAuthenticationProvider;
    
    @Autowired
    private UserLoginSuccessHandler userLoginSuccessHandler;
    
    @Autowired
    private UserLoginFailureHandler userLoginFailureHandler;
    
    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
    
    @Autowired
    private UserAuthenticationEntryPointHandler userAuthenticationEntryPointHandler;
    
    @Autowired
    private UserAuthAccessDeniedHandler userAuthAccessDeniedHandler;
    
    @Autowired
    private UserPermissionEvaluator userPermissionEvaluator;

    //自定义的登陆验证逻辑
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(userAuthenticationProvider);
    }

    //注入自定义PermissionEvaluator,使用hasPermission()的时候才需要
    @Bean
    public DefaultWebSecurityExpressionHandler userSecurityExpressionHandler(){
        DefaultWebSecurityExpressionHandler handler = new DefaultWebSecurityExpressionHandler();
        handler.setPermissionEvaluator(userPermissionEvaluator);
        return handler;
    }
    //登录拦截配置
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                //关闭csrf
                .csrf().disable()
                //不通过session获取securityContext,通过每个请求中携带的Token来识别用户
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and().cors()
                .and()
                //所有URL都需要认证
                .authorizeRequests().antMatchers("/**").authenticated()
                .and()
                //未登录处理类
                .httpBasic().authenticationEntryPoint(userAuthenticationEntryPointHandler)
                .and()
                //对于登录接口允许访问
                .formLogin().loginPage("/login.html")
                .loginProcessingUrl("/login").permitAll()
                //自定义登录用户名为account,默认的是username
                .usernameParameter("account")
                .passwordParameter("password")
                //登录认证成功handler
                .successHandler(userLoginSuccessHandler)
                //登录认证失败handler
                .failureHandler(userLoginFailureHandler)
                .and()
                //配置登出地址
                .logout().logoutUrl("/logout")
                //成功登出
                .logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler(HttpStatus.OK))
                .and()
            	//用户无权限handler
                .exceptionHandling().accessDeniedHandler(userAuthAccessDeniedHandler)
                .and()
                //配置认证过滤器
                .addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class)
                //禁用缓存
                .headers().cacheControl();
    }
}

以下是几个关键的自定义的类,我们需要一一实现

image-20240506185522388

2. UserAuthenticationProvider实现自定义的登录逻辑

自定义UserAuthenticationProvider类实现AuthenticationProvider接口,用户发起登录请求后会执行authenticate()方法来进行登录验证。

该方法会返回一个UsernamePasswordAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities)类型的结果,该类的父类AbstractAuthenticationToken实现了Authentication接口,用来封装用户的认证信息。

/**
 * 自定义登录验证逻辑
 *
 * @title: UserAuthenticationProvider
 * @Author DengMj
 * @Date: 2024/5/5 17:14
 * @Version 1.0
 */
@Component
public class UserAuthenticationProvider implements AuthenticationProvider {

    @Autowired
    private UserService userService;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String account = (String) authentication.getPrincipal();	//登录请求中的账号
        String password = (String) authentication.getCredentials();		//登录密码

        LoginUser loginUser = (LoginUser) userService.loadUserByUsername(account);
        if (null == loginUser) {
            throw new UsernameNotFoundException("账号不存在!");
        }

        //这里直接用了MD5加密,没用Security中的passwordEncoder
        if (!DigestUtils.md5DigestAsHex(password.getBytes()).equals(loginUser.getPassword()))
            throw new BadCredentialsException("密码错误!");

        //UsernamePasswordAuthenticationToken的第三个参数需要Set<GrantedAuthority>类型的。
        //所以需要把我们的角色信息封装进去
        Set<GrantedAuthority> authorities = new HashSet<>();
        authorities.add(new SimpleGrantedAuthority(loginUser.getUser().getRole()));

        //返回封装好的用户认证信息
        return new UsernamePasswordAuthenticationToken(account, password, authorities);
    }

    @Override
    public boolean supports(Class<?> aClass) {
        return true;
    }
}

以下是UsernamePasswordAuthenticationToken类的构造函数,其中principle表示用户信息(比如用户名),credentials是用户凭证(比如密码),authorities权限信息。

image-20240506193317740

登录后的用户认证信息默认会被保存在SecurityContext中,每个作用域是HTTP session,所以一次登录之后,该会话中的请求都可以通过上下文信息中的用户认证信息认证成功。

但是我们一般都会使用无状态的RESTful API,不依赖于服务器端的会话来维持用户的认证状态,而是通过每个请求中携带的Token来识别用户。所以我们不想要Spring Security维护HTTP会话,即不使用HTTP会话来存储安全上下文信息,例如认证信息 。所以需要在SecurityConfig中做以下配置:

image-20240506194540891

3. JwtAuthenticationTokenFilter过滤器

自定义一个JwtAuthenticationTokenFilter类去实现OncePerRequestFilter接口,这样每次有请求进来的时候,都会先去执行doFilterInternal()方法。

/**
 * @title: JwtAuthenticationTokenFilter
 * @Author DengMj
 * @Date: 2024/5/6 11:04
 * @Version 1.0
 */
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
    @Autowired
    RedisUtil redisUtil;
    
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String token = request.getHeader("token");
        
        //没有token
        if (StringUtils.isEmpty(token)){
            //放行,交个后续的其他过滤器处理
            filterChain.doFilter(request, response);
            return;
        }

        //验证token
        if (!JwtUtils.verify(token)){
            throw new RemoteException("token非法");
        }

        //解析token拿到User信息
        User userByToken = JwtUtils.getUserByToken(token);
        
        String redisKey = "token:" + userByToken.getAccount();
        JSONObject jsonObject = (JSONObject) redisUtil.get(redisKey);
        User user = jsonObject.toJavaObject(User.class);
        //redis中没有记录,说明用户没有登录,或者登录过期了
        if (null == user){
            throw new RemoteException("用户未登录");
        }

        //用户的权限信息
        Set<GrantedAuthority> authorities = new HashSet<>();
        authorities.add(new SimpleGrantedAuthority(user.getRole()));

        //在上下文中保存用户认证信息
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(new LoginUser(user), token, authorities);
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        filterChain.doFilter(request, response);

    }
}

将过滤器添加到配置类的HttpSecurity配置中

image-20240506210640510

4. 自定义登录相关的处理类

SecurityConfig配置类中,void configure(HttpSecurity http)方法可以指定登录认证后各种情况的handler,接下来我们挨个实现这些类。

image-20240506195850279

1.UserLoginSuccessHandler

如果通过了UserAuthenticationProvider的登录逻辑验证,那么就会执行该类中的onAuthenticationSuccess()方法,我们可以在这里将用户信息存入到Redis中,并且返回jwt签署的token。

/**
 * @title: UserLoginSuccessHandler 登录成功处理类
 * @Author DengMj
 * @Date: 2024/5/5 19:51
 * @Version 1.0
 */
@Slf4j
@Component
public class UserLoginSuccessHandler implements AuthenticationSuccessHandler {
    @Autowired
    UserService userService;

    @Autowired
    RedisUtil redisUtil;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        String account = (String) authentication.getPrincipal();

        LoginUser loginUser = (LoginUser) userService.loadUserByUsername(account);
        User user = loginUser.getUser();
        String token = JwtUtils.sign(user);

        //把用户信息存入到Redis中
        String redisKey = "token:" + user.getAccount();
        redisUtil.set(redisKey, user);
        redisUtil.expire(redisKey, TimeUnit.HOURS.toMillis(2));
        log.info(APIResult.newSuccessResult(token));

        //返回token
        response.setContentType("application/json");
        response.setCharacterEncoding("utf-8");
        response.getWriter().println(APIResult.newSuccessResult(token));
    }
}

2.UserLoginFailureHandler

如果登录失败就会走这个handler,可以根据抛出的不同的异常来判定登陆失败的原因并通过response返回。

/**
 * 登录失败处理类
 * @title: UserLoginFailureHandler
 * @Author DengMj
 * @Date: 2024/5/6 15:39
 * @Version 1.0
 */
@Slf4j
@Component
public class UserLoginFailureHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
        response.setContentType("application/json");
        response.setCharacterEncoding("utf-8");

        log.info("登陆失败:{}", e.getMessage());

        if (e instanceof UsernameNotFoundException)
            response.getWriter().println(APIResult.newFailResult("账号不存在!"));

        else if (e instanceof LockedException)
            response.getWriter().println(APIResult.newFailResult("账号被冻结!"));

        else if (e instanceof BadCredentialsException)
            response.getWriter().println(APIResult.newFailResult("密码错误!"));

        else
            response.getWriter().println(APIResult.newFailResult("登陆失败!"));
    }
}

3.UserAuthenticationEntryPointHandler

Spring Security 没有登录时,会被 UsernamePasswordAuthenticationFilter 拦截器拦截,它是处理表单登录的默认拦截器。如果没有登录就尝试访问受保护的资源,Spring Security 会返回登录页面或者返回401 Unauthorized错误,具体取决于配置。

如果想自定义未登录的处理方式,可以通过实现 AuthenticationEntryPoint 接口来定制。

/**
 * 用户未登录处理类
 * @title: UserAuthenticationEntryPointHandler
 * @Author DengMj
 * @Date: 2024/5/6 15:52
 * @Version 1.0
 */
@Component
public class UserAuthenticationEntryPointHandler implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
        response.setContentType("application/json");
        response.setCharacterEncoding("utf-8");
        response.getWriter().println(APIResult.newFailResult(401, "未登录!"));
    }
}

5. 认证流程总结

到这里登录认证就已经实现了,总结一下整个流程就是:

  1. 用户发起登录请求

  2. 请求被JwtAuthenticationTokenFilter过滤器拦截,执行doFilterInternal方法,此时发现token为null,因此放行给后续过滤器处理

  3. 后续过滤器发现这是一个地址为/login的登录请求,由于我们在配置类的configure方法中配置了允许登录接口访问,因此该请求不会被拦截

    image-20240506204707178

  4. 然后会调用UserAuthenticationProvider中的authenticate方法进行登录验证

  5. 根据登录验证的结果判断是调用UserLoginFailureHandler还是UserLoginSuccessHandler

  6. response返回结果,整个登录认证的过程就结束了

大概的执行流程就是这样,debug得出来的这个执行流程,具体底层的东西还不是特别清楚。

6. 在请求方法上添加注解@PreAuthorize

使用注解@PreAuthorize(“hasAuthority(‘admin’)”)表明该请求需要admin权限

@PreAuthorize("hasAuthority('admin')")
@GetMapping("/getUserById/{id}")
// @PreAuthorize("hasPermission('/user', 'admin')")
public String getUserById(@PathVariable int id){
    User userById = userService.getUserById(id);
    return APIResult.newSuccessResult(userById);
}

@PreAuthorize注解可选的参数包括这些,其中hasRole()hasAuthority()方法是差不多的,只是一个前缀ROLE_的区别,具体的看后面会讲到。

image-20240506213233571

如果使用hasPermission()可以自己定义UserPermissionEvaluator类通过实现PermissionEvaluator接口来自定义鉴权逻辑

UserPermissionEvaluator
/**
 * @title: UserPermissionEvaluator
 * @Author DengMj
 * @Date: 2024/5/6 16:39
 * @Version 1.0
 */
@Component
public class UserPermissionEvaluator implements PermissionEvaluator {
    /**
     * @param authentication 用户信息
     * @param targetUrl  请求路径
     * @param permission 需要的权限
     * @return
     */
    @Override
    public boolean hasPermission(Authentication authentication, Object targetUrl, Object permission) {
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        return loginUser.getAuthorities().contains(permission);
    }

    @Override
    public boolean hasPermission(Authentication authentication, Serializable serializable, String s, Object o) {
        return false;
    }
}

7. UserAuthAccessDeniedHandler

自定义UserAuthAccessDeniedHandler类用来处理当访问被拒绝时的逻辑,并将其加入到配置类中

/**
 * 暂无权限处理类
 * @title: UserAuthAccessDeniedHandler
 * @Author DengMj
 * @Date: 2024/5/6 15:54
 * @Version 1.0
 */
@Component
public class UserAuthAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
        response.setContentType("application/json");
        response.setCharacterEncoding("utf-8");
        response.getWriter().println(APIResult.newFailResult(403, "暂无权限"));
    }
}

image-20240506211202753

8. 鉴权流程总结

这样我们的鉴权功能就实现了,总结一下实现的流程:

  1. jwt过滤器从请求中拿到token,解析出用户信息,每个用户信息中都带有该用户的角色信息(role字段),将用户认证信息保存在上下文中
  2. 根据我们在controller方法上标注的注解@PreAuthorize("hasAuthority('admin')"),会执行源码中的这段代码,实现鉴权

image-20240506213045237

  1. 如果没有权限则会通过UserAuthAccessDeniedHandler来处理访问被拒绝时的逻辑

以上只是简单的实现了一下Security的登录认证和鉴权相关的操作,底层原理还需要再花时间学习~

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

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

相关文章

泛微E9开发 限制整型、日期型、附件型字段的取值范围

1、功能背景 在用户进行输入时&#xff0c;通过控制输入数据的范围来实现实际效果&#xff0c;如上级管理者对下级员工进行年度评分时&#xff0c;只能输入1~100分&#xff0c;现在表单中新增三种类型不同的字段&#xff0c;具体如下所示&#xff1a; 2、展示效果 限制整数的…

代码随想录算法训练营DAY44|C++动态规划Part6|完全背包理论基础、518.零钱兑换II、377. 组合总和 Ⅳ

文章目录 完全背包理论基础完全背包问题的定义与01背包的核心区别为什么完全背包的循环顺序可以互换&#xff1f;CPP代码 ⭐️518.零钱兑换II思路CPP代码 ⭐️377. 组合总和 Ⅳ思路CPP代码 扩展题 完全背包理论基础 卡码网第52题 文章链接&#xff1a;完全背包理论基础 视频链接…

android系统serviceManger源码解析

一&#xff0c;serviceManger时序图 本文涉及到的源码文件&#xff1a; /frameworks/native/cmds/servicemanager/main.cpp /frameworks/native/libs/binder/ProcessState.cpp /frameworks/native/cmds/servicemanager/ServiceManager.cpp /frameworks/native/libs/binder/IP…

cURL:命令行下的网络工具

序言 在当今互联网时代&#xff0c;我们经常需要与远程服务器通信&#xff0c;获取数据、发送请求或下载文件。在这些情况下&#xff0c;cURL 是一个强大而灵活的工具&#xff0c;它允许我们通过命令行进行各种类型的网络交互。本文将深入探讨 cURL 的基本用法以及一些高级功能…

融资融券利率最低多少:一文了解2024年最低融资融券开通攻略(利率4%-5%)

一、什么是融资融券利率&#xff1f; 融资融券利率通常指的是投资者在进行融资融券交易时需要支付给券商的利息费用的比率&#xff08;年化利率&#xff09;。 具体来说&#xff0c;融资融券利率包括两部分&#xff1a; 1、融资利率&#xff1a;这是客户借入资金进行证券买入…

S32DS查看freeRTOS运行状态

在工具栏上面可以选择查看任务、队列、计时器、堆栈&#xff0c;都需要暂停下来查看。 打开之后千万不要急&#xff0c;因为需要比较久的时间&#xff0c;一个一个字节地读取出来。 任务列表是最常用的&#xff0c;任务名称、句柄、状态、优先级和堆栈使用情况都能看到。 计时…

智能网联汽车网络和数据安全态势分析

文章目录 前言一、我国智能网联汽车安全态势分析(一)我国高度重视智能网联汽车安全发展(二)产业高速发展伴随网络安全隐患(三)网络和数据安全风险事件威胁加剧二、智能网联汽车网络和数据安全典型实践剖析(一)立标准规范,把牢安全发展“方向盘”(二)强车主服务,系好…

企业计算机服务器中了locked勒索病毒怎么处理,locked勒索病毒解密建议

随着互联网技术在企业当中的应用&#xff0c;越来越多的企业利用网络开展各项工作业务&#xff0c;网络为企业提供了极大便利&#xff0c;也大大加快了企业发展步伐&#xff0c;提高了企业生产办公效率。但网络技术的发展也为企业的数据安全带来严重威胁。近期&#xff0c;云天…

【GameFi】链游 | Seraph | 区块链上的动作角色扮演 NFT 装备收集和掠夺游戏

官网下载 新赛季公告&#xff1a;https://www.seraph.game/#/news/357 开始时间&#xff1a;2024年4月19日 11:00 (UTC8&#xff09; discard会有人发送一些激活码&#xff0c;或者有一些活动&#xff0c;只需要填表格关注账号&#xff0c;参与了就会将激活码发到你的邮箱 …

STM32单片机实战开发笔记-独立看门狗IWDG

嵌入式单片机开发实战例程合集&#xff1a; 链接&#xff1a;https://pan.baidu.com/s/11av8rV45dtHO0EHf8e_Q0Q?pwd28ab 提取码&#xff1a;28ab IWDG模块测试 1、功能描述 STM32F10X内置两个看门狗&#xff0c;提供了更高的安全性&#xff0c;时间的精确下性和使用的灵活性…

OpenHarmony 实战开发——ABI

OpenHarmony系统支持丰富的设备形态&#xff0c;支持多种架构指令集&#xff0c;支持多种操作系统内核&#xff1b;为了应用在各种OpenHarmony设备上的兼容性&#xff0c;本文定义了"OHOS" ABI&#xff08;Application Binary Interface&#xff09;的基础标准&#…

数据库大作业——基于qt开发的图书管理系统 (一)环境的配置与项目需求的分析

前言 博主最近数据库原理结课要做课程设计了,要求开发基于数据库实现的图书管理系统&#xff0c;博主想了想决定做一个基于Qt的图书管理系统,博主在此之前其实也没有用过多少Qt&#xff0c;仅以此专栏记录博主学习与开发的全过程&#xff0c;大家一起学习&#xff0c;一起进步…

Transformer - 编码器和解码器中的QKV分别来自哪

Transformer - 编码器和解码器中的QKV分别来自哪 flyfish Transformer - 注意⼒机制 Scaled Dot-Product Attention 计算过程 Transformer - 注意⼒机制 代码实现 Transformer - 注意⼒机制 Scaled Dot-Product Attention不同的代码比较 Transformer - 注意⼒机制 代码解释 Tr…

【Pytorch】2.TensorBoard的运用

什么是TensorBoard 是一个可视化和理解深度爵溪模型的工具。它可以通过显示模型结构、训练过程中的指标和图形化展示训练的效果来帮助用户更好地理解和调试他们的模型 TensorBoard的使用 安装tensorboard环境 在终端使用 conda install tensorboard通过anaconda安装 导入类Sum…

大厂Java面试题:MyBatis是如何进行分页的?分页插件的实现原理是什么?

大家好&#xff0c;我是王有志。 今天给大家带来的是一道来自京东的关于 MyBatis 实现分页功能的面试题&#xff1a;MyBatis是如何进行分页的&#xff1f;分页插件的实现原理是什么&#xff1f;通常&#xff0c;分页的方式可以分为两种&#xff1a; 逻辑&#xff08;内存&…

c#绘制渐变色的Led

项目场景&#xff1a; c#绘制渐变色的button using System; using System.ComponentModel; using System.Drawing; using System.Drawing.Drawing2D; using System.Windows.Forms; using static System.Windows.Forms.AxHost;namespace WindowsFormsApp2 {public class Gradie…

基于FPGA的数字信号处理(9)--定点数据的两种溢出处理模式:饱和(Saturate)和绕回(Wrap)

1、前言 在逻辑设计中&#xff0c;为了保证运算结果的正确性&#xff0c;常常需要对结果的位宽进行扩展。比如2个3bits的无符号数相加&#xff0c;只有将结果设定为4bits&#xff0c;才能保证结果一定是正确的。不然&#xff0c;某些情况如77 14(1110)&#xff0c;如果结果只…

FileBird Pro插件下载:革新您的WordPress媒体库管理

WordPress媒体库是您网站的重要组成部分&#xff0c;它存储了所有的图片、视频、文档等文件。但随着网站的扩展&#xff0c;媒体库的管理变得越来越复杂。FileBird Pro插件&#xff0c;作为一款专为WordPress用户设计的媒体库管理工具&#xff0c;以其直观的界面和强大的功能&a…

嵌入式系统应用-拓展-FLASH之操作 SFUD (Serial Flash Universal Driver)之KEIL移植

1 SFUD介绍 1.1 初步介绍 SFUD 是一个开源的串行 SPI 闪存通用驱动库。由于市面上有各种类型的串行闪存设备&#xff0c;每种设备都具有不同的规格和指令&#xff0c;因此 SFUD 的设计目的是解决这些差异。这使得我们的产品可以支持不同品牌和规格的闪存&#xff0c;增强了软…

幻兽帕鲁游戏主机多少钱?幻兽帕鲁游戏服务器一个月仅需32元

随着游戏产业的蓬勃发展&#xff0c;腾讯云紧跟潮流&#xff0c;推出了针对热门游戏《幻兽帕鲁Palworld》的专属游戏服务器。对于广大游戏爱好者来说&#xff0c;这无疑是一个激动人心的消息。那么&#xff0c;腾讯云幻兽帕鲁游戏主机到底多少钱呢&#xff1f;让我们一起来揭晓…