《优化接口设计的思路》系列:第四篇—接口的权限控制

在这里插入图片描述
系列文章导航
《优化接口设计的思路》系列:第一篇—接口参数的一些弯弯绕绕
《优化接口设计的思路》系列:第二篇—接口用户上下文的设计与实现
《优化接口设计的思路》系列:第三篇—留下用户调用接口的痕迹
《优化接口设计的思路》系列:第四篇—接口的权限控制

本文参考项目源码地址:summo-springboot-interface-demo

前言

大家好!我是sum墨,一个一线的底层码农,平时喜欢研究和思考一些技术相关的问题并整理成文,限于本人水平,如果文章和代码有表述不当之处,还请不吝赐教。

作为一名从业已达六年的老码农,我的工作主要是开发后端Java业务系统,包括各种管理后台和小程序等。在这些项目中,我设计过单/多租户体系系统,对接过许多开放平台,也搞过消息中心这类较为复杂的应用,但幸运的是,我至今还没有遇到过线上系统由于代码崩溃导致资损的情况。这其中的原因有三点:一是业务系统本身并不复杂;二是我一直遵循某大厂代码规约,在开发过程中尽可能按规约编写代码;三是经过多年的开发经验积累,我成为了一名熟练工,掌握了一些实用的技巧。

我们在做系统的时候,只要这个系统里面存在角色和权限相关的业务需求,那么接口的权限控制肯定必不可少。但是大家一搜接口权限相关的资料,出来的就是整合Shrio、Spring Security等各种框架,然后下面一顿贴配置和代码,看得人云里雾里。实际上接口的权限控制是整个系统权限控制里面很小的一环,没有设计好底层数据结构,是无法做好接口的权限控制的。那么怎么做一个系统的权限控制呢?我认为有以下几步:

那么接下来我就按这个流程一一给大家说明权限是怎么做出来的。(注:只需要SpringBoot和Redis,不需要额外权限框架。)

一、权限底层表结构设计

第一,只要一个系统是给人用的,那么这个系统就一定会有一张用户表;第二,只要有人的地方,就一定会有角色权限的划分,最简单的就是超级管理员、普通用户;第三,如此常见的设计,会有一套相对规范的设计标准。
权限底层表结构设计的标准就是:RBAC模型

1. RBAC模型简介

RBAC(Role-Based Access Control)权限模型的概念,即:基于角色的权限控制。通过角色关联用户,角色关联权限的方式间接赋予用户权限。

回到业务需求上来,应该是下面这样的要求:

上图可以看出,用户 多对多 角色 多对多 权限

用表结构展示的话就是这样,一共5张表,3张实体表,2张关联表

2. 建表语句

(1) t_user

DROP TABLE IF EXISTS `t_user`;
CREATE TABLE `t_user` (`user_id` bigint(20) unsigned zerofill NOT NULL AUTO_INCREMENT COMMENT '用户ID',`user_name` varchar(32) DEFAULT NULL COMMENT '用户名称',`gmt_create` datetime DEFAULT NULL COMMENT '创建时间',`gmt_modified` datetime DEFAULT NULL COMMENT '更新时间',`creator_id` bigint DEFAULT NULL COMMENT '创建人ID',`modifier_id` bigint DEFAULT NULL COMMENT '更新人ID',PRIMARY KEY (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ;

(2) t_role

DROP TABLE IF EXISTS `t_role`;
CREATE TABLE `t_role` (`role_id` bigint(20) unsigned zerofill NOT NULL AUTO_INCREMENT COMMENT '角色ID',`role_name` varchar(32) CHARACTER SET utf8mb4   DEFAULT NULL COMMENT '角色名称',`role_code` varchar(32) CHARACTER SET utf8mb4   DEFAULT NULL COMMENT '角色code',`gmt_create` datetime DEFAULT NULL COMMENT '创建时间',`gmt_modified` datetime DEFAULT NULL COMMENT '更新时间',`creator_id` bigint DEFAULT NULL COMMENT '创建人ID',`modifier_id` bigint DEFAULT NULL COMMENT '更新人ID',PRIMARY KEY (`role_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;

(3) t_auth

DROP TABLE IF EXISTS `t_auth`;
CREATE TABLE `t_auth` (`auth_id` bigint(20) unsigned zerofill NOT NULL AUTO_INCREMENT COMMENT '权限ID',`auth_code` varchar(32) DEFAULT NULL COMMENT '权限code',`auth_name` varchar(32) CHARACTER SET utf8mb4  DEFAULT NULL COMMENT '权限名称',`gmt_create` datetime DEFAULT NULL COMMENT '创建时间',`gmt_modified` datetime DEFAULT NULL COMMENT '更新时间',`creator_id` bigint DEFAULT NULL COMMENT '创建人ID',`modifier_id` bigint DEFAULT NULL COMMENT '更新人ID',PRIMARY KEY (`auth_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;

(4) t_user_role

DROP TABLE IF EXISTS `t_user_role`;
CREATE TABLE `t_user_role` (`id` bigint(20) unsigned zerofill NOT NULL AUTO_INCREMENT COMMENT '物理ID',`user_id` bigint NOT NULL COMMENT '用户ID',`role_id` bigint NOT NULL COMMENT '角色ID',`gmt_create` datetime DEFAULT NULL COMMENT '创建时间',`gmt_modified` datetime DEFAULT NULL COMMENT '更新时间',`creator_id` bigint DEFAULT NULL COMMENT '创建人ID',`modifier_id` bigint DEFAULT NULL COMMENT '更新人ID',PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

(5) t_role_auth

DROP TABLE IF EXISTS `t_role_auth`;
CREATE TABLE `t_role_auth` (`id` bigint(20) unsigned zerofill NOT NULL AUTO_INCREMENT COMMENT '物理ID',`role_id` bigint DEFAULT NULL COMMENT '角色ID',`auth_id` bigint DEFAULT NULL COMMENT '权限ID',`gmt_create` datetime DEFAULT NULL COMMENT '创建时间',`gmt_modified` datetime DEFAULT NULL COMMENT '更新时间',`creator_id` bigint DEFAULT NULL COMMENT '创建人ID',`modifier_id` bigint DEFAULT NULL COMMENT '更新人ID',PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

二、用户身份认证和授权

上面已经把表设计好了,接下来就是代码开发了。不过,在开发之前我们要搞清楚认证授权这两个词是啥意思。

  • 什么是认证?
    认证是确认一个用户的身份,确保用户是其所声称的人。它通过验证用户的身份信息,例如用户名和密码,来确认用户的身份。
  • 什么是授权?
    授权是根据用户的身份和权限,给予用户特定的访问权限或使用某些资源的权力。它确定用户可以执行的操作,并限制他们不能执行的操作。授权确保用户只能访问他们被允许的内容和功能。

光看定义也很难懂,这里我举个例子配合说明。

现有两个用户:小A和小B;两个角色:管理员和普通用户;4个操作:新增/删除/修改/查询。图例如下:

那么,对于小A来说,认证就是小A登录系统后,会授予管理员的角色,授权就是授予小A新增/删除/修改/查询的权限;
同理,对于小B来说,认证就是小B登录系统后,会授予普通用户的角色,授权就是授予小B查询的权限。

接下来且看如何实现

1. 初始化数据

t_user表数据

INSERT INTO `t_user` (`user_id`, `user_name`, `gmt_create`, `gmt_modified`, `creator_id`, `modifier_id`) VALUES (1, '小A', '2023-09-21 09:48:14', '2023-09-21 09:48:19', -1, -1);
INSERT INTO `t_user` (`user_id`, `user_name`, `gmt_create`, `gmt_modified`, `creator_id`, `modifier_id`) VALUES (2, '小B', '2023-09-21 09:48:14', '2023-09-21 09:48:19', -1, -1);

t_role表数据

INSERT INTO `t_role` (`role_id`, `role_name`, `role_code`, `gmt_create`, `gmt_modified`, `creator_id`, `modifier_id`) VALUES (1, '管理员', 'admin', '2023-09-21 09:52:45', '2023-09-21 09:52:47', -1, -1);
INSERT INTO `t_role` (`role_id`, `role_name`, `role_code`, `gmt_create`, `gmt_modified`, `creator_id`, `modifier_id`) VALUES (2, '普通用户', 'normal', '2023-09-21 09:52:45', '2023-09-21 09:52:47', -1, -1);

t_auth表数据

INSERT INTO `t_auth` (`auth_id`, `auth_code`, `auth_name`, `gmt_create`, `gmt_modified`, `creator_id`, `modifier_id`) VALUES (1, 'add', '新增', '2023-09-21 09:52:45', '2023-09-21 09:52:47', -1, -1);
INSERT INTO `t_auth` (`auth_id`, `auth_code`, `auth_name`, `gmt_create`, `gmt_modified`, `creator_id`, `modifier_id`) VALUES (2, 'delete', '删除', '2023-09-21 09:52:45', '2023-09-21 09:52:47', -1, -1);
INSERT INTO `t_auth` (`auth_id`, `auth_code`, `auth_name`, `gmt_create`, `gmt_modified`, `creator_id`, `modifier_id`) VALUES (3, 'query', '查询', '2023-09-21 09:52:45', '2023-09-21 09:52:47', -1, -1);
INSERT INTO `t_auth` (`auth_id`, `auth_code`, `auth_name`, `gmt_create`, `gmt_modified`, `creator_id`, `modifier_id`) VALUES (4, 'update', '更新', '2023-09-21 09:52:45', '2023-09-21 09:52:47', -1, -1);

t_user_role表数据

INSERT INTO `t_user_role` (`user_id`, `role_id`, `gmt_create`, `gmt_modified`, `creator_id`, `modifier_id`) VALUES (1, 1, '2023-09-21 09:52:45', '2023-09-21 09:52:47', -1, -1);
INSERT INTO `t_user_role` (`user_id`, `role_id`, `gmt_create`, `gmt_modified`, `creator_id`, `modifier_id`) VALUES (2, 2, '2023-09-21 09:52:45', '2023-09-21 09:52:47', -1, -1);

t_role_auth表数据

INSERT INTO `t_role_auth` (`role_id`, `auth_id`, `gmt_create`, `gmt_modified`, `creator_id`, `modifier_id`) VALUES (1, 2, '2023-09-21 09:52:45', '2023-09-21 09:52:47', -1, -1);
INSERT INTO `t_role_auth` (`role_id`, `auth_id`, `gmt_create`, `gmt_modified`, `creator_id`, `modifier_id`) VALUES (1, 1, '2023-09-21 09:52:45', '2023-09-21 09:52:47', -1, -1);
INSERT INTO `t_role_auth` (`role_id`, `auth_id`, `gmt_create`, `gmt_modified`, `creator_id`, `modifier_id`) VALUES (1, 3, '2023-09-21 09:52:45', '2023-09-21 09:52:47', -1, -1);
INSERT INTO `t_role_auth` (`role_id`, `auth_id`, `gmt_create`, `gmt_modified`, `creator_id`, `modifier_id`) VALUES (1, 4, '2023-09-21 09:52:45', '2023-09-21 09:52:47', -1, -1);
INSERT INTO `t_role_auth` (`role_id`, `auth_id`, `gmt_create`, `gmt_modified`, `creator_id`, `modifier_id`) VALUES (2, 3, '2023-09-21 09:52:45', '2023-09-21 09:52:47', -1, -1);

2、新增/user/login接口模拟登录

接口代码如下

@GetMapping("/login")
public ResponseEntity<String> userLogin(@RequestParam(required = true) String userName,HttpServletRequest httpServletRequest,HttpServletResponse httpServletResponse) {return userService.login(userName, httpServletRequest, httpServletResponse);
}

业务代码如下

@Override
public ResponseEntity<String> login(String userName, HttpServletRequest httpServletRequest,HttpServletResponse httpServletResponse) {//根据名称查询用户信息UserDO userDO = userMapper.selectOne(new QueryWrapper<UserDO>().lambda().eq(UserDO::getUserName, userName));if (Objects.isNull(userDO)) {return ResponseEntity.ok("未查询到用户");}//查询当前用户的角色信息List<UserRoleDO> userRoleDOList = userRoleMapper.selectList(new QueryWrapper<UserRoleDO>().lambda().eq(UserRoleDO::getUserId, userDO.getUserId()));if (CollectionUtils.isEmpty(userRoleDOList)) {return ResponseEntity.ok("当前用户没有角色");}//查询当前用户的权限List<RoleAuthDO> roleAuthDOS = roleAuthMapper.selectList(new QueryWrapper<RoleAuthDO>().lambda().in(RoleAuthDO::getRoleId, userRoleDOList.stream().map(UserRoleDO::getRoleId).collect(Collectors.toList())));if (CollectionUtils.isEmpty(roleAuthDOS)) {return ResponseEntity.ok("当前角色没有对应权限");}//查询权限codeList<AuthDO> authDOS = authMapper.selectList(new QueryWrapper<AuthDO>().lambda().in(AuthDO::getAuthId, roleAuthDOS.stream().map(RoleAuthDO::getAuthId).collect(Collectors.toList())));//生成唯一tokenString token = UUID.randomUUID().toString();//缓存用户信息redisUtil.set(token, JSONObject.toJSONString(userDO), tokenTimeout);//缓存用户权限信息redisUtil.set("auth_" + userDO.getUserId(),JSONObject.toJSONString(authDOS.stream().map(AuthDO::getAuthCode).collect(Collectors.toList())),tokenTimeout);//向localhost中添加CookieCookie cookie = new Cookie("token", token);cookie.setDomain("localhost");cookie.setPath("/");cookie.setMaxAge(tokenTimeout.intValue());httpServletResponse.addCookie(cookie);//返回登录成功return ResponseEntity.ok(JSONObject.toJSONString(userDO));
}

上面代码用流程图表示如下

3. 调用登录接口

小A登录:http://localhost:8080/user/login?userName=小A
小B登录:http://localhost:8080/user/login?userName=小B

(没画前端界面,大家将就看下哈)

小A登录调用返回如下

小B登录调用返回如下

三、用户权限验证逻辑

通过第二步,用户已经进行了认证、授权的操作,那么接下来就是用户验权:即验证用户是否有调用接口的权限。

1. 定义接口权限注解

前面定义了4个权限:新增/删除/修改/查询,分别对应着4个接口。这里我们使用注解进行一一对应。
注解定义如下:
RequiresPermissions.java

package com.summo.demo.config.permissions;import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequiresPermissions {/*** 权限列表* @return*/String[] value();/*** 权限控制方式,且或者和* @return*/Logical logical() default Logical.AND;}

该注解有两个属性,value和logical。value是一个数组,代表当前接口拥有哪些权限;logical有两个值AND和OR,AND的意思是当前用户必须要有value中所有的权限才可以调用该接口,OR的意思是当前用户只需要有value中任意一个权限就可以调用该接口。

注解处理代码逻辑如下:
RequiresPermissionsHandler.java

package com.summo.demo.config.permissions;import java.lang.reflect.Method;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;import com.alibaba.fastjson.JSONObject;import com.summo.demo.config.context.GlobalUserContext;
import com.summo.demo.config.context.UserContext;
import com.summo.demo.config.manager.UserManager;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;@Aspect
@Component
public class RequiresPermissionsHandler {@Autowiredprivate UserManager userManager;@Pointcut("@annotation(com.summo.demo.config.permissions.RequiresPermissions)")public void pointcut() {// do nothing}@Around("pointcut()")public Object around(ProceedingJoinPoint joinPoint) throws Throwable {//获取用户上下文UserContext userContext = GlobalUserContext.getUserContext();if (Objects.isNull(userContext)) {throw new RuntimeException("用户认证失败,请检查是否登录");}//获取注解MethodSignature signature = (MethodSignature)joinPoint.getSignature();Method method = signature.getMethod();RequiresPermissions requiresPermissions = method.getAnnotation(RequiresPermissions.class);//获取当前接口上数据权限String[] permissions = requiresPermissions.value();if (Objects.isNull(permissions) && permissions.length == 0) {throw new RuntimeException("用户认证失败,请检查该接口是否添加了数据权限");}//判断当前是and还是orString[] notHasPermissions;switch (requiresPermissions.logical()) {case AND://当逻辑为and时,所有的数据权限必须存在notHasPermissions = checkPermissionsByAnd(userContext.getUserId(), permissions);if (Objects.nonNull(notHasPermissions) && notHasPermissions.length > 0) {throw new RuntimeException(MessageFormat.format("用户权限不足,缺失以下权限:[{0}]", JSONObject.toJSONString(notHasPermissions)));}break;case OR://当逻辑为and时,所有的数据权限必须存在notHasPermissions = checkPermissionsByOr(userContext.getUserId(), permissions);if (Objects.nonNull(notHasPermissions) && notHasPermissions.length > 0) {throw new RuntimeException(MessageFormat.format("用户权限不足,缺失以下权限:[{0}]", JSONObject.toJSONString(notHasPermissions)));}break;default://默认为and}return joinPoint.proceed();}/*** 当数据权限为or时,进行判断** @param userId      用户ID* @param permissions 权限组* @return 没有授予的权限*/private String[] checkPermissionsByOr(Long userId, String[] permissions) {// 获取用户权限集Set<String> permissionSet = userManager.queryAuthByUserId(userId);if (permissionSet.isEmpty()) {return permissions;}//一一比对List<String> tempPermissions = new ArrayList<>();for (String permission1 : permissions) {permissionSet.forEach(permission -> {if (permission1.equals(permission)) {tempPermissions.add(permission);}});}if (Objects.nonNull(tempPermissions) && tempPermissions.size() > 0) {return null;}return permissions;}/*** 当数据权限为and时,进行判断** @param userId      用户ID* @param permissions 权限组* @return 没有授予的权限*/private String[] checkPermissionsByAnd(Long userId, String[] permissions) {// 获取用户权限集Set<String> permissionSet = userManager.queryAuthByUserId(userId);if (permissionSet.isEmpty()) {return permissions;}//如果permissions大小为1,可以单独处理一下if (permissionSet.size() == 1 && permissionSet.contains(permissions[0])) {return null;}if (permissionSet.size() == 1 && !permissionSet.contains(permissions[0])) {return permissions;}//一一比对List<String> tempPermissions = new ArrayList<>();for (String permission1 : permissions) {permissionSet.forEach(permission -> {if (permission1.equals(permission)) {tempPermissions.add(permission);}});}//如果tempPermissions的长度与permissions相同,那么说明权限吻合if (permissions.length == tempPermissions.size()) {return null;}//否则取出当前用户没有的权限,并返回用作提示List<String> notHasPermissions = Arrays.stream(permissions).filter(permission -> !tempPermissions.contains(permission)).collect(Collectors.toList());return notHasPermissions.toArray(new String[notHasPermissions.size()]);}}

2. 注解使用方式

使用比较简单,直接放到接口的方法上

@GetMapping("/add")
@RequiresPermissions(value = "add", logical = Logical.OR)
public ResponseEntity<String> add(@RequestBody AddReq addReq) {return userService.add(addReq);
}@GetMapping("/delete")
@RequiresPermissions(value = "delete", logical = Logical.OR)
public ResponseEntity<String> delete(@RequestParam Long userId) {return userService.delete(userId);
}@GetMapping("/query")
@RequiresPermissions(value = "query", logical = Logical.OR)
public ResponseEntity<String> query(@RequestParam String userName) {return userService.query(userName);
}@GetMapping("/update")
@RequiresPermissions(value = "update", logical = Logical.OR)
public ResponseEntity<String> update(@RequestBody UpdateReq updateReq) {return userService.update(updateReq);
}

3. 接口验权的流程

四、用户权限变动后的状态刷新

其实前面三步完成后,正向流已经完成了,但用户的权限是变化的,比如:

小B的权限从查询变为了查询更新

但小B的token还未过期,这时应该怎么办呢?

还记得登录的时候,我有缓存两个信息吗

对应代码中的

//缓存用户信息
redisUtil.set(token, JSONObject.toJSONString(userDO), tokenTimeout);
//缓存用户权限信息
redisUtil.set("auth_" + userDO.getUserId(),JSONObject.toJSONString(authDOS.stream().map(AuthDO::getAuthCode).collect(Collectors.toList())),tokenTimeout);

在这里我其实将token和权限是分开存储的,token只存用户信息,而权限信息用auth_userId为key进行存储的,这样就可以做到即使token还在,我也能动态修改当前用户的权限信息了,且权限实时变更不会影响用户体验。

不过,这个地方有一个争议的点
用户权限发生变更的时候,是更新权限缓存呢?还是直接删除用户的权限缓存呢?

我的建议是:删除权限缓存。原因有三

  • 用户权限缓存并不是一直存在,存在连缓存都没有的情况。
  • 缓存更新只适用于单个用户权限的更新,但是我要把角色和权限的关联变动了呢?
  • 直接把权限缓存删除,用户会不会报错?我查询权限缓存的方式是:先查询缓存,缓存没有在查询数据库,所以并不会出现缓存被删除就报错的情况。

tips:如何优雅的实现“先查询缓存再查询数据库?”请看我这篇文章:https://juejin.cn/post/7124885941117779998

五、认证失败或无权限等异常情况处理

出现由于权限不足或认证失败的问题,常见的做法有重定向到登录页、通知用户刷新界面等,具体怎么处理还要看产品是怎么要求的。
关于网站的异常有很多,权限相关的状态码是401、服务器错误的状态码是500,除此之外还会有自定义的错误码,我打算放在接口优化系列的后面用专篇说明,敬请期待哦~

写在最后

《优化接口设计的思路》系列已结写到第四篇了,前面几篇都没有总结,在这篇总结一下吧。

从我开始写博客到现在已经6年了,差不多也写了将近60篇左右的文章。刚开始的时候就是写SpringBoot,写SpringBoot如何整合Vue,那是2017年。

得益于老大的要求(或者是公司想省钱),刚工作的时候就是前后端代码都写,但是写的一塌糊涂,甚至连最基础的项目环境都搭不好。那时候在网上找个pom.xml配置,依赖死活下载不下来,后来才知道maven仓库默认国外的源,要把它换成国内的才能提高下载速度。那时候上班就是下午把项目跑起来了,第二天上午项目又启动不了了,如此循环往复,我的笔记里面存了非常多的配置文件。再后来技术水平提高了点,单项目终于会玩了,微服务又火起来了,了解过SpringCloud的小伙伴应该知道SpringCloud的版本更复杂,搭建环境更难。在这可能有人会疑惑,你不会不能去问人吗?我也很无奈,一则是社恐不敢问,二则是我们部门全是菜鸟,都等着我学会教他们呢…

后来我老大说,既然用不来人家的,那就自己写一套,想起来那时真单纯,我就真的自己开始写微服务架构。最开始我对微服务的唯一印象就是一个服务提供者、一个服务消费者,肯定是两个应用,至于为啥是这样,查的百度都是这样写的。然后我就建了两个应用,一个网关应用、一个业务应用,自己写HttpUtil进行服务间调用,也不知道啥是注册中心,我只知道网关应用那里要有业务应用的IP地址,否则网关调不了业务代码。当时的调用代码我已经找不了,只记得当时代码的形状很像一个“>”,用了太多的if…else…了!!!

那时候虽然代码写的很烂、bug一堆,但我们老大也没骂我们,每周四还会给我们上夜校,跟我们讲一些大厂的框架和技术栈。他跟我们说,现在多用用人家的技术,到时候出去面试大厂也容易一些。写博文也是老大让我们做的,他说现在一点点的积累,等到过几年就会变成文库了。现在想来,真是一个不错的老大!

现在2023年了,我还在写代码,但也不仅仅只是写代码,还带一些项目,独立负责的也有。要说我现在的代码水平嘛,属于那种工厂熟练工水平,八股里面的什么JVM调优啊、高并发系统架构设计啊我一次都没有接触到过,远远称不上大神。不过我还是想写一些文章,不是为了炫技,只是想把我工作中遇到的问题变成后续解决问题的经验,说真的这些文章已经开始帮到我了,如果它们也能帮助到你,荣幸之至!

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

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

相关文章

知识库搭建保姆级教程,如何从0到1完成知识库搭建

在这个信息爆炸的时代&#xff0c;如何获取、整理和应用知识成为了我们个体价值和企业核心竞争力打造的重要表现&#xff0c;搭建一个高效的知识库可以提升我们企业的竞争力&#xff0c;必要时还能快速切换赛道&#xff0c;开展一个新的领域。 今天我们将结合HelpLook 来与你一…

C++之互斥锁、读写锁、互斥量、 信号量、原子锁机制总结(二百二十五)

简介&#xff1a; CSDN博客专家&#xff0c;专注Android/Linux系统&#xff0c;分享多mic语音方案、音视频、编解码等技术&#xff0c;与大家一起成长&#xff01; 优质专栏&#xff1a;Audio工程师进阶系列【原创干货持续更新中……】&#x1f680; 人生格言&#xff1a; 人生…

MAC word 如何并列排列两张图片

系统&#xff1a;MAC os 参考博客 https://baijiahao.baidu.com/s?id1700824516945958911&wfrspider&forpc 步骤1 新建一个word文档和表格 修改表格属性 去掉自动重调尺寸以适应内容 插入图片 在表格的位置插入对应的图片如下 去除边框 最终结果如下

UE5 ChaosVehicles载具研究

一、基本组成 载具Actor类名称&#xff1a;WheeledVehiclePawn Actor最原始的结构 官方增加了两个摇臂相机&#xff0c;可以像驾驶游戏那样切换多机位、旋转观察 选择骨骼网格体、动画蓝图类、开启物理模拟 二、SportsCar_Pawn 角阻尼&#xff1a;物体旋转的阻力。数值越大…

云原生技术盛会KubeCon即将召开!亚马逊云科技作为钻石赞助商参会

KubeCon2023将于9月26-28日在上海跨国采购会展中心隆重召开。作为云原生领域最负盛名的技术大会之一&#xff0c;KubeConCloudNativeCon是连接全球开发者与云原生社区的最佳平台&#xff0c;此次还新增Open Source Summit环节&#xff0c;吸引了全球顶尖的云原生专家们汇聚其中…

数据链路层协议

文章目录 数据链路层协议0. 数据链路层解决的问题1. 以太网协议(1) 认识以太网(2) 以太网帧格式<1> 两个核心问题 (3) 认识MAC地址(4) 局域网通信原理(5) MTU<1> 认识MTU<2> MTU对IP协议的影响<3> MTU对UDP协议的影响<4> MTU对TCP协议的影响<…

Python开发与应用实验2 | Python基础语法应用

*本文是博主对学校专业课Python各种实验的再整理与详解&#xff0c;除了代码部分和解析部分&#xff0c;一些题目还增加了拓展部分&#xff08;⭐&#xff09;。拓展部分不是实验报告中原有的内容&#xff0c;而是博主本人自己的补充&#xff0c;以方便大家额外学习、参考。 &a…

安装Python3.x--Windows

1 下载安装包 确定安装是干什么&#xff0c;要下哪个版本&#xff08;如果是配置项目环境&#xff0c;最好按项目需求的版本来装&#xff09; 1.1 官网链接 https://www.python.org 最新版本 指定版本 2 安装说明 点击下载exe&#xff0c;运行自定义安装路径&#xff0c;下…

TS编译选项——不允许使用隐式any类型、不明确类型的this、严格检查空值、编译后文件自动设置严格模式

一、不允许使用隐式any类型 在tsconfig.js文件中配置noImplicitAny属性 {"compilerOptions": {// 不允许使用隐式any类型"noImplicitAny": true} } 开启后即可禁止使用隐式的any类型 注意&#xff1a;显式的any类型并不会被禁止 二、不允许使用不明确类…

腾讯mini项目-【指标监控服务重构】2023-08-26

今日已办 Venus 的 Trace 无感化 定义 handler 函数 fiber.Handler 的主要处理逻辑返回处理中出现的 error返回处理中响应 json 的函数 // handler // Description: // Author xzx 2023-08-26 18:00:03 // Param c // Return error // Return func() error : function for …

和 Node.js 说拜拜,Deno零配置解决方案

不知道大家注意没有&#xff0c;在我们启动各种类型的 Node repo 时&#xff0c;root 目录很快就会被配置文件塞满。例如&#xff0c;在最新版本的 Next.js 中&#xff0c;我们就有 next.config.js、eslintrc.json、tsconfig.json 和 package.json。而在样式那边&#xff0c;还…

Spring面试题9:Spring的BeanFactory和FactoryBean的区别和联系

该文章专注于面试,面试只要回答关键点即可,不需要对框架有非常深入的回答,如果你想应付面试,是足够了,抓住关键点 面试官:说一说Spring的BeanFactory和FactoryBean的区别和联系 区别:BeanFactory是一个工厂接口,主要负责管理和创建Bean实例。它是Spring提供的最底层的…

优维产品最佳实践:主机合规性检查

我们常常会感到这样的困惑&#xff0c;为什么这么多的无效主机记录&#xff0c;为什么这些主机很多信息空白&#xff0c;当许多人一起维护主机信息时&#xff0c;常常会出现信息错漏的情况。主机是运维最重要最基本的CMDB信息&#xff0c;而「合规性检查」为我们提供了更高效便…

Spring Cloud Alibaba Gateway 简单使用

文章目录 Spring Cloud Alibaba Gateway1.Gateway简介2. 流量网关和服务网关的区别3. Spring Cloud Gateway 网关的搭建3.1 Spring Cloud Gateway 配置项的说明3.2 依赖导入3.3 配置文件 Spring Cloud Alibaba Gateway 1.Gateway简介 Spring Cloud Gateway是一个基于Spring F…

混合Rollup:探秘 Metis、Fraxchain、Aztec、Miden和Ola

1. 引言 混合Rollup为新的以太坊L2扩容方案&#xff0c;其分为2大类&#xff1a; 将乐观与ZK技术结合的混合Rollup同时支持公开智能合约 和 私人智能合约 的混合Rollup 本文将重点关注Metis、Fraxchain、Aztec、Miden和Ola这五大项目。 2. 何为混合Rollup&#xff1f; 混合…

MySQL-树型结构数据查询

表结构 进行树形结构查询&#xff0c;利用特殊语法进行操作 with recursive t as(select parent_id , business_namefrom business_line where id 21union allselect a.parent_id, a.business_namefrom business_line a join t on a.id t.parent_id) select business_name f…

[AI Agent学习] MetaGPT源码浅析

前言 工作上&#xff0c;需要使用AI Agent&#xff0c;所以需要深入学习一下AI Agent&#xff0c;光阅读各种文章&#xff0c;总觉无法深入细节&#xff0c;所以开看各类AI Agent相关的开源项目&#xff0c;此为第一篇&#xff0c;学习一下MetaGPT的源码。 基本目标 MetaGPT是一…

【深度学习实验】前馈神经网络(八):模型评价(自定义支持分批进行评价的Accuracy类)

目录 一、实验介绍 二、实验环境 1. 配置虚拟环境 2. 库版本介绍 三、实验内容 0. 导入必要的工具包 1. __init__(构造函数) 2. update函数(更新评价指标) 5. accumulate(计算准确率) 4. reset(重置评价指标) 5. 构造数据进行测试 6. 代码整合 一、实验介绍 本文将实…

进行 XSS 攻击 和 如何防御

跨站脚本攻击&#xff08;XSS 攻击&#xff09;是 Web 开发中最危险的攻击之一。以下是它们的工作原理以及防御方法。 XSS 攻击 跨站脚本攻击就是在另一个用户的计算机上运行带有恶意的 JS 代码。假如我们的程序没有对这些恶意的脚本进行防御的话&#xff0c;他们就会由我们的…

李宏毅hw-10 ——adversarial attack

一、查漏补缺&#xff1a; 1.关于glob.glob的用法&#xff0c;返回一个文件路径的 列表&#xff1a; 当然&#xff0c;再套用1个sort&#xff0c;就是将所有的文件路径按照字母进行排序了 2.relpath relative_path返回相对于基准路径的相对路径的函数 二、代码剖析&#xff…