欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Spring?Security權(quán)限管理實現(xiàn)接口動態(tài)權(quán)限控制

 更新時間:2022年06月20日 10:22:34   作者:MacroZheng  
這篇文章主要為大家介紹了Spring?Security權(quán)限管理實現(xiàn)接口動態(tài)權(quán)限控制,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪

SpringBoot實戰(zhàn)電商項目mall(30k+star)地址:https://github.com/macrozheng/mall

摘要

權(quán)限控管理作為后臺管理系統(tǒng)中必要的功能,mall項目中結(jié)合Spring Security實現(xiàn)了基于路徑的動態(tài)權(quán)限控制,可以對后臺接口訪問進行細粒度的控制,今天我們來講下它的后端實現(xiàn)原理。

前置知識

學習本文需要一些Spring Security的知識,對Spring Security不太了解的朋友可以看下以下文章。

數(shù)據(jù)庫設(shè)計

權(quán)限管理相關(guān)表已經(jīng)重新設(shè)計,將原來的權(quán)限拆分成了菜單和資源,菜單管理用于控制前端菜單的顯示和隱藏,資源管理用來控制后端接口的訪問權(quán)限。

數(shù)據(jù)庫表結(jié)構(gòu)

其中ums_adminums_role、ums_admin_role_relation為原來的表,其他均為新增表。

數(shù)據(jù)庫表介紹

接下來我們將對每張表的用途做個詳細介紹。

ums_admin

后臺用戶表,定義了后臺用戶的一些基本信息。

create table ums_admin
(
   id                   bigint not null auto_increment,
   username             varchar(64) comment '用戶名',
   password             varchar(64) comment '密碼',
   icon                 varchar(500) comment '頭像',
   email                varchar(100) comment '郵箱',
   nick_name            varchar(200) comment '昵稱',
   note                 varchar(500) comment '備注信息',
   create_time          datetime comment '創(chuàng)建時間',
   login_time           datetime comment '最后登錄時間',
   status               int(1) default 1 comment '帳號啟用狀態(tài):0->禁用;1->啟用',
   primary key (id)
);

ums_role

后臺用戶角色表,定義了后臺用戶角色的一些基本信息,通過給后臺用戶分配角色來實現(xiàn)菜單和資源的分配。

create table ums_role
(
   id                   bigint not null auto_increment,
   name                 varchar(100) comment '名稱',
   description          varchar(500) comment '描述',
   admin_count          int comment '后臺用戶數(shù)量',
   create_time          datetime comment '創(chuàng)建時間',
   status               int(1) default 1 comment '啟用狀態(tài):0->禁用;1->啟用',
   sort                 int default 0,
   primary key (id)
);

ums_admin_role_relation

后臺用戶和角色關(guān)系表,多對多關(guān)系表,一個角色可以分配給多個用戶。

create table ums_admin_role_relation
(
   id                   bigint not null auto_increment,
   admin_id             bigint,
   role_id              bigint,
   primary key (id)
);

ums_menu

后臺菜單表,用于控制后臺用戶可以訪問的菜單,支持隱藏、排序和更改名稱、圖標。

create table ums_menu
(
   id                   bigint not null auto_increment,
   parent_id            bigint comment '父級ID',
   create_time          datetime comment '創(chuàng)建時間',
   title                varchar(100) comment '菜單名稱',
   level                int(4) comment '菜單級數(shù)',
   sort                 int(4) comment '菜單排序',
   name                 varchar(100) comment '前端名稱',
   icon                 varchar(200) comment '前端圖標',
   hidden               int(1) comment '前端隱藏',
   primary key (id)
);

ums_resource

后臺資源表,用于控制后臺用戶可以訪問的接口,使用了Ant路徑的匹配規(guī)則,可以使用通配符定義一系列接口的權(quán)限。

create table ums_resource
(
   id                   bigint not null auto_increment,
   category_id          bigint comment '資源分類ID',
   create_time          datetime comment '創(chuàng)建時間',
   name                 varchar(200) comment '資源名稱',
   url                  varchar(200) comment '資源URL',
   description          varchar(500) comment '描述',
   primary key (id)
);

ums_resource_category

后臺資源分類表,在細粒度進行權(quán)限控制時,可能資源會比較多,所以設(shè)計了個資源分類的概念,便于給角色分配資源。

create table ums_resource_category
(
   id                   bigint not null auto_increment,
   create_time          datetime comment '創(chuàng)建時間',
   name                 varchar(200) comment '分類名稱',
   sort                 int(4) comment '排序',
   primary key (id)
);

ums_role_menu_relation

后臺角色菜單關(guān)系表,多對多關(guān)系,可以給一個角色分配多個菜單。

create table ums_role_menu_relation
(
   id                   bigint not null auto_increment,
   role_id              bigint comment '角色ID',
   menu_id              bigint comment '菜單ID',
   primary key (id)
);

ums_role_resource_relation

后臺角色資源關(guān)系表,多對多關(guān)系,可以給一個角色分配多個資源。

create table ums_role_resource_relation
(
   id                   bigint not null auto_increment,
   role_id              bigint comment '角色ID',
   resource_id          bigint comment '資源ID',
   primary key (id)
);

結(jié)合Spring Security實現(xiàn)

實現(xiàn)動態(tài)權(quán)限是在原mall-security模塊的基礎(chǔ)上進行改造完成的,原實現(xiàn)有不清楚的可以自行參照前置知識中的文檔來學習。

以前的權(quán)限控制

以前的權(quán)限控制是采用Spring Security的默認機制實現(xiàn)的,下面我們以商品模塊的代碼為例來講講實現(xiàn)原理。

首先我們在需要權(quán)限的接口上使用@PreAuthorize注解定義好需要的權(quán)限;

/**
 * 商品管理Controller
 * Created by macro on 2018/4/26.
 */
@Controller
@Api(tags = "PmsProductController", description = "商品管理")
@RequestMapping("/product")
public class PmsProductController {
    @Autowired
    private PmsProductService productService;
    @ApiOperation("創(chuàng)建商品")
    @RequestMapping(value = "/create", method = RequestMethod.POST)
    @ResponseBody
    @PreAuthorize("hasAuthority('pms:product:create')")
    public CommonResult create(@RequestBody PmsProductParam productParam, BindingResult bindingResult) {
        int count = productService.create(productParam);
        if (count > 0) {
            return CommonResult.success(count);
        } else {
            return CommonResult.failed();
        }
    }
}

然后將該權(quán)限值存入到權(quán)限表中,當用戶登錄時,將其所擁有的權(quán)限查詢出來;

/**
 * UmsAdminService實現(xiàn)類
 * Created by macro on 2018/4/26.
 */
@Service
public class UmsAdminServiceImpl implements UmsAdminService {
    @Override
    public UserDetails loadUserByUsername(String username){
        //獲取用戶信息
        UmsAdmin admin = getAdminByUsername(username);
        if (admin != null) {
            List<UmsPermission> permissionList = getPermissionList(admin.getId());
            return new AdminUserDetails(admin,permissionList);
        }
        throw new UsernameNotFoundException("用戶名或密碼錯誤");
    }
}

之后Spring Security把用戶擁有的權(quán)限值和接口上注解定義的權(quán)限值進行比對,如果包含則可以訪問,反之就不可以訪問;

但是這樣做會帶來一些問題,我們需要在每個接口上都定義好訪問該接口的權(quán)限值,而且只能挨個控制接口的權(quán)限,無法批量控制。其實每個接口都可以由它的訪問路徑唯一確定,我們可以使用基于路徑的動態(tài)權(quán)限控制來解決這些問題。

基于路徑的動態(tài)權(quán)限控制

接下來我們詳細介紹下如何使用Spring Security實現(xiàn)基于路徑的動態(tài)權(quán)限。

首先我們需要創(chuàng)建一個過濾器,用于實現(xiàn)動態(tài)權(quán)限控制,這里需要注意的是doFilter方法,對于OPTIONS請求直接放行,否則前端調(diào)用會出現(xiàn)跨域問題。對于配置在IgnoreUrlsConfig中的白名單路徑我也需要直接放行,所有的鑒權(quán)操作都會在super.beforeInvocation(fi)中進行。

/**
 * 動態(tài)權(quán)限過濾器,用于實現(xiàn)基于路徑的動態(tài)權(quán)限過濾
 * Created by macro on 2020/2/7.
 */
public class DynamicSecurityFilter extends AbstractSecurityInterceptor implements Filter {
    @Autowired
    private DynamicSecurityMetadataSource dynamicSecurityMetadataSource;
    @Autowired
    private IgnoreUrlsConfig ignoreUrlsConfig;
    @Autowired
    public void setMyAccessDecisionManager(DynamicAccessDecisionManager dynamicAccessDecisionManager) {
        super.setAccessDecisionManager(dynamicAccessDecisionManager);
    }
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        FilterInvocation fi = new FilterInvocation(servletRequest, servletResponse, filterChain);
        //OPTIONS請求直接放行
        if(request.getMethod().equals(HttpMethod.OPTIONS.toString())){
            fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
            return;
        }
        //白名單請求直接放行
        PathMatcher pathMatcher = new AntPathMatcher();
        for (String path : ignoreUrlsConfig.getUrls()) {
            if(pathMatcher.match(path,request.getRequestURI())){
                fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
                return;
            }
        }
        //此處會調(diào)用AccessDecisionManager中的decide方法進行鑒權(quán)操作
        InterceptorStatusToken token = super.beforeInvocation(fi);
        try {
            fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
        } finally {
            super.afterInvocation(token, null);
        }
    }
    @Override
    public void destroy() {
    }
    @Override
    public Class<?> getSecureObjectClass() {
        return FilterInvocation.class;
    }
    @Override
    public SecurityMetadataSource obtainSecurityMetadataSource() {
        return dynamicSecurityMetadataSource;
    }
}

在DynamicSecurityFilter中調(diào)用super.beforeInvocation(fi)方法時會調(diào)用AccessDecisionManager中的decide方法用于鑒權(quán)操作,而decide方法中的configAttributes參數(shù)會通過SecurityMetadataSource中的getAttributes方法來獲取,configAttributes其實就是配置好的訪問當前接口所需要的權(quán)限,下面是簡化版的beforeInvocation源碼。

public abstract class AbstractSecurityInterceptor implements InitializingBean,
		ApplicationEventPublisherAware, MessageSourceAware {
protected InterceptorStatusToken beforeInvocation(Object object) {
        //獲取元數(shù)據(jù)
		Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource()
				.getAttributes(object);
		Authentication authenticated = authenticateIfRequired();
		//進行鑒權(quán)操作
		try {
			this.accessDecisionManager.decide(authenticated, object, attributes);
		}
		catch (AccessDeniedException accessDeniedException) {
			publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated,
					accessDeniedException));
			throw accessDeniedException;
		}
	}
}

知道了鑒權(quán)的原理,接下來我們需要自己實現(xiàn)SecurityMetadataSource接口的getAttributes方法,用于獲取當前訪問路徑所需資源。

/**
 * 動態(tài)權(quán)限數(shù)據(jù)源,用于獲取動態(tài)權(quán)限規(guī)則
 * Created by macro on 2020/2/7.
 */
public class DynamicSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
    private static Map<String, ConfigAttribute> configAttributeMap = null;
    @Autowired
    private DynamicSecurityService dynamicSecurityService;
    @PostConstruct
    public void loadDataSource() {
        configAttributeMap = dynamicSecurityService.loadDataSource();
    }
    public void clearDataSource() {
        configAttributeMap.clear();
        configAttributeMap = null;
    }
    @Override
    public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {
        if (configAttributeMap == null) this.loadDataSource();
        List<ConfigAttribute>  configAttributes = new ArrayList<>();
        //獲取當前訪問的路徑
        String url = ((FilterInvocation) o).getRequestUrl();
        String path = URLUtil.getPath(url);
        PathMatcher pathMatcher = new AntPathMatcher();
        Iterator<String> iterator = configAttributeMap.keySet().iterator();
        //獲取訪問該路徑所需資源
        while (iterator.hasNext()) {
            String pattern = iterator.next();
            if (pathMatcher.match(pattern, path)) {
                configAttributes.add(configAttributeMap.get(pattern));
            }
        }
        // 未設(shè)置操作請求權(quán)限,返回空集合
        return configAttributes;
    }
    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        return null;
    }
    @Override
    public boolean supports(Class<?> aClass) {
        return true;
    }
}

由于我們的后臺資源規(guī)則被緩存在了一個Map對象之中,所以當后臺資源發(fā)生變化時,我們需要清空緩存的數(shù)據(jù),然后下次查詢時就會被重新加載進來。

這里我們需要修改UmsResourceController類,注入DynamicSecurityMetadataSource,當修改后臺資源時,需要調(diào)用clearDataSource方法來清空緩存的數(shù)據(jù)。

/**
 * 后臺資源管理Controller
 * Created by macro on 2020/2/4.
 */
@Controller
@Api(tags = "UmsResourceController", description = "后臺資源管理")
@RequestMapping("/resource")
public class UmsResourceController {
    @Autowired
    private UmsResourceService resourceService;
    @Autowired
    private DynamicSecurityMetadataSource dynamicSecurityMetadataSource;
    @ApiOperation("添加后臺資源")
    @RequestMapping(value = "/create", method = RequestMethod.POST)
    @ResponseBody
    public CommonResult create(@RequestBody UmsResource umsResource) {
        int count = resourceService.create(umsResource);
        dynamicSecurityMetadataSource.clearDataSource();
        if (count &gt; 0) {
            return CommonResult.success(count);
        } else {
            return CommonResult.failed();
        }
    }
 }

之后我們需要實現(xiàn)AccessDecisionManager接口來實現(xiàn)權(quán)限校驗,對于沒有配置資源的接口我們直接允許訪問,對于配置了資源的接口,我們把訪問所需資源和用戶擁有的資源進行比對,如果匹配則允許訪問。

/**
 * 動態(tài)權(quán)限決策管理器,用于判斷用戶是否有訪問權(quán)限
 * Created by macro on 2020/2/7.
 */
public class DynamicAccessDecisionManager implements AccessDecisionManager {
    @Override
    public void decide(Authentication authentication, Object object,
                       Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
        // 當接口未被配置資源時直接放行
        if (CollUtil.isEmpty(configAttributes)) {
            return;
        }
        Iterator<ConfigAttribute> iterator = configAttributes.iterator();
        while (iterator.hasNext()) {
            ConfigAttribute configAttribute = iterator.next();
            //將訪問所需資源或用戶擁有資源進行比對
            String needAuthority = configAttribute.getAttribute();
            for (GrantedAuthority grantedAuthority : authentication.getAuthorities()) {
                if (needAuthority.trim().equals(grantedAuthority.getAuthority())) {
                    return;
                }
            }
        }
        throw new AccessDeniedException("抱歉,您沒有訪問權(quán)限");
    }
    @Override
    public boolean supports(ConfigAttribute configAttribute) {
        return true;
    }
    @Override
    public boolean supports(Class<?> aClass) {
        return true;
    }
}

我們之前在DynamicSecurityMetadataSource中注入了一個DynamicSecurityService對象,它是我自定義的一個動態(tài)權(quán)限業(yè)務(wù)接口,其主要用于加載所有的后臺資源規(guī)則。

/**
 * 動態(tài)權(quán)限相關(guān)業(yè)務(wù)類
 * Created by macro on 2020/2/7.
 */
public interface DynamicSecurityService {
    /**
     * 加載資源ANT通配符和資源對應MAP
     */
    Map<String, ConfigAttribute> loadDataSource();
}

接下來我們需要修改Spring Security的配置類SecurityConfig,當有動態(tài)權(quán)限業(yè)務(wù)類時在FilterSecurityInterceptor過濾器前添加我們的動態(tài)權(quán)限過濾器。

這里在創(chuàng)建動態(tài)權(quán)限相關(guān)對象時,還使用了@ConditionalOnBean這個注解,當沒有動態(tài)權(quán)限業(yè)務(wù)類時就不會創(chuàng)建動態(tài)權(quán)限相關(guān)對象,實現(xiàn)了有動態(tài)權(quán)限控制和沒有這兩種情況的兼容。

/**
 * 對SpringSecurity的配置的擴展,支持自定義白名單資源路徑和查詢用戶邏輯
 * Created by macro on 2019/11/5.
 */
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired(required = false)
    private DynamicSecurityService dynamicSecurityService;
    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = httpSecurity
                .authorizeRequests();
        //有動態(tài)權(quán)限配置時添加動態(tài)權(quán)限校驗過濾器
        if(dynamicSecurityService!=null){
            registry.and().addFilterBefore(dynamicSecurityFilter(), FilterSecurityInterceptor.class);
        }
    }
    @ConditionalOnBean(name = "dynamicSecurityService")
    @Bean
    public DynamicAccessDecisionManager dynamicAccessDecisionManager() {
        return new DynamicAccessDecisionManager();
    }
    @ConditionalOnBean(name = "dynamicSecurityService")
    @Bean
    public DynamicSecurityFilter dynamicSecurityFilter() {
        return new DynamicSecurityFilter();
    }
    @ConditionalOnBean(name = "dynamicSecurityService")
    @Bean
    public DynamicSecurityMetadataSource dynamicSecurityMetadataSource() {
        return new DynamicSecurityMetadataSource();
    }
}

這里還有個問題需要提下,當前端跨域訪問沒有權(quán)限的接口時,會出現(xiàn)跨域問題,只需要在沒有權(quán)限訪問的處理類RestfulAccessDeniedHandler中添加允許跨域訪問的響應頭即可。

/**
 * 自定義返回結(jié)果:沒有權(quán)限訪問時
 * Created by macro on 2018/4/26.
 */
public class RestfulAccessDeniedHandler implements AccessDeniedHandler{
    @Override
    public void handle(HttpServletRequest request,
                       HttpServletResponse response,
                       AccessDeniedException e) throws IOException, ServletException {
        response.setHeader("Access-Control-Allow-Origin", "*");
        response.setHeader("Cache-Control","no-cache");
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json");
        response.getWriter().println(JSONUtil.parse(CommonResult.forbidden(e.getMessage())));
        response.getWriter().flush();
    }
}

當我們其他模塊需要動態(tài)權(quán)限控制時,只要創(chuàng)建一個DynamicSecurityService對象就行了,比如在mall-admin模塊中我們啟用了動態(tài)權(quán)限功能。

/**
 * mall-security模塊相關(guān)配置
 * Created by macro on 2019/11/9.
 */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MallSecurityConfig extends SecurityConfig {
    @Autowired
    private UmsAdminService adminService;
    @Autowired
    private UmsResourceService resourceService;
    @Bean
    public UserDetailsService userDetailsService() {
        //獲取登錄用戶信息
        return username -&gt; adminService.loadUserByUsername(username);
    }
    @Bean
    public DynamicSecurityService dynamicSecurityService() {
        return new DynamicSecurityService() {
            @Override
            public Map&lt;String, ConfigAttribute&gt; loadDataSource() {
                Map&lt;String, ConfigAttribute&gt; map = new ConcurrentHashMap&lt;&gt;();
                List&lt;UmsResource&gt; resourceList = resourceService.listAll();
                for (UmsResource resource : resourceList) {
                    map.put(resource.getUrl(), new org.springframework.security.access.SecurityConfig(resource.getId() + ":" + resource.getName()));
                }
                return map;
            }
        };
    }
}

權(quán)限管理功能演示

具體參考:大家心心念念的權(quán)限管理功能,這次安排上了!

mall項目更新源碼地址 https://github.com/macrozheng/mall

以上就是Spring Security權(quán)限管理實現(xiàn)接口動態(tài)權(quán)限控制的詳細內(nèi)容,更多關(guān)于Spring Security接口動態(tài)權(quán)限控制的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • Spring實現(xiàn)定時任務(wù)的幾種方式總結(jié)

    Spring實現(xiàn)定時任務(wù)的幾種方式總結(jié)

    Spring Task 是 Spring 框架提供的一種任務(wù)調(diào)度和異步處理的解決方案,可以按照約定的時間自動執(zhí)行某個代碼邏輯它可以幫助開發(fā)者在 Spring 應用中輕松地實現(xiàn)定時任務(wù)、異步任務(wù)等功能,提高應用的效率和可維護性,需要的朋友可以參考下本文
    2024-07-07
  • SpringBoot整合Mybatis簡單實現(xiàn)增刪改查

    SpringBoot整合Mybatis簡單實現(xiàn)增刪改查

    這篇文章主要介紹了SpringBoot整合Mybatis簡單實現(xiàn)增刪改查,文章為圍繞主題展開詳細的內(nèi)容介紹,具有一定的參考價值,需要的小伙伴可以參考一下
    2022-08-08
  • MyBatis?實現(xiàn)多對多中間表插入數(shù)據(jù)

    MyBatis?實現(xiàn)多對多中間表插入數(shù)據(jù)

    這篇文章主要介紹了MyBatis?實現(xiàn)多對多中間表插入數(shù)據(jù),具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2022-02-02
  • Spring框架七大模塊簡單介紹

    Spring框架七大模塊簡單介紹

    這篇文章主要介紹了Spring框架七大模塊簡單介紹,具有一定參考價值,需要的朋友可以了解下。
    2017-11-11
  • Spring boot JPA實現(xiàn)分頁和枚舉轉(zhuǎn)換代碼示例

    Spring boot JPA實現(xiàn)分頁和枚舉轉(zhuǎn)換代碼示例

    這篇文章主要介紹了Spring boot JPA實現(xiàn)分頁和枚舉轉(zhuǎn)換代碼示例,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下
    2020-09-09
  • 一分鐘掌握Java?ElasticJob分布式定時任務(wù)

    一分鐘掌握Java?ElasticJob分布式定時任務(wù)

    ElasticJob?是面向互聯(lián)網(wǎng)生態(tài)和海量任務(wù)的分布式調(diào)度解決方案,本文主要通過簡單的示例帶大家深入了解ElasticJob分布式定時任務(wù)的相關(guān)知識,需要的可以參考一下
    2023-05-05
  • mybatis注解之@Mapper和@MapperScan的使用

    mybatis注解之@Mapper和@MapperScan的使用

    這篇文章主要介紹了mybatis注解之@Mapper和@MapperScan的使用,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2021-10-10
  • 深入探究HashMap二次Hash原因

    深入探究HashMap二次Hash原因

    在java開發(fā)中,HashMap是最常用、最常見的集合容器類之一,文中通過示例代碼介紹HashMap為啥要二次Hash,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2023-01-01
  • Quarkus集成open api接口使用swagger ui展示

    Quarkus集成open api接口使用swagger ui展示

    這篇文章主要為大家介紹了Quarkus集成open?api接口使用swagger?ui的展示示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步
    2022-02-02
  • java實現(xiàn)簡單快遞系統(tǒng)

    java實現(xiàn)簡單快遞系統(tǒng)

    這篇文章主要為大家詳細介紹了java實現(xiàn)簡單快遞系統(tǒng),文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2022-03-03

最新評論