关于博客中使用的Guns版本问题请先阅读 Guns二次开发目录
到此篇博客为止,前面的一系列博客中,我们实现了商品分类管理模块的增删改查,演示了如何在Guns源码的基础上来开发我们自己的业务模块。当然了,这还远没有结束,我们还需要将Guns原有的权限控制功能应用到我们写的一系列后端接口中。Guns的权限系统对模块的权限控制分为前后端两部分,前端控制按钮(比如【添加】、【修改】和【删除】等按钮)是否显示出来,后端代码则是通过加一个注解来控制当前角色是否有权限访问这个接口,假设用户当前的角色没有权限访问某个接口,却又访问了这个接口时,在有权限控制的时候,就会控制这种越权行为。
1、给资源加上控制权限
(1)找到前端控制权限的代码
category.html 页面中,下图的红框中的代码则是对这些按钮做了权限判断的,只有当前用户拥有访问这些接口的权限,才会展示这些按钮:
(2)设置当前用户没有权限访问【启用/停用】接口
为了方便本篇博客的介绍,我们不妨先设置当前角色(我当前登录的是管理员角色) 没有访问【启用/停用】按钮的权限。设置方法如下:
修改之后,退出当前账号后再重新登录:
然后再访问分类管理页面,发现【启用/停用】按钮没有显示了:
(3)前端代码制造一个bug——不做权限控制
因为guns 自带的权限控制系统,不仅是在前端需要做控制,后端接口也需要做控制,为了方便介绍,此处我们不妨在前端制造一个bug,在前端不做控制的情况下,由后端来做限制,看看是否能够起到保护资源的的目的。
前端页面中,对【启用/停用】按钮不做权限控制:
(4)后端接口加上权限控制
因为此时我的后端接口是没有做权限控制的,所以如果此时点击【启用/停用】按钮,请求是依旧能够发送成功的。所以接下来我们要在后端接口加上权限控制,方法很简单,只需要加上一个@Permission注解,这个注解所在的包是 cn.stylefeng.guns.core.common.annotion.Permission 。
(5)测试后端权限控制是否生效
这是效果,可以发现后端权限校验生效了:
权限注解的核心代码在这个类中,内部逻辑很简单,有需要的可以自己去读源码或者debug查看流程
2、完善Guns自带的权限系统
有这么一个场景:后台管理系统用户张三拥有【超级客服】的角色,而【超级客服】拥有操作资源A和资源B的权限,某天张三把超级管理员李四给得罪了,李四很不爽,于是把张三降为【普通客服】,而普通客服只拥有操作资源B的权限。可是这时候问题来了,李四在把张三降为【普通客服】的前一秒,张三还处于账号登录状态并且还拥有【超级客服】的权限,当李四成功把张三降级后,查看登录日志,发现张三还能操作资源A。李四没有办法,总不能拉下脸让张三自己退出后再重新登录,无奈之下只能重启服务器。但是这个操作无疑得罪的就不只是张三一人了。
这其实也正是当前版本的Guns的权限系统的一个问题,更确切的说,这是Shiro权限框架的一个硬伤——无法动态更新用户权限。我们不妨来查看Guns的源码来更直观的发现问题。
(1)找到@Permission注解的实现类
看过guns相关视频的人都知道@Permission的实现原理(视频链接在文章开头的guns目录中有),PermissionAop.java文件中则是对@Permission权限注解的具体实现。
经过debug,可以发现checkAll()函数才是权限校验的核心逻辑:
实现逻辑在类PermissionCheckServiceServiceImpl.java中:
然后,一路debug可以发现 ,ShiroKit.hasPermission()方法才是检验用户是否拥有访问指定权限,走到这里的时候,可以发现,校验用户权限不是从数据库查询的,而是从Subject对象中获取的,而Subject对象中保存的权限信息,则是在登录时从数据库查询到的,而登录之后发生的权限更新信息,是无法同步更到Subject对象中的,这也是为什么Guns无法动态更新用户权限的原因。
(2)解决Guns无法及时刷新用户权限的问题
既然知道了问题产生的原因,那就开始解决问题了。我的解决方案是:在获取用户权限的时候,不从Subject对象中获取,而是每次都去数据库中查询。这时又会产生一个问题,那就是每次访问一个接口,都得去数据库查询用户的权限,这样无疑会增加服务器的压力和增大每个接口的处理时间。鉴于此,我便引入了redis来做mybatis-plus的二级缓存,以此来提高查询效率。
修改checkAll()方法的内部实现逻辑:
开启一下几个文件的二级缓存:
注:guns中开启mybatis-plus二级缓存的方式,请看这篇文章:
3、相关的源码
PermissionCheckServiceServiceImpl.java
/**
* Copyright 2018-2020 stylefeng & fengshuonan (https://gitee.com/stylefeng)
* <p>
* 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
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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<String> roleList = StringUtil.split(user.getRoleid(), ",");
if(roleList.isEmpty()){
return false;
}
RoleMapper roleMapper = SpringContextHolder.getBean(RoleMapper.class);
int count=0;
Wrapper<Role> 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<Object> objects = CollectionUtil.newArrayList(permissions);
// String join = CollectionUtil.join(objects, ",");
// if (ShiroKit.hasAnyRoles(join)) {
// return true;
// }
// return false;
// }
/**
* 查询用户权限(查询最新的权限,不受登录缓存影响)
* @return
*/
@Override
public boolean checkAll() {
HttpServletRequest request = HttpContext.getRequest();
ShiroUser shiroUser = ShiroKit.getUser();
if (null == shiroUser) {
return false;
}
String requestURI = request.getRequestURI().replaceFirst(ConfigListener.getConf().get("contextPath"), "");
String[] str = requestURI.split("/");
if (str.length > 3) {
requestURI = "/" + str[1] + "/" + str[2];
}
//查询当前用户拥有的角色
UserMapper userMapper = SpringContextHolder.getBean(UserMapper.class);
User user = userMapper.selectById(shiroUser.getId());
List<String> roleList = StringUtil.split(user.getRoleid(), ",");
if(roleList.isEmpty()){
return false;
}
//查询当前资源对应的菜单id
MenuMapper menuMapper = SpringContextHolder.getBean(MenuMapper.class);
Menu menuParam = new Menu();
menuParam.setUrl(requestURI);
menuParam.setStatus(1);
Menu menu = menuMapper.selectOne(menuParam);
if(menu==null){
return false;
}
//查询当前角色是否能访问当前资源
RelationMapper relationMapper = SpringContextHolder.getBean(RelationMapper.class);
Wrapper<Relation> relationWrapper = new EntityWrapper<>();
relationWrapper.eq("menuid",menu.getId())
.in("roleid",roleList);
Integer count = relationMapper.selectCount(relationWrapper);
if(count<1){
return false;
}
return true;
}
// @Override
// public boolean checkAll() {
// HttpServletRequest request = HttpContext.getRequest();
// ShiroUser shiroUser = ShiroKit.getUser();
// if (null == shiroUser) {
// return false;
// }
// String requestURI = request.getRequestURI().replaceFirst(ConfigListener.getConf().get("contextPath"), "");
// String[] str = requestURI.split("/");
// if (str.length > 3) {
// requestURI = "/" + str[1] + "/" + str[2];
// }
//
// if (ShiroKit.hasPermission(requestURI)) {
// return true;
// }
// return false;
// }
}
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<String,Object> 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<Map<String, Object>> list = categoryService.listCategory(queryParam,pageInfo);
//因为是自定义分页,所以返回的数据格式需要做特殊封装,主要是两个属性名的定义要固定
JSONObject jo=new JSONObject();//也可以使用 Map<String,Object>
//属性名必须是【data】,对应的值是List<Map<String, Object>>格式
jo.put("data",new CategoryWrapper(list).wrap());
jo.put("pageInfo",pageInfo);//属性名必须是 pageInfo,
return jo;
}
/**
* 停用或启用商品分类及其所有子类
* @param id
* @param version
* @param status
* @return
*/
@Permission
@RequestMapping(value = "/status")
@ResponseBody
public Object updateStatus(@RequestParam("id")int id,
@RequestParam("version")int version,
@RequestParam("status")int status) {
categoryService.updateStatus(id,version,status);
return SUCCESS_TIP;
}
/**
* 新增分类管理
*/
@Permission
@RequestMapping(value = "/add")
@ResponseBody
public Object add(@Valid CategoryForm categoryForm) {
/**
* 1、修改接收数据的实体类,因为如果直接使用DAO层的实体类来接收,
* 会导致一些不需要的数据被写进数据库
* 2、对必传数据要判断是否为空
* 3、只接收需要的数据,比如这个CategoryForm实体类,id这个字段我是不需要的,但是只是
* 添加这个接口不需要,我修改接口是需要的,此时不能在CategoryForm这个类中不定义id这个属性。
* 所以,正确的做法是,在添加接口的具体逻辑里,我不在乎你是否传了id,因为我压根不会操作这个字段
*/
categoryService.addCategory(categoryForm);
return SUCCESS_TIP;
}
/**
* 删除分类管理
*/
@Permission
@RequestMapping(value = "/delete")
@ResponseBody
public Object delete(@RequestParam("id") int id,
@RequestParam("version")int version) {
/**
* 删除商品分类的逻辑:
* (1)只能做逻辑删除,不能做物理删除,因为有可能商品管理中用到了这个分类,
* 如果做了物理删除,以后映射查询商品的时候可能会出错
* (2)删除的时候不能只删除当前分类,还要将当前分类下的所有子类做递归逻辑删除,
* 为了保证数据安全,前端要做二次确认的提示,防止用户误操作
*/
//操作流水和授权暂时不实现,后面篇幅介绍
categoryService.deleteCategoryById(id,version);
return SUCCESS_TIP;
}
/**
* 修改分类管理
*
* 逻辑:
* (1)已废弃的商品分类不能修改
* (2)允许修改的地方:分类名称,排序数字
* (3)如果修改的分类名称已经存在,修改失败
* (4)其它情况修改成功
*/
@Permission
@RequestMapping(value = "/update")
@ResponseBody
public Object update(@Valid CategoryForm categoryForm) {
//修改流水暂时不处理,后面专门使用单独的篇幅演示
categoryService.updateCategory(categoryForm);
return SUCCESS_TIP;
}
/**
* 获取菜单列表(选择父级菜单用)
*/
@RequestMapping(value = "/selectCategoryTreeList")
@ResponseBody
public List<ZTreeNode> selectMenuTreeList() {
List<ZTreeNode> roleTreeList = categoryService.categoryTreeList();
roleTreeList.add(ZTreeNode.createParent());
return roleTreeList;
}
}
缓存的开启方式示例:
该系列更多文章请前往 Guns二次开发目录