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,以及学习它的编码风格,绝对收益不浅。
相关文章:

ruoyi若依框架SpringSecurity实现分析
系列文章 ruoyi若依框架学习笔记-01 ruoyi若依框架分页实现分析 ruoyi若依框架SpringSecurity实现分析 文章目录 系列文章前言具体分析一、项目中的SpringSecurity版本二、登录认证流程分析三、权限鉴定四、退出登录五、SpringSecurity配置类 总结 前言 在ruoyi-vue若依框…...

Habitat环境学习四:Habitat-sim基础用于导航——使用导航网格NavMesh
如何使用导航网格NavMesh 官方教程1、NavMesh基础定义1.1 使用NavMesh的原因1.2 什么是NavMesh 2、NavMesh的使用方法2.1 获取自上而下Top down view视角地图2.2 在NavMesh中进行查询以及随机产生可导航点2.3 查找最短路径2.4 场景加载NavMesh2.5 重新计算并生成NavMesh2.6 什么…...
python学习笔记 -- 字符串
目录 一、输出字符串的格式 二、字符串的一些函数 1、len函数:字符串长度 2、查找字符所在位置index 3、某字符在字符串中的个数count 4、字符切片 对字符串进行翻转 -- 利用步长 5、修改大小写字母: 6、判断开头和结尾 7、拆分字符串 一、输出…...
2024年GPT如何发展?
2023 年,人工智能领域最具影响的莫过于 GPT-4、ChatGPT 了。 ChatGPT 凭一己之力掀起了 AI 领域的热潮,火爆全球,似乎开启了第四次工业革命。 ChatGPT 入选《Nature》2023 年度十大人物(Nature’s 10),这…...

从REPR设计模式看 .NET的新生代类库FastEndpoints的威力
📢欢迎点赞 :👍 收藏 ⭐留言 📝 如有错误敬请指正,赐人玫瑰,手留余香!📢本文作者:由webmote 原创📢作者格言:新的征程,我们面对的不仅仅是技术还有人心,人心不可测,海水不可量,唯有技术,才是深沉黑夜中的一座闪烁的灯塔 !序言 又到了一年年末,春节将至…...
前端入门:(五)JavaScript 续
10. 浏览器存储 10.1 Cookie的概念和使用 Cookie是一种存储在用户计算机上的小型文本文件,用于跟踪和识别用户。Cookie通常用于存储用户的偏好设置、会话信息等,可以通过JavaScript进行读取和设置。 // 示例:设置和读取Cookie document.co…...

研究多态恶意软件,探讨网络安全与AI
前言 近期ChatGPT火遍全球,AI技术被应用到了全球各行各业当中,国内外各大厂商也开始推出自己的ChatGPT,笔者所在公司在前段时间也推出了自研的安全GPT,AI技术在网络安全行业得到了很多的应用,不管是网络安全研究人员、…...
linux驱动工作原理
linux或者windows驱动是如何对上和对下工作的,请用中文回答 在Linux系统中,设备驱动程序通过在/dev目录下创建文件系统条目与硬件通信。应用程序通过打开这些文件来获取描述符,以此来与设备交互。驱动程序内部使用主次设备号来标识设备。而在…...
Rust语言入门(第3篇)
引用与借用 上一篇中,我们介绍了rust的所有权概念,若直接传递变量做函数参数,堆上的变量就会失去所有权,而栈上变量则由于复制,仍有所有权。 fn main(){let b 3;makes_copy(b);println!("after using a variab…...

电脑服务器离线安装.net framework 3.5解决方案(错误:0x8024402c )(如何确定当前系统是否安装NET Framework 3.5)
问题环境: 日常服务的搭建或多或少都会有需要到NET Framework 3.5的微软程序运行框架,本次介绍几种不同的安装方式主要解决运行在Windows 2012 以上的操作系统的服务。 NET Framework 3.5 是什么? .NET Framework是微软公司推出的程序运行框架…...

Three.js学习8:基础贴图
一、贴图 贴图(Texture Mapping),也翻译为纹理映射,“贴图”这个翻译更直观。 贴图,就是把图片贴在 3D 物体材质的表面,让它具有一定的纹理,来为 3D 物体添加细节的一种方法。这使我们能够添加…...

【Linux】进程学习(二):进程状态
目录 1.进程状态1.1 阻塞1.2 挂起 2. 进程状态2.1 运行状态-R进一步理解运行状态 2.2 睡眠状态-S2.3 休眠状态-D2.4 暂停状态-T2.5 僵尸状态-Z僵尸进程的危害 2.6 死亡状态-X2.7 孤儿进程 1.进程状态 1.1 阻塞 阻塞:进程因为等待某种条件就绪,而导致的…...

Spring Boot 笔记 003 Bean注册
使用Idea导入第三方jar包 在porn.xml种添加的第三方jar包依赖,并刷新 可以在启动类中尝试调用 以上放到启动类中,不推荐,建议创建一个专门定义的类 package com.geji.config;import cn.itcast.pojo.Country; import cn.itcast.pojo.Province;…...

PCIE 参考时钟架构
一、PCIe架构组件 首先先看下PCIE架构组件,下图中主要包括: ROOT COMPLEX (RC) (CPU); PCIE PCI/PCI-X Bridge; PCIE SWITCH; PCIE ENDPOINT (EP) (pcie设备); BUFFER; 各个器件的时钟来源都是由100MHz经过Buffer后提供。一个PCIE树上最多可以有256…...

【开源】JAVA+Vue.js实现在线课程教学系统
目录 一、摘要1.1 系统介绍1.2 项目录屏 二、研究内容2.1 课程类型管理模块2.2 课程管理模块2.3 课时管理模块2.4 课程交互模块2.5 系统基础模块 三、系统设计3.1 用例设计3.2 数据库设计 四、系统展示4.1 管理后台4.2 用户网页 五、样例代码5.1 新增课程类型5.2 网站登录5.3 课…...
计算机网络(第六版)复习提纲29
第六章:应用层 SS6.1 域名系统DNS 1 DNS被设计为一个联机分布式数据库系统,并采用客户服务器方式(C/S) 2 域名的体系结构 3 域名服务器及其体系结构 A 域名服务器的分类 1 根域名服务器 2 顶级域名服务器(TLD服务器&a…...

有道ai写作,突破免费限制,无限制使用
预览效果 文末提供源码包及apk下载地址 有道ai写作python版 import hashlib import time import json import ssl import base64 import uuidfrom urllib.parse import quote import requests from requests_toolbelt.multipart.encoder import MultipartEncoder from Crypto…...
node.js 使用 elementtree 生成思维导图 Freemind 文件
请参阅: java : pdfbox 读取 PDF文件内书签 请注意:书的目录.txt 编码:UTF-8,推荐用 Notepad 转换编码。 npm install elementtree --save 编写 txt_etree_mm.js 如下 // 读目录.txt文件,使用 elementtree 生成思维导图 Free…...

Vue中路由守卫的详细应用
作为一名web前端开发者,我们肯定经常使用Vue框架来构建我们的项目。而在Vue中,路由是非常重要的一部分,它能够实现页面的跳转和导航,提供更好的用户体验。然而,有时我们需要在路由跳转前或跳转后执行一些特定的逻辑&am…...

Flink Checkpoint过程
Checkpoint 使用了 Chandy-Lamport 算法 流程 1. 正常流式处理(尚未Checkpoint) 如下图,Topic 有两个分区,并行度也为 2,根据奇偶数 我们假设任务从 Kafka 的某个 Topic 中读取数据,该Topic 有 2 个 Pa…...

iOS 26 携众系统重磅更新,但“苹果智能”仍与国行无缘
美国西海岸的夏天,再次被苹果点燃。一年一度的全球开发者大会 WWDC25 如期而至,这不仅是开发者的盛宴,更是全球数亿苹果用户翘首以盼的科技春晚。今年,苹果依旧为我们带来了全家桶式的系统更新,包括 iOS 26、iPadOS 26…...

阿里云ACP云计算备考笔记 (5)——弹性伸缩
目录 第一章 概述 第二章 弹性伸缩简介 1、弹性伸缩 2、垂直伸缩 3、优势 4、应用场景 ① 无规律的业务量波动 ② 有规律的业务量波动 ③ 无明显业务量波动 ④ 混合型业务 ⑤ 消息通知 ⑥ 生命周期挂钩 ⑦ 自定义方式 ⑧ 滚的升级 5、使用限制 第三章 主要定义 …...

基于ASP.NET+ SQL Server实现(Web)医院信息管理系统
医院信息管理系统 1. 课程设计内容 在 visual studio 2017 平台上,开发一个“医院信息管理系统”Web 程序。 2. 课程设计目的 综合运用 c#.net 知识,在 vs 2017 平台上,进行 ASP.NET 应用程序和简易网站的开发;初步熟悉开发一…...
Java多线程实现之Callable接口深度解析
Java多线程实现之Callable接口深度解析 一、Callable接口概述1.1 接口定义1.2 与Runnable接口的对比1.3 Future接口与FutureTask类 二、Callable接口的基本使用方法2.1 传统方式实现Callable接口2.2 使用Lambda表达式简化Callable实现2.3 使用FutureTask类执行Callable任务 三、…...
Python如何给视频添加音频和字幕
在Python中,给视频添加音频和字幕可以使用电影文件处理库MoviePy和字幕处理库Subtitles。下面将详细介绍如何使用这些库来实现视频的音频和字幕添加,包括必要的代码示例和详细解释。 环境准备 在开始之前,需要安装以下Python库:…...

分布式增量爬虫实现方案
之前我们在讨论的是分布式爬虫如何实现增量爬取。增量爬虫的目标是只爬取新产生或发生变化的页面,避免重复抓取,以节省资源和时间。 在分布式环境下,增量爬虫的实现需要考虑多个爬虫节点之间的协调和去重。 另一种思路:将增量判…...
CSS设置元素的宽度根据其内容自动调整
width: fit-content 是 CSS 中的一个属性值,用于设置元素的宽度根据其内容自动调整,确保宽度刚好容纳内容而不会超出。 效果对比 默认情况(width: auto): 块级元素(如 <div>)会占满父容器…...

MFC 抛体运动模拟:常见问题解决与界面美化
在 MFC 中开发抛体运动模拟程序时,我们常遇到 轨迹残留、无效刷新、视觉单调、物理逻辑瑕疵 等问题。本文将针对这些痛点,详细解析原因并提供解决方案,同时兼顾界面美化,让模拟效果更专业、更高效。 问题一:历史轨迹与小球残影残留 现象 小球运动后,历史位置的 “残影”…...

MyBatis中关于缓存的理解
MyBatis缓存 MyBatis系统当中默认定义两级缓存:一级缓存、二级缓存 默认情况下,只有一级缓存开启(sqlSession级别的缓存)二级缓存需要手动开启配置,需要局域namespace级别的缓存 一级缓存(本地缓存&#…...

自然语言处理——文本分类
文本分类 传统机器学习方法文本表示向量空间模型 特征选择文档频率互信息信息增益(IG) 分类器设计贝叶斯理论:线性判别函数 文本分类性能评估P-R曲线ROC曲线 将文本文档或句子分类为预定义的类或类别, 有单标签多类别文本分类和多…...