Spring Security 框架篇-深入了解 Spring Security 的授权核心功能(RBAC 权限模型、自定义异常处理器、校验权限方法)

🔥博客主页: 【小扳_-CSDN博客】
❤感谢大家点赞👍收藏⭐评论✍

文章目录

        1.0 权限系统

        1.1 引入

        1.2 RBAC 权限模型

        1.3 数据库设计

        2.0 Spring Security 核心功能-授权

        2.1 思路分析

        2.2 编写 SQL 语句

        2.3 将用户权限进行封装

        2.4 获取用户权限

        2.5 启动预授权

        3.0 自定义异常处理器

        4.0 自定义校验权限方法


        1.0 权限系统

        在 Java 开发中,Spring Security 是一个非常强大的框架,用于处理应用程序的安全性。它不仅提供了认证(Authentication)功能,还提供了授权(Authorization)功能,确保用户只能访问他们被授权的资源。

        Spring Security 框架中对认证功能进行介绍:

Spring Security 框架篇-深入了解 Spring Security 的认证功能流程和自定义实现登录接口(实现自定义认证过滤器、登出功能)-CSDN博客

        1.1 引入

Spring Security 的授权功能主要围绕着几个核心概念构建:

        1)权限(Permissions):
        权限是最低级别的安全控制,通常与特定的操作或资源相关联。例如,read、write、delete等权限可以被分配给特定的数据对象或业务操作。
        2)角色(Roles):
        角色是一组权限的集合。在应用中,通常会根据职责的不同来定义不同的角色,如管理员、普通用户、访客等。每个角色拥有一系列权限,这些权限定义了该角色可以执行的操作。
        3)访问决策管理器(Access Decision Manager):
        这个组件负责决定是否允许请求访问受保护的资源。它基于配置的策略(如一致同意、多数同意等)来评估所有相关的权限和角色。
        4)投票者(Voters):
        投票者是访问决策管理器的一部分,它们根据用户的权限对访问请求进行投票。投票结果影响最终的访问决策。
        5)方法安全(Method Security):
        Spring Security 提供了在方法级别上实现细粒度安全控制的能力。通过使用注解如 @PreAuthorize 和 @PostAuthorize,可以为特定的方法调用设置访问控制规则。
        6)URL 安全(URL Security):
        通过配置 HTTP 请求的安全性,可以限制对特定 URL 模式的访问。这通常是通过在 HttpSecurity 配置中定义匹配模式和相应的访问规则来实现的。

        1.2 RBAC 权限模型

        RBAC,即基于角色的访问控制,是一种广泛使用的安全模型,它将权限与角色关联起来,而不是直接与用户关联。这样做的好处包括但不限于:
        简化管理:通过管理少量的角色及其权限,而不是大量的个人用户权限,大大减少了管理工作量。
        提高灵活性:当组织结构或业务需求发生变化时,可以通过调整角色和权限来快速适应,而不需要逐个修改用户权限。
        增强安全性:通过最小权限原则,每个用户只拥有完成其工作所需的最少权限,从而减少潜在的安全风险。

在 Spring Security 中实现 RBAC 模型通常涉及以下步骤:
        定义角色:根据应用的需求定义不同的角色。
        分配权限:为每个角色分配适当的权限。
        用户角色映射:将用户与一个或多个角色关联起来。
        配置安全策略:利用 Spring Security 提供的工具(如 @Secured 注解、@PreAuthorize 表达式等)来定义哪些角色可以访问哪些资源或执行哪些操作。
通过这种方式,Spring Security 可以有效地支持复杂的权限管理和访问控制需求,帮助开发者构建更加安全可靠的应用程序。

        1.3 数据库设计

如果将其分解进行数据库设计,RBAC0基本模型可以分成以下五个部分:

        1)User(用户):每个用户都有唯一的UID识别,并被授予不同的角色
        2)Role(角色):不同角色具有不同的权限
        3)Permission(权限):访问权限
        4)用户-角色映射:用户和角色之间的映射关系
        5)角色-权限映射:角色和权限之间的映射

转换成数据库表的话大致如下所示:

        这就可以实现给用户进行授权相关的权限。

根据业务需求,对数据库进行设计:

-- ----------------------------
-- Table structure for sys_menu
-- ----------------------------
DROP TABLE IF EXISTS `sys_menu`;
CREATE TABLE `sys_menu`  (`id` bigint(0) NOT NULL AUTO_INCREMENT,`menu_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT 'NULL' COMMENT '菜单名',`path` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '路由地址',`component` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '组件路径',`visible` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT '0' COMMENT '菜单状态(0显示 1隐藏)',`status` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT '0' COMMENT '菜单状态(0正常 1停用)',`perms` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '权限标识',`icon` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT '#' COMMENT '菜单图标',`create_by` bigint(0) NULL DEFAULT NULL,`create_time` datetime(0) NULL DEFAULT NULL,`update_by` bigint(0) NULL DEFAULT NULL,`update_time` datetime(0) NULL DEFAULT NULL,`del_flag` int(0) NULL DEFAULT 0 COMMENT '是否删除(0未删除 1已删除)',`remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '备注',PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '菜单表' ROW_FORMAT = Dynamic;-- ----------------------------
-- Records of sys_menu
-- ----------------------------
INSERT INTO `sys_menu` VALUES (1, '部门管理', ' dept', ' system/dept/index', '0', '0', 'system:dept:list', '#', NULL, NULL, NULL, NULL, 0, NULL);
INSERT INTO `sys_menu` VALUES (2, ' 测试', 'test', 'system/test/index', '0', '0', 'system:test:list', '#', NULL, NULL, NULL, NULL, 0, NULL);-- ----------------------------
-- Table structure for sys_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_role`;
CREATE TABLE `sys_role`  (`id` bigint(0) NOT NULL AUTO_INCREMENT,`name` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,`role_key` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '角色权限字符串',`status` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT '0' COMMENT '角色状态(0正常 1停用)',`del_flag` int(0) NULL DEFAULT 0 COMMENT 'del_flag',`create_by` bigint(0) NULL DEFAULT NULL,`create_time` datetime(0) NULL DEFAULT NULL,`update_by` bigint(0) NULL DEFAULT NULL,`update_time` datetime(0) NULL DEFAULT NULL,`remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '备注',PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '角色表' ROW_FORMAT = Dynamic;-- ----------------------------
-- Records of sys_role
-- ----------------------------
INSERT INTO `sys_role` VALUES (1, 'CEO', 'ceo', '0', 0, NULL, NULL, NULL, NULL, NULL);
INSERT INTO `sys_role` VALUES (2, 'Coder', ' coder', '0', 0, NULL, NULL, NULL, NULL, NULL);-- ----------------------------
-- Table structure for sys_role_menu
-- ----------------------------
DROP TABLE IF EXISTS `sys_role_menu`;
CREATE TABLE `sys_role_menu`  (`role_id` bigint(0) NOT NULL AUTO_INCREMENT COMMENT '角色ID',`menu_id` bigint(0) NOT NULL DEFAULT 0 COMMENT '菜单id',PRIMARY KEY (`role_id`, `menu_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;-- ----------------------------
-- Records of sys_role_menu
-- ----------------------------
INSERT INTO `sys_role_menu` VALUES (1, 1);
INSERT INTO `sys_role_menu` VALUES (1, 2);
INSERT INTO `sys_role_menu` VALUES (2, 2);-- ----------------------------
-- Table structure for sys_user
-- ----------------------------
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user`  (`id` bigint(0) NOT NULL AUTO_INCREMENT COMMENT '主键',`user_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT 'NULL' COMMENT '用户名',`nick_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT 'NULL' COMMENT '昵称',`password` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT 'NULL' COMMENT '密码',`status` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT '0' COMMENT '账号状态(0正常 1停用)',`email` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '邮箱',`phonenumber` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '手机号',`sex` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '用户性别(0男,1女,2未知)',`avatar` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '头像',`user_type` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '1' COMMENT '用户类型(0管理员,1普通用户)',`create_by` bigint(0) NULL DEFAULT NULL COMMENT '创建人的用户id',`create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',`update_by` bigint(0) NULL DEFAULT NULL COMMENT '更新人',`update_time` datetime(0) NULL DEFAULT NULL COMMENT '更新时间',`del_flag` int(0) NULL DEFAULT 0 COMMENT '删除标志(0代表未删除,1代表已删除)',PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '用户表' ROW_FORMAT = Dynamic;-- ----------------------------
-- Records of sys_user
-- ----------------------------
INSERT INTO `sys_user` VALUES (1, 'xbs', '小扳手', '$2a$10$WCD7xp6lxrS.PvGmL86nhuFHMKJTc58Sh0dG1EQw0zSHjlLFyFvde', '0', NULL, NULL, NULL, NULL, '1', NULL, NULL, NULL, NULL, 0);
INSERT INTO `sys_user` VALUES (2, 'test', '测试用户', '$10$WCD7xp6lxrS.PvGmL86nhuFHMKJTc58Sh0dG1EQw0zSHjlLFyFvde', '0', NULL, NULL, NULL, NULL, '1', NULL, NULL, NULL, NULL, 0);-- ----------------------------
-- Table structure for sys_user_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_user_role`;
CREATE TABLE `sys_user_role`  (`user_id` bigint(0) NOT NULL AUTO_INCREMENT COMMENT '用户id',`role_id` bigint(0) NOT NULL DEFAULT 0 COMMENT '角色id',PRIMARY KEY (`user_id`, `role_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;-- ----------------------------
-- Records of sys_user_role
-- ----------------------------
INSERT INTO `sys_user_role` VALUES (1, 1);
INSERT INTO `sys_user_role` VALUES (2, 2);

        2.0 Spring Security 核心功能-授权

        2.1 思路分析

        在登录验证过程中,即在实现 UserDetailsService 接口的 MyUserDetailService 实现类里面除了将用户的基本信息进行封装返回,还需要将用户的权限信息也要进行封装返回。

        2.2 编写 SQL 语句

以用户 "xbs" 为例子:

        当然可以直接通过多个表关联查询,这里将 SQL 分开是为了更好梳理用户、角色、权限之间的关系。

        2.3 将用户权限进行封装

        由于需要封装到 LoginUser 类中,则添加 List<String> permissions 成员变量来存放权限信息,且为了其他类可以获取到这些权限信息,还需要重写 getAuthorities() 方法,返回权限信息。

@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginUser implements UserDetails {/** 使用构造方法初始化 */private User user;/** 存储权限信息 */private List<String> permissions;/** 存储SpringSecurity所需要的权限信息的集合 */@JSONField(serialize = false)private List<GrantedAuthority> authorities;/** 重写getAuthorities方法,将String类型的权限List转换成GrantedAuthority类型的集合 */@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {if (authorities != null){return authorities;}authorities = permissions.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());return authorities;}public LoginUser(User user, List<String> roles) {this.user = user;this.permissions = roles;}@Overridepublic String getPassword() {return user.getPassword();}@Overridepublic String getUsername() {return user.getUserName();}/** 下面的方法暂时全部都让他们返回true */@Overridepublic boolean isAccountNonExpired() {return true;}@Overridepublic boolean isAccountNonLocked() {return true;}@Overridepublic boolean isCredentialsNonExpired() {return true;}@Overridepublic boolean isEnabled() {return true;}
}

        如果其他类频繁的调用  getAuthorities() 方法来获取权限信息,就会重复的进行封装 GrantedAuthority 实体类,因此,当 authorities 不为 Null,说明之前已经被调用 getAuthorities() 方法来获取权限信息,现在直接可以从 authorities 实体获取权限信息即可。

        这里需要注意的是因为 SimpleGrantedAuthority 无法序列化,如果直接使用,fastjson 转换时就会出错,即 Redis 存取时会运行异常,因此我们需要使用 @JSONField 关闭其序列化; 

MyUserDetailService 类:

@Service
public class MyUserDetailService implements UserDetailsService {@Autowiredprivate UserMapper userMapper;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {//根据username查询数据库User user = userMapper.selectByUserName(username);if (user == null){throw new RuntimeException("用户不存在");}//还需要查询用户权限信息List<String> roles = userMapper.selectUserRoleByUserId(user.getId());//将查询到的结果user和权限进行封装return new LoginUser(user,roles);}
}

        2.4 获取用户权限

        在登录认证中,需要获取用户权限,且将用户权限存放到 SecurityContextHolder 容器中,方便在后续的过滤器进行权限校验。

        在 MySecurityFilter 登录认证过滤器中,验证成功之后,将获取的用户权限加载到 SecurityContextHolder 容器,在后续的 FlterSecurityInterceptor 权限校验器中进行校验,如果校验失败则会抛出 403 异常。

代码如下:

MySecurityFilter 登录认证过滤器

@Configuration
public class MySecurityFilter extends OncePerRequestFilter {@Autowiredprivate RedisCache redisCache;@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {//先去尝试获取从请求头中获取token数据String jwt = request.getHeader("token");//判断token是否为nullif (StringUtils.isNullOrEmpty(jwt)){//这里有两种情况//第一种就是正在去登录过程//第二种就是token过期了//因此直接放行即可,在SpringSecurity后续的过滤器中会进行判断filterChain.doFilter(request,response);return;}//如果token不为null,则需要去解析tokenString userId;try {//解析tokenuserId = JwtUtil.parseJWT(jwt).getSubject();} catch (Exception e) {e.printStackTrace();//解析失败,直接返回错误信息throw new RuntimeException("token非法");}//根据用户id去Redis中查找缓存信息LoginUser loginUser = redisCache.getCacheObject("login:" + userId);if (Objects.isNull(loginUser)){throw new RuntimeException("用户登录已过期,请重新登录");}//获取用户权限信息Collection<? extends GrantedAuthority> authorities = loginUser.getAuthorities();//将用户的基本信息与权限信息封装到UsernamePasswordAuthenticationToken中UsernamePasswordAuthenticationToken loginUserToken= new UsernamePasswordAuthenticationToken(loginUser,null,authorities);//再将封装好的信息放入SecurityContextHolder中,为了后续的过滤器进行判断SecurityContextHolder.getContext().setAuthentication(loginUserToken);//放行filterChain.doFilter(request,response);}
}

        2.5 启动预授权

        Spring Security 为我们提供了基于注解的权限控制方案,可以使用注解去指定访问对应的资源所需的权限,需要手动开启,在 SecurityConfig 配置类上开启预授权功能。

        在开启了预授权之后,就可以使用 @PreAuthorize 注解对接口进行权限控制了,该注解中使用 SPEL 表达式进行权限配置,表达式中的参数为权限名,对应我们获取的 perms 权限集合。

举个例子:

        使用 @PreAuthorize 注解,调用 hasAuthority("权限标识") 方法,如果该用户存在该权限,则返回 true,证明该用户有权限访问该接口;如果该用户不存在该权限,则返回 false,说明该用户不存在该权限,不允许访问当前接口。

相关源码:

        3.0 自定义异常处理器

        这写着认证授权时我们会发现有些地方我们是手动抛了异常的,但是为什么在返回的数据中并没有我们想要的数据呢?

        这是因为在 Spring Security 中默认对抛出的异常进行了捕获并对其进行了处理,因此就会返回上图中的数据。但是没明显我们并不是很想让他返回这种数据,而是在抛异常时也能返回我们的统一返回格式,这样可以让前端能对响应进行统一的处理。要实现这个功能我们需要知道 Spring Security 的异常处理机制。

        在 Spring Security 过滤链中,可以通过重写相关的方法来自定义异常处理器。

1)对于登录验证过程中抛出的异常:

        需要实现 AuthenticationEntryPoint 接口,重写 commence() 方法,从而来手动自定义异常处理器。

        这个可以使用统一返回格式,使用 Result 对象封装好相关的信息,然后通过 httpServletResponse.getWriter() 方法得到 PrintWriter 对象,再由该对象调用 write() 方法写到客户端。

        举个简单的例子,这里就直接返回字符串给客户端了。

@Component
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {@Overridepublic void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {PrintWriter writer = httpServletResponse.getWriter();writer.write("登录验证过程中抛出的异常");}
}

2)对于授权验证过程中抛出的异常:

        需要实现 AccessDeniedHandler 接口,重写 handle() 方法。

@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {@Overridepublic void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {PrintWriter writer = httpServletResponse.getWriter();writer.write("权限不足");}
}

        在编写完成对应的处理器之后,需要配置给 Spring Security 框架,让框架使用我们自定义的异常处理器,这同样是在 SecurityConfig 配置类中的 configure 方法进行配置。

SecurityConfig 配置类:

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {/*** 将BCryptPasswordEncoder加入到容器中**/@Beanpublic PasswordEncoder passwordEncoder(){return new BCryptPasswordEncoder();}@Bean@Overridepublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}@Autowiredprivate MySecurityFilter mySecurityFilter;@Autowiredprivate MyAuthenticationEntryPoint myAuthenticationEntryPoint;@Autowiredprivate AccessDeniedHandlerImpl accessDeniedHandlerImpl;@Overrideprotected void configure(HttpSecurity http) throws Exception {http//关闭csrf.csrf().disable()//不通过Session获取SecurityContext.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authorizeRequests().antMatchers("/userLogin").anonymous()// 除上面外的所有请求全部需要鉴权认证.anyRequest().authenticated();// 参数列表分别为需要插入的过滤器和标识过滤器的字节码http.addFilterBefore(mySecurityFilter, UsernamePasswordAuthenticationFilter.class);//配置自定义异常处理器http.exceptionHandling().authenticationEntryPoint(myAuthenticationEntryPoint).accessDeniedHandler(accessDeniedHandlerImpl);}
}

        4.0 自定义校验权限方法

        在 Spring Security 框架中,一般是通过 @PreAuthorize("hasAuthority('权限标识')") 这种方法来进行校验的,也可以手动自定义来创建校验权限的方法,会更加灵活。

        首先定义一个实体类,且将该类交给 IOC 容器成为 Bean 对象:

@Component("ex")
public class ExpressionRoot {public boolean hasAuthority(String authority){//先获取当前用户权限Authentication authentication = SecurityContextHolder.getContext().getAuthentication();LoginUser principal = (LoginUser) authentication.getPrincipal();//再获取权限集合List<String> permissions = principal.getPermissions();//判断用户权限集合中是否存在该权限return permissions.contains(authority);}
}

        在 SecurityContextHolder 容器中获取用户权限信息,直接调用 getPermissions() 方法获取到权限集合。接着判断用户权限集合是否包含当前接口所需要的权限 authority 。

        然后在 @PreAuthorize 注解中调用该类的方法即可:

    @GetMapping("/hello")@PreAuthorize("@ex.hasAuthority('system:test:list')")public String hello(){return "hello xbs";}

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

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

相关文章

使用 API 和离线库查询 IP 地址方法详解

目录 一、IP 地址查询能获取哪些信息1.地理位置信息2.网络信息3.网络类型 二、IP 地址查询方法&#xff0c;附代码1.在线查询 IP 地址方法2.使用 API 进行 IP 地址查询3.使用离线库进行 IP 地址查询 互联网监管部门要求公开 IP 归属地&#xff0c;引起了很大热度&#xff0c;但…

微服务day02

教学文档&#xff1a; 黑马教学文档 Docker Docker的安装 镜像和容器 命令解读 常见命令 案例 查看DockerHub&#xff0c;拉取Nginx镜像&#xff0c;创建并运行容器 搜索Nginx镜像&#xff1a;在 www.hub.docker.com 网站进行查询 拉取镜像&#xff1a; docker pull ngin…

一个小程序如何对接多个收款账户?

背景 我又来了&#xff0c;之前对接过网约巴士系统 网约巴士旅游专线平台搭建历程&#xff0c;运营了两年多了。在运营中完善、在完善中学习&#xff0c;一直是不变的真理。有一句话说得好&#xff1a;先做一个垃圾、用起来再说。 今天又需要升级了&#xff0c;需求是&#…

基于航片的玉米异常情况识别赛题正在报名中 | CCF BDCI进行时

一年一度的行业盛事2024 CCF大数据与计算智能大赛&#xff08;简称2024 CCF BDCI&#xff09;又在激烈进行中啦 多个赛题等你挑战&#xff0c;还没有报名的伙伴们抓紧时间咯&#xff0c;叫上你伙伴练起来吧&#xff01; 2024 CCF大数据与计算智能大赛 CCF大数据与计算智能大…

面试题:Spring(一)

1. Spring框架中bean是单例么&#xff1f; Service Scope("singleton") public class UserServiceImpl implements UserService { }singleton : bean在每个Spring IOC容器中只有一个实例。prototype&#xff1a;一个bean的定义可以有多个实例。 2. Spring框架中的…

Android View事件分发

目录 1.什么是View事件分发&#xff1f; 2.事件的类型 3.事件的发生 4.事件分发的方法 4.1 dispatchTouchEvent() 4.2 onTouchEvent() 4.3 onInterceptTouchEvent() 5.滑动冲突 5.1 外部拦截法 5.2内部拦截法 6.onTouch的执行高于onClick 7. onTouch()和onTouchEve…

uniapp 实现瀑布流

效果演示 组件下载 瀑布流布局-waterfall - DCloud 插件市场

6.qsqlquerymodel源码分析

目录 继承关系入口浅析qsqlquery刷新数据 扩展列或者移除列以及取别名读取数据与增减行读取数据 下一章节&#xff1a;如何使用qsqlquerymodel 与 qtableview实现自定义表格 继承关系 qsqlquerymodel 继承与qabstracttablemodel 入口 负责填充数据 void QSqlQueryModel::s…

Vue3中使用LogicFlow实现简单流程图

实现结果 实现功能&#xff1a; 拖拽创建节点自定义节点/边自定义快捷键人员选择弹窗右侧动态配置组件配置项获取/回显必填项验证历史记录&#xff08;撤销/恢复&#xff09; 自定义节点与拖拽创建节点 拖拽节点面板node-panel.vue <template><div class"node-…

Devops业务价值流:软件研发最佳实践

在当今快速迭代的软件开发环境中&#xff0c;DevOps业务价值流已成为推动软件研发高效与质量并重的关键实践。软件研发阶段作为产品生命周期的核心环节&#xff0c;其每一步都承载着将创意转化为现实的重要使命。在历经需求澄清的精准定位、架构设计的宏观规划以及项目初始化的…

wireshark工具使用

复制数据 1.右键展开整帧数据 2.复制“所有可见项目” mark标记数据 标记&#xff1a; 跳转&#xff1a; 保存成文件&#xff1a; 文件–>导出特定分组—>Marked packets only

管理 Elasticsearch 变得更容易了,非常容易!

作者&#xff1a;来自 Elastic Ken Exner Elasticsearch 用户&#xff0c;我们听到了你的心声。管理 Elasticsearch 有时会变得很复杂&#xff0c;面临的挑战包括性能调整、问题检测和资源优化。我们一直致力于简化你的体验。今天&#xff0c;我们宣布了自收购 Opster 以来的一…

深度洞察| 超6亿银发精准流量,40+泛银发群体参与消费三大变化

作者 | NewAgingPro团队 前言 9月24日&#xff0c;AgeClub成立银发流量及场景联盟&#xff08;简称&#xff1a;AgeMCN&#xff09;&#xff0c;助力银发经济高质量发展。 10月11日&#xff0c;AgeClub发布《2024银发流量全景洞察报告》&#xff0c;探索银发流量发展新模式…

Spring Boot——日志介绍和配置

1. 日志的介绍 在前面的学习中&#xff0c;控制台上打印出来的一大堆内容就是日志&#xff0c;可以帮助我们发现问题&#xff0c;分析问题&#xff0c;定位问题&#xff0c;除此之外&#xff0c;日志还可以进行系统的监控&#xff0c;数据采集等 2. 日志的使用 在程序中获取日…

Redis 组网方式入门

文章目录 一、组网方式1. 单实例模式描述优点缺点适用场景 2. 主从复制模式&#xff08;Master-Slave Replication&#xff09;描述优点缺点适用场景基于docker的redis主从复制1. 配置主节点2. 配置从节点3. 查看节点状态4. 验证主从数据同步5. 查看同步进度 3. 哨兵模式&#…

信号-2-信号捕捉

相关概念&#xff1a;递达 未决 / 阻塞 忽略 阻塞 vs 忽略 阻塞&#xff1a; 如果指定信号信号被阻塞&#xff0c; block期间该信号不能被递达&#xff0c;一直在pending表中。知道block被撤销后&#xff0c; 该信号才能递达&#xff0c;递达后对应pending位置置零。 忽…

(蓝桥杯C/C++)——基础算法(下)

目录 一、时空复杂度 1.时间复杂度 2.空间复杂度 3.分析技巧 4.代码示例 二、递归 1.递归的介绍 2.递归如何实现 3.递归和循环的比较 4.代码示例 三、差分 1.差分的原理和特点 2.差分的实现 3.例题讲解 四、枚举 1.枚举算法介绍 2.解空间的类型 3. 循环枚举解…

【极限编程(XP)】

极限编程&#xff08;XP&#xff09;简介 定义与核心价值观&#xff1a;极限编程&#xff08;Extreme Programming&#xff0c;XP&#xff09;是一种轻量级、敏捷的软件开发方法。它强调团队合作、客户参与、持续测试和快速反馈等价值观&#xff0c;旨在提高软件开发的效率和质…

如何编写安全的 Go 代码

原文&#xff1a;Jakub Jarosz - 2024.11.02 在编写 Go 代码时&#xff0c;如何时刻考虑安全性&#xff1f;要在一篇简短的文章中回答这个问题似乎不太可能。因此&#xff0c;我们将把范围缩小到一些具体做法上。 这些实践如果持续应用&#xff0c;将有助于我们编写健壮、安全…

Go八股(Ⅳ)***slice,string,defer***

***slice&#xff0c;string&#xff0c;defer*** 1.slice和arry的区别 arry&#xff1a; Go语言中arry即为数据的一种集合&#xff0c;需要在声明时指定容量和初值&#xff0c;且一旦声明就长度固定&#xff0c;访问时按照索引访问。通过内置函数len可以获取数组中的元素个…