欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

后台管理系统权限设计

程序员文章站 2022-04-26 11:26:25
...

       在开发后台管理系统的时候,例如OA系统,绩效系统,crm客户关系管理系统,都会存在用户权限划分的问题。该文章描述了角色的定义,以及权限的划分等含义。如下图所示:

后台管理系统权限设计


  1. useruser_basicinfo:记录用户资料。
  2. role:角色表,记录公司各种角色,比如:管理员,销售,销售主管,开发,开发经理等
  3. permission:菜单表,记录系统的菜单栏目和页面上的按钮(例如:绩效管理,会议管理,新增绩效,预约会议),包括url和code等信息。
  4. role_permission:角色权限表,记录某个角色有多少个菜单。一个角色对应多个菜单,菜单不可重复。
  5. user_role:用户角色表,用户可拥有多个角色。

一.建表脚本

CREATE TABLE `permission` (
  `ID` int(11) NOT NULL AUTO_INCREMENT,
  `PID` int(11) DEFAULT NULL COMMENT '父节点名称',
  `NAME` varchar(50) COLLATE utf8_bin NOT NULL COMMENT '名称',
  `TYPE` varchar(20) COLLATE utf8_bin DEFAULT NULL COMMENT '类型:菜单or功能',
  `SORT` int(11) DEFAULT NULL COMMENT '排序',
  `URL` varchar(255) COLLATE utf8_bin DEFAULT NULL COMMENT '菜单URL',
  `PERM_CODE` varchar(50) COLLATE utf8_bin DEFAULT NULL COMMENT '菜单编码',
  `ICON` varchar(255) COLLATE utf8_bin DEFAULT NULL COMMENT '图标',
  `STATE` varchar(10) COLLATE utf8_bin DEFAULT NULL,
  `DESCRIPTION` varchar(200) COLLATE utf8_bin DEFAULT NULL,
  `CATEGORY_ID` bigint(20) DEFAULT NULL COMMENT '导航ID',
  `ICON_URL` varchar(200) COLLATE utf8_bin DEFAULT NULL COMMENT '图标的URL',
  `IS_VALID` tinyint(4) NOT NULL DEFAULT '0' COMMENT '0 有效  1 无效',
  `DOMAIN_ID` int(11) DEFAULT NULL COMMENT '来源系统ID',
  PRIMARY KEY (`ID`),
  KEY `idx_pid` (`PID`)
) ENGINE=InnoDB AUTO_INCREMENT=177 DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='菜单';
CREATE TABLE `role` (
  `ID` int(11) NOT NULL AUTO_INCREMENT,
  `NAME` varchar(20) COLLATE utf8_bin NOT NULL COMMENT '角色名称',
  `ROLE_CODE` varchar(20) COLLATE utf8_bin NOT NULL COMMENT '角色编码',
  `DESCRIPTION` varchar(200) COLLATE utf8_bin DEFAULT NULL COMMENT '角色描述',
  `SORT` smallint(6) DEFAULT NULL COMMENT '排序',
  `DEL_FLAG` varchar(255) COLLATE utf8_bin DEFAULT NULL,
  PRIMARY KEY (`ID`)
) ENGINE=InnoDB AUTO_INCREMENT=18 DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='角色表';
CREATE TABLE `role_permission` (
  `ID` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键id',
  `ROLE_ID` int(11) DEFAULT NULL COMMENT '角色id',
  `PERMISSION_ID` int(11) DEFAULT NULL COMMENT '菜单ID',
  PRIMARY KEY (`ID`)
) ENGINE=InnoDB AUTO_INCREMENT=614 DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='角色权限表';CREATE TABLE `user_role` (
  `ID` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `USER_ID` int(11) NOT NULL COMMENT '用户ID',
  `ROLE_ID` int(11) NOT NULL COMMENT '角色ID',
  PRIMARY KEY (`ID`)
) ENGINE=InnoDB AUTO_INCREMENT=327 DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='用户角色表';
用户表结构就不贴了。

二.url拦截

            permission表中的菜单和按钮如何控制?

      方案一:当然做得简单点,登录时获取用户权限信息,有权限则显示菜单和按钮标签,无权限则隐藏。(不推荐)

      方案二:在方案一的基础上做个拦截器,每次访问url的时候,判断该用户是否有该权限。

    拦截器:

package com.dfws.manage.utils;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.StringTokenizer;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.http.entity.ContentType;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

import com.alibaba.druid.util.StringUtils;
import com.dfws.manage.base.ApiResult;
import com.dfws.manage.customer.dto.Login;
import com.dfws.manage.shiro.redis.conf.CasTicketCache;
import com.dfws.manage.user.domain.DfwsSysUserAccount;
import com.dfws.manage.user.service.DfwsSysUserAccountService;
import com.tianyu.jty.system.share.SharedUser;

	/**
	 * 登陆拦截器
	 */
	public class AuthorizeInterceptor extends HandlerInterceptorAdapter {
	    private static final Logger logger = Logger.getLogger(AuthorizeInterceptor.class);

	    private final String loginUrl = "/notLogin";//未登录接口地址
	    
		public static Collection<String> ANON_URLS = new ArrayList<String>();

		public static String DEFAULT_SEPARATOR = ",";
		
		@Value(value = "${cas.logout.url}")
		private String logoutUrl;
		
		 @Autowired
		@Qualifier(value = "casTicketCache")
		private CasTicketCache casTicketCache;

		public static void setAnonUrls(String anonUrls) {
			StringTokenizer tokenizer = new StringTokenizer(anonUrls, DEFAULT_SEPARATOR);
			while(tokenizer.hasMoreElements()) {
				String token = tokenizer.nextToken();
				ANON_URLS.add(token);
			}
			ANON_URLS = Collections.unmodifiableCollection(ANON_URLS);
		}
		
	    @Autowired
	    private DfwsSysUserAccountService userAccountService;
	    
	    
	    @Override
	    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
	                           ModelAndView modelAndView) throws Exception {
	        super.postHandle(request, response, handler, modelAndView);
	    }

	/**
	 * 前置处理
	 */
	@Override
	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

		String uri = request.getRequestURI();

		String contextPath = request.getContextPath();

		String uriWithoutContx = uri.substring(contextPath.length());

		if (ANON_URLS.contains(uriWithoutContx)) {
			return true;
		}

		System.out.println("请求接口:" + uriWithoutContx);

		System.err.println(request.getParameter("loginName"));
		System.err.println(request.getParameter("token"));
		String loginName = request.getParameter("loginName");
		String token = request.getParameter("token");

		boolean reLogin = false;

		if(uriWithoutContx.contains("dict") || uriWithoutContx.equals("crm")
				|| uriWithoutContx.equals("excel")){
			return true;
		}
		
		if (!StringUtils.isEmpty(loginName) && !StringUtils.isEmpty(token)) {
			SharedUser user = casTicketCache.get(token);
			if (user == null ) {
				reLogin = true;
			}
		} else {
			reLogin = true;
		}

		// 需要做重新登陆
		if (reLogin) {

			// System.out.println(request.getScheme()+"://"+request.getServerName()+":"+request.getServerPort()+loginUrl+"/" );

			// System.out.println(request.getContextPath() + loginUrl);
			// response.sendRedirect(request.getContextPath() + loginUrl);

			response.sendRedirect(logoutUrl);

			return false;

		} else { 
			// 不需要做重新登陆
			return true;
		}

		// return super.preHandle(request, response, handler);
	}

	    @Override
	    public void afterConcurrentHandlingStarted(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
	        super.afterConcurrentHandlingStarted(request, response, handler);
	    }

	    /**
	     * 结束处理
	     */
	    @Override
	    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
	        super.afterCompletion(request, response, handler, ex);
	    }

	    /**
	     * 获取http的所有请求参数
	     */
	    private String getAllParamFromHttpReq(HttpServletRequest request) {
	        Enumeration paramNames = request.getParameterNames();
	        StringBuffer sb = new StringBuffer();
	        while (paramNames.hasMoreElements()) {
	            String paramName = (String) paramNames.nextElement();

	            String[] paramValues = request.getParameterValues(paramName);
	            if (paramValues.length == 1) {
	                String paramValue = paramValues[0];
	                if (paramValue.length() != 0) {
	                    sb.append("&").append(paramName).append("=").append(paramValue);
	                }
	            }
	        }

	        return sb.toString();
	    }
	
}
    <!-- 全局拦截器处理 -->
  <mvc:interceptors>
 	 <mvc:interceptor>
    	 <mvc:mapping path="/**/*"/>
         <mvc:exclude-mapping path="/css/**"/>
         <mvc:exclude-mapping path="/**/*.css"/>
         <mvc:exclude-mapping path="/js/**"/>
         <mvc:exclude-mapping path="/**/*.js"/>
         <mvc:exclude-mapping path="/img/**"/>
         <mvc:exclude-mapping path="/fonts/**"/>
         <mvc:exclude-mapping path="/backstage/**"/>

         <mvc:exclude-mapping path="/preLogin/**"/>
         <mvc:exclude-mapping path="/login/**"/>
    	 <mvc:exclude-mapping path="/captcha/**"/>

	    <!-- 直接将bean配置到mvc:interceptors,将会应用到所有的url请求HandlerMapping,达到全局拦截器的目的。 -->
	    <bean class="com.dfws.manage.utils.AuthorizeInterceptor"/>
     </mvc:interceptor>

  </mvc:interceptors>

三:权限扩展(数据权限)

      上面仅仅实现了用户的基本菜单和按钮权限,然后一个大型公司的项目,数据比较敏感,会控制菜单的数据展示范围。数据权限的控制根据公司业务需求定义。下面提两个参考方案。
  1. 根据部门划分数据权限,新增数据权限角色,将部门挂载这个角色中,然后给这个角色添加人员。有该角色的人,则可以看到该角色下的所有部门。比如:客户管理栏目,本来是可以看到公司所有客户信息,加了数据权限后,你只能看到你所在角色的部门和下级部门的数据的,如果你的角色拥有的全公司的部门,则你能看到全公司的数据。在写sql的时候,用where in查找即可,in里面查询的是员工id。
  2. 数据权限直接分配到个人,数据权限与人绑定关系,数据权限范围:全公司,多个部门和个人。比如:小李,需要看到小张的数据,则把小张的数据权限分配给小李下即可。

注:方案一相对来说较简单,易维护。方案二数据权限比较复杂,灵活方便,不好维护。