关于博客中使用的Guns版本问题请先阅读 Guns二次开发目录
到此篇博客为止,前面的一系列博客中,我们实现了商品分类管理模块的增删改查,演示了如何在Guns源码的基础上来开发我们自己的业务模块。当然了,这还远没有结束,我们还需要将Guns原有的权限控制功能应用到我们写的一系列后端接口中。Guns的权限系统对模块的权限控制分为前后端两部分,前端控制按钮(比如【添加】、【修改】和【删除】等按钮)是否显示出来,后端代码则是通过加一个注解来控制当前角色是否有权限访问这个接口,假设用户当前的角色没有权限访问某个接口,却又访问了这个接口时,在有权限控制的时候,就会控制这种越权行为。
category.html 页面中,下图的红框中的代码则是对这些按钮做了权限判断的,只有当前用户拥有访问这些接口的权限,才会展示这些按钮:
为了方便本篇博客的介绍,我们不妨先设置当前角色(我当前登录的是管理员角色) 没有访问【启用/停用】按钮的权限。设置方法如下:
修改之后,退出当前账号后再重新登录:
然后再访问分类管理页面,发现【启用/停用】按钮没有显示了:
因为guns 自带的权限控制系统,不仅是在前端需要做控制,后端接口也需要做控制,为了方便介绍,此处我们不妨在前端制造一个bug,在前端不做控制的情况下,由后端来做限制,看看是否能够起到保护资源的的目的。
前端页面中,对【启用/停用】按钮不做权限控制:
因为此时我的后端接口是没有做权限控制的,所以如果此时点击【启用/停用】按钮,请求是依旧能够发送成功的。所以接下来我们要在后端接口加上权限控制,方法很简单,只需要加上一个@Permission注解,这个注解所在的包是 cn.stylefeng.guns.core.common.annotion.Permission 。
这是效果,可以发现后端权限校验生效了:
权限注解的核心代码在这个类中,内部逻辑很简单,有需要的可以自己去读源码或者debug查看流程
有这么一个场景:后台管理系统用户张三拥有【超级客服】的角色,而【超级客服】拥有操作资源A和资源B的权限,某天张三把超级管理员李四给得罪了,李四很不爽,于是把张三降为【普通客服】,而普通客服只拥有操作资源B的权限。可是这时候问题来了,李四在把张三降为【普通客服】的前一秒,张三还处于账号登录状态并且还拥有【超级客服】的权限,当李四成功把张三降级后,查看登录日志,发现张三还能操作资源A。李四没有办法,总不能拉下脸让张三自己退出后再重新登录,无奈之下只能重启服务器。但是这个操作无疑得罪的就不只是张三一人了。
这其实也正是当前版本的Guns的权限系统的一个问题,更确切的说,这是Shiro权限框架的一个硬伤——无法动态更新用户权限。我们不妨来查看Guns的源码来更直观的发现问题。
看过guns相关视频的人都知道@Permission的实现原理(视频链接在文章开头的guns目录中有),PermissionAop.java文件中则是对@Permission权限注解的具体实现。
经过debug,可以发现checkAll()函数才是权限校验的核心逻辑:
实现逻辑在类PermissionCheckServiceServiceImpl.java中:
然后,一路debug可以发现 ,ShiroKit.hasPermission()方法才是检验用户是否拥有访问指定权限,走到这里的时候,可以发现,校验用户权限不是从数据库查询的,而是从Subject对象中获取的,而Subject对象中保存的权限信息,则是在登录时从数据库查询到的,而登录之后发生的权限更新信息,是无法同步更到Subject对象中的,这也是为什么Guns无法动态更新用户权限的原因。
既然知道了问题产生的原因,那就开始解决问题了。我的解决方案是:在获取用户权限的时候,不从Subject对象中获取,而是每次都去数据库中查询。这时又会产生一个问题,那就是每次访问一个接口,都得去数据库查询用户的权限,这样无疑会增加服务器的压力和增大每个接口的处理时间。鉴于此,我便引入了redis来做mybatis-plus的二级缓存,以此来提高查询效率。
修改checkAll()方法的内部实现逻辑:
开启一下几个文件的二级缓存:
注:guns中开启mybatis-plus二级缓存的方式,请看这篇文章:
PermissionCheckServiceServiceImpl.java
/**
* Copyright 2018-2020 stylefeng & fengshuonan (https://gitee.com/stylefeng)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.stylefeng.guns.core.shiro.service.impl;
import cn.hutool.core.collection.CollectionUtil;
import cn.stylefeng.guns.core.listener.ConfigListener;
import cn.stylefeng.guns.core.shiro.ShiroKit;
import cn.stylefeng.guns.core.shiro.ShiroUser;
import cn.stylefeng.guns.core.shiro.service.PermissionCheckService;
import cn.stylefeng.guns.elephish.utils.DBUtil;
import cn.stylefeng.guns.elephish.utils.StringUtil;
import cn.stylefeng.guns.modular.system.dao.MenuMapper;
import cn.stylefeng.guns.modular.system.dao.RelationMapper;
import cn.stylefeng.guns.modular.system.dao.RoleMapper;
import cn.stylefeng.guns.modular.system.dao.UserMapper;
import cn.stylefeng.guns.modular.system.model.Menu;
import cn.stylefeng.guns.modular.system.model.Relation;
import cn.stylefeng.guns.modular.system.model.Role;
import cn.stylefeng.guns.modular.system.model.User;
import cn.stylefeng.roses.core.util.HttpContext;
import cn.stylefeng.roses.core.util.SpringContextHolder;
import com.baomidou.mybatisplus.mapper.EntityWrapper;
import com.baomidou.mybatisplus.mapper.Wrapper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.List;
/**
* 权限自定义检查
*/
@Service
@Transactional(readOnly = true)
public class PermissionCheckServiceServiceImpl implements PermissionCheckService {
@Override
public boolean check(Object[] permissions) {
ShiroUser shiroUser = ShiroKit.getUser();
if (null == shiroUser) {
return false;
}
//查询当前用户拥有的角色
UserMapper userMapper = SpringContextHolder.getBean(UserMapper.class);
User user = userMapper.selectById(shiroUser.getId());
List roleList = StringUtil.split(user.getRoleid(), ",");
if(roleList.isEmpty()){
return false;
}
RoleMapper roleMapper = SpringContextHolder.getBean(RoleMapper.class);
int count=0;
Wrapper roleWrapper = null;
for (Object obj : permissions){
if(obj==null){
continue;
}
//查询对应的角色名称是否存在
roleWrapper = new EntityWrapper<>();
roleWrapper.in("id", roleList)
.eq("tips",obj.toString().trim());
count = roleMapper.selectCount(roleWrapper);
if(count>0){
return true;
}
}
return false;
}
//旧
// public boolean check(Object[] permissions) {
// ShiroUser user = ShiroKit.getUser();
// if (null == user) {
// return false;
// }
// ArrayList
CategoryController.java 类:
package cn.stylefeng.guns.elephish.controller;
import cn.stylefeng.guns.core.common.annotion.BussinessLog;
import cn.stylefeng.guns.core.common.annotion.Permission;
import cn.stylefeng.guns.core.common.node.ZTreeNode;
import cn.stylefeng.guns.core.log.LogObjectHolder;
import cn.stylefeng.guns.elephish.bean.PageInfo;
import cn.stylefeng.guns.elephish.bean.QueryParam;
import cn.stylefeng.guns.elephish.constants.dictmaps.CategoryDict;
import cn.stylefeng.guns.elephish.form.CategoryForm;
import cn.stylefeng.guns.elephish.utils.DBUtil;
import cn.stylefeng.guns.elephish.wrapper.CategoryWrapper;
import cn.stylefeng.roses.core.base.controller.BaseController;
import com.alibaba.fastjson.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.ui.Model;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestParam;
import cn.stylefeng.guns.elephish.service.ICategoryService;
import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid;
import java.util.List;
import java.util.Map;
/**
* 分类管理控制器
*
* @author fengshuonan
* @Date 2020-04-13 11:02:46
*/
@Controller
@RequestMapping("/category")
public class CategoryController extends BaseController {
private Logger logger = LoggerFactory.getLogger(getClass());
private String PREFIX = "/elephish/product/category/";
@Autowired
private ICategoryService categoryService;
/**
* 跳转到添加分类管理
*/
@RequestMapping("/category_add")
public String categoryAdd(Integer parentId,String parentName,Integer depth,
Integer currentPage,HttpServletRequest request) {
request.setAttribute("parentId",parentId);
request.setAttribute("parentName",parentName);
request.setAttribute("depth",depth);
request.setAttribute("currentPage",currentPage);
return PREFIX + "category_add.html";
}
/**
* 跳转到修改分类管理
*/
@RequestMapping("/category_update")
public String categoryUpdate(@RequestParam("id") int id,@RequestParam("timeZone") String timeZone,
@RequestParam("currentPage") int currentPage,Model model) {
// Category map = categoryService.selectById(categoryId);
// LogObjectHolder.me().set(category);
Map map = categoryService.getCategoryDetails(id,timeZone);
map.put("currentPage",currentPage);//当前页码
model.addAttribute("item",map);
return PREFIX + "category_edit.html";
}
/**
* 跳转到分类管理首页
*/
@RequestMapping("")
public String index() {
return PREFIX + "category.html";
}
/**
* 获取分类管理列表
*/
@Permission
@RequestMapping(value = "/list")
@ResponseBody
public Object list(QueryParam queryParam,PageInfo pageInfo) {
List
缓存的开启方式示例:
该系列更多文章请前往 Guns二次开发目录