• 基于Springboot Aop实现注解鉴权式框架


    前置准备

    介绍

    • 本项目是基于Springboot AOP开发的功能简单的鉴权框架,本篇文章会介绍开发流程
    • 建议配合JWTThreadLocal一起使用效果更佳
    • 本框架在正式使用时需要先编写配置类,然后在经过JWT过滤的接口方法上方添加鉴定角色或鉴定权限的注解。
    • 使用AOP鉴定角色和权限时,两者只要有一个不符合要求,则拒绝执行接口。同时抛出异常,在全局异常处理当这捕获处理该异常

    导入依赖

    因为该框架基于Springboot AOP开发,所以需要导入AOP依赖,同时在框架开发完成之后需要打包,这里也给出了打包插件

        <dependencies>
            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-aopartifactId>
            dependency>
        dependencies>
    
        <build>
            <plugins>
                <plugin>
                    <groupId>org.springframework.bootgroupId>
                    <artifactId>spring-boot-maven-pluginartifactId>
                    <configuration>
                        <layout>NONElayout>
                        <classifier>execclassifier>
                    configuration>
                plugin>
            plugins>
        build>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    完整配置

    
    <project xmlns="http://maven.apache.org/POM/4.0.0"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0modelVersion>
    
        <groupId>org.zqgroupId>
        <artifactId>eVerifyartifactId>
        <version>1.0version>
    
        <properties>
            <maven.compiler.source>1.8maven.compiler.source>
            <maven.compiler.target>1.8maven.compiler.target>
        properties>
    
        <parent>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-parentartifactId>
            <version>2.6.4version>
            <relativePath/> 
        parent>
    
        <dependencies>
            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-aopartifactId>
            dependency>
        dependencies>
    
        <build>
            <plugins>
                <plugin>
                    <groupId>org.springframework.bootgroupId>
                    <artifactId>spring-boot-maven-pluginartifactId>
                    <configuration>
                        <layout>NONElayout>
                        <classifier>execclassifier>
                    configuration>
                plugin>
            plugins>
        build>
    
    project>
    
    • 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

    框架结构

    以下是框架编写完成之后的结构
    在这里插入图片描述

    自定义注解

    介绍

    在学习shiro框架之后,发现注解鉴权写法非常方便,这里直接参考shiro框架编写了鉴定角色和鉴定权限的注解

    鉴定角色

    编写CheckRoles注解,用于鉴定用户有没有该角色,当用户没有相应角色时抛出异常,msg字段属性作为异常的e.message信息,在使用注解时可以手动设置msg的值,同时抛出异常时也使用手动设置的值,type字段相当于选择条件是||还是&&,即有其中一个角色或者全部角色时为真
    注意:未实现通配符匹配

    import com.zq.annotation.type.CheckRolesType;
    
    import java.lang.annotation.*;
    
    @Target({ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface CheckRoles {
        String[] value();
        String msg() default "Check role failed,maybe you don't have role(s) to access the current interface";
        CheckRolesType type() default CheckRolesType.OR;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    package com.zq.annotation.type;
    
    public enum CheckRolesType {
        AND, OR
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    鉴定权限

    编写CheckPermission注解,用于鉴定用户有没有该权限,当用户没有相应权限时抛出异常,msg字段属性作为异常的e.message信息,在使用注解时可以手动设置msg的值,同时抛出异常时也使用手动设置的值,type字段相当于选择条件是||还是&&,即有其中一个权限或者全部权限时为真
    注意:未实现通配符匹配

    import com.zq.annotation.type.CheckPermissionType;
    
    import java.lang.annotation.*;
    
    @Target({ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface CheckPermission {
        String[] value();
        String msg() default "Check permission failed,maybe you don't have permission(s) to access the current interface";
        CheckPermissionType type() default CheckPermissionType.OR;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    package com.zq.annotation.type;
    
    public enum CheckPermissionType{
        AND,OR
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    自定义异常

    AuthenticationException

    AuthenticationException异常时该框架中的最顶级的自定义异常,该框架中其他自定义异常均继承于它

    package com.zq.exception;
    
    public class AuthenticationException extends RuntimeException{
        public AuthenticationException(String message) {
            super(message);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    NoSuchRolesException

    当用户没有指定角色时抛出该异常

    package com.zq.exception;
    
    public class NoSuchRolesException extends AuthenticationException {
        public NoSuchRolesException(String message) {
            super(message);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    NoSuchPermissionException

    当用户没有该权限时抛出该异常

    package com.zq.exception;
    
    public class NoSuchPermissionException extends AuthenticationException {
        public NoSuchPermissionException(String message) {
            super(message);
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    编写Verify

    Verfiy接口

    编写一个Verify接口,分别声明校验角色和校验权限的接口方法

    package com.zq.verify;
    
    
    import com.zq.annotation.type.CheckPermissionType;
    import com.zq.annotation.type.CheckRolesType;
    
    public interface Verify {
        boolean verifyRoles(String[] roles, CheckRolesType type);
        boolean verifyPermissions(String[] permissions, CheckPermissionType type);
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    boolean verifyRoles(String[] roles, CheckRolesType type);

    校验用户是否有该角色,roles为来自CheckRoles注解里面的String[] value()

    type是使用CheckRoles时设置的逻辑关系,没有设置则默认为OR

    • type==OR,则只要用户有String[] roles里面任意一个角色,就返回true
    • type==AND,则需要用户有String[] roles里面的所有角色,才返回true

    boolean verifyPermissions(String[] permissions, CheckPermissionType type);

    校验用户是否有该权限,permissions为来自CheckPermissions注解里面的String[] value()

    type是使用CheckPermissions时设置的逻辑关系,没有设置则默认为OR

    • type==OR,则只要用户有String[] roles里面任意一个角色,就返回true
    • type==AND,则需要用户有String[] roles里面的所有角色,才返回true

    VerifyConfigurer配置类

    编写VerifyConfigurer配置类,继承于Verify接口,并实现接口方法

    package com.zq.verify;
    
    import com.zq.annotation.type.CheckPermissionType;
    import com.zq.annotation.type.CheckRolesType;
    
    import java.util.List;
    
    public class VerifyConfigurer implements Verify {
    
        public List<String> getRoles(){
            return null;
        }
    
        public List<String> getPermissions(){
            return null;
        }
    
        /**
         * 校验用户是否拥有角色
         * @param roles
         * @param type
         * @return
         */
        @Override
        public boolean verifyRoles(String[] roles, CheckRolesType type) {
            if(roles==null || roles.length==0) return true;
            final List<String> rolesList = getRoles();
            if(rolesList==null || rolesList.size()==0) return false;
            if(type==CheckRolesType.OR)
                return checkOR(roles,rolesList);
            return checkAND(roles,rolesList);
        }
    
        /**
         * 校验用户是否拥有权限
         * @param permissions
         * @param type
         * @return
         */
        @Override
        public boolean verifyPermissions(String[] permissions, CheckPermissionType type) {
            if(permissions ==null || permissions.length==0) return true;
            final List<String> permissionsList = getPermissions();
            if(permissionsList ==null || permissionsList.size()==0) return false;
            if(type==CheckPermissionType.OR)
                return checkOR(permissions,permissionsList );
            return checkAND(permissions,permissionsList);
        }
    
        public boolean checkOR(String[] src, List<String> target) {
            for (String role : src) {
                if (target.contains(role)) {
                    return true;
                }
            }
            return false;
        }
    
        public boolean checkAND(String[] src, List<String> target){
            for (String role : src) {
                if (!target.contains(role)) {
                    return false;
                }
            }
            return true;
        }
    }
    
    
    • 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

    public List getRoles()
    查询用户角色的方法,后续需要程序员自己重写该方法

    public List getPermissions()
    查询用户权限的方法,后续需要程序员自己重写该方法

    public boolean verifyRoles(String[] roles, CheckRolesType type)
    使用getRoles()方法查询用户角色,并与传进来的roles参数进行逻辑匹配,参数type为匹配逻辑,匹配失败返回false,成功返回true

    public boolean verifyPermissions(String[] permissions, CheckPermissionType type)
    使用getPermissions()方法查询用户权限,并与传进来的permissions参数进行逻辑匹配,参数type为匹配逻辑,匹配失败返回false,成功返回true

    public boolean checkOR(String[] src, List target)
    查询srctarget中是否存在交集,即有没有相同角色,有则返回true,没有返回fasle

    public boolean checkAND(String[] src, List target)
    查询src∈target是否为真,即src中的所有角色在target中都能找到,都能则返回true,否则返回false

    使用AOP鉴权

    失败案例

    之前我是使用最常见的springboot aop写法,使用以下写法

    @Pointcut(value = "execution(* com.zq.drawingBed.controller..*.*(..))")
    public void pointCut(){}
    
    • 1
    • 2

    但是上面的写法不太灵活,无法将value属性抽取出来放入配置文件中再注入进去,所以我尝试在项目启动的时候使用反射获取
    public void pointCut(){}方法上面的@Pointcut注解的value属性,然后使用反射修改了value对象(String)中字符数组的地址,确实修改成功了,但可能是因为底层使用了动态代理,即使使用反射也无法改变切入点,所以这种写法失败了,下面介绍更加灵活的一种写法

    成功案例

    编写前置通知

    继承MethodBeforeAdviceAdvisorAdapter接口,实现controller接口方法调用前的拦截

    package com.zq.aop;
    
    import com.zq.annotation.CheckPermission;
    import com.zq.annotation.CheckRoles;
    import com.zq.exception.NoSuchPermissionException;
    import com.zq.exception.NoSuchRolesException;
    import com.zq.verify.Verify;
    import org.aopalliance.aop.Advice;
    import org.aopalliance.intercept.MethodInterceptor;
    import org.springframework.aop.Advisor;
    import org.springframework.aop.MethodBeforeAdvice;
    import org.springframework.aop.framework.adapter.AdvisorAdapter;
    
    import java.lang.reflect.Method;
    
    /**
     * 自定义前置AOP
     */
    public class VerifyBeforeAdvice implements MethodBeforeAdvice,  AdvisorAdapter {
    
        private Verify verify;
    
    
        public void setVerify(Verify verify) {
            this.verify = verify;
        }
    
        @Override
        public void before(Method method, Object[] args, Object target) {
            final CheckRoles rolesAnnotation = method.getAnnotation(CheckRoles.class);
            final CheckPermission permissionAnnotation = method.getAnnotation(CheckPermission.class);
            if(rolesAnnotation!=null&&
                    !verify.verifyRoles(rolesAnnotation.value(), rolesAnnotation.type()))
                throw new NoSuchRolesException(rolesAnnotation.msg());
            if(permissionAnnotation!=null&&
                    !verify.verifyPermissions(permissionAnnotation.value(), permissionAnnotation.type()))
                throw new NoSuchPermissionException(permissionAnnotation.msg());
        }
    
        @Override
        public boolean supportsAdvice(Advice advice) {
            return true;
        }
    
        @Override
        public MethodInterceptor getInterceptor(Advisor advisor) {
            return null;
        }
    }
    
    
    
    • 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

    private Verify verify;
    程序员使用该框架的时候,需要重写getRoles()getPermissions方法,将重写后的class对象注入到上面的字段里面去

    public void before(Method method, Object[] args, Object target)
    controller接口方法调用前的拦截时调用的方法,在该方法中使用 verify对象进行角色和权限校验

    动态配置切点

    编写一个建造者类,用于动态设置切点,并生成AOP对象

    package com.zq.aop;
    
    import com.zq.verify.Verify;
    import org.springframework.aop.Pointcut;
    import org.springframework.aop.aspectj.AspectJExpressionPointcut;
    import org.springframework.aop.support.DefaultPointcutAdvisor;
    
    /**
     * 自定义AOP
     */
    public class VerifyPointCutAdvisorBulider {
    
        public VerifyPointCutAdvisorBulider() {
        }
    
        public VerifyPointCutAdvisorBulider(Verify verify, String controllerPath) {
            this.verify = verify;
            this.controllerPath = controllerPath;
        }
    
        private Verify verify;
        private String controllerPath;
    
        public Verify getVerify() {
            return verify;
        }
    
        public String getControllerPath() {
            return controllerPath;
        }
    
        public void setVerify(Verify verify) {
            this.verify = verify;
        }
    
        public void setControllerPath(String controllerPath) {
            this.controllerPath = controllerPath;
        }
    
        private Pointcut createPointCut(String controllerPath) {
            AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
            pointcut.setExpression("execution(public * "+controllerPath+"..*(..))");
            return pointcut;
        }
    
        private VerifyBeforeAdvice createAdvice () {
            VerifyBeforeAdvice beforeAdvice = new VerifyBeforeAdvice();
            beforeAdvice.setVerify(verify);
            return beforeAdvice;
        }
    
        public DefaultPointcutAdvisor bulid () {
            DefaultPointcutAdvisor defaultPointcutAdvisor = new DefaultPointcutAdvisor();
    
            Pointcut pointCut = createPointCut(controllerPath);
            defaultPointcutAdvisor.setPointcut(pointCut);
    
            VerifyBeforeAdvice beforeAdvice = createAdvice();
            defaultPointcutAdvisor.setAdvice(beforeAdvice);
            return defaultPointcutAdvisor;
        }
    }
    
    
    • 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

    private Verify verify;
    程序员使用该框架的时候,需要重写getRoles()getPermissions方法,将重写后的class对象注入到上面的字段里面去

    private String controllerPath;
    动态配置的注入点,这里是将项目中的controller包所在的路径当成注入点使用

    private Pointcut createPointCut(String controllerPath)
    使用controllerPath,创建切点对象

    private VerifyBeforeAdvice createAdvice ()
    创建AOP前置通知操作对象,并将重写了getRoles()getPermissions()方法的verify对象注入进去

    public DefaultPointcutAdvisor bulid ()
    使用建造者模式,构建AOP对象

    打包及使用

    打包

    使用IDEA自带的maven打包工具打包:
    在这里插入图片描述
    打包成功之后控制台显示jar包所在的路径:
    在这里插入图片描述

    使用

    导入AOP依赖

    该框架基于Springboot AOP开发,所以需要导入该依赖:

            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-aopartifactId>
            dependency>
    
    • 1
    • 2
    • 3
    • 4

    导入Jar包

    将上面打包生成的Jar包,或者直接点击链接下载eVerify-1.0.jar
    导入到springboot项目中
    在这里插入图片描述

    可以直接复制粘贴到lib目录下,右键选中该jar包,点击添加为库

    编写配置类

    config目录下创建VerfiyConfig配置类,继承于VerifyConfigurer

    • 重写getRoles()getPermissions()方法,这里注入了UserService,用来查询数据库,以实现上面两个方法
    • 设置controller包所在的项目路径(作为鉴权AOP的切入点)
    • 使用VerifyPointCutAdvisorBulider配置鉴权AOP对象,并注入到IOC容器中
    package com.zq.config;
    
    import com.zq.aop.VerifyPointCutAdvisorBulider;
    import com.zq.service.impl.UserService;
    import com.zq.verify.VerifyConfigurer;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.aop.support.DefaultPointcutAdvisor;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    import java.util.List;
    
    @Slf4j
    @Configuration
    public class VerifyConfig extends VerifyConfigurer {
    
        @Autowired
        private UserService userService;
    
        private String controllerPath="com.zq.controller";
    
        public List<String> getRoles() {
            return userService.getRoles("admin");
        }
    
        @Override
        public List<String> getPermissions() {
            return userService.getPermissions("admin");
        }
    
        @Bean(value = "AuthenticationAop")
        public DefaultPointcutAdvisor createDefaultPointcutAdvisor(){
            log.info("鉴权配置启动");
            VerifyPointCutAdvisorBulider bulider=new VerifyPointCutAdvisorBulider();
            bulider.setVerify(this);
            bulider.setControllerPath(controllerPath);
            return bulider.bulid();
        }
    }
    
    
    • 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

    全局异常处理

    该框架中编写了三个自定义异常,其中两个军继承于AuthenticationException异常,所以可以直接捕获该异常进行处理,e.getMessage()的内容为注解里面设置的msg的参数内容,当校验失败的时候使用msg作为异常的message内容

    package com.zq.exception;
    
    import org.springframework.web.bind.annotation.ExceptionHandler;
    import org.springframework.web.bind.annotation.RestControllerAdvice;
    
    @RestControllerAdvice
    public class GlobalExceptionHandler {
    
        @ExceptionHandler(value = {AuthenticationException.class})
        public String handleAuthenticationException(AuthenticationException e) {
            return e.getMessage();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    上面的代码要根据自己的项目修改返回值类型,这里为了演示写成了String

    使用鉴权注解

    package com.zq.controller;
    
    import com.zq.annotation.CheckPermission;
    import com.zq.annotation.CheckRoles;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestMethod;
    import org.springframework.web.bind.annotation.RestController;
    
    @Slf4j
    @RestController
    @RequestMapping(value = "test", method = RequestMethod.GET)
    public class TestController {
    
        @CheckRoles("admin")
        @CheckPermission("user:add")
        @GetMapping(value = "t01")
        public String t01() {
            return "01";
        }
    
        @CheckRoles("admin")
        @CheckPermission("user:edit")
        @GetMapping(value = "t02")
        public String t02() {
            return "02";
        }
    
        @CheckRoles("admin")
        @GetMapping(value = "t03")
        public String t03() {
            return "03";
        }
    
        @CheckRoles("user")
        @GetMapping(value = "t04")
        public String t04() {
            return "04";
        }
    
        @CheckPermission("user:add")
        @GetMapping(value = "t05")
        public String t05() {
            return "05";
        }
    }
    
    
    • 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

    测试接口

    • 当有权限时
      在这里插入图片描述
    • 当没有权限时
      在这里插入图片描述在这里插入图片描述

    其他问题

    该框架建议配合JWT一起使用,校验流程可以如下:
    1.用户成功登录之后返回token
    2.用户下次请求时携带token,在JWT过滤器中校验token是否有效

    token中拿的user信息建议放到Threadlocal里面,下次其他方法需要的时候二次利用

    3.eVerify框架在AOP中获取被调用接口方法标注的角色和权限注解,根据从token中计算的usernameuserId查询数据库,校验是否有角色或者权限,以此判断该用户是否有权限访问该接口
    4.若使用JWT框架,请不要在未经过JWT过滤的接口上使用鉴权注解
    5.一般来说是从token中拿user信息,在getRolesgetPermissions方法里面用user信息查数据库

    源码

    框架地址 :eVerify

    新手上路,有问题请指正,谢谢

  • 相关阅读:
    【leetcode】002二进制加法
    2023 “华为杯” 中国研究生数学建模竞赛(B题)深度剖析|数学建模完整代码+建模过程全解全析
    iOS模拟器 Unable to boot the Simulator —— Ficow笔记
    文盘Rust -- 如何把配置文件打包到二进制文件里
    Python基础篇(十一)-- 模块和包
    ubuntu18.04 LTS卸载qtcreator-10.0.2
    Java网络编程 - 网络基础、Socket网络编程、TCP和UDP网路编程、URL编程
    Android面试题--HashMap原理分析
    极智开发 | CUDA线程模型与全局索引计算方式
    业务:财务软件之会计六要素
  • 原文地址:https://blog.csdn.net/qq_16525829/article/details/127344309