• 原创|对接三方服务商回调鉴权的程序代码设计


    通过系统应用服务总会与三方服务商进行对接,既然有对接,就会有回调。但是此应用服务由于部署在公网访问,为了考虑系统安全系以及防止报文被篡改,这就意味着我们需要跟三方服务商进行鉴权技术方案设计。此文章,就是一个具体典型的案例,由于此应用服务有两个不同的场景,但是鉴权设计上又有不同差异之处,所以在总体程序设计上巧妙的满足场景的需求前提下,又能尽可能做到更好的扩展维护。

    一、背景

    此次涉及到对接三方的两个不同场景,暂且定位场景1和场景2。场景1的鉴权方案就是通过http接口回调,在请求头+请求报文上做鉴权处理,具体鉴权机制:请求头中的签名=md5(base64(报文)+回调url+私钥+时间戳)。
    而场景2的鉴权就是在请求报文中增加鉴权字段,该鉴权字段=md5(秘钥+字段1+字段2+字段3+…)。
    总而言之,都是通过md5加密,只不过加密的数据步骤有些区别。
    为了考虑减少代码的耦合度,同时尽可能提高后续的扩展性,在程序设计上引入了设计模式。

    二、详细设计

    1、UML设计

    在这里插入图片描述
    从上图可以看出,依然采用定义一个上下文对象BaseAuthenticateContext,该类定义一个泛型,意味着需要子类来继承,并指定请求参数类。通过AbstractAuthenticateHandler它来封装鉴权的共性逻辑,比如鉴权流程,以及相关复用的代码。相关子类来继承它,实现相关抽象方法即可。AuthenticateDispatcher这个类来对外暴露,外部调用无需晓得具体使用哪个Handler来处理,还需要委托给它即可。

    2、程序设计

    2.1、AuthenticateActionEnum

    定义一个枚举,来维护所有的鉴权场景类型,这里把场景抽象成Action

    /**
     * 鉴权活动枚举类型
     *
     * @author : Sieg Heil
     * @since 2022/11/25 10:30 AM
     */
    @Getter
    @ToString
    public enum AuthenticateActionEnum {
        /**
         * 企业直播活动变更
         */
        ACTIVITY_STATUS_CHANGE("企业直播活动变更"),
        /**
         * 视频点播事件通知
         */
        VOD_EVENT_NOTIFY("视频点播事件通知");
    
        /**
         * 构造函数
         *
         * @param desc 描述
         */
        AuthenticateActionEnum(String desc) {
            this.desc = desc;
        }
    
        /**
         * 描述
         */
        private final String desc;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    2.2、AuthenticateDispatcher

    通过@Autowired这个注解,把AbstractAuthenticateHandler的子类集合自动装配,作为该类的一个成员。同时,提供一个分发的方法。

    /**
     * 鉴权处理分发器
     *
     * @author : Sieg Heil
     * @since 2022/11/25 10:28 AM
     */
    @Component
    public class AuthenticateDispatcher {
    
        @Autowired
        private List<AbstractAuthenticateHandler> handlerList;
    
        /**
         * 执行处理
         *
         * @param context 上下文对象
         */
        public void execute(BaseAuthenticateContext context) {
            handlerList.stream()
                .filter(handler -> handler.getAction() == context.getAction())
                .forEach(handler -> handler.execute(context));
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    2.3、BaseAuthenticateContext

    定义一个上下文类。该类,包含一个内部静态类Response,并作为它的成员属性,来封装鉴权执行结果。

    /**
     * 回调鉴权上下文对象
     *
     * @author : Sieg Heil
     * @since 2022/11/25 10:08 AM
     */
    @ToString
    @Getter
    @Setter
    public abstract class BaseAuthenticateContext<Request> {
        /**
         * 活动类型
         */
        private AuthenticateActionEnum action;
        /**
         * 请求参数
         */
        private Request request;
        /**
         * 响应结果
         */
        private Response response;
    
        @ToString
        @Getter
        @Setter
        public static class Response {
            /**
             * 静态变量
             */
            public static String SUCCESS = "鉴权成功";
            /**
             * 鉴权是否成功
             */
            private boolean success;
            /**
             * 鉴权结果
             */
            private String result;
    
            /**
             * 静态方法
             *
             * @param result 鉴权结果
             * @return 响应对象
             */
            public static Response buildSuccess(String result) {
                Response response = new Response();
                response.setResult(result);
                response.setSuccess(Boolean.TRUE);
                return response;
            }
    
            /**
             * 静态方法
             *
             * @param result 鉴权结果
             * @return 响应对象
             */
            public static Response buildFailure(String result) {
                Response response = new Response();
                response.setResult(result);
                response.setSuccess(Boolean.FALSE);
                return response;
            }
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    2.3.1、ActivityStatusChangeAuthenticateContext

    具体的一个场景子类

    /**
     * [企业直播活动变更]回调鉴权上下文对象
     *
     * @author : Sieg Heil
     * @since 2022/11/25 10:08 AM
     */
    @ToString(callSuper = true)
    @Getter
    @Setter
    public class ActivityStatusChangeAuthenticateContext extends BaseAuthenticateContext<SubscribeLiveActivityStatusChangeRequest> {
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    2.3.2、VodEventNotifyAuthenticateContext

    具体的一个场景子类

    /**
     * [视频点播事件通知]回调鉴权上下文对象
     *
     * @author : Sieg Heil
     * @since 2022/11/25 10:08 AM
     */
    @ToString(callSuper = true)
    @Getter
    @Setter
    public class VodEventNotifyAuthenticateContext extends BaseAuthenticateContext<VolcVodRequestContext> {
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    2.4、AbstractAuthenticateHandler

    鉴权处理类的基类,外部暴露的公共方法为public void execute(Context context)。该方法内部封装了具体鉴权的相关步骤,相关子类只需要实现相关抽象方法即可。

    三个重要抽象方法:

    • abstract String getTraceId(Context context) :用于获取请求的traceId,便于日志打印,后续方便追踪问题。
    • abstract void doExecute(Context context):用于做具体的鉴权执行逻辑
    • abstract AuthenticateConfig getConfig():获取处理类场景的鉴权配置,该配置可以通过yml配置文件或者apollo实现,管理维护相关鉴权配置参数。
    /**
     * 抽象鉴权处理器
     *
     * @author : Sieg Heil
     * @since 2022/11/25 10:14 AM
     */
    @Slf4j
    public abstract class AbstractAuthenticateHandler<Context extends BaseAuthenticateContext> implements LoggerService {
    
        @Autowired
        protected VolcAuthenticateApolloConfig volcAuthenticateApolloConfig;
    
        @PostConstruct
        void init() {
            getLog().info("AuthenticateApolloConfig={}", JsonUtils.toJson(volcAuthenticateApolloConfig));
        }
    
        /**
         * 鉴权活动类型
         */
        protected AuthenticateActionEnum action;
        /**
         * 活动名称
         */
        protected String actionName;
    
        /**
         * 构造函数
         *
         * @param action 活动类型
         */
        public AbstractAuthenticateHandler(AuthenticateActionEnum action) {
            this.action = action;
            if (Objects.nonNull(action)) {
                this.actionName = action.name();
            }
        }
    
        /**
         * 对外部方法
         *
         * @param context
         */
        public void execute(Context context) {
            String traceId = getTraceId(context);
            if (logDebug()) {
                getLog().info("[{}|{}],context={}", traceId, actionName, JsonUtils.toJson(context));
            }
            AuthenticateConfig config = getConfig();
            if (null == config) {
                context.setResponse(BaseAuthenticateContext.Response.buildSuccess(SUCCESS));
                return;
            }
            boolean enableSwitch = Optional.ofNullable(config.getEnableSwitch()).orElse(Boolean.FALSE);
            //如果没有开启鉴权,则不执行鉴权
            if (!enableSwitch) {
                context.setResponse(BaseAuthenticateContext.Response.buildSuccess(SUCCESS));
                return;
            }
            doExecute(context);
            BaseAuthenticateContext.Response response = context.getResponse();
            getLog().info("[{}|{}]{}", traceId, actionName, JsonUtils.toJson(response));
            if (!response.isSuccess()) {
                throw new ForbiddenException("鉴权失败[" + response.getResult() + "]", response.getResult());
            }
        }
    
        @Override
        public boolean logDebug() {
            Boolean enableLogDebug = volcAuthenticateApolloConfig.getEnableLogDebug();
            Boolean enable = Optional.ofNullable(enableLogDebug).orElse(Boolean.TRUE);
            return enable.booleanValue();
        }
    
        /**
         * 获取一个traceId,用于问题排查使用
         *
         * @param context 上下文对象
         * @return traceId
         */
        protected abstract String getTraceId(Context context);
    
        /**
         * 执行鉴权
         * 需要子类实现此方法,完成具体的健全处理
         *
         * @param context 上下文对象
         */
        protected abstract void doExecute(Context context);
    
        /**
         * 获取鉴权配置
         *
         * @return 鉴权配置
         */
        protected abstract AuthenticateConfig getConfig();
    
        public AuthenticateActionEnum getAction() {
            return action;
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    2.4.1、ActivityStatusChangeAuthenticateHandler

    鉴权场景1的具体鉴权逻辑。

    /**
     * [企业直播活动变更]回调鉴权处理器
     *
     * @author : Sieg Heil
     * @since 2022/11/25 10:20 AM
     */
    @Component
    @Slf4j
    public class ActivityStatusChangeAuthenticateHandler extends AbstractAuthenticateHandler<ActivityStatusChangeAuthenticateContext> {
    
        /**
         * 构造函数
         */
        public ActivityStatusChangeAuthenticateHandler() {
            super(AuthenticateActionEnum.ACTIVITY_STATUS_CHANGE);
        }
    
    
        @Override
        public Logger getLog() {
            return log;
        }
    
        @Override
        protected String getTraceId(ActivityStatusChangeAuthenticateContext context) {
            return context.getRequest().getActivityID();
        }
    
        @Override
        protected AuthenticateConfig getConfig() {
            return volcAuthenticateApolloConfig.getActivityStatusChange();
        }
    
        @Override
        protected void doExecute(ActivityStatusChangeAuthenticateContext context) {
            SubscribeLiveActivityStatusChangeRequest request = context.getRequest();
            String sign = request.getSign();
            String signature = getSignature(context);
            if (Objects.equals(sign, signature)) {
                context.setResponse(BaseAuthenticateContext.Response.buildSuccess(SUCCESS));
            } else {
                String traceId = getTraceId(context);
                if (logDebug()) {
                    getLog().info("[{}|{}],ts={},encrypted={}", traceId, actionName, request.getTimestamp(), signature);
                }
                String debug = MessageFormat.format("activityId={0},signature={1},md5={2}", traceId, sign, signature);
                context.setResponse(BaseAuthenticateContext.Response.buildFailure(debug));
            }
        }
    
        /**
         * 获取报文加密后的密文
         *
         * @param context 上下文对象
         * @return 密文
         */
        private String getSignature(ActivityStatusChangeAuthenticateContext context) {
            SubscribeLiveActivityStatusChangeRequest request = context.getRequest();
            String privateKey = volcAuthenticateApolloConfig.getActivityStatusChange().getPrivateKey();
            StringBuilder content = new StringBuilder(privateKey);
            content.append(request.getActivityID()).append(request.getEventType())
                .append(request.getStatus()).append(request.getTimestamp());
            String original = content.toString();
            String encrypted = Md5Util.encrypt(original);
            return encrypted;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    2.4.2、VodEventNotifyAuthenticateHandler

    鉴权场景2的具体鉴权逻辑。

    /**
     * [视频点播事件通知]回调鉴权处理器
     *
     * @author : Sieg Heil
     * @since 2022/11/25 10:20 AM
     */
    @Component
    @Slf4j
    public class VodEventNotifyAuthenticateHandler extends AbstractAuthenticateHandler<VodEventNotifyAuthenticateContext> {
    
        /**
         * 构造函数
         */
        public VodEventNotifyAuthenticateHandler() {
            super(AuthenticateActionEnum.VOD_EVENT_NOTIFY);
        }
    
        @Override
        public Logger getLog() {
            return log;
        }
    
        @Override
        protected String getTraceId(VodEventNotifyAuthenticateContext context) {
            return context.getRequest().getRequest().getRequestId();
        }
    
        @Override
        protected AuthenticateConfig getConfig() {
            return volcAuthenticateApolloConfig.getVodEventNotify();
        }
    
        @Override
        protected void doExecute(VodEventNotifyAuthenticateContext context) {
            VolcVodRequestContext requestContext = context.getRequest();
            String sign = requestContext.getSignature();
            String original = getMd5Content(context);
            String signature = Md5Util.encrypt(original);
            if (Objects.equals(sign, signature)) {
                context.setResponse(BaseAuthenticateContext.Response.buildSuccess(SUCCESS));
            } else {
                String traceId = getTraceId(context);
                if (logDebug()) {
                    getLog().info("[{}|{}],encrypted={}", traceId, actionName, signature);
                }
                String debug = MessageFormat.format("requestId={0},signature={1},md5={2}", traceId, sign, signature);
                context.setResponse(BaseAuthenticateContext.Response.buildFailure(debug));
            }
        }
    
        private String getMd5Content(VodEventNotifyAuthenticateContext context){
            VolcVodRequestContext requestContext = context.getRequest();
            String requestBody = requestContext.getRequestBody();
            String privateKey = volcAuthenticateApolloConfig.getVodEventNotify().getPrivateKey();
            String callbackUrl = volcAuthenticateApolloConfig.getVodEventNotify().getCallbackUrl();
            String callbackContent = encode(requestBody);
            StringBuilder original = new StringBuilder(callbackUrl).append("|")
                .append(requestContext.getTimestamp()).append("|")
                .append(privateKey).append("|")
                .append(callbackContent);
            return original.toString();
        }
    
        private String encode(String value) {
            Base64.Encoder encoder = Base64.getEncoder();
            return encoder.encodeToString(value.getBytes(StandardCharsets.UTF_8));
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69

    2.5、AuthenticateConfig

    鉴权配置类

    /**
     * 鉴权配置类
     *
     * @author : Sieg Heil
     * @since 2022/11/25 11:47 AM
     */
    @ToString(callSuper = true)
    @Getter
    @Setter
    public class AuthenticateConfig {
        /**
         * 鉴权开关
         */
        private Boolean enableSwitch;
        /**
         * 鉴权私钥
         */
        private String privateKey;
        /**
         * 回调url
         */
        private String callbackUrl;
        /**
         * 鉴权策略
         */
        private StrategyEnum strategy;
    
        /**
         * 鉴权策略类型
         */
        public enum StrategyEnum {
            /**
             * 对报文进行MD5加密,防止报文被篡改
             */
            MD5
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    2.6、VolcAuthenticateApolloConfig

    所有回调场景鉴权配置类

    /**
     * 回调鉴权配置
     *
     * @author : Sieg Heil
     * @since 2022/11/25 11:55 AM
     */
    @Component
    @RefreshScope
    @ConfigurationProperties(prefix = "xxx.xxx.authenticate.volc")
    @ToString
    @Getter
    @Setter
    public class VolcAuthenticateApolloConfig {
    
        /**
         * 是否启用日志输出,便于追踪问题
         */
        private Boolean enableLogDebug;
    
        /**
         * 企业直播活动变更
         */
        private AuthenticateConfig activityStatusChange;
    
        /**
         * 视频点播事件通知
         */
        private AuthenticateConfig vodEventNotify;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    2.7、yml配置

    yml配置文件,可以通过diamond或者apollo,当前应用服务对接了apollo。

    xxx:
      xxx:
        # 回调配置 true|false
        callback:
          # 订阅企业直播活动状态变更
          subscribeVolcActivityStatusChange:
            # 启用日志输出
            logDebug: true
            # 启用日志输出
            enableHandle: false
          # 订阅视频点播事件通知
          subscribeVolcVodEventNotify:
            # 启用日志输出
            logDebug: true
            # 启用日志输出
            enableHandle: false   
        # 鉴权配置
        authenticate:
          # 鉴权配置
          volc:
            # 是否启用日志输出,便于追踪问题 true|false
            enableLogDebug: true
            # 企业直播活动变更
            activityStatusChange:
              # 鉴权开关
              enableSwitch: true
              # 鉴权私钥
              privateKey: xxxxx
              # 回调url
              callbackUrl: xxxx
              # 鉴权策略
              strategy: MD5
            # 视频点播事件通知
            vodEventNotify:
              # 鉴权开关
              enableSwitch: true
              # 鉴权私钥
              privateKey: xxxx
              # 回调url
              callbackUrl: xxxx         
              # 鉴权策略
              strategy: MD5          
            
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

    2.8、业务接入

    /**
     * Created at 2022/5/24 11:01 AM
     *
     * @author : Sieg Heil
     */
    @ThriftService(service = "volcEngineCallback")
    @Validated
    @Slf4j
    public class VolcEngineCallbackServiceImpl implements VolcEngineCallbackService{
    
        @Autowired
        private VolcEngineCallbackConverter volcEngineCallbackConverter;
    
        @Autowired
        private CallbackEnableSwitch callbackEnableSwitch;
    
        @Autowired
        private SubscribeLiveStatusEventDispatcher subscribeLiveStatusEventDispatcher;
    
        @Autowired
        private SubscribeVodEventDispatcher subscribeVodEventDispatcher;
    
        @Autowired
        private AuthenticateDispatcher authenticateDispatcher;
    
        @Override
        public void subscribeLiveActivityStatusChange(SubscribeLiveActivityStatusChangeRequest request) {
            ActivityStatusChangeAuthenticateContext authenticateContext = volcEngineCallbackConverter.convertToActivityStatusChangeAuthenticateContext(request);
            authenticateDispatcher.execute(authenticateContext);
            if (!callbackEnableSwitch.subscribeVolcActivityStatusChange()) {
                String traceId = request.getActivityID();
                log.info("[SubscribeLiveActivityStatusChange|{}]业务处理开关关闭|{}", traceId, callbackEnableSwitch.subscribeVolcActivityStatusChange());
                return;
            }
            SubscribeLiveStatusEventContext context = volcEngineCallbackConverter.convertToSubscribeLiveStatusEventContext(request);
            subscribeLiveStatusEventDispatcher.execute(context);
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
  • 相关阅读:
    Spring Boot和Spring有什么区别
    B2B撮合管理系统优势有哪些?如何助力传统仪器仪表制造业企业数字化转型
    代码随想录第45天|70. 爬楼梯 (进阶)322. 零钱兑换
    进程,线程,协程
    扎实打牢数据结构算法根基,从此不怕算法面试系列之008 week01 02-08 通过常见算法,对常见的时间复杂度做梳理
    MySQL基础
    【Linux】进程概念(下篇) —— 程序地址空间详解
    前端页面设计 | 博客系统
    阿里云CDN是什么意思?
    137.如何进行离线计算-3
  • 原文地址:https://blog.csdn.net/shichen2010/article/details/128154059