Spring Boot动态权限变更实现的整体方案

java1234

共 44950字,需浏览 90分钟

 ·

2021-07-05 13:52

点击上方蓝色字体,选择“标星公众号”

优质文章,第一时间送达

  作者 |  阿拉伯1999

来源 |  urlify.cn/ZRriqe

1、前言

  在Web项目中,权限管理即权限访问控制为网站访问安全提供了保障,并且很多项目使用了Session作为缓存,结合AOP技术进行token认证和权限控制。权限控制流程大致如下图所示:

  现在,如果管理员修改了用户的角色,或修改了角色的权限,都会导致用户权限发生变化,此时如何实现动态权限变更,使得前端能够更新用户的权限树,后端访问鉴权AOP模块能够知悉这种变更呢?

2、问题及解决方案

  现在的问题是,管理员没法访问用户Session,因此没法将变更通知此用户。而用户如果已经登录,或直接关闭浏览器页面而不是登出操作,Session没有过期前,用户访问接口时,访问鉴权AOP模块仍然是根据之前缓存的Session信息进行处理,没法做到动态权限变更。

  使用Security+WebSocket是一个方案,但没法处理不在线用户。

  解决方案的核心思想是利用ServletContext对象的共享特性,来实现用户权限变更的信息传递。然后在AOP类中查询用户是否有变更通知记录需要处理,如果权限发生变化,则修改response消息体,添加附加通知信息给前端。前端收到附加的通知信息,可更新功能权限树,并进行相关处理。

  这样,利用的变更通知服务,不仅后端的用户url访问接口可第一时间获悉变更,还可以通知到前端,从而实现了动态权限变更。

3、方案实现

3.1、开发变更通知类

  服务接口类ChangeNotifyService,代码如下:

package com.abc.questInvest.service;

/**
 * @className  : ChangeNotifyService
 * @description  : 变更通知服务
 * @summary  :
 * @history  :
 * ------------------------------------------------------------------------------
 * date   version  modifier  remarks                   
 * ------------------------------------------------------------------------------
 * 2021/06/28 1.0.0  sheng.zheng  初版
 *
 */
public interface ChangeNotifyService {

 /**
  * 
  * @methodName  : getChangeNotifyInfo
  * @description  : 获取指定用户ID的变更通知信息 
  * @param userId : 用户ID
  * @return  : 返回0表示无变更通知信息,其它值按照bitmap编码。目前定义如下:
  *   bit0: : 修改用户的角色组合值,从而导致权限变更;
  *   bit1: : 修改角色的功能项,从而导致权限变更;
  *   bit2: : 用户禁用,从而导致权限变更;
  *   bit3: : 用户调整部门,从而导致数据权限变更;
  * @history  :
  * ------------------------------------------------------------------------------
  * date   version  modifier  remarks                   
  * ------------------------------------------------------------------------------
  * 2021/06/28 1.0.0  sheng.zheng  初版
  *
  */
 public Integer getChangeNotifyInfo(Integer userId);
 
 /**
  * 
  * @methodName  : setChangeNotifyInfo
  * @description  : 设置变更通知信息
  * @param userId : 用户ID
  * @param changeNotifyInfo : 变更通知值
  *   bit0: : 修改用户的角色组合值,从而导致权限变更;
  *   bit1: : 修改角色的功能项,从而导致权限变更;
  *   bit2: : 用户禁用,从而导致权限变更;
  *   bit3: : 用户调整部门,从而导致数据权限变更;
  * @history  :
  * ------------------------------------------------------------------------------
  * date   version  modifier  remarks                   
  * ------------------------------------------------------------------------------
  * 2021/06/28 1.0.0  sheng.zheng  初版
  *
  */
 public void setChangeNotifyInfo(Integer userId,Integer changeNotifyInfo);  
}

  服务实现类ChangeNotifyServiceImpl,代码如下:

package com.abc.questInvest.service.impl;

import java.util.HashMap;
import java.util.Map;

import org.springframework.stereotype.Service;

import com.abc.questInvest.service.ChangeNotifyService;

/**
 * @className  : ChangeNotifyServiceImpl
 * @description  : ChangeNotifyService实现类
 * @summary  :
 * @history  :
 * ------------------------------------------------------------------------------
 * date   version  modifier  remarks                   
 * ------------------------------------------------------------------------------
 * 2021/06/28 1.0.0  sheng.zheng  初版
 *
 */
@Service
public class ChangeNotifyServiceImpl implements ChangeNotifyService {
 
 //用户ID与变更过通知信息映射表
 private Map<Integer,Integer> changeNotifyMap = new HashMap<Integer,Integer>();
 
 /**
  * 
  * @methodName  : getChangeNotifyInfo
  * @description  : 获取指定用户ID的变更通知信息 
  * @param userId : 用户ID
  * @return  : 返回0表示无变更通知信息,其它值按照bitmap编码。目前定义如下:
  *   bit0: : 修改用户的角色组合值,从而导致权限变更;
  *   bit1: : 修改角色的功能项,从而导致权限变更;
  *   bit2: : 用户禁用,从而导致权限变更;
  *   bit3: : 用户调整部门,从而导致数据权限变更;
  * @history  :
  * ------------------------------------------------------------------------------
  * date   version  modifier  remarks                   
  * ------------------------------------------------------------------------------
  * 2021/06/28 1.0.0  sheng.zheng  初版
  *
  */
 @Override
 public Integer getChangeNotifyInfo(Integer userId) {
  Integer changeNotifyInfo = 0;
  //检查该用户是否有变更通知信息
  if (changeNotifyMap.containsKey(userId)) {
   changeNotifyInfo = changeNotifyMap.get(userId);
   //移除数据,加锁保护
   synchronized(changeNotifyMap) {
    changeNotifyMap.remove(userId);
   }
  }
  return changeNotifyInfo;
 }
 
 /**
  * 
  * @methodName  : setChangeNotifyInfo
  * @description  : 设置变更通知信息,该功能一般由管理员触发调用
  * @param userId : 用户ID
  * @param changeNotifyInfo : 变更通知值
  *   bit0: : 修改用户的角色组合值,从而导致权限变更;
  *   bit1: : 修改角色的功能项,从而导致权限变更;
  *   bit2: : 用户禁用,从而导致权限变更;
  *   bit3: : 用户调整部门,从而导致数据权限变更;
  * @history  :
  * ------------------------------------------------------------------------------
  * date   version  modifier  remarks                   
  * ------------------------------------------------------------------------------
  * 2021/06/28 1.0.0  sheng.zheng  初版
  *
  */
 @Override
 public void setChangeNotifyInfo(Integer userId,Integer changeNotifyInfo) {
  //检查该用户是否有变更通知信息
  if (changeNotifyMap.containsKey(userId)) {
   //如果有,表示之前变更通知未处理
   //获取之前的值
   Integer oldChangeNotifyInfo = changeNotifyMap.get(userId);
   //计算新值。bitmap编码,或操作
   Integer newChangeNotifyInfo = oldChangeNotifyInfo | changeNotifyInfo;
   //设置数据,加锁保护
   synchronized(changeNotifyMap) {
    changeNotifyMap.put(userId,newChangeNotifyInfo);
   }
  }else {
   //如果没有,设置一条
   changeNotifyMap.put(userId,changeNotifyInfo);
  }
 }
}

  此处,变更通知类型,与使用的demo项目有关,目前定义了4种变更通知类型。实际上,除了权限相关的变更,还有与Session缓存字段相关的变更,也需要通知,否则用户还是在使用旧数据。

3.2、将变更通知类对象,纳入全局配置服务对象中进行管理

  全局配置服务类GlobalConfigService,负责管理全局的配置服务对象,服务接口类代码如下:

package com.abc.questInvest.service;

/**
 * @className  : GlobalConfigService
 * @description  : 全局变量管理类
 * @summary  :
 * @history  :
 * ------------------------------------------------------------------------------
 * date   version  modifier  remarks                   
 * ------------------------------------------------------------------------------
 * 2021/06/02 1.0.0  sheng.zheng  初版
 *
 */
public interface GlobalConfigService {
 
 /**
  * 
  * @methodName  : loadData
  * @description  : 加载数据 
  * @return  : 成功返回true,否则返回false
  * @history  :
  * ------------------------------------------------------------------------------
  * date   version  modifier  remarks                   
  * ------------------------------------------------------------------------------
  * 2021/06/02 1.0.0  sheng.zheng  初版
  *
  */
 public boolean loadData();
 
 //获取TableCodeConfigService对象
 public TableCodeConfigService getTableCodeConfigService(); 
 
 //获取SysParameterService对象
 public SysParameterService getSysParameterService();
 
 //获取FunctionTreeService对象
 public FunctionTreeService getFunctionTreeService();

 //获取RoleFuncRightsService对象
 public RoleFuncRightsService getRoleFuncRightsService();
 
 //获取ChangeNotifyService对象
 public ChangeNotifyService getChangeNotifyService();
 
}

  服务实现类GlobalConfigServiceImpl,代码如下:

package com.abc.questInvest.service.impl;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.abc.questInvest.service.ChangeNotifyService;
import com.abc.questInvest.service.FunctionTreeService;
import com.abc.questInvest.service.GlobalConfigService;
import com.abc.questInvest.service.RoleFuncRightsService;
import com.abc.questInvest.service.SysParameterService;
import com.abc.questInvest.service.TableCodeConfigService;

/**
 * @className  : GlobalConfigServiceImpl
 * @description  : GlobalConfigService实现类
 * @summary  :
 * @history  :
 * ------------------------------------------------------------------------------
 * date   version  modifier  remarks                   
 * ------------------------------------------------------------------------------
 * 2021/06/02 1.0.0  sheng.zheng  初版
 *
 */
@Service
public class GlobalConfigServiceImpl implements GlobalConfigService{
 
 //ID编码配置表数据服务
 @Autowired
 private TableCodeConfigService tableCodeConfigService;
 
 //系统参数表数据服务
 @Autowired
 private SysParameterService sysParameterService;
 
 //功能树表数据服务
 @Autowired
 private FunctionTreeService functionTreeService;
 
 //角色权限表数据服务
 @Autowired 
 private RoleFuncRightsService roleFuncRightsService;
 
 //变更通知服务
 @Autowired 
 private ChangeNotifyService changeNotifyService;
 
 
 /**
  * 
  * @methodName  : loadData
  * @description  : 加载数据 
  * @return  : 成功返回true,否则返回false
  * @history  :
  * ------------------------------------------------------------------------------
  * date   version  modifier  remarks                   
  * ------------------------------------------------------------------------------
  * 2021/06/02 1.0.0  sheng.zheng  初版
  *
  */
 @Override
 public boolean loadData() {
  boolean bRet = false;
  
  //加载table_code_config表记录
  bRet = tableCodeConfigService.loadData();
  if (!bRet) {
   return bRet;
  }
  
  //加载sys_parameters表记录
  bRet = sysParameterService.loadData();
  if (!bRet) {
   return bRet;
  }
  
  //changeNotifyService目前没有持久层,无需加载
  //如果服务重启,信息丢失,也没关系,因为此时Session也会失效
  
  //加载function_tree表记录
  bRet = functionTreeService.loadData();
  if (!bRet) {
   return bRet;
  }
  
  //加载role_func_rights表记录
  //先设置完整功能树
  roleFuncRightsService.setFunctionTree(functionTreeService.getFunctionTree());
  //然后加载数据
  bRet = roleFuncRightsService.loadData();
  if (!bRet) {
   return bRet;
  }
  
  return bRet;
 }
 
 //获取TableCodeConfigService对象
 @Override
 public TableCodeConfigService getTableCodeConfigService() {
  return tableCodeConfigService;
 }
 
 //获取SysParameterService对象
 @Override
 public SysParameterService getSysParameterService() {
  return sysParameterService;
 }
 
 //获取FunctionTreeService对象
 @Override
 public FunctionTreeService getFunctionTreeService() {
  return functionTreeService;
 } 
 
 //获取RoleFuncRightsService对象
 @Override
 public RoleFuncRightsService getRoleFuncRightsService() {
  return roleFuncRightsService;
 }
 
 //获取ChangeNotifyService对象
 @Override
 public ChangeNotifyService getChangeNotifyService() {
  return changeNotifyService;
 }

}

  GlobalConfigServiceImpl类,管理了很多配置服务类,此处主要关注ChangeNotifyService类对象。

3.3、使用ServletContext,管理全局配置服务类对象

  全局配置服务类在应用启动时加载到Spring容器中,这样可实现共享,减少对数据库的访问压力。

  实现一个ApplicationListener类,代码如下:

package com.abc.questInvest;

import javax.servlet.ServletContext;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.stereotype.Component;
import org.springframework.web.context.WebApplicationContext;

import com.abc.questInvest.service.GlobalConfigService;

/**
 * @className : ApplicationStartup
 * @description : 应用侦听器
 *
 */
@Component
public class ApplicationStartup implements ApplicationListener<ContextRefreshedEvent>{
    //全局变量管理对象,此处不能自动注入
    private GlobalConfigService globalConfigService = null;
    
    @Override
    public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {
        try {
         if(contextRefreshedEvent.getApplicationContext().getParent() == null){ 
          //root application context 没有parent.
    
          System.out.println("========定义全局变量==================");
          // 将 ApplicationContext 转化为 WebApplicationContext
             WebApplicationContext webApplicationContext =
                     (WebApplicationContext)contextRefreshedEvent.getApplicationContext();
             // 从 webApplicationContext 中获取  servletContext
             ServletContext servletContext = webApplicationContext.getServletContext();
             
             //加载全局变量管理对象
             globalConfigService = (GlobalConfigService)webApplicationContext.getBean(GlobalConfigService.class);
             //加载数据
             boolean bRet = globalConfigService.loadData();
             if (false == bRet) {
              System.out.println("加载全局变量失败");
              return;
             }        
             //======================================================================
             // servletContext设置值
             servletContext.setAttribute("GLOBAL_CONFIG_SERVICE", globalConfigService);  
             
         }
     } catch (Exception e) {
         e.printStackTrace();
     }        
    }
}

  在启动类中,加入该应用侦听器ApplicationStartup。

 public static void main(String[] args) {
     SpringApplication springApplication = new SpringApplication(QuestInvestApplication.class);
        springApplication.addListeners(new ApplicationStartup());
        springApplication.run(args);  
 }

  现在,有了一个GlobalConfigService类型的全局变量globalConfigService。

3.4、发出变更通知

  此处举2个例子,说明发出变更通知的例子,这两个例子,都在用户管理模块,UserManServiceImpl类中。

  1)管理员修改用户信息,可能导致权限相关项发生变动,2)禁用用户,发出变更过通知。

  发出通知的相关代码如下:

 /**
  * 
  * @methodName  : editUser
  * @description  : 修改用户信息
  * @param userInfo : 用户信息对象
  * @history  :
  * ------------------------------------------------------------------------------
  * date   version  modifier  remarks                   
  * ------------------------------------------------------------------------------
  * 2021/06/08 1.0.0  sheng.zheng  初版
  * 2021/06/28 1.0.1  sheng.zheng  增加变更通知的处理
  *
  */
 @Override
 public void editUser(HttpServletRequest request,UserInfo userInfo) {
  //输入参数校验
  checkValidForParams("editUser",userInfo);
  
  //获取操作人账号
  String operatorName = (String) request.getSession().getAttribute("username");
  userInfo.setOperatorName(operatorName);  

  //登录名和密码不修改
  userInfo.setLoginName(null);
  userInfo.setSalt(null);
  userInfo.setPasswd(null);
  
  //获取修改之前的用户信息
  Integer userId = userInfo.getUserId();
  UserInfo oldUserInfo = userManDao.selectUserByKey(userId);

  //修改用户记录
  try {
   userManDao.updateSelective(userInfo);   
  }catch(Exception e) {
   e.printStackTrace();
   log.error(e.getMessage());
   throw new BaseException(ExceptionCodes.USERS_EDIT_USER_FAILED);
  }
  
  //检查是否有需要通知的变更
  Integer changeFlag = 0;
  if (userInfo.getRoles() != null) {
   if(oldUserInfo.getRoles() != userInfo.getRoles()) {
    //角色组合有变化,bit0
    changeFlag |= 0x01;
   }
  }
  if (userInfo.getDeptId() != null) {
   if (oldUserInfo.getDeptId() != userInfo.getDeptId()) {
    //部门ID有变化,bit3
    changeFlag |= 0x08;
   }
  }
  if (changeFlag > 0) {
   //如果有变更过通知项
   //获取全局变量
   ServletContext servletContext = request.getServletContext();
   GlobalConfigService globalConfigService = (GlobalConfigService)servletContext.getAttribute("GLOBAL_CONFIG_SERVICE");
   globalConfigService.getChangeNotifyService().setChangeNotifyInfo(userId, changeFlag);   
  }
 }

 /**
  * 
  * @methodName  : disableUser
  * @description  : 禁用用户
  * @param params : map对象,形式如下:
  *  {
  *   "userId" : 1
  *  }
  * @history  :
  * ------------------------------------------------------------------------------
  * date   version  modifier  remarks                   
  * ------------------------------------------------------------------------------
  * 2021/06/08 1.0.0  sheng.zheng  初版
  * 2021/06/28 1.0.1  sheng.zheng  增加变更通知的处理
  *
  */
 @Override
 public void disableUser(HttpServletRequest request,Map<String,Object> params) {
  //输入参数校验
  checkValidForParams("disableUser",params);
  
  UserInfo userInfo = new UserInfo();
  
  //获取操作人账号
  String operatorName = (String) request.getSession().getAttribute("username");
  
  //设置userInfo信息
  Integer userId = (Integer)params.get("userId");
  userInfo.setUserId(userId);
  userInfo.setOperatorName(operatorName);
  //设置禁用标记
  userInfo.setDeleteFlag((byte)1);
  
  //修改密码
  try {
   userManDao.updateEnable(userInfo);   
  }catch(Exception e) {
   e.printStackTrace();
   log.error(e.getMessage());
   throw new BaseException(ExceptionCodes.USERS_EDIT_USER_FAILED);
  }  
  
  //禁用用户,发出变更通知
  //获取全局变量
  ServletContext servletContext = request.getServletContext();
  GlobalConfigService globalConfigService = (GlobalConfigService)servletContext.getAttribute("GLOBAL_CONFIG_SERVICE");
  //禁用用户:bit2
  globalConfigService.getChangeNotifyService().setChangeNotifyInfo(userId, 0x04);    
 }

  本demo项目的角色相对较少,没有使用用户角色关系表,而是使用了bitmap编码,角色ID取值为2^n,用户角色组合roles字段为一个Integer值。如roles=7,表示角色ID组合=[1,2,4]。
  另外,如果修改了角色的功能权限集合,则需要查询受影响的用户ID列表,依次发出通知,可类似处理。

3.5、修改Response响应消息体

  Response响应消息体,为BaseResponse,代码如下:

package com.abc.questInvest.vo.common;

import lombok.Data;

/**
 * @className  : BaseResponse
 * @description  : 基本响应消息体对象
 * @summary  :
 * @history  :
 * ------------------------------------------------------------------------------
 * date   version  modifier  remarks                   
 * ------------------------------------------------------------------------------
 * 2021/05/31 1.0.0  sheng.zheng  初版
 * 2021/06/28 1.0.1  sheng.zheng  增加变更通知的附加信息
 *
 */
@Data
public class BaseResponse<T> {
    //响应码
    private int code;

    //响应消息
    private String message;
        
    //响应实体信息
    private T data;

    //分页信息
    private Page page;

    //附加通知信息
    private Additional additional;
}

  BaseResponse类增加了Additional类型的additional属性字段,用于输出附加信息。

  Additional类的定义如下:

package com.abc.questInvest.vo.common;

import lombok.Data;

/**
 * @className  : Additional
 * @description  : 附加信息
 * @summary  :
 * @history  :
 * ------------------------------------------------------------------------------
 * date   version  modifier  remarks                   
 * ------------------------------------------------------------------------------
 * 2021/06/28 1.0.0  sheng.zheng  初版
 *
 */
@Data
public class Additional {
    //通知码,附加信息
    private int notifycode;

    //通知码对应的消息
    private String notification;
    
    //更新的token
    private String token;
    
    //更新的功能权限树
    private String rights;

}

  附加信息类Additional中,各属性字段的说明:

  • notifycode,为通知码,即可对应通知消息的类型,目前只有一种,可扩展。

  • notification,为通知码对应的消息。

  通知码,在ExceptionCodes枚举文件中定义:

    //变更通知信息
    USER_RIGHTS_CHANGED(51, "message.USER_RIGHTS_CHANGED""用户权限发生变更"),
 ;  //end enum

    ExceptionCodes(int code, String messageId, String message) {
        this.code = code;
        this.messageId = messageId;
        this.message = message;
    }
  • token,用于要求前端更新token。更新token的目的是确认前端已经收到权限变更通知。因为下次url请求将使用新的token,如果前端未收到或未处理,仍然用旧的token访问,就要跳到登录页了。

  • rights,功能树的字符串输出,是树型结构的JSON字符串。

3.6、AOP鉴权处理

  AuthorizationAspect为鉴权认证的切面类,代码如下:

package com.abc.questInvest.aop;

import java.util.List;

import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;

import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import com.abc.questInvest.common.constants.Constants;
import com.abc.questInvest.common.utils.Utility;
import com.abc.questInvest.dao.UserManDao;
import com.abc.questInvest.entity.FunctionInfo;
import com.abc.questInvest.entity.UserInfo;
import com.abc.questInvest.exception.BaseException;
import com.abc.questInvest.exception.ExceptionCodes;
import com.abc.questInvest.service.GlobalConfigService;
import com.abc.questInvest.service.LoginService;
import com.abc.questInvest.vo.TreeNode;
import com.abc.questInvest.vo.common.Additional;
import com.abc.questInvest.vo.common.BaseResponse;

/**
 * @className  : AuthorizationAspect
 * @description  : 接口访问鉴权切面类
 * @summary  : 使用AOP,进行token认证以及用户对接口的访问权限鉴权
 * @history  :
 * ------------------------------------------------------------------------------
 * date   version  modifier  remarks                   
 * ------------------------------------------------------------------------------
 * 2021/06/06 1.0.0  sheng.zheng  初版
 * 2021/06/28 1.0.1  sheng.zheng  增加变更通知的处理,增加了afterReturning增强
 *
 */
@Aspect
@Component
@Order(2)
public class AuthorizationAspect {
 @Autowired
    private UserManDao userManDao;
 
 //设置切点
    @Pointcut("execution(public * com.abc.questInvest.controller..*.*(..))" +
    "&& !execution(public * com.abc.questInvest.controller.LoginController.*(..))" + 
    "&& !execution(public * com.abc.questInvest.controller.QuestInvestController.*(..))")    
    public void verify(){}
    
    @Before("verify()"
    public void doVerify(){ 
  ServletRequestAttributes attributes=(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();

  HttpServletRequest request=attributes.getRequest(); 
  
  // ================================================================================
  // token认证
  
  //从header中获取token值
  String token = request.getHeader("Authorization");
  if (null == token || token.equals("")){ 
   //return;
   throw new BaseException(ExceptionCodes.TOKEN_IS_NULL); 
  } 
     
  //从session中获取token和过期时间
  String sessionToken = (String)request.getSession().getAttribute("token");
  
  //判断session中是否有信息,可能是非登录用户
  if (null == sessionToken || sessionToken.equals("")) {
   throw new BaseException(ExceptionCodes.TOKEN_WRONG);
  }
     
  //比较token
  if(!token.equals(sessionToken)) {
   //如果请求头中的token与存在session中token两者不一致
   throw new BaseException(ExceptionCodes.TOKEN_WRONG);   
  }
  
  long expireTime = (long)request.getSession().getAttribute("expireTime");
  //检查过期时间
  long time = System.currentTimeMillis();
  if (time > expireTime) {
   //如果token过期
   throw new BaseException(ExceptionCodes.TOKEN_EXPIRED);
  }else {
   //token未过期,更新过期时间
   long newExpiredTime = time + Constants.TOKEN_EXPIRE_TIME * 1000;
   request.getSession().setAttribute("expireTime", newExpiredTime);
  }
  
  // ============================================================================
  // 接口调用权限
  //获取用户ID
  Integer userId = (Integer)request.getSession().getAttribute("userId"); 
  //获取全局变量
  ServletContext servletContext = request.getServletContext();
  GlobalConfigService globalConfigService = (GlobalConfigService)servletContext.getAttribute("GLOBAL_CONFIG_SERVICE");
  
  //===================变更通知处理开始==============================================
  //检查有无变更通知信息
  Integer changeNotifyInfo = globalConfigService.getChangeNotifyService().getChangeNotifyInfo(userId);
  //设置成员属性为false
  boolean rightsChangedFlag = false;  
  if (changeNotifyInfo > 0) {
   //有通知信息
   if ((changeNotifyInfo & 0x09) > 0) {
    //bit0:修改用户的角色组合值,从而导致权限变更
    //bit3:用户调整部门,从而导致数据权限变更
    //mask 0b1001 = 0x09 
    //都需要查询用户表,并更新信息;合在一起查询。
    UserInfo userInfo = userManDao.selectUserByKey(userId);
    //更新Session
               request.getSession().setAttribute("roles", userInfo.getRoles());
               request.getSession().setAttribute("deptId", userInfo.getDeptId()); 
                 if ((changeNotifyInfo & 0x01) > 0) {
                  //权限变更标志置位
                  rightsChangedFlag = true;
                 }
   }else if((changeNotifyInfo & 0x02) > 0) {
    //bit1:修改角色的功能值,从而导致权限变更
               //权限变更标志置位
             rightsChangedFlag = true;
   }else if((changeNotifyInfo & 0x04) > 0) {
    //bit2:用户禁用,从而导致权限变更
    //设置无效token,可阻止该用户访问系统
    request.getSession().setAttribute("token""");
    //直接抛出异常,由前端显示:Forbidden页面
    throw new BaseException(ExceptionCodes.ACCESS_FORBIDDEN);
   }
   if (rightsChangedFlag == true) {
    //写Session,用于将信息传递到afterReturning方法中
    request.getSession().setAttribute("rightsChanged", 1);
   }
  }
  //===================变更通知处理结束==============================================
    
  //从session中获取用户权限值
  Integer roles = (Integer)request.getSession().getAttribute("roles");
  //获取当前接口url值
  String servletPath = request.getServletPath();
    
  //获取该角色对url的访问权限
  Integer rights = globalConfigService.getRoleFuncRightsService().getRoleUrlRights(Utility.parseRoles(roles), servletPath);
  if (rights == 0) {
   //如果无权限访问此接口,抛出异常,由前端显示:Forbidden页面
   throw new BaseException(ExceptionCodes.ACCESS_FORBIDDEN);
  }  
    }    
    
    @AfterReturning(value="verify()" ,returning="result")
    public void afterReturning(BaseResponse result) {
     //限制必须是BaseResponse类型,其它类型的返回值忽略
     //获取Session
        ServletRequestAttributes sra = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = sra.getRequest();
     Integer rightsChanged = (Integer)request.getSession().getAttribute("rightsChanged");
     if (rightsChanged != null && rightsChanged == 1) {
      //如果有用户权限变更,通知前端来刷新该用户的功能权限树
      //构造附加信息
      Additional additional = new Additional();
      additional.setNotifycode(ExceptionCodes.USER_RIGHTS_CHANGED.getCode());
      additional.setNotification(ExceptionCodes.USER_RIGHTS_CHANGED.getMessage());
      //更新token
      String loginName = (String)request.getSession().getAttribute("username");
      String token = LoginService.generateToken(loginName);
      additional.setToken(token);
      //更新token,要求下次url访问使用新的token
      request.getSession().setAttribute("token", token);
      //获取用户的功能权限树
      Integer roles = (Integer)request.getSession().getAttribute("roles");
      ServletContext servletContext = request.getServletContext();
      GlobalConfigService globalConfigService = (GlobalConfigService)servletContext.getAttribute("GLOBAL_CONFIG_SERVICE");
         //获取用户权限的角色功能数
      List<Integer> roleList = Utility.parseRoles(roles);
         TreeNode<FunctionInfo> rolesFunctionTree = 
           globalConfigService.getRoleFuncRightsService().
           getRoleRights(roleList);
         additional.setRights(rolesFunctionTree.toString());
      //修改response信息
         result.setAdditional(additional);
      //移除Session的rightsChanged项
      request.getSession().removeAttribute("rightsChanged");
     }
    }
}

  AuthorizationAspect类定义了切点verify(),@Before增强用于鉴权验证,增加了对变更通知信息的处理。并利用Session,用rightsChanged属性字段记录需要通知前端的标志,在@AfterReturning后置增强中根据该属性字段的值,进行一步的处理。

  @Before增强的doVerify方法中,如果发现角色组合有改变,但仍有访问此url权限时,会继续后续处理,这样不会中断业务;如果没有访问此url权限,则返回访问受限异常信息,由前端显示访问受限页码(类似403 Forbidden 页码)。

  在后置增强@AfterReturning中,限定了返回值类型,如果该请求响应的类型是BaseResponse类型,则修改reponse消息体,附加通知信息;如果不是,则不处理,会等待下一个url请求,直到返回类型是BaseResponse类型。也可以采用自定义response的header的方式,这样,就无需等待了。

  generateToken方法,是LoginService类的静态方法,用于生成用户token。

  至于Utility的parseRoles方法,是将bitmap编码的roles解析为角色ID的列表,代码如下:

 //========================= 权限组合值解析 ======================================     
    /**
     * 
     * @methodName  : parseRoles
     * @description  : 解析角色组合值
     * @param roles  : 按位设置的角色组合值
     * @return   : 角色ID列表
     * @history   :
     * ------------------------------------------------------------------------------
     * date   version  modifier  remarks                   
     * ------------------------------------------------------------------------------
     * 2021/06/24 1.0.0  sheng.zheng  初版
     *
     */
    public static List<Integer> parseRoles(int roles){
     List<Integer> roleList = new ArrayList<Integer>();

     int newRoles = roles;
     int bit0 = 0;
     int roleId = 0;
     for (int i = 0; i < 32; i++) {
      //如果组合值的余位都为0,则跳出
      if (newRoles == 0) {
       break;
      }
      
      //取得最后一位
      bit0 = newRoles & 0x01;
      if (bit0 == 1) {
       //如果该位为1,左移i位
       roleId = 1 << i;
       roleList.add(roleId);
      }
      
      //右移一位
      newRoles = newRoles >> 1;
     }
     return roleList;
    } 

  getRoleRights方法,是角色功能权限服务类RoleFuncRightsService的方法,它提供了根据List类型的角色ID列表,快速获取功能权限树的功能。







浏览 42
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报