2 Star 9 Fork 2

ckw1988/KRest

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
克隆/下载
贡献代码
同步代码
取消
提示: 由于 Git 不支持空文件夾,创建文件夹后会生成空的 .keep 文件
Loading...
README
Apache-2.0

KRest

介绍

krest是一款基于SpringBoot的轻量级RESTful框架,旨在整合no-session服务中常见的基础模块(身份验证、权限控制、通信加密等),并提供统一精简的配置、使用方式。本品功能完善、配置简单,具有耦合低、侵入性小、安全稳定、扩展灵活等特点。

产品特性

  1. 本产品的核心功能能通过整合shiro+jwt+cryption(通信加密)三个模块实现,并通过委托模式(delegating)将各个模块的配置方式统一整合,最大程度地精简用户的配置和使用方式。同时充分利用SpringBoot的自动装配机制,保留了灵活强大的可扩展空间。
  2. 本品不会参与任何数据持久层或远程缓存的读写方式,无论是加解密策略、秘钥,还是用户-权限信息,其存储配置方式均可完全自由地按照用户自行定义的方式实现。唯一需要遵守的是确保权限数据的结构设计符合RBAC规范。
  3. 充分运用了shiro1.8版本的新机制,对jwt模块有了更为原生态的兼容。
  4. 用Hutool加密工具自行实现了一套通信加密模块Cryption,采用了不对称和对称加密的结合的加密策略,安全性高。并且同样做了人性化的封装,以保证配置简单,使用灵活。
  5. 本项目的demos目录下包含三个功能完善的演示模块以及相应的客户端postman脚本和sql脚本。demo源码中有非常完善的注释说明帮助你快速上手本框架,也可以直接当做企业级项目的初始工程。

如果你对本项目所运用的shiro+jwt整合原理感兴趣,想单纯地学习实现两者的整合,可以出门左拐参考我另一个项目:shiro-jwt-integration 我把KRest中所运用的jwt+shiro整合部分的知识单独提取出来做了这份教程和示例。

版本要求

  • JDK1.8以上

快速开始

  1. 在pom中导入包(本项目已发布到maven中央库,直接写配置即可。)

     <dependency>
        <groupId>com.chenkaiwei.krest</groupId>
        <artifactId>krest-core</artifactId>
         <version>${最新版本号}</version>
     </dependency>

    注:您可以在本项目的发布(release)页找到最新的版本号,也可在maven中央库中查看(https://search.maven.org/search?q=a:krest-core )。

  2. 新建一个config类(或在您原有的config类上)实现KrestConfigurer接口

    @Configuration
    public class DemoConfig implements KrestConfigurer {  
    
        //1.按下图方式配置角色-权限映射,返回值中key为角色(Role)名称,value为该角色所拥有的所有权限(Permission)。
       //此处的硬编码仅作示例,您可以将这部分数据以任何您喜欢的方式存储(数据库、钥匙串、文本文件等,均可),只需在本方法中以同步方法取出并确保其按标准格式返回即可。
        @Override
        public Map<String, Collection<String>> configRolePermissionsMap() {
            Map<String, Collection<String>> res=new HashMap<String, Collection<String>>();
            res.put("admin", Arrays.asList("p1","p2","p3","p4"));
            res.put("user", Arrays.asList("p3","p4"));
            return res;
        }
            
        //2.配置jwt Token的加密策略,字符串部分为秘钥
        //同上,该加密策略中的加密方式和秘钥,也可自由实现存取方式,只需返回格式符合要求即可。
        @Override
        public Algorithm configJwtAlgorithm() {
            return Algorithm.HMAC256("mydemosecretkey");
        }
    
        //3.(可选)配置忽略jwt验证的路径规则,默认配置如下四条。本方法中的语法来自shiro,如果您对路径映射规则有更多的需求,也可一并在本方法中配置。
        @Override
        public void configFilterChainDefinitionMap(Map<String, String> filterRuleMap) {
            //配置不参与token验证的uri
            filterRuleMap.put("/static/*", "anon");
            filterRuleMap.put("/error", "anon");
            filterRuleMap.put("/register", "anon");
            filterRuleMap.put("/login", "anon");
        }
    }
  3. (可选)实现登录方法(即登录成功后发放初始新token)。如果您在本服务中仅须实现已有token的验证和更新发放功能(比如已从别的服务完成登录获取token),则可跳过本步骤。

    在controller中简单实现一个登陆方法,示例如下:

    @PostMapping("/login")
    public Map login(@RequestBody User userInput) throws Exception {
    
        Map res=new HashMap();
        if(userInput.getUsername().equals("zhang3")&&userInput.getPassword().equals("12345")){
            //TODO ↑你自己的验证规则
    
            JwtUser jwtUser=new JwtUser("zhang3", Arrays.asList("admin"));
            res.put("token",KrestUtil.createJwtTokenByUser(jwtUser));
            //↑ 关键是这一步,将token返回给客户端以供后续请求时验证身份。
    
            res.put("message","login success");
        }else{
            res.put("message","login failed");
            // throw new KrestAuthenticationException("登录失败");
    
        }
        return res;
    }

    登陆成功后使用JwtUser封装用户,并以此生成token,返回给客户端。

    本例中zhang3为用户名,admin为其角色(role)。该角色名即为步骤2中角色-权限对照表中的的key,请保证两者的对应。

  4. 客户端的操作(即客户端部分的Jwt令牌使用规则,已经懂的可以不看): 客户端在获取到token后,应将其加上"Bearer "前缀使用。在后续的请求中,只须将该"Bearer "+token的字符串以"Authorization"为属性名加到请求头中,即可自动实现身份验证。

    完成后的效果类似下表

    KEY VALUE
    Authorization Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJhZG1pbiJdLCJleHAiOjE2NDY3OTcyMjEsInVzZXJuYW1lIjoiemhhbmczIn0.HroVIdxf5qmpjWJlOs0QGW7OtaTcjirD9aMViK4oDdI

    注意:Bearer和令牌字符串之间有且仅有一个半角空格。

  5. 服务端的操作。只要你完成步骤2中的配置,则对jwt token 的验证工作会全部由框架自动完成。唯一需要你亲自动手的是在请求的返回值中加入以下代码,即可实现Token的自动刷新,下一步骤中配置的过期时间和刷新机制也会同时生效。

    resBody.put("token",KrestUtil.createNewJwtTokenIfNeeded());

    一般建议使用ResponseBodyAdvice等方式统一封装在返回结果中。如此设计是为了避免对用户的返回数据结构进行过多约束。

  6. (可选)自定义token过期时间和自动刷新时间。

    在application.yml文件中增加如下配置:

     krest:
       jwt:
         expire-time: 1m
         #↑token过期时间,默认20分钟
         refresh-time-before-expire: 40s
         #↑过期前多久更新token。默认10分钟。若设为与expire-time一致,则每次请求都会刷新。

    至此,配置完成。

  7. 运行测试

    在controller中加入如下代码

    @GetMapping("/permissionDemo")
    @RequiresPermissions("p1")//表示当用户拥有"p1"权限时才被许可访问该方法。role同理,这部分使用来自shiro语法。
    public Map permissionDemo(){
        Map<String,String> res=new HashMap<>();
        res.put("result","you have got the permission [permissionDemo]");
        res.put("token",KrestUtil.createNewJwtTokenIfNeeded());
        return res;
    }

进阶使用

启用框架自带的用户名-密码登录功能

本框架包含一套完善的用户名密码登陆机制,通过实现shiro的原生组件实现,可在配置文件中一键开启。配置如下。

 krest:
   enable-username-password-realm: true
         

您仅需在config文件中配置一些最必要的设置即可激活该功能。其具体规则参考krest-demo-1源码。

自定义异常返回

  1. 继承KrestErrorController并覆盖getErrorResponseBody方法来自定义返回错误时的数据结构。

  2. 通过定义全局ExceptionHandler来捕获异常。具体规则参考demo中的GlobalExceptionController文件。

 @Slf4j
 @RestControllerAdvice
 public class GlobalExceptionController {
 
     //按你自己的方式来统一返回格式,此处仅做示例,为了好懂就不抽象了。
     @ExceptionHandler({KrestAuthenticationException.class,AuthenticationException.class})
     public ResponseEntity KrestAuthenticationExceptionHandler(KrestAuthenticationException e) {
         log.error("krestExceptionHandler");
         log.error(e.getLocalizedMessage());
 
         Map<String,Object> body=new HashMap<String,Object>();
         body.put("status",HttpStatus.FORBIDDEN.value());//也可以自定义更详细的状态码
         body.put("message",e.getLocalizedMessage());
         body.put("exception",e.getClass().getName());
         body.put("error",HttpStatus.FORBIDDEN.getReasonPhrase());
         return new ResponseEntity(body, HttpStatus.FORBIDDEN);//仅是示例,按需求定义
     }
 
 
     //权限验证错误
     @ExceptionHandler(UnauthorizedException.class)
     public ResponseEntity unauthorizedExceptionHandler(UnauthorizedException e) {
         log.error("unauthorizedExceptionHandler");
         log.error(e.getLocalizedMessage());
 
         Map<String,Object> body=new HashMap<String,Object>();
         body.put("status",HttpStatus.UNAUTHORIZED.value());
         body.put("message",e.getLocalizedMessage());
         body.put("exception",e.getClass().getName());
         body.put("error",HttpStatus.UNAUTHORIZED.getReasonPhrase());
         return new ResponseEntity(body, HttpStatus.UNAUTHORIZED);//仅是示例,按需求定义
     }
     @ExceptionHandler(Exception.class)
     public ResponseEntity exceptionHandler(Exception e) {
         log.error("exceptionHandler");
         log.error(e.getLocalizedMessage());
         log.error(e.getStackTrace().toString());
 
         Map<String,Object> body=new HashMap<String,Object>();
         body.put("message",e.getLocalizedMessage());
         body.put("exception",e.getClass().getName());
         body.put("error",HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase());
         return new ResponseEntity(body, HttpStatus.INTERNAL_SERVER_ERROR);//仅是示例,按需求定义
     }
 }

通信加密模块

本框架还包含一个通信加密模块。使用规范如下:

  1. 由客户端生成一个临时秘钥(tempSecretKey),以此为秘钥,使用 对称加密 策略将整条消息体加密(也可只加密您需要的字段)。加密策略须与服务端协商一致。

  2. 双方约定一个不对称加密策略,用以加密解密临时秘钥(tempSecretKey)。公钥由客户端维护,私钥由服务端维护。客户端在访问服务端的加密接口前,将临时秘钥用不对称加密的公钥加密后放入头信息的Cryption字段中。伴随在步骤1中已加密的消息体一并发送到服务端对应的接口。

  3. 以下是服务端部分:需要先在配置文件中开启接口加密功能:

         krest.cryption.enable-cryption = true
  4. 在Krest配置文件中实现:

    
      //解密临时秘钥的策略是一次配置一直使用
      @Override
      public AsymmetricCrypto configTempSecretKeyCryptoAlgorithm() {
    
          String privateKey="MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAJdor7t0PvE590FArr-hv_pqtsk1R-iXaFX0upUJ8XkmHrMU6qpZM27oZzMOm62r_DzLTWNKZal-QH987OXQj35TnhrbwLxl57PZ6wfV_hggHlMgtnp_7yYJAPgS2mVN0E5VInPmuMcES598pB-1lvnUJ0-386ny_FS9-IUJRMDBAgMBAAECgYEAlmR00aT49FFYiOc_7Lc04v9myltzLtRd3at2PZ4fze-QZN9s7IIn9Y1BHNTwy8ReiuCB4RNAAeiXFks3YFsWe5yHHsW_Y3ntN0Tla_nkVkjm2iG_dIKHS5iY3ERoheR8i0d0T1BnmwbyCwdl7-QWmjVdeZ8YPFxAQ72Wr6DLY6UCQQDwgAGW8rdxbKQjqSIoFTRfSTLfI6Ba3dsb7xNiQE3RSiq_k4LskbnNCAqf7WNy85gNjENX-W8lmP1t6rJqC5tvAkEAoSrFs6HpF2I469ALkZH6iapi7k97W4nlnnOeaNAx9uuXy9hyQiKSGZafSidxPvmbV1qV2CVxc53FhTCD5b4OzwJAK4LtRrMZD1NZiv4hqODVPdwPcSGP9ICpEK-7cQ4zRgdGHq0Ahe6DkB3BVlfrozOBMgpLcNI3ErVQPJ-2scrxzwJAWybfzisCtBD_dI-kG17evkG51mLpt-oUDjwCGfG2cJrqrYXriXAYBZTk3oHUUPPHYe5_1VHICsXu0tePob6OjQJAeZXbdfkNx7-uZ295rTj3Yq3H11uB6hB317eODHtnnCMVH0ww50C9pGnRPO2dEaShCwLUeOucxBim_usmIBaPOw";
          //↑ 用你自己的方式获取秘钥。确保和客户端保存的公钥 成对。
    
          RSA rsa = new RSA(privateKey, null);
     //   byte[] encrypt = StrUtil.bytes(cryptionString, CharsetUtil.CHARSET_UTF_8);//先转成二进制数组
    
          return rsa;//兼容hutool中继承AsymmetricCrypto实现的两种不对称加密方式
      }
    
      //使用临时秘钥解密加密的策略每次请求都需要生成一个新的,所以动词是create不是init。
      public SymmetricCrypto createMessageBodyCryptoAlgorithm(String tempSecretKey) {
    
          AES aes = new AES("CBC", "PKCS5Padding",
                  // 密钥,可以自定义
                  tempSecretKey.getBytes(),
                  // iv加盐,按照实际需求添加
                  "1111222233334444".getBytes());
          return aes;
      }

    configTempSecretKeyCryptoAlgorithm:对应步骤2中加密临时秘钥的不对称加密策略,此处配置的策略用于解密临时秘钥。 createMessageBodyCryptoAlgorithm:对应步骤1中使用临时秘钥加密消息体的对称加解密策略,此处生成的策略用于使用临时秘钥解密消息体内容。

  5. 配置完成后,只需在controller中将服务端对应的加密接口由@Cryption注解标签修饰,并选择您所需要的加解密策略即可。(该注解包含四种策略:请求时的消息体全加密、返回时的消息体全加密、以上二种叠加、以及自定义的局部消息加密。)

      @PostMapping("cryptionTest")
      @Cryption(CryptionModle.WHOLE_REQUEST)//请求时加密(框架自动解密)
      public Map cryptionTest(@RequestBody User inputUser) {
          Map result = new HashMap<>();
          result.put("isEncrypted", true);
          result.put("msgFromClient", inputUser);
          return result;
      }
  6. 前三种加解密模式由框架自动完成,传入/传出消息时即为已按照注解配置解/加密后的内容,全程无需用户参与,只需在客户端做好相应的加密解密操作即可。

    自定义局部信息加解密也会自动解析并装配好临时秘钥的算法。不同之处在于使用时在消息体中通过调用KrestUtil.decryptMessageBody和KrestUtil.encryptMessageBody来加/解密您与客户端所约定的相应密文字段。

      @PostMapping("cryptionCustomize")
      @Cryption(CryptionModle.CUSTOMIZE)
      public Map cryptionCustomize(@RequestBody Map inputObj){
    
          String cryptionPart=(String)inputObj.get("cryptionPart");
          String decryptMessageBody=KrestUtil.decryptMessageBody(cryptionPart);
          //↑解密请求信息中被加密的部分
    
          Map result = new HashMap<>();
          result.put("cryptionPart", decryptMessageBody);
          result.put("nocryptionPart", inputObj.get("nocryptionPart"));
          result.put("whoyouare",KrestUtil.getJwtUser());
          result.put("token",KrestUtil.createNewJwtTokenIfNeeded());
          return result;
      }
  7. 本模块使用的加解密方式来自huTool中的加解密策略,如需扩展可自行学习相应规则。

  8. 本模块完整的示例代码参考krest-demo-1。模块中还包含了一个postman脚本,可将其导入postman客户端以供参考。

后续开发计划

  • 完善javadoc注解
  • 增加ip地址校验策略,防token盗用
  • 增加一个自动在返回参数的最外层增加token的机制,用application.yml控制开关
  • 其他欢迎补充

联系作者

欢迎试用并留下宝贵意见,帮助本产品进一步成熟和完善。如您在本产品的使用中有任何疑问或交流建议,请随时联系作者。

  • Email: ckw1988@163.com
  • QQ群: 818464800(推荐)
  • 在github或gitee的本项目的issue下留言也是可以的
Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright 2022 chenkaiwei(chenkaiwei.com) Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

简介

一款基于SpringBoot的轻量级RESTful框架套装,旨在整合no-session服务中常见的基础模块,并提供统一精简的配置、使用方式。 展开 收起
Java
Apache-2.0
取消

发行版 (3)

全部

贡献者

全部

近期动态

加载更多
不能加载更多了
马建仓 AI 助手
尝试更多
代码解读
代码找茬
代码优化
Java
1
https://gitee.com/ckw1988/krest.git
git@gitee.com:ckw1988/krest.git
ckw1988
krest
KRest
master

搜索帮助