jeecg-boot-base-core/src/main/java/org/jeecg/config/shiro/ShiroRealm.java
- package org.jeecg.config.shiro;
-
- import lombok.extern.slf4j.Slf4j;
- import org.apache.shiro.authc.AuthenticationException;
- import org.apache.shiro.authc.AuthenticationInfo;
- import org.apache.shiro.authc.AuthenticationToken;
- import org.apache.shiro.authc.SimpleAuthenticationInfo;
- import org.apache.shiro.authz.AuthorizationInfo;
- import org.apache.shiro.authz.SimpleAuthorizationInfo;
- import org.apache.shiro.realm.AuthorizingRealm;
- import org.apache.shiro.subject.PrincipalCollection;
- import org.jeecg.common.api.CommonAPI;
- import org.jeecg.common.config.TenantContext;
- import org.jeecg.common.constant.CacheConstant;
- import org.jeecg.common.constant.CommonConstant;
- import org.jeecg.common.system.util.JwtUtil;
- import org.jeecg.common.system.vo.LoginUser;
- import org.jeecg.common.util.RedisUtil;
- import org.jeecg.common.util.SpringContextUtils;
- import org.jeecg.common.util.TokenUtils;
- import org.jeecg.common.util.oConvertUtils;
- import org.springframework.context.annotation.Lazy;
- import org.springframework.stereotype.Component;
-
- import javax.annotation.Resource;
- import javax.servlet.http.HttpServletRequest;
- import java.util.Set;
-
- /**
- * @Description: 用户登录鉴权和获取用户授权
- * @Author: Scott
- * @Date: 2019-4-23 8:13
- * @Version: 1.1
- */
- @Component
- @Slf4j
- public class ShiroRealm extends AuthorizingRealm {
- @Lazy
- @Resource
- private CommonAPI commonApi;
-
- @Lazy
- @Resource
- private RedisUtil redisUtil;
-
- /**
- * 必须重写此方法,不然Shiro会报错
- */
- @Override
- public boolean supports(AuthenticationToken token) {
- return token instanceof JwtToken;
- }
-
- /**
- * 权限信息认证(包括角色以及权限)是用户访问controller的时候才进行验证(redis存储的此处权限信息)
- * 触发检测用户权限时才会调用此方法,例如checkRole,checkPermission
- *
- * @param principals 身份信息
- * @return AuthorizationInfo 权限信息
- */
- @Override
- protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
- log.debug("===============Shiro权限认证开始============ [ roles、permissions]==========");
- String username = null;
- if (principals != null) {
- LoginUser sysUser = (LoginUser) principals.getPrimaryPrincipal();
- username = sysUser.getUsername();
- }
- SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
-
- // 设置用户拥有的角色集合,比如“admin,test”
- Set
roleSet = commonApi.queryUserRoles(username); - //System.out.println(roleSet.toString());
- info.setRoles(roleSet);
-
- // 设置用户拥有的权限集合,比如“sys:role:add,sys:user:add”
- Set
permissionSet = commonApi.queryUserAuths(username); - info.addStringPermissions(permissionSet);
- //System.out.println(permissionSet);
- log.info("===============Shiro权限认证成功==============");
- return info;
- }
-
- /**
- * 用户信息认证是在用户进行登录的时候进行验证(不存redis)
- * 也就是说验证用户输入的账号和密码是否正确,错误抛出异常
- *
- * @param auth 用户登录的账号密码信息
- * @return 返回封装了用户信息的 AuthenticationInfo 实例
- * @throws AuthenticationException
- */
- @Override
- protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
- log.debug("===============Shiro身份认证开始============doGetAuthenticationInfo==========");
- String token = (String) auth.getCredentials();
- if (token == null) {
- HttpServletRequest req = SpringContextUtils.getHttpServletRequest();
- log.info("————————身份认证失败——————————IP地址: "+ oConvertUtils.getIpAddrByRequest(req) +",URL:"+req.getRequestURI());
- throw new AuthenticationException("token为空!");
- }
- // 校验token有效性
- LoginUser loginUser = null;
- try {
- loginUser = this.checkUserTokenIsEffect(token);
- } catch (AuthenticationException e) {
- JwtUtil.responseError(SpringContextUtils.getHttpServletResponse(),401,e.getMessage());
- e.printStackTrace();
- return null;
- }
- return new SimpleAuthenticationInfo(loginUser, token, getName());
- }
-
- /**
- * 校验token的有效性
- *
- * @param token
- */
- public LoginUser checkUserTokenIsEffect(String token) throws AuthenticationException {
- // 解密获得username,用于和数据库进行对比
- String username = JwtUtil.getUsername(token);
- if (username == null) {
- throw new AuthenticationException("token非法无效!");
- }
-
- // 查询用户信息
- log.debug("———校验token是否有效————checkUserTokenIsEffect——————— "+ token);
- LoginUser loginUser = TokenUtils.getLoginUser(username, commonApi, redisUtil);
- //LoginUser loginUser = commonApi.getUserByName(username);
- if (loginUser == null) {
- throw new AuthenticationException("用户不存在!");
- }
- // 判断用户状态
- if (loginUser.getStatus() != 1) {
- throw new AuthenticationException("账号已被锁定,请联系管理员!");
- }
- // 校验token是否超时失效 & 或者账号密码是否错误
- if (!jwtTokenRefresh(token, username, loginUser.getPassword())) {
- throw new AuthenticationException(CommonConstant.TOKEN_IS_INVALID_MSG);
- }
- //update-begin-author:taoyan date:20210609 for:校验用户的tenant_id和前端传过来的是否一致
- String userTenantIds = loginUser.getRelTenantIds();
- if(oConvertUtils.isNotEmpty(userTenantIds)){
- String contextTenantId = TenantContext.getTenant();
- log.debug("登录租户:" + contextTenantId);
- log.debug("用户拥有那些租户:" + userTenantIds);
- //登录用户无租户,前端header中租户ID值为 0
- String str ="0";
- if(oConvertUtils.isNotEmpty(contextTenantId) && !str.equals(contextTenantId)){
- //update-begin-author:taoyan date:20211227 for: /issues/I4O14W 用户租户信息变更判断漏洞
- String[] arr = userTenantIds.split(",");
- if(!oConvertUtils.isIn(contextTenantId, arr)){
- boolean isAuthorization = false;
- //========================================================================
- // 查询用户信息(如果租户不匹配从数据库中重新查询一次用户信息)
- String loginUserKey = CacheConstant.SYS_USERS_CACHE + "::" + username;
- redisUtil.del(loginUserKey);
- LoginUser loginUserFromDb = commonApi.getUserByName(username);
- if (oConvertUtils.isNotEmpty(loginUserFromDb.getRelTenantIds())) {
- String[] newArray = loginUserFromDb.getRelTenantIds().split(",");
- if (oConvertUtils.isIn(contextTenantId, newArray)) {
- isAuthorization = true;
- }
- }
- //========================================================================
-
- //*********************************************
- if(!isAuthorization){
- log.info("租户异常——登录租户:" + contextTenantId);
- log.info("租户异常——用户拥有租户组:" + userTenantIds);
- throw new AuthenticationException("登录租户授权变更,请重新登陆!");
- }
- //*********************************************
- }
- //update-end-author:taoyan date:20211227 for: /issues/I4O14W 用户租户信息变更判断漏洞
- }
- }
- //update-end-author:taoyan date:20210609 for:校验用户的tenant_id和前端传过来的是否一致
- return loginUser;
- }
-
- /**
- * JWTToken刷新生命周期 (实现: 用户在线操作不掉线功能)
- * 1、登录成功后将用户的JWT生成的Token作为k、v存储到cache缓存里面(这时候k、v值一样),缓存有效期设置为Jwt有效时间的2倍
- * 2、当该用户再次请求时,通过JWTFilter层层校验之后会进入到doGetAuthenticationInfo进行身份验证
- * 3、当该用户这次请求jwt生成的token值已经超时,但该token对应cache中的k还是存在,则表示该用户一直在操作只是JWT的token失效了,程序会给token对应的k映射的v值重新生成JWTToken并覆盖v值,该缓存生命周期重新计算
- * 4、当该用户这次请求jwt在生成的token值已经超时,并在cache中不存在对应的k,则表示该用户账户空闲超时,返回用户信息已失效,请重新登录。
- * 注意: 前端请求Header中设置Authorization保持不变,校验有效性以缓存中的token为准。
- * 用户过期时间 = Jwt有效时间 * 2。
- *
- * @param userName
- * @param passWord
- * @return
- */
- public boolean jwtTokenRefresh(String token, String userName, String passWord) {
- String cacheToken = String.valueOf(redisUtil.get(CommonConstant.PREFIX_USER_TOKEN + token));
- if (oConvertUtils.isNotEmpty(cacheToken)) {
- // 校验token有效性
- if (!JwtUtil.verify(cacheToken, userName, passWord)) {
- String newAuthorization = JwtUtil.sign(userName, passWord);
- // 设置超时时间
- redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, newAuthorization);
- redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, JwtUtil.EXPIRE_TIME *2 / 1000);
- log.debug("——————————用户在线操作,更新token保证不掉线—————————jwtTokenRefresh——————— "+ token);
- }
- //update-begin--Author:scott Date:20191005 for:解决每次请求,都重写redis中 token缓存问题
- // else {
- // // 设置超时时间
- // redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, cacheToken);
- // redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, JwtUtil.EXPIRE_TIME / 1000);
- // }
- //update-end--Author:scott Date:20191005 for:解决每次请求,都重写redis中 token缓存问题
- return true;
- }
-
- //redis中不存在此TOEKN,说明token非法返回false
- return false;
- }
-
- /**
- * 清除当前用户的权限认证缓存
- *
- * @param principals 权限信息
- */
- @Override
- public void clearCache(PrincipalCollection principals) {
- super.clearCache(principals);
- }
-
- }
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)

该方法的执行流程是:
1、调用同一文件下的checkUserTokenIsEffect()具体进行用户验证。
loginUser = this.checkUserTokenIsEffect(token);
2、验证成功时返回SimpleAuthenticationInfo对象,失败则抛异常或返回null。
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功能,有待进一步学习。
- public static LoginUser getLoginUser(String username, CommonAPI commonApi, RedisUtil redisUtil) {
- LoginUser loginUser = null;
- String loginUserKey = CacheConstant.SYS_USERS_CACHE + "::" + username;
- //【重要】此处通过redis原生获取缓存用户,是为了解决微服务下system服务挂了,其他服务互调不通问题---
- if (redisUtil.hasKey(loginUserKey)) {
- try {
- loginUser = (LoginUser) redisUtil.get(loginUserKey);
- //解密用户
- SensitiveInfoUtil.handlerObject(loginUser, false);
- } catch (IllegalAccessException e) {
- e.printStackTrace();
- }
- } else {
- // 查询用户信息
- loginUser = commonApi.getUserByName(username);
- }
- return loginUser;
- }
在执行commonApi.getUserByName(username);(org.jeecg.modules.system.service.impl.SysBaseApiImpl#getUserByName)之前,redis中不存在key为sys:cache:encrypt:user::admin的键值对,执行后出现了,继续深入getUserByName内:
- @Override
- //@SensitiveDecode
- public LoginUser getUserByName(String username) {
- //update-begin-author:taoyan date:2022-6-6 for: VUEN-1276 【v3流程图】测试bug 1、通过我发起的流程或者流程实例,查看历史,流程图预览问题
- if (oConvertUtils.isEmpty(username)) {
- return null;
- }
- //update-end-author:taoyan date:2022-6-6 for: VUEN-1276 【v3流程图】测试bug 1、通过我发起的流程或者流程实例,查看历史,流程图预览问题
- LoginUser user = sysUserService.getEncodeUserInfo(username);
-
- //相同类中方法间调用时脱敏解密 Aop会失效,获取用户信息太重要,此处采用原生解密方法,不采用@SensitiveDecodeAble注解方式
- try {
- SensitiveInfoUtil.handlerObject(user, false);
- } catch (IllegalAccessException e) {
- e.printStackTrace();
- }
-
- return user;
- }
getUserByName()的主要功能代码是调用
sysUserService.getEncodeUserInfo(username);
(org.jeecg.modules.system.service.impl.SysUserServiceImpl#getEncodeUserInfo)
- @Override
- @Cacheable(cacheNames=CacheConstant.SYS_USERS_CACHE, key="#username")
- @SensitiveEncode
- public LoginUser getEncodeUserInfo(String username){
- if(oConvertUtils.isEmpty(username)) {
- return null;
- }
- LoginUser loginUser = new LoginUser();
- SysUser sysUser = userMapper.getUserByName(username);
- //查询用户的租户ids
- this.setUserTenantIds(sysUser);
- if(sysUser==null) {
- return null;
- }
- BeanUtils.copyProperties(sysUser, loginUser);
- return loginUser;
- }
在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用法,这时我小心假设它是创建缓存的方法,并通过百度印证了猜想。