基于Jwt资源无状态认证权限管理系统bootshiro
小Hub领读:
在你们还在睡觉的时候,小Hub已经又偷偷学习完了一个开源项目bootshiro,我要把我学习到的知识分享给你们。
这个项目我觉得有一个重点,可能很少在其他开源项目上看到,那就是:数据传输动态秘钥加密。尤其是在登录注册过程中,如何给表单的密码动态加密。你们会吗?我们来一起学习学习吧!
别忘了文末给小Hub一个赞噢!
1、前言
基于springboot2+ shiro+jwt的真正rest api资源无状态认证权限管理框架,开发人员无需关注权限问题,后端开发完api,前端页面配置即可。
小Hub提醒:记住哇,这个项目不要太纠结在jwt或者shiro的配置上,关注你学习的重点,注册登录过程中的表单密码动态加密!所以,当你clone项目下来之后,如果分不清学习重点,那就是浪费时间啦!
官方文档也是挺多的,可以看看!但希望你们学习,学会积累和记录。
2、项目信息
作者:tomsun28
git链接:https://gitee.com/tomsun28/bootshiro
官方文档
前后端分离实践:https://segmentfault.com/blog/tomsun28
api权限管理系统与前后端分离实践:https://segmentfault.com/a/1190000014368885
基于shiro的改造集成真正支持restful请求:https://segmentfault.com/a/1190000014545172
签发的用户认证token超时刷新策略:https://segmentfault.com/a/1190000014545422
传输密码动态加密解密:https://segmentfault.com/a/1190000014544933
3、项目截图
首页截图
后台截图
4、代码结构
└─com
└─usthe
└─bootshiro
│ BootshiroApplication.java
│
├─config
│ ├─database #druid数据源配置
│ │ DataSourceConfiguration.java
│ │ DruidConfiguration.java
│ │
│ ├─restful #swagger2配置
│ │ SwaggerConfiguration.java
│ │
│ └─security #跨域配置
│ SecurityCorsConfiguration.java
│
├─controller #接口控制器
│ AccountController.java #登录注册
│ BaseAction.java
│ LogController.java
│ ResourceController.java
│ RoleController.java
│ UserController.java
│
├─dao
│ AuthAccountLogMapper.java
│ ...
│
├─domain
│ ├─bo
│ │ AuthAccountLog.java
│ │ ...
│ │
│ └─vo #一些vo
│ Account.java
│ ...
│
├─plugin #mybatis代码生成相关
│ MybatisGenerator.java
│ OverIsMergeablePlugin.java
│
├─service
│ │ AccountLogService.java
│ │ ...
│ │
│ └─impl
│ AccountLogServiceImpl.java
│ ...
│
├─shiro #shiro相关
│ ├─config
│ │ RestPathMatchingFilterChainResolver.java
│ │ RestShiroFilterFactoryBean.java
│ │ ShiroConfiguration.java
│ │
│ ├─filter
│ │ AbstractPathMatchingFilter.java
│ │ BonJwtFilter.java #基于jwt的shiro自动登录认证
│ │ PasswordFilter.java的 #基于账户密码的shiro自动登录认证
│ │ ShiroFilterChainManager.java
│ │ StatelessWebSubjectFactory.java
│ │
│ ├─matcher
│ │ JwtMatcher.java
│ │ PasswordMatcher.java
│ │
│ ├─provider
│ │ │ AccountProvider.java
│ │ │ ShiroFilterRulesProvider.java
│ │ │
│ │ └─impl
│ │ AccountProviderImpl.java
│ │ ShiroFilterRulesProviderImpl.java
│ │
│ ├─realm #多个realm
│ │ AonModularRealmAuthenticator.java
│ │ JwtRealm.java
│ │ PasswordRealm.java
│ │ RealmManager.java
│ │
│ ├─rule
│ │ RolePermRule.java
│ │
│ └─token
│ JwtToken.java
│ PasswordToken.java
│
├─support
│ │ GlobalExceptionHandler.java
│ │ SpringContextHolder.java
│ │ XssSqlFilter.java #预防xss攻击
│ │ XssSqlHttpServletRequestWrapper.java
│ │ XssSqlStringJsonSerializer.java
│ │
│ ├─factory
│ │ LogFactory.java
│ │ LogTaskFactory.java
│ │
│ └─manager
│ LogExeManager.java
│
└─util
AesUtil.java
...
5、基本介绍
技术栈
前端
usthe、angular5
后端
springboot、shiro、jwt、druid、swagger2、mybatis、mybatis-generator、pagehelper、redis
功能大纲
用户管理、资源管理、菜单管理、API管理、角色管理
6、项目启动步骤
fork 项目到自己的仓库(欢迎star^.^)
clone 项目到本地
用idea导入
更改开发环境mysql数据库和redis地址(前提安装数据库并导入usthe.sql创建数据库usthe)
运行BootshiroApplication
bootshiro就可以提供api了 http://localhost:8080
推荐使用postman进行api调试
7、学习重点(学习目的)
restful接口设计
数据传输动态秘钥加密
jwt过期自动刷新
预防Xss攻击
8、模块分析
这个项目,我们可以主要学习一下怎么给表单的密码动态加密的,所以,我们先来研究一下注册和登录功能。
分享一套SpringBoot开发博客系统源码,以及完整开发文档!速度保存!
动态密钥加密
注册功能
在项目的根目录下,有个postmantestexample.json,这是一个postman的导出文件,我们把这个文件重新导入到postman中,然后进行联调。
分别对应着登录,调用认证,注册3个接口。
我们先来看下注册功能的测试。
因为是个post请求,参数是json数据,所以放在body中,其中password和userKey是个参数来的,那么这两个参数哪里来的呢?我们看到Pre-request Script脚本中。
这个脚本的大概意思是访问http://localhost:8080/account/register?tokenKey=get 链接,获取key和userKey,然后key经过AES算法加密之后得到了参数password,所以我们刚才说注册接口中的body的两个参数就是这里注入进去的。
passwork明显经过了一层加密,这样传输的过程中,即使表单的数据被别人截取到了,也不能获得密码,只有经过后端的AES解密之后,才能获取到密码。
那么有两个问题在这里
获取key和userKey的方法在哪?
后端如何解密的?
我们先来看第一个问题:
我们在过滤器中找到了PasswordFilter,是一个基于用户名密码的过滤器,继承AccessControlFilter,我们看下代码:
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
// 判断若为获取登录注册加密动态秘钥请求
if (isPasswordTokenGet(request)) {
//动态生成秘钥,redis存储秘钥供之后秘钥验证使用,设置有效期5秒用完即丢弃
String tokenKey = CommonUtil.getRandomString(16);
String userKey = CommonUtil.getRandomString(6);
try {
redisTemplate.opsForValue().set("TOKEN_KEY_"+ IpUtil.getIpFromRequest(WebUtils.toHttp(request)).toUpperCase()+userKey.toUpperCase(),tokenKey,5, TimeUnit.SECONDS);
// 动态秘钥response返回给前端
Message message = new Message();
message.ok(1000,"issued tokenKey success")
.addData("tokenKey",tokenKey).addData("userKey", userKey.toUpperCase());
RequestResponseUtil.responseWrite(JSON.toJSONString(message),response);
}catch (Exception e) {
LOGGER.warn("签发动态秘钥失败"+e.getMessage(),e);
Message message = new Message();
message.ok(1000,"issued tokenKey fail");
RequestResponseUtil.responseWrite(JSON.toJSONString(message),response);
}
return false;
}
// 判断是否是登录请求
if(isPasswordLoginPost(request)){
...
}
...
}
而我们看下isPasswordTokenGet(request)方法就知道,其实就满足我们的条件:
private boolean isPasswordTokenGet(ServletRequest request) {
String tokenKey = RequestResponseUtil.getParameter(request,"tokenKey");
return (request instanceof HttpServletRequest)
&& "GET".equals(((HttpServletRequest) request).getMethod().toUpperCase())
&& "get".equals(tokenKey);
}
所以当我们发起http://localhost:8080/account/register?tokenKey=get请求的时候,就会进入到这个过滤器的这个条件中,就获取到了key和userKey,是随机生成的:
String tokenKey = CommonUtil.getRandomString(16);
String userKey = CommonUtil.getRandomString(6);
redisTemplate.opsForValue().set("TOKEN_KEY_"+ IpUtil.getIpFromRequest(WebUtils.toHttp(request)).toUpperCase()+userKey.toUpperCase(),tokenKey,5, TimeUnit.SECONDS);
存到了redis中,有效期为5秒。所以这里动态生成了密钥,并redis存储秘钥供之后秘钥验证使用,设置有效期5秒用完即丢弃。好了,我们已经弄清楚了第一个问题,那么来看看第二个问题。
我们找到com.usthe.bootshiro.controller.AccountController#accountRegister方法,其中最关键的代码如下:
// 从Redis取出密码传输加密解密秘钥
String tokenKey = redisTemplate.opsForValue().get("TOKEN_KEY_" + IpUtil.getIpFromRequest(WebUtils.toHttp(request)).toUpperCase()+userKey);
String realPassword = AesUtil.aesDecode(password, tokenKey);
String salt = CommonUtil.getRandomString(6);
// 存储到数据库的密码为 MD5(原密码+盐值)
authUser.setPassword(Md5Util.md5(realPassword + salt));
authUser.setSalt(salt);
authUser.setCreateTime(new Date());
可以看出,tokenKey就是加密解密的重点key,所以AesUtil.aesDecode解密之后得到正在的密码,然后加盐保存到数据库中即可。
总结一下上面我们的请求过程:
在注册之前,我们先通过过滤器获取到了动态密钥,然后前端提交form注册表单之后先通过js给password进行AES加密,然后发送内容到达后台,后台在redis中获取动态密钥,然后进行解密获取到真实的密码,再进行注册。
完美!
登录功能
同注册功能。
关于这个项目其他的内容大部分都是与shiro相关的,这里我就不再多做分析啦。感兴趣的同学可以再去细看哈。
9、总结
好啦,我是吕一明,感谢来到MarkerHub看我的文章,分享知识,让Java不再难懂,我们明天见哈!
(完)
MarkerHub文章索引:(点击阅读原文直达)
https://github.com/MarkerHub/JavaIndex
【推荐阅读】
别用Date了,Java8新特性之日期处理,现在学会也不迟!
RabbitMQ 死信机制真的可以作为延时任务这个场景的解决方案吗?