• JEECG shiro验证实现分析


    jeecg-boot-base-core/src/main/java/org/jeecg/config/shiro/ShiroRealm.java

    1. package org.jeecg.config.shiro;
    2. import lombok.extern.slf4j.Slf4j;
    3. import org.apache.shiro.authc.AuthenticationException;
    4. import org.apache.shiro.authc.AuthenticationInfo;
    5. import org.apache.shiro.authc.AuthenticationToken;
    6. import org.apache.shiro.authc.SimpleAuthenticationInfo;
    7. import org.apache.shiro.authz.AuthorizationInfo;
    8. import org.apache.shiro.authz.SimpleAuthorizationInfo;
    9. import org.apache.shiro.realm.AuthorizingRealm;
    10. import org.apache.shiro.subject.PrincipalCollection;
    11. import org.jeecg.common.api.CommonAPI;
    12. import org.jeecg.common.config.TenantContext;
    13. import org.jeecg.common.constant.CacheConstant;
    14. import org.jeecg.common.constant.CommonConstant;
    15. import org.jeecg.common.system.util.JwtUtil;
    16. import org.jeecg.common.system.vo.LoginUser;
    17. import org.jeecg.common.util.RedisUtil;
    18. import org.jeecg.common.util.SpringContextUtils;
    19. import org.jeecg.common.util.TokenUtils;
    20. import org.jeecg.common.util.oConvertUtils;
    21. import org.springframework.context.annotation.Lazy;
    22. import org.springframework.stereotype.Component;
    23. import javax.annotation.Resource;
    24. import javax.servlet.http.HttpServletRequest;
    25. import java.util.Set;
    26. /**
    27. * @Description: 用户登录鉴权和获取用户授权
    28. * @Author: Scott
    29. * @Date: 2019-4-23 8:13
    30. * @Version: 1.1
    31. */
    32. @Component
    33. @Slf4j
    34. public class ShiroRealm extends AuthorizingRealm {
    35. @Lazy
    36. @Resource
    37. private CommonAPI commonApi;
    38. @Lazy
    39. @Resource
    40. private RedisUtil redisUtil;
    41. /**
    42. * 必须重写此方法,不然Shiro会报错
    43. */
    44. @Override
    45. public boolean supports(AuthenticationToken token) {
    46. return token instanceof JwtToken;
    47. }
    48. /**
    49. * 权限信息认证(包括角色以及权限)是用户访问controller的时候才进行验证(redis存储的此处权限信息)
    50. * 触发检测用户权限时才会调用此方法,例如checkRole,checkPermission
    51. *
    52. * @param principals 身份信息
    53. * @return AuthorizationInfo 权限信息
    54. */
    55. @Override
    56. protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
    57. log.debug("===============Shiro权限认证开始============ [ roles、permissions]==========");
    58. String username = null;
    59. if (principals != null) {
    60. LoginUser sysUser = (LoginUser) principals.getPrimaryPrincipal();
    61. username = sysUser.getUsername();
    62. }
    63. SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
    64. // 设置用户拥有的角色集合,比如“admin,test”
    65. Set roleSet = commonApi.queryUserRoles(username);
    66. //System.out.println(roleSet.toString());
    67. info.setRoles(roleSet);
    68. // 设置用户拥有的权限集合,比如“sys:role:add,sys:user:add”
    69. Set permissionSet = commonApi.queryUserAuths(username);
    70. info.addStringPermissions(permissionSet);
    71. //System.out.println(permissionSet);
    72. log.info("===============Shiro权限认证成功==============");
    73. return info;
    74. }
    75. /**
    76. * 用户信息认证是在用户进行登录的时候进行验证(不存redis)
    77. * 也就是说验证用户输入的账号和密码是否正确,错误抛出异常
    78. *
    79. * @param auth 用户登录的账号密码信息
    80. * @return 返回封装了用户信息的 AuthenticationInfo 实例
    81. * @throws AuthenticationException
    82. */
    83. @Override
    84. protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
    85. log.debug("===============Shiro身份认证开始============doGetAuthenticationInfo==========");
    86. String token = (String) auth.getCredentials();
    87. if (token == null) {
    88. HttpServletRequest req = SpringContextUtils.getHttpServletRequest();
    89. log.info("————————身份认证失败——————————IP地址: "+ oConvertUtils.getIpAddrByRequest(req) +",URL:"+req.getRequestURI());
    90. throw new AuthenticationException("token为空!");
    91. }
    92. // 校验token有效性
    93. LoginUser loginUser = null;
    94. try {
    95. loginUser = this.checkUserTokenIsEffect(token);
    96. } catch (AuthenticationException e) {
    97. JwtUtil.responseError(SpringContextUtils.getHttpServletResponse(),401,e.getMessage());
    98. e.printStackTrace();
    99. return null;
    100. }
    101. return new SimpleAuthenticationInfo(loginUser, token, getName());
    102. }
    103. /**
    104. * 校验token的有效性
    105. *
    106. * @param token
    107. */
    108. public LoginUser checkUserTokenIsEffect(String token) throws AuthenticationException {
    109. // 解密获得username,用于和数据库进行对比
    110. String username = JwtUtil.getUsername(token);
    111. if (username == null) {
    112. throw new AuthenticationException("token非法无效!");
    113. }
    114. // 查询用户信息
    115. log.debug("———校验token是否有效————checkUserTokenIsEffect——————— "+ token);
    116. LoginUser loginUser = TokenUtils.getLoginUser(username, commonApi, redisUtil);
    117. //LoginUser loginUser = commonApi.getUserByName(username);
    118. if (loginUser == null) {
    119. throw new AuthenticationException("用户不存在!");
    120. }
    121. // 判断用户状态
    122. if (loginUser.getStatus() != 1) {
    123. throw new AuthenticationException("账号已被锁定,请联系管理员!");
    124. }
    125. // 校验token是否超时失效 & 或者账号密码是否错误
    126. if (!jwtTokenRefresh(token, username, loginUser.getPassword())) {
    127. throw new AuthenticationException(CommonConstant.TOKEN_IS_INVALID_MSG);
    128. }
    129. //update-begin-author:taoyan date:20210609 for:校验用户的tenant_id和前端传过来的是否一致
    130. String userTenantIds = loginUser.getRelTenantIds();
    131. if(oConvertUtils.isNotEmpty(userTenantIds)){
    132. String contextTenantId = TenantContext.getTenant();
    133. log.debug("登录租户:" + contextTenantId);
    134. log.debug("用户拥有那些租户:" + userTenantIds);
    135. //登录用户无租户,前端header中租户ID值为 0
    136. String str ="0";
    137. if(oConvertUtils.isNotEmpty(contextTenantId) && !str.equals(contextTenantId)){
    138. //update-begin-author:taoyan date:20211227 for: /issues/I4O14W 用户租户信息变更判断漏洞
    139. String[] arr = userTenantIds.split(",");
    140. if(!oConvertUtils.isIn(contextTenantId, arr)){
    141. boolean isAuthorization = false;
    142. //========================================================================
    143. // 查询用户信息(如果租户不匹配从数据库中重新查询一次用户信息)
    144. String loginUserKey = CacheConstant.SYS_USERS_CACHE + "::" + username;
    145. redisUtil.del(loginUserKey);
    146. LoginUser loginUserFromDb = commonApi.getUserByName(username);
    147. if (oConvertUtils.isNotEmpty(loginUserFromDb.getRelTenantIds())) {
    148. String[] newArray = loginUserFromDb.getRelTenantIds().split(",");
    149. if (oConvertUtils.isIn(contextTenantId, newArray)) {
    150. isAuthorization = true;
    151. }
    152. }
    153. //========================================================================
    154. //*********************************************
    155. if(!isAuthorization){
    156. log.info("租户异常——登录租户:" + contextTenantId);
    157. log.info("租户异常——用户拥有租户组:" + userTenantIds);
    158. throw new AuthenticationException("登录租户授权变更,请重新登陆!");
    159. }
    160. //*********************************************
    161. }
    162. //update-end-author:taoyan date:20211227 for: /issues/I4O14W 用户租户信息变更判断漏洞
    163. }
    164. }
    165. //update-end-author:taoyan date:20210609 for:校验用户的tenant_id和前端传过来的是否一致
    166. return loginUser;
    167. }
    168. /**
    169. * JWTToken刷新生命周期 (实现: 用户在线操作不掉线功能)
    170. * 1、登录成功后将用户的JWT生成的Token作为k、v存储到cache缓存里面(这时候k、v值一样),缓存有效期设置为Jwt有效时间的2倍
    171. * 2、当该用户再次请求时,通过JWTFilter层层校验之后会进入到doGetAuthenticationInfo进行身份验证
    172. * 3、当该用户这次请求jwt生成的token值已经超时,但该token对应cache中的k还是存在,则表示该用户一直在操作只是JWT的token失效了,程序会给token对应的k映射的v值重新生成JWTToken并覆盖v值,该缓存生命周期重新计算
    173. * 4、当该用户这次请求jwt在生成的token值已经超时,并在cache中不存在对应的k,则表示该用户账户空闲超时,返回用户信息已失效,请重新登录。
    174. * 注意: 前端请求Header中设置Authorization保持不变,校验有效性以缓存中的token为准。
    175. * 用户过期时间 = Jwt有效时间 * 2。
    176. *
    177. * @param userName
    178. * @param passWord
    179. * @return
    180. */
    181. public boolean jwtTokenRefresh(String token, String userName, String passWord) {
    182. String cacheToken = String.valueOf(redisUtil.get(CommonConstant.PREFIX_USER_TOKEN + token));
    183. if (oConvertUtils.isNotEmpty(cacheToken)) {
    184. // 校验token有效性
    185. if (!JwtUtil.verify(cacheToken, userName, passWord)) {
    186. String newAuthorization = JwtUtil.sign(userName, passWord);
    187. // 设置超时时间
    188. redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, newAuthorization);
    189. redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, JwtUtil.EXPIRE_TIME *2 / 1000);
    190. log.debug("——————————用户在线操作,更新token保证不掉线—————————jwtTokenRefresh——————— "+ token);
    191. }
    192. //update-begin--Author:scott Date:20191005 for:解决每次请求,都重写redis中 token缓存问题
    193. // else {
    194. // // 设置超时时间
    195. // redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, cacheToken);
    196. // redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, JwtUtil.EXPIRE_TIME / 1000);
    197. // }
    198. //update-end--Author:scott Date:20191005 for:解决每次请求,都重写redis中 token缓存问题
    199. return true;
    200. }
    201. //redis中不存在此TOEKN,说明token非法返回false
    202. return false;
    203. }
    204. /**
    205. * 清除当前用户的权限认证缓存
    206. *
    207. * @param principals 权限信息
    208. */
    209. @Override
    210. public void clearCache(PrincipalCollection principals) {
    211. super.clearCache(principals);
    212. }
    213. }
    ShiroRealm是用户认证时调用的关键判断逻辑,这部分代码不是shiro库中的公共代码,而是项目开发者自己实现的。

    可以通过debug的代码栈窗口,查看调用流程。

    把debug断点设在org.jeecg.modules.system.service.impl.SysUserServiceImpl#getEncodeUserInfo方法内,运行到断点处停止,可以看到代码栈窗口显示了以往调用的方法,从下至上排列,最先调用的方法在最下方,最后调用的方法在最上方,其中通过接口调用的方法(比如org.jeecg.modules.system.service.ISysUserService#getEncodeUserInfo),还夹着invoke方法,盲猜是在执行注释,执行invoke后redis中出现了相应的缓存数据。

    @Cacheable(cacheNames=CacheConstant.SYS_USERS_CACHE, key="#username")
    

    流程顺序为:

    1、从org.jeecg.config.shiro.ShiroRealm#doGetAuthenticationInfo调用

    this.checkUserTokenIsEffect(token)

    2、从org.jeecg.config.shiro.ShiroRealm#checkUserTokenIsEffect调用

    TokenUtils.getLoginUser(username, commonApi, redisUtil)

    3、从org.jeecg.common.util.TokenUtils#getLoginUser调用

    commonApi.getUserByName(username)

    4、从org.jeecg.modules.system.service.impl.SysBaseApiImpl#getUserByName调用

    sysUserService.getEncodeUserInfo(username)

    5、从org.jeecg.modules.system.service.impl.SysUserServiceImpl#getEncodeUserInfo调用

    userMapper.getUserByName(username)

    一、用户登录时,经过一系列前期流程,shiro最终调用开发者自定义的ShiroRealm的org.jeecg.config.shiro.ShiroRealm#doGetAuthenticationInfo方法。

    该方法的执行流程是:

    1、调用同一文件下的checkUserTokenIsEffect()具体进行用户验证。

    loginUser = this.checkUserTokenIsEffect(token);
    

    2、验证成功时返回SimpleAuthenticationInfo对象,失败则抛异常或返回null。

    二、org.jeecg.config.shiro.ShiroRealm#checkUserTokenIsEffect内部流程:

    1、先通过token解密出用户名:
    String username = JwtUtil.getUsername(token);

    2、再通过用户名从redis或mysql获取用户信息(优先查redis,redis速度快):

    LoginUser loginUser = TokenUtils.getLoginUser(username, commonApi, redisUtil);

    3、调用jwtTokenRefresh(token, username, loginUser.getPassword())校验用户

    4、最后校验用户的tenant_id和前端传过来的是否一致,关于这部分的作用,可能属于saas功能,有待进一步学习。

    三、org.jeecg.common.util.TokenUtils#getLoginUser涉及redis缓存的使用,第一次通过token(解密出来的用户名)查询用户信息时,redis未缓存该用户信息,需要去数据库中查询,查询后返回的结果直接缓存到redis中

    1. public static LoginUser getLoginUser(String username, CommonAPI commonApi, RedisUtil redisUtil) {
    2. LoginUser loginUser = null;
    3. String loginUserKey = CacheConstant.SYS_USERS_CACHE + "::" + username;
    4. //【重要】此处通过redis原生获取缓存用户,是为了解决微服务下system服务挂了,其他服务互调不通问题---
    5. if (redisUtil.hasKey(loginUserKey)) {
    6. try {
    7. loginUser = (LoginUser) redisUtil.get(loginUserKey);
    8. //解密用户
    9. SensitiveInfoUtil.handlerObject(loginUser, false);
    10. } catch (IllegalAccessException e) {
    11. e.printStackTrace();
    12. }
    13. } else {
    14. // 查询用户信息
    15. loginUser = commonApi.getUserByName(username);
    16. }
    17. return loginUser;
    18. }

    在执行commonApi.getUserByName(username);(org.jeecg.modules.system.service.impl.SysBaseApiImpl#getUserByName)之前,redis中不存在key为sys:cache:encrypt:user::admin的键值对,执行后出现了,继续深入getUserByName内:

    1. @Override
    2. //@SensitiveDecode
    3. public LoginUser getUserByName(String username) {
    4. //update-begin-author:taoyan date:2022-6-6 for: VUEN-1276 【v3流程图】测试bug 1、通过我发起的流程或者流程实例,查看历史,流程图预览问题
    5. if (oConvertUtils.isEmpty(username)) {
    6. return null;
    7. }
    8. //update-end-author:taoyan date:2022-6-6 for: VUEN-1276 【v3流程图】测试bug 1、通过我发起的流程或者流程实例,查看历史,流程图预览问题
    9. LoginUser user = sysUserService.getEncodeUserInfo(username);
    10. //相同类中方法间调用时脱敏解密 Aop会失效,获取用户信息太重要,此处采用原生解密方法,不采用@SensitiveDecodeAble注解方式
    11. try {
    12. SensitiveInfoUtil.handlerObject(user, false);
    13. } catch (IllegalAccessException e) {
    14. e.printStackTrace();
    15. }
    16. return user;
    17. }

    getUserByName()的主要功能代码是调用

    sysUserService.getEncodeUserInfo(username);

    (org.jeecg.modules.system.service.impl.SysUserServiceImpl#getEncodeUserInfo)

    1. @Override
    2. @Cacheable(cacheNames=CacheConstant.SYS_USERS_CACHE, key="#username")
    3. @SensitiveEncode
    4. public LoginUser getEncodeUserInfo(String username){
    5. if(oConvertUtils.isEmpty(username)) {
    6. return null;
    7. }
    8. LoginUser loginUser = new LoginUser();
    9. SysUser sysUser = userMapper.getUserByName(username);
    10. //查询用户的租户ids
    11. this.setUserTenantIds(sysUser);
    12. if(sysUser==null) {
    13. return null;
    14. }
    15. BeanUtils.copyProperties(sysUser, loginUser);
    16. return loginUser;
    17. }

      在getEncodeUserInfo()方法上有一个注释:

    @Cacheable(cacheNames=CacheConstant.SYS_USERS_CACHE, key="#username")

    正式创建缓存的关键代码,@Cacheable 注解在方法上,表示该方法的返回结果是可以缓存的。也就是说,该方法的返回结果会放在缓存中,以便于以后使用相同的参数调用该方法时,会返回缓存中的值,而不会实际执行该方法。
    在没有看到这个代码前,我是想不到@Cacheable的作用的,但是我知道redis中这一类键值对都是以CacheConstant.SYS_USERS_CACHE("sys:cache:encrypt:user")为key的开头的,通过ctrl+鼠标左键一键定位到SYS_USERS_CACHE定义处,能看到它有24个用法:

    点击“24个用法”挨个查看:

     就能发现一些规律,@CacheEvict注释是删除相应的redis缓存数据,打开前五个代码中的用法,发现也是在删除redis缓存数据。一一排除删除的用法,就剩下了@Cacheable用法,这时我小心假设它是创建缓存的方法,并通过百度印证了猜想。

  • 相关阅读:
    【Purple Pi OH RK3566鸿蒙开发板】OpenHarmony音频播放应用,真实体验感爆棚!
    【mid】sdp解析:奇怪的vector 引用
    九、iOS原生应用(宿主App)与uni小程序间的通讯
    C语言 牛客网习题 10.20 day2
    Android security知识点总结
    基于Java的磁盘调度模拟系统
    PyTorch实战-实现神经网络图像分类基础Tensor最全操作详解(一)
    vie的刷新机制
    C/C++ 11/14/17 有栈式协同程式的基础框架类库【关于】
    cdk&java | 实现化学反应中的片段分组
  • 原文地址:https://blog.csdn.net/qq_27361945/article/details/133803465