知名网站建设定制Spring Cloud Gateway 网关实现白名单功能

文章目录

1 摘要

知名网站建设定制对于后台而言,知名网站建设定制网关层作为所有网络请求的入口。知名网站建设定制一般基于安全考虑,知名网站建设定制会在网关层做权限认证,但是对于一些例如登录、注册等接口以及一些资源数据,这些是不需要有认证信息,因此需要在网关层设计一个白名单的功能。本文将基于 Spring Cloud Gateway 2.X 实现白名单功能。

注意事项 : Gateway 层的白名单实现原理是在过滤器内判断请求地址是否符合白名单,如果通过则跳过当前过滤器。如果有多个过滤器,则需要在每一个过滤器里边添加白名单判断。

2 核心 Maven 依赖

./cloud-alibaba-gateway-filter/pom.xml
  • 1
        <!-- cloud gateway -->        <dependency>            <groupId>org.springframework.cloud</groupId>            <artifactId>spring-cloud-starter-gateway</artifactId>            <version>${spring-cloud-gateway.version}</version>        </dependency>        <!-- hutool -->        <dependency>            <groupId>cn.hutool</groupId>            <artifactId>hutool-all</artifactId>            <version>${hutool.version}</version>        </dependency>        <!-- mysql -->        <dependency>            <groupId>mysql</groupId>            <artifactId>mysql-connector-java</artifactId>            <version>${mysql.version}</version>            <scope>runtime</scope>        </dependency>        <!-- Mybatis Plus(include Mybatis) -->        <dependency>            <groupId>com.baomidou</groupId>            <artifactId>mybatis-plus-boot-starter</artifactId>            <version>${mybatis-plus.version}</version>        </dependency>        <!-- Redisson -->        <dependency>            <groupId>org.redisson</groupId>            <artifactId>redisson-spring-boot-starter</artifactId>            <version>${redisson-spring.version}</version>        </dependency>        <!-- lombok -->        <dependency>            <groupId>org.projectlombok</groupId>            <artifactId>lombok</artifactId>            <version>${lombok.version}</version>        </dependency>
  • 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

依赖对应的版本为:

        <lombok.version>1.18.24</lombok.version>        <spring-cloud-gateway.version>2.2.5.RELEASE</spring-cloud-gateway.version>        <servlet-api.version>4.0.1</servlet-api.version>        <hutool.version>5.7.8</hutool.version>        <mysql.version>8.0.27</mysql.version>        <mybatis-plus.version>3.4.3.4</mybatis-plus.version>        <redisson-spring.version>3.17.5</redisson-spring.version>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

3 白名单数据库设计

./doc/sql/gateway-database-create.sql
  • 1
/*==============================================================*//* Table: gateway_white_list                                    *//*==============================================================*/create table gateway_white_list(   id                   bigint unsigned not null comment 'id',   route_type           varchar(32) comment '路由类型',   path                 varchar(128) comment '请求路径',   comment              varchar(128) comment '说明',   create_date          bigint unsigned comment '创建时间',   update_date          bigint unsigned comment '更新时间',   primary key (id))engine = innodb defaultcharset = utf8mb4;alter table gateway_white_list comment '网关路由白名单';
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

4 核心代码

4.1 白名单实体类

./cloud-alibaba-gateway-filter/src/main/java/com/ljq/demo/springboot/alibaba/gateway/filter/model/WhiteListEntity.java
  • 1
package com.ljq.demo.springboot.alibaba.gateway.filter.model;import com.baomidou.mybatisplus.annotation.*;import lombok.Data;import java.io.Serializable;/** * @Description: 网关路由白名单 * @Author: junqiang.lu * @Date: 2022/8/23 */@Data@TableName(value = "gateway_white_list")public class WhiteListEntity implements Serializable {    private static final long serialVersionUID = -854919732121208131L;    /**     * id     */    @TableId(type = IdType.NONE)    private Long id;    /**     * 路由类型     */    private String routeType;    /**     * 请求路径     */    private String path;    /**     * 说明     */    private String comment;    /**     * 创建时间     */    private Long createDate;    /**     * 更新时间     */    private Long updateDate;}
  • 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

4.2 白名单 DAO 接口

./cloud-alibaba-gateway-filter/src/main/java/com/ljq/demo/springboot/alibaba/gateway/filter/dao/WhiteListMapper.java
  • 1
package com.ljq.demo.springboot.alibaba.gateway.filter.dao;import com.baomidou.mybatisplus.core.mapper.BaseMapper;import com.ljq.demo.springboot.alibaba.gateway.filter.model.WhiteListEntity;import org.springframework.stereotype.Repository;/** * @Description: 网关路由白名单DAO接口 * @Author: junqiang.lu * @Date: 2022/8/23 */@Repositorypublic interface WhiteListMapper extends BaseMapper<WhiteListEntity> {}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

4.3 初始化加载白名单

./cloud-alibaba-gateway-filter/src/main/java/com/ljq/demo/springboot/alibaba/gateway/filter/common/component/WhiteListHandler.java
  • 1
package com.ljq.demo.springboot.alibaba.gateway.filter.common.component;import com.baomidou.mybatisplus.core.toolkit.Wrappers;import com.ljq.demo.springboot.alibaba.gateway.filter.common.constant.RedisKeyConst;import com.ljq.demo.springboot.alibaba.gateway.filter.dao.WhiteListMapper;import com.ljq.demo.springboot.alibaba.gateway.filter.model.WhiteListEntity;import lombok.extern.slf4j.Slf4j;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.ApplicationArguments;import org.springframework.boot.ApplicationRunner;import org.springframework.stereotype.Component;import java.util.HashMap;import java.util.List;import java.util.Map;/** * @Description: 网关白名单处理类 * @Author: junqiang.lu * @Date: 2022/8/23 */@Slf4j@Componentpublic class WhiteListHandler implements ApplicationRunner {    @Autowired    private WhiteListMapper whiteListMapper;    @Autowired    private RedisComponent redisComponent;    @Override    public void run(ApplicationArguments args) throws Exception {        // 将所有白名单加载到缓存中        log.info("-------------加载网关路由白名单------------------");        List<WhiteListEntity> whiteListList = whiteListMapper.selectList(Wrappers.emptyWrapper());        Map<String, Object> whiteListMap = new HashMap<>(16);        whiteListList.forEach(whiteList -> whiteListMap.put(whiteList.getId().toString(), whiteList));        redisComponent.mapPutBatch(RedisKeyConst.KEY_GATEWAY_WHITE_LIST, whiteListMap);    }}
  • 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

4.4 权限过滤器

./cloud-alibaba-gateway-filter/src/main/java/com/ljq/demo/springboot/alibaba/gateway/filter/interceptor/AuthFilter.java
  • 1
package com.ljq.demo.springboot.alibaba.gateway.filter.interceptor;import cn.hutool.json.JSONUtil;import com.ljq.demo.springboot.alibaba.gateway.filter.common.api.ApiResult;import com.ljq.demo.springboot.alibaba.gateway.filter.common.component.RedisComponent;import com.ljq.demo.springboot.alibaba.gateway.filter.common.constant.RedisKeyConst;import com.ljq.demo.springboot.alibaba.gateway.filter.model.WhiteListEntity;import lombok.extern.slf4j.Slf4j;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.cloud.gateway.filter.GatewayFilterChain;import org.springframework.cloud.gateway.filter.GlobalFilter;import org.springframework.core.Ordered;import org.springframework.core.io.buffer.DataBuffer;import org.springframework.http.HttpHeaders;import org.springframework.http.HttpStatus;import org.springframework.http.MediaType;import org.springframework.http.server.reactive.ServerHttpResponse;import org.springframework.stereotype.Component;import org.springframework.util.CollectionUtils;import org.springframework.web.server.ServerWebExchange;import reactor.core.publisher.Mono;import java.nio.charset.StandardCharsets;import java.util.List;/** * @Description: 鉴权拦截器 * @Author: junqiang.lu * @Date: 2020/12/8 */@Slf4j@Componentpublic class AuthFilter implements GlobalFilter, Ordered {    private static final String TOKEN_KEY = "token";    @Autowired    private RedisComponent redisComponent;    /**     * 权限过滤     *     * @param exchange     * @param chain     * @return     */    @Override    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {        String requestPath = exchange.getRequest().getPath().value();        log.info("requestPath: {}",requestPath);        // 判断是否符合白名单        if (validateWhiteList(requestPath)) {            return chain.filter(exchange);        }        List<String> tokenList = exchange.getRequest().getHeaders().get(TOKEN_KEY);        log.info("token: {}", tokenList);        if (CollectionUtils.isEmpty(tokenList) || tokenList.get(0).trim().isEmpty()) {            ServerHttpResponse response = exchange.getResponse();            // 错误信息            byte[] data = JSONUtil.toJsonStr(ApiResult.fail("Token is null")).getBytes(StandardCharsets.UTF_8);            DataBuffer buffer = response.bufferFactory().wrap(data);            response.setStatusCode(HttpStatus.UNAUTHORIZED);            response.getHeaders().add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);            return response.writeWith(Mono.just(buffer));        }        return chain.filter(exchange);    }    /**     * 设置执行级别     *     * @return     */    @Override    public int getOrder() {        return Ordered.LOWEST_PRECEDENCE;    }    /**     * 请求路径     *     * @param requestPath     * @return     */    public boolean validateWhiteList(String requestPath) {        List<WhiteListEntity> whiteListList = redisComponent.mapGetAll(RedisKeyConst.KEY_GATEWAY_WHITE_LIST,                WhiteListEntity.class);        for (WhiteListEntity whiteList : whiteListList) {            if (requestPath.contains(whiteList.getPath()) || requestPath.matches(whiteList.getPath())) {                return true;            }        }        return false;    }}
  • 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

必须在进入过滤器后首先进行白名单校验

白名单规则支持正则表达式

4.5 白名单 Service 层

Service 接口

./cloud-alibaba-gateway-filter/src/main/java/com/ljq/demo/springboot/alibaba/gateway/filter/service/WhiteListService.java
  • 1
package com.ljq.demo.springboot.alibaba.gateway.filter.service;import com.ljq.demo.springboot.alibaba.gateway.filter.common.api.ApiResult;import com.ljq.demo.springboot.alibaba.gateway.filter.model.*;/** * @Description: 白名单业务接口 * @Author: junqiang.lu * @Date: 2022/8/23 */public interface WhiteListService {    /**     * 新增单条     *     * @param addParam     * @return     */    ApiResult add(WhiteListAddParam addParam);    /**     * 查询单条     *     * @param infoParam     * @return     */    ApiResult info(WhiteListInfoParam infoParam);    /**     * 分页查询     *     * @param pageParam     * @return     */    ApiResult page(WhiteListPageParam pageParam);    /**     * 更新单条     *     * @param updateParam     * @return     */    ApiResult update(WhiteListUpdateParam updateParam);    /**     * 删除单条     *     * @param deleteParam     * @return     */    ApiResult delete(WhiteListDeleteParam deleteParam);}
  • 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

Service 业务实现类

./cloud-alibaba-gateway-filter/src/main/java/com/ljq/demo/springboot/alibaba/gateway/filter/service/impl/WhiteListServiceImpl.java
  • 1
package com.ljq.demo.springboot.alibaba.gateway.filter.service.impl;import cn.hutool.core.bean.BeanUtil;import cn.hutool.core.bean.copier.CopyOptions;import cn.hutool.core.util.StrUtil;import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;import com.baomidou.mybatisplus.core.metadata.IPage;import com.baomidou.mybatisplus.core.toolkit.Wrappers;import com.baomidou.mybatisplus.extension.plugins.pagination.Page;import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;import com.ljq.demo.springboot.alibaba.gateway.filter.common.api.ApiResult;import com.ljq.demo.springboot.alibaba.gateway.filter.common.component.RedisComponent;import com.ljq.demo.springboot.alibaba.gateway.filter.common.constant.RedisKeyConst;import com.ljq.demo.springboot.alibaba.gateway.filter.dao.WhiteListMapper;import com.ljq.demo.springboot.alibaba.gateway.filter.model.*;import com.ljq.demo.springboot.alibaba.gateway.filter.service.WhiteListService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;import org.springframework.transaction.annotation.Propagation;import org.springframework.transaction.annotation.Transactional;import java.util.Objects;/** * @Description: 网关路由白名单业务实现类 * @Author: junqiang.lu * @Date: 2022/8/23 */@Servicepublic class WhiteListServiceImpl extends ServiceImpl<WhiteListMapper, WhiteListEntity> implements WhiteListService {    @Autowired    private RedisComponent redisComponent;    /**     * 新增单条     *     * @param addParam     * @return     */    @Override    @Transactional(propagation = Propagation.REQUIRED, rollbackFor = {Exception.class})    public ApiResult add(WhiteListAddParam addParam) {        WhiteListEntity whiteListParam = new WhiteListEntity();        BeanUtil.copyProperties(addParam, whiteListParam, CopyOptions.create().ignoreError().ignoreNullValue());        long nowTime = System.currentTimeMillis();        whiteListParam.setCreateDate(nowTime);        whiteListParam.setUpdateDate(nowTime);        this.save(whiteListParam);        redisComponent.mapPut(RedisKeyConst.KEY_GATEWAY_WHITE_LIST, whiteListParam.getId().toString(), whiteListParam);        return ApiResult.success(whiteListParam);    }    /**     * 查询单条     *     * @param infoParam     * @return     */    @Override    public ApiResult info(WhiteListInfoParam infoParam) {        WhiteListEntity whiteList = redisComponent.mapGet(RedisKeyConst.KEY_GATEWAY_WHITE_LIST,                infoParam.getId().toString(), WhiteListEntity.class);        if (Objects.isNull(whiteList)) {            whiteList = this.getById(infoParam.getId());        }        return ApiResult.success(whiteList);    }    /**     * 分页查询     *     * @param pageParam     * @return     */    @Override    public ApiResult page(WhiteListPageParam pageParam) {        IPage<WhiteListEntity> page = new Page<>(pageParam.getCurrentPage(), pageParam.getPageSize());        LambdaQueryWrapper<WhiteListEntity> queryWrapper = Wrappers.lambdaQuery();        queryWrapper.eq(StrUtil.isNotBlank(pageParam.getRouteType()), WhiteListEntity::getRouteType,                        pageParam.getRouteType())                .like(StrUtil.isNotBlank(pageParam.getPath()), WhiteListEntity::getPath, pageParam.getPath())                .like(StrUtil.isNotBlank(pageParam.getComment()), WhiteListEntity::getComment, pageParam.getComment());        return ApiResult.success(this.page(page, queryWrapper));    }    /**     * 更新单条     *     * @param updateParam     * @return     */    @Override    @Transactional(propagation = Propagation.REQUIRED, rollbackFor = {Exception.class})    public ApiResult update(WhiteListUpdateParam updateParam) {        WhiteListEntity whiteListParam = new WhiteListEntity();        BeanUtil.copyProperties(updateParam, whiteListParam, CopyOptions.create().ignoreError().ignoreNullValue());        long nowTime = System.currentTimeMillis();        whiteListParam.setUpdateDate(nowTime);        boolean flag = this.updateById(whiteListParam);        if (flag) {            redisComponent.mapPut(RedisKeyConst.KEY_GATEWAY_WHITE_LIST, whiteListParam.getId().toString(),                    whiteListParam);        }        return ApiResult.success(flag);    }    /**     * 删除单条     *     * @param deleteParam     * @return     */    @Override    @Transactional(propagation = Propagation.REQUIRED, rollbackFor = {Exception.class})    public ApiResult delete(WhiteListDeleteParam deleteParam) {        boolean flag = this.removeById(deleteParam.getId());        if (flag) {            redisComponent.mapRemove(RedisKeyConst.KEY_GATEWAY_WHITE_LIST, deleteParam.getId().toString());        }        return ApiResult.success(flag);    }}
  • 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
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123

白名单的写操作都需要同步到缓存中

4.6 白名单控制层

./cloud-alibaba-gateway-filter/src/main/java/com/ljq/demo/springboot/alibaba/gateway/filter/controller/WhiteListController.java
  • 1
package com.ljq.demo.springboot.alibaba.gateway.filter.controller;import com.baomidou.mybatisplus.core.metadata.IPage;import com.ljq.demo.springboot.alibaba.gateway.filter.common.api.ApiResult;import com.ljq.demo.springboot.alibaba.gateway.filter.model.*;import com.ljq.demo.springboot.alibaba.gateway.filter.service.WhiteListService;import lombok.extern.slf4j.Slf4j;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.http.MediaType;import org.springframework.http.ResponseEntity;import org.springframework.validation.annotation.Validated;import org.springframework.web.bind.annotation.*;/** * @Description: 网关路由白名单控制器 * @Author: junqiang.lu * @Date: 2022/8/23 */@Slf4j@RestController@RequestMapping(value = "/api/gateway/whitelist")public class WhiteListController {    @Autowired    private WhiteListService whiteListService;    /**     * 新增白名单     *     * @param addParam     * @return     */    @PostMapping(value = "/add", produces = {MediaType.APPLICATION_JSON_VALUE})    public ResponseEntity<ApiResult<WhiteListEntity>> add(@RequestBody @Validated WhiteListAddParam addParam) {        log.info("/add,新增白名单参数: {}", addParam);        return ResponseEntity.ok(whiteListService.add(addParam));    }    /**     * 查询单条白名单     *     * @param infoParam     * @return     */    @GetMapping(value = "/info", produces = {MediaType.APPLICATION_JSON_VALUE})    public ResponseEntity<ApiResult<WhiteListEntity>> info(@Validated WhiteListInfoParam infoParam) {        log.info("/info,查询单条白名单参数: {}", infoParam);        return ResponseEntity.ok(whiteListService.info(infoParam));    }    /**     * 查询单条白名单     *     * @param pageParam     * @return     */    @GetMapping(value = "/page", produces = {MediaType.APPLICATION_JSON_VALUE})    public ResponseEntity<ApiResult<IPage<WhiteListEntity>>> page(@Validated WhiteListPageParam pageParam) {        log.info("/page,分页查询白名单参数: {}", pageParam);        return ResponseEntity.ok(whiteListService.page(pageParam));    }    /**     * 修改白名单     *     * @param updateParam     * @return     */    @PutMapping(value = "/update", produces = {MediaType.APPLICATION_JSON_VALUE})    public ResponseEntity<ApiResult<Boolean>> update(@RequestBody @Validated WhiteListUpdateParam updateParam) {        log.info("/update,修改白名单参数: {}", updateParam);        return ResponseEntity.ok(whiteListService.update(updateParam));    }    /**     * 删除单条白名单     *     * @param deleteParam     * @return     */    @DeleteMapping(value = "/delete", produces = {MediaType.APPLICATION_JSON_VALUE})    public ResponseEntity<ApiResult<Boolean>> delete(@RequestBody @Validated WhiteListDeleteParam deleteParam) {        log.info("/delete,删除单条白名单参数: {}", deleteParam);        return ResponseEntity.ok(whiteListService.delete(deleteParam));    }}
  • 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

5 Github 源码

Gtihub 源码地址 :

网站建设定制开发 软件系统开发定制 定制软件开发 软件开发定制 定制app开发 app开发定制 app开发定制公司 电商商城定制开发 定制小程序开发 定制开发小程序 客户管理系统开发定制 定制网站 定制开发 crm开发定制 开发公司 小程序开发定制 定制软件 收款定制开发 企业网站定制开发 定制化开发 android系统定制开发 定制小程序开发费用 定制设计 专注app软件定制开发 软件开发定制定制 知名网站建设定制 软件定制开发供应商 应用系统定制开发 软件系统定制开发 企业管理系统定制开发 系统定制开发