查看原文
其他

基于Jwt资源无状态认证权限管理系统bootshiro

吕一明 MarkerHub 2022-11-21

小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、代码结构

  1. └─com

  2.     └─usthe

  3.         └─bootshiro

  4.             │  BootshiroApplication.java

  5.             │

  6.             ├─config

  7.             │  ├─database #druid数据源配置

  8.             │  │      DataSourceConfiguration.java

  9.             │  │      DruidConfiguration.java

  10.             │  │

  11.             │  ├─restful #swagger2配置

  12.             │  │      SwaggerConfiguration.java

  13.             │  │

  14.             │  └─security #跨域配置

  15.             │          SecurityCorsConfiguration.java

  16.             │

  17.             ├─controller #接口控制器

  18.             │      AccountController.java #登录注册

  19.             │      BaseAction.java

  20.             │      LogController.java

  21.             │      ResourceController.java

  22.             │      RoleController.java

  23.             │      UserController.java

  24.             │

  25.             ├─dao

  26.             │      AuthAccountLogMapper.java

  27.             │      ...

  28.             │

  29.             ├─domain

  30.             │  ├─bo

  31.             │  │      AuthAccountLog.java

  32.             │  │      ...

  33.             │  │

  34.             │  └─vo #一些vo

  35.             │          Account.java

  36.             │          ...

  37.             │

  38.             ├─plugin #mybatis代码生成相关

  39.             │      MybatisGenerator.java

  40.             │      OverIsMergeablePlugin.java

  41.             │

  42.             ├─service

  43.             │  │  AccountLogService.java

  44.             │  │  ...

  45.             │  │

  46.             │  └─impl

  47.             │          AccountLogServiceImpl.java

  48.             │          ...

  49.             │

  50.             ├─shiro #shiro相关

  51.             │  ├─config

  52.             │  │      RestPathMatchingFilterChainResolver.java

  53.             │  │      RestShiroFilterFactoryBean.java

  54.             │  │      ShiroConfiguration.java

  55.             │  │

  56.             │  ├─filter

  57.             │  │      AbstractPathMatchingFilter.java

  58.             │  │      BonJwtFilter.java #基于jwt的shiro自动登录认证

  59.             │  │      PasswordFilter.java的 #基于账户密码的shiro自动登录认证

  60.             │  │      ShiroFilterChainManager.java

  61.             │  │      StatelessWebSubjectFactory.java

  62.             │  │

  63.             │  ├─matcher

  64.             │  │      JwtMatcher.java

  65.             │  │      PasswordMatcher.java

  66.             │  │

  67.             │  ├─provider

  68.             │  │  │  AccountProvider.java

  69.             │  │  │  ShiroFilterRulesProvider.java

  70.             │  │  │

  71.             │  │  └─impl

  72.             │  │          AccountProviderImpl.java

  73.             │  │          ShiroFilterRulesProviderImpl.java

  74.             │  │

  75.             │  ├─realm #多个realm

  76.             │  │      AonModularRealmAuthenticator.java

  77.             │  │      JwtRealm.java

  78.             │  │      PasswordRealm.java

  79.             │  │      RealmManager.java

  80.             │  │

  81.             │  ├─rule

  82.             │  │      RolePermRule.java

  83.             │  │

  84.             │  └─token

  85.             │          JwtToken.java

  86.             │          PasswordToken.java

  87.             │

  88.             ├─support

  89.             │  │  GlobalExceptionHandler.java

  90.             │  │  SpringContextHolder.java

  91.             │  │  XssSqlFilter.java #预防xss攻击

  92.             │  │  XssSqlHttpServletRequestWrapper.java

  93.             │  │  XssSqlStringJsonSerializer.java

  94.             │  │

  95.             │  ├─factory

  96.             │  │      LogFactory.java

  97.             │  │      LogTaskFactory.java

  98.             │  │

  99.             │  └─manager

  100.             │          LogExeManager.java

  101.             │

  102.             └─util

  103.                     AesUtil.java

  104.                     ...

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,我们看下代码:

  1. @Override

  2. protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {


  3. // 判断若为获取登录注册加密动态秘钥请求

  4. if (isPasswordTokenGet(request)) {

  5. //动态生成秘钥,redis存储秘钥供之后秘钥验证使用,设置有效期5秒用完即丢弃

  6. String tokenKey = CommonUtil.getRandomString(16);

  7. String userKey = CommonUtil.getRandomString(6);

  8. try {

  9. redisTemplate.opsForValue().set("TOKEN_KEY_"+ IpUtil.getIpFromRequest(WebUtils.toHttp(request)).toUpperCase()+userKey.toUpperCase(),tokenKey,5, TimeUnit.SECONDS);

  10. // 动态秘钥response返回给前端

  11. Message message = new Message();

  12. message.ok(1000,"issued tokenKey success")

  13. .addData("tokenKey",tokenKey).addData("userKey", userKey.toUpperCase());

  14. RequestResponseUtil.responseWrite(JSON.toJSONString(message),response);


  15. }catch (Exception e) {

  16. LOGGER.warn("签发动态秘钥失败"+e.getMessage(),e);

  17. Message message = new Message();

  18. message.ok(1000,"issued tokenKey fail");

  19. RequestResponseUtil.responseWrite(JSON.toJSONString(message),response);

  20. }

  21. return false;

  22. }


  23. // 判断是否是登录请求

  24. if(isPasswordLoginPost(request)){

  25. ...

  26. }

  27. ...

  28. }

而我们看下isPasswordTokenGet(request)方法就知道,其实就满足我们的条件:

  1. private boolean isPasswordTokenGet(ServletRequest request) {


  2. String tokenKey = RequestResponseUtil.getParameter(request,"tokenKey");


  3. return (request instanceof HttpServletRequest)

  4. && "GET".equals(((HttpServletRequest) request).getMethod().toUpperCase())

  5. && "get".equals(tokenKey);

  6. }

所以当我们发起http://localhost:8080/account/register?tokenKey=get请求的时候,就会进入到这个过滤器的这个条件中,就获取到了key和userKey,是随机生成的:

  1. String tokenKey = CommonUtil.getRandomString(16);

  2. String userKey = CommonUtil.getRandomString(6);

  3. 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方法,其中最关键的代码如下:

  1. // 从Redis取出密码传输加密解密秘钥

  2. String tokenKey = redisTemplate.opsForValue().get("TOKEN_KEY_" + IpUtil.getIpFromRequest(WebUtils.toHttp(request)).toUpperCase()+userKey);

  3. String realPassword = AesUtil.aesDecode(password, tokenKey);

  4. String salt = CommonUtil.getRandomString(6);

  5. // 存储到数据库的密码为 MD5(原密码+盐值)

  6. authUser.setPassword(Md5Util.md5(realPassword + salt));

  7. authUser.setSalt(salt);

  8. 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 死信机制真的可以作为延时任务这个场景的解决方案吗?

SpringBoot 全局日期格式化(基于注解)

理解这9大内置过滤器,才算是精通Shiro

eblog项目讲解视频上线啦,长达17个小时!!



好文章!我在看!

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存