springboot+springSecurity+jwt实现登录认证后令牌授权

news2024/11/25 10:07:04

springboot+springSecurity+jwt实现登录认证后令牌授权(已绑定整个项目的源码)

目录

  • springboot+springSecurity+jwt实现登录认证后令牌授权(已绑定整个项目的源码)
  • 一、自定义数据源登录认证
    • 1、实现spring security中UserDetails类
    • 2、用户查询和更新密码的mapper
    • 3、查询或更新密码的service
    • 4、自定义用户登录过滤器
    • 5、继承WebSecurityConfigurerAdapter,配置spring security
  • 二、添加令牌授权的过滤器
    • 1、生成和解析jwt
    • 2、jwt过滤器
  • 三、验证

一、自定义数据源登录认证

默认情况下,spring security会生成默认用户名和密码,但是在实际开发中,我们大多数都时从数据库中获取用户密码的,所以这里需要向默认登录认证过滤器,替换为我们需要的。

1、实现spring security中UserDetails类

package cn.cdjs.entity;

import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;

/**
 * @Author Jiangjinlong
 * @Date 2023/7/21 16:31
 * @PackageName:com.security.entity
 * @ClassName: User
 * @Description: TODO
 */
@Data
public class User implements UserDetails {
    @TableId
    private String username;
    private String password;
    /*
    是否启用
     */
    private Boolean enabled;
    /*
    账户是否过期
     */
    private Boolean accountNonExpired;
    /*
    账户是否锁定
     */
    private Boolean accountNonLocked;
    /*
    密码是否过期
     */
    private Boolean credentialsNonExpired;


    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return null;
        //本次不设置用户权限
        //Set<SimpleGrantedAuthority> authorities = new HashSet<>();
        //roles.forEach(role -> {
        //    SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(role.getName());
        //    authorities.add(simpleGrantedAuthority);
        //});
        //return authorities;

        //List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
        //roles.forEach(role -> grantedAuthorities.add(new
        //        SimpleGrantedAuthority(role.getRoleName())));
        //return grantedAuthorities;
    }

    @Override
    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    @Override
    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

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

    public void setAccountNonExpired(Boolean accountNonExpired) {
        this.accountNonExpired = accountNonExpired;
    }

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

    public void setAccountNonLocked(Boolean accountNonLocked) {
        this.accountNonLocked = accountNonLocked;
    }

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

    public void setCredentialsNonExpired(Boolean credentialsNonExpired) {
        this.credentialsNonExpired = credentialsNonExpired;
    }

    @Override
    public boolean isEnabled() {
        return enabled;
    }

    public void setEnabled(Boolean enabled) {
        this.enabled = enabled;
    }
}

2、用户查询和更新密码的mapper

package cn.cdjs.mapper;

import cn.cdjs.entity.User;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;

/**
 * @Author Jiangjinlong
 * @Date 2023/7/21 16:44
 * @PackageName:com.security.mapper
 * @ClassName: UserDao
 * @Description: TODO
 */
@Mapper
public interface UserDao extends BaseMapper<User> {

    //根据用户名查询用户
    @Select("select username, password, enabled, accountNonExpired, accountNonLocked, credentialsNonExpired\n" +
            "        from user\n" +
            "        where username = #{username}")
    User loadUserByUsername(@Param("username") String username);
    //根据用户更名新密码密码
    @Update("update `user` set password=#{password} where username=#{username}")
    Integer updatePassword(@Param("username") String username,@Param("password") String password);
}

3、查询或更新密码的service

  • loadUserByUsername:用于从数据库中查询用户是否存在;
  • updatePassword:用户将数据库中明文密码,更新为密文,确保数据的安全性;
package cn.cdjs.service;

import cn.cdjs.entity.User;
import cn.cdjs.mapper.UserDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsPasswordService;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;

/**
 * @Author Jiangjinlong
 * @Date 2023/7/21 16:42
 * @PackageName:com.security.config
 * @ClassName: MyUserDetailService
 * @Description: 数据源的认证
 */
@Service
public class MyUserDetailService implements UserDetailsService, UserDetailsPasswordService {
    private final UserDao userDao;

    @Autowired
    public MyUserDetailService(UserDao userDao) {
        this.userDao = userDao;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userDao.loadUserByUsername(username);
        if (ObjectUtils.isEmpty(user)) throw new UsernameNotFoundException("用户名不正确");
        return user;
    }
    @Override
    public UserDetails updatePassword(UserDetails user, String newPassword) {
        Integer result = userDao.updatePassword(user.getUsername(),newPassword);
        if (result==1) {
            ((User) user).setPassword(newPassword);
        }
        return user;
    }
}

4、自定义用户登录过滤器

由于根据spingsecurity的认证流程,发起认证请求,请求中携带⽤户名、密码,该请求会被
UsernamePasswordAuthenticationFilter 拦截,通过里面的attemptAuthentication⽅法
中将请求中⽤户名和密码,封装为Authentication对象,并交给AuthenticationManager 进⾏认证。所以这里需继承UsernamePasswordAuthenticationFilter ,并重新Authentication对象。

package cn.cdjs.config.security;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;

/**
 * @Author Jiangjinlong
 * @Date 2023/7/24 14:02
 * @PackageName:com.nohtml.config
 * @ClassName: LoginFilter
 * @Description: 自定义前后端分离认证得filter
 */
@Configuration
public class LoginFilter extends UsernamePasswordAuthenticationFilter {
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        //1.判断是否是post方式请求
        if (!request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        }
        //2.判断是否是json格式请求类型
        if (request.getContentType().equalsIgnoreCase("application/json")) {
            //3.从json数据中获取用户输入的用户密码进行认证
            try {
                Map<String,String> userInfo = new ObjectMapper().readValue(request.getInputStream(), Map.class);
                String username = userInfo.get(getUsernameParameter());
                String password = userInfo.get(getPasswordParameter());
                UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username, password);
                this.setDetails(request, authRequest);
                return this.getAuthenticationManager().authenticate(authRequest);
            }catch (IOException e){
                e.printStackTrace();
            }
        }
        return super.attemptAuthentication(request, response);
    }
}

5、继承WebSecurityConfigurerAdapter,配置spring security

这里包含了用户登录和令牌授权的配置

  • 注入登录过滤器LoginFilter
    • 指定登录认证的url
    • 指定登录时请求体里面的用户名和密码key
    • 自定义认证成功和失败的响应体
  • 重写configure
    • 指定认证方式
    • 自定义异常处理
    • 自定义登出异常响应
    • 配置全局跨域
    • 将令牌暴露给前端,以便js能够获取到令牌
package cn.cdjs.config.security;

import cn.cdjs.entity.User;
import cn.cdjs.service.MyUserDetailService;
import cn.cdjs.utils.other.AjaxResult;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import java.util.Arrays;
import java.util.HashMap;

/**
 * @Author Jiangjinlong
 * @Date 2023/7/24 13:57
 * @PackageName:com.nohtml.config
 * @ClassName: SecurityConfig
 * @Description: TODO
 */
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter{

    @Autowired
    private TokenProvider tokenProvider;

    private final MyUserDetailService myUserDetailService;

    public SecurityConfig(MyUserDetailService myUserDetailService) {
        this.myUserDetailService = myUserDetailService;
    }


    @Override
    protected void configure(AuthenticationManagerBuilder auth)throws Exception{
        auth.userDetailsService(myUserDetailService);
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception{
        return super.authenticationManagerBean();
    }

    @Bean
    public LoginFilter loginFilter() throws Exception {
        LoginFilter loginFilter = new LoginFilter();
        loginFilter.setFilterProcessesUrl("/api/doLogin");//指定认证的url
        //指定用户名和密码的key
        loginFilter.setUsernameParameter("uname");
        loginFilter.setPasswordParameter("passwd");
        loginFilter.setAuthenticationManager(authenticationManagerBean());
        loginFilter.setAuthenticationSuccessHandler((request, response, authentication) -> {
            HashMap<Object, Object> hashMap = new HashMap<>();
            hashMap.put("userinfo", authentication.getPrincipal());
            AjaxResult ajaxResult = new AjaxResult();
            ajaxResult.setResultObj(hashMap);
            String valueAsString = new ObjectMapper().writeValueAsString(ajaxResult);
            User User = (User) authentication.getPrincipal();
            String token = tokenProvider.generateToken(User);
            response.setHeader("Authorization", "Bearer " + token);
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().println(valueAsString);
        });//认证成功处理
        loginFilter.setAuthenticationFailureHandler((request, response, exception) -> {
            HashMap<Object, Object> hashMap = new HashMap<>();
            hashMap.put("msg", "登陆失败"+exception.getMessage());
            response.setContentType("application/json;charset=UTF-8");
            response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
            String valueAsString = new ObjectMapper().writeValueAsString(hashMap);
            response.getWriter().println(valueAsString);
        });//认证失败处理
        return loginFilter;
    }
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().anyRequest().authenticated()
                .and()
                .formLogin()
                .and()
                .exceptionHandling()//认证异常处理
                .authenticationEntryPoint((request, response, authException) -> {
                    response.setContentType("application/json;charset=UTF-8");
                    response.setStatus(HttpStatus.UNAUTHORIZED.value());
                    response.getWriter().println(new AjaxResult().setMessage("请认证"));
                })
                .and()
                .logout()
                .logoutUrl("/logout")
                .logoutSuccessHandler((request, response, authentication) -> {
                    HashMap<Object, Object> hashMap = new HashMap<>();
                    hashMap.put("msg", "操作成功");
                    hashMap.put("status", "200");
                    hashMap.put("authentication",authentication.getPrincipal());
                    response.setContentType("application/json;charset=UTF-8");
                    String valueAsString = new ObjectMapper().writeValueAsString(hashMap);
                    response.getWriter().println(valueAsString);
                })
                .and()
                .cors()//跨域处理方案
                .configurationSource(corsConfigurationSource())
                .and()
                .csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        /**
         * At:用某个filter替换过滤器链的某个filter
         * Before:放在滤器链的某个filter之前
         * After:放在滤器链的某个filter之后
         */
        http.addFilterAt(loginFilter(), UsernamePasswordAuthenticationFilter.class);
        http.addFilterBefore(tokenAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
    }

    CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        corsConfiguration.setAllowedOrigins(Arrays.asList("http://localhost:8080")); // 允许的域
        corsConfiguration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE")); // 允许的HTTP方法
        corsConfiguration.addAllowedHeader("*"); // 允许的头部字段,包括Authorization
        corsConfiguration.setAllowCredentials(true); // 允许带上凭据,如Cookie
        corsConfiguration.addExposedHeader("Authorization"); // 将Authorization字段暴露给JavaScript
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", corsConfiguration);
        return source;
    }
    @Bean
    public TokenAuthenticationFilter tokenAuthenticationFilter() {
        return new TokenAuthenticationFilter(tokenProvider);
    }
}

二、添加令牌授权的过滤器

1、生成和解析jwt

package cn.cdjs.config.security;

import cn.cdjs.entity.User;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import java.util.*;

@Component
public class TokenProvider {

    private static final String secret="qwer123456";

    private static final Long expiration = 7200L;

    public String generateToken(User user) {
        // 从认证对象中获取用户信息和权限
        String username = user.getUsername();
        //Collection<? extends GrantedAuthority> authorities = user.getAuthorities();

        // 生成令牌
        Date now = new Date();
        Date expiryDate = new Date(now.getTime() + expiration * 1000);

        // 使用密钥和算法创建令牌
        String token = Jwts.builder()
                .setSubject(username)
                //.claim("authorities", authorities.stream()
                //        .map(GrantedAuthority::getAuthority)
                //        .collect(Collectors.toList()))
                .setIssuedAt(now)
                .setExpiration(expiryDate)
                .signWith(SignatureAlgorithm.HS512, secret)
                .compact();

        return token;
    }

    public boolean validateToken(String token, UserDetails userDetails) {
        final String username = getUsernameFromToken(token);
        //return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
        return username!=null;
    }

    public String getUsernameFromToken(String token) {
        return Jwts.parser()
                .setSigningKey(secret)
                .parseClaimsJws(token)
                .getBody()
                .getSubject();
    }

    public boolean isTokenExpired(String token) {
        final Date expirationDate = Jwts.parser()
                .setSigningKey(secret)
                .parseClaimsJws(token)
                .getBody()
                .getExpiration();
        return expirationDate.before(new Date());
    }

    public boolean validateToken(String token) {
        // 验证令牌的逻辑,检查签名、过期时间等
        return true;
    }

    public Authentication getAuthentication(String token) {
        // 根据令牌获取用户身份验证信息的逻辑,通常是解析令牌中的信息并构建Authentication对象
        // 解析令牌并获取用户信息
        Claims claims = Jwts.parser()
                .setSigningKey(secret)
                .parseClaimsJws(token)
                .getBody();

        String username = claims.getSubject();

        //解析用户权限
        //List<String> authorities = (List<String>) claims.get("authorities");
        //
        //Collection<? extends GrantedAuthority> grantedAuthorities =
        //        authorities.stream()
        //                .map(SimpleGrantedAuthority::new)
        //                .collect(Collectors.toList());

        // 创建认证对象
        return new UsernamePasswordAuthenticationToken(username, null, null);
    }
}

2、jwt过滤器

package cn.cdjs.config.security;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @Author Jiangjinlong
 * @Date 2023/9/11 17:19
 * @PackageName:cn.cdjs.config.security
 * @ClassName: TokenAuthenticationFilter
 * @Description: TODO
 */
public class TokenAuthenticationFilter extends OncePerRequestFilter {
    private final TokenProvider tokenProvider;

    public TokenAuthenticationFilter(TokenProvider tokenProvider) {
        this.tokenProvider = tokenProvider;
    }
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String token = extractTokenFromRequest(request);

        if (token != null && tokenProvider.validateToken(token,null)) {
            Authentication authentication = tokenProvider.getAuthentication(token);
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }

        filterChain.doFilter(request, response);
    }

    private String extractTokenFromRequest(HttpServletRequest request) {
        // 从请求头中获取Authorization头的值
        String authorizationHeader = request.getHeader("Authorization");

        // 检查Authorization头是否存在且以"Bearer "开头
        if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
            // 提取令牌部分,去掉"Bearer "前缀
            return authorizationHeader.substring(7);
        }

        return null; // 如果未找到令牌,则返回null
    }
}

三、验证

用户登录认证
在这里插入图片描述
使用令牌访问受保护资源
在这里插入图片描述

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

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

相关文章

22.1 JavaScript 基础

1. JavaScript 1.1 简介 JavaScript(简称js): 是一种广泛应用于网页开发的脚本语言. 它被用于增强网页的交互性和动态性, 可以让开发者对网页进行操作和控制. JavaScript可用于处理用户输入, 改变网页的内容, 动态加载数据, 创建动画效果等. 它在现代的Web开发中扮演着至关重…

【GIS】栅格转面报错:ERROR 000864输入栅格: 输入不在定义的属性域内。 ERROR 000863: 无效的 GP 数据类型

问题: 栅格转面(矢量)时,ArcGIS窗口显示:ERROR 000864输入栅格: 输入不在定义的属性域内。 ERROR 000863: 无效的 GP 数据类型. 原因: 栅格转面时输入的栅格数据集的字段必须是整型. 解决办法: 使用Spatial Analyst中的转为整型工具,将栅格数据转为整型后再进行栅格转面的操作…

pycharm增加新的编译器

安装了python2和3的电脑上&#xff0c;使用pycharm时候&#xff0c;最好将2和3都加入其编译器。 方法&#xff1a; 1、File-settings... 2、如图选择&#xff0c;然后点击加号&#xff0c;添加python2或者3的exe

通常用哪些软件做数据可视化大屏?

一般就两种&#xff0c;一种是可视化大屏编辑软件&#xff0c;另一种则是BI系统&#xff08;BI数据可视化工具&#xff09;。考虑到数据来源多、数据量大以及数据分析效率、直观易懂性等实实在在的客观问题&#xff0c;建议采用BI系统来制作数据可视化大屏。 BI系统做可视化大…

首个国家级元宇宙计划发布,和数集团迎来赛道发展新机遇

近日&#xff0c;工业和信息化部、教育部、文化和旅游部、国务院国资委、国家广播电视总局办公厅五部门联合印发《元宇宙产业创新发展三年行动计划&#xff08;2023-2025年&#xff09;》&#xff08;以下简称《计划》&#xff09;&#xff0c;其中在发展目标中提到要培育3-5家…

手把手教你搭建WordPress博客网站并发布至公网

文章目录 概述前置准备1 安装数据库管理工具1.1 安装图形图数据库管理工具&#xff0c;SQL_Front 2 创建一个新数据库2.1 创建数据库2.2 为数据库创建一个用户 3 安装PHP7.44. 创建一个新站点4.1 创建站点根目录4.2 访问WordPress官网&#xff0c;下载最新版本的压缩包4.3 创建…

Linux之yum/git的使用

目录 一、yum 1、关于yum 2、yum的操作 ①、yum list ②、yum install ③、yum remove 二、git 1、Linux中连接gitee 2、git的操作 ①git add [文件] ②git commit -m "提交日志" ③git push 3、可能出现的问题 ①配置用户名、邮箱 ②出现提交冲突 ③…

2023在家做什么副业兼职能赚钱?分享几种线上兼职副业!

现代社会&#xff0c;很多人都渴望能够在家中轻松自由地赚取一份收入。无论是为了照顾孩子、追求个人兴趣还是摆脱繁忙的通勤&#xff0c;都有越来越多的人开始探索如何在家赚钱。幸运的是&#xff0c;随着技术的进步和互联网的普及&#xff0c;我们现在有更多的机会去实现这个…

halcon对图片进行处理基础

实例图片 C:\Users\Public\Documents\MVTec\HALCON-19.11-Progress\examples\images*读取图片 read_image (Image1, D:/c/image/1.png) *读取大小 get_image_size(Image1,Width, Height) *创建窗口 dev_close_window() dev_open_window(0, 0, Width, Height, black, WindowHan…

D. Cyclic Operations Codeforces Round 897 (Div. 2)

Problem - D - Codeforces 题目大意&#xff1a;有一个长度为n的数组a&#xff0c;每次操作可以选取一个长度为k的所有数互不相同的数组b&#xff0c;令a[bi]b[i%k1]&#xff0c;问能否将一个全为零的数组通过任意次操作得到a 1<k<n<1e5 思路&#xff1a;通过上述操…

如何用Jmeter编写脚本压测

随着商业业务不断扩张&#xff0c;调用adsearch服务频率越来越高&#xff0c;所以这次想做个压测&#xff0c;了解目前多少并发量可以到达adsearch服务的界值。 这次选用的jmeter压测工具&#xff0c;压测思路如图&#xff1a; 同时&#xff0c;我也准备了一份软件测试面试视频…

Windows C++ VS2022 OpenVINO 物体检测 Demo

准备工作&#xff1a; 1、下载opencv 地址&#xff1a;Releases - OpenCV 我下载的是opencv-4.5.5&#xff0c;存放的路径为&#xff1a; 2、下载OpenVino 地址&#xff1a;https://storage.openvinotoolkit.org/repositories/openvino/packages/2023.0.1/ 我存放的路径为…

蓝牙运动耳机哪款好用、最好用的运动耳机推荐

运动耳机现如今可谓是备受热捧的运动潮流单品&#xff0c;消费者对于耳机的需求实际上非常多元化。一款出色的运动耳机不仅要满足基本的运动需求&#xff0c;还需要具备丰富的使用功能&#xff0c;这直接决定了耳机的附加价值。接下来&#xff0c;我将向大家推荐5款佩戴舒适、牢…

HarmonyOS应用开发—资源分类与访问

应用开发过程中&#xff0c;经常需要用到颜色、字体、间距、图片等资源&#xff0c;在不同的设备或配置中&#xff0c;这些资源的值可能不同。 应用资源&#xff1a;借助资源文件能力&#xff0c;开发者在应用中自定义资源&#xff0c;自行管理这些资源在不同的设备或配置中的表…

idea装载jerbel以及文件上传下载

一、JRebel 1.1 Jrebel介绍 JRebel是一个Java开发工具&#xff0c;它是一款用于实时代码重载的插件。它的主要功能是在不重新启动应用程序的情况下&#xff0c;将修改后的Java代码实时应用到正在运行的应用程序中&#xff0c;从而加快开发周期&#xff0c;提高开发效率。 实…

算法通关村第十九关:青铜-动态规划是怎么回事

青铜挑战-动态规划是怎么回事 动态规划&#xff08;简称DP&#xff0c;Dynamic Programming&#xff09;&#xff1a;最热门、最重要的算法之一。面试中大量出现&#xff0c;整体偏难。 1. 热身&#xff1a;重复计算和记忆化搜索&#xff08;如何说一万次"我爱你"&…

Windows驱动开发(一)

1. 引言 很难为术语 “驱动程序”提供一个精确的定义。 就最基本的意义而言&#xff0c;驱动程序是一个软件组件&#xff0c;可让操作系统和设备彼此通信。 例如&#xff0c;假设应用程序需要从设备中读取某些数据。 应用程序会调用由操作系统实现的函数&#xff0c;操作系统…

WPF——Control与Template理解

文章目录 一、前言二、控件三、模板3.1 DataTemplate3.2 ControlTemplate3.3 ContentPresenter 四、结语 一、前言 最近又翻看了下刘铁猛的《深入浅出WPF》&#xff0c;发现对模板章节中的部分内容有了更深的体会&#xff0c;所以写篇文扯扯。 文章标题是Control与Template&a…

画流程图用什么软件好?安利这几款

画流程图用什么软件好&#xff1f;画流程图是一项非常重要的技能&#xff0c;它可以帮助我们更好地规划和管理工作流程&#xff0c;提高工作效率。在现代的企业中&#xff0c;流程图已经成为了不可或缺的一部分&#xff0c;它可以用来描述各种业务流程、流程控制、组织结构等等…

数据治理实战步骤

写在前面:数据治理是数字化转型的基础,是数字要素流通的首要任务。但是面对不同的情况,数据治理的手段不同。 数据治理专员要转换思想,数据治理中单靠技术、软件是不行的,比如一些单位认为数据治理平台是万能的,直接上平台一般是做不好的,需基于企业的组织文化、愿景等对…