RuoYi-Vue 最新 SpringBoot3 前后端分离版本源码分析

news2024/11/15 8:05:03

RuoYi-Vue 最新 SpringBoot3 前后端分离版本源码分析

  • RuoYi-Vue 本地环境部署
  • 权限管理
    • SpringSecurity 配置
    • 登录接口(认证管理)
      • Authentication 认证
      • token的生成
    • 权限控制
  • 异步任务管理
  • 操作日志
  • 数据权限

RuoYi-Vue 本地环境部署

直接去 gitee 上拉取最新版本即可,分支切换到 springboo3 就可以了,本地部署也非常简单,只需要更改数据库和 Redis 配置即可

在线体验

若依官网:http://ruoyi.vip
演示地址:http://vue.ruoyi.vip
代码下载:https://gitee.com/y_project/RuoYi/tree/springboot3/

在这里插入图片描述

权限管理

RuoYi-Vue 最新 SpringBoot3 前后端分离版本是使用 SpringSecurity 来进行安全认证和权限控制的,从 maven 依赖可以看到版本是 SpringSecurity6.3.0

SpringSecurity 配置

SpringSecurity 的配置类是实现安全控制的核心部分,开启 SpringSecurity 各种功能,以确保 Web 应用程序的安全性,包括认证、授权、回话管理、过滤器添加等.

// 表示开启方法级别的权限控制=> @PreAuthorize
@EnableMethodSecurity(prePostEnabled = true, securedEnabled = true)
@Configuration
public class SecurityConfig {
   //身份验证实现
   @Bean
   public AuthenticationManager authenticationManager() {
       DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
       // 在UserDetailsServiceImpl#loadUserByUsername方法进行认证
       daoAuthenticationProvider.setUserDetailsService(userDetailsService);
       daoAuthenticationProvider.setPasswordEncoder(bCryptPasswordEncoder());
       return new ProviderManager(daoAuthenticationProvider);
   }
  @Bean
  protected SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
   return httpSecurity
       // CSRF禁用,因为不使用session
       .csrf(csrf -> csrf.disable())
       // 禁用HTTP响应标头
       .headers((headersCustomizer) -> {
           headersCustomizer.cacheControl(cache -> cache.disable()).frameOptions(options -> options.sameOrigin());
       })
       // 认证失败处理类(认证失败的进入unauthorizedHandler类处理)
       .exceptionHandling(exception -> exception.authenticationEntryPoint(unauthorizedHandler))
       // 基于token,所以不需要session
       .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
       // 注解标记允许匿名访问的url
       .authorizeHttpRequests((requests) -> {
           permitAllUrl.getUrls().forEach(url -> requests.requestMatchers(url).permitAll());
           // 对于登录login 注册register 验证码captchaImage 允许匿名访问
           requests.requestMatchers("/login", "/register", "/captchaImage").permitAll()
               // 静态资源,可匿名访问
               .requestMatchers(HttpMethod.GET, "/", "/*.html", "/**.html", "/**.css", "/**.js", "/profile/**").permitAll()
               .requestMatchers("/swagger-ui.html", "/v3/api-docs/**", "/swagger-ui/**", "/druid/**").permitAll()
               // 除上面外的所有请求全部需要鉴权认证
               .anyRequest().authenticated();
       })
       // 添加Logout filter(退出的时候进入logoutSuccessHandler)
       .logout(logout -> logout.logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler))
       // 添加JWT filter(每次请求都会进入UsernamePasswordAuthenticationFilter,校验token有效性、合法性)
       .addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class)
       // 添加CORS filter
       .addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class)
       .addFilterBefore(corsFilter, LogoutFilter.class)
       .build();
  }
}

看看退出账户的时候logoutSuccessHandler做啥了

@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
   LoginUser loginUser = tokenService.getLoginUser(request);
   if (StringUtils.isNotNull(loginUser)) {
       String userName = loginUser.getUsername();
       // 删除用户缓存记录
       tokenService.delLoginUser(loginUser.getToken());
       // 记录用户退出日志
       AsyncManager.me().execute(AsyncFactory.recordLogininfor(userName, Constants.LOGOUT, MessageUtils.message("user.logout.success")));
   }
   ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.success(MessageUtils.message("user.logout.success"))));
}

每次请求都会被 UsernamePasswordAuthenticationFilter 拦截

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
   // 通过令牌服务获取登录用户信息
   LoginUser loginUser = tokenService.getLoginUser(request);
   if (StringUtils.isNotNull(loginUser) && StringUtils.isNull(SecurityUtils.getAuthentication())) {
       // 验证用户令牌是否有效
       tokenService.verifyToken(loginUser);
       // 创建认证对象
       UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
       // 设置认证对象的详细信息,这些详细信息是基于web的认证细节
       authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
       // 将认证对象设置到安全上下文中,这样应用的其它部分可以访问到用户信息
       SecurityContextHolder.getContext().setAuthentication(authenticationToken);
   }
   // 继续执行下一个过滤器链
   chain.doFilter(request, response);
}

登录接口(认证管理)

@PostMapping("/login")
 public AjaxResult login(@RequestBody LoginBody loginBody) {
   AjaxResult ajax = AjaxResult.success();
   // 生成令牌
   String token = loginService.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode(), loginBody.getUuid());
   ajax.put(Constants.TOKEN, token);
   return ajax;
 }

核心实现在 loginService.login 方法

public String login(String username, String password, String code, String uuid) {
   // 验证码校验
   validateCaptcha(username, code, uuid);
   // 登录前置校验(前端校验了,后端再次校验长度啊,空格、IP黑名单校验之类的...)
   loginPreCheck(username, password);
   // 用户验证,具体验证的细节是由 SpringSecurity 认证管理器来处理的
   Authentication authentication = null;
   try {
       UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
       AuthenticationContextHolder.setContext(authenticationToken);
       // 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
       authentication = authenticationManager.authenticate(authenticationToken);
   } catch (Exception e) {
       ...
   } finally {
       AuthenticationContextHolder.clearContext();
   }
   // 异步生成登录日志
   AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));
   LoginUser loginUser = (LoginUser) authentication.getPrincipal();
   recordLoginInfo(loginUser.getUserId());
   // 生成token
   return tokenService.createToken(loginUser);
}
public void validateCaptcha(String username, String code, String uuid) {
   // 检查是否启用了验证码功能
    boolean captchaEnabled = configService.selectCaptchaEnabled();
    if (captchaEnabled) {
        // 构建验证码缓存 key
        String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + StringUtils.nvl(uuid, "");
        String captcha = redisCache.getCacheObject(verifyKey);
        // redis验证码不存在,抛出验证码已失效异常
        if (captcha == null) {
            AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire")));
            throw new CaptchaExpireException();
        }
        // 删除缓存的验证码,用完就删,因为验证码是一次生效的
        redisCache.deleteObject(verifyKey);
        // 校验用户提交的验证码和缓存的验证码,不成功,抛出验证码错误
        if (!code.equalsIgnoreCase(captcha)) {
            AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error")));
            throw new CaptchaException();
        }
    }
}

整个 login 方法大致分为两大块:
首先是认证细节
第二个就是认证完成之后的 token 生成

Authentication 认证

具体验证的细节是由 SpringSecurity 认证管理器来处理的,来看看UserDetailsServiceImpl.loadUserByUsername逻辑

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    SysUser user = userService.selectUserByUserName(username); // 查询用户信息
    if (StringUtils.isNull(user)) {
        log.info("登录用户:{} 不存在.", username);
        throw new ServiceException(MessageUtils.message("user.not.exists"));
    } else if (UserStatus.DELETED.getCode().equals(user.getDelFlag())) {
        log.info("登录用户:{} 已被删除.", username);
        throw new ServiceException(MessageUtils.message("user.password.delete"));
    } else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
        log.info("登录用户:{} 已被停用.", username);
        throw new ServiceException(MessageUtils.message("user.blocked"));
    }
    // 验证用户密码输入是否正确
    passwordService.validate(user);
    // 创建并返回登录用户对象
    return createLoginUser(user);
}

来看看密码校验部分逻辑

public void validate(SysUser user) {
     // 获取当前的认证信息,从认证信息中提取用户名和密码
     Authentication usernamePasswordAuthenticationToken = AuthenticationContextHolder.getContext();
     String username = usernamePasswordAuthenticationToken.getName();
     String password = usernamePasswordAuthenticationToken.getCredentials().toString();
     // 尝试从缓存中获取当前用户的密码重试次数
     Integer retryCount = redisCache.getCacheObject(getCacheKey(username));
     // 如果缓存中没有,则初始化 0
     if (retryCount == null) {
         retryCount = 0;
     }
     // 如果重试次数超过了配置文件中配置的最大重试次数(${user.password.maxRetryCount}),抛异常
     if (retryCount >= Integer.valueOf(maxRetryCount).intValue()) {
         throw new UserPasswordRetryLimitExceedException(maxRetryCount, lockTime);
     }

     if (!matches(user, password)) { // 对比用户密码和数据库密码,使用到 hash 散列算法处理
         retryCount = retryCount + 1; // 密码不匹配,增加重试次数
         redisCache.setCacheObject(getCacheKey(username), retryCount, lockTime, TimeUnit.MINUTES);
         throw new UserPasswordNotMatchException();
     } else {
         clearLoginRecordCache(username);
     }
 }
 public boolean matches(SysUser user, String rawPassword) {
     return this.matchesPassword(rawPassword, user.getPassword());
 }

 public static boolean matchesPassword(String rawPassword, String encodedPassword) {
     BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
     return passwordEncoder.matches(rawPassword, encodedPassword);
 }

创建登录用户对象返回

public UserDetails createLoginUser(SysUser user) {
    return new LoginUser(user.getUserId(), user.getDeptId(), user, permissionService.getMenuPermission(user));
}

这个对象是这样的
在这里插入图片描述

token的生成

 public String createToken(LoginUser loginUser) {
     String token = IdUtils.fastUUID();
     loginUser.setToken(token);
     // 设置用户登录信息,都是一些 setXX操作
     setUserAgent(loginUser);
     // 登录后,在 redis会缓存登录用户信息,key是 login_tokens+uuid
     refreshToken(loginUser);

     Map<String, Object> claims = new HashMap<>();
     claims.put(Constants.LOGIN_USER_KEY, token);
     return createToken(claims);
 }
 private String createToken(Map<String, Object> claims) {
     String token = Jwts.builder().setClaims(claims)
         // 使用 HS512 算法和 secret 密钥对 JWT 进行签名
         .signWith(SignatureAlgorithm.HS512, secret).compact();
     return token;
 }

最终生成的 token 会返回前端,存到 cookie中,所以登录后可以在 cooke 中看到这个 token
在这里插入图片描述
最后,总结一下登录流程
在这里插入图片描述

权限控制

前端页面,不同的用户登录会显示不同的操作权限,这是因为每个用户拥有的角色权限可能不一样,怎么判断的呢,使用 v-hasPermi指令,这个指令是若依框架自定义的组件

<el-button
 size="mini"
  type="text"
  icon="el-icon-edit"
  @click="handleUpdate(scope.row)"
  v-hasPermi="['system:dept:edit']"
>修改</el-button>

那用户权限怎么获取呢?会发现,每次刷新浏览器都会发送 http://localhost/dev-api/getInfo 查询用户信息,可以看下这个接口,其实就是获取当前用户拥有的角色和权限集合

 @GetMapping("getInfo")
 public AjaxResult getInfo() {
     SysUser user = SecurityUtils.getLoginUser().getUser();
     // 角色集合
     Set<String> roles = permissionService.getRolePermission(user);
     // 权限集合
     Set<String> permissions = permissionService.getMenuPermission(user);
     AjaxResult ajax = AjaxResult.success();
     ajax.put("user", user);
     ajax.put("roles", roles);
     ajax.put("permissions", permissions);
     return ajax;
 }

整个流程大致是这样的
在这里插入图片描述
那后端权限怎么控制呢?SpringSecurity提供的@PreAuthorize 注解是实现方法级别访问控制的核心工具,它通过在方法前进行权限校验,确保只有符合条件的用户才能访问特定的功能

@PreAuthorize("@ss.hasPermi('system:user:list')")
@GetMapping("/list")
 public TableDataInfo list(SysUser user) {
     startPage();
     List<SysUser> list = userService.selectUserList(user);
     return getDataTable(list);
 }

其中@ss代表 PermissionService 类,就是调用这个类的 各种方法,原理就是 AOP

@Service("ss")
public class PermissionService {
/**
 * 验证用户是否具备某权限
 *
 * @param permission 权限字符串
 * @return 用户是否具备某权限
 */
public boolean hasPermi(String permission) {
    // 判空
    if (StringUtils.isEmpty(permission)) {
        return false;
    }
    // 获取当前登录用户信息
    LoginUser loginUser = SecurityUtils.getLoginUser();
    // 为空直接返回 false
    if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getPermissions())) {
        return false;
    }
    // 将权限信息设置到上下文中,供后续操作使用
    PermissionContextHolder.setContext(permission);
    // 检查用户权限集合中是否包含指定权限
    Set<String> permissions = loginUser.getPermissions();
    return permissions.contains(Constants.ALL_PERMISSION) || permissions.contains(StringUtils.trim(permission));
	}
}

PermissionService 类还定义了其它方法,如下
在这里插入图片描述

异步任务管理

来分析一下 登录的接口

    @PostMapping("/login")
    public AjaxResult login(@RequestBody LoginBody loginBody)
    {
        AjaxResult ajax = AjaxResult.success();
        // 生成令牌
        String token = loginService.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode(),
                loginBody.getUuid());
        ajax.put(Constants.TOKEN, token);
        return ajax;
    }

接着定位到 loginService.login 方法

public String login(String username, String password, String code, String uuid)
    {
        // 验证码校验
        validateCaptcha(username, code, uuid);
        // 登录前置校验
        loginPreCheck(username, password);
        // 用户验证
        Authentication authentication = null;
        ....
        // 这行就是异步生成任务(记录登录日志),然后调用线程池执行任务
        AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        recordLoginInfo(loginUser.getUserId());
        // 生成token
        return tokenService.createToken(loginUser);
    }

记录的登录日志用于在这里查询
在这里插入图片描述

任务的生成,TimerTask 实现了 Runnable 接口,本质是个任务,这里是插入数据到库

public static TimerTask recordLogininfor(final String username, final String status, final String message,
            final Object... args)
    {
        ...
        return new TimerTask()
        {
            @Override
            public void run()
            {
                ...
                // 封装对象
                SysLogininfor logininfor = new SysLogininfor();
                ...
                // 插入数据
                SpringUtils.getBean(ISysLogininforService.class).insertLogininfor(logininfor);
            }
        };
    }

而任务是由 AsyncManager.me().execute 调用线程池执行的

/**
 * 异步操作任务调度线程池
 */
private ScheduledExecutorService executor = SpringUtils.getBean("scheduledExecutorService");

public void execute(TimerTask task)
{
    executor.schedule(task, OPERATE_DELAY_TIME, TimeUnit.MILLISECONDS);
}

整个过程流程图总结如下
在这里插入图片描述

操作日志

在日常编程中,记录日志是我们的得力助手,尤其在处理关键业务时,它能帮助我们追踪和审查操作过程,那 RuoYi 是怎么记录操作日志的呢?

在需要被记录日志的 Controller 方法上添加 @Log 注解,使用方法如下:

// 删除用户
@PreAuthorize("@ss.hasPermi('system:user:remove')")
@Log(title = "用户管理", businessType = BusinessType.DELETE)
@DeleteMapping("/{userIds}")
public AjaxResult remove(@PathVariable Long[] userIds) {
    if (ArrayUtils.contains(userIds, getUserId())) {
        return error("当前用户不能删除");
    }
    return toAjax(userService.deleteUserByIds(userIds));
}

可以看到注解的定义

@Target({ ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Log {
    /**
     * 模块
     */
    public String title() default "";

    /**
     * 功能
     */
    public BusinessType businessType() default BusinessType.OTHER;

    /**
     * 操作人类别
     */
    public OperatorType operatorType() default OperatorType.MANAGE;
    ...
}

使用注解的形式记录操作日志,肯定是用 AOP 思想来做的,所以一定有个切面,LogAspect 这个切面用是前置通知和后置通知组合,当然也可以使用 around 环绕通知来做
在这里插入图片描述
最终也是异步任何结合线程池来保存数据的

protected void handleLog(final JoinPoint joinPoint, Log controllerLog, final Exception e, Object jsonResult) {
  ....  
  // 设置方法名称
  String className = joinPoint.getTarget().getClass().getName();
  String methodName = joinPoint.getSignature().getName();
  operLog.setMethod(className + "." + methodName + "()");
  // 设置请求方式
  operLog.setRequestMethod(ServletUtils.getRequest().getMethod());
  // 处理设置注解上的参数
  getControllerMethodDescription(joinPoint, controllerLog, operLog, jsonResult);
  // 设置消耗时间
  operLog.setCostTime(System.currentTimeMillis() - TIME_THREADLOCAL.get());
  // 异步保存数据库
  AsyncManager.me().execute(AsyncFactory.recordOper(operLog));
}

大致流程如下
在这里插入图片描述

数据权限

如何确保用户只能访问他们授权查看的数据?这就是我们所说的数据权限控制

若依的用户管理和部门管理实现了数据权限的功能,自定义了 @DataScop 注解

 @Override
 @DataScope(deptAlias = "d", userAlias = "u")
 public List<SysUser> selectUserList(SysUser user)
 {
     return userMapper.selectUserList(user);
 }

在这里插入图片描述

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

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

相关文章

comfyUI好在哪?为啥大家都在用?

前言 comfyUI自从面世以来&#xff0c;就以一种潜力股的姿态快速流行了起来&#xff0c;越来越多的小伙伴开始使用comfyUI。也许你一开始会被comfyUI密密麻麻的“线路”吓到&#xff0c;但其实comfyUI也没那么复杂&#xff0c;并且好处多多。 本文将带大家一起&#xff0c;快…

2024年8月31日(星期六)骑行瓦恭村

2024年8月31日 (星期六&#xff09;骑行瓦恭村&#xff08;向日葵&#xff0c;谷花鱼&#xff09;&#xff0c;早8:30到9:00&#xff0c; 昆明氧气厂门囗集合&#xff0c;9:00准时出发【因迟到者&#xff0c;骑行速度快者&#xff0c;可自行追赶偶遇。】 偶遇地点:昆明氧气厂…

华为Huawei路由器交换机SSH配置

华为设备的SSH登录配置需要5个步骤&#xff0c;示例如下&#xff1a; 一、配置命令 使能SSH功能 stelnet server enable生成公钥 rsa local-key-pair create 1024配置AAA用户密码及相应授权 aaalocal-user xxx password cipher xxxyyy1234local-user xxx privilege level …

RAG必备知识:OpenAI官宣结构化输出|结构化输出工具大汇总

官宣:根据非常普遍的需求,OpenAI的API 中开始支持结构化输出。 在 API 中引入结构化输出 - 模型输出现在遵循开发人员提供的 JSON 模式。让大型语言模型(Large Language Models, LLMs)进行结构化输出是自然语言处理(NLP)领域中的一个重要目标。结构化输出指的是将自然语…

Java SpringBoot结合Vue打造法律援助平台,实现高效在线法律咨询服务

✍✍计算机编程指导师 ⭐⭐个人介绍&#xff1a;自己非常喜欢研究技术问题&#xff01;专业做Java、Python、微信小程序、安卓、大数据、爬虫、Golang、大屏等实战项目。 ⛽⛽实战项目&#xff1a;有源码或者技术上的问题欢迎在评论区一起讨论交流&#xff01; ⚡⚡ Java实战 |…

布偶猫应该怎么喂?希喂、交响乐金罐、尾巴生活彩虹泥适合布偶猫吗?

我开了家布偶猫咖&#xff0c;这些长相甜美可爱的小家伙超会撒娇卖萌&#xff0c;把客人迷的团团转。布偶猫又叫仙布拉多尔猫&#xff0c;它是现存体型最大、体重最重的猫之一&#xff0c;它们体型大&#xff0c;食量也大&#xff0c;但肠胃却特别弱&#xff0c;所以一定要特别…

Mac环境下Python3虚拟环境创建、Flask安装以及创建运行第一个最小的Flask项目

为什么要使用虚拟环境&#xff1f;随着你的 Python 项目越来越多&#xff0c;你会发现不同的项目会需要 不同的版本的 Python 库。同一个 Python 库的不同版本可能不兼容。 虚拟环境可以为每一个项目安装独立的 Python 库&#xff0c;这样就可以隔离不同项目之间的 Python 库&…

普通人怎么利用ai赚钱?这10个AI帮你实现!

以前我是真想不到&#xff0c;AI写的破文章&#xff0c;复制粘贴发出去竟然有收入&#xff1f; 今天先码住10个ai工具&#xff0c;下次更新10投稿文章获得收入的平台&#xff01; 1、豆包&#xff1a;完全免费&#xff0c;ai对话里有一个功能叫做网页摘要&#xff0c;可以把你…

从源码到产品:视频美颜SDK与直播美颜插件的开发详解

开发一款高效的视频美颜SDK与直播美颜插件&#xff0c;不仅需要深入理解图像处理技术&#xff0c;还需要考虑到性能优化、跨平台支持等多个方面的挑战。接下来&#xff0c;笔者将从源码开发的角度&#xff0c;详解视频美颜SDK与直播美颜插件的开发过程。 一、视频美颜SDK的核心…

ai取名生成器在哪?建议看看这篇文章

ai取名字叫什么好听&#xff1f;你是否在为给孩子、宠物或是项目起一个既独特又好听的名字而烦恼&#xff1f;别担心&#xff0c;AI取名字软件来帮你解锁命名新境界&#xff01; 现在有很多智能工具利用先进的人工智能技术&#xff0c;可以为你量身定制一系列令人耳目一新的名…

选择个人开发者进行软件开发的潜在风险分析

在科技飞速发展的今天&#xff0c;软件开发行业正变得越来越精细化和专业化&#xff0c;对专业技能和团队协作能力的需求日益增长。虽然在某些情况下&#xff0c;雇佣个人开发者似乎成本较低且操作简便&#xff0c;但其背后隐藏的挑战和风险同样不容小觑。本篇分析将探讨选择个…

使用Dockerfile创建应用镜像

在Docker file中定义所需要执⾏的指令&#xff0c;使⽤ docker build创建镜 像&#xff0c;过程中会按照dockerfile所定义的内容进⾏打开临时性容器&#xff0c;把 docker file中命令全部执⾏完成&#xff0c;就得到了⼀个容器应⽤镜像&#xff0c;每 ⼀⾏命令都会出现容器&…

axure9树形元件节点的添加

树形元件 | AxureChina 在需要添加节点处右键添加->添加子节点

Prometheus+exporter+Grafana

参考文档&#xff1a;https://www.cnblogs.com/morang/p/devops-prometheus-grafana-install-use-db-site-monitor.html Prometheus&#xff1a;监控中心&#xff0c;监控exporter的实时数据 exporter&#xff1a;采集数据工具&#xff0c;如下&#xff1a; Grafana&#xff…

超详细!!!uniapp通过unipush全流程实现app消息推送

云风网 云风笔记 云风知识库 一、HBuilder新建APP项目 二、配置推送服务 1、登录Dcloud开发者中心开发者中心&#xff0c;查看我的应用 2、生成云端证书 3、创建平台信息 4、配置推送服务信息 这里需要关联服务空间&#xff0c;可以申请免费服务空间进行测试 三、代码配置 1…

IOS 17 基于UITabBarController实现首页TabBar

实现方式 创建首页MainController继承自UITabBarController就可以实现Tabbar效果。 实现效果 创建几个子控制器 创建子控制器 DiscoveryController&#xff0c;VideoController&#xff0c;MeController&#xff0c;FeedController&#xff0c;RoomController&#xff0c;继…

【书生大模型实战营(暑假场)】进阶任务四 InternVL 多模态模型部署微调实践

进阶任务四 InternVL 多模态模型部署微调实践 任务文档视频 1 InternVL 基本介绍 InternVL 是一种用于多模态任务的深度学习模型&#xff0c;旨在处理和理解多种类型的数据输入&#xff0c;如图像和文本。它结合了视觉和语言模型&#xff0c;能够执行复杂的跨模态任务&#x…

YOLOv5改进 | 融合改进 | C3融合Faster-GELU模块提升检测速度【完整代码 + 主要代码解析】

秋招面试专栏推荐 &#xff1a;深度学习算法工程师面试问题总结【百面算法工程师】——点击即可跳转 &#x1f4a1;&#x1f4a1;&#x1f4a1;本专栏所有程序均经过测试&#xff0c;可成功执行&#x1f4a1;&#x1f4a1;&#x1f4a1; 专栏目录&#xff1a; 《YOLOv5入门 改…

[GKCTF 2021]excel 骚操作1

使用010editor打开发现zip头&#xff0c;改后缀名xlsx为zip&#xff0c;解压&#xff0c;在D:\python\flag (1)\xl\worksheets目录下有个sheet1.xml就是ecxel的sheet1的主要样式style 看到很多c r"B2" s"1&#xff0c;只是单元格数据不同而已 &#xff0c;还有的…

Windows10打印机共享小技巧

现在工作中打印机&#xff0c;大多数都是通过TCP/IP直连的方式安装打印的&#xff1b;但还是会碰到有需要通过主机安装后共享连接的网络打印机&#xff1b;特别是在Windows10主机上共享打印机&#xff0c;因为系统安全策略的优化&#xff0c;连接共享打印机时&#xff0c;会遇到…