Spring Boot在Web应用中基于JdbcRealm安全验证过程

目录
  • 正文
  • 01-RBAC 基于角色的访问控制
  • 02-Shiro 中基于 JdbcRealm 实现用户认证、授权
  • 03-集成到 Spring Boot Web 应用中
  • 04-总结

正文

在安全领域,Subject 用来指代与系统交互的实体,可以是用户、第三方应用等,它是安全认证框架(例如 Shiro)验证的主题。 Principal 是 Subject 具有的属性,例如用户名、身份证号、电话号码、邮箱等任何安全验证过程中关心的要素。 Primary principal 指能够唯一区分 Subject 的属性,例如身份证号码,论坛系统中的登录名等,通过它可以唯一识别一个 Subject。 Credential 是认证过程中与 Principal 一同提交到系统的信息,通常是只有 Subject 知道的加密信息,例如密码、PGP Key等。

应用系统或者说安全认证框架验证一个 Subject 的过程为:

  • Subject 提供 principal(例如用户名)和 credential(例如密码)
  • 安全认证框架(例如 Shiro)会验证 Subject 提供的信息与保存在应用系统中的信息(例如存储在数据库或者 LDAP 中)是否匹配。 若匹配,则认为 Subject 为合法用户;否则,为非法用户。

01-RBAC 基于角色的访问控制

Role-Based Access Control(RBAC)是最普遍的权限设计模型。 它包含了三个实体:

  • 用户
  • 角色
  • 权限

我们定义三张表,来存储这三个实体:

CREATE TABLE `demo_user` (
  `user_id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '编号',
  `username` varchar(20) NOT NULL COMMENT '帐号',
  `password` varchar(32) NOT NULL COMMENT '密码MD5(密码+盐)',
  `locked` tinyint(4) DEFAULT NULL COMMENT '状态(0:正常,1:锁定)',
  `ctime` bigint(20) DEFAULT NULL COMMENT '创建时间',
  PRIMARY KEY (`user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户';
-- 插入两条数据,表示两个用户
INSERT INTO demo_user (user_id, username, password, locked, ctime)
VALUES (1, 'admin', 'admin', '0', sysdate()),
       (2, 'lihua', 'lihua123', '0', sysdate()),
       (3, 'hanmeimei', 'hanmeimei123', '0', sysdate());
CREATE TABLE `demo_role` (
  `role_id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '编号',
  `name` varchar(20) DEFAULT NULL COMMENT '角色名称',
  `description` varchar(1000) DEFAULT NULL COMMENT '角色描述',
  PRIMARY KEY (`role_id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='角色';
-- 插入两条数据,表示两个角色
INSERT INTO demo_role(role_id, name, description)
VALUES (1, 'admin', '管理员'),
       (2, 'user', '普通用户');
CREATE TABLE `demo_permission` (
  `permission_id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '编号',
  `name` varchar(20) DEFAULT NULL COMMENT '名称',
  `permission_value` varchar(50) DEFAULT NULL COMMENT '权限值',
  `status` tinyint(4) DEFAULT NULL COMMENT '状态(0:禁止,1:正常)',
  PRIMARY KEY (`permission_id`)
) ENGINE=InnoDB AUTO_INCREMENT=86 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='权限';
-- 插入三条数据,表示三种不同的权限
INSERT INTO demo_permission(permission_id, name, permission_value, status)
VALUES (1, '新增用户', 'user:add', 1),
       (2, '删除用户', 'user:delete', 1),
       (3, '查看用户', 'user:get', 1);

实体之间具有如下的关系:

  • 角色权限,一对多,一个角色可以具有多个权限。
  • 用户角色,一对多,一个用户可以具有多个角色。
  • 用户权限,一对多,一个用户有多个权限。权限的来源有两种,一类是直接赋予它某些权限,另一类是通过赋予它多个角色而赋予它角色关联的权限。

我们定义三张表,来存储上述三种关系:

CREATE TABLE `demo_role_permission` (
  `role_permission_id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '编号',
  `role_id` int(10) unsigned NOT NULL COMMENT '角色编号',
  `permission_id` int(10) unsigned NOT NULL COMMENT '权限编号',
  PRIMARY KEY (`role_permission_id`)
) ENGINE=InnoDB AUTO_INCREMENT=129 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='角色权限关联表';
-- 插入四条条数据,admin 具有增、删、查用户权限,user 具有查用户权限
INSERT INTO demo_role_permission(role_permission_id, role_id, permission_id)
VALUES (1, 1, 1),
       (2, 1, 2),
       (3, 1, 3),
       (4, 2, 3);
CREATE TABLE `demo_user_role` (
  `user_role_id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '编号',
  `user_id` int(10) unsigned NOT NULL COMMENT '用户编号',
  `role_id` int(10) DEFAULT NULL COMMENT '角色编号',
  PRIMARY KEY (`user_role_id`)
) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户角色关联表';
-- 插入三条数据
INSERT INTO demo_user_role (user_role_id, user_id, role_id)
VALUES (1, 1, 1),
       (2, 2, 2),
       (3, 3, 2);
CREATE TABLE `demo_user_permission` (
  `user_permission_id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '编号',
  `user_id` int(10) unsigned NOT NULL COMMENT '用户编号',
  `permission_id` int(10) unsigned NOT NULL COMMENT '权限编号',
  PRIMARY KEY (`user_permission_id`)
) ENGINE=InnoDB AUTO_INCREMENT=28 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户权限关联表';
-- 插入五条数据
INSERT INTO demo_user_permission (user_permission_id, user_id, permission_id)
VALUES (1, 1, 1),
       (2, 1, 2),
       (3, 1, 3),
       (4, 2, 3),
       (5, 3, 3);

02-Shiro 中基于 JdbcRealm 实现用户认证、授权

Shiro 中 Realm 是负责与应用系统中的权限模型打交道的组件,所以它也被称为 Security DAO(Data Access Object)。 Shiro 中 Realm 的类型设计结构图如下所示:

AuthenticatingRealm 和 AuthorizingRealm 分别实现了认证、授权的整体流程,将如何获取存储认证信息、权限信息通过模板方法方式留给派生类去实现:

  • AuthenticatingRealm#doGetAuthenticationInfo,如何获取系统存储的认证信息,例如用户、密码等。对应上节中的 demo_user 表。
  • AuthorizingRealm#doGetAuthorizationInfo,如何获得用户的角色、权限信息,对应上节中的 demo_role、demo_permission 表。

接下来,我详细分析下 Shiro 提供的一个基于数据库的实现类 JdbcRealm。 简化后的 doGetAuthenticationInfo 流程:

protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
    // 用户提交上来的信息
    String username = ((UsernamePasswordToken) token).getUsername();
    Connection conn = null;
    SimpleAuthenticationInfo info = null;
    try {
        conn = dataSource.getConnection(); // 数据库引用
        // 从数据库中获取密码
        String password = getPasswordForUser(conn, username)[0];    // 关键点1
        // 创建验证结果
        info = new SimpleAuthenticationInfo(username, password.toCharArray(), getName());
    } catch (SQLException e) {
        final String message = "There was a SQL error while authenticating user [" + username + "]";
        throw new AuthenticationException(message, e);
    } finally {
        JdbcUtils.closeConnection(conn);
    }
    return info;
}

简化后的 doGetAuthorizationInfo 方法:

protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
    // 这里的 principals 主要为获得用户名
    String username = (String) getAvailablePrincipal(principals);
    Connection conn = null;
    Set<String> roleNames = null;
    Set<String> permissions = null;
    try {
        conn = dataSource.getConnection();  // 数据库引用
        // Retrieve roles and permissions from database
        roleNames = getRoleNamesForUser(conn, username);  // 关键点2
        if (permissionsLookupEnabled) {
            permissions = getPermissions(conn, username, roleNames);  // 关键点3
        }
    } catch (SQLException e) {
        final String message = "There was a SQL error while authorizing user [" + username + "]";
        throw new AuthorizationException(message, e);
    } finally {
        JdbcUtils.closeConnection(conn);
    }
    // 创建权限信息
    SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(roleNames);
    info.setStringPermissions(permissions);
    return info;
}

注:从 JdbcRealm 的源码中能够发现如下几点:

  • 关键点1,需要一个 SQL 语句,从数据库表中查询用户的密码。结合上节中定义的表,这个 SQL 语句为 SELECT password FROM demo_user WHERE username = ?
  • 关键点2,需要从数据库中查询用户的角色,SQL 语句为 SELECT name FROM demo_user_role ur, demo_user u, demo_role r WHERE ur.user_id = u.user_id AND ur.role_id = r.role_id AND u.username = ?
  • 关键点3,需要根据角色列表从数据库中查询角色具备的权限集合,SQL 语句为 SELECT permission_value FROM demo_role_permission rp, demo_role r, demo_permission p WHERE rp.role_id = r.role_id AND rp.permission_id = p.permission_id AND r.name = ?
  • 其他点,我们需要一个数据库引用,即 DataSource 对象。要查询权限,需要 permissionsLookupEnabled == true

综上,我们需要对 JdbcRealm 对象进行设置,使其能够获取到我们存储在数据库中的信息。

@Bean
public Realm realm(@Autowired DataSource dataSource) {
    final JdbcRealm jdbcRealm = new JdbcRealm();
    jdbcRealm.setDataSource(dataSource);
    String authQuery = "SELECT password FROM demo_user WHERE username = ?";
    jdbcRealm.setAuthenticationQuery(authQuery);
    jdbcRealm.setPermissionsLookupEnabled(true);
    String roleQuery = "SELECT name FROM demo_user_role ur, demo_user u, demo_role r WHERE ur.user_id = u.user_id AND ur.role_id = r.role_id AND u.username = ?";
    String permissionQuery = "SELECT permission_value FROM demo_role_permission rp, demo_role r, demo_permission p WHERE rp.role_id = r.role_id AND rp.permission_id = p.permission_id AND r.name = ?";
    jdbcRealm.setUserRolesQuery(roleQuery);
    jdbcRealm.setPermissionsQuery(permissionQuery);
    return jdbcRealm;
}

除了使用 JdbcRealm 的方法外,还可以仿照它编写我们自己的实现。接下来,我将结合 Spring Data JPA 编写一个 JpaRealm。

public class JpaRealm extends AuthorizingRealm {
    @Autowired
    private DemoUserRepository userRepository;
    @Autowired
    private DemoUserRoleRepository userRoleRepository;
    @Autowired
    private DemoRolePermissionRepository rolePermissionRepository;
    @Autowired
    private DemoPermissionRepository permissionRepository;
    @Autowired
    private DemoRoleRepository roleRepository;
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        // 查询权限的过程与 JdbcRealm 一样,只不过使用了 jpa
        //null usernames are invalid
        if (principals == null) {
            throw new AuthorizationException("PrincipalCollection method argument cannot be null.");
        }
        String username = (String) getAvailablePrincipal(principals);
        final DemoUser user = this.userRepository.findByUsername(username)
                .orElseThrow(() -> new UnknownAccountException("No account found for user [" + username + "]"));
        final List<Integer> roleIds = userRoleRepository.findByUserId(user.getUserId()).stream()
                .map(DemoUserRole::getUserRoleId)
                .collect(Collectors.toList());
        final Set<String> roleNames = roleRepository.findAllById(roleIds).stream()
                .map(DemoRole::getName)
                .collect(Collectors.toSet());
        final List<Integer> permissionIds = rolePermissionRepository.findAllByRoleIdIn(roleIds).stream()
                .map(DemoRolePermission::getPermissionId)
                .collect(Collectors.toList());
        final Set<String> permissions = permissionRepository.findAllById(permissionIds).stream()
                .map(DemoPermission::getPermissionValue)
                .collect(Collectors.toSet());
        final SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(roleNames);
        info.setStringPermissions(permissions);
        return info;
    }
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        // 查询身份信息的过程与 JdbcRealm 一样
        UsernamePasswordToken upToken = (UsernamePasswordToken) token;
        String username = upToken.getUsername();
        // Null username is invalid
        if (username == null) {
            throw new AccountException("Null usernames are not allowed by this realm.");
        }
        try {
            final DemoUser user = this.userRepository.findByUsername(username)
                    .orElseThrow(() -> new UnknownAccountException("No account found for user [" + username + "]"));
            SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(username, user.getPassword().toCharArray(), getName());
            return info;
        } catch (Throwable t) {
            final String message = "There was a SQL error while authenticating user [" + username + "]";
            // Rethrow any SQL errors as an authentication exception
            throw new AuthenticationException(message, t);
        }
    }
}

在之前初始化 JdbcRealm 的地方,换成 JpaRealm 就可以了。

@Bean
public Realm jpaRealm() {
    return new JpaRealm();
}

03-集成到 Spring Boot Web 应用中

接下来,我把前两节的东西整合在一个 Spring Boot Web 应用中,并测试下效果吧。

首先,编写两个 Controller 类,以便能够从浏览器或 Postman 中访问它:

@RestController
public class LoginController {
    @GetMapping("/login")
    public String login() {
        return "please login!";
    }
    @GetMapping("/index")
    public String index() {
        final Subject subject = SecurityUtils.getSubject();
        if (subject.isAuthenticated()) {
            final Object principal = subject.getPrincipal();
            return "hello, " + principal;
        }
        return "hello";
    }
}

LoginController 负责处理到 "/login" 和 "/index' 的请求,主要是为了匹配 Shiro 中的 loginUrl 和 successfulUrl。

shiro:
  enabled: true
  web:
    enabled: true
  loginUrl: /login
  successUrl: /index

当未登录用户访问时,会重定向到 "/login",你可以看到一个请求登录的提示。 登录成功后,会重定向到 "/index" ,并显示 "hello, ${用户名}" 提示。

注:为了偷懒,我没有写登录界面,默认情况下 Shiro 中的 AuthenticatingFilter 会处理到 loginUrl 的 POST 请求,并从中提取 principal 来进行登录验证。

// org.apache.shiro.web.filter.AccessControlFilter.isLoginRequest
protected boolean isLoginRequest(ServletRequest request, ServletResponse response) {
    return pathsMatch(getLoginUrl(), request);
}

所以,当需要登录时,只需要向 "/login" 发送一个 POST 请求即可,例如:

curl --location --request POST 'http://localhost:18886/shiro-web/login' \
--form 'username="lihua"' \
--form 'password="lihua123"'

然后,我编写了另一个 Controller 它主要用来实现对 User 的增删查操作:

@RestController
public class UserController {
    @Autowired
    private DemoUserRepository userRepository;
    @RequiresPermissions("user:get")
    @GetMapping("/users")
    public List<DemoUser> all() {
        final List<DemoUser> all = userRepository.findAll();
        return all;
    }
    @RequiresPermissions("user:get")
    @GetMapping("/users/{id}")
    public DemoUser one(@PathVariable Integer id) {
        final Optional<DemoUser> byId = userRepository.findById(id);
        return byId.orElse(null);
    }
    @RequiresPermissions("user:add")
    @PostMapping("/users")
    public String add(@RequestBody DemoUser user) {
        userRepository.save(user);
        return "success";
    }
    @RequiresPermissions("user:delete")
    @DeleteMapping("/users/{id}")
    public String delete(@PathVariable Integer id) {
        userRepository.deleteById(id);
        return "success";
    }
}

加上之前的代码,所有的元素我们就凑齐了,可以 run 起来检查一下了。 如果需要完整的源代码,可以在我的 gitee.com 上下载。

04-总结

今天,我介绍了如何使用 Shiro 中提供的 JdbcRealm 实现基于 RBAC 模型的权限验证。 之后,又仿照 JdbcRealm 实现了一个基于 JPA 的 Realm 实现,并将它们集成在了一个 Web 应用中进行了验证。 希望今天的内容能对你有所帮助。

以上就是Spring Boot在Web应用中基于JdbcRealm安全验证过程的详细内容,更多关于Spring Boot JdbcRealm安全验证的资料请关注我们其它相关文章!

(0)

相关推荐

  • SpringBoot Bean花式注解方法示例上篇

    目录 1.XML方式声明 2.注解法@Component 3.完全注解式 4.简化注解@Import 1.XML方式声明 这里我举两个例子,一个是自定义的bean,另一个是第三方bean,这样会全面一些. 你还可以定义这个bean的模式,有单例模式和多例模式,prototype代表多例,singleton代表单例. <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://ww

  • SpringBoot Bean花式注解方法示例下篇

    目录 1.容器初始化完成后注入bean 2.导入源的编程式处理 3.bean裁定 拓展 4.最终裁定 1.容器初始化完成后注入bean import lombok.Data; import org.springframework.stereotype.Component; @Component("miao") @Data public class Cat { } 被注入的JavaBean import org.springframework.context.annotation.Con

  • Spring Boot Shiro在Web应用中的作用详解

    目录 01-Tomcat 中的 Filter 责任链 02-Shiro 中的 filter 链结构 03-shiro-filters 如何与 servlet 中的 filter 关联起来 04-总结 01-Tomcat 中的 Filter 责任链 在前面的文章中,我介绍了如何使用 Apache Shiro 进行安全认证. 其实 Shiro 在 Web 应用中出现的频率更高. 今天我将来分析下,Shiro 是如何应用到 Web 应用中的. Servlet 规范中定义了 Filter 和 Filte

  • Spring Boot Shiro auto-configure工作流程详解

    目录 01-Shiro 自动配置原理 02-自动配置类 03-Filter 相关的配置类 04-总结 01-Shiro 自动配置原理 Shiro 与 Spring Boot 集成可以通过 shiro-spring-boot-stater 实现,并能完成必要类自动装配. 实现方式是通过 Spring Boot 的自动配置机制,即 WEB-INF/spring.factories 中通过 EnableAutoConfiguration 指定了 6 个自动化配置类: org.springframewo

  • Springboot整合ActiveMQ实现消息队列的过程浅析

    目录 pom中导入坐标 书写yml配置 业务层代码 监听器代码 业务层代码 确保你启动了自己电脑的activemq. pom中导入坐标 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-activemq</artifactId> </dependency> 书写yml配置 spring:  activemq:

  • Spring Boot在Web应用中基于JdbcRealm安全验证过程

    目录 正文 01-RBAC 基于角色的访问控制 02-Shiro 中基于 JdbcRealm 实现用户认证.授权 03-集成到 Spring Boot Web 应用中 04-总结 正文 在安全领域,Subject 用来指代与系统交互的实体,可以是用户.第三方应用等,它是安全认证框架(例如 Shiro)验证的主题. Principal 是 Subject 具有的属性,例如用户名.身份证号.电话号码.邮箱等任何安全验证过程中关心的要素. Primary principal 指能够唯一区分 Subje

  • Spring Boot入门(web+freemarker)

    1.配置maven文件pom.xml <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apach

  • 使用Spring Boot创建Web应用程序的示例代码

    在这篇文章中,我们将探讨使用Spring Boot创建Web应用程序的细节. 我们将探索Spring Boot如何帮助你加速应用程序开发. 我们将使用Spring Boot构建一个简单的Web应用程序,并为其添加一些有用的服务. 1. 介绍 启动一个新项目的主要挑战之一是该项目的初始设置. 我们需要对不同的目录结构进行调用,并且需要确保我们遵循所有行业标准.对于使用Spring Boot创建Web应用程序,我们需要以下工具: 我们自己喜欢的IDE (我将使用IntelliJ) Maven JDK

  • Spring boot搭建web应用集成thymeleaf模板实现登陆

    Spring boot 搭建web应用集成了thymeleaf模板实现登陆 下面是pom.xml的配置 <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schema

  • spring boot devtools在Idea中实现热部署方法

    1 pom.xml文件 注:热部署功能spring-boot-1.3开始有的 <!--添加依赖--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <!-- optional=true,依赖不会传递,该项目依赖devtools:之后依赖myboot项目的项目如果想要使用d

  • spring boot 2.x html中引用css和js失效问题及解决方法

    在application.properties中配置了static的默认路径 我的static目录结构是这样的 index.html中这样引用css或者js文件,用到了th标签 html使用th标签需要先导入 以上这样配置好了之后发现网页的css和js果然加载出来了. 自定义拦截器失效 在自定义拦截器注册之后发现静态资源被拦截了,注释掉拦截器发现果然是拦截器的问题. 在注册方法上使用了excludePathPatterns()排除静态资源的拦截,发现该方法失效了,查看源码WebMvcConfig

  • Spring Boot实现qq邮箱验证码注册和登录验证功能

    1.登录注册思路 这是一个使用spring boot做的一个qq邮箱注册和登录的项目. 没写前端页面,使用postman测试.有截图详细. 1.1.思路 注册:通过输入的邮箱发送验证码,检验前端传来的验证码是否和后台生成的一致,若一致,将数据写入数据库,完成注册: 登录:通过输入的邮箱查询密码,然后比较密码是否一致,一致就是登录成功. 1.2.整个项目结构图 2.准备 2.1.开启邮箱POP3/SMTP服务 登录qq邮箱后,点击左上方的设置,选择账户,如下图. 然后一直往下滑,看到如下图的POP

  • 基于Spring Boot保护Web应用程序

    如果在类路径上添加了Spring Boot Security依赖项,则Spring Boot应用程序会自动为所有HTTP端点提供基本身份验证.端点"/"和"/home"不需要任何身份验证.所有其他端点都需要身份验证. 要将Spring Boot Security添加到Spring Boot应用程序,需要在构建配置文件中添加Spring Boot Starter Security依赖项. Maven用户可以在pom.xml 文件中添加以下依赖项. <depend

  • Spring Boot开发Web应用详解

    Spring Boot快速入门中我们完成了一个简单的RESTful Service,体验了快速开发的特性.在留言中也有朋友提到如何把处理结果渲染到页面上.那么本篇就在上篇基础上介绍一下如何进行Web应用的开发. 静态资源访问 在我们开发Web应用的时候,需要引用大量的js.css.图片等静态资源. 默认配置 Spring Boot默认提供静态资源目录位置需置于classpath下,目录名需符合如下规则: /static /public /resources /META-INF/resources

  • Spring Boot整合Web项目常用功能详解

    前言 在Web应用开发过程中,一般都涵盖一些常用功能的实现,如数据库访问.异常处理.消息队列.缓存服务.OSS服务,以及接口日志配置,接口文档生成等.如果每个项目都来一套,则既费力又难以维护.可以通过Spring Boot的Starter来将这些常用功能进行整合与集中维护,以达到开箱即用的目的. 项目基于Spring Boot 2.1.5.RELEASE 版. 项目地址 整个项目分为如下几部分: spring-boot-autoconfigure: 具体的各功能实现,每个功能通过package的

随机推荐