按钮级别的动态权限实现方案【原创思想】

按钮基本的权限校验实现


目录

简述

核心类

一、Action 类

二、Menu 类

三、Role 类

分析

权限配置定义和读取

一、Action配置

二、菜单配置

三、配置读取生成权限表

提供接口配置角色

权限校验

登录

结尾


 

简述

    哈哈哈哈哈哈,我答应会经常更新的。很好,再一次做不到,打脸!来来来写写博客吹吹牛逼来了!

    是这样的目前很多业务系统都希望系统的权限是可以动态可调整的,而且还希望系统的权限可以精确到按钮级别的。通常会这样做先做个按钮和菜单的功能配置表,去控制前端按钮的显示与否。然后再做一个接口管理表,管理所有接口在某个角色当中是否有权限调用。而且我们需要通过前端页面去配置这些接口和按钮。

    今天就说一下我是怎么去实现,不需要去配置有什么接口,有什么按钮的动态权限,同时我也会基于AOP去实现权限认证。


核心类

一、Action 类

主要是记录系统里面有多少个操作,每个接口就算一个操作,我们将功能按钮和对应的接口抽象成一个Action 每一个Action都需要定义一个全局唯一的ActionId,Action还关联着对应的菜单 menuId(主要生成权限树)。

@Entity
@Table(name = "T_ACTION")
public class Action {

    public static final int STATUS_ENABLE = 1;
    public static final int STATUS_DISABLE = 0;


    /**
     * 行为ID 全局唯一 英文标识
     */
    @Id
    @Column(nullable = false)
    private String actionId;

    /**
     * 行为名称 主要用作显示
     */
    @Column(nullable = false)
    private String actionName;

    /**
     * 行为对应的接口URI
     */
    @Column(nullable = false, length = 256)
    private String actionUri;

    /**
     * 行为所属的菜单ID
     */
    @Column(nullable = false)
    private String menuId;

    @Column(nullable = false, length = 6)
    private int status;
}

二、Menu 类

菜单对象,主要用于是否需要显示前端菜单项的。判断可以通过菜单以及子孙菜单是否有相应的action权限,如果当前角色 一个action的权限都没有那就直接将菜单隐藏,这样在返回给前端的权限树当中,就不会有相应的菜单。

Entity
@Table(name = "T_MENU")
public class Menu {

    public static final int STATUS_ENABLE = 1;
    public static final int STATUS_DISABLE = 0;

    public Menu() {

    }

    public Menu(String menuId, String menuName, String parentMenuId) {
        this.menuId = menuId;
        this.menuName = menuName;
        this.parentMenuId = parentMenuId;
        this.status = STATUS_ENABLE;
    }

    /**
     * 菜单ID 全局唯一标识
     */
    @Id
    @Column(nullable = false)
    private String menuId;

    /**
     * 菜单名称 用于显示
     */
    @Column(nullable = false)
    private String menuName;

    /**
     * 父菜单ID 如果没有父级菜单ID 即为顶级菜单
     */
    private String parentMenuId;

    /**
     * 权限状态如果为0则功能已经不存在 或者 已经被取消权限判断
     */
    @Column(nullable = false, length = 6)
    private int status;

}

三、Role 类

Role 主要是配置和管理有那些求权限,同时就是关联着相应的管理员用户。

@Entity
@Table(name = "T_ROLE")
public class Role {

    public static final int STATUS_DISABLE = 0;

    public static final int STATUS_ENABLE = 1;

    @Id
    @GeneratedValue
    @Column(nullable = false)
    private Long roleId;

    /**
     * 角色
     */
    @Column(nullable = false)
    private String name;

    /**
     * 角色描述
     */
    @Column(nullable = false,length = 300)
    private String description;

    /**
     * 角色状态
     */
    @Column(nullable = false)
    private int status;

    /**
     * 角色所对应的权限
     */
    @ManyToMany(targetEntity = Action.class)
    @JoinTable(name = "T_ACTION_ROLE", joinColumns = {@JoinColumn(name = "ROLE_ID")}, inverseJoinColumns = {@JoinColumn(name = "ACTION_ID")})
    private Set<Action> actions;

    @OneToMany(mappedBy = "role")
    private Set<AdminUser> adminUsers;

    /**
     * 创建角色的时间
     */
    @Column(nullable = false)
    private Date createTime;

}

分析

目前我们要做到动态权限要做到几个地方:

  1. 做到接口即Action,定义了接口就等于定义了相关的Action功能,之后我们会通过这个Action去控制权限;
  2. 需要配置菜单,动态关联上对应的Action;
  3. 每次启动服务,可以检查整理出最新的action列表(与你的接口变更一致)和 menu列表;
  4. 用户登录的时候,将用户对应角色拥有的菜单和对应的action 组织成一个权限树,返回给前端渲染菜单和按钮;
  5. 用户请求接口时候,将会通过AOP读取接口定义的ActionId,通过用户对应关系获得该用户的角色是否有权限访问该接口;
  6. 提供接口配置相应的角色权限;

权限配置定义和读取

一、Action配置

我们会定义一个annotation,通过annotation配置到对应的接口上,用作actionId定义和AOP校验的切入点。

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Permission {

    String actionId();

    String menuId();

    /**
     * 功能名称
     * @return
     */
    String name();

}

另外还有一个是不检查权限,只是需要当前用户一定是已经登录并且可以获得其用户信息。我将会定义另外一个annotation进行判断


@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Authorization {
}

定义完成这两个annotation之后,我将使用这两个annotation定义在ResController对应的方法当中。使用方法如下:

@RestController
@RequestMapping("/decorate")
public class DecorateController {
    @Autowired
    private IDecorateService decorateService;

    
    @GetMapping("/view")
    @Permission(actionId = "decorateManage_viewBtn", menuId = "decorateManage", name = "查看小程序装修")
    public ResponseBasic<DecorateRsp> view() {
        return ResponseBasic.ok(decorateService.view());
    }
}

可以看到我在/view 这个接口上面,定义了一个名称为 查看小程序装修 actionId 为 decorateManage_viewBtn 属于 decorateManage 菜单的权限,这个权限将会是全局唯一的。同时我们这个actionId 在用户登录的时候用户拥有什么权限actionId 会全部给前端渲染页面。

二、菜单配置

我们需要定义个菜单配置的类,其实只是配置一个bean将所有配置的菜单保存起来

/**
 * Created by TONY YAN
 */
public class PermissionConfig {

    private Set<Menu> menus = new HashSet<>();

    public Set<Menu> getMenus() {
        return this.menus;
    }

    public PermissionConfig addMenu(Menu menu) {
        menus.add(menu);
        return this;
    }

}

然后就开始配置菜单,在其中一个配置类里面配置定义好的PermissionConfig,定义好各个菜单的名称和menuId,还有就是就是父级的menuId

@Bean
    public PermissionConfig menuDefinition() {
        PermissionConfig permissionConfig = new PermissionConfig();
        permissionConfig.addMenu(new Menu("staffManageIndex", "员工管理", null));
        permissionConfig.addMenu(new Menu("roleManage", "角色管理", "staffManageIndex"));
        permissionConfig.addMenu(new Menu("staffManage", "员工管理", "staffManageIndex"));

        permissionConfig.addMenu(new Menu("storeManage", "门店管理", "staffManageIndex"));
        permissionConfig.addMenu(new Menu("orderManageIndex", "订单管理", null));
        permissionConfig.addMenu(new Menu("orderSurvey", "订单概况", "orderManageIndex"));
        permissionConfig.addMenu(new Menu("orderManage", "所有订单", "orderManageIndex"));
        permissionConfig.addMenu(new Menu("goodsManage", "商品管理", null));
        permissionConfig.addMenu(new Menu("memberManage", "会员管理", null));
        permissionConfig.addMenu(new Menu("decorateManage", "小程序装修", null));
        permissionConfig.addMenu(new Menu("cardManage", "礼品卡管理", null));
        return permissionConfig;
    }

三、配置读取生成权限表

OK,那我们还需要提供一个借口给前端用户,选择某个角色应该拥有什么权限。所以我们需要每次启动SpringBoot应用的时候,将所有其本身定义的所有权限action给记录起来。需要完成这一步的思路是这样的,我们需要在SpringBoot启动后,然后扫描Spring容器里面所有的Bean找到对应的@Permission的配置。上代码:

1、我们需要实现initializingBean接口,在afterPropertiesSet 方法里面获得Spring的context,然后通过获得所有的RestController类的Bean。

/**
 * Created by TONY YAN
 */
@Component
public class PermissionInitializing implements InitializingBean {

    @Autowired
    private ApplicationContext context;

    @Autowired
    private PermissionService permissionService;

    @Override
    public void afterPropertiesSet() throws Exception {
        Map<String, Object> beans = this.context.getBeansWithAnnotation(RestController.class);
        this.permissionService.permissionInit(beans.values());
    }

}

2、permissionInit这个方法就是读取配置的关键

/**
 * Created by TONY YAN
 */
@Service
@Transactional
public class PermissionService {

    @Autowired
    private ActionRepository actionRepository;

    @Autowired
    private MenuRepository menuRepository;

    @Autowired
    private PermissionConfig permissionConfig;

    @Autowired
    private JdbcTemplate jdbcTemplate;

    private Logger logger = LoggerFactory.getLogger(PermissionService.class);

    /**
     * 权限表自动化生成
     * @param beans
     * @throws Exception
     */
    public void permissionInit(Collection<Object> beans) throws Exception {
        List<Action> actions = new LinkedList<>();
        this.menuRepository.setAllAMenuDisable();
        this.actionRepository.setAllActionDisable();
        for (Object obj : beans) {
            Object targetObj = ProxyUtils.getTarget(obj);
            List<Action> actionListByClz = this.ingestActionList(targetObj.getClass());
            actions.addAll(actionListByClz);
        }
        ambiguityActionCheck(actions);
        this.actionRepository.save(actions);
        Set<Menu> menus = this.permissionConfig.getMenus();
        checkActionOutOfMenu(actions, menus);
        this.menuRepository.save(menus);
        checkAndRemoveDisableMenu();
        checkAndRemoveDisableAction();
        logger.info("Lotus permission Initialized");
    }

    /**
     * 检查action权限对应的菜单是否存在
     * @param actions
     * @param menus
     */
    public void checkActionOutOfMenu(List<Action> actions, Set<Menu> menus) {
        Set<String> menuIdSet = menus.stream().map(menu -> menu.getMenuId()).collect(Collectors.toSet());
        for (Action action : actions) {
            if (!menuIdSet.contains(action.getMenuId())) {
                logger.warn("Lotus permission Initializing warning : Action id -> "
                        + action.getActionId() + " is out of any menus by configuration. " +
                        " Cause menuID -> " + action.getMenuId() + " undefined ." +
                        " ActionInfo = "
                        + action.getActionName() + "[" + action.getActionUri() + "] ");
            }
        }

    }

    /**
     * 将所有已经被移除掉的menu全部删掉
     */
    public void checkAndRemoveDisableMenu() {
        this.menuRepository.deleteByStatus(0);
    }

    /**
     * 删除所有已经没有用的action、action和角色多对多的关联。
     */
    public void checkAndRemoveDisableAction() {
        List<Action> actions = this.actionRepository.findByStatus(0);
        for (Action action : actions) {
            jdbcTemplate.execute("delete from t_action_role where ACTION_ID = '" + action.getActionId() + "'");
        }
        this.actionRepository.deleteByStatus(0);
    }




    /**
     * 所有actionID的全局唯一性检查,如果有冲突马上抛异常阻止SpringBoot初始化启动
     * @param actions
     */
    private void ambiguityActionCheck(List<Action> actions) {
        Map<String, Integer> checkMap = new HashMap<>();
        for (Action action : actions) {
            Integer counter = checkMap.get(action.getActionId());
            if (counter == null) {
                checkMap.put(action.getActionId(), 1);
            } else {
                if (++counter > 1) {
                    throw new PermssionConfigException("Permission config error actionId ambiguity , more then one " + action.getActionId());
                }
            }
        }
    }

    /**
     * 通过一个RESTController 类获得所有的action对象列表
     * @param clz
     * @return
     */
    private List<Action> ingestActionList(Class clz) {
        Method[] methodArray = clz.getDeclaredMethods();
        List<Action> actions = new ArrayList<>();
        for (Method method : methodArray) {
            if (method.getAnnotation(Permission.class) != null) {
                Action action = this.ingestAction(method);
                actions.add(action);
            }
        }
        return actions;
    }

    /**
     * 通过一个RESTController的方法,获得一个Action对象
     * @param method
     * @return
     */
    private Action ingestAction(Method method) {
        Permission permission = method.getAnnotation(Permission.class);
        if (permission == null) {
            return null;
        }
        Action action = new Action();
        action.setActionUri(ingestActionUri(method));
        action.setActionName(permission.name());
        action.setMenuId(permission.menuId());
        action.setActionId(permission.actionId());
        action.setStatus(Action.STATUS_ENABLE);
        return action;
    }

    /**
     * 获得URI
     * @param method
     * @return
     */
    private String ingestActionUri(Method method) {
        String methodUriPart = "";
        GetMapping getMapAn = method.getAnnotation(GetMapping.class);
        if (getMapAn != null) {
            String[] paths = getMapAn.path() == null || getMapAn.path().length <= 0 ? getMapAn.value() : getMapAn.path();
            methodUriPart = paths != null && paths.length > 0 ? paths[0] : "";
        }
        RequestMapping reqMapAn = method.getAnnotation(RequestMapping.class);
        if (reqMapAn != null) {
            String[] paths = reqMapAn.path() == null || reqMapAn.path().length <= 0 ? reqMapAn.value() : reqMapAn.path();
            methodUriPart = paths != null && paths.length > 0 ? paths[0] : "";
        }
        PostMapping postMapAn = method.getAnnotation(PostMapping.class);
        if (postMapAn != null) {
            String[] paths = postMapAn.path() == null || postMapAn.path().length <= 0 ? postMapAn.value() : postMapAn.path();
            methodUriPart = paths != null && paths.length > 0 ? paths[0] : "";
        }
        String classUriPart = "";
        RequestMapping clzReqMapAn = method.getDeclaringClass().getAnnotation(RequestMapping.class);
        if (clzReqMapAn != null) {
            String[] paths = clzReqMapAn.path() == null || clzReqMapAn.path().length <= 0 ? clzReqMapAn.value() : clzReqMapAn.path();
            classUriPart = paths != null && paths.length > 0 ? paths[0] : "";
        }
        boolean hasSeparator = false;
        boolean moreThenOneSeparator = false;
        if (!StringUtils.isEmpty(methodUriPart)) {
            hasSeparator = methodUriPart.indexOf("/") == 0;
        }
        if (!StringUtils.isEmpty(classUriPart)) {
            if (hasSeparator && classUriPart.indexOf("/") == classUriPart.length() - 1) {
                moreThenOneSeparator = true;
            } else if (!hasSeparator) {
                hasSeparator = classUriPart.indexOf("/") == classUriPart.length() - 1;
            }
        }
        String uri = "";
        if (!StringUtils.isEmpty(methodUriPart) || !StringUtils.isEmpty(classUriPart)) {
            if (moreThenOneSeparator) {
                methodUriPart = methodUriPart.substring(1, methodUriPart.length());
                uri = classUriPart + methodUriPart;
            } else if (hasSeparator) {
                uri = classUriPart + methodUriPart;
            } else {
                uri = classUriPart + "/" + methodUriPart;
            }
            if (uri.indexOf("/") != 0) {
                uri = "/" + uri;
            }
        }
        return uri;
    }

}

虽然有点长但是还是算比较简单,然后说说这个permissionInit方法干了什么。首先将以前旧的所有Action和Menu表的数据设置成无效,然后开始迭代所有bean,在这里有个地方需要注意的Object targetObj = ProxyUtils.getTarget(obj); 。其中ProxyUtils的类后面会贴出来,意思是这样的,首先我们在Spring容器获得的所有Bean基本上都是被CGLib 或者 JdkDynamicProxy代理过的,所以我们这边获得的所有都是代理对象,导致我们获得class对象都是一个代理类,导致我们没有办法读取到原始类里面的@permission配置。所以通过ProxyUtils 获得真实的原来对象。

然后通过ingestActionList 方法读取出类里面的所有@Permission配置,然后通过ambiguityActionCheck方法检查其权限是否冲突,如果冲突就抛出异常(消息可以看看其代码),然后将权限保存到表当中,由于生成的权限status都是在生效状态,等于如果是上次启动的时候已经存在的权限,在这次保存的时候得到更新从失效改成了生效,而新的权限记录将会插入一条数据。

通过PermissionConfig 对象(之前配置过的),获得所有的菜单对象,并且保存。也是同样道理以前已经存在的菜单对象在这次从新保存的时候得到了更新从一个开始的失效状态,更新成生效状态。

检查@Permission配置的Action定义的所属菜单是否存在,如果没有对应menuId的菜单对象,就给出warning的日志消息。

然后将所有状态为失效的Action和Menu记录删除,保持最终的权限条目结果。然后完成初始化,下面帖出ProxyUtils的代码:

public class ProxyUtils {

    public static Object getTarget(Object proxy) throws Exception {
        if (!AopUtils.isAopProxy(proxy)) {
            return proxy;
        }
        if (AopUtils.isJdkDynamicProxy(proxy)) {
            return getJdkDynamicProxyTargetObject(proxy);
        } else { 
            return getCglibProxyTargetObject(proxy);
        }
    }


    private static Object getCglibProxyTargetObject(Object proxy) throws Exception {
        Field h = proxy.getClass().getDeclaredField("CGLIB$CALLBACK_0");
        h.setAccessible(true);
        Object dynamicAdvisedInterceptor = h.get(proxy);
        Field advised = dynamicAdvisedInterceptor.getClass().getDeclaredField("advised");
        advised.setAccessible(true);
        Object target = ((AdvisedSupport) advised.get(dynamicAdvisedInterceptor)).getTargetSource().getTarget();
        return target;
    }


    private static Object getJdkDynamicProxyTargetObject(Object proxy) throws Exception {
        Field h = proxy.getClass().getSuperclass().getDeclaredField("h");
        h.setAccessible(true);
        AopProxy aopProxy = (AopProxy) h.get(proxy);
        Field advised = aopProxy.getClass().getDeclaredField("advised");
        advised.setAccessible(true);
        Object target = ((AdvisedSupport) advised.get(aopProxy)).getTargetSource().getTarget();
        return target;
    }

}

提供接口配置角色

目前已经拥有菜单和功能的基础条目了,先就差某个角色拥有什么功能这样的一个功能了。首先我们需要提供一个功能让前端获得整体一个权限树构成,就是说有那些菜单,里面有什么权限和什么子菜单这样一个结构。其中我们需要提供所有全局权限树,还有就是角色的权限树,还有就是用户的权限树等等。这里就简单贴出代码,不算是这里的核心思想。只要清楚数据库结构,就能简单实现界面所需要的接口。

@Service
public class PermissionTreeService {

    @Autowired
    private ActionRepository actionRepository;

    @Autowired
    private MenuRepository menuRepository;

    @Autowired
    private PermissionConfig permissionConfig;

    @Autowired
    private JdbcTemplate jdbcTemplate;

    private Logger logger = LoggerFactory.getLogger(PermissionTreeService.class);

    /**
     * 获得角色所有的权限actionID
     * @param role
     * @return
     */
    public Set<String> getUserActionIdList(Role role) {
        if (role == null) {
            return new HashSet<>();
        }
        Set<Action> actions = role.getActions();
        Set<String> actionIds = actions.stream().map(action -> action.getActionId()).collect(Collectors.toSet());
        return actionIds;
    }

    /**
     * 获得用户所有的权限actionID
     * @param adminUser
     * @return
     */
    public Set<String> getUserActionIdList(AdminUser adminUser) {
        if (adminUser == null) {
            return new HashSet<>();
        }
        Set<Action> actions = null;
        if (adminUser.getSuperAdmin() == 99) {
            actions = this.actionRepository.findAll().stream().collect(Collectors.toSet());
        } else {
            actions = adminUser.getRole().getActions();
        }
        Set<String> actionIds = actions.stream().map(action -> action.getActionId()).collect(Collectors.toSet());
        return actionIds;
    }

    /**
     * 获得用户的权限树
     * @param adminUser
     * @return
     */
    public List<MenuVo> userPermissionTree(AdminUser adminUser) {
        List<MenuVo> menuVos = this.getActionTree(adminUser);
        return this.menuFilterLayer(menuVos);
    }

    /**
     * 获得角色用户的权限树
     * @param role
     * @return
     */
    public List<MenuVo> userPermissionTree(Role role) {
        List<MenuVo> menuVos = this.getActionTree(role);
        return this.menuFilterLayer(menuVos);
    }


    /**
     * 菜单过滤器层
     * @param menuVos
     * @return
     */
    public List<MenuVo> menuFilterLayer(List<MenuVo> menuVos) {
        List<MenuVo> result = new ArrayList<>();
        for (MenuVo menuVo : menuVos) {
            if (hasMenusPermission(menuVo)) {
                if (menuVo.getSubMenus() != null && menuVo.getSubMenus().size() > 0) {
                    menuVo.setSubMenus(menuFilterLayer(menuVo.getSubMenus()));
                }
                menuVo.setActions(actionFilterLayer(menuVo.getActions()));
                result.add(menuVo);
            }
        }
        return result;
    }

    /**
     * action过滤器层
     * @param actionList
     * @return
     */
    public List<ActionVo> actionFilterLayer(List<ActionVo> actionList) {
        List<ActionVo> result = actionList.stream().filter(actionVo -> actionVo.getHasPermission() == 1).collect(Collectors.toList());
        return result;
    }


    public boolean hasMenusPermission(MenuVo menuVo) {
        List<ActionVo> actionVos = menuVo.getActions();
        for (ActionVo actionVo : actionVos) {
            if (actionVo.getHasPermission() != 0) {
                return true;
            }
        }
        if (menuVo.getSubMenus() != null && menuVo.getSubMenus().size() > 0) {
            for (MenuVo subVo : menuVo.getSubMenus()) {
                if (hasMenusPermission(subVo)) {
                    return true;
                }
            }
        }
        return false;
    }

    public List<MenuVo> getActionTree(Role role) {
        Set<String> userActionSet = this.getUserActionIdList(role);
        return getActionTreeCore(userActionSet);
    }

    public List<MenuVo> getActionTree(AdminUser adminUser) {
        Set<String> userActionSet = this.getUserActionIdList(adminUser);
        return getActionTreeCore(userActionSet);
    }

  
    public List<MenuVo> getActionTreeCore(Set<String> userActionSet) {
        List<Menu> menus = this.menuRepository.findAll();
        List<Action> actions = this.actionRepository.findAll();
        List<MenuVo> vos = new ArrayList<>();
        for (Menu menu : menus) {
            if (menu.getParentMenuId() == null) {
                MenuVo vo = new MenuVo();
                CommonUtils.copyProperty(menu, vo);
                findSubMenus(vo, menus, actions, userActionSet);
                findActionInMenu(vo, actions, userActionSet);
                vos.add(vo);
            }
        }
        return vos;
    }


    public void findActionInMenu(MenuVo menuVo, List<Action> actions, Set<String> userActionIdSet) {
        List<ActionVo> vos = new ArrayList<>();
        for (Action action : actions) {
            if (action.getMenuId().equals(menuVo.getMenuId())) {
                ActionVo actionVo = new ActionVo();
                CommonUtils.copyProperty(action, actionVo);
                actionVo.setHasPermission(userActionIdSet.contains(action.getActionId()) ? 1 : 0);
                vos.add(actionVo);
            }
        }
        menuVo.setActions(vos);
    }

    /**
     * 获得子菜单单
     * @param menuVo
     * @param menus
     * @param actions
     * @param userActionIdSet
     */
    public void findSubMenus(MenuVo menuVo, List<Menu> menus, List<Action> actions, Set<String> userActionIdSet) {
        List<MenuVo> subMenuVoList = new ArrayList<>();
        for (Menu menu : menus) {
            if (menu.getParentMenuId() == null) {
                continue;
            }
            if (menu.getParentMenuId().equals(menuVo.getMenuId())) {
                MenuVo subVo = new MenuVo();
                CommonUtils.copyProperty(menu, subVo);
                subMenuVoList.add(subVo);
            }
        }
        if (subMenuVoList.size() > 0) {
            menuVo.setSubMenus(subMenuVoList);
            for (MenuVo vo : subMenuVoList) {
                findActionInMenu(vo, actions, userActionIdSet);
                findSubMenus(vo, menus, actions, userActionIdSet);
            }

        }
    }

}

提供具体的权限和角色绑定的接口这个也是比较简单的业务了,下面也简单贴出来一下。哎哟提醒一下我这里用了大量的深拷贝。

    public ResponseBasic createRole(ReqRoleVo req) {
        Role role = new Role();
        if (this.roleRepository.findByName(req.getName()) != null) {
            return ResponseBasic.error(ResponseBasic.ERROR_CODE, "角色名称名已经存在");
        }
        role.setCreateTime(new Date());
        req.setRoleId(null);
        CommonUtils.copyProperty(req, role);
        role.setStatus(Role.STATUS_ENABLE);
        Set<Action> actions = this.actionRepository.findByActionIdIn(req.getActionIdList());
        role.setActions(actions);
        this.roleRepository.save(role);
        return ResponseBasic.ok();
    }

    public ResponseBasic<RoleDetailVo> getRoleDetail(ReqGetRole req) {
        Role role = this.roleRepository.findOne(req.getRoleId());
        RoleDetailVo detailVo = new RoleDetailVo();
        CommonUtils.copyProperty(role, detailVo);
        List<MenuVo> permissionTree = this.permissionTreeService.getActionTree(role);
        detailVo.setPermissionTree(permissionTree);
        return ResponseBasic.ok(detailVo);
    }

    public ResponseBasic<List<MenuVo>> getPermissionTree() {
        List<MenuVo> permissionTree = this.permissionTreeService.getActionTreeCore(new HashSet<>());
        return ResponseBasic.ok(permissionTree);
    }


    public ResponseBasic<AppPage<SimpleRoleVo>> getRoleList(ReqSearchRole req) {
        Page<Role> page = this.roleRepository.findAll(new PageRequest(req.getPageNo() - 1, req.getPageSize()));
        Page<SimpleRoleVo> vos = page.map(new Converter<Role, SimpleRoleVo>() {
            @Override
            public SimpleRoleVo convert(Role role) {
                SimpleRoleVo vo = new SimpleRoleVo();
                CommonUtils.copyProperty(role, vo);
                vo.setStatusName(role.getStatus() == 0 ? "禁用" : "正常");
                vo.setNumOfUser(userRepository.countByRole(role));
                return vo;
            }
        });
        return ResponseBasic.ok(new AppPage<>(vos));
    }


    public ResponseBasic updateRole(ReqRoleVo req) {
        Role role = this.roleRepository.findOne(req.getRoleId());
        if (role == null) {
            return ResponseBasic.error(ResponseBasic.ERROR_CODE, "角色不存在");
        }
        CommonUtils.copyProperty(req, role);
        Set<Action> actions = this.actionRepository.findByActionIdIn(req.getActionIdList());
        role.setActions(actions);
        this.roleRepository.save(role);
        return ResponseBasic.ok();
    }

    public ResponseBasic<List<RoleItemVo>> getSimpleRoleList() {
        List<Role> roles = this.roleRepository.findAll(new Sort("createTime"));
        if (roles != null) {
            List<RoleItemVo> roleItemVos = roles.stream().map(role -> {
                RoleItemVo itemVo = new RoleItemVo();
                itemVo.setRoleId(role.getRoleId());
                itemVo.setRoleName(role.getName());
                return itemVo;
            }).collect(Collectors.toList());
            return ResponseBasic.ok(roleItemVos);
        }
        return ResponseBasic.ok(new ArrayList<>());
    }

    public ResponseBasic<List<RoleListItemVo>> getRoleListPermissionTree() {
        List<Role> roles = this.roleRepository.findAll(new Sort(Sort.Direction.DESC, "createTime"));
        List<RoleListItemVo> roleListItemVoList = new ArrayList<>();
        for (Role role : roles) {
            RoleListItemVo listItemVo = new RoleListItemVo();
            CommonUtils.copyProperty(role, listItemVo);
            listItemVo.setRoleName(role.getName());
            listItemVo.setPermissionTree(this.permissionTreeService.userPermissionTree(role));
            roleListItemVoList.add(listItemVo);
        }
        return ResponseBasic.ok(roleListItemVoList);
    }

权限校验

说了那么多应该都到权限校验这一部分了,首选我们是通过AOP实现权限校验的。同时我们使用redis保存用户的Token令牌,然而我们前端会通过Http header 的authorization 参数将token带会来进行校验。而且我们会分成这几部分去完成整一个权限校验。

  1. 通过实现HandlerInterceptor接口,在preHandle方法当中获得其令牌信息并转换成用户对象,保存在ThreadLocal当中;
  2. 在HandlerInterceptor接口的 afterCompletion 方法当中删除ThreadLocal的用户信息缓存,防止内存泄露;
  3. 提供统一的封装方法,从而在同一个请求线程当中获得其用户对象;
  4. 通过@Aspect 配置AOP的@Around环绕织入,将所有的@Permission的方法进行AOP动态代理;
  5. 检查其ThreadLocal是否获得已经有用户的对象,如果没有则未登录,直接返回错误或者抛出指定异常;
  6. 获得被织入的方法的@Permission annotation配置的ActionId 参数值,同时检查用户对象的ActionId SET里面是否包含其ActionId,如果没有包含,返回错误或者抛出指定异常,否则正常执行处理;
  7. 同样道理@Authorization的annotation也会进行AOP代理,但是不会检查用户的权限列表,只会去看用户登录了没有,如果没有则返回错误或者抛出指定的异常;

好啦,就是核心的几个类。我这边贴出来。。。

第一个就是AuthInterceptor 和 AuthService,其中AuthService支持在任何地方进行注入,通过他可以获得当前登录的用户对象。

/**
 * Created by TONY YAN
 */
@Component
public class AuthInterceptor implements HandlerInterceptor {

    @Autowired
    private AuthService authService;

    @Override
    public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception {
        this.authService.setAdminUserInCache(httpServletRequest);
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {
    }

    @Override
    public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {
        this.authService.removeAdminUserInCahce();
    }
}

AuthService类其中在获得用户令牌的方法当中,也会调用到PermissionTreeService的方法去获得当前用户的角色权限树和ActionId列表,详细请看代码注释。

public class AuthService {

    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private PermissionTreeService permissionTreeService;

    public static long expireDays = 20;

    private static ThreadLocal<AuthToken> USER_THREAD_LOCAL = new ThreadLocal<>();


    /**
     * 将根据token获得用户对象并存储到threadLocal当中
     *
     * @param request
     */
    public void setAdminUserInCache(HttpServletRequest request) {
        String token = request.getParameter("Authorization");

        if (StringUtils.isEmpty(token)) {
            token = request.getHeader("Authorization");
        }
        if (StringUtils.isEmpty(token)) {
            return;
        }
        AuthToken user = this.getUserByToken(token);
        if (user == null) {
            return;
        }
        user.setToken(token);
        USER_THREAD_LOCAL.set(user);
    }

    public void removeAdminUserInCahce() {
        USER_THREAD_LOCAL.remove();
    }


    /**
     * 获得用户令牌
     *
     * @param user
     * @return
     */
    public AuthToken getUserTokenAndSetUser(AdminUser user) {
        ValueOperations<String, AuthToken> valOps = this.redisTemplate.opsForValue();
        String token = UUID.randomUUID().toString();
        String key = this.getRedisUserKey(token);
        AuthToken authToken = new AuthToken();
        CommonUtils.copyProperty(user, authToken);
        authToken.setActionIds(this.permissionTreeService.getUserActionIdList(user));
        authToken.setMenuVos(this.permissionTreeService.userPermissionTree(user));
        authToken.setToken(token);
        valOps.set(key, authToken, expireDays, TimeUnit.MINUTES);
        return authToken;
    }


    /**
     * 获得缓存用户
     *
     * @return
     */
    public AuthToken getUserByToken() {
        return USER_THREAD_LOCAL.get();
    }


    /**
     * 获得缓存用户
     *
     * @param userToken
     * @return
     */
    public AuthToken getUserByToken(String userToken) {
        ValueOperations<String, AuthToken> valOps = this.redisTemplate.opsForValue();
        String key = this.getRedisUserKey(userToken);
        AuthToken user = valOps.get(key);
        if (user != null) {
            this.redisTemplate.expire(key, expireDays, TimeUnit.DAYS);
        }
        return user;
    }


    /**
     * 从redis当中移除用户
     */
    public void removeAdminUserInRedis() {
        AuthToken user = this.getUserByToken();
        if (user == null) {
            return;
        }
        String key = this.getRedisUserKey(user.getToken());
        this.redisTemplate.delete(key);
    }

    /**
     * 获得用户token缓存的key
     *
     * @param userToken
     * @return
     */
    public String getRedisUserKey(String userToken) {
        return PermissionContacts.ADMIN_USER_TOKEN + userToken;
    }

}

然后就是核心的PermissionAspect 类,主要是做相关的拦截权限校验操作。然后你会看到我偷鸡,只要获得当前用户的superAdmin字段是99的话,我就会赋予全部权限。这是为什么了,主要是系统在初始化的时候没有用户,那这样也会妨碍到角色的配置问题,所以才在这个位置做出取巧的一个方法,当首次配置后,直接将这个用户的superAdmin去掉然后绑定到其中一个超级管理员权限的角色即可。

/**
 * Created by TONY YAN
 */
@Component
@Aspect
public class PermissionAspect {

    @Autowired
    private AuthService authService;

    /**
     * 拦截所有@Permission的方法,检查其当前用户是否有权限访问
     * @param joinPoint
     * @return
     * @throws Throwable
     */
    @Around("@annotation(com.lotus.admin.permission.annotation.Permission)")
    public Object check(ProceedingJoinPoint joinPoint) throws Throwable {
        AuthToken authToken = this.authService.getUserByToken();
        if (authToken == null) {
            return ResponseBasic.error(ResponseBasic.AUTH_ERROR_CODE, "用户未登陆");
        }
        String actionId = this.getPermissionMethod(joinPoint).getAnnotation(Permission.class).actionId();
        boolean ok = false;
        if (authToken.getSuperAdmin() == 99) {
            ok = true;
        } else {
            if (authToken.getActionIds().contains(actionId)) {
                ok = true;
            }
        }
        if (ok) {
            return joinPoint.proceed(joinPoint.getArgs());
        }
        return ResponseBasic.error(ResponseBasic.PERMISSION_ERROR_CODE, "你没有权限使用该功能");
    }

    /**
     * 拦截所有@Authorization的方法,检查当前调用方法的线程是否已经获得其用户对象,如果没有则未登录
     * @param joinPoint
     * @return
     * @throws Throwable
     */
    @Around("@annotation(com.lotus.admin.permission.annotation.Authorization)")
    public Object authCheck(ProceedingJoinPoint joinPoint) throws Throwable {
        AuthToken authToken = this.authService.getUserByToken();
        if (authToken == null) {
            return ResponseBasic.error(ResponseBasic.AUTH_ERROR_CODE, "用户未登陆");
        }
        return joinPoint.proceed(joinPoint.getArgs());
    }

    /**
     * 获得方法对象
     * @param joinPoint
     * @return
     * @throws NoSuchMethodException
     */
    private Method getPermissionMethod(ProceedingJoinPoint joinPoint) throws NoSuchMethodException {
        Signature sig = joinPoint.getSignature();
        MethodSignature msig = null;
        if (!(sig instanceof MethodSignature)) {
            throw new IllegalArgumentException("该注解只能用于方法");
        }
        msig = (MethodSignature) sig;
        Object target = joinPoint.getTarget();
        Method currentMethod = target.getClass().getMethod(msig.getName(), msig.getParameterTypes());
        return currentMethod;
    }

}

登录

其实我不打算在这里贴出登录的代码的,也没有什么好贴出来的。反正AuthService已经有获得令牌的代码了,就差用户名校验这些逻辑了估计大家都会了,我就不打算贴出了其实登录这里的核心代码就是调用了AuthService的public AuthToken getUserTokenAndSetUser(AdminUser user) 方法而已。


结尾

这个权限框架,也是解决了很多业务上的痛点。也算是写了这么多年代码的一个经验总结吧。其实还是有很多需要进步的东西的,因为也没有写多久才用了一周不到的时间完成。其中有如下的优化想法:

  1. 其中可以做一个权限依赖的功能,将一个权限依赖某一个权限,这样的导致用户定义了用户有编辑权限当时没有查看权限的这种尴尬情况;
  2. 然后也可以做一下intellij IDE插件将权限的actionID全局检查在编写代码的时候进行动态检查,进一步防止ID冲突;
  3. 还可以做一个单独的微服务业务,通过Spring初始化的阶段将本服务的信息传递给权限认证服务进行认证检查和冲突检查等,统一管理整个系统的权限;
  4. 除了支持按钮级别的接口和按钮的显示控制之外,还可以支持开放的服务接口的权限配置;
  5. 再进一步就是可以通过统一拦截repository的访问,通过统一的基类Entity 通过统一数据权限字段去管理数据级别的权限检验,做到业务开发不需要感知判断当前用户的权限直接调用repository 进行数据查询,在repository层中将拦截自动加上权限数据筛选的条件,从而做到数据级别的权限。
  6. 可以将这个东西进一步封装成一个通用的微服务权限认证框架,这是一个伟大的梦想。。。。

 

好啦,有什么就留言我吧。。。绝对原创转载请注明出处。。。

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章