# KRest
**Repository Path**: ckw1988/krest
## Basic Information
- **Project Name**: KRest
- **Description**: 一款基于SpringBoot的轻量级RESTful框架套装,旨在整合no-session服务中常见的基础模块,并提供统一精简的配置、使用方式。
- **Primary Language**: Java
- **License**: Apache-2.0
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 9
- **Forks**: 2
- **Created**: 2022-01-26
- **Last Updated**: 2022-12-02
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# 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](https://gitee.com/ckw1988/shiro-jwt-integration) 我把KRest中所运用的jwt+shiro整合部分的知识单独提取出来做了这份教程和示例。
## 版本要求
* JDK1.8以上
## 快速开始
1. 在pom中导入包(本项目已发布到maven中央库,直接写配置即可。)
```xml
com.chenkaiwei.krest
krest-core
${最新版本号}
```
注:您可以在本项目的发布(release)页找到最新的版本号,也可在maven中央库中查看(https://search.maven.org/search?q=a:krest-core )。
2. 新建一个config类(或在您原有的config类上)实现KrestConfigurer接口
```java
@Configuration
public class DemoConfig implements KrestConfigurer {
//1.按下图方式配置角色-权限映射,返回值中key为角色(Role)名称,value为该角色所拥有的所有权限(Permission)。
//此处的硬编码仅作示例,您可以将这部分数据以任何您喜欢的方式存储(数据库、钥匙串、文本文件等,均可),只需在本方法中以同步方法取出并确保其按标准格式返回即可。
@Override
public Map> configRolePermissionsMap() {
Map> res=new HashMap>();
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 filterRuleMap) {
//配置不参与token验证的uri
filterRuleMap.put("/static/*", "anon");
filterRuleMap.put("/error", "anon");
filterRuleMap.put("/register", "anon");
filterRuleMap.put("/login", "anon");
}
}
```
3. (可选)实现登录方法(即登录成功后发放初始新token)。如果您在本服务中仅须实现已有token的验证和更新发放功能(比如已从别的服务完成登录获取token),则可跳过本步骤。
在controller中简单实现一个登陆方法,示例如下:
```java
@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的自动刷新,下一步骤中配置的过期时间和刷新机制也会同时生效。
```java
resBody.put("token",KrestUtil.createNewJwtTokenIfNeeded());
```
一般建议使用ResponseBodyAdvice等方式统一封装在返回结果中。如此设计是为了避免对用户的返回数据结构进行过多约束。
6. (可选)自定义token过期时间和自动刷新时间。
在application.yml文件中增加如下配置:
```yaml
krest:
jwt:
expire-time: 1m
#↑token过期时间,默认20分钟
refresh-time-before-expire: 40s
#↑过期前多久更新token。默认10分钟。若设为与expire-time一致,则每次请求都会刷新。
```
至此,配置完成。
7. 运行测试
在controller中加入如下代码
```java
@GetMapping("/permissionDemo")
@RequiresPermissions("p1")//表示当用户拥有"p1"权限时才被许可访问该方法。role同理,这部分使用来自shiro语法。
public Map permissionDemo(){
Map res=new HashMap<>();
res.put("result","you have got the permission [permissionDemo]");
res.put("token",KrestUtil.createNewJwtTokenIfNeeded());
return res;
}
```
## 进阶使用
#### 启用框架自带的用户名-密码登录功能
本框架包含一套完善的用户名密码登陆机制,通过实现shiro的原生组件实现,可在配置文件中一键开启。配置如下。
```yaml
krest:
enable-username-password-realm: true
```
您仅需在config文件中配置一些最必要的设置即可激活该功能。其具体规则参考krest-demo-1源码。
#### 自定义异常返回
1. 继承KrestErrorController并覆盖getErrorResponseBody方法来自定义返回错误时的数据结构。
2. 通过定义全局ExceptionHandler来捕获异常。具体规则参考demo中的GlobalExceptionController文件。
```java
@Slf4j
@RestControllerAdvice
public class GlobalExceptionController {
//按你自己的方式来统一返回格式,此处仅做示例,为了好懂就不抽象了。
@ExceptionHandler({KrestAuthenticationException.class,AuthenticationException.class})
public ResponseEntity KrestAuthenticationExceptionHandler(KrestAuthenticationException e) {
log.error("krestExceptionHandler");
log.error(e.getLocalizedMessage());
Map body=new HashMap();
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 body=new HashMap();
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 body=new HashMap();
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. 以下是服务端部分:需要先在配置文件中开启接口加密功能:
```yml
krest.cryption.enable-cryption = true
```
4. 在Krest配置文件中实现:
```java
//解密临时秘钥的策略是一次配置一直使用
@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注解标签修饰,并选择您所需要的加解密策略即可。(该注解包含四种策略:请求时的消息体全加密、返回时的消息体全加密、以上二种叠加、以及自定义的局部消息加密。)
```java
@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来加/解密您与客户端所约定的相应密文字段。
```java
@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下留言也是可以的