基于Spring Boot 3 + Spring Security6 + JWT + Redis实现登录、token身份认证

基于Spring Boot3实现Spring Security6 + JWT + Redis实现登录、token身份认证。

  • 用户从数据库中获取。
  • 使用RESTFul风格的APi进行登录。
  • 使用JWT生成token。
  • 使用Redis进行登录过期判断。
  • 所有的工具类和数据结构在源码中都有。

系列文章指路👉
系列文章-基于Vue3创建前端项目并引入、配置常用的库和工具类

项目源码👉
/shijizhe/boot-test

文章目录

    • 依赖版本
    • 原理
    • 代码结构
      • security 配置
      • 用户登录、注册controller,用户服务
      • 用到的工具类
    • 注册 AuthController.register
    • 登录
      • 1.登录API:AuthController.login
      • 2. 登录过滤器:继承UsernamePasswordAuthenticationFilter
      • 3.身份认证:实现AuthenticationProvider
      • 4.从数据库中查询用户信息:实现UserDetailsService
      • 5. Security配置: 使用注解@EnableWebSecurity
    • token身份认证
      • 1. token身份认证过滤器: OncePerRequestFilter
    • UserAuthUtils
      • getUserId
    • 用户登出
      • 实现LogoutSuccessHandler
      • 修改Security配置 : YaSecurityConfig
    • 下一步的计划
    • 参考文章

依赖版本

  • Spring Boot 3.0.6
  • Spring Security 6.0.3

原理

这张图大家已经估计已经看过很多次了。
原理
实现登录认证的过程,其实就是对上述的类按照自己的需求进行自定义扩展的过程。具体不多讲了,别的文章里讲得比我透彻。

show you my code.

代码结构

security 配置

在这里插入图片描述

用户登录、注册controller,用户服务

在这里插入图片描述

用到的工具类

在这里插入图片描述

注册 AuthController.register

将用户密码使用BCrypt加密存储。

    @PostMapping("/register")@Operation(summary = "register", description = "用户注册")public Object register(@RequestBody @Valid UserRegisterDTO userRegisterDTO) {YaUser userById = userService.getUserById(userRegisterDTO.getUserId());if(Objects.nonNull(userById)){return BaseResult.fail("用户id已存在");}try {BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();YaUser yaUser = UserRegisterMapper.INSTANCE.registerToUser(userRegisterDTO);yaUser.setUserPassword(encoder.encode(userRegisterDTO.getUserPassword()));userService.insertUser(yaUser);return BaseResult.success("用户注册成功");}catch (Exception e){return BaseResult.fail("用户注册过程中遇到异常:" + e);}}

登录

1.登录API:AuthController.login

我们使用RESTFul风格的API来代替表单进行登录。这个接口只是提供一个Swagger调用登录接口的入口,实际逻辑由Filter控制。
在这里插入图片描述

2. 登录过滤器:继承UsernamePasswordAuthenticationFilter

拦截指定的登录请求,交给AuthenticationProvider处理。对Provider返回的登录结果进行处理。

  • 通过指定filterProcessesUrl,指定登录接口的路径。
  • 登录失败,将异常信息返回前端。
  • 登录成功,通过JwtUtils生成token,放入响应header中。并将token用户信息(json字符串)存入Redis中。
  • 通过JwtUtils生成token设置为永不过期,存入Redis的token过期时间设置为30分钟,以便后边做登录过期的判断。
/*** <p>*  拦截登陆过滤器* </p>** @author Ya Shi* @since 2024/3/21 16:20*/
@Slf4j
public class YaLoginFilter extends UsernamePasswordAuthenticationFilter {private final RedisUtils redisUtils;private final Long expiration;public YaLoginFilter(AuthenticationManager authenticationManager, RedisUtils redisUtils, Long expiration) {this.expiration = expiration;this.redisUtils = redisUtils;super.setAuthenticationManager(authenticationManager);super.setPostOnly(true);super.setFilterProcessesUrl("/auth/login");super.setUsernameParameter("userId");super.setPasswordParameter("userPassword");}@SneakyThrows@Overridepublic Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {log.info("YaLoginFilter authentication start");// 数据是通过 RequestBody 传输UserLoginDTO user = JSON.parseObject(request.getInputStream(), StandardCharsets.UTF_8, UserLoginDTO.class);return super.getAuthenticationManager().authenticate(new UsernamePasswordAuthenticationToken(user.getUserId(), user.getUserPassword()));}@Overrideprotected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response,FilterChain chain,Authentication authResult) {log.info("YaLoginFilter authentication success: {}", authResult);// 如果验证成功, 就生成Token并返回UserDetails userDetails = (UserDetails) authResult.getPrincipal();String userId = userDetails.getUsername();String token = JwtUtils.generateToken(userId);response.setHeader(TOKEN_HEADER, TOKEN_PREFIX + token);// 将token存入Redis中redisUtils.set(REDIS_KEY_AUTH_TOKEN + userId, token, expiration);log.info("YaLoginFilter authentication end");// 将UserDetails存入redis中redisUtils.set(REDIS_KEY_AUTH_USER_DETAIL + userId, JSON.toJSONString(userDetails), 1, TimeUnit.DAYS);ServletUtils.renderResult(response, new BaseResult<>(ResultEnum.SUCCESS.code, "登陆成功"));log.info("YaLoginFilter authentication end");}/*** 如果 attemptAuthentication 抛出 AuthenticationException 则会调用这个方法*/@Overrideprotected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,AuthenticationException failed) throws IOException {log.info("YaLoginFilter authentication failed: {}", failed.getMessage());ServletUtils.renderResult(response, new BaseResult<>(ResultEnum.FAILED_UNAUTHORIZED.code, "登陆失败:" + failed.getMessage()));}

3.身份认证:实现AuthenticationProvider

调用UserDetailsService查询用户的账户、权限信息与登录接口输入的账户、密码对比。认证通过则返回用户信息。

/*** <p>*  自定义认证* </p>** @author Ya Shi* @since 2024/3/21 15:00*/@Component
public class YaAuthenticationProvider implements AuthenticationProvider {@AutowiredYaUserDetailService userDetailService;@AutowiredPasswordEncoder passwordEncoder;@Overridepublic Authentication authenticate(Authentication authentication) throws AuthenticationException {// 获取用户输入的用户名和密码String username = authentication.getName();String password = authentication.getCredentials().toString();UserDetails userDetails = userDetailService.loadUserByUsername(username);boolean matches = passwordEncoder.matches(password, userDetails.getPassword());if(!matches){throw new AuthenticationException("User password error."){};}return new UsernamePasswordAuthenticationToken(userDetails, userDetails.getPassword(), userDetails.getAuthorities());}@Overridepublic boolean supports(Class<?> authentication) {return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);}
}

4.从数据库中查询用户信息:实现UserDetailsService

从数据库中查询出用户的信息,供AuthenticationProvider登录认证时使用。

  • 用户权限这块,目前还没用到,可以忽略。用户鉴权可能后边会单独补上。
  • 为什么这里没先从Redis取用户信息?
    1. 如果权限或者用户信息变更这里取不到
    2. Redis里不建议存储用户密码。
/*** <p>*  继承UserDetailsService,实现自定义登陆认证* </p>** @author Ya Shi* @since 2024/3/19 11:32*/
@Service
public class YaUserDetailService implements UserDetailsService {@AutowiredUserService userService;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {YaUser user = userService.getUserById(username);if(Objects.isNull(user)){throw new UsernameNotFoundException("User not Found.");}List<YaUserRole> roles = userService.listRoleById(username);List<GrantedAuthority> authorities = new ArrayList<>(roles.size());roles.forEach( role -> authorities.add(new SimpleGrantedAuthority(role.getRoleId())));return new User(username, user.getUserPassword(), authorities);}
}

5. Security配置: 使用注解@EnableWebSecurity

  • 注意:Spring Security 6 配置不再继承adapterextends WebSecurityConfigurerAdapter,而是使用@EnableWebSecurity
  • YaTokenFilter是token身份认证过滤器,每次请求都会拦截,然后校验请求header中的token,这个下面会讲。
  • 配置了身份认证过滤器以后,每个请求都会被拦截,即使是在过滤链中配置了permitAll(),还是会返回请求403.
    1. 因此,针对匿名请求、静态资源和swagger请求,在WebSecurityCustomizer中配置WebSecurity.ignoring,相当于直接绕过所有的Filter
    2. 针对登录和注册请求,在身份过滤器中额外配置白名单,单独放行。
  • 自己学习的过程中,很多文章没有按照代码执行顺序去讲,登录和身份认证也是混着讲的,导致整个登录认证的流程理解起来有些困难。
/*** <p>*  Spring Security 配置文件* </p>** @author Ya Shi* @since 2024/2/29 11:27*/
@Configuration
@EnableWebSecurity // 开启网络安全注解
public class YaSecurityConfig {@Autowiredprivate AuthenticationConfiguration authenticationConfiguration;@Autowiredprivate RedisUtils redisUtils;@Value("${ya-app.auth.jwt.expiration:1800}")private Long expiration;@Beanpublic SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {http// 禁用basic明文验证.httpBasic().disable()// 禁用csrf保护.csrf().disable()// 禁用session.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))// 身份认证过滤器.authenticationManager(authenticationManager(authenticationConfiguration)).authenticationProvider(new YaAuthenticationProvider()).authorizeHttpRequests(authorizeHttpRequests ->authorizeHttpRequests// 允许OPTIONS请求访问.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()// 允许登录/注册接口访问.requestMatchers(HttpMethod.POST, "/auth/login").permitAll().requestMatchers(HttpMethod.POST, "/auth/register").permitAll()// 允许匿名接口访问.requestMatchers("/anon/**").permitAll()// 允许swagger访问.requestMatchers("/swagger-ui/**").permitAll().requestMatchers("/doc.html/**").permitAll().requestMatchers("/v3/api-docs/**").permitAll().requestMatchers("/webjars/**").permitAll().anyRequest().authenticated()).addFilterAt(new YaLoginFilter(authenticationManager(authenticationConfiguration), redisUtils, expiration), UsernamePasswordAuthenticationFilter.class)// 让校验Token的过滤器在身份认证过滤器之前.addFilterBefore(new YaTokenFilter(redisUtils, expiration), YaLoginFilter.class)// 禁用默认登出页.logout().disable();return http.build();}@Beanpublic AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {return config.getAuthenticationManager();}@Beanpublic WebSecurityCustomizer webSecurityCustomizer() {return (web) -> web.ignoring().requestMatchers("/webjars/**").requestMatchers("/swagger-ui/**", "/doc.html/**", "/v3/api-docs/**").requestMatchers("/anon/**");}/*** 使用BCrypt加密密码*/@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}
}

token身份认证

1. token身份认证过滤器: OncePerRequestFilter

  • 对于注册、登录请求,直接放行。
  • 认证失败的几种情况:
    1. 未登录: 未携带token
    2. 凭证异常: 携带错误token
    3. 登录过期: 携带正确的token,但是token在Redis中不存在
    4. 账号在别处登录: 携带正确的token,但是token与Redis中的token不一致。
  • token认证成功后,重新设置Redis中的token的有效时间,实现token续期。查询Redis中的用户信息,如果没有,使用UserDetailsService的服务重新查询出信息,存入缓存中。
    *调用 SecurityContextHolder.getContext().setAuthentication()将用户信息存入Security上下文中,完成身份认证。
/*** <p>*  每次请求过滤token* </p>** @author Ya Shi* @since 2024/3/21 16:52*/
@Slf4j
public class YaTokenFilter extends OncePerRequestFilter {private final RedisUtils redisUtils;private final Long expiration;private static final Set<String> WHITE_LIST = Stream.of("/auth/register","/auth/login").collect(Collectors.toSet());public YaTokenFilter(RedisUtils redisUtils, Long expiration) {this.redisUtils = redisUtils;this.expiration = expiration;}@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {log.info("YaTokenFilter doFilterInternal start");final String authorization = request.getHeader(AuthConstants.TOKEN_HEADER);log.info("YaTokenFilter ya-auth-token: {}", authorization);// 白名单if (WHITE_LIST.contains(request.getServletPath())) {chain.doFilter(request, response);return;}// 1.请求头中没有携带tokenif (StrUtil.isBlank(authorization)) {ServletUtils.renderResult(response, new BaseResult<>(ResultEnum.FAILED_UNAUTHORIZED));return;}// 携带tokenfinal String token = authorization.replace(AuthConstants.TOKEN_PREFIX, "");String userId;// 2.提供的token异常try {userId =  JwtUtils.extractUserId(token);}catch (Exception e){log.error("YaTokenFilter doFilterInternal 解析jwt异常:{}", e.toString());ServletUtils.renderResult(response, new BaseResult<>(ResultEnum.FAILED_UNAUTHORIZED.code, "凭证异常"));return;}String redisToken = redisUtils.getString(AuthConstants.REDIS_KEY_AUTH_TOKEN + userId);// 3.token过期if(StrUtil.isBlank(redisToken)){ServletUtils.renderResult(response, new BaseResult<>(ResultEnum.FAILED_UNAUTHORIZED.code, "登录已过期,请重新登录过期。"));return;}// 4.提供的token是合法的,但是redis中的token又被使用登录功能重新刷新了一下,导致不一致。if(!Objects.equals(redisToken, token)){ServletUtils.renderResult(response, new BaseResult<>(ResultEnum.FAILED_UNAUTHORIZED.code, "账号在别处登陆。"));return;}// token续期redisUtils.set(REDIS_KEY_AUTH_TOKEN + userId, token, expiration);// 获取用户信息和权限String userDetailStr =  redisUtils.getString(AuthConstants.REDIS_KEY_AUTH_USER_DETAIL + userId);UserDetails userDetails;if(Objects.isNull(userDetailStr)){userDetails = yaUserDetailService().loadUserByUsername(userId);redisUtils.set(REDIS_KEY_AUTH_USER_DETAIL + userId, JSON.toJSONString(userDetails), 1, TimeUnit.DAYS);}else{userDetails = initUser(userDetailStr);}SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(userDetails, userDetails.getPassword(), userDetails.getAuthorities()));log.info("YaTokenFilter doFilterInternal end");chain.doFilter(request, response);}private YaUserDetailService yaUserDetailService(){return SpringContextUtils.getBean(YaUserDetailService.class);}private User initUser(String userJsonStr){JSONObject userJson = JSON.parseObject(userJsonStr);String userId = userJson.getString("username");JSONArray authArray = userJson.getJSONArray("authorities");List<GrantedAuthority> authorities = new ArrayList<>(authArray.size());for(int i=0; i< authArray.size();i++){JSONObject authObj = authArray.getJSONObject(i);authorities.add(new SimpleGrantedAuthority(authObj.getString("authority")));}return new User(userId, "[PROTECTED]", authorities);}}

UserAuthUtils

已经登录的用户,可以从Security的上下文中获取用户的账号、基本信息、权限等。可以将其封装为工具类。因为练手的用户表较为简单,也没有部分、员工、角色、权限等概念,因此仅封装了getUserId做抛砖引玉的作用。可以根据实际使用自己封装更多的方法。

getUserId

public static String getUserId() {if (Objects.isNull(SecurityContextHolder.getContext().getAuthentication())) {return null;}UserDetails userDetails = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();if (Objects.isNull(userDetails)) {return null;}return userDetails.getUsername();}

用户登出

JWT本身是无状态的,但是我们后端将jwt存到redis里,相当于手动使JWT变得有状态了。那么我们在登出时就需要清空Redis中的jwt。

实现LogoutSuccessHandler

/*** <p>*  登出成功* </p>** @author Ya Shi* @since 2024/3/28 10:47*/
@Slf4j
public class YaLogoutSuccessHandler implements LogoutSuccessHandler {private final RedisUtils redisUtils;public YaLogoutSuccessHandler(RedisUtils redisUtils) {this.redisUtils = redisUtils;}@Overridepublic void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {final String authorization = request.getHeader(AuthConstants.TOKEN_HEADER);// 1.请求头中没有携带tokenif (StrUtil.isBlank(authorization)) {ServletUtils.renderResult(response, BaseResult.successWithMessage("没有登录信息,无需退出"));return;}// 携带tokenfinal String token = authorization.replace(AuthConstants.TOKEN_PREFIX, "");String userId;// 2.提供的token异常try {userId =  JwtUtils.extractUserId(token);}catch (Exception e){log.error("YaLogoutHandler logout 解析jwt异常:{}", e.toString());ServletUtils.renderResult(response, new BaseResult<>(ResultEnum.FAILED_UNAUTHORIZED.code, "凭证异常"));return;}// 清空RedisredisUtils.delete(REDIS_KEY_AUTH_TOKEN + userId);log.info("YaLogoutSuccessHandler onLogoutSuccess");ServletUtils.renderResult(response, BaseResult.successWithMessage("退出登录成功"));}
}

修改Security配置 : YaSecurityConfig

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {http  ... // 前面的配置忽略.logout().logoutUrl("/auth/logout").logoutSuccessHandler(new YaLogoutSuccessHandler(redisUtils));return http.build();
}

下一步的计划

  • 用户鉴权
  • 排查permitAll()失效的问题。
  • 做一个练手用的用户中心,提供统一的注册、登录、认证、鉴权服务,供其他的应用调用。
  • 把前期已经实现的基础的配置和工具类封装为jar包,供以后的程序使用。

参考文章

  • SpringBoot3.0 + SpringSecurity6.0+JWT
  • SpringSecurity (3) SpringBoot + JWT 实现身份认证和权限验证
  • Spring Security 6.0(spring boot 3.0) 下认证配置流程
  • SpringSecurity的PermitAll、WebSecurityCustomizer和授权
  • springsecurity的http.permitall与web.ignoring的区别

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

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

相关文章

MongoDB内存过高问题分析解决

告警 公司有个3.2.7版本的mongo复制集&#xff0c;最近几天频繁告警内存过高。 服务器配置16C64G内存。mongo备节点内存使用到55G&#xff0c;触发告警。 以下内容基于3.2.7版本&#xff0c;3.2.7版本已经太老&#xff0c;很多后来的命令和配置&#xff0c;3.2.7都没有。 …

【黑马头条】-day04自媒体文章审核-阿里云接口-敏感词分析DFA-图像识别OCR-异步调用MQ

文章目录 day4学习内容自媒体文章自动审核今日内容 1 自媒体文章自动审核1.1 审核流程1.2 内容安全第三方接口1.3 引入阿里云内容安全接口1.3.1 添加依赖1.3.2 导入aliyun模块1.3.3 注入Bean测试 2 app端文章保存接口2.1 表结构说明2.2 分布式id2.2.1 分布式id-技术选型2.2.2 雪…

Unity2018发布安卓报错 Exception: Gradle install not valid

Unity2018发布安卓报错 Exception: Gradle install not valid Exception: Gradle install not valid UnityEditor.Android.GradleWrapper.Run (System.String workingdir, System.String task, System.Action1[T] progress) (at <c67d1645d7ce4b76823a39080b82c1d1>:0) …

Prometheus +Grafana +node_exporter可视化监控Linux虚机

1、介绍 待补充 2、架构图 待补充 Prometheus &#xff1a;主要是负责存储、抓取、聚合、查询方面。 node_exporter &#xff1a;主要是负责采集物理机、中间件的信息。 3、搭建过程 配置要求&#xff1a;1台主服务器 n台从服务器 &#xff08;被监控的linux虚机&am…

MoneyPrinterTurbo搭建详细流程(Linux)及常见问题

先附上链接: MoneyPrinterTurbohttps://github.com/harry0703/MoneyPrinterTurboMoneyPrinterTurbo是一款合成视频的软件。 你只需要提供一个主题或者关键字,就可以全自动生成视频文案、视频素材、视频字幕、视频背景音乐,然后合成一个高清的短视频。 接下来讲解详细的搭…

macOS 13 Ventura (苹果最新系统) v13.6.6正式版

macOS 13 Ventura是苹果电脑的全新操作系统&#xff0c;它为用户带来了众多引人注目的新功能和改进。该系统加强了FaceTime和视频通话的体验&#xff0c;同时优化了邮件、Safari浏览器和日历等内置应用程序&#xff0c;使其更加流畅、快速和安全。特别值得一提的是&#xff0c;…

测试人进阶技能:单元测试报告应用指南

为什么需要单元测试 从产品角度而言&#xff0c;常规的功能测试、系统测试都是站在产品局部或全局功能进行测试&#xff0c;能够很好地与用户的需要相结合&#xff0c;但是缺乏了对产品研发细节&#xff08;特别是代码细节的理解&#xff09;。 从测试人员角度而言&#xff0…

MySQL索引18连问,谁能顶住

前言 过完这个节&#xff0c;就要进入金银季&#xff0c;准备了 18 道 MySQL 索引题&#xff0c;一定用得上。 作者&#xff1a;感谢每一个支持&#xff1a; github 1. 索引是什么 索引是一种数据结构&#xff0c;用来帮助提升查询和检索数据速度。可以理解为一本书的目录&…

Hadoop面试重点

文章目录 1. Hadoop 常用端口号2.Hadoop特点3.Hadoop1.x、2.x、3.x区别 1. Hadoop 常用端口号 hadoop2.xhadoop3.x访问HDFS 端口500709870访问 MR 执行情况端口80888088历史服务器1988819888客户端访问集群端口90008020 2.Hadoop特点 高可靠&#xff1a;Hadoop底层维护多个数…

Spring boot 发送文本邮件 和 html模板邮件

Spring boot 发送文本邮件 和 html模板邮件 提示&#xff1a;这里使用 spring-boot-starter-mail 发送文本邮件 和 html模板邮件 文章目录 Spring boot 发送文本邮件 和 html模板邮件一、开启QQ邮箱里的POP3/SMTP服务①&#xff1a;开启步骤 二、简单配置①&#xff1a;引入依赖…

8.均值滤波

1 简介 均值滤波是一种低通滤波&#xff0c;它可以有效过滤图片中的椒盐噪声&#xff0c;但是副作用也同样明显&#xff0c;会使图片的边缘过于模糊。   均值滤波的卷积核系数均为1。   这里最终重复一下算法实现以及验证的步骤&#xff1a;     1.MATLAB读取图片并转化…

macOS Sonoma如何查看隐藏文件

在使用Git进行项目版本控制时&#xff0c;我们可能会遇到一些隐藏文件&#xff0c;比如.gitkeep文件。它通常出现在Git项目的子目录中&#xff0c;主要作用是确保空目录也可以被跟踪。 终端命令 在尝试查看.gitkeep文件时&#xff0c;使用Terminal命令来显示隐藏文件 default…

【Pt】马灯贴图绘制过程 03-制作油渍、积尘效果

目录 效果 一、制作油渍效果 1.1 基本油渍 1.2 流淌的油渍痕迹 二、制作浮尘效果 三、制作积尘效果 效果 一、制作油渍效果 1.1 基本油渍 将上篇制作的“锈迹_深色”和“锈迹_浅色”两个文件夹再次合并为一个文件夹 这里就命名为“锈迹” 添加一个填充图层 设置Base …

小程序利用WebService跟asp.net交互过程发现的问题并处理

最近在研究一个项目&#xff0c;用到asp.net跟小程序交互&#xff0c;简单的说就是小程序端利用wx.request发起请求。获取asp.net 响应回来的数据。但经常会报错。点击下图的测试按钮 出现如下错误&#xff1a; 百思不得其解&#xff0c;试了若干方法&#xff0c;都不行。 因为…

企业网站建设的方法的相关问题的解决办法的问题

现在市场上比较大的公司都建立了自己的企业网站&#xff0c;比如华为、小米等&#xff0c;在他们的企业网站中&#xff0c;可以充分展示自己产品的优势&#xff0c;介绍公司的优质服务。 这都是让顾客改变购买想法的重要因素。 现在互联网发达了&#xff0c;很多人在购买产品的…

智慧工地安全生产与风险预警大平台的构建,需要哪些技术?

随着科技的不断发展&#xff0c;智慧工地已成为现代建筑行业的重要发展趋势。智慧工地方案是一种基于先进信息技术的工程管理模式&#xff0c;旨在提高施工效率、降低施工成本、保障施工安全、提升施工质量。一般来说&#xff0c;智慧工地方案的构建&#xff0c;需要通过集成物…

计算机网络:物理层 - 信道极限容量

计算机网络&#xff1a;物理层 - 信道极限容量 实际信道中的数字信号奈式准则香农公式练习 实际信道中的数字信号 信号在传输过程中会受到各种因素的影响&#xff0c;如图所示&#xff1a; 这是一个数字信号&#xff0c;当它通过实际的信道后&#xff0c;波形会产生失真&#…

Linux学习:进程(3)与 环境变量

目录 1. 进程的优先级1.1 什么是进程的优先级1.2 优先级的具体表示与查看方式 2. 进程的切换与调度2.1 切换2.2 调度 3. 环境变量3.1 main参数/命令行参数3.2 什么是环境变量3.3 环境变量的使用与特性3.5 本地变量与环境变量的脚本配置文件 1. 进程的优先级 在计算机运行的过程…

【JavaScript】数组 ② ( JavaScript 数组索引 | JavaScript 遍历数组 | 使用 for 循环遍历数组 )

文章目录 一、JavaScript 数组索引1、数组索引2、数组索引 - 代码示例 二、JavaScript 遍历数组1、使用 for 循环遍历数组2、使用 for 循环遍历数组 - 代码示例 一、JavaScript 数组索引 1、数组索引 在 JavaScript 中 , 数组 的 " 索引 " 又称为 " 下标 "…

电脑突然死机怎么办?

死机是电脑常见的故障问题&#xff0c;尤其是对于老式电脑来说&#xff0c;一言不合电脑画面就静止了&#xff0c;最后只能强制关机重启。那么你一定想知道是什么原因造成的吧&#xff0c;一般散热不良最容易让电脑死机&#xff0c;还有系统故障&#xff0c;比如不小心误删了系…