ruoyi若依框架SpringSecurity实现分析

系列文章

ruoyi若依框架学习笔记-01

ruoyi若依框架分页实现分析

ruoyi若依框架SpringSecurity实现分析

文章目录

  • 系列文章
  • 前言
  • 具体分析
    • 一、项目中的SpringSecurity版本
    • 二、登录认证流程分析
    • 三、权限鉴定
    • 四、退出登录
    • 五、SpringSecurity配置类
  • 总结


前言

在ruoyi-vue若依框架中使用到了SpringSecurity作为认证授权的技术栈。今天,来分析一下若依中是如何实现认证授权的。


具体分析

一、项目中的SpringSecurity版本

在这里插入图片描述
可见,当前springsecurity的版本还是相对比较旧的security5。所以我们在自己的项目中可以对此进行重构,升级成security6的版本。但目前只是先分析一下它的实现原理。

二、登录认证流程分析

在这里插入图片描述

首先看一下登录控制器
ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysLoginController.java

@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;}

内容比较简单,我们去看一下login方法

public String login(String username, String password, String code, String uuid){// 验证码校验validateCaptcha(username, code, uuid);// 登录前置校验loginPreCheck(username, password);// 用户验证Authentication authentication = null;try{//UsernamePasswordAuthenticationToken是Authenticatiion的实现类UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);//这里设置成上下文的意图是在身份验证过程中,//其他组件或方法可以获取到该对象,以便进行相关的操作,//比如记录登录日志、获取用户信息等。AuthenticationContextHolder.setContext(authenticationToken);// 该方法会去调用UserDetailsServiceImpl.loadUserByUsernameauthentication = authenticationManager.authenticate(authenticationToken);}catch (Exception e){if (e instanceof BadCredentialsException){//这里去使用日志记录相关错误AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));throw new UserPasswordNotMatchException();}else{AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, e.getMessage()));throw new ServiceException(e.getMessage());}}finally{//最后要在对应操作完成之后,清理ContextAuthenticationContextHolder.clearContext();}AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));//这里就是拿到自定义封装的用户信息LoginUser loginUser = (LoginUser) authentication.getPrincipal();//将登录信息存入到数据库recordLoginInfo(loginUser.getUserId());// 生成tokenreturn tokenService.createToken(loginUser);}

然后我们去看一下loadUserByUsername方法
com.ruoyi.framework.web.service.UserDetailsServiceImpl

@Overridepublic 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);//将用户信息封装成我们自定义的LoginUser对象,他是UserDetails的实现类return createLoginUser(user);}public UserDetails createLoginUser(SysUser user){return new LoginUser(user.getUserId(), user.getDeptId(), user, permissionService.getMenuPermission(user));}

由于框架本身的UserDetails对象无法满足我们的需求,所以需要自定义一个实现类。
这里是UserDetails的实现类com.ruoyi.common.core.domain.model.LoginUser#serialVersionUID

public class LoginUser implements UserDetails
{private static final long serialVersionUID = 1L;/*** 用户ID*/private Long userId;/*** 部门ID*/private Long deptId;/*** 用户唯一标识*/private String token;/*** 登录时间*/private Long loginTime;/*** 过期时间*/private Long expireTime;/*** 登录IP地址*/private String ipaddr;/*** 登录地点*/private String loginLocation;/*** 浏览器类型*/private String browser;/*** 操作系统*/private String os;/*** 权限列表*/private Set<String> permissions;/*** 用户信息*/private SysUser user;public LoginUser(){}public LoginUser(SysUser user, Set<String> permissions){this.user = user;this.permissions = permissions;}public LoginUser(Long userId, Long deptId, SysUser user, Set<String> permissions){this.userId = userId;this.deptId = deptId;this.user = user;this.permissions = permissions;}public Long getUserId(){return userId;}public void setUserId(Long userId){this.userId = userId;}public Long getDeptId(){return deptId;}public void setDeptId(Long deptId){this.deptId = deptId;}public String getToken(){return token;}public void setToken(String token){this.token = token;}@JSONField(serialize = false)@Overridepublic String getPassword(){return user.getPassword();}@Overridepublic String getUsername(){return user.getUserName();}/*** 账户是否未过期,过期无法验证*/@JSONField(serialize = false)@Overridepublic boolean isAccountNonExpired(){return true;}/*** 指定用户是否解锁,锁定的用户无法进行身份验证* * @return*/@JSONField(serialize = false)@Overridepublic boolean isAccountNonLocked(){return true;}/*** 指示是否已过期的用户的凭据(密码),过期的凭据防止认证* * @return*/@JSONField(serialize = false)@Overridepublic boolean isCredentialsNonExpired(){return true;}/*** 是否可用 ,禁用的用户不能身份验证* * @return*/@JSONField(serialize = false)@Overridepublic boolean isEnabled(){return true;}public Long getLoginTime(){return loginTime;}public void setLoginTime(Long loginTime){this.loginTime = loginTime;}public String getIpaddr(){return ipaddr;}public void setIpaddr(String ipaddr){this.ipaddr = ipaddr;}public String getLoginLocation(){return loginLocation;}public void setLoginLocation(String loginLocation){this.loginLocation = loginLocation;}public String getBrowser(){return browser;}public void setBrowser(String browser){this.browser = browser;}public String getOs(){return os;}public void setOs(String os){this.os = os;}public Long getExpireTime(){return expireTime;}public void setExpireTime(Long expireTime){this.expireTime = expireTime;}public Set<String> getPermissions(){return permissions;}public void setPermissions(Set<String> permissions){this.permissions = permissions;}public SysUser getUser(){return user;}public void setUser(SysUser user){this.user = user;}@Overridepublic Collection<? extends GrantedAuthority> getAuthorities(){return null;}
}

其实我觉得最后应该也要重写getAuthorities()方法内部,如下

		if (ObjectUtils.isEmpty(authorities)) {authorities = new ArrayList<>();permissions.forEach(permission -> authorities.add(()->permission));}return authorities;

还望大佬发表看法。
在ruoyi-vue中有对认证失败的处理类
com.ruoyi.framework.security.handle.AuthenticationEntryPointImpl
在其中直接将错误信息渲染返回给前端

@Overridepublic void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e)throws IOException{int code = HttpStatus.UNAUTHORIZED;String msg = StringUtils.format("请求访问:{},认证失败,无法访问系统资源", request.getRequestURI());ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.error(code, msg)));}

当用户需要访问其他接口的时候,我们需要验证请求头中是否携带合法的token,并将用户信息存入Authentication中。
com.ruoyi.framework.security.filter.JwtAuthenticationTokenFilter中有token过滤器,验证token有效性。

@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter
{@Autowiredprivate TokenService tokenService;@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)throws ServletException, IOException{//从token中获取用户信息,封装成LoginUser对象LoginUser loginUser = tokenService.getLoginUser(request);if (StringUtils.isNotNull(loginUser) && StringUtils.isNull(SecurityUtils.getAuthentication())){//验证token是否合法tokenService.verifyToken(loginUser);//封装成authentication对象UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());//设置详细信息可以获取到请求的详细信息,例如请求的IP地址、请求的会话ID等。//设置详细信息可以帮助记录日志、进行安全审计和监控等操作authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));SecurityContextHolder.getContext().setAuthentication(authenticationToken);}chain.doFilter(request, response);}
}

三、权限鉴定

我们先看一下controller层方法的上面的注解
在这里插入图片描述
"@ss.hasPermi(‘system:menu:list’)"的意思是调用ss这个容器下的hasPermi方法
可以看到,ruoyi是自定义的权限校验方法。那我们来看一下这个容器
com.ruoyi.framework.web.service.PermissionService

@Service("ss")
public class PermissionService
{/*** 验证用户是否具备某权限* * @param permission 权限字符串* @return 用户是否具备某权限*/public boolean hasPermi(String permission){if (StringUtils.isEmpty(permission)){return false;}LoginUser loginUser = SecurityUtils.getLoginUser();if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getPermissions())){return false;}PermissionContextHolder.setContext(permission);return hasPermissions(loginUser.getPermissions(), permission);}/*** 验证用户是否不具备某权限,与 hasPermi逻辑相反** @param permission 权限字符串* @return 用户是否不具备某权限*/public boolean lacksPermi(String permission){return hasPermi(permission) != true;}/*** 验证用户是否具有以下任意一个权限** @param permissions 以 PERMISSION_DELIMETER 为分隔符的权限列表* @return 用户是否具有以下任意一个权限*/public boolean hasAnyPermi(String permissions){if (StringUtils.isEmpty(permissions)){return false;}LoginUser loginUser = SecurityUtils.getLoginUser();if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getPermissions())){return false;}PermissionContextHolder.setContext(permissions);Set<String> authorities = loginUser.getPermissions();for (String permission : permissions.split(Constants.PERMISSION_DELIMETER)){if (permission != null && hasPermissions(authorities, permission)){return true;}}return false;}/*** 判断用户是否拥有某个角色* * @param role 角色字符串* @return 用户是否具备某角色*/public boolean hasRole(String role){if (StringUtils.isEmpty(role)){return false;}LoginUser loginUser = SecurityUtils.getLoginUser();if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getUser().getRoles())){return false;}for (SysRole sysRole : loginUser.getUser().getRoles()){String roleKey = sysRole.getRoleKey();if (Constants.SUPER_ADMIN.equals(roleKey) || roleKey.equals(StringUtils.trim(role))){return true;}}return false;}/*** 验证用户是否不具备某角色,与 isRole逻辑相反。** @param role 角色名称* @return 用户是否不具备某角色*/public boolean lacksRole(String role){return hasRole(role) != true;}/*** 验证用户是否具有以下任意一个角色** @param roles 以 ROLE_NAMES_DELIMETER 为分隔符的角色列表* @return 用户是否具有以下任意一个角色*/public boolean hasAnyRoles(String roles){if (StringUtils.isEmpty(roles)){return false;}LoginUser loginUser = SecurityUtils.getLoginUser();if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getUser().getRoles())){return false;}for (String role : roles.split(Constants.ROLE_DELIMETER)){if (hasRole(role)){return true;}}return false;}/*** 判断是否包含权限* * @param permissions 权限列表* @param permission 权限字符串* @return 用户是否具备某权限*/private boolean hasPermissions(Set<String> permissions, String permission){return permissions.contains(Constants.ALL_PERMISSION) || permissions.contains(StringUtils.trim(permission));}
}

别忘了在配置类中加上注解 @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
这里就不具体一个一个分析了,因为比较简单,都是很容易看懂的。

四、退出登录

若依中还自定义了退出登录处理类com.ruoyi.framework.security.handle.LogoutSuccessHandlerImpl

@Configuration
public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler
{@Autowiredprivate TokenService tokenService;/*** 退出处理* * @return*/@Overridepublic 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"))));}
}

五、SpringSecurity配置类

@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter
{/*** 自定义用户认证逻辑*/@Autowiredprivate UserDetailsService userDetailsService;/*** 认证失败处理类*/@Autowiredprivate AuthenticationEntryPointImpl unauthorizedHandler;/*** 退出处理类*/@Autowiredprivate LogoutSuccessHandlerImpl logoutSuccessHandler;/*** token认证过滤器*/@Autowiredprivate JwtAuthenticationTokenFilter authenticationTokenFilter;/*** 跨域过滤器*/@Autowiredprivate CorsFilter corsFilter;/*** 允许匿名访问的地址*///这里我先不分析,以后补上@Autowiredprivate PermitAllUrlProperties permitAllUrl;/*** 解决 无法直接注入 AuthenticationManager** @return* @throws Exception*/@Bean@Overridepublic AuthenticationManager authenticationManagerBean() throws Exception{return super.authenticationManagerBean();}/*** anyRequest          |   匹配所有请求路径* access              |   SpringEl表达式结果为true时可以访问* anonymous           |   匿名可以访问* denyAll             |   用户不能访问* fullyAuthenticated  |   用户完全认证可以访问(非remember-me下自动登录)* hasAnyAuthority     |   如果有参数,参数表示权限,则其中任何一个权限可以访问* hasAnyRole          |   如果有参数,参数表示角色,则其中任何一个角色可以访问* hasAuthority        |   如果有参数,参数表示权限,则其权限可以访问* hasIpAddress        |   如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问* hasRole             |   如果有参数,参数表示角色,则其角色可以访问* permitAll           |   用户可以任意访问* rememberMe          |   允许通过remember-me登录的用户访问* authenticated       |   用户登录后可访问*/@Overrideprotected void configure(HttpSecurity httpSecurity) throws Exception{// 注解标记允许匿名访问的urlExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = httpSecurity.authorizeRequests();permitAllUrl.getUrls().forEach(url -> registry.antMatchers(url).permitAll());//我主要看这部分httpSecurity// CSRF禁用,因为不使用session.csrf().disable()// 禁用HTTP响应标头.headers().cacheControl().disable().and()// 认证失败处理类.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()// 基于token,所以不需要session.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()// 过滤请求.authorizeRequests()// 对于登录login 注册register 验证码captchaImage 允许匿名访问.antMatchers("/login", "/register", "/captchaImage").permitAll()// 静态资源,可匿名访问.antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**").permitAll().antMatchers("/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/*/api-docs", "/druid/**").permitAll()// 除上面外的所有请求全部需要鉴权认证.anyRequest().authenticated().and().headers().frameOptions().disable();// 添加Logout filterhttpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);// 添加JWT filterhttpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);// 添加CORS filterhttpSecurity.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class);httpSecurity.addFilterBefore(corsFilter, LogoutFilter.class);}/*** 强散列哈希加密实现*/@Beanpublic BCryptPasswordEncoder bCryptPasswordEncoder(){return new BCryptPasswordEncoder();}/*** 身份认证接口*/@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception{auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());}
}

总结

其实ruoyi-vue中对SpringSecurity的使用非常时候我们用来复习SpringSecurity,以及学习它的编码风格,绝对收益不浅。

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

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

相关文章

记一次页面接口502问题:“502 Bad Gateway”

接收别人的项目进行迭代&#xff0c;项目部署到服务器上之后&#xff0c;有一个接口数据刷不出来&#xff0c;一直502 后来联想到网关的问题&#xff0c;想通过设置白名单的方式解决&#xff0c;设置之后依旧不行。 查看nginx日志发现报错&#xff1a; *169 connect() failed …

【Git】移除Git中的文件

有的时候需要移除或者更新 Git 中的文件&#xff0c;我们无法直接在远程仓库中移除&#xff0c;移除或者更新操作需要在本地端实现。 1、移除被跟踪文件 当某个文件被添加到暂存区或者本地仓库&#xff0c;此时会被标记为“跟踪状态”&#xff0c;此时 Git 就会代为管理这个文…

【go语言】一个简单HTTP服务的例子

一、Go语言安装 Go语言&#xff08;又称Golang&#xff09;的安装过程相对简单&#xff0c;下面是在不同操作系统上安装Go语言的步骤&#xff1a; 在Windows上安装Go语言&#xff1a; 访问Go语言的官方网站&#xff08;golang.org&#xff09;或者使用国内镜像站点&#xff0…

vue3 腾讯tdesign 后台管理框架的使用

1.介绍 TDesign 是具有包容性的设计体系&#xff0c;它强调为业务提供产品、服务等过程中&#xff0c;追求以人为本、人人受益的包容性&#xff0c;要求搭建过程中&#xff0c;了解业务底层&#xff0c;理解业务场景的多样性&#xff0c;并在繁杂的业务场景中寻找共性和特性&a…

GeoServer 2.11.1升级解决Eclipse Jetty 的一系列安全漏洞问题

Eclipse Jetty 资源管理错误漏洞(CVE-2021-28165) Eclipse Jetty HTTP请求走私漏洞(CVE-2017-7656) Eclipse Jetty HTTP请求走私漏洞(CVE-2017-7657) Eclipse Jetty HTTP请求走私漏洞(CVE-2017-7658) Jetty 信息泄露漏洞(CVE-2017-9735) Eclipse Jetty 安全漏洞(CVE-2022-20…

算法沉淀——分治算法(leetcode真题剖析)

算法沉淀——分治算法 快排思想01.颜色分类02.排序数组03.数组中的第K个最大元素04.库存管理 III 归并思想01.排序数组02.交易逆序对的总数03.计算右侧小于当前元素的个数04.翻转对 分治算法是一种解决问题的算法范式&#xff0c;其核心思想是将一个大问题分解成若干个小问题&a…

Unity笔记:相机移动

基础知识 鼠标输入 在Unity中&#xff0c;开发者在“Edit” > “Project Settings” > “Input Manager”中设置输入&#xff0c;如下图所示&#xff1a; 在设置了Mouse X后&#xff0c;Input.GetAxis("Mouse X")返回的是鼠标在X轴上的增量值。这意味着它会…

Solidworks:从2D走向3D

Sokidworks 的强大之处在于三维实体建模&#xff0c;这个形状看似复杂&#xff0c;实际上只需要拉伸一次&#xff0c;再做一次减法拉伸就行了。第一次做三维模型&#xff0c;费了不少时间才搞明白。 接下来做一个稍微复杂一点的模型&#xff0c;和上面这个操作差不多&#xff0…

LeetCode.144. 二叉树的前序遍历

题目 144. 二叉树的前序遍历 分析 这道题目是比较基础的题目&#xff0c;我们首先要知道二叉树的前序遍历是什么&#xff1f; 就是【根 左 右】 的顺序&#xff0c;然后利用递归的思想&#xff0c;就可以得到这道题的答案&#xff0c;任何的递归都可以采用 栈 的结构来实现…

利用Windows10漏洞破解密码(保姆级教学)

前言: 本篇博客只是技术分享并非非法传播知识,实验内容均是在虚拟机中进行,并非真实环境 正文: 一.windows10电脑密码破解 1)开启windows10虚拟机,停留在这个页面 2&#xff09;按5次Shift键,出现这个粘滞键,如果没有出现的,则说明漏洞已经修复 3)重新启动,在这个页面的时候…

Qt网络编程-TCP与UDP

网络基础 TCP与UDP基础 关于TCP与UDP的基础这里就不过多介绍了&#xff0c;具体可以查看对应百度百科介绍&#xff1a; TCP&#xff08;传输控制协议&#xff09;_百度百科 (baidu.com) UDP_百度百科 (baidu.com) 需要知道这两者的区别&#xff1a; 可靠性&#xff1a; TC…

【C语言】实现单链表

目录 &#xff08;一&#xff09;头文件 &#xff08;二&#xff09;功能实现 &#xff08;1&#xff09;打印单链表 &#xff08;2&#xff09;头插与头删 &#xff08;3&#xff09;尾插与尾删 &#xff08;4&#xff09; 删除指定位置节点 和 删除指定位置之后的节点 …

【工作学习 day04】 9. uniapp 页面和组件的生命周期

问题描述 uniapp常用的有&#xff1a;页面和组件&#xff0c;并且页面和组件各自有各自的生命周期函数&#xff0c;那么在页面/组件请求数据时&#xff0c;是用created呢&#xff0c;还是用onLoad呢&#xff1f; 先说结论: 组件使用组件的生命周期&#xff0c;页面使用页面的…

第62讲商品搜索动态实现以及性能优化

商品搜索后端动态获取数据 后端动态获取数据&#xff1a; /*** 商品搜索* param q* return*/GetMapping("/search")public R search(String q){List<Product> productList productService.list(new QueryWrapper<Product>().like("name", q)…

【Java程序设计】【C00260】基于Springboot的企业客户信息反馈平台(有论文)

基于Springboot的企业客户信息反馈平台&#xff08;有论文&#xff09; 项目简介项目获取开发环境项目技术运行截图 项目简介 这是一个基于Springboot的企业客户信息反馈平台 本系统分为平台功能模块、管理员功能模块以及客户功能模块。 平台功能模块&#xff1a;在平台首页可…

python从入门到精通(十六):python爬虫的BeautifulSoup4

python爬虫的BeautifulSoup4 BeautifulSoup4导入模块解析文件创建对象python解析器beautifulsoup对象的种类Tag获取整个标签获取标签里的属性和属性值Navigablestring 获取标签里的内容BeautifulSoup获取整个文档Comment输出的内容不包含注释符号BeautifulSoup文档遍历Beautifu…

OpenCV-36 多边形逼近与凸包

目录 一、多边形的逼近 二、凸包 一、多边形的逼近 findContours后的轮廓信息countours可能过于复杂不平滑&#xff0c;可以用approxPolyDP函数对该多边形曲线做适当近似&#xff0c;这就是轮廓的多边形逼近。 apporxPolyDP就是以多边形去逼近轮廓&#xff0c;采用的是Doug…

Android9~Android13 某些容量SD卡被格式化为内部存储时容量显示错误问题的研究与解决方案

声明:原创文章,禁止转载! Android9~Android13 某些容量SD卡被格式化为内部存储时容量显示错误问题的研究与解决方案 分析Android11 系统对于EMMC/UFS作为内部存储、SD卡被格式化为内部存储、SD卡/U盘被格式化为便携式存储的不同处理 一.现象描述 实测Android9 Android10 A…

牛客错题整理——C语言(实时更新)

1.以下程序的运行结果是&#xff08;&#xff09; #include <stdio.h> int main() { int sum, pad,pAd; sum pad 5; pAd sum, pAd, pad; printf("%d\n",pAd); }答案为7 由于赋值运算符的优先级高于逗号表达式&#xff0c;因此pAd sum, pAd, pad;等价于(…

速度规划:s形曲线应用(变速 停车)opencv c++显示(3)

理论篇 先看该篇&#xff0c;这里沿用了里面的变量。 应用推导篇 分为变速和停车两部分&#xff08;字迹潦草&#xff0c;可结合代码看&#xff09; 代码篇 变速函数入口&#xff1a; velocityPlanner vp; vp.SetParameters(0, 1);停车函数入口&#xff1a; ParkingVelo…